Update Operation

9 minute read

What is the Update Operation?

The Update operation modifies an existing resource when its configuration changes. It’s called during terraform apply when Terraform detects changes between the plan and current state.

When Update is called:

  • ✅ User modifies resource configuration
  • ✅ terraform plan shows changes
  • ✅ User runs terraform apply
  • ✅ Changes can be applied in-place (no replacement)

Update must:

  1. Read both plan (desired) and state (current)
  2. Determine what changed
  3. Make API call to update the resource
  4. Wait for update to complete
  5. Update state with new values

Update Method Signature

func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    // Implementation here
}

Parameters:

  • ctx: Context for cancellation and logging
  • req: Contains both plan (desired) and state (current)
  • resp: Used to set updated state

Basic Update Implementation

Step-by-Step

func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    // Step 1: Get plan (desired state)
    var plan DatabaseResourceModel
    diags := req.Plan.Get(ctx, &plan)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Step 2: Get current state
    var state DatabaseResourceModel
    diags = req.State.Get(ctx, &state)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Step 3: Log operation
    tflog.Info(ctx, "Updating database resource", map[string]any{
        "id": state.ID.ValueString(),
    })
    
    // Step 4: Build update request
    updateReq := api.UpdateDatabaseRequest{
        Name:          plan.Name.ValueString(),
        InstanceClass: plan.InstanceClass.ValueString(),
        StorageGB:     int(plan.StorageGB.ValueInt64()),
    }
    
    // Step 5: Make API call
    db, err := r.client.UpdateDatabase(ctx, state.ID.ValueString(), updateReq)
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Update Database",
            fmt.Sprintf("Could not update database: %s", err.Error()),
        )
        return
    }
    
    // Step 6: Update state with new values
    plan.Status = types.StringValue(db.Status)
    plan.Endpoint = types.StringValue(db.Endpoint)
    plan.UpdatedAt = types.StringValue(db.UpdatedAt)
    
    // Step 7: Save updated state
    diags = resp.State.Set(ctx, plan)
    resp.Diagnostics.Append(diags...)
    
    tflog.Info(ctx, "Database updated successfully")
}

Plan vs State

Update receives both plan and state:

func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var plan DatabaseResourceModel   // What user wants
    var state DatabaseResourceModel  // What currently exists
    
    req.Plan.Get(ctx, &plan)
    req.State.Get(ctx, &state)
    
    // Compare to see what changed
    if !plan.InstanceClass.Equal(state.InstanceClass) {
        tflog.Debug(ctx, "Instance class changed", map[string]any{
            "old": state.InstanceClass.ValueString(),
            "new": plan.InstanceClass.ValueString(),
        })
    }
    
    if !plan.StorageGB.Equal(state.StorageGB) {
        tflog.Debug(ctx, "Storage size changed", map[string]any{
            "old": state.StorageGB.ValueInt64(),
            "new": plan.StorageGB.ValueInt64(),
        })
    }
}

Detecting Changes

Simple Attribute Changes

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)
    
    // Build update request with only changed values
    updateReq := api.UpdateDatabaseRequest{}
    hasChanges := false
    
    // Check instance class
    if !plan.InstanceClass.Equal(state.InstanceClass) {
        updateReq.InstanceClass = plan.InstanceClass.ValueString()
        hasChanges = true
        tflog.Debug(ctx, "Instance class will be updated")
    }
    
    // Check storage
    if !plan.StorageGB.Equal(state.StorageGB) {
        updateReq.StorageGB = int(plan.StorageGB.ValueInt64())
        hasChanges = true
        tflog.Debug(ctx, "Storage size will be updated")
    }
    
    // Check tags
    if !plan.Tags.Equal(state.Tags) {
        tagsMap := make(map[string]string)
        plan.Tags.ElementsAs(ctx, &tagsMap, false)
        updateReq.Tags = tagsMap
        hasChanges = true
        tflog.Debug(ctx, "Tags will be updated")
    }
    
    if !hasChanges {
        tflog.Debug(ctx, "No changes detected")
        resp.State.Set(ctx, plan)
        return
    }
    
    // Make update call
    db, err := r.client.UpdateDatabase(ctx, state.ID.ValueString(), updateReq)
    // ...
}

Complex Attribute Changes

// Comparing list changes
if !plan.Replicas.Equal(state.Replicas) {
    var planReplicas, stateReplicas []ReplicaModel
    plan.Replicas.ElementsAs(ctx, &planReplicas, false)
    state.Replicas.ElementsAs(ctx, &stateReplicas, false)
    
    tflog.Debug(ctx, "Replicas changed", map[string]any{
        "old_count": len(stateReplicas),
        "new_count": len(planReplicas),
    })
    
    // Determine additions, deletions, modifications
    // Update accordingly
}

Partial Updates

Some APIs support partial updates:

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)
    
    id := state.ID.ValueString()
    
    // Update instance class if changed
    if !plan.InstanceClass.Equal(state.InstanceClass) {
        err := r.client.UpdateInstanceClass(ctx, id, plan.InstanceClass.ValueString())
        if err != nil {
            resp.Diagnostics.AddError(
                "Failed to Update Instance Class",
                err.Error(),
            )
            return
        }
        tflog.Info(ctx, "Instance class updated")
    }
    
    // Update storage if changed
    if !plan.StorageGB.Equal(state.StorageGB) {
        err := r.client.UpdateStorage(ctx, id, int(plan.StorageGB.ValueInt64()))
        if err != nil {
            resp.Diagnostics.AddError(
                "Failed to Update Storage",
                err.Error(),
            )
            return
        }
        tflog.Info(ctx, "Storage updated")
    }
    
    // Update tags if changed
    if !plan.Tags.Equal(state.Tags) {
        tagsMap := make(map[string]string)
        plan.Tags.ElementsAs(ctx, &tagsMap, false)
        
        err := r.client.UpdateTags(ctx, id, tagsMap)
        if err != nil {
            resp.Diagnostics.AddError(
                "Failed to Update Tags",
                err.Error(),
            )
            return
        }
        tflog.Info(ctx, "Tags updated")
    }
    
    // Fetch updated resource
    db, err := r.client.GetDatabase(ctx, id)
    if err != nil {
        resp.Diagnostics.AddError("Read After Update Failed", err.Error())
        return
    }
    
    // Update state
    plan.Status = types.StringValue(db.Status)
    plan.UpdatedAt = types.StringValue(db.UpdatedAt)
    resp.State.Set(ctx, plan)
}

Async Updates

For long-running updates, wait for completion:

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)
    
    id := state.ID.ValueString()
    
    tflog.Info(ctx, "Starting database update (async operation)")
    
    // Start update
    err := r.client.UpdateDatabase(ctx, id, api.UpdateDatabaseRequest{
        InstanceClass: plan.InstanceClass.ValueString(),
        StorageGB:     int(plan.StorageGB.ValueInt64()),
    })
    
    if err != nil {
        resp.Diagnostics.AddError("Update Failed", err.Error())
        return
    }
    
    // Wait for update to complete
    tflog.Debug(ctx, "Waiting for database update to complete")
    
    err = r.client.WaitForDatabaseReady(ctx, id, 20*time.Minute)
    if err != nil {
        resp.Diagnostics.AddError(
            "Update Timeout",
            fmt.Sprintf(
                "Database update did not complete: %s\n\n"+
                "The update may still be in progress. Check the console.",
                err.Error(),
            ),
        )
        return
    }
    
    // Fetch updated state
    db, err := r.client.GetDatabase(ctx, id)
    if err != nil {
        resp.Diagnostics.AddError("Read After Update Failed", err.Error())
        return
    }
    
    // Update state with current values
    plan.Status = types.StringValue(db.Status)
    plan.Endpoint = types.StringValue(db.Endpoint)
    plan.UpdatedAt = types.StringValue(db.UpdatedAt)
    
    resp.State.Set(ctx, plan)
    tflog.Info(ctx, "Database update completed successfully")
}

Update-in-Place vs Replace

Some changes can be applied in-place, others require replacement.

Update-in-Place

Changes that don’t require replacing the resource:

// These can typically be updated in-place:
// - Tags
// - Storage size (increase only)
// - Backup retention
// - Maintenance window
// - Instance class (with downtime)

func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "storage_gb": schema.Int64Attribute{
                Required:            true,
                MarkdownDescription: "Storage size. Can be increased in-place.",
                // No RequiresReplace
            },
            "tags": schema.MapAttribute{
                Optional:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Tags. Can be updated in-place.",
            },
        },
    }
}

Force Replacement

Changes that require destroying and recreating:

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(),  // Forces replacement
                },
                MarkdownDescription: "Database engine. Changing requires replacement.",
            },
            "name": schema.StringAttribute{
                Required: true,
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
                MarkdownDescription: "Database name. Changing requires replacement.",
            },
        },
    }
}

Effect:

# User changes engine
resource "mycloud_database" "example" {
  engine = "mysql"  # was "postgres"
}
terraform plan

  # mycloud_database.example must be replaced
-/+ resource "mycloud_database" "example" {
      ~ engine = "postgres" -> "mysql"  # forces replacement
    }

Handling Errors

Validation Errors

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)
    
    // Validate storage increase only
    if plan.StorageGB.ValueInt64() < state.StorageGB.ValueInt64() {
        resp.Diagnostics.AddError(
            "Invalid Storage Update",
            fmt.Sprintf(
                "Storage can only be increased, not decreased.\n"+
                "Current: %d GB, Requested: %d GB",
                state.StorageGB.ValueInt64(),
                plan.StorageGB.ValueInt64(),
            ),
        )
        return
    }
    
    // Continue with update...
}

API Errors

db, err := r.client.UpdateDatabase(ctx, id, updateReq)
if err != nil {
    if apiErr, ok := err.(*api.Error); ok {
        if apiErr.Code == "INSUFFICIENT_CAPACITY" {
            resp.Diagnostics.AddError(
                "Insufficient Capacity",
                fmt.Sprintf(
                    "Cannot scale to %s due to insufficient capacity in region.\n"+
                    "Try a different instance class or contact support.",
                    plan.InstanceClass.ValueString(),
                ),
            )
            return
        }
        
        if apiErr.Code == "UPDATE_IN_PROGRESS" {
            resp.Diagnostics.AddError(
                "Update Already in Progress",
                "Another update is currently in progress. Wait for it to complete before making changes.",
            )
            return
        }
    }
    
    resp.Diagnostics.AddError(
        "Update Failed",
        fmt.Sprintf("Could not update database: %s", err.Error()),
    )
    return
}

Updating Tags

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)
    
    id := state.ID.ValueString()
    
    // Check if tags changed
    if !plan.Tags.Equal(state.Tags) {
        // Get plan tags
        var planTags map[string]string
        if !plan.Tags.IsNull() {
            plan.Tags.ElementsAs(ctx, &planTags, false)
        } else {
            planTags = make(map[string]string)
        }
        
        // Get state tags
        var stateTags map[string]string
        if !state.Tags.IsNull() {
            state.Tags.ElementsAs(ctx, &stateTags, false)
        } else {
            stateTags = make(map[string]string)
        }
        
        // Determine changes
        toAdd := make(map[string]string)
        toUpdate := make(map[string]string)
        toDelete := []string{}
        
        // Find additions and updates
        for key, value := range planTags {
            if oldValue, exists := stateTags[key]; exists {
                if oldValue != value {
                    toUpdate[key] = value
                }
            } else {
                toAdd[key] = value
            }
        }
        
        // Find deletions
        for key := range stateTags {
            if _, exists := planTags[key]; !exists {
                toDelete = append(toDelete, key)
            }
        }
        
        // Apply tag changes
        if len(toAdd) > 0 || len(toUpdate) > 0 {
            err := r.client.AddTags(ctx, id, mergeMaps(toAdd, toUpdate))
            if err != nil {
                resp.Diagnostics.AddError("Failed to Add Tags", err.Error())
                return
            }
        }
        
        if len(toDelete) > 0 {
            err := r.client.RemoveTags(ctx, id, toDelete)
            if err != nil {
                resp.Diagnostics.AddError("Failed to Remove Tags", err.Error())
                return
            }
        }
        
        tflog.Info(ctx, "Tags updated", map[string]any{
            "added":   len(toAdd),
            "updated": len(toUpdate),
            "deleted": len(toDelete),
        })
    }
    
    // Fetch updated resource
    db, err := r.client.GetDatabase(ctx, id)
    if err != nil {
        resp.Diagnostics.AddError("Read After Update Failed", err.Error())
        return
    }
    
    // Update state
    plan.Status = types.StringValue(db.Status)
    plan.UpdatedAt = types.StringValue(db.UpdatedAt)
    
    // Convert tags to Terraform map
    if len(db.Tags) > 0 {
        tagsMap := make(map[string]attr.Value)
        for key, value := range db.Tags {
            tagsMap[key] = types.StringValue(value)
        }
        mapValue, _ := types.MapValue(types.StringType, tagsMap)
        plan.Tags = mapValue
    }
    
    resp.State.Set(ctx, plan)
}

Complete Example

