Production Best Practices
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