Resource Practice Questions
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 valuesQuestion 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 stateUpdate 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 updatingDelete 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?