func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    // 1. Get plan and state
    var plan DatabaseResourceModel
    var state DatabaseResourceModel
    
    diags := req.Plan.Get(ctx, &plan)
    resp.Diagnostics.Append(diags...)
    
    diags = req.State.Get(ctx, &state)
    resp.Diagnostics.Append(diags...)
    
    if resp.Diagnostics.HasError() {
        return
    }
    
    // 2. Check client
    if r.client == nil {
        resp.Diagnostics.AddError("Unconfigured Client", "...")
        return
    }
    
    id := state.ID.ValueString()
    
    // 3. Log operation
    tflog.Info(ctx, "Updating database resource", map[string]any{
        "id": id,
    })
    
    // 4. Build update request
    updateReq := api.UpdateDatabaseRequest{}
    hasChanges := false
    
    // Instance class
    if !plan.InstanceClass.Equal(state.InstanceClass) {
        updateReq.InstanceClass = plan.InstanceClass.ValueString()
        hasChanges = true
        tflog.Debug(ctx, "Instance class changed", map[string]any{
            "old": state.InstanceClass.ValueString(),
            "new": plan.InstanceClass.ValueString(),
        })
    }
    
    // Storage
    if !plan.StorageGB.Equal(state.StorageGB) {
        newStorage := plan.StorageGB.ValueInt64()
        oldStorage := state.StorageGB.ValueInt64()
        
        // Validate increase only
        if newStorage < oldStorage {
            resp.Diagnostics.AddError(
                "Invalid Storage Update",
                fmt.Sprintf("Storage can only be increased (%d -> %d)", oldStorage, newStorage),
            )
            return
        }
        
        updateReq.StorageGB = int(newStorage)
        hasChanges = true
        tflog.Debug(ctx, "Storage size changed", map[string]any{
            "old": oldStorage,
            "new": newStorage,
        })
    }
    
    if !hasChanges {
        tflog.Debug(ctx, "No changes to apply")
        resp.State.Set(ctx, plan)
        return
    }
    
    // 5. Make API call
    err := r.client.UpdateDatabase(ctx, id, updateReq)
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Update Database",
            fmt.Sprintf("Could not update database %s: %s", id, err.Error()),
        )
        return
    }
    
    // 6. Wait for update to complete
    tflog.Debug(ctx, "Waiting for update to complete")
    err = r.client.WaitForDatabaseReady(ctx, id, 20*time.Minute)
    if err != nil {
        resp.Diagnostics.AddError(
            "Update Timeout",
            fmt.Sprintf("Database update did not complete: %s", err.Error()),
        )
        return
    }
    
    // 7. Fetch updated resource
    db, err := r.client.GetDatabase(ctx, id)
    if err != nil {
        resp.Diagnostics.AddError(
            "Read After Update Failed",
            err.Error(),
        )
        return
    }
    
    // 8. Update state with new values
    plan.Status = types.StringValue(db.Status)
    plan.Endpoint = types.StringValue(db.Endpoint)
    plan.UpdatedAt = types.StringValue(db.UpdatedAt)
    
    // 9. Save state
    diags = resp.State.Set(ctx, plan)
    resp.Diagnostics.Append(diags...)
    
    tflog.Info(ctx, "Database updated successfully", map[string]any{
        "id": id,
    })
}

Best Practices

1. Validate Before Updating

// ✅ Good: Validate first
if newSize < oldSize {
    resp.Diagnostics.AddError("Invalid Update", "...")
    return
}

// Then update
err := r.client.UpdateDatabase(...)

2. Log What Changed

// ✅ Good: Log changes
tflog.Debug(ctx, "Instance class changed", map[string]any{
    "old": state.InstanceClass.ValueString(),
    "new": plan.InstanceClass.ValueString(),
})

3. Wait for Async Operations

// ✅ Good: Wait for completion
r.client.UpdateDatabase(ctx, id, req)
r.client.WaitForDatabaseReady(ctx, id, timeout)

// ❌ Bad: Don't wait
r.client.UpdateDatabase(ctx, id, req)
// Resource might not be ready!

4. Use Plan Modifiers for Immutable Fields

// ✅ Good: Use RequiresReplace
"engine": schema.StringAttribute{
    Required: true,
    PlanModifiers: []planmodifier.String{
        stringplanmodifier.RequiresReplace(),
    },
}

// ❌ Bad: Check in Update (error prone)
if !plan.Engine.Equal(state.Engine) {
    resp.Diagnostics.AddError("Cannot change engine", "...")
}

5. Fetch State After Update

// ✅ Good: Fetch to get computed values
r.client.UpdateDatabase(...)
db, _ := r.client.GetDatabase(...)
plan.Status = types.StringValue(db.Status)

// ❌ Bad: Assume values
plan.Status = types.StringValue("modifying")

Key Takeaways

  • ✅ Update modifies existing resources in-place
  • ✅ Receives both plan (desired) and state (current)
  • ✅ Compare plan and state to detect changes
  • ✅ Only update changed attributes when possible
  • ✅ Use RequiresReplace for immutable fields
  • ✅ Validate updates before making API calls
  • ✅ Wait for async operations to complete
  • ✅ Fetch updated resource after modification
  • ✅ Log what changed for debugging
  • ✅ Handle partial update errors gracefully

What’s Next?

Now let’s implement the Delete operation to destroy resources!

Next: Delete Operation →


Previous: ← Read Operation

Last updated: June 30, 2026

Updated: