Configuration Sources

8 minute read

Configuration Sources Overview

Terraform providers should support configuration from multiple sources to provide flexibility and follow the principle of least surprise. Users expect to configure providers through:

  1. Terraform configuration (explicit in HCL)
  2. Environment variables (for CI/CD and automation)
  3. Configuration files (for complex setups)
  4. Defaults (sensible fallbacks)

Configuration Precedence

The standard precedence order (highest to lowest):

  1. Explicit Terraform configuration - Highest priority
  2. Environment variables - Second priority
  3. Configuration files - Third priority
  4. Defaults - Lowest priority
// Precedence implementation
func getValue(explicit types.String, envVar string, configFile string, defaultValue string) string {
    // 1. Explicit config (highest priority)
    if !explicit.IsNull() && explicit.ValueString() != "" {
        return explicit.ValueString()
    }
    
    // 2. Environment variable
    if envValue := os.Getenv(envVar); envValue != "" {
        return envValue
    }
    
    // 3. Config file
    if configFile != "" {
        return configFile
    }
    
    // 4. Default (lowest priority)
    return defaultValue
}

Explicit Configuration

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "api_key": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "API key for authentication",
        },
        "endpoint": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "API endpoint URL",
        },
        "region": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Default region",
        },
    },
}

Usage

provider "mycloud" {
  api_key  = "sk-abc123"      # Explicit - highest priority
  endpoint = "https://api.example.com"
  region   = "us-west-2"
}

Environment Variables

Naming Convention

Use a consistent prefix for all environment variables:

MYCLOUD_API_KEY
MYCLOUD_ENDPOINT
MYCLOUD_REGION
MYCLOUD_TIMEOUT

Pattern: <PROVIDER>_<ATTRIBUTE>

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // API Key: Explicit config or environment
    apiKey := config.APIKey.ValueString()
    if apiKey == "" {
        apiKey = os.Getenv("MYCLOUD_API_KEY")
    }
    
    // Endpoint: Explicit config, environment, or default
    endpoint := config.Endpoint.ValueString()
    if endpoint == "" {
        endpoint = os.Getenv("MYCLOUD_ENDPOINT")
    }
    if endpoint == "" {
        endpoint = "https://api.mycloud.com"
    }
    
    // Region: Explicit config, environment, or default
    region := config.Region.ValueString()
    if region == "" {
        region = os.Getenv("MYCLOUD_REGION")
    }
    if region == "" {
        region = "us-east-1"
    }
    
    // Validate required fields
    if apiKey == "" {
        resp.Diagnostics.AddError(
            "Missing API Key",
            "API key must be provided via api_key attribute or MYCLOUD_API_KEY environment variable",
        )
        return
    }
    
    // Create client...
}

Usage

# Set environment variables
export MYCLOUD_API_KEY="sk-abc123"
export MYCLOUD_ENDPOINT="https://api.example.com"
export MYCLOUD_REGION="us-west-2"

# Terraform uses environment variables
terraform plan
# No explicit config needed
provider "mycloud" {
  # All values from environment
}

Document Environment Variables

"api_key": schema.StringAttribute{
    Optional:            true,
    Sensitive:           true,
    MarkdownDescription: "API key for authentication. Can be set via `MYCLOUD_API_KEY` environment variable.",
},

Configuration Files

Support Config File

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "config_file": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Path to configuration file. Defaults to `~/.mycloud/config.json`",
        },
    },
}

Configuration File Format

JSON format:

{
  "api_key": "sk-abc123",
  "endpoint": "https://api.example.com",
  "region": "us-west-2",
  "timeout": 30,
  "max_retries": 3
}

YAML format:

api_key: sk-abc123
endpoint: https://api.example.com
region: us-west-2
timeout: 30
max_retries: 3

Loading Configuration File

type ConfigFile struct {
    APIKey     string `json:"api_key"`
    Endpoint   string `json:"endpoint"`
    Region     string `json:"region"`
    Timeout    int    `json:"timeout"`
    MaxRetries int    `json:"max_retries"`
}

func loadConfigFile(path string) (*ConfigFile, error) {
    // Expand home directory
    if strings.HasPrefix(path, "~/") {
        homeDir, err := os.UserHomeDir()
        if err != nil {
            return nil, err
        }
        path = filepath.Join(homeDir, path[2:])
    }
    
    // Read file
    data, err := os.ReadFile(path)
    if err != nil {
        if os.IsNotExist(err) {
            return &ConfigFile{}, nil // File doesn't exist, return empty config
        }
        return nil, err
    }
    
    // Parse JSON
    var config ConfigFile
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("failed to parse config file: %w", err)
    }
    
    return &config, nil
}

Implementation with Config File

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Load config file
    configFilePath := config.ConfigFile.ValueString()
    if configFilePath == "" {
        configFilePath = "~/.mycloud/config.json"
    }
    
    fileConfig, err := loadConfigFile(configFilePath)
    if err != nil {
        tflog.Warn(ctx, "Failed to load config file", map[string]any{
            "path":  configFilePath,
            "error": err.Error(),
        })
        fileConfig = &ConfigFile{}
    }
    
    // Apply precedence: explicit > env > file > default
    apiKey := config.APIKey.ValueString()
    if apiKey == "" {
        apiKey = os.Getenv("MYCLOUD_API_KEY")
    }
    if apiKey == "" {
        apiKey = fileConfig.APIKey
    }
    
    endpoint := config.Endpoint.ValueString()
    if endpoint == "" {
        endpoint = os.Getenv("MYCLOUD_ENDPOINT")
    }
    if endpoint == "" {
        endpoint = fileConfig.Endpoint
    }
    if endpoint == "" {
        endpoint = "https://api.mycloud.com"
    }
    
    // Similar for other fields...
    
    // Create client...
}

Usage

# Use custom config file
provider "mycloud" {
  config_file = "~/.mycloud/production.json"
}

# Or use default config file
provider "mycloud" {
  # Loads ~/.mycloud/config.json automatically
}

Profiles

Support Multiple Profiles

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "profile": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Configuration profile to use. Defaults to 'default'.",
        },
        "config_file": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Path to configuration file",
        },
    },
}

Config File with Profiles

{
  "profiles": {
    "default": {
      "api_key": "sk-default-key",
      "endpoint": "https://api.mycloud.com",
      "region": "us-east-1"
    },
    "production": {
      "api_key": "sk-prod-key",
      "endpoint": "https://api.mycloud.com",
      "region": "us-west-2"
    },
    "staging": {
      "api_key": "sk-staging-key",
      "endpoint": "https://staging.api.mycloud.com",
      "region": "us-east-1"
    }
  }
}

Loading Profile

type ConfigFileWithProfiles struct {
    Profiles map[string]ConfigFile `json:"profiles"`
}

func loadProfile(configPath string, profileName string) (*ConfigFile, error) {
    data, err := os.ReadFile(configPath)
    if err != nil {
        return nil, err
    }
    
    var configFile ConfigFileWithProfiles
    if err := json.Unmarshal(data, &configFile); err != nil {
        return nil, err
    }
    
    profile, exists := configFile.Profiles[profileName]
    if !exists {
        return nil, fmt.Errorf("profile %q not found", profileName)
    }
    
    return &profile, nil
}

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Determine profile
    profile := "default"
    if !config.Profile.IsNull() {
        profile = config.Profile.ValueString()
    } else if envProfile := os.Getenv("MYCLOUD_PROFILE"); envProfile != "" {
        profile = envProfile
    }
    
    // Load profile config
    configPath := config.ConfigFile.ValueString()
    if configPath == "" {
        configPath = "~/.mycloud/config.json"
    }
    
    fileConfig, err := loadProfile(configPath, profile)
    if err != nil {
        tflog.Debug(ctx, "Could not load profile", map[string]any{
            "profile": profile,
            "error":   err.Error(),
        })
        fileConfig = &ConfigFile{}
    }
    
    // Apply configuration with precedence...
}

Usage

# Use production profile
provider "mycloud" {
  profile = "production"
}

# Use staging profile
provider "mycloud" {
  profile = "staging"
}
# Or via environment variable
export MYCLOUD_PROFILE="production"
terraform plan

Complete Configuration Helper

Configuration Resolution Function

type ConfigSource struct {
    Explicit types.String
    EnvVar   string
    FileVal  string
    Default  string
}

func resolveString(source ConfigSource) string {
    // 1. Explicit config
    if !source.Explicit.IsNull() && source.Explicit.ValueString() != "" {
        return source.Explicit.ValueString()
    }
    
    // 2. Environment variable
    if source.EnvVar != "" {
        if envVal := os.Getenv(source.EnvVar); envVal != "" {
            return envVal
        }
    }
    
    // 3. Config file
    if source.FileVal != "" {
        return source.FileVal
    }
    
    // 4. Default
    return source.Default
}

func resolveInt(explicit types.Number, envVar string, fileVal int, defaultVal int) int {
    // Explicit
    if !explicit.IsNull() {
        val, _ := explicit.ValueBigFloat().Int64()
        return int(val)
    }
    
    // Environment
    if envVar != "" {
        if envStr := os.Getenv(envVar); envStr != "" {
            if val, err := strconv.Atoi(envStr); err == nil {
                return val
            }
        }
    }
    
    // File
    if fileVal != 0 {
        return fileVal
    }
    
    // Default
    return defaultVal
}

func resolveBool(explicit types.Bool, envVar string, fileVal bool, defaultVal bool) bool {
    // Explicit
    if !explicit.IsNull() {
        return explicit.ValueBool()
    }
    
    // Environment
    if envVar != "" {
        if envStr := os.Getenv(envVar); envStr != "" {
            if val, err := strconv.ParseBool(envStr); err == nil {
                return val
            }
        }
    }
    
    // File
    if fileVal {
        return fileVal
    }
    
    // Default
    return defaultVal
}

Usage in Configure

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Load config file if specified
    fileConfig := &ConfigFile{}
    if configPath := config.ConfigFile.ValueString(); configPath != "" {
        if fc, err := loadConfigFile(configPath); err == nil {
            fileConfig = fc
        }
    }
    
    // Resolve all configuration with precedence
    apiKey := resolveString(ConfigSource{
        Explicit: config.APIKey,
        EnvVar:   "MYCLOUD_API_KEY",
        FileVal:  fileConfig.APIKey,
        Default:  "",
    })
    
    endpoint := resolveString(ConfigSource{
        Explicit: config.Endpoint,
        EnvVar:   "MYCLOUD_ENDPOINT",
        FileVal:  fileConfig.Endpoint,
        Default:  "https://api.mycloud.com",
    })
    
    region := resolveString(ConfigSource{
        Explicit: config.Region,
        EnvVar:   "MYCLOUD_REGION",
        FileVal:  fileConfig.Region,
        Default:  "us-east-1",
    })
    
    timeout := resolveInt(
        config.Timeout,
        "MYCLOUD_TIMEOUT",
        fileConfig.Timeout,
        30,
    )
    
    maxRetries := resolveInt(
        config.MaxRetries,
        "MYCLOUD_MAX_RETRIES",
        fileConfig.MaxRetries,
        3,
    )
    
    // Validate required configuration
    if apiKey == "" {
        resp.Diagnostics.AddError(
            "Missing API Key",
            "API key must be provided via:\n"+
                "  - api_key attribute\n"+
                "  - MYCLOUD_API_KEY environment variable\n"+
                "  - config_file",
        )
        return
    }
    
    // Log resolved configuration (without secrets)
    tflog.Debug(ctx, "Resolved provider configuration", map[string]any{
        "endpoint":    endpoint,
        "region":      region,
        "timeout":     timeout,
        "max_retries": maxRetries,
    })
    
    // Create client with resolved config...
}

Best Practices

1. Always Support Environment Variables

// ✅ Good: Check environment
apiKey := config.APIKey.ValueString()
if apiKey == "" {
    apiKey = os.Getenv("MYCLOUD_API_KEY")
}

// ❌ Bad: Only explicit config
apiKey := config.APIKey.ValueString()

2. Provide Clear Error Messages

// ✅ Good: Shows all options
if apiKey == "" {
    resp.Diagnostics.AddError(
        "Missing API Key",
        "API key must be provided via:\n"+
            "  - api_key attribute\n"+
            "  - MYCLOUD_API_KEY environment variable\n"+
            "  - ~/.mycloud/config.json file",
    )
}

3. Document Precedence

"api_key": schema.StringAttribute{
    Optional:            true,
    Sensitive:           true,
    MarkdownDescription: "API key for authentication. Precedence: explicit config > MYCLOUD_API_KEY env var > config file.",
}

4. Use Consistent Naming

# ✅ Good: Consistent prefix
MYCLOUD_API_KEY
MYCLOUD_ENDPOINT
MYCLOUD_REGION

# ❌ Bad: Inconsistent
MYCLOUD_KEY
MC_ENDPOINT
REGION

5. Support Config Files for Complex Setups

// ✅ Good: Optional config file for complex configs
"config_file": schema.StringAttribute{
    Optional: true,
    MarkdownDescription: "Path to configuration file",
}

6. Log Configuration Sources

// ✅ Good: Log where values came from (for debugging)
tflog.Debug(ctx, "Configuration sources", map[string]any{
    "api_key_source": getSource(config.APIKey, "MYCLOUD_API_KEY", fileConfig.APIKey),
    "endpoint":       endpoint,
})

Configuration Examples

Example 1: Production Setup

# production.tf
provider "mycloud" {
  profile     = "production"
  config_file = "~/.mycloud/config.json"
}

Example 2: CI/CD Pipeline

# .gitlab-ci.yml or similar
export MYCLOUD_API_KEY="${CI_MYCLOUD_API_KEY}"
export MYCLOUD_REGION="us-west-2"
terraform apply
provider "mycloud" {
  # All values from environment
}

Example 3: Development

provider "mycloud" {
  api_key  = "sk-dev-key"  # Explicit for dev
  endpoint = "http://localhost:8080"
  region   = "local"
}

Example 4: Mixed Sources

provider "mycloud" {
  api_key = var.api_key         # From variable (CI/CD)
  region  = "us-west-2"         # Explicit
  # timeout from MYCLOUD_TIMEOUT env var
  # max_retries from config file
}

Key Takeaways

  • ✅ Support multiple configuration sources
  • ✅ Follow standard precedence: explicit > env > file > default
  • ✅ Always support environment variables for CI/CD
  • ✅ Use consistent naming for environment variables
  • ✅ Provide clear error messages listing all options
  • ✅ Document precedence in schema descriptions
  • ✅ Support config files for complex setups
  • ✅ Consider profile support for multiple environments
  • ✅ Log configuration sources for debugging
  • ✅ Validate required configuration clearly

What’s Next?

Now let’s learn about the shared client pattern for efficient resource access.

Next: Shared Client Pattern →


Previous: ← Client Configuration

Last updated: June 30, 2026

Updated: