Resource Practice Questions

16 minute read

Resource Basics

Question 1: Resource Interface Requirements

What are the three essential interfaces that a resource must implement?

View Answer The three essential interfaces are: 1. **`resource.Resource`** - Required interface with: - `Metadata()` - Sets resource type name - `Schema()` - Defines resource schema - `Create()` - Creates resource - `Read()` - Reads resource state - `Update()` - Updates resource - `Delete()` - Deletes resource 2. **`resource.ResourceWithConfigure`** - For receiving shared client: - `Configure()` - Receives provider-configured client 3. **`resource.ResourceWithImportState`** - For import support: - `ImportState()` - Enables resource import **Example:** ```go var _ resource.Resource = &MyResource{} var _ resource.ResourceWithConfigure = &MyResource{} var _ resource.ResourceWithImportState = &MyResource{} ```

Question 2: Model Tags

Why do we use tfsdk struct tags in the resource model?

View Answer The `tfsdk` struct tags map Go struct fields to Terraform schema attribute names: ```go type DatabaseResourceModel struct { ID types.String `tfsdk:"id"` // Maps to "id" attribute Name types.String `tfsdk:"name"` // Maps to "name" attribute } ``` **Benefits:** - Maps Go fields to Terraform HCL attributes - Enables automatic marshaling/unmarshaling - Allows Go naming conventions while keeping HCL idiomatic - Required by `req.Plan.Get()` and `resp.State.Set()` Without these tags, the framework cannot map between Go structs and Terraform state/plan.

Question 3: Plan Modifiers

What’s the difference between stringplanmodifier.RequiresReplace() and stringplanmodifier.UseStateForUnknown()?

View Answer **`RequiresReplace()`:** - Forces resource recreation when attribute changes - Shows `+/-` (destroy and recreate) in plan - Use for immutable attributes ```go "engine": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, } ``` **`UseStateForUnknown()`:** - Uses current state value when new value is unknown - Prevents unnecessary updates - Use for computed attributes that don't change ```go "id": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, } ``` **Example Plan:** ``` # With RequiresReplace -/+ engine = "postgres" -> "mysql" # Forces replacement # With UseStateForUnknown id = "db-12345" # Stays same, not updated ```

Create Operation

Question 4: Setting Computed Attributes

In the Create operation, when should you set computed attributes?

View Answer Set computed attributes **after** the resource is created and you have real values from the API: ```go func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan DatabaseResourceModel req.Plan.Get(ctx, &plan) // 1. Create resource via API db, err := r.client.CreateDatabase(ctx, ...) // 2. Set computed attributes from API response plan.ID = types.StringValue(db.ID) plan.ARN = types.StringValue(db.ARN) plan.Endpoint = types.StringValue(db.Endpoint) plan.Status = types.StringValue(db.Status) plan.CreatedAt = types.StringValue(db.CreatedAt) // 3. Save to state resp.State.Set(ctx, plan) } ``` **Don't:** - Set computed values before API call - Leave computed values as null/unknown - Use placeholder values

Question 5: Handling Async Creation

How do you handle resources that take time to become ready after creation?

View Answer Use a wait/poll pattern with context: ```go func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // 1. Get timeout createTimeout, _ := plan.Timeouts.Create(ctx, 30*time.Minute) createCtx, cancel := context.WithTimeout(ctx, createTimeout) defer cancel() // 2. Create resource db, err := r.client.CreateDatabase(createCtx, ...) // 3. Wait for ready state err = r.waitForDatabaseReady(createCtx, db.ID) if err != nil { if errors.Is(err, context.DeadlineExceeded) { resp.Diagnostics.AddError("Create Timeout", ...) return } resp.Diagnostics.AddError("Wait Failed", err.Error()) return } // 4. Fetch final state db, err = r.client.GetDatabase(createCtx, db.ID) // 5. Set state resp.State.Set(ctx, plan) } func (r *DatabaseResource) waitForDatabaseReady(ctx context.Context, id string) error { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: db, err := r.client.GetDatabase(ctx, id) if err != nil { return err } if db.Status == "available" { return nil } if db.Status == "failed" { return fmt.Errorf("database creation failed") } } } } ```

