Delete Operation

8 minute read

What is the Delete Operation?

The Delete operation destroys a resource and removes it from your infrastructure. It’s called when a user runs terraform destroy or removes a resource from their configuration.

When Delete is called:

  • ✅ User runs terraform destroy
  • ✅ Resource removed from configuration
  • ✅ User runs terraform apply after removal
  • ✅ Resource needs to be replaced

Delete must:

  1. Read current state to get resource ID
  2. Make API call to delete the resource
  3. Wait for deletion to complete
  4. Handle already-deleted resources
  5. Remove resource from state (automatic)

Delete Method Signature

func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    // Implementation here
}

Parameters:

  • ctx: Context for cancellation and logging
  • req: Contains current state
  • resp: Used to add diagnostics (state removal is automatic)

Basic Delete Implementation

Step-by-Step

func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    // Step 1: Get current state
    var state DatabaseResourceModel
    diags := req.State.Get(ctx, &state)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Step 2: Get resource ID
    id := state.ID.ValueString()
    
    // Step 3: Log the operation
    tflog.Info(ctx, "Deleting database resource", map[string]any{
        "id": id,
    })
    
    // Step 4: Delete the resource
    err := r.client.DeleteDatabase(ctx, id)
    
    // Step 5: Handle already-deleted resources
    if err != nil {
        if api.IsNotFoundError(err) {
            tflog.Warn(ctx, "Database already deleted", map[string]any{
                "id": id,
            })
            // Not an error - resource is gone, which is what we want
            return
        }
        
        resp.Diagnostics.AddError(
            "Failed to Delete Database",
            fmt.Sprintf("Could not delete database %s: %s", id, err.Error()),
        )
        return
    }
    
    tflog.Info(ctx, "Database deleted successfully", map[string]any{
        "id": id,
    })
    
    // Note: State is automatically removed, no need to call resp.State.RemoveResource()
}

Handling Already-Deleted Resources

Resources might be deleted outside of Terraform:

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()
    
    err := r.client.DeleteDatabase(ctx, id)
    if err != nil {
        // Check if already deleted
        if api.IsNotFoundError(err) {
            // This is OK - resource is already gone
            tflog.Warn(ctx, "Database not found during delete, treating as success", map[string]any{
                "id": id,
            })
            return  // Success
        }
        
        // Other errors are problems
        resp.Diagnostics.AddError(
            "Failed to Delete Database",
            fmt.Sprintf("Could not delete database: %s", err.Error()),
        )
        return
    }
    
    tflog.Info(ctx, "Database deleted")
}

Async Deletion

For resources with async deletion, wait for completion:

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()
    
    tflog.Info(ctx, "Starting database deletion (async operation)")
    
    // Start deletion
    err := r.client.DeleteDatabase(ctx, id)
    if err != nil {
        if api.IsNotFoundError(err) {
            return  // Already deleted
        }
        resp.Diagnostics.AddError("Delete Failed", err.Error())
        return
    }
    
    // Wait for deletion to complete
    tflog.Debug(ctx, "Waiting for database deletion to complete")
    
    err = r.client.WaitForDatabaseDeleted(ctx, id, 15*time.Minute)
    if err != nil {
        // Check if it's just not found (already deleted)
        if api.IsNotFoundError(err) {
            tflog.Debug(ctx, "Database not found during wait, deletion complete")
            return
        }
        
        resp.Diagnostics.AddError(
            "Deletion Timeout",
            fmt.Sprintf(
                "Database deletion did not complete: %s\n\n"+
                "The deletion may still be in progress. Check the console.\n"+
                "You may need to manually delete this resource.",
                err.Error(),
            ),
        )
        return
    }
    
    tflog.Info(ctx, "Database deletion completed successfully")
}

Forced Deletion

