Create Operation

8 minute read

What is the Create Operation?

The Create operation provisions a new resource in your infrastructure. It’s called when a user runs terraform apply for a resource that doesn’t yet exist in the state file.

When Create is called:

  • ✅ New resource in Terraform configuration
  • ✅ Resource not in state file
  • ✅ User runs terraform apply

Create must:

  1. Read the plan (desired configuration)
  2. Make API call to create the resource
  3. Handle errors gracefully
  4. Set computed attributes
  5. Save the new state

Create Method Signature

func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    // Implementation here
}

Parameters:

  • ctx: Context for cancellation, timeouts, and logging
  • req: Contains the plan (desired configuration)
  • resp: Used to set state and add diagnostics

Basic Create Implementation

Step-by-Step

func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    // Step 1: Get plan data
    var plan DatabaseResourceModel
    diags := req.Plan.Get(ctx, &plan)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Step 2: Log the operation
    tflog.Info(ctx, "Creating database resource", map[string]any{
        "name": plan.Name.ValueString(),
    })
    
    // Step 3: Make API call
    db, err := r.client.CreateDatabase(ctx, api.CreateDatabaseRequest{
        Name:          plan.Name.ValueString(),
        Engine:        plan.Engine.ValueString(),
        EngineVersion: plan.EngineVersion.ValueString(),
        InstanceClass: plan.InstanceClass.ValueString(),
        StorageGB:     int(plan.StorageGB.ValueInt64()),
    })
    
    // Step 4: Handle errors
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Create Database",
            fmt.Sprintf("Could not create database: %s", err.Error()),
        )
        return
    }
    
    // Step 5: Set computed attributes
    plan.ID = types.StringValue(db.ID)
    plan.Status = types.StringValue(db.Status)
    plan.Endpoint = types.StringValue(db.Endpoint)
    plan.CreatedAt = types.StringValue(db.CreatedAt)
    
    // Step 6: Save state
    diags = resp.State.Set(ctx, plan)
    resp.Diagnostics.Append(diags...)
    
    tflog.Info(ctx, "Database created successfully", map[string]any{
        "id": db.ID,
    })
}

Detailed Implementation

1. Reading Plan Data

// Always parse plan first
var plan DatabaseResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)

// Check for errors before continuing
if resp.Diagnostics.HasError() {
    return  // Stop execution if plan parsing failed
}

What happens here:

  • Terraform serializes the HCL configuration
  • Framework deserializes it into your model struct
  • Any mismatches trigger errors

2. Extracting Values

// Extract string values
name := plan.Name.ValueString()
engine := plan.Engine.ValueString()

// Extract numeric values
storageGB := plan.StorageGB.ValueInt64()

// Extract boolean values
enabled := plan.Enabled.ValueBool()

// Check for null/unknown
if plan.Tags.IsNull() {
    // Tags not provided
}

if plan.Tags.IsUnknown() {
    // Tags value not yet known (shouldn't happen in Create)
}

3. Making API Calls

// Build request
req := api.CreateDatabaseRequest{
    Name:          plan.Name.ValueString(),
    Engine:        plan.Engine.ValueString(),
    EngineVersion: plan.EngineVersion.ValueString(),
    InstanceClass: plan.InstanceClass.ValueString(),
    StorageGB:     int(plan.StorageGB.ValueInt64()),
}

// Make API call
db, err := r.client.CreateDatabase(ctx, req)
if err != nil {
    resp.Diagnostics.AddError(
        "API Call Failed",
        fmt.Sprintf("Failed to create database: %s", err.Error()),
    )
    return
}

4. Handling Tags (Map Attributes)

// Convert Terraform map to Go map
if !plan.Tags.IsNull() {
    tagsMap := make(map[string]string)
    diags := plan.Tags.ElementsAs(ctx, &tagsMap, false)
    resp.Diagnostics.Append(diags...)
    
    if !resp.Diagnostics.HasError() {
        req.Tags = tagsMap
    }
}

5. Setting Computed Attributes

// ID - usually from API response
plan.ID = types.StringValue(db.ID)

// Status - current state
plan.Status = types.StringValue(db.Status)

// Endpoint - connection string
plan.Endpoint = types.StringValue(db.Endpoint)

// Timestamps
plan.CreatedAt = types.StringValue(db.CreatedAt)
plan.UpdatedAt = types.StringValue(db.UpdatedAt)

// ARN or other identifiers
plan.ARN = types.StringValue(db.ARN)

6. Saving State

// Save the final state
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)

// Note: No need to check HasError here
// Terraform will handle it

Error Handling

API Errors

db, err := r.client.CreateDatabase(ctx, req)
if err != nil {
    // Check for specific error types
    if apiErr, ok := err.(*api.Error); ok {
        if apiErr.Code == "QUOTA_EXCEEDED" {
            resp.Diagnostics.AddError(
                "Quota Exceeded",
                "You have reached your database quota. Please delete unused databases or contact support.",
            )
            return
        }
        
        if apiErr.Code == "NAME_CONFLICT" {
            resp.Diagnostics.AddError(
                "Database Name Conflict",
                fmt.Sprintf("A database with name '%s' already exists. Please choose a different name.", name),
            )
            return
        }
    }
    
    // Generic error
    resp.Diagnostics.AddError(
        "Failed to Create Database",
        fmt.Sprintf("Could not create database: %s", err.Error()),
    )
    return
}

Validation Errors

// Validate before making API call
if plan.StorageGB.ValueInt64() < 20 {
    resp.Diagnostics.AddError(
        "Invalid Storage Size",
        fmt.Sprintf("Storage must be at least 20GB, got %d GB", plan.StorageGB.ValueInt64()),
    )
    return
}

// Validate engine version compatibility
if plan.Engine.ValueString() == "postgres" {
    version := plan.EngineVersion.ValueString()
    if !isValidPostgresVersion(version) {
        resp.Diagnostics.AddError(
            "Invalid Engine Version",
            fmt.Sprintf("PostgreSQL version %s is not supported. Valid versions: 12.x, 13.x, 14.x, 15.x", version),
        )
        return
    }
}

Complex Attributes

List of Objects

type DatabaseResourceModel struct {
    // ... other fields ...
    Replicas types.List `tfsdk:"replicas"`  // List of replica configurations
}

type ReplicaModel struct {
    Region        types.String `tfsdk:"region"`
    InstanceClass types.String `tfsdk:"instance_class"`
}

// In Create method
var replicas []ReplicaModel
if !plan.Replicas.IsNull() {
    diags := plan.Replicas.ElementsAs(ctx, &replicas, false)
    resp.Diagnostics.Append(diags...)
    
    // Convert to API format
    apiReplicas := make([]api.ReplicaConfig, len(replicas))
    for i, replica := range replicas {
        apiReplicas[i] = api.ReplicaConfig{
            Region:        replica.Region.ValueString(),
            InstanceClass: replica.InstanceClass.ValueString(),
        }
    }
    
    req.Replicas = apiReplicas
}

Nested Attributes

type DatabaseResourceModel struct {
    // ... other fields ...
    BackupConfig types.Object `tfsdk:"backup_config"`
}

type BackupConfigModel struct {
    RetentionDays types.Int64  `tfsdk:"retention_days"`
    Window        types.String `tfsdk:"window"`
    Enabled       types.Bool   `tfsdk:"enabled"`
}

// In Create method
if !plan.BackupConfig.IsNull() {
    var backupConfig BackupConfigModel
    diags := plan.BackupConfig.As(ctx, &backupConfig, basetypes.ObjectAsOptions{})
    resp.Diagnostics.Append(diags...)
    
    req.BackupConfig = api.BackupConfig{
        RetentionDays: int(backupConfig.RetentionDays.ValueInt64()),
        Window:        backupConfig.Window.ValueString(),
        Enabled:       backupConfig.Enabled.ValueBool(),
    }
}

Async Operations

For long-running operations, wait for completion:

func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var plan DatabaseResourceModel
    req.Plan.Get(ctx, &plan)
    
    tflog.Info(ctx, "Creating database (async operation)")
    
    // Start creation
    db, err := r.client.CreateDatabase(ctx, createReq)
    if err != nil {
        resp.Diagnostics.AddError("Create Failed", err.Error())
        return
    }
    
    // Wait for database to be ready
    tflog.Debug(ctx, "Waiting for database to become available", map[string]any{
        "id": db.ID,
    })
    
    err = r.client.WaitForDatabaseReady(ctx, db.ID, 30*time.Minute)
    if err != nil {
        resp.Diagnostics.AddError(
            "Database Creation Timeout",
            fmt.Sprintf(
                "Database created but did not become available: %s\n\n"+
                "The database may still be initializing. Check the console.",
                err.Error(),
            ),
        )
        return
    }
    
    // Fetch final state
    db, err = r.client.GetDatabase(ctx, db.ID)
    if err != nil {
        resp.Diagnostics.AddError("Read After Create Failed", err.Error())
        return
    }
    
    // Set final state
    plan.ID = types.StringValue(db.ID)
    plan.Status = types.StringValue(db.Status)
    plan.Endpoint = types.StringValue(db.Endpoint)
    
    resp.State.Set(ctx, plan)
    tflog.Info(ctx, "Database is now available")
}

Idempotency

Create should be idempotent when possible:

func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var plan DatabaseResourceModel
    req.Plan.Get(ctx, &plan)
    
    name := plan.Name.ValueString()
    
    // Try to create
    db, err := r.client.CreateDatabase(ctx, createReq)
    
    // If already exists, fetch and use it
    if err != nil {
        if apiErr, ok := err.(*api.Error); ok && apiErr.Code == "ALREADY_EXISTS" {
            tflog.Warn(ctx, "Database already exists, importing", map[string]any{
                "name": name,
            })
            
            // Fetch existing database
            db, err = r.client.GetDatabaseByName(ctx, name)
            if err != nil {
                resp.Diagnostics.AddError(
                    "Failed to Fetch Existing Database",
                    err.Error(),
                )
                return
            }
        } else {
            resp.Diagnostics.AddError("Create Failed", err.Error())
            return
        }
    }
    
    // Continue with normal flow
    plan.ID = types.StringValue(db.ID)
    resp.State.Set(ctx, plan)
}

Complete Example

func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    // 1. Parse plan
    var plan DatabaseResourceModel
    diags := req.Plan.Get(ctx, &plan)
    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. Please report this issue.",
        )
        return
    }
    
    // 3. Extract values
    name := plan.Name.ValueString()
    engine := plan.Engine.ValueString()
    engineVersion := plan.EngineVersion.ValueString()
    instanceClass := plan.InstanceClass.ValueString()
    storageGB := int(plan.StorageGB.ValueInt64())
    
    // 4. Build API request
    createReq := api.CreateDatabaseRequest{
        Name:          name,
        Engine:        engine,
        EngineVersion: engineVersion,
        InstanceClass: instanceClass,
        StorageGB:     storageGB,
    }
    
    // 5. Handle optional tags
    if !plan.Tags.IsNull() {
        tagsMap := make(map[string]string)
        diags := plan.Tags.ElementsAs(ctx, &tagsMap, false)
        resp.Diagnostics.Append(diags...)
        if !resp.Diagnostics.HasError() {
            createReq.Tags = tagsMap
        }
    }
    
    // 6. Log operation
    tflog.Info(ctx, "Creating database", map[string]any{
        "name":   name,
        "engine": engine,
    })
    
    // 7. Create database
    db, err := r.client.CreateDatabase(ctx, createReq)
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Create Database",
            fmt.Sprintf(
                "Could not create database '%s': %s\n\n"+
                "Please verify:\n"+
                "  - Database name is unique\n"+
                "  - Instance class is available in your region\n"+
                "  - You have sufficient quota",
                name, err.Error(),
            ),
        )
        return
    }
    
    // 8. Wait for ready state
    tflog.Debug(ctx, "Waiting for database to be ready")
    err = r.client.WaitForDatabaseReady(ctx, db.ID, 30*time.Minute)
    if err != nil {
        resp.Diagnostics.AddError(
            "Database Creation Timeout",
            fmt.Sprintf("Database did not become ready: %s", err.Error()),
        )
        return
    }
    
    // 9. Fetch final state
    db, err = r.client.GetDatabase(ctx, db.ID)
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Read Database After Creation",
            err.Error(),
        )
        return
    }
    
    // 10. Set computed attributes
    plan.ID = types.StringValue(db.ID)
    plan.Status = types.StringValue(db.Status)
    plan.Endpoint = types.StringValue(db.Endpoint)
    plan.ARN = types.StringValue(db.ARN)
    plan.CreatedAt = types.StringValue(db.CreatedAt)
    plan.UpdatedAt = types.StringValue(db.UpdatedAt)
    
    // 11. Save state
    diags = resp.State.Set(ctx, plan)
    resp.Diagnostics.Append(diags...)
    
    tflog.Info(ctx, "Database created successfully", map[string]any{
        "id":       db.ID,
        "endpoint": db.Endpoint,
    })
}

Best Practices

1. Always Check Errors

// ✅ Good: Check and return
if err != nil {
    resp.Diagnostics.AddError("Error", err.Error())
    return
}

// ❌ Bad: Ignore errors
db, _ := r.client.CreateDatabase(ctx, req)

2. Provide Detailed Error Messages

// ✅ Good: Actionable error message
resp.Diagnostics.AddError(
    "Failed to Create Database",
    fmt.Sprintf(
        "Could not create database '%s': %s\n\n"+
        "Common causes:\n"+
        "  - Database name already in use\n"+
        "  - Insufficient quota\n"+
        "  - Invalid instance class for region",
        name, err.Error(),
    ),
)

// ❌ Bad: Generic message
resp.Diagnostics.AddError("Error", "Failed")

3. Log Important Steps

// ✅ Good: Log key operations
tflog.Info(ctx, "Creating database", map[string]any{
    "name":   name,
    "engine": engine,
})

tflog.Debug(ctx, "Waiting for ready state")

tflog.Info(ctx, "Database created", map[string]any{
    "id": db.ID,
})

4. Set All Computed Attributes

// ✅ Good: Set all computed values
plan.ID = types.StringValue(db.ID)
plan.Status = types.StringValue(db.Status)
plan.Endpoint = types.StringValue(db.Endpoint)
plan.ARN = types.StringValue(db.ARN)
plan.CreatedAt = types.StringValue(db.CreatedAt)

// ❌ Bad: Missing computed values
plan.ID = types.StringValue(db.ID)
// Other computed fields not set

5. Validate Before API Call

// ✅ Good: Validate first
if storageGB < 20 || storageGB > 65536 {
    resp.Diagnostics.AddError("Invalid Storage", ...)
    return
}

// Then make API call
db, err := r.client.CreateDatabase(ctx, req)

Common Mistakes

❌ Not Checking HasError

// Bad: Continue after error
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
// Should check HasError here!

// Might panic if plan parsing failed
name := plan.Name.ValueString()

❌ Not Setting Computed Attributes

// Bad: Computed fields not set
// User configuration
plan.Name = types.StringValue("my-db")

// API call
db, _ := r.client.CreateDatabase(...)

// Missing! ID, Status, Endpoint should be set
resp.State.Set(ctx, plan)

❌ Not Handling Null Values

// Bad: Might panic if Tags is null
tagsMap := make(map[string]string)
plan.Tags.ElementsAs(ctx, &tagsMap, false)  // Panics if null!

// Good: Check for null first
if !plan.Tags.IsNull() {
    tagsMap := make(map[string]string)
    plan.Tags.ElementsAs(ctx, &tagsMap, false)
}

Key Takeaways

  • ✅ Create provisions new resources during terraform apply
  • ✅ Always parse plan and check for errors
  • ✅ Make API call to create the resource
  • ✅ Handle errors with detailed messages
  • ✅ Wait for async operations to complete
  • ✅ Set all computed attributes from API response
  • ✅ Save final state with resp.State.Set()
  • ✅ Log important operations for debugging
  • ✅ Check for null values before accessing
  • ✅ Validate inputs before making API calls

What’s Next?

Now let’s implement the Read operation to keep state synchronized!

Next: Read Operation →


Previous: ← Resource Basics

Last updated: June 30, 2026

Updated: