Configuration Sources
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:
- Terraform configuration (explicit in HCL)
- Environment variables (for CI/CD and automation)
- Configuration files (for complex setups)
- Defaults (sensible fallbacks)
Configuration Precedence
The standard precedence order (highest to lowest):
- Explicit Terraform configuration - Highest priority
- Environment variables - Second priority
- Configuration files - Third priority
- 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.
Previous: ← Client Configuration
Last updated: June 30, 2026