Production Best Practices

8 minute read

Production Readiness Overview

Production-ready providers require careful attention to reliability, security, performance, and maintainability.

Security Best Practices

API Key Management

// ✅ Good: Never log sensitive data
func (p *provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config providerModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    
    apiKey := config.APIKey.ValueString()
    
    // Mask in logs
    ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "api_key")
    ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "secret")
    
    tflog.Info(ctx, "Provider configured", map[string]interface{}{
        "endpoint": config.Endpoint.ValueString(),
        // API key NOT logged
    })
}

// ❌ Bad: Exposing secrets
tflog.Debug(ctx, "Config", map[string]interface{}{
    "api_key": apiKey,  // Don't do this!
})

Secure Communication

func NewClient(apiKey string) *Client {
    // Configure TLS
    tlsConfig := &tls.Config{
        MinVersion: tls.VersionTLS12,
        CipherSuites: []uint16{
            tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        },
    }
    
    transport := &http.Transport{
        TLSClientConfig: tlsConfig,
        // Connection pooling
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    }
    
    return &Client{
        httpClient: &http.Client{
            Transport: transport,
            Timeout:   30 * time.Second,
        },
        apiKey: apiKey,
    }
}

Input Validation

func (p *provider) ConfigSchema(ctx context.Context) schema.Schema {
    return schema.Schema{
        Attributes: map[string]schema.Attribute{
            "api_key": schema.StringAttribute{
                Required:  true,
                Sensitive: true,
                Validators: []validator.String{
                    stringvalidator.LengthAtLeast(32),
                    stringvalidator.RegexMatches(
                        regexp.MustCompile(`^[a-zA-Z0-9_-]+$`),
                        "must contain only alphanumeric characters, underscores, and hyphens",
                    ),
                },
            },
            "endpoint": schema.StringAttribute{
                Optional: true,
                Validators: []validator.String{
                    stringvalidator.RegexMatches(
                        regexp.MustCompile(`^https://`),
                        "must use HTTPS",
                    ),
                },
            },
        },
    }
}

Reliability Patterns

Retry Logic

type RetryConfig struct {
    MaxRetries int
    BaseDelay  time.Duration
    MaxDelay   time.Duration
}

func (c *Client) CreateCacheWithRetry(ctx context.Context, input *CreateCacheInput) (*Cache, error) {
    config := RetryConfig{
        MaxRetries: 3,
        BaseDelay:  100 * time.Millisecond,
        MaxDelay:   5 * time.Second,
    }
    
    var lastErr error
    for attempt := 0; attempt <= config.MaxRetries; attempt++ {
        cache, err := c.CreateCache(ctx, input)
        if err == nil {
            return cache, nil
        }
        
        lastErr = err
        
        // Check if retryable
        if !isRetryableError(err) {
            return nil, err
        }
        
        // Don't sleep on last attempt
        if attempt < config.MaxRetries {
            delay := calculateBackoff(attempt, config)
            
            tflog.Warn(ctx, "Retrying after error", map[string]interface{}{
                "attempt":   attempt + 1,
                "max":       config.MaxRetries,
                "delay_ms":  delay.Milliseconds(),
                "error":     err.Error(),
            })
            
            select {
            case <-time.After(delay):
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
    }
    
    return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}

func calculateBackoff(attempt int, config RetryConfig) time.Duration {
    delay := config.BaseDelay * time.Duration(1<<uint(attempt))
    if delay > config.MaxDelay {
        delay = config.MaxDelay
    }
    // Add jitter
    jitter := time.Duration(rand.Int63n(int64(delay / 4)))
    return delay + jitter
}

func isRetryableError(err error) bool {
    if apiErr, ok := err.(*APIError); ok {
        return apiErr.StatusCode >= 500 || apiErr.StatusCode == 429
    }
    if netErr, ok := err.(net.Error); ok {
        return netErr.Temporary() || netErr.Timeout()
    }
    return false
}

Circuit Breaker

type CircuitBreaker struct {
    maxFailures  int
    resetTimeout time.Duration
    state        CircuitState
    failures     int
    lastFailTime time.Time
    mu           sync.RWMutex
}

type CircuitState int

const (
    StateClosed CircuitState = iota
    StateOpen
    StateHalfOpen
)

func NewCircuitBreaker(maxFailures int, resetTimeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        maxFailures:  maxFailures,
        resetTimeout: resetTimeout,
        state:        StateClosed,
    }
}

func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error {
    cb.mu.Lock()
    
    // Check if circuit should reset
    if cb.state == StateOpen && time.Since(cb.lastFailTime) > cb.resetTimeout {
        cb.state = StateHalfOpen
        cb.failures = 0
    }
    
    if cb.state == StateOpen {
        cb.mu.Unlock()
        return errors.New("circuit breaker is open")
    }
    
    cb.mu.Unlock()
    
    // Execute function
    err := fn()
    
    cb.mu.Lock()
    defer cb.mu.Unlock()
    
    if err != nil {
        cb.failures++
        cb.lastFailTime = time.Now()
        
        if cb.failures >= cb.maxFailures {
            cb.state = StateOpen
            tflog.Warn(ctx, "Circuit breaker opened", map[string]interface{}{
                "failures": cb.failures,
            })
        }
        return err
    }
    
    // Success
    if cb.state == StateHalfOpen {
        cb.state = StateClosed
        cb.failures = 0
        tflog.Info(ctx, "Circuit breaker closed")
    }
    
    return nil
}

Graceful Degradation

func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var state CacheResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    // Try to fetch from API
    cache, err := r.client.GetCache(ctx, state.ID.ValueString())
    if err != nil {
        if isNotFoundError(err) {
            // Resource deleted
            resp.State.RemoveResource(ctx)
            return
        }
        
        // Network or API error - log but continue with existing state
        tflog.Warn(ctx, "Failed to refresh state, using cached values", map[string]interface{}{
            "error":    err.Error(),
            "cache_id": state.ID.ValueString(),
        })
        
        // Add warning to diagnostics
        resp.Diagnostics.AddWarning(
            "Failed to Refresh State",
            fmt.Sprintf("Could not retrieve current state from API: %s. Using cached values. Please verify resource status manually.", err),
        )
        
        // Continue with existing state
        return
    }
    
    // Update with fresh data
    state.Status = types.StringValue(cache.Status)
    resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

Performance Optimization

Connection Pooling

func NewOptimizedClient(apiKey string) *Client {
    transport := &http.Transport{
        // Connection pool configuration
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        MaxConnsPerHost:     50,
        IdleConnTimeout:     90 * time.Second,
        
        // Timeouts
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
        
        // Keep-alive
        DisableKeepAlives: false,
        
        // Compression
        DisableCompression: false,
    }
    
    return &Client{
        httpClient: &http.Client{
            Transport: transport,
            Timeout:   30 * time.Second,
        },
    }
}

Caching Strategy

import "github.com/hashicorp/golang-lru/v2/expirable"

type CachedClient struct {
    client *Client
    cache  *expirable.LRU[string, *Cache]
}

func NewCachedClient(client *Client) *CachedClient {
    // LRU cache with TTL
    cache := expirable.NewLRU[string, *Cache](
        1000,              // max entries
        nil,               // no eviction callback
        5*time.Minute,     // TTL
    )
    
    return &CachedClient{
        client: client,
        cache:  cache,
    }
}

func (c *CachedClient) GetCache(ctx context.Context, id string) (*Cache, error) {
    // Check cache
    if cached, ok := c.cache.Get(id); ok {
        tflog.Trace(ctx, "Cache hit", map[string]interface{}{"id": id})
        return cached, nil
    }
    
    // Fetch from API
    cache, err := c.client.GetCache(ctx, id)
    if err != nil {
        return nil, err
    }
    
    // Store in cache
    c.cache.Add(id, cache)
    
    return cache, nil
}

// Invalidate cache on mutations
func (c *CachedClient) UpdateCache(ctx context.Context, id string, input *UpdateCacheInput) (*Cache, error) {
    cache, err := c.client.UpdateCache(ctx, id, input)
    if err != nil {
        return nil, err
    }
    
    // Invalidate cache entry
    c.cache.Remove(id)
    
    return cache, nil
}

Batch Operations

func (c *Client) BatchGetCaches(ctx context.Context, ids []string) (map[string]*Cache, error) {
    if len(ids) == 0 {
        return make(map[string]*Cache), nil
    }
    
    // Batch API request
    body, err := json.Marshal(map[string]interface{}{
        "ids": ids,
    })
    if err != nil {
        return nil, err
    }
    
    req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/caches/batch", bytes.NewReader(body))
    if err != nil {
        return nil, err
    }
    
    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var result struct {
        Caches []*Cache `json:"caches"`
    }
    
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }
    
    // Convert to map
    caches := make(map[string]*Cache, len(result.Caches))
    for _, cache := range result.Caches {
        caches[cache.ID] = cache
    }
    
    return caches, nil
}

Observability

Structured Logging

func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    // Add request context
    requestID := uuid.New().String()
    ctx = tflog.SetField(ctx, "request_id", requestID)
    ctx = tflog.SetField(ctx, "operation", "create")
    ctx = tflog.SetField(ctx, "resource_type", "cachecloud_cache")
    
    start := time.Now()
    defer func() {
        duration := time.Since(start)
        tflog.Info(ctx, "Operation completed", map[string]interface{}{
            "duration_ms": duration.Milliseconds(),
            "success":     !resp.Diagnostics.HasError(),
        })
    }()
    
    var plan CacheResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
    
    tflog.Info(ctx, "Creating cache", map[string]interface{}{
        "name":      plan.Name.ValueString(),
        "node_type": plan.NodeType.ValueString(),
    })
    
    // Implementation...
}

Metrics Collection

type Metrics struct {
    operationCounts   sync.Map // operation -> count
    operationDuration sync.Map // operation -> []duration
}

func (m *Metrics) RecordOperation(operation string, duration time.Duration, success bool) {
    // Increment count
    key := fmt.Sprintf("%s.%v", operation, success)
    val, _ := m.operationCounts.LoadOrStore(key, new(int64))
    atomic.AddInt64(val.(*int64), 1)
    
    // Record duration
    durations, _ := m.operationDuration.LoadOrStore(operation, &[]time.Duration{})
    durationList := durations.(*[]time.Duration)
    *durationList = append(*durationList, duration)
}

func (m *Metrics) GetSummary() map[string]interface{} {
    summary := make(map[string]interface{})
    
    m.operationCounts.Range(func(key, value interface{}) bool {
        summary[key.(string)] = atomic.LoadInt64(value.(*int64))
        return true
    })
    
    return summary
}

Error Handling

Comprehensive Error Types

type APIError struct {
    StatusCode int
    Code       string
    Message    string
    RequestID  string
}

func (e *APIError) Error() string {
    return fmt.Sprintf("API error %d (%s): %s (request: %s)", 
        e.StatusCode, e.Code, e.Message, e.RequestID)
}

type ValidationError struct {
    Field   string
    Value   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error for %s='%s': %s", 
        e.Field, e.Value, e.Message)
}

// Handle errors appropriately
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    cache, err := r.client.CreateCache(ctx, input)
    if err != nil {
        switch e := err.(type) {
        case *APIError:
            if e.StatusCode == 409 {
                resp.Diagnostics.AddError(
                    "Resource Already Exists",
                    fmt.Sprintf("A cache with name '%s' already exists", input.Name),
                )
            } else {
                resp.Diagnostics.AddError(
                    "API Error",
                    fmt.Sprintf("Failed to create cache: %s", e.Message),
                )
            }
        case *ValidationError:
            resp.Diagnostics.AddAttributeError(
                path.Root(e.Field),
                "Invalid Value",
                e.Message,
            )
        default:
            resp.Diagnostics.AddError(
                "Unexpected Error",
                fmt.Sprintf("An unexpected error occurred: %s", err),
            )
        }
        return
    }
}

Resource Lifecycle Management

Proper Cleanup

func (r *CacheResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    var state CacheResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    cacheID := state.ID.ValueString()
    
    // Delete with retry
    err := retry.Do(
        func() error {
            return r.client.DeleteCache(ctx, cacheID)
        },
        retry.Attempts(3),
        retry.Delay(time.Second),
        retry.OnRetry(func(n uint, err error) {
            tflog.Warn(ctx, "Retrying delete", map[string]interface{}{
                "attempt": n + 1,
                "error":   err.Error(),
            })
        }),
    )
    
    if err != nil {
        if isNotFoundError(err) {
            // Already deleted - success
            tflog.Info(ctx, "Cache already deleted")
            return
        }
        
        resp.Diagnostics.AddError(
            "Delete Failed",
            fmt.Sprintf("Failed to delete cache %s: %s", cacheID, err),
        )
        return
    }
    
    // Wait for deletion to complete
    if err := r.waitForDeletion(ctx, cacheID); err != nil {
        resp.Diagnostics.AddWarning(
            "Deletion Pending",
            fmt.Sprintf("Cache deletion initiated but not yet complete: %s", err),
        )
    }
    
    tflog.Info(ctx, "Cache deleted successfully", map[string]interface{}{
        "cache_id": cacheID,
    })
}

func (r *CacheResource) waitForDeletion(ctx context.Context, id string) error {
    timeout := time.After(5 * time.Minute)
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-timeout:
            return errors.New("timeout waiting for deletion")
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            _, err := r.client.GetCache(ctx, id)
            if isNotFoundError(err) {
                return nil // Deleted
            }
            if err != nil {
                return err
            }
        }
    }
}

Configuration Management

Provider Configuration

type providerModel struct {
    APIKey     types.String `tfsdk:"api_key"`
    Endpoint   types.String `tfsdk:"endpoint"`
    MaxRetries types.Int64  `tfsdk:"max_retries"`
    Timeout    types.Int64  `tfsdk:"timeout"`
    RateLimit  types.Float64 `tfsdk:"rate_limit"`
}

func (p *provider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "api_key": schema.StringAttribute{
                Required:  true,
                Sensitive: true,
                Description: "API key for authentication",
            },
            "endpoint": schema.StringAttribute{
                Optional:    true,
                Description: "Custom API endpoint",
                Validators: []validator.String{
                    stringvalidator.RegexMatches(
                        regexp.MustCompile(`^https://`),
                        "must use HTTPS",
                    ),
                },
            },
            "max_retries": schema.Int64Attribute{
                Optional:    true,
                Description: "Maximum number of API call retries",
                Validators: []validator.Int64{
                    int64validator.Between(0, 10),
                },
            },
            "timeout": schema.Int64Attribute{
                Optional:    true,
                Description: "API timeout in seconds",
                Validators: []validator.Int64{
                    int64validator.Between(10, 300),
                },
            },
            "rate_limit": schema.Float64Attribute{
                Optional:    true,
                Description: "Maximum requests per second",
                Validators: []validator.Float64{
                    float64validator.Between(1.0, 1000.0),
                },
            },
        },
    }
}

Key Takeaways

  • ✅ Never log sensitive data
  • ✅ Use HTTPS with strong TLS
  • ✅ Implement retry with exponential backoff
  • ✅ Add circuit breakers for failing services
  • ✅ Cache aggressively with TTL
  • ✅ Batch API calls when possible
  • ✅ Use structured logging extensively
  • ✅ Handle errors specifically
  • ✅ Implement proper cleanup
  • ✅ Configure timeouts appropriately

Congratulations!

You’ve learned essential production best practices for reliable provider operation!

Next Module: Appendix & Resources →


Previous Module: ← Debugging & Troubleshooting
Next Module: Appendix & Resources →

Last updated: July 3, 2026

Updated: