Resource Implementation Lab
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!
Previous: ← Resource Timeouts
Last updated: June 30, 2026