Delete Operation
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 applyafter removal - ✅ Resource needs to be replaced
Delete must:
- Read current state to get resource ID
- Make API call to delete the resource
- Wait for deletion to complete
- Handle already-deleted resources
- 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 loggingreq: Contains current stateresp: 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!
Previous: ← Update Operation
Last updated: June 30, 2026