Resource Basics
What is a Resource?
A resource in Terraform represents an infrastructure object that has a lifecycle - it can be created, read, updated, and deleted. Resources are the primary way users interact with your provider.
Examples:
aws_instance- EC2 instanceazurerm_virtual_machine- Azure VMgoogle_compute_instance- GCP instancemycloud_database- Your custom database resource
Resource Structure
Basic Resource Struct
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Resource struct
type DatabaseResource struct {
client *api.Client // Shared client from provider
}
// Resource model (maps to Terraform configuration)
type DatabaseResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Engine types.String `tfsdk:"engine"`
EngineVersion types.String `tfsdk:"engine_version"`
InstanceClass types.String `tfsdk:"instance_class"`
StorageGB types.Int64 `tfsdk:"storage_gb"`
Status types.String `tfsdk:"status"`
Endpoint types.String `tfsdk:"endpoint"`
CreatedAt types.String `tfsdk:"created_at"`
Tags types.Map `tfsdk:"tags"`
}
Required Interfaces
// Ensure resource implements required interfaces
var _ resource.Resource = &DatabaseResource{}
var _ resource.ResourceWithConfigure = &DatabaseResource{}
// Constructor function
func NewDatabaseResource() resource.Resource {
return &DatabaseResource{}
}
Resource Metadata
The Metadata method provides basic information about the resource.
func (r *DatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_database"
}
What this does:
- Sets the resource name that users will use in Terraform
req.ProviderTypeNamecomes from the provider (e.g., “mycloud”)- Result:
mycloud_database
Usage in Terraform:
resource "mycloud_database" "example" {
name = "my-db"
}
Resource Schema
The Schema method defines the resource’s attributes and their types.
func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a database instance",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Unique identifier for the database",
},
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Name of the database instance",
},
"engine": schema.StringAttribute{
Required: true,
MarkdownDescription: "Database engine (postgres, mysql, mongodb)",
},
"engine_version": schema.StringAttribute{
Required: true,
MarkdownDescription: "Version of the database engine",
},
"instance_class": schema.StringAttribute{
Required: true,
MarkdownDescription: "Instance class (db.t3.small, db.t3.medium, etc.)",
},
"storage_gb": schema.Int64Attribute{
Required: true,
MarkdownDescription: "Storage size in gigabytes",
},
"status": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Current status of the database",
},
"endpoint": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Connection endpoint for the database",
},
"created_at": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Timestamp when the database was created",
},
"tags": schema.MapAttribute{
Optional: true,
ElementType: types.StringType,
MarkdownDescription: "Key-value tags for the database",
},
},
}
}
Attribute Types Recap
- Required: User must provide a value
- Optional: User may provide a value
- Computed: Provider calculates the value
- Optional + Computed: User can provide, or provider computes
Configure Method
The Configure method receives the shared client from the provider.
func (r *DatabaseResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Check if provider data is available
if req.ProviderData == nil {
return
}
// Type assert to get client
client, ok := req.ProviderData.(*api.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *api.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
// Store client for use in CRUD operations
r.client = client
}
Flow:
- Provider’s
Configurecreates client - Provider sets
resp.ResourceData = client - Resource’s
Configurereceives client viareq.ProviderData - Resource stores client in struct
- CRUD operations use stored client
CRUD Method Stubs
Every resource must implement these methods:
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// TODO: Implement create logic
}
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// TODO: Implement read logic
}
func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// TODO: Implement update logic
}
func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// TODO: Implement delete logic
}
Complete Resource Template
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/mycompany/mycloud-api-client/api"
)
// Resource struct
type DatabaseResource struct {
client *api.Client
}
// Resource model
type DatabaseResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Engine types.String `tfsdk:"engine"`
EngineVersion types.String `tfsdk:"engine_version"`
InstanceClass types.String `tfsdk:"instance_class"`
StorageGB types.Int64 `tfsdk:"storage_gb"`
Status types.String `tfsdk:"status"`
Endpoint types.String `tfsdk:"endpoint"`
CreatedAt types.String `tfsdk:"created_at"`
Tags types.Map `tfsdk:"tags"`
}
// Ensure interfaces are implemented
var _ resource.Resource = &DatabaseResource{}
var _ resource.ResourceWithConfigure = &DatabaseResource{}
// Constructor
func NewDatabaseResource() resource.Resource {
return &DatabaseResource{}
}
// Metadata
func (r *DatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_database"
}
// Schema
func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a database instance",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Database unique identifier",
},
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Database name",
},
"engine": schema.StringAttribute{
Required: true,
MarkdownDescription: "Database engine",
},
"engine_version": schema.StringAttribute{
Required: true,
MarkdownDescription: "Engine version",
},
"instance_class": schema.StringAttribute{
Required: true,
MarkdownDescription: "Instance class",
},
"storage_gb": schema.Int64Attribute{
Required: true,
MarkdownDescription: "Storage in GB",
},
"status": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Database status",
},
"endpoint": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Connection endpoint",
},
"created_at": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Creation timestamp",
},
"tags": schema.MapAttribute{
Optional: true,
ElementType: types.StringType,
MarkdownDescription: "Resource tags",
},
},
}
}
// Configure
func (r *DatabaseResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*api.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *api.Client, got: %T.", req.ProviderData),
)
return
}
r.client = client
}
// Create
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
tflog.Info(ctx, "Creating database resource")
// Implementation in next chapter
}
// Read
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
tflog.Debug(ctx, "Reading database resource")
// Implementation in next chapter
}
// Update
func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
tflog.Info(ctx, "Updating database resource")
// Implementation in next chapter
}
// Delete
func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
tflog.Info(ctx, "Deleting database resource")
// Implementation in next chapter
}
Registering Resources
Resources must be registered in the provider:
func (p *MyCloudProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewDatabaseResource,
NewServerResource,
NewNetworkResource,
// Add more resources here
}
}
Resource Lifecycle Overview
1. terraform plan
┌─────────────────┐
│ User Config │ (what user wants)
└────────┬────────┘
│
↓
┌─────────────────┐
│ Provider │
│ + Resources │ (validates schema)
└────────┬────────┘
│
↓
┌─────────────────┐
│ Read (refresh) │ (checks current state)
└────────┬────────┘
│
↓
┌─────────────────┐
│ Plan Diff │ (what needs to change)
└─────────────────┘
2. terraform apply
┌─────────────────┐
│ Create/Update/ │
│ Delete Ops │ (make changes)
└────────┬────────┘
│
↓
┌─────────────────┐
│ Read (verify) │ (confirm changes)
└────────┬────────┘
│
↓
┌─────────────────┐
│ State File │ (save current state)
└─────────────────┘
Context Parameter
Every CRUD method receives a context.Context:
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Use context for:
// 1. Logging
tflog.Info(ctx, "Creating database")
// 2. API calls
db, err := r.client.CreateDatabase(ctx, ...)
// 3. Cancellation
if ctx.Err() != nil {
return // User cancelled
}
}
Context uses:
- ✅ Request cancellation
- ✅ Timeouts
- ✅ Logging with correlation IDs
- ✅ Passing to API client calls
Request and Response
Request Types
// Create: plan data
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan DatabaseResourceModel
diags := req.Plan.Get(ctx, &plan) // Get desired config
}
// Read: current state
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
diags := req.State.Get(ctx, &state) // Get current state
}
// Update: both plan and state
func (r *DatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan DatabaseResourceModel
var state DatabaseResourceModel
req.Plan.Get(ctx, &plan) // Desired config
req.State.Get(ctx, &state) // Current state
}
// Delete: current state
func (r *DatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
}
Response Types
// Set new state
resp.State.Set(ctx, &newState)
// Add error (stops execution)
resp.Diagnostics.AddError("Title", "Details")
// Add warning (continues execution)
resp.Diagnostics.AddWarning("Title", "Details")
// Remove resource from state (Read only)
resp.State.RemoveResource(ctx)
Logging Best Practices
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Info: Important milestones
tflog.Info(ctx, "Creating database resource")
// Debug: Detailed information
tflog.Debug(ctx, "Database configuration", map[string]any{
"name": plan.Name.ValueString(),
"engine": plan.Engine.ValueString(),
})
// Trace: Very detailed (usually not needed)
tflog.Trace(ctx, "Making API call", map[string]any{
"endpoint": "/api/v1/databases",
})
// Error: Use Diagnostics, not tflog.Error
if err != nil {
resp.Diagnostics.AddError("Create Failed", err.Error())
return
}
tflog.Info(ctx, "Database created successfully", map[string]any{
"id": db.ID,
})
}
Common Patterns
Pattern 1: Parse Plan/State
// Always check for errors
var plan DatabaseResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
Pattern 2: Check Client
if r.client == nil {
resp.Diagnostics.AddError(
"Unconfigured Client",
"Expected configured API client. Please report this issue.",
)
return
}
Pattern 3: Set State
// Always set state at the end
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
Pattern 4: Error Handling
if err != nil {
resp.Diagnostics.AddError(
"Operation Failed",
fmt.Sprintf("Could not perform operation: %s", err.Error()),
)
return // Stop execution
}
Best Practices
1. Validate Configuration
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,
Validators: []validator.Int64{
int64validator.Between(20, 65536), // 20GB - 64TB
},
},
},
}
}
2. Use Descriptive Error Messages
// ❌ Bad
resp.Diagnostics.AddError("Error", "Failed")
// ✅ Good
resp.Diagnostics.AddError(
"Failed to Create Database",
fmt.Sprintf(
"Could not create database '%s': %s\n\n"+
"Verify that:\n"+
" - Database name is unique\n"+
" - Instance class is available in your region\n"+
" - You have sufficient quota",
name, err.Error(),
),
)
3. Log Important Operations
tflog.Info(ctx, "Creating database", map[string]any{
"name": plan.Name.ValueString(),
"engine": plan.Engine.ValueString(),
})
4. Handle nil Client
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
if r.client == nil {
resp.Diagnostics.AddError("Unconfigured Client", "...")
return
}
// Continue...
}
5. Use Structured Models
// ✅ Good: Structured model
type DatabaseResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Engine types.String `tfsdk:"engine"`
}
// ❌ Bad: Dynamic map
var data map[string]any
Key Takeaways
- ✅ Resources represent infrastructure objects with CRUD lifecycle
- ✅ Resource struct holds shared client from provider
- ✅ Model struct maps to Terraform configuration
- ✅ Metadata sets the resource type name
- ✅ Schema defines attributes and their types
- ✅ Configure receives shared client from provider
- ✅ CRUD methods implement resource operations
- ✅ Context is used for logging, cancellation, and API calls
- ✅ Request contains plan/state data
- ✅ Response is used to set state and add diagnostics
What’s Next?
Now that you understand resource basics, let’s implement the Create operation!
Previous: ← Resource Implementation Overview
Last updated: June 30, 2026