Resource Basics

8 minute read

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 instance
  • azurerm_virtual_machine - Azure VM
  • google_compute_instance - GCP instance
  • mycloud_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.ProviderTypeName comes 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:

  1. Provider’s Configure creates client
  2. Provider sets resp.ResourceData = client
  3. Resource’s Configure receives client via req.ProviderData
  4. Resource stores client in struct
  5. 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!

Next: Create Operation →


Previous: ← Resource Implementation Overview

Last updated: June 30, 2026

Updated: