Update Operation
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 planshows changes - ✅ User runs
terraform apply - ✅ Changes can be applied in-place (no replacement)
Update must:
- Read both plan (desired) and state (current)
- Determine what changed
- Make API call to update the resource
- Wait for update to complete
- 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 loggingreq: 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!
Previous: ← Read Operation
Last updated: June 30, 2026