Some resources support force deletion:

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()
    
    // Try normal deletion first
    err := r.client.DeleteDatabase(ctx, id)
    
    if err != nil {
        if apiErr, ok := err.(*api.Error); ok {
            if apiErr.Code == "PROTECTED" {
                tflog.Warn(ctx, "Database is protected, attempting force delete")
                
                // Try force deletion
                err = r.client.DeleteDatabase(ctx, id, api.DeleteOptions{
                    Force: true,
                })
                
                if err != nil {
                    resp.Diagnostics.AddError(
                        "Force Delete Failed",
                        fmt.Sprintf(
                            "Could not force delete protected database.\n"+
                            "Error: %s\n\n"+
                            "You may need to manually disable protection first.",
                            err.Error(),
                        ),
                    )
                    return
                }
            }
        }
    }
    
    tflog.Info(ctx, "Database deleted")
}

Cleanup Operations

Perform cleanup before deleting:

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()
    
    tflog.Info(ctx, "Performing cleanup before deletion")
    
    // 1. Delete backups if configured
    if !state.DeleteBackups.IsNull() && state.DeleteBackups.ValueBool() {
        tflog.Debug(ctx, "Deleting backups")
        err := r.client.DeleteBackups(ctx, id)
        if err != nil {
            tflog.Warn(ctx, "Failed to delete backups", map[string]any{
                "error": err.Error(),
            })
            // Continue with deletion even if backup deletion fails
        }
    }
    
    // 2. Delete replicas
    tflog.Debug(ctx, "Checking for replicas")
    replicas, err := r.client.ListReplicas(ctx, id)
    if err == nil && len(replicas) > 0 {
        tflog.Debug(ctx, "Deleting replicas", map[string]any{
            "count": len(replicas),
        })
        
        for _, replica := range replicas {
            err := r.client.DeleteReplica(ctx, replica.ID)
            if err != nil && !api.IsNotFoundError(err) {
                tflog.Warn(ctx, "Failed to delete replica", map[string]any{
                    "replica_id": replica.ID,
                    "error":      err.Error(),
                })
            }
        }
        
        // Wait for replicas to be deleted
        time.Sleep(5 * time.Second)
    }
    
    // 3. Delete main database
    tflog.Info(ctx, "Deleting main database")
    err = r.client.DeleteDatabase(ctx, id)
    if err != nil && !api.IsNotFoundError(err) {
        resp.Diagnostics.AddError("Delete Failed", err.Error())
        return
    }
    
    // 4. Wait for deletion
    r.client.WaitForDatabaseDeleted(ctx, id, 15*time.Minute)
    
    tflog.Info(ctx, "Database and cleanup completed")
}

Final Snapshots

Create a final snapshot before deletion:

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 configured
    if !state.FinalSnapshotName.IsNull() {
        snapshotName := state.FinalSnapshotName.ValueString()
        
        tflog.Info(ctx, "Creating final snapshot", map[string]any{
            "snapshot_name": snapshotName,
        })
        
        snapshot, err := r.client.CreateSnapshot(ctx, api.CreateSnapshotRequest{
            DatabaseID: id,
            Name:       snapshotName,
        })
        
        if err != nil {
            resp.Diagnostics.AddError(
                "Failed to Create Final Snapshot",
                fmt.Sprintf(
                    "Could not create final snapshot before deletion: %s\n\n"+
                    "Deletion will not proceed. Fix the snapshot issue or remove "+
                    "final_snapshot_name to delete without a snapshot.",
                    err.Error(),
                ),
            )
            return
        }
        
        tflog.Info(ctx, "Final snapshot created", map[string]any{
            "snapshot_id": snapshot.ID,
        })
        
        // Wait for snapshot to complete
        err = r.client.WaitForSnapshotReady(ctx, snapshot.ID, 10*time.Minute)
        if err != nil {
            resp.Diagnostics.AddWarning(
                "Snapshot May Not Be Complete",
                fmt.Sprintf("Snapshot creation may not be complete: %s", err.Error()),
            )
            // Continue with deletion anyway
        }
    }
    
    // Delete database
    err := r.client.DeleteDatabase(ctx, id)
    if err != nil && !api.IsNotFoundError(err) {
        resp.Diagnostics.AddError("Delete Failed", err.Error())
        return
    }
    
    tflog.Info(ctx, "Database deleted successfully")
}

Error Handling

Protected Resources

err := r.client.DeleteDatabase(ctx, id)
if err != nil {
    if apiErr, ok := err.(*api.Error); ok {
        if apiErr.Code == "DELETE_PROTECTION_ENABLED" {
            resp.Diagnostics.AddError(
                "Database is Protected",
                fmt.Sprintf(
                    "Cannot delete database %s because deletion protection is enabled.\n\n"+
                    "To delete this database:\n"+
                    "1. Disable deletion protection in your configuration\n"+
                    "2. Run terraform apply to update the resource\n"+
                    "3. Run terraform destroy to delete\n\n"+
                    "Or manually disable protection in the console.",
                    id,
                ),
            )
            return
        }
        
        if apiErr.Code == "HAS_DEPENDENTS" {
            resp.Diagnostics.AddError(
                "Database Has Dependents",
                fmt.Sprintf(
                    "Cannot delete database because it has dependent resources:\n%s\n\n"+
                    "Delete dependent resources first.",
                    apiErr.Details,
                ),
            )
            return
        }
    }
    
    resp.Diagnostics.AddError("Delete Failed", err.Error())
}

Network Errors

err := r.client.DeleteDatabase(ctx, id)
if err != nil {
    if api.IsNetworkError(err) {
        resp.Diagnostics.AddError(
            "Network Error During Deletion",
            fmt.Sprintf(
                "Could not delete database due to network error: %s\n\n"+
                "The deletion may or may not have been initiated. "+
                "Check the console to verify the database state.\n"+
                "You may need to retry the deletion.",
                err.Error(),
            ),
        )
        return
    }
}

Complete Example

func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    // 1. Get current state
    var state DatabaseResourceModel
    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",
            "Expected configured API client.",
        )
        return
    }
    
    id := state.ID.ValueString()
    
    // 3. Log operation
    tflog.Info(ctx, "Deleting database resource", map[string]any{
        "id":   id,
        "name": state.Name.ValueString(),
    })
    
    // 4. Create final snapshot if configured
    if !state.FinalSnapshotName.IsNull() {
        snapshotName := state.FinalSnapshotName.ValueString()
        
        tflog.Info(ctx, "Creating final snapshot", map[string]any{
            "name": snapshotName,
        })
        
        _, err := r.client.CreateSnapshot(ctx, api.CreateSnapshotRequest{
            DatabaseID: id,
            Name:       snapshotName,
        })
        
        if err != nil {
            resp.Diagnostics.AddError(
                "Failed to Create Final Snapshot",
                fmt.Sprintf("Could not create snapshot: %s", err.Error()),
            )
            return
        }
        
        tflog.Info(ctx, "Final snapshot created")
    }
    
    // 5. Delete database
    tflog.Debug(ctx, "Calling delete API")
    
    err := r.client.DeleteDatabase(ctx, id)
    if err != nil {
        // Already deleted is OK
        if api.IsNotFoundError(err) {
            tflog.Warn(ctx, "Database already deleted")
            return
        }
        
        // Check for protection
        if apiErr, ok := err.(*api.Error); ok {
            if apiErr.Code == "DELETE_PROTECTION_ENABLED" {
                resp.Diagnostics.AddError(
                    "Database is Protected",
                    "Deletion protection must be disabled before deleting.\n"+
                        "Set delete_protection = false and apply, then destroy.",
                )
                return
            }
        }
        
        // Other errors
        resp.Diagnostics.AddError(
            "Failed to Delete Database",
            fmt.Sprintf("Could not delete database: %s", err.Error()),
        )
        return
    }
    
    // 6. Wait for deletion to complete
    tflog.Debug(ctx, "Waiting for deletion to complete")
    
    err = r.client.WaitForDatabaseDeleted(ctx, id, 15*time.Minute)
    if err != nil {
        // Not found is OK (already deleted)
        if api.IsNotFoundError(err) {
            tflog.Debug(ctx, "Database confirmed deleted")
            return
        }
        
        resp.Diagnostics.AddWarning(
            "Deletion May Be Incomplete",
            fmt.Sprintf(
                "Database deletion did not complete within timeout: %s\n\n"+
                "The deletion may still be in progress. Check the console.",
                err.Error(),
            ),
        )
        // Continue - let Terraform remove from state
        return
    }
    
    tflog.Info(ctx, "Database deleted successfully", map[string]any{
        "id": id,
    })
    
    // State is automatically removed
}

Best Practices

1. Handle Already-Deleted Resources

// ✅ Good: Treat not-found as success
if api.IsNotFoundError(err) {
    tflog.Warn(ctx, "Resource already deleted")
    return  // Success
}

// ❌ Bad: Error on not-found
if api.IsNotFoundError(err) {
    resp.Diagnostics.AddError("Not Found", err.Error())
    return
}

2. Wait for Async Deletions

// ✅ Good: Wait for completion
r.client.DeleteDatabase(ctx, id)
r.client.WaitForDatabaseDeleted(ctx, id, timeout)

// ❌ Bad: Don't wait
r.client.DeleteDatabase(ctx, id)
// Resource might not actually be deleted!

3. Log Deletion Progress

// ✅ Good: Log key steps
tflog.Info(ctx, "Starting database deletion")
tflog.Debug(ctx, "Waiting for deletion to complete")
tflog.Info(ctx, "Database deleted successfully")

4. Create Final Snapshots

// ✅ Good: Optional final snapshot
if !state.FinalSnapshotName.IsNull() {
    r.client.CreateSnapshot(ctx, ...)
}

5. Don’t Manually Remove State

// ✅ Good: State removed automatically
// Just return successfully

// ❌ Bad: Don't call this
resp.State.RemoveResource(ctx)  // Automatic, don't call!

Common Mistakes

❌ Not Handling Already-Deleted

// Bad: Fails if resource already gone
err := r.client.DeleteDatabase(ctx, id)
if err != nil {
    resp.Diagnostics.AddError("Delete Failed", err.Error())
    return
}

// Good: Handle not-found
if err != nil {
    if api.IsNotFoundError(err) {
        return  // OK
    }
    resp.Diagnostics.AddError("Delete Failed", err.Error())
}

❌ Not Waiting for Async Deletions

// Bad: Might not be deleted
r.client.DeleteDatabase(ctx, id)
// Done?

// Good: Wait for completion
r.client.DeleteDatabase(ctx, id)
r.client.WaitForDatabaseDeleted(ctx, id, 15*time.Minute)

❌ Manually Removing State

// Bad: Don't do this
resp.State.RemoveResource(ctx)

// Good: State is automatically removed on successful deletion
// Just return without errors

Schema Support for Deletion

Delete Protection

"delete_protection": schema.BoolAttribute{
    Optional:            true,
    MarkdownDescription: "Enable deletion protection",
},

Final Snapshot

"final_snapshot_name": schema.StringAttribute{
    Optional:            true,
    MarkdownDescription: "Name for final snapshot before deletion",
},

"skip_final_snapshot": schema.BoolAttribute{
    Optional:            true,
    MarkdownDescription: "Skip final snapshot on deletion",
},

Key Takeaways

  • ✅ Delete destroys resources and removes them from state
  • ✅ State is automatically removed on success
  • ✅ Handle already-deleted resources gracefully (not an error)
  • ✅ Wait for async deletions to complete
  • ✅ Support final snapshots before deletion
  • ✅ Handle deletion protection and dependent resources
  • ✅ Log deletion progress for debugging
  • ✅ Perform cleanup operations before deletion
  • ✅ Don’t manually remove state (automatic)
  • ✅ Treat network errors carefully (deletion may be in progress)

What’s Next?

Now let’s learn how to import existing resources into Terraform!

Next: Resource Import →


Previous: ← Update Operation

Last updated: June 30, 2026

Updated: