Resource Implementation Lab

11 minute read

Lab Overview

In this hands-on lab, you’ll build a complete resource for managing caching instances in a fictional “CacheCloud” service.

What You’ll Build:

  • Complete CRUD operations
  • Resource import functionality
  • Configurable timeouts
  • Proper error handling
  • State management
  • Validation

Time: 75-90 minutes

Prerequisites

# Required tools
go version  # 1.21+
terraform version  # 1.5+

# Create lab directory
mkdir terraform-provider-cachecloud
cd terraform-provider-cachecloud
go mod init github.com/yourorg/terraform-provider-cachecloud

Step 1: Project Structure

terraform-provider-cachecloud/
├── main.go
├── go.mod
├── internal/
│   ├── provider/
│   │   ├── provider.go
│   │   └── cache_resource.go
│   └── client/
│       └── client.go
└── examples/
    └── resources/
        └── cache.tf
mkdir -p internal/provider internal/client examples/resources

Step 2: API Client

Create internal/client/client.go:

package client

import (
    "context"
    "fmt"
    "time"
)

type Client struct {
    baseURL string
    apiKey  string
}

type Cache struct {
    ID              string
    Name            string
    Engine          string
    EngineVersion   string
    NodeType        string
    NumNodes        int
    Port            int
    Status          string
    Endpoint        string
    ARN             string
    CreatedAt       string
    UpdatedAt       string
    Tags            map[string]string
}

type CreateCacheRequest struct {
    Name          string
    Engine        string
    EngineVersion string
    NodeType      string
    NumNodes      int
    Port          int
    Tags          map[string]string
}

type UpdateCacheRequest struct {
    NodeType string
    NumNodes int
    Tags     map[string]string
}

func NewClient(baseURL, apiKey string) *Client {
    return &Client{
        baseURL: baseURL,
        apiKey:  apiKey,
    }
}

func (c *Client) CreateCache(ctx context.Context, req CreateCacheRequest) (*Cache, error) {
    // Simulate API call
    fmt.Printf("Creating cache: %s\n", req.Name)
    
    cache := &Cache{
        ID:            fmt.Sprintf("cache-%d", time.Now().Unix()),
        Name:          req.Name,
        Engine:        req.Engine,
        EngineVersion: req.EngineVersion,
        NodeType:      req.NodeType,
        NumNodes:      req.NumNodes,
        Port:          req.Port,
        Status:        "creating",
        Endpoint:      fmt.Sprintf("%s.cache.example.com:6379", req.Name),
        ARN:           fmt.Sprintf("arn:aws:cache:us-east-1:123456789012:cache/%s", req.Name),
        CreatedAt:     time.Now().Format(time.RFC3339),
        UpdatedAt:     time.Now().Format(time.RFC3339),
        Tags:          req.Tags,
    }
    
    return cache, nil
}

func (c *Client) GetCache(ctx context.Context, id string) (*Cache, error) {
    // Simulate API call
    fmt.Printf("Getting cache: %s\n", id)
    
    cache := &Cache{
        ID:            id,
        Name:          "example-cache",
        Engine:        "redis",
        EngineVersion: "7.0",
        NodeType:      "cache.t3.micro",
        NumNodes:      2,
        Port:          6379,
        Status:        "available",
        Endpoint:      "example-cache.cache.example.com:6379",
        ARN:           fmt.Sprintf("arn:aws:cache:us-east-1:123456789012:cache/%s", id),
        CreatedAt:     time.Now().Add(-1 * time.Hour).Format(time.RFC3339),
        UpdatedAt:     time.Now().Format(time.RFC3339),
        Tags:          map[string]string{"Environment": "production"},
    }
    
    return cache, nil
}

func (c *Client) UpdateCache(ctx context.Context, id string, req UpdateCacheRequest) error {
    // Simulate API call
    fmt.Printf("Updating cache: %s\n", id)
    return nil
}

func (c *Client) DeleteCache(ctx context.Context, id string) error {
    // Simulate API call
    fmt.Printf("Deleting cache: %s\n", id)
    return nil
}

func (c *Client) WaitForCacheReady(ctx context.Context, id string) error {
    // Simulate waiting for cache to be ready
    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()
    
    attempts := 0
    maxAttempts := 5
    
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            attempts++
            fmt.Printf("Waiting for cache %s to be ready (attempt %d/%d)\n", id, attempts, maxAttempts)
            
            if attempts >= maxAttempts {
                return nil
            }
        }
    }
}

func (c *Client) WaitForCacheDeleted(ctx context.Context, id string) error {
    // Simulate waiting for cache to be deleted
    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()
    
    attempts := 0
    maxAttempts := 3
    
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            attempts++
            fmt.Printf("Waiting for cache %s to be deleted (attempt %d/%d)\n", id, attempts, maxAttempts)
            
            if attempts >= maxAttempts {
                return nil
            }
        }
    }
}

Step 3: Provider Setup

Create internal/provider/provider.go:

package provider

import (
    "context"
    "os"
    
    "github.com/hashicorp/terraform-plugin-framework/datasource"
    "github.com/hashicorp/terraform-plugin-framework/provider"
    "github.com/hashicorp/terraform-plugin-framework/provider/schema"
    "github.com/hashicorp/terraform-plugin-framework/resource"
    "github.com/hashicorp/terraform-plugin-framework/types"
    
    "github.com/yourorg/terraform-provider-cachecloud/internal/client"
)

type CacheCloudProvider struct {
    version string
}

type CacheCloudProviderModel struct {
    APIKey   types.String `tfsdk:"api_key"`
    Endpoint types.String `tfsdk:"endpoint"`
}

func New(version string) func() provider.Provider {
    return func() provider.Provider {
        return &CacheCloudProvider{
            version: version,
        }
    }
}

func (p *CacheCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
    resp.TypeName = "cachecloud"
    resp.Version = p.version
}

func (p *CacheCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "api_key": schema.StringAttribute{
                Optional:  true,
                Sensitive: true,
            },
            "endpoint": schema.StringAttribute{
                Optional: true,
            },
        },
    }
}

func (p *CacheCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config CacheCloudProviderModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    
    apiKey := config.APIKey.ValueString()
    if apiKey == "" {
        apiKey = os.Getenv("CACHECLOUD_API_KEY")
    }
    
    endpoint := config.Endpoint.ValueString()
    if endpoint == "" {
        endpoint = "https://api.cachecloud.example.com"
    }
    
    client := client.NewClient(endpoint, apiKey)
    
    resp.ResourceData = client
    resp.DataSourceData = client
}

func (p *CacheCloudProvider) Resources(ctx context.Context) []func() resource.Resource {
    return []func() resource.Resource{
        NewCacheResource,
    }
}

func (p *CacheCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
    return []func() datasource.DataSource{}
}

Step 4: Cache Resource - Schema and Model

Create internal/provider/cache_resource.go:

package provider

import (
    "context"
    "fmt"
    "regexp"
    "time"
    
    "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
    "github.com/hashicorp/terraform-plugin-framework/attr"
    "github.com/hashicorp/terraform-plugin-framework/path"
    "github.com/hashicorp/terraform-plugin-framework/resource"
    "github.com/hashicorp/terraform-plugin-framework/resource/schema"
    "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
    "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
    "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
    "github.com/hashicorp/terraform-plugin-framework/types"
    "github.com/hashicorp/terraform-plugin-log/tflog"
    
    "github.com/yourorg/terraform-provider-cachecloud/internal/client"
)

type CacheResource struct {
    client *client.Client
}

type CacheResourceModel struct {
    ID            types.String   `tfsdk:"id"`
    Name          types.String   `tfsdk:"name"`
    Engine        types.String   `tfsdk:"engine"`
    EngineVersion types.String   `tfsdk:"engine_version"`
    NodeType      types.String   `tfsdk:"node_type"`
    NumNodes      types.Int64    `tfsdk:"num_nodes"`
    Port          types.Int64    `tfsdk:"port"`
    Status        types.String   `tfsdk:"status"`
    Endpoint      types.String   `tfsdk:"endpoint"`
    ARN           types.String   `tfsdk:"arn"`
    CreatedAt     types.String   `tfsdk:"created_at"`
    UpdatedAt     types.String   `tfsdk:"updated_at"`
    Tags          types.Map      `tfsdk:"tags"`
    Timeouts      timeouts.Value `tfsdk:"timeouts"`
}

var _ resource.Resource = &CacheResource{}
var _ resource.ResourceWithConfigure = &CacheResource{}
var _ resource.ResourceWithImportState = &CacheResource{}

func NewCacheResource() resource.Resource {
    return &CacheResource{}
}

func (r *CacheResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
    resp.TypeName = req.ProviderTypeName + "_cache"
}

func (r *CacheResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        MarkdownDescription: "Manages a CacheCloud cache cluster",
        
        Attributes: map[string]schema.Attribute{
            "id": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Cache cluster identifier",
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.UseStateForUnknown(),
                },
            },
            "name": schema.StringAttribute{
                Required:            true,
                MarkdownDescription: "Name of the cache cluster",
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
            },
            "engine": schema.StringAttribute{
                Required:            true,
                MarkdownDescription: "Cache engine (redis or memcached)",
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
            },
            "engine_version": schema.StringAttribute{
                Required:            true,
                MarkdownDescription: "Engine version",
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
            },
            "node_type": schema.StringAttribute{
                Required:            true,
                MarkdownDescription: "Node type (e.g., cache.t3.micro)",
            },
            "num_nodes": schema.Int64Attribute{
                Required:            true,
                MarkdownDescription: "Number of cache nodes",
            },
            "port": schema.Int64Attribute{
                Optional:            true,
                Computed:            true,
                MarkdownDescription: "Port number. Defaults to 6379 for Redis, 11211 for Memcached",
                PlanModifiers: []planmodifier.Int64{
                    int64planmodifier.UseStateForUnknown(),
                },
            },
            "status": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Cache cluster status",
            },
            "endpoint": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Cache endpoint",
            },
            "arn": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "ARN of the cache cluster",
            },
            "created_at": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Creation timestamp",
            },
            "updated_at": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Last update timestamp",
            },
            "tags": schema.MapAttribute{
                Optional:            true,
                ElementType:         types.StringType,
                MarkdownDescription: "Resource tags",
            },
        },
        
        Blocks: map[string]schema.Block{
            "timeouts": timeouts.Block(ctx, timeouts.Opts{
                Create: true,
                Update: true,
                Delete: true,
            }),
        },
    }
}

func (r *CacheResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
    if req.ProviderData == nil {
        return
    }
    
    client, ok := req.ProviderData.(*client.Client)
    if !ok {
        resp.Diagnostics.AddError(
            "Unexpected Resource Configure Type",
            fmt.Sprintf("Expected *client.Client, got: %T", req.ProviderData),
        )
        return
    }
    
    r.client = client
}

Step 5: Create Operation

Add to cache_resource.go:

func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var plan CacheResourceModel
    diags := req.Plan.Get(ctx, &plan)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Get create timeout
    createTimeout, diags := plan.Timeouts.Create(ctx, 20*time.Minute)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    createCtx, cancel := context.WithTimeout(ctx, createTimeout)
    defer cancel()
    
    tflog.Info(ctx, "Creating cache cluster", map[string]any{
        "name":    plan.Name.ValueString(),
        "timeout": createTimeout.String(),
    })
    
    // Validate engine
    engine := plan.Engine.ValueString()
    if engine != "redis" && engine != "memcached" {
        resp.Diagnostics.AddError(
            "Invalid Engine",
            fmt.Sprintf("Engine must be 'redis' or 'memcached', got: %s", engine),
        )
        return
    }
    
    // Set default port if not specified
    port := int(plan.Port.ValueInt64())
    if port == 0 {
        if engine == "redis" {
            port = 6379
        } else {
            port = 11211
        }
    }
    
    // Convert tags
    var tags map[string]string
    if !plan.Tags.IsNull() {
        plan.Tags.ElementsAs(ctx, &tags, false)
    }
    
    // Create cache
    cache, err := r.client.CreateCache(createCtx, client.CreateCacheRequest{
        Name:          plan.Name.ValueString(),
        Engine:        engine,
        EngineVersion: plan.EngineVersion.ValueString(),
        NodeType:      plan.NodeType.ValueString(),
        NumNodes:      int(plan.NumNodes.ValueInt64()),
        Port:          port,
        Tags:          tags,
    })
    
    if err != nil {
        resp.Diagnostics.AddError("Create Failed", err.Error())
        return
    }
    
    // Wait for cache to be ready
    err = r.client.WaitForCacheReady(createCtx, cache.ID)
    if err != nil {
        resp.Diagnostics.AddError(
            "Create Timeout",
            fmt.Sprintf("Cache did not become ready within %s", createTimeout),
        )
        return
    }
    
    // Fetch final state
    cache, err = r.client.GetCache(createCtx, cache.ID)
    if err != nil {
        resp.Diagnostics.AddError("Read After Create Failed", err.Error())
        return
    }
    
    // Set state
    plan.ID = types.StringValue(cache.ID)
    plan.Port = types.Int64Value(int64(cache.Port))
    plan.Status = types.StringValue(cache.Status)
    plan.Endpoint = types.StringValue(cache.Endpoint)
    plan.ARN = types.StringValue(cache.ARN)
    plan.CreatedAt = types.StringValue(cache.CreatedAt)
    plan.UpdatedAt = types.StringValue(cache.UpdatedAt)
    
    // Convert tags
    if len(cache.Tags) > 0 {
        tagsMap := make(map[string]attr.Value)
        for k, v := range cache.Tags {
            tagsMap[k] = types.StringValue(v)
        }
        mapValue, _ := types.MapValue(types.StringType, tagsMap)
        plan.Tags = mapValue
    }
    
    resp.State.Set(ctx, plan)
    tflog.Info(ctx, "Cache created successfully", map[string]any{
        "id": cache.ID,
    })
}

Step 6: Read Operation

Add to cache_resource.go:

func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var state CacheResourceModel
    diags := req.State.Get(ctx, &state)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    id := state.ID.ValueString()
    
    tflog.Debug(ctx, "Reading cache cluster", map[string]any{
        "id": id,
    })
    
    cache, err := r.client.GetCache(ctx, id)
    if err != nil {
        resp.Diagnostics.AddError("Read Failed", err.Error())
        return
    }
    
    // Update state
    state.Name = types.StringValue(cache.Name)
    state.Engine = types.StringValue(cache.Engine)
    state.EngineVersion = types.StringValue(cache.EngineVersion)
    state.NodeType = types.StringValue(cache.NodeType)
    state.NumNodes = types.Int64Value(int64(cache.NumNodes))
    state.Port = types.Int64Value(int64(cache.Port))
    state.Status = types.StringValue(cache.Status)
    state.Endpoint = types.StringValue(cache.Endpoint)
    state.ARN = types.StringValue(cache.ARN)
    state.UpdatedAt = types.StringValue(cache.UpdatedAt)
    
    // Tags
    if len(cache.Tags) > 0 {
        tagsMap := make(map[string]attr.Value)
        for k, v := range cache.Tags {
            tagsMap[k] = types.StringValue(v)
        }
        mapValue, _ := types.MapValue(types.StringType, tagsMap)
        state.Tags = mapValue
    }
    
    resp.State.Set(ctx, state)
}

Step 7: Update Operation

Add to cache_resource.go:

func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var plan, state CacheResourceModel
    
    diags := req.Plan.Get(ctx, &plan)
    resp.Diagnostics.Append(diags...)
    
    diags = req.State.Get(ctx, &state)
    resp.Diagnostics.Append(diags...)
    
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Get update timeout
    updateTimeout, diags := plan.Timeouts.Update(ctx, 15*time.Minute)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    updateCtx, cancel := context.WithTimeout(ctx, updateTimeout)
    defer cancel()
    
    id := state.ID.ValueString()
    
    tflog.Info(ctx, "Updating cache cluster", map[string]any{
        "id":      id,
        "timeout": updateTimeout.String(),
    })
    
    // Validate num_nodes increase only
    if plan.NumNodes.ValueInt64() < state.NumNodes.ValueInt64() {
        resp.Diagnostics.AddError(
            "Invalid Update",
            fmt.Sprintf(
                "Number of nodes can only be increased, not decreased.\n"+
                "Current: %d, Requested: %d",
                state.NumNodes.ValueInt64(),
                plan.NumNodes.ValueInt64(),
            ),
        )
        return
    }
    
    // Convert tags
    var tags map[string]string
    if !plan.Tags.IsNull() {
        plan.Tags.ElementsAs(ctx, &tags, false)
    }
    
    // Update cache
    err := r.client.UpdateCache(updateCtx, id, client.UpdateCacheRequest{
        NodeType: plan.NodeType.ValueString(),
        NumNodes: int(plan.NumNodes.ValueInt64()),
        Tags:     tags,
    })
    
    if err != nil {
        resp.Diagnostics.AddError("Update Failed", err.Error())
        return
    }
    
    // Wait for update
    err = r.client.WaitForCacheReady(updateCtx, id)
    if err != nil {
        resp.Diagnostics.AddError("Update Timeout", err.Error())
        return
    }
    
    // Fetch updated state
    cache, err := r.client.GetCache(updateCtx, id)
    if err != nil {
        resp.Diagnostics.AddError("Read After Update Failed", err.Error())
        return
    }
    
    // Update state
    plan.Status = types.StringValue(cache.Status)
    plan.UpdatedAt = types.StringValue(cache.UpdatedAt)
    
    resp.State.Set(ctx, plan)
    tflog.Info(ctx, "Cache updated successfully")
}

Step 8: Delete Operation

Add to cache_resource.go:

func (r *CacheResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    var state CacheResourceModel
    diags := req.State.Get(ctx, &state)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Get delete timeout
    deleteTimeout, diags := state.Timeouts.Delete(ctx, 10*time.Minute)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    deleteCtx, cancel := context.WithTimeout(ctx, deleteTimeout)
    defer cancel()
    
    id := state.ID.ValueString()
    
    tflog.Info(ctx, "Deleting cache cluster", map[string]any{
        "id":      id,
        "timeout": deleteTimeout.String(),
    })
    
    err := r.client.DeleteCache(deleteCtx, id)
    if err != nil {
        resp.Diagnostics.AddError("Delete Failed", err.Error())
        return
    }
    
    err = r.client.WaitForCacheDeleted(deleteCtx, id)
    if err != nil {
        resp.Diagnostics.AddWarning(
            "Delete Timeout",
            fmt.Sprintf("Deletion may not be complete: %s", err.Error()),
        )
    }
    
    tflog.Info(ctx, "Cache deleted successfully")
}

Step 9: Import Support

Add to cache_resource.go:

func (r *CacheResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
    id := req.ID
    
    tflog.Info(ctx, "Importing cache cluster", map[string]any{
        "id": id,
    })
    
    // Validate ID format
    if !regexp.MustCompile(`^cache-[0-9]+$`).MatchString(id) {
        resp.Diagnostics.AddError(
            "Invalid Import ID",
            fmt.Sprintf(
                "Cache ID must match format 'cache-<number>'.\n"+
                "Example: terraform import cachecloud_cache.example cache-1234567890\n"+
                "Got: %s",
                id,
            ),
        )
        return
    }
    
    // Set ID
    resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
    
    tflog.Info(ctx, "Cache import completed")
}

Step 10: Main Entry Point

Create main.go:

package main

import (
    "context"
    "flag"
    "log"
    
    "github.com/hashicorp/terraform-plugin-framework/providerserver"
    
    "github.com/yourorg/terraform-provider-cachecloud/internal/provider"
)

var version = "dev"

func main() {
    var debug bool
    flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers")
    flag.Parse()
    
    opts := providerserver.ServeOpts{
        Address: "registry.terraform.io/yourorg/cachecloud",
        Debug:   debug,
    }
    
    err := providerserver.Serve(context.Background(), provider.New(version), opts)
    if err != nil {
        log.Fatal(err.Error())
    }
}

Step 11: Install Dependencies

go get github.com/hashicorp/terraform-plugin-framework@latest
go get github.com/hashicorp/terraform-plugin-framework-timeouts@latest
go get github.com/hashicorp/terraform-plugin-log@latest
go mod tidy

Step 12: Build and Test

Build Provider

go build -o terraform-provider-cachecloud

Create Example Configuration

Create examples/resources/cache.tf:

terraform {
  required_providers {
    cachecloud = {
      source = "registry.terraform.io/yourorg/cachecloud"
    }
  }
}

provider "cachecloud" {
  api_key = "test-api-key"
}

resource "cachecloud_cache" "example" {
  name           = "my-cache"
  engine         = "redis"
  engine_version = "7.0"
  node_type      = "cache.t3.micro"
  num_nodes      = 2
  
  tags = {
    Environment = "development"
    Team        = "platform"
  }
  
  timeouts {
    create = "30m"
    update = "20m"
    delete = "15m"
  }
}

output "cache_endpoint" {
  value = cachecloud_cache.example.endpoint
}

Install Provider Locally

# For macOS ARM64
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/darwin_arm64
cp terraform-provider-cachecloud ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/darwin_arm64/

# For macOS Intel
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/darwin_amd64
cp terraform-provider-cachecloud ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/darwin_amd64/

# For Linux
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/linux_amd64
cp terraform-provider-cachecloud ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/linux_amd64/

Test the Provider

cd examples/resources

# Initialize
terraform init

# Plan
terraform plan

# Apply
terraform apply

# Test import
terraform import cachecloud_cache.example cache-1234567890

# Destroy
terraform destroy

Lab Validation

Test Checklist

  • Provider builds without errors
  • Create operation works
  • Read operation populates state
  • Update operation modifies resource
  • Update validates num_nodes increase only
  • Delete operation completes
  • Import validates ID format
  • Import populates state via Read
  • Timeouts are configurable
  • Tags are managed correctly
  • Computed attributes are set
  • Logs show operation progress

Expected Output

terraform apply

Terraform will perform the following actions:

  # cachecloud_cache.example will be created
  + resource "cachecloud_cache" "example" {
      + arn            = (known after apply)
      + created_at     = (known after apply)
      + endpoint       = (known after apply)
      + engine         = "redis"
      + engine_version = "7.0"
      + id             = (known after apply)
      + name           = "my-cache"
      + node_type      = "cache.t3.micro"
      + num_nodes      = 2
      + port           = (known after apply)
      + status         = (known after apply)
      + tags           = {
          + "Environment" = "development"
          + "Team"        = "platform"
        }
      + updated_at     = (known after apply)

      + timeouts {
          + create = "30m"
          + delete = "15m"
          + update = "20m"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

cachecloud_cache.example: Creating...
cachecloud_cache.example: Creation complete after 10s

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

cache_endpoint = "my-cache.cache.example.com:6379"

Challenge Exercises

1. Add Snapshot Support

Add a snapshot_name attribute for final snapshots before deletion.

2. Add Validation

Add validators for node_type and num_nodes.

3. Add Data Source

Create a data source to look up cache clusters by name.

4. Add Maintenance Window

Add a maintenance_window attribute that can be updated in-place.

5. Add Multi-AZ Support

Add a multi_az boolean attribute that requires replacement.

Key Takeaways

  • ✅ Resource implementation requires all CRUD operations
  • ✅ Timeouts provide user control over operation duration
  • ✅ Import enables managing existing infrastructure
  • ✅ Proper error handling improves user experience
  • ✅ Logging aids in debugging
  • ✅ Validation catches errors early
  • ✅ Plan modifiers control resource behavior
  • ✅ Computed attributes are set from API responses
  • ✅ Context with timeout enables cancellation
  • ✅ Testing validates implementation

What’s Next?

Test your knowledge with comprehensive practice questions!

Next: Resource Practice →


Previous: ← Resource Timeouts

Last updated: June 30, 2026

Updated: