AWS Resolvers¶
When running applications on AWS, you often need to fetch configuration from AWS services like SSM Parameter Store, CloudFormation stacks, or S3 buckets. HoloConf provides AWS-specific resolvers to make this seamless.
Installation¶
AWS resolvers are distributed separately to keep the core library lean:
AWS resolvers are automatically discovered when you import holoconf:
SSM Parameter Store¶
AWS Systems Manager Parameter Store is perfect for storing configuration and secrets. Let's see how to use it.
First, let's try to reference an SSM parameter:
Automatic Decryption¶
SSM parameters are automatically decrypted if they use AWS KMS encryption. You don't need to do anything special:
HoloConf automatically calls SSM with WithDecryption=true, so you get the decrypted value.
Automatic Sensitivity Detection¶
SSM SecureString parameters are automatically marked as sensitive and redacted in dumps:
import holoconf
config = holoconf.Config.load("config.yaml")
# Sensitive values are automatically redacted
print(config.to_yaml(redact=True))
# password: '[REDACTED]'
# But you can still access the actual value
password = config.password
print(f"Password length: {len(password)}")
# Password length: 20
If you want to override sensitivity detection, you can do so explicitly:
# Force sensitivity even for String parameters
debug_token: ${ssm:/myapp/dev/token,sensitive=true}
# Disable sensitivity for SecureString (not recommended!)
public_value: ${ssm:/myapp/public-key,sensitive=false}
Handling Missing Parameters¶
What happens if a parameter doesn't exist?
Provide a default for optional parameters:
Now if the parameter doesn't exist, it uses 30 instead of erroring.
Cross-Region Parameters¶
By default, SSM parameters are fetched from your configured AWS region. To fetch from a different region:
# Fetch from us-west-2, even if default region is us-east-1
west_config: ${ssm:/shared/config,region=us-west-2}
AWS Secrets Manager Integration¶
SSM provides a special path prefix to access Secrets Manager:
# Access Secrets Manager secret via SSM
db_creds: ${ssm:/aws/reference/secretsmanager/myapp/db-credentials}
This is convenient because you can use the same resolver for both SSM Parameter Store and Secrets Manager.
Authentication and Credentials¶
SSM resolvers use the standard AWS credential chain:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - AWS profile from
~/.aws/credentials - IAM instance profile (when running on EC2)
- ECS task role (when running in ECS)
To use a specific profile:
Or set the environment variable:
CloudFormation Outputs¶
When you deploy infrastructure with CloudFormation, you often need to reference stack outputs in your application configuration. The cfn resolver makes this easy.
Let's say you have a CloudFormation stack called myapp-infrastructure with these outputs:
DatabaseEndpoint- The database hostCacheEndpoint- The Redis cache hostApiUrl- The API endpoint
Reference them in your config:
database:
host: ${cfn:myapp-infrastructure.DatabaseEndpoint}
cache:
host: ${cfn:myapp-infrastructure.CacheEndpoint}
api:
url: ${cfn:myapp-infrastructure.ApiUrl}
Syntax¶
The CloudFormation resolver uses this syntax:
For example:
This fetches the ApiEndpoint output from the myapp-prod stack.
Handling Missing Stacks or Outputs¶
What if the stack doesn't exist?
Or if the output key doesn't exist:
Provide a default for optional outputs:
Cross-Region Stacks¶
To reference a stack in a different region:
S3 Objects¶
For larger configuration files or shared team configurations, you can store them in S3 and reference them with the s3 resolver.
Let's say you have a shared configuration file in S3:
Reference it in your config:
Automatic Format Detection¶
S3 objects are automatically parsed based on file extension:
.json- Parsed as JSON.yaml,.yml- Parsed as YAML.txt,.pem, or no extension - Returned as plain text
# Parses as JSON
api_config: ${s3:my-bucket/config/api.json}
# Parses as YAML
db_config: ${s3:my-bucket/config/database.yaml}
# Returns as plain text
certificate: ${s3:my-bucket/certs/server.pem}
S3 URI Syntax¶
You can use either format:
# Without s3:// prefix (recommended)
config: ${s3:my-bucket/path/to/file.json}
# With s3:// prefix (also works)
config: ${s3:s3://my-bucket/path/to/file.json}
Both work identically.
Handling Missing Objects¶
What if the S3 object doesn't exist?
Provide a default:
Versioned Objects¶
To fetch a specific version of an S3 object:
This is useful for:
- Rolling back to a previous configuration
- Ensuring consistent config across deployments
- Auditing configuration changes
Authentication and Permissions¶
S3 resolvers use the same AWS credential chain as SSM resolvers. Your credentials need these permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-config-bucket/*"
}
]
}
Configuration API¶
When you need to set defaults for AWS resolvers across your application, the configuration API provides a two-tier system: global configuration that applies to all AWS services, and service-specific configuration for fine-grained control.
Why Use the Configuration API?¶
Before diving into the API, let's understand when and why you'd use it:
- Testing with moto or LocalStack - Point AWS services to local endpoints for integration tests
- Multi-region applications - Set a default region without adding
region=to every resolver call - Environment-based profiles - Use different AWS profiles for dev, staging, and production
- Test isolation - Clean up configuration between test runs
Global Configuration¶
Set defaults that apply to all AWS resolvers:
Now all AWS resolvers will use us-east-1 and the prod profile by default:
# All three use us-east-1 and prod profile
database:
password: ${ssm:/myapp/db-password}
endpoint: ${cfn:my-stack.DatabaseEndpoint}
schema: ${s3:my-bucket/schema.sql}
Service-Specific Configuration¶
Override global defaults for individual services. This is particularly useful for setting custom endpoints when testing with moto or LocalStack:
import holoconf_aws
# Configure S3 to use LocalStack
holoconf_aws.s3(
endpoint="http://localhost:4566",
region="us-west-2" # Can override global region
)
# Configure SSM separately
holoconf_aws.ssm(
endpoint="http://localhost:4566",
profile="testing" # Can override global profile
)
# Configure CloudFormation
holoconf_aws.cfn(
endpoint="http://localhost:4566"
)
use holoconf_aws;
// Configure S3 for LocalStack
holoconf_aws::configure_s3(
Some("http://localhost:4566".to_string()),
Some("us-west-2".to_string()),
None
);
// Configure SSM separately
holoconf_aws::configure_ssm(
Some("http://localhost:4566".to_string()),
None,
Some("testing".to_string())
);
// Configure CloudFormation
holoconf_aws::configure_cfn(
Some("http://localhost:4566".to_string()),
None,
None
);
Configuration Precedence¶
Configuration follows a four-level precedence chain from highest to lowest priority:
- Resolver kwargs - Explicit overrides in your config file
- Service configuration - Set via
holoconf_aws.s3(),ssm(), orcfn() - Global configuration - Set via
holoconf_aws.configure() - AWS SDK defaults - Environment variables, credentials file, IAM roles
Here's how precedence works in practice:
# Uses us-west-2 (service config overrides global)
config: ${s3:bucket/config.yaml}
# Uses eu-west-1 (resolver kwargs override everything)
europe: ${s3:bucket/eu-config.yaml,region=eu-west-1}
# Uses us-east-1 (global config, no SSM-specific override)
password: ${ssm:/myapp/password}
# 4. AWS SDK default (no configuration set)
# Falls back to AWS_REGION environment variable or ~/.aws/config
Additive Configuration¶
Configuration calls are additive - passing None leaves existing values unchanged:
import holoconf_aws
# Set both region and profile
holoconf_aws.configure(region="us-east-1", profile="prod")
# Later, update only the region - profile remains "prod"
holoconf_aws.configure(region="us-west-2")
# Or update only the profile - region remains "us-west-2"
holoconf_aws.configure(profile="staging")
use holoconf_aws;
// Set both region and profile
holoconf_aws::configure(
Some("us-east-1".to_string()),
Some("prod".to_string())
);
// Update only region - profile remains "prod"
holoconf_aws::configure(
Some("us-west-2".to_string()),
None
);
// Update only profile - region remains "us-west-2"
holoconf_aws::configure(
None,
Some("staging".to_string())
);
Resetting Configuration¶
To clear all configuration and start fresh, use reset():
The reset() function is particularly useful for test isolation - it clears both configuration and the internal AWS client cache.
Real-World Example: Testing with moto¶
Let's see how to use the configuration API for testing with moto, the AWS service mocking library:
import pytest
import holoconf
import holoconf_aws
from moto import mock_aws
@pytest.fixture
def aws_config():
"""Configure AWS resolvers for testing."""
# Point all AWS services to moto's mock endpoints
holoconf_aws.configure(region="us-east-1")
# Start moto mock
with mock_aws():
# Set up test data
import boto3
ssm = boto3.client("ssm", region_name="us-east-1")
ssm.put_parameter(
Name="/myapp/db-password",
Value="test-password",
Type="SecureString"
)
yield
# Clean up after test
holoconf_aws.reset()
def test_config_with_ssm(aws_config):
"""Test that SSM parameters are resolved correctly."""
config = holoconf.Config.loads("""
database:
password: ${ssm:/myapp/db-password}
""")
assert config.database.password == "test-password"
use holoconf_core::Config;
use holoconf_aws;
#[test]
fn test_config_with_localstack() {
// Configure for LocalStack
holoconf_aws::configure(
Some("us-east-1".to_string()),
None
);
holoconf_aws::configure_ssm(
Some("http://localhost:4566".to_string()),
None,
None
);
// Register resolvers
holoconf_aws::register_all();
// Load config that uses SSM
let config = Config::from_yaml_str(r#"
database:
password: ${ssm:/myapp/db-password}
"#).unwrap();
// ... test assertions ...
// Clean up
holoconf_aws::reset();
}
Real-World Example: Multi-Region Application¶
Here's how to handle an application deployed in multiple AWS regions:
# config.yaml - no need to specify region on every resolver
database:
host: ${ssm:/myapp/db-host}
password: ${ssm:/myapp/db-password}
cache:
endpoint: ${cfn:myapp-infra.CacheEndpoint}
feature_flags: ${s3:myapp-config/features.yaml}
When you deploy to us-west-2, just set AWS_REGION=us-west-2 and all resolvers automatically use the correct region.
Real-World Example: Environment-Based Profiles¶
Use different AWS profiles for different environments:
import os
import holoconf
import holoconf_aws
# Configure based on environment
env = os.environ.get("ENV", "dev")
if env == "dev":
holoconf_aws.configure(profile="dev", region="us-east-1")
elif env == "staging":
holoconf_aws.configure(profile="staging", region="us-east-1")
elif env == "prod":
holoconf_aws.configure(profile="prod", region="us-east-1")
# Load config - uses the appropriate profile
config = holoconf.Config.load("config.yaml")
use holoconf_core::Config;
use holoconf_aws;
use std::env;
// Register AWS resolvers
holoconf_aws::register_all();
// Configure based on environment
let environment = env::var("ENV").unwrap_or_else(|_| "dev".to_string());
match environment.as_str() {
"dev" => holoconf_aws::configure(
Some("us-east-1".to_string()),
Some("dev".to_string())
),
"staging" => holoconf_aws::configure(
Some("us-east-1".to_string()),
Some("staging".to_string())
),
"prod" => holoconf_aws::configure(
Some("us-east-1".to_string()),
Some("prod".to_string())
),
_ => {}
}
// Load config - uses the appropriate profile
let config = Config::load("config.yaml")?;
AWS Authentication Summary¶
All AWS resolvers (ssm, cfn, s3) use the standard AWS credential chain:
-
Environment variables:
-
AWS profile from
~/.aws/credentials: -
IAM instance profile (when running on EC2)
-
ECS task role (when running in ECS/Fargate)
-
IRSA (IAM Roles for Service Accounts) (when running in EKS)
You can also specify region and profile per-resolver:
# Different regions for different parameters
east_db: ${ssm:/myapp/db-host,region=us-east-1}
west_db: ${ssm:/myapp/db-host,region=us-west-2}
# Different profiles for different accounts
prod_config: ${ssm:/prod/config,profile=prod-account}
shared_config: ${ssm:/shared/config,profile=shared-account}
Performance Considerations¶
Caching¶
AWS resolvers cache values for the lifetime of the Config object to avoid repeated API calls:
To get fresh values, reload the config:
Lazy Resolution¶
Like all resolvers, AWS resolvers are lazy - they only execute when you access the value:
This means you only pay for the API calls you actually need.
Batch Optimization¶
For SSM parameters, consider using parameter hierarchies to reduce API calls:
# Instead of many individual parameters:
db_host: ${ssm:/myapp/prod/db/host}
db_port: ${ssm:/myapp/prod/db/port}
db_name: ${ssm:/myapp/prod/db/name}
# Store as structured data in one parameter:
database: ${ssm:/myapp/prod/database}
Then store a JSON value in SSM:
aws ssm put-parameter \
--name /myapp/prod/database \
--type SecureString \
--value '{"host":"prod-db.example.com","port":5432,"name":"myapp"}'
One API call instead of three!
Quick Reference¶
| Resolver | Syntax | Description | Example |
|---|---|---|---|
ssm |
${ssm:/path} |
SSM Parameter Store | ${ssm:/myapp/prod/db-password} |
cfn |
${cfn:Stack.Output} |
CloudFormation output | ${cfn:myapp-stack.DatabaseEndpoint} |
s3 |
${s3:bucket/key} |
S3 object content | ${s3:my-bucket/config.json} |
All AWS resolvers support:
default=value- Fallback if not foundsensitive=true/false- Override sensitivity detectionregion=name- Override AWS region
SSM additionally supports:
profile=name- AWS profile for credentials- Automatic access to Secrets Manager via
/aws/reference/secretsmanager/prefix
S3 additionally supports:
version_id=id- Fetch specific object version
What You've Learned¶
You now understand:
- Installing and registering AWS resolvers
- Fetching parameters from SSM Parameter Store with
${ssm:/path} - Automatic decryption and sensitivity detection for SSM
- Referencing CloudFormation stack outputs with
${cfn:Stack.Output} - Including S3 object content with
${s3:bucket/key} - Cross-region and cross-account access
- AWS authentication and credential chain
- Configuration API for setting global and service-specific defaults
- Precedence chain for configuration (kwargs > service > global > SDK)
- Test isolation with
reset()for cleaning up between tests - Caching and performance optimization
Next Steps¶
- Custom Resolvers - Write your own resolvers for custom data sources
- Core Resolvers - Environment variables, file includes, HTTP fetching
See Also¶
- ADR-002 Resolver Architecture - Technical design
- ADR-019 Resolver Extension Packages - Extension architecture
- FEAT-007 AWS Resolvers - Full specification