Question 6: Default Values

How do you set default values for optional attributes in Create?

View Answer Check if the attribute is null and set a default: ```go func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan DatabaseResourceModel req.Plan.Get(ctx, &plan) // Set default port if not specified port := int(plan.Port.ValueInt64()) if plan.Port.IsNull() || port == 0 { if plan.Engine.ValueString() == "postgres" { port = 5432 } else if plan.Engine.ValueString() == "mysql" { port = 3306 } } // Use port in API call db, err := r.client.CreateDatabase(ctx, api.CreateDatabaseRequest{ Port: port, // ... }) // Set actual port in state plan.Port = types.Int64Value(int64(db.Port)) resp.State.Set(ctx, plan) } ``` **Alternative:** Use `Default` in schema (requires default plan modifier).

Read Operation

Question 7: Handling Deleted Resources

What should you do in Read when a resource no longer exists?

View Answer Remove the resource from state using `resp.State.RemoveResource()`: ```go func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state DatabaseResourceModel req.State.Get(ctx, &state) db, err := r.client.GetDatabase(ctx, state.ID.ValueString()) if err != nil { // Check if it's a 404 error if api.IsNotFoundError(err) { tflog.Warn(ctx, "Database not found, removing from state") resp.State.RemoveResource(ctx) return } // Other errors resp.Diagnostics.AddError("Read Failed", err.Error()) return } // Update state... resp.State.Set(ctx, state) } ``` **Why?** This allows Terraform to detect drift and recreate the resource on next apply.

Question 8: Drift Detection

How does Read enable drift detection?

View Answer Read updates state with current API values. Terraform compares this to the configuration: ```go func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state DatabaseResourceModel req.State.Get(ctx, &state) // Fetch current state from API db, err := r.client.GetDatabase(ctx, state.ID.ValueString()) // Update ALL attributes from API state.InstanceClass = types.StringValue(db.InstanceClass) state.StorageGB = types.Int64Value(int64(db.StorageGB)) state.MultiAZ = types.BoolValue(db.MultiAZ) // Save actual current state resp.State.Set(ctx, state) } ``` **Drift Scenarios:** 1. User changes resource in console: Read updates state → Terraform detects drift 2. Resource deleted externally: Read calls `RemoveResource()` → Terraform recreates 3. Tags modified outside Terraform: Read updates tags → Terraform restores desired tags **Terraform plan shows drift:** ``` Note: Objects have changed outside of Terraform ~ resource "mycloud_database" "example" { ~ instance_class = "db.t3.medium" -> "db.t3.large" # Drift detected # ... } ```

Question 9: Read Performance

Should Read wait for async operations to complete?

View Answer **No.** Read should be fast and return current state immediately: ```go // ✅ Good: Quick read func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { db, err := r.client.GetDatabase(ctx, state.ID.ValueString()) if err != nil { // Handle error } // Set state as-is, even if status is "modifying" state.Status = types.StringValue(db.Status) resp.State.Set(ctx, state) } // ❌ Bad: Waiting in Read func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { db, err := r.client.GetDatabase(ctx, state.ID.ValueString()) // Don't wait in Read! if db.Status != "available" { r.waitForDatabaseReady(ctx, db.ID) // ❌ Bad } } ``` **Why?** - Read is called frequently (refresh, plan, apply) - Waiting slows down all operations - Status should reflect reality, not desired state

Update Operation

Question 10: Detecting Changes

How do you detect which attributes changed in Update?

View Answer Compare plan and state: ```go func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan, state DatabaseResourceModel req.Plan.Get(ctx, &plan) req.State.Get(ctx, &state) // Detect changes instanceClassChanged := !plan.InstanceClass.Equal(state.InstanceClass) storageChanged := !plan.StorageGB.Equal(state.StorageGB) tagsChanged := !plan.Tags.Equal(state.Tags) // Update only what changed if instanceClassChanged || storageChanged { err := r.client.ModifyDatabase(ctx, state.ID.ValueString(), api.ModifyRequest{ InstanceClass: plan.InstanceClass.ValueString(), StorageGB: int(plan.StorageGB.ValueInt64()), }) } if tagsChanged { var planTags, stateTags map[string]string plan.Tags.ElementsAs(ctx, &planTags, false) state.Tags.ElementsAs(ctx, &stateTags, false) // Calculate tag diff toAdd, toRemove := diffTags(planTags, stateTags) if len(toAdd) > 0 { r.client.TagResource(ctx, state.ID.ValueString(), toAdd) } if len(toRemove) > 0 { r.client.UntagResource(ctx, state.ID.ValueString(), toRemove) } } resp.State.Set(ctx, plan) } ```

Question 11: Partial Updates

When is it important to handle partial updates?

View Answer Handle partial updates when: 1. **Different attributes use different API calls** 2. **Some changes require separate operations** 3. **One change might fail while others succeed** ```go func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan, state DatabaseResourceModel req.Plan.Get(ctx, &plan) req.State.Get(ctx, &state) // Update instance class (requires modification) if !plan.InstanceClass.Equal(state.InstanceClass) { err := r.client.ModifyDatabase(ctx, state.ID.ValueString(), ...) if err != nil { resp.Diagnostics.AddError("Modify Failed", err.Error()) return // Stop here } } // Update tags (separate API call) if !plan.Tags.Equal(state.Tags) { err := r.client.UpdateTags(ctx, state.ID.ValueString(), ...) if err != nil { // Tag update failed, but database was modified // State reflects partial update resp.Diagnostics.AddWarning( "Tag Update Failed", "Database was updated but tags failed to update", ) } } // Set state with actual current values resp.State.Set(ctx, plan) } ``` **Result:** If instance class update succeeds but tags fail, state reflects the successful database modification.

Question 12: RequiresReplace

How do you signal that an attribute change requires resource replacement?

View Answer Use `RequiresReplace` plan modifier in schema: ```go func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "engine": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, "name": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, "instance_class": schema.StringAttribute{ Required: true, // No RequiresReplace - can be updated in-place }, }, } } ``` **Result:** ```hcl # Changing engine requires replacement -/+ resource "mycloud_database" "example" { ~ engine = "postgres" -> "mysql" # (forces replacement) } # Changing instance_class updates in-place ~ resource "mycloud_database" "example" { ~ instance_class = "db.t3.small" -> "db.t3.medium" } ``` **When to use:** - Immutable attributes (ID, engine, region) - Changes that require new resource - Attributes the API doesn't support updating

Delete Operation

Question 13: Already-Deleted Resources

How should Delete handle a resource that’s already been deleted?

View Answer Treat it as success: ```go func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state DatabaseResourceModel req.State.Get(ctx, &state) err := r.client.DeleteDatabase(ctx, state.ID.ValueString()) // Check if already deleted if err != nil && api.IsNotFoundError(err) { tflog.Info(ctx, "Database already deleted") return // Success - no error } if err != nil { resp.Diagnostics.AddError("Delete Failed", err.Error()) return } // Wait for deletion to complete err = r.waitForDatabaseDeleted(ctx, state.ID.ValueString()) if err != nil && !api.IsNotFoundError(err) { resp.Diagnostics.AddError("Wait Failed", err.Error()) } } ``` **Why?** The desired state is "resource doesn't exist" - already met if resource is not found.

Question 14: Final Snapshots

How do you create a final snapshot before deletion?

View Answer Create snapshot before calling delete: ```go type DatabaseResourceModel struct { // ... FinalSnapshotName types.String `tfsdk:"final_snapshot_name"` SkipFinalSnapshot types.Bool `tfsdk:"skip_final_snapshot"` } func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state DatabaseResourceModel req.State.Get(ctx, &state) id := state.ID.ValueString() // Create final snapshot if requested if !state.SkipFinalSnapshot.ValueBool() { snapshotName := state.FinalSnapshotName.ValueString() if snapshotName == "" { // Auto-generate snapshot name snapshotName = fmt.Sprintf("%s-final-%s", state.Name.ValueString(), time.Now().Format("20060102-150405")) } tflog.Info(ctx, "Creating final snapshot", map[string]any{ "snapshot": snapshotName, }) err := r.client.CreateSnapshot(ctx, id, snapshotName) if err != nil { resp.Diagnostics.AddError( "Final Snapshot Failed", fmt.Sprintf("Cannot delete database: %s", err.Error()), ) return } } // Delete database err := r.client.DeleteDatabase(ctx, id) if err != nil && !api.IsNotFoundError(err) { resp.Diagnostics.AddError("Delete Failed", err.Error()) return } } ``` **Configuration:** ```hcl resource "mycloud_database" "example" { name = "production-db" final_snapshot_name = "prod-db-backup" skip_final_snapshot = false # Create snapshot on destroy } ```

Question 15: Deletion Protection

How do you implement deletion protection?

View Answer Check protection flag before deletion: ```go type DatabaseResourceModel struct { // ... DeletionProtection types.Bool `tfsdk:"deletion_protection"` } func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state DatabaseResourceModel req.State.Get(ctx, &state) // Check deletion protection if !state.DeletionProtection.IsNull() && state.DeletionProtection.ValueBool() { resp.Diagnostics.AddError( "Deletion Protection Enabled", fmt.Sprintf( "Cannot delete database %s because deletion protection is enabled.\n\n"+ "To delete this database:\n"+ "1. Set deletion_protection = false\n"+ "2. Run 'terraform apply'\n"+ "3. Run 'terraform destroy'", state.ID.ValueString(), ), ) return } // Proceed with deletion err := r.client.DeleteDatabase(ctx, state.ID.ValueString()) // ... } ``` **Usage:** ```hcl resource "mycloud_database" "production" { name = "production-db" deletion_protection = true # Prevents accidental deletion } ``` **To delete:** ```hcl resource "mycloud_database" "production" { name = "production-db" deletion_protection = false # Allow deletion } ```

Import

Question 16: Import ID Formats

What are common patterns for import ID formats?

View Answer **1. Simple ID (most common):** ```bash terraform import mycloud_database.example db-abc123 ``` ```go func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } ``` **2. Composite ID:** ```bash terraform import mycloud_database.example us-east-1/db-abc123 ``` ```go func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { parts := strings.Split(req.ID, "/") if len(parts) != 2 { resp.Diagnostics.AddError( "Invalid Import ID", "Format: /", ) return } resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), parts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), parts[1])...) } ``` **3. Multiple Format Support:** ```bash # By ID terraform import mycloud_database.example db-abc123 # By name terraform import mycloud_database.example name:production-db # By ARN terraform import mycloud_database.example arn:aws:rds:us-east-1:123456789012:db:mydb ``` ```go func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { id := req.ID // Try different formats if strings.HasPrefix(id, "arn:") { // Parse ARN id = extractIDFromARN(id) } else if strings.HasPrefix(id, "name:") { // Look up by name name := strings.TrimPrefix(id, "name:") db, err := r.client.GetDatabaseByName(ctx, name) if err != nil { resp.Diagnostics.AddError("Lookup Failed", err.Error()) return } id = db.ID } resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) } ``` </details> ### Question 17: Import Validation How do you validate the import ID format?
View Answer Use regex or format checks: ```go func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { id := req.ID // Validate format validIDPattern := regexp.MustCompile(`^db-[a-z0-9]{8,}$`) if !validIDPattern.MatchString(id) { resp.Diagnostics.AddError( "Invalid Import ID", fmt.Sprintf( "Database ID must match pattern 'db-'.\n\n"+ "Examples:\n"+ " terraform import mycloud_database.example db-abc123xyz\n\n"+ "Got: %s", id, ), ) return } // Set ID resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) tflog.Info(ctx, "Database import completed", map[string]any{ "id": id, }) } ``` **Benefits:** - Catches typos early - Provides helpful error messages - Prevents API errors from invalid IDs </details> ### Question 18: Required Attributes in Import How do you handle required attributes that aren't known during import?
View Answer **Option 1: Read will populate them (preferred):** ```go func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // Just set ID - Read will fetch all attributes resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) // Read is called automatically after ImportState // Read will populate all required attributes from API } ``` **Option 2: Set default values:** ```go func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) // Set placeholder for required attributes that aren't known yet resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), "imported")...) // Read will update with real values } ``` **Option 3: Make attribute optional+computed:** ```go "name": schema.StringAttribute{ Optional: true, // Allow null during import Computed: true, // Will be populated by Read }, ```
## Timeouts ### Question 19: Custom Timeout Defaults How do you set different timeout defaults based on resource configuration?
View Answer Calculate default based on attributes: ```go func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan DatabaseResourceModel req.Plan.Get(ctx, &plan) // Calculate appropriate timeout based on configuration defaultTimeout := 30 * time.Minute // Large databases take longer if plan.StorageGB.ValueInt64() > 1000 { defaultTimeout = 60 * time.Minute } // Multi-AZ deployments take longer if !plan.MultiAZ.IsNull() && plan.MultiAZ.ValueBool() { defaultTimeout = 45 * time.Minute } // Replicas add time numReplicas := int(plan.NumReplicas.ValueInt64()) if numReplicas > 0 { defaultTimeout += time.Duration(numReplicas) * 10 * time.Minute } // Get timeout with calculated default createTimeout, _ := plan.Timeouts.Create(ctx, defaultTimeout) tflog.Debug(ctx, "Using create timeout", map[string]any{ "timeout": createTimeout.String(), "storage_gb": plan.StorageGB.ValueInt64(), "multi_az": plan.MultiAZ.ValueBool(), "num_replicas": numReplicas, }) // Use timeout... } ```
### Question 20: Timeout Error Messages What information should you include in timeout error messages?
View Answer Provide actionable guidance: ```go if errors.Is(err, context.DeadlineExceeded) { resp.Diagnostics.AddError( "Create Operation Timeout", fmt.Sprintf( "Database creation did not complete within %s.\n\n"+ "The database may still be creating. Check the console:\n"+ " Resource ID: %s\n"+ " Current Status: %s\n\n"+ "To increase the timeout, add to your configuration:\n"+ " timeouts {\n"+ " create = \"60m\" # or longer\n"+ " }\n\n"+ "Factors affecting creation time:\n"+ " • Storage size: %d GB\n"+ " • Multi-AZ: %t\n"+ " • Number of replicas: %d\n"+ " • Region capacity and load", createTimeout, state.ID.ValueString(), state.Status.ValueString(), plan.StorageGB.ValueInt64(), plan.MultiAZ.ValueBool(), plan.NumReplicas.ValueInt64(), ), ) return } ``` **Key elements:** - How long it waited - Current resource status - How to increase timeout - Why it might be taking long - Where to check status
## Real-World Scenarios ### Scenario 1: Tag Management Design a solution for managing tags that handles: - Adding new tags - Removing old tags - Updating existing tag values - Preserving system-managed tags
View Solution ```go func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan, state DatabaseResourceModel req.Plan.Get(ctx, &plan) req.State.Get(ctx, &state) // Check if tags changed if !plan.Tags.Equal(state.Tags) { var planTags, stateTags map[string]string plan.Tags.ElementsAs(ctx, &planTags, false) state.Tags.ElementsAs(ctx, &stateTags, false) // Calculate changes toAdd := make(map[string]string) toUpdate := make(map[string]string) toRemove := []string{} // Find additions and updates for k, v := range planTags { if oldValue, exists := stateTags[k]; exists { if oldValue != v { toUpdate[k] = v // Value changed } } else { toAdd[k] = v // New tag } } // Find removals for k := range stateTags { // Don't remove system tags if strings.HasPrefix(k, "aws:") { continue } if _, exists := planTags[k]; !exists { toRemove = append(toRemove, k) } } // Apply changes id := state.ID.ValueString() if len(toAdd) > 0 || len(toUpdate) > 0 { allUpdates := make(map[string]string) for k, v := range toAdd { allUpdates[k] = v } for k, v := range toUpdate { allUpdates[k] = v } err := r.client.TagResource(ctx, id, allUpdates) if err != nil { resp.Diagnostics.AddError("Tag Update Failed", err.Error()) return } } if len(toRemove) > 0 { err := r.client.UntagResource(ctx, id, toRemove) if err != nil { resp.Diagnostics.AddError("Tag Removal Failed", err.Error()) return } } } // Fetch updated resource db, err := r.client.GetDatabase(ctx, state.ID.ValueString()) // Update tags in state (including system tags) if len(db.Tags) > 0 { tagsMap := make(map[string]attr.Value) for k, v := range db.Tags { // Only include user-managed tags in state if !strings.HasPrefix(k, "aws:") { tagsMap[k] = types.StringValue(v) } } mapValue, _ := types.MapValue(types.StringType, tagsMap) plan.Tags = mapValue } resp.State.Set(ctx, plan) } ```
### Scenario 2: Connection String Design a resource that generates a connection string from multiple computed attributes.
View Solution ```go type DatabaseResourceModel struct { ID types.String `tfsdk:"id"` Engine types.String `tfsdk:"engine"` Endpoint types.String `tfsdk:"endpoint"` Port types.Int64 `tfsdk:"port"` DatabaseName types.String `tfsdk:"database_name"` Username types.String `tfsdk:"username"` ConnectionString types.String `tfsdk:"connection_string"` } func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "connection_string": schema.StringAttribute{ Computed: true, Sensitive: true, // Contains credentials MarkdownDescription: "Database connection string", }, // ... other attributes }, } } func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan DatabaseResourceModel req.Plan.Get(ctx, &plan) // Create database db, err := r.client.CreateDatabase(ctx, ...) // Set computed attributes plan.ID = types.StringValue(db.ID) plan.Endpoint = types.StringValue(db.Endpoint) plan.Port = types.Int64Value(int64(db.Port)) // Generate connection string based on engine connStr := generateConnectionString( plan.Engine.ValueString(), db.Endpoint, db.Port, plan.DatabaseName.ValueString(), plan.Username.ValueString(), ) plan.ConnectionString = types.StringValue(connStr) resp.State.Set(ctx, plan) } func generateConnectionString(engine, endpoint string, port int, dbName, username string) string { switch engine { case "postgres": return fmt.Sprintf( "postgresql://%s:${password}@%s:%d/%s", username, endpoint, port, dbName, ) case "mysql": return fmt.Sprintf( "mysql://%s:${password}@%s:%d/%s", username, endpoint, port, dbName, ) default: return "" } } ``` **Usage:** ```hcl resource "mycloud_database" "app" { engine = "postgres" database_name = "appdb" username = "appuser" # ... } output "connection_string" { value = mycloud_database.app.connection_string sensitive = true } ```
## Key Takeaways - ✅ Resource requires Resource, ResourceWithConfigure, and ResourceWithImportState interfaces - ✅ Create sets computed attributes from API responses - ✅ Read removes resource from state if not found (drift detection) - ✅ Update compares plan and state to detect changes - ✅ Delete treats already-deleted resources as success - ✅ Import validates ID format and delegates to Read for population - ✅ Timeouts provide user control over long operations - ✅ Context enables cancellation and timeout handling - ✅ Plan modifiers control when resources are replaced - ✅ Proper error messages guide users to solutions ## Next Steps Congratulations on completing Module 6! Continue to Data Sources: [Next: Module 7 - Data Sources →](/hashicorp/terraform-provider-development/data-sources/){: .btn .btn--primary} --- **Previous:** [← Resource Implementation Lab](/hashicorp/terraform-provider-development/resource-implementation-lab/) *Last updated: June 30, 2026*

Updated: