Create Operation
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:
- Read the plan (desired configuration)
- Make API call to create the resource
- Handle errors gracefully
- Set computed attributes
- 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 loggingreq: 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!
Previous: ← Resource Basics
Last updated: June 30, 2026