Troubleshooting Lab
Lab Overview
Apply debugging and troubleshooting skills to realistic scenarios you’ll encounter in provider development.
Exercise 1: State Drift Investigation
Scenario
Users report that terraform plan shows changes even though they haven’t modified their configuration.
$ terraform plan
Terraform will perform the following actions:
# cachecloud_cache.example will be updated in-place
~ resource "cachecloud_cache" "example" {
~ tags = {
- "Environment" = "production"
+ "Environment" = "Production"
}
}
Investigation Steps
Step 1: Enable Logging
export TF_LOG=TRACE
export TF_LOG_PATH=terraform.log
terraform plan
Step 2: Examine Logs
# Look for Read operation
grep "CacheResource.Read" terraform.log
# Check what API returned
grep "API response" terraform.log
Step 3: Identify Root Cause
// Found in logs: API returns "Production" but state has "production"
// Problem: Case inconsistency
// Investigation in code
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// API response
cache, err := r.client.GetCache(ctx, state.ID.ValueString())
// Direct assignment - no normalization!
state.Tags = cache.Tags // Problem here
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
Step 4: Fix
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
cache, err := r.client.GetCache(ctx, state.ID.ValueString())
// Normalize tags before setting state
normalizedTags := make(map[string]string)
for k, v := range cache.Tags {
normalizedTags[k] = strings.ToLower(v)
}
// Convert to types.Map
tagValues := make(map[string]attr.Value)
for k, v := range normalizedTags {
tagValues[k] = types.StringValue(v)
}
state.Tags, _ = types.MapValue(types.StringType, tagValues)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
Step 5: Verify
# Test the fix
go test -run TestCacheResource_Read_TagsNormalization
# Verify with actual provider
terraform plan
# No changes shown
Exercise 2: Type Conversion Panic
Scenario
Provider crashes with panic during apply:
$ terraform apply
panic: interface conversion: interface {} is []interface {}, not string
goroutine 1 [running]:
github.com/example/terraform-provider-cachecloud/internal/provider.(*CacheResource).Create(...)
Investigation
Step 1: Reproduce
resource "cachecloud_cache" "test" {
name = "test-cache"
subnet_ids = ["subnet-a", "subnet-b"] # This causes panic
}
Step 2: Debug
# Run with debugger
dlv debug ./main.go -- --debug
# Set breakpoint at panic location
(dlv) break resource_cache.go:45
(dlv) continue
Step 3: Inspect Variables
(dlv) print plan.SubnetIDs
github.com/hashicorp/terraform-plugin-framework/types/basetypes.ListValue {...}
(dlv) whatis plan.SubnetIDs
Type: github.com/hashicorp/terraform-plugin-framework/types/basetypes.ListValue
# Try to access directly (this will panic)
(dlv) print plan.SubnetIDs.([]string)
panic: interface conversion
Step 4: Identify Issue
// Problematic code
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
// ❌ Wrong: Can't directly cast types.List to []string
subnets := plan.SubnetIDs.([]string) // PANIC!
}
Step 5: Fix
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
// ✅ Correct: Use ElementsAs
var subnets []string
resp.Diagnostics.Append(plan.SubnetIDs.ElementsAs(ctx, &subnets, false)...)
if resp.Diagnostics.HasError() {
return
}
// Now use subnets safely
cache, err := r.client.CreateCache(ctx, &CreateCacheInput{
SubnetIDs: subnets,
})
}
Step 6: Test
func TestCacheResource_Create_SubnetIDs(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "cachecloud_cache" "test" {
name = "test"
subnet_ids = ["subnet-a", "subnet-b"]
}
`,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("cachecloud_cache.test", "subnet_ids.#", "2"),
),
},
},
})
}
Exercise 3: API Rate Limiting
Scenario
Large-scale deployments fail with rate limit errors:
Error: 429 Too Many Requests
Error: Failed to create cache: rate limit exceeded
Investigation
Step 1: Analyze Pattern
# Check logs for API call frequency
grep "HTTP request" terraform.log | wc -l
# 1000 requests in 10 seconds = 100 req/s
# API limit is 50 req/s
Step 2: Identify Bottleneck
// Current code makes individual calls
func (d *CachesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var caches []*Cache
// Gets all cache IDs
ids, err := d.client.ListCacheIDs(ctx) // 1 call
// ❌ Individual call for each cache
for _, id := range ids { // 999 more calls!
cache, err := d.client.GetCache(ctx, id)
caches = append(caches, cache)
}
}
Step 3: Implement Solution
// Add rate limiting
import "golang.org/x/time/rate"
type Client struct {
httpClient *http.Client
limiter *rate.Limiter
}
func NewClient(apiKey string, rps float64) *Client {
return &Client{
httpClient: &http.Client{},
limiter: rate.NewLimiter(rate.Limit(rps), 10),
}
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
// Wait for rate limiter
if err := c.limiter.Wait(req.Context()); err != nil {
return nil, err
}
return c.httpClient.Do(req)
}
// Add batching
func (c *Client) BatchGetCaches(ctx context.Context, ids []string) ([]*Cache, error) {
// Single API call for multiple caches
body, _ := json.Marshal(map[string]interface{}{
"ids": ids,
})
req, _ := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/caches/batch", bytes.NewReader(body))
resp, err := c.Do(req)
if err != nil {
return nil, err
}
var caches []*Cache
json.NewDecoder(resp.Body).Decode(&caches)
return caches, nil
}
Step 4: Update Data Source
func (d *CachesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
ids, err := d.client.ListCacheIDs(ctx) // 1 call
// ✅ Batch call
caches, err := d.client.BatchGetCaches(ctx, ids) // 1 more call
// Total: 2 calls instead of 1000!
}
Step 5: Verify
# Test with large deployment
terraform plan
# Check logs
grep "HTTP request" terraform.log | wc -l
# 2 requests - success!
Exercise 4: Memory Leak
Scenario
Provider memory usage grows continuously during long-running operations.
Investigation
Step 1: Profile Memory
// Add pprof endpoint
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... rest of main
}
Step 2: Capture Heap Dumps
# Initial heap dump
curl http://localhost:6060/debug/pprof/heap > heap1.prof
# Run operations
terraform apply
# Final heap dump
curl http://localhost:6060/debug/pprof/heap > heap2.prof
# Compare
go tool pprof -base heap1.prof heap2.prof
(pprof) top
flat flat% sum% cum cum%
512.50MB 85.42% 85.42% 512.50MB 85.42% (*Client).GetCache
87.50MB 14.58% 100% 87.50MB 14.58% (*CacheDataSource).Read
Step 3: Identify Leak
// Problem: Unbounded cache
type Client struct {
cache map[string]*Cache // Never cleaned!
}
func (c *Client) GetCache(ctx context.Context, id string) (*Cache, error) {
// Check cache
if cached, found := c.cache[id]; found {
return cached, nil
}
// Fetch and cache
cache, err := c.fetchCache(ctx, id)
if err != nil {
return nil, err
}
// ❌ Store forever - memory leak!
c.cache[id] = cache
return cache, nil
}
Step 4: Fix
import "github.com/hashicorp/golang-lru/v2"
type Client struct {
cache *lru.Cache[string, *cacheEntry]
}
type cacheEntry struct {
cache *Cache
expiresAt time.Time
}
func NewClient(apiKey string) *Client {
cache, _ := lru.New[string, *cacheEntry](100) // Bounded size
return &Client{
cache: cache,
}
}
func (c *Client) GetCache(ctx context.Context, id string) (*Cache, error) {
// Check cache
if entry, found := c.cache.Get(id); found {
if time.Now().Before(entry.expiresAt) {
return entry.cache, nil
}
c.cache.Remove(id) // Expired
}
// Fetch
cache, err := c.fetchCache(ctx, id)
if err != nil {
return nil, err
}
// Store with TTL
c.cache.Add(id, &cacheEntry{
cache: cache,
expiresAt: time.Now().Add(5 * time.Minute),
})
return cache, nil
}
Step 5: Verify Fix
# Profile again
curl http://localhost:6060/debug/pprof/heap > heap3.prof
# Run operations
terraform apply
curl http://localhost:6060/debug/pprof/heap > heap4.prof
# Compare - should be stable now
go tool pprof -base heap3.prof heap4.prof
(pprof) top
# Memory stable
Exercise 5: Concurrency Issues
Scenario
Intermittent panics in parallel operations:
panic: concurrent map writes
goroutine 42 [running]:
github.com/example/terraform-provider-cachecloud/internal/client.(*Client).recordMetric(...)
Investigation
Step 1: Enable Race Detector
go test -race ./internal/client/
==================
WARNING: DATA RACE
Write at 0x00c000120000 by goroutine 7:
github.com/example/terraform-provider-cachecloud/internal/client.(*Client).recordMetric()
/path/to/client.go:42 +0x123
Previous write at 0x00c000120000 by goroutine 6:
github.com/example/terraform-provider-cachecloud/internal/client.(*Client).recordMetric()
/path/to/client.go:42 +0x123
==================
Step 2: Identify Issue
type Client struct {
metrics map[string]int // ❌ Not thread-safe!
}
func (c *Client) recordMetric(key string) {
// ❌ Concurrent map access - race condition!
c.metrics[key]++
}
func (c *Client) BatchFetch(ctx context.Context, ids []string) {
var wg sync.WaitGroup
for _, id := range ids {
wg.Add(1)
go func(cacheID string) {
defer wg.Done()
c.GetCache(ctx, cacheID)
c.recordMetric("get_cache") // ❌ Concurrent writes!
}(id)
}
wg.Wait()
}
Step 3: Fix
import "sync"
type Client struct {
metrics sync.Map // ✅ Thread-safe map
// Or use mutex
metricsLock sync.RWMutex
metricsData map[string]int
}
// Option 1: sync.Map
func (c *Client) recordMetric(key string) {
val, _ := c.metrics.LoadOrStore(key, new(int64))
atomic.AddInt64(val.(*int64), 1)
}
// Option 2: Mutex
func (c *Client) recordMetric(key string) {
c.metricsLock.Lock()
defer c.metricsLock.Unlock()
c.metricsData[key]++
}
Step 4: Test
func TestConcurrentAccess(t *testing.T) {
client := NewClient("test-key")
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
client.recordMetric("test")
}()
}
wg.Wait()
// Verify count
count := client.getMetric("test")
assert.Equal(t, 100, count)
}
Step 5: Verify
# Run with race detector
go test -race ./...
# No warnings
Troubleshooting Checklist
Before Debugging
- Can you reproduce the issue consistently?
- Do you have the full error message?
- Do you have the Terraform configuration?
- Do you have provider logs?
- What version of the provider?
- What version of Terraform?
During Debugging
- Enable TRACE logging
- Use debugger at suspected location
- Check for null/unknown values
- Verify type conversions
- Check for race conditions
- Profile if performance issue
After Fix
- Write test to prevent regression
- Update documentation
- Check for similar issues elsewhere
- Profile to verify fix
- Add defensive code if needed
Key Takeaways
- ✅ Reproduce issues systematically
- ✅ Use logging and debugger effectively
- ✅ Check for common patterns (state, types, concurrency)
- ✅ Profile performance problems
- ✅ Write tests for bug fixes
- ✅ Document troubleshooting steps
Congratulations!
You’ve completed Module 15 and gained practical debugging skills!
Next Module: Production Best Practices →
Previous: ← Common Issues
Next Module: Production Best Practices →
Last updated: July 2, 2026