AWS SSM Parameter Store¶
Overview¶
AWS Systems Manager (SSM) Parameter Store provides secure, hierarchical storage for configuration data and secrets management. This document explores the theoretical foundations and implementation patterns for using SSM Parameter Store in production Django applications running on AWS ECS, with LocalStack for local development.
Why SSM Parameter Store?¶
The Production Secrets Problem¶
Environment variables work well for local development, but production environments require:
- Centralized secrets management - Single source of truth for all environments
- Encryption at rest - Secrets stored encrypted, not in plain text
- Access control - IAM policies determine who/what can access secrets
- Audit logging - CloudTrail records all parameter access
- Version history - Parameter changes are tracked and reversible
- No filesystem storage - Secrets never touch disk in plain text
Theory: Production secrets should never be stored in environment variables set in container definitions or task definitions. They should be fetched at runtime from a secure store. SSM Parameter Store provides this with native AWS integration.
SSM vs AWS Secrets Manager¶
AWS offers two secrets management services. Understanding when to use each is important:
SSM Parameter Store: - Free for standard parameters (up to 10,000) - Simple key-value storage - Integrated with AWS Systems Manager - Good for: Configuration data, non-rotating secrets, feature flags - Lower cost for high-volume usage
AWS Secrets Manager: - Costs $0.40/secret/month + $0.05 per 10,000 API calls - Built-in secret rotation - Database credential management - Good for: Database passwords, rotating API keys, OAuth tokens - Better for secrets requiring automatic rotation
Theory: For small teams with Django applications on ECS, SSM Parameter Store is usually sufficient. The application handles secret rotation through deployment, and the cost savings are significant. Use Secrets Manager when you need automatic rotation or RDS integration.
Decision Matrix:
| Need | Use SSM Parameter Store | Use Secrets Manager |
|---|---|---|
| Static API keys | ✓ | - |
| Django SECRET_KEY | ✓ | - |
| Database password (manual rotation) | ✓ | - |
| Database password (auto rotation) | - | ✓ |
| OAuth tokens with refresh | - | ✓ |
| Feature flags | ✓ | - |
| High volume access (>100k/day) | ✓ | - |
Parameter Naming Conventions¶
Hierarchical Structure¶
SSM Parameter Store supports hierarchical naming using forward slashes. This creates a natural tree structure for organizing parameters.
Recommended Structure:
/environment/service/parameter-name
Examples:
/dev/django/DJANGO_SECRET_KEY
/dev/django/DATABASE_PASSWORD
/dev/auth0/CLIENT_SECRET
/prod/django/DJANGO_SECRET_KEY
/prod/django/DATABASE_PASSWORD
/prod/sendgrid/API_KEY
Theory: Hierarchical naming enables:
1. Path-based permissions - Grant access to all /dev/* or specific /prod/django/*
2. Bulk operations - Retrieve all parameters under a path
3. Clear organization - Immediately see environment and service ownership
4. Namespace isolation - Different services can use same parameter names
Naming Convention Patterns¶
Environment Prefix Pattern:
Pros: Simple, flat structure within environment Cons: All parameters for all services in one namespace
Service-Scoped Pattern:
/environment/service/PARAMETER_NAME
/dev/poseidon/DJANGO_SECRET_KEY
/dev/poseidon/AUTH0_CLIENT_SECRET
/dev/celery/BROKER_URL
/prod/poseidon/DJANGO_SECRET_KEY
Pros: Clear service ownership, can scope IAM per service Cons: More verbose, requires consistent service naming
Hybrid Pattern (Recommended):
/environment/PARAMETER_NAME # Shared parameters
/environment/service/PARAMETER_NAME # Service-specific parameters
/dev/DATABASE_PASSWORD # Shared database
/dev/poseidon/DJANGO_SECRET_KEY # Poseidon-specific
/dev/poseidon/AUTH0_CLIENT_SECRET # Poseidon-specific
/dev/celery/BROKER_URL # Celery-specific
Theory: Start with environment-scoped parameters for shared resources. Add service namespacing when multiple services need different values for the same parameter name. This balance keeps the structure simple while preventing conflicts.
Naming Best Practices¶
- Use all caps for parameter names - Matches environment variable convention
- Use underscores, not hyphens - Consistent with Python naming
- Be specific -
AUTH0_CLIENT_SECRETnotCLIENT_SECRET - Avoid redundancy -
/prod/django/DJANGO_SECRET_KEY→/prod/django/SECRET_KEY - Document exceptions - Some parameters may need different conventions
# Good naming
/prod/poseidon/SECRET_KEY
/prod/poseidon/AUTH0_CLIENT_ID
/prod/poseidon/SENDGRID_API_KEY
# Inconsistent naming
/prod/poseidon/secret-key # Lowercase, hyphens
/prod/poseidon/auth0ClientId # camelCase
/prod/poseidon/APIKey_SendGrid # Mixed conventions
Encryption at Rest (SecureString)¶
Parameter Types¶
SSM Parameter Store supports three parameter types:
- String: Plain text, no encryption
- StringList: Comma-separated values, no encryption
- SecureString: Encrypted at rest using AWS KMS
Theory: Use SecureString for anything sensitive. Use String for non-sensitive configuration. Never use String for secrets "because it's easier" - the encryption overhead is negligible.
SecureString Implementation¶
Creating SecureString Parameters:
# Using default AWS-managed KMS key
aws ssm put-parameter \
--name "/prod/django/SECRET_KEY" \
--value "your-secret-value" \
--type "SecureString" \
--description "Django SECRET_KEY for production"
# Using custom KMS key
aws ssm put-parameter \
--name "/prod/django/SECRET_KEY" \
--value "your-secret-value" \
--type "SecureString" \
--key-id "alias/my-custom-key" \
--description "Django SECRET_KEY for production"
Theory: AWS encrypts the parameter value before storing it. When you retrieve the parameter with WithDecryption=True, AWS decrypts it using KMS. The decrypted value never touches disk - it's encrypted in S3, encrypted in transit via TLS, and only decrypted in memory.
KMS Key Management¶
Default AWS-Managed Key:
- Key alias: alias/aws/ssm
- No additional cost
- AWS manages rotation
- Suitable for most use cases
Custom Customer-Managed Key (CMK): - You control rotation schedule - More granular access control - Additional cost ($1/month + API usage) - Required for cross-account access
Theory: Use the default AWS-managed key unless you have specific compliance requirements or need cross-account parameter sharing. The default key provides strong encryption (AES-256) with automatic rotation and no management overhead.
Encryption and Access Flow¶
sequenceDiagram
participant App as Django Application
participant SSM as SSM Parameter Store
participant KMS as AWS KMS
participant S3 as S3 (Storage)
App->>SSM: GetParameter(WithDecryption=true)
SSM->>S3: Retrieve encrypted value
S3-->>SSM: Return encrypted blob
SSM->>KMS: Decrypt(encrypted_blob)
KMS->>KMS: Verify IAM permissions
KMS-->>SSM: Return decrypted value
SSM-->>App: Return plain text parameter
Note over App,S3: TLS encrypts all network traffic
Note over S3: Values stored encrypted at rest
Note over App: Decrypted value only in memory
Theory: Encryption at rest protects against storage compromise. Even if an attacker gained access to the underlying S3 bucket, they couldn't decrypt parameters without KMS access. The application must have both SSM and KMS permissions.
IAM Policies for Access¶
Principle of Least Privilege¶
IAM policies should grant the minimum permissions required for the application to function.
ECS Task Role Pattern:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Resource": [
"arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/*"
]
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": [
"arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
]
}
]
}
Theory: This policy grants:
1. Read-only access to parameters under /prod/poseidon/*
2. Decrypt permission for the KMS key encrypting those parameters
3. No write access - tasks can't modify parameters
4. No access to other environments - /dev/* or /staging/* are blocked
Scope-Based Permissions¶
Path-Based Scoping:
{
"Effect": "Allow",
"Action": ["ssm:GetParameter*"],
"Resource": [
"arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/*",
"arn:aws:ssm:us-east-1:123456789012:parameter/prod/shared/*"
]
}
This grants access to:
- Service-specific parameters: /prod/poseidon/*
- Shared parameters: /prod/shared/*
Parameter-Specific Scoping:
{
"Effect": "Allow",
"Action": ["ssm:GetParameter"],
"Resource": [
"arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/SECRET_KEY",
"arn:aws:ssm:us-east-1:123456789012:parameter/prod/poseidon/DATABASE_PASSWORD"
]
}
This grants access to specific parameters only - maximum security, maximum maintenance.
Theory: Path-based scoping strikes the best balance for small teams. It's secure (can't access other environments), flexible (new parameters don't need IAM updates), and maintainable (one policy per service/environment combination).
Development vs Production IAM¶
Development Environment (more permissive):
- Access to /dev/* parameters
- Possibly /staging/* for testing
- Never /prod/* from development
Production Environment (most restrictive):
- Access only to /prod/service/*
- Read-only access
- Specific KMS key permissions
// Production task role - minimal permissions
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParametersByPath"],
"Resource": "arn:aws:ssm:*:*:parameter/prod/poseidon/*"
}
// Development task role - broader for debugging
{
"Effect": "Allow",
"Action": ["ssm:GetParameter*", "ssm:DescribeParameters"],
"Resource": "arn:aws:ssm:*:*:parameter/dev/*"
}
Theory: Production should have the absolute minimum permissions to run. Development can have broader permissions for debugging and experimentation. Never give production tasks access to development parameters or write access to any parameters.
Local Development with LocalStack¶
LocalStack SSM Simulation¶
LocalStack provides a local AWS cloud stack for development and testing. The SSM service runs at http://localhost:4566.
Theory: LocalStack enables developers to work with SSM Parameter Store locally without AWS credentials, costs, or network dependencies. It simulates the SSM API, allowing the same code to run locally and in production.
Configuration for LocalStack¶
Environment Variables:
# .env for local development
AWS_ENDPOINT_URL=http://localstack:4566
AWS_DEFAULT_REGION=us-east-1
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
Django Settings Integration:
# settings/development.py
import os
import boto3
# Configure boto3 for LocalStack
ssm_endpoint = os.environ.get('AWS_ENDPOINT_URL')
if ssm_endpoint:
# LocalStack configuration
ssm_client = boto3.client(
'ssm',
endpoint_url=ssm_endpoint,
region_name='us-east-1',
aws_access_key_id='test',
aws_secret_access_key='test'
)
else:
# Production configuration (uses IAM role)
ssm_client = boto3.client('ssm', region_name='us-east-1')
Theory: The application detects LocalStack via AWS_ENDPOINT_URL. When set, it uses LocalStack. When unset (production), it uses real AWS. Same application code, different runtime environment.
Initializing LocalStack Parameters¶
Real-world production applications need a mechanism to populate LocalStack with development parameters. This is typically done with an initialization script.
Script Pattern (based on zenith project):
#!/usr/bin/env python3
"""Initialize LocalStack SSM with development parameters."""
import boto3
def init_localstack_ssm():
ssm = boto3.client(
'ssm',
endpoint_url='http://localhost:4566',
region_name='us-east-1',
aws_access_key_id='test',
aws_secret_access_key='test'
)
# Parameters to create
parameters = {
'/dev/django/SECRET_KEY': 'dev-secret-key-not-for-production',
'/dev/django/DATABASE_PASSWORD': 'postgres',
'/dev/auth0/CLIENT_SECRET': 'dev-auth0-secret',
}
for name, value in parameters.items():
ssm.put_parameter(
Name=name,
Value=value,
Type='SecureString',
Overwrite=True
)
print(f"Created parameter: {name}")
if __name__ == '__main__':
init_localstack_ssm()
Theory: The initialization script creates development-safe values for all parameters the application needs. These values work for local development but are clearly not production secrets. Run this script after starting LocalStack to prepare the environment.
Production Parameter Export Pattern¶
A more sophisticated approach exports production parameter names (not values) and creates development equivalents:
Two-Phase Process:
- Export Production Structure (read-only, safe):
- List all production parameter names
- Capture metadata (type, description)
-
Save to local file for review
-
Initialize LocalStack (uses exported structure):
- Read parameter names from export
- Create development values (not production values)
- Optionally redact sensitive parameters
Implementation:
# Phase 1: Export production structure (safe, read-only)
def export_production_parameters():
"""Fetch production parameter names and metadata."""
prod_ssm = boto3.client('ssm', region_name='us-east-1')
# Get all parameter names
paginator = prod_ssm.get_paginator('describe_parameters')
parameters = []
for page in paginator.paginate():
for param in page['Parameters']:
parameters.append({
'Name': param['Name'],
'Type': param['Type'],
'Description': param.get('Description', ''),
})
# Save to file
with open('production-parameters.json', 'w') as f:
json.dump(parameters, f, indent=2)
return parameters
# Phase 2: Initialize LocalStack from export
def init_localstack_from_export():
"""Create LocalStack parameters from production structure."""
local_ssm = boto3.client(
'ssm',
endpoint_url='http://localhost:4566',
aws_access_key_id='test',
aws_secret_access_key='test'
)
with open('production-parameters.json') as f:
parameters = json.load(f)
for param in parameters:
# Use development-safe values
dev_value = f"[DEV-VALUE-{param['Name']}]"
local_ssm.put_parameter(
Name=param['Name'],
Value=dev_value,
Type=param['Type'],
Description=f"Development: {param['Description']}"
)
Theory: This two-phase approach ensures: 1. Production parameters are never exposed during export (names only, not values) 2. Development environment matches production structure 3. Missing parameters are caught early (not at runtime) 4. New parameters in production automatically sync to development
Docker Compose Integration¶
docker-compose.yml:
services:
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- SERVICES=ssm,kms,s3
- DEBUG=1
volumes:
- localstack-data:/var/lib/localstack
django:
build: .
environment:
- AWS_ENDPOINT_URL=http://localstack:4566
- AWS_DEFAULT_REGION=us-east-1
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
depends_on:
- localstack
Theory: Docker Compose orchestrates LocalStack alongside the application. The depends_on ensures LocalStack starts before Django. The shared network allows Django to reach LocalStack at http://localstack:4566.
Startup Scripts¶
Initialize LocalStack parameters automatically when containers start:
#!/bin/bash
# scripts/wait-for-localstack.sh
echo "Waiting for LocalStack to be ready..."
until curl -s http://localstack:4566/_localstack/health | grep -q "\"ssm\": \"available\""; do
sleep 1
done
echo "LocalStack ready, initializing SSM parameters..."
python scripts/init-localstack-ssm.py
Theory: LocalStack takes a few seconds to start. The health check endpoint reports when services are available. Only after SSM is ready should you create parameters. The startup script ensures proper ordering.
Parameter Versioning¶
Automatic Version Tracking¶
SSM Parameter Store automatically versions parameters when you update them:
# Create initial parameter (version 1)
aws ssm put-parameter \
--name "/prod/django/SECRET_KEY" \
--value "initial-secret" \
--type "SecureString"
# Update parameter (version 2)
aws ssm put-parameter \
--name "/prod/django/SECRET_KEY" \
--value "updated-secret" \
--type "SecureString" \
--overwrite
# Get current version (version 2)
aws ssm get-parameter \
--name "/prod/django/SECRET_KEY" \
--with-decryption
# Get specific version
aws ssm get-parameter \
--name "/prod/django/SECRET_KEY:1" \
--with-decryption
Theory: Versioning provides an audit trail and rollback capability. If a parameter update breaks production, you can immediately retrieve the previous version. Every update increments the version number automatically.
Version Labels¶
You can also apply labels to specific versions:
# Label version 3 as "stable"
aws ssm label-parameter-version \
--name "/prod/django/SECRET_KEY" \
--parameter-version 3 \
--labels "stable"
# Get parameter by label
aws ssm get-parameter \
--name "/prod/django/SECRET_KEY:stable" \
--with-decryption
Theory: Labels provide semantic meaning to versions. You might label the current production version as stable, pre-deployment versions as staging, or known-good versions as verified. This is useful for staged rollouts or blue-green deployments.
Version Retention¶
SSM retains up to 100 versions of a parameter. Older versions are automatically purged.
Theory: 100 versions is sufficient for most use cases. If you update a parameter daily, that's over 3 months of history. For critical parameters, document the current version in your runbooks or deployment scripts.
Parameter Retrieval Patterns¶
Python Implementation¶
Basic Retrieval:
import boto3
ssm = boto3.client('ssm', region_name='us-east-1')
def get_parameter(name: str) -> str:
"""Retrieve a single parameter value."""
response = ssm.get_parameter(
Name=name,
WithDecryption=True
)
return response['Parameter']['Value']
# Usage
secret_key = get_parameter('/prod/django/SECRET_KEY')
Theory: WithDecryption=True is essential for SecureString parameters. Without it, you get the encrypted blob, not the plain text value.
Batch Retrieval¶
Retrieving parameters individually is slow. Batch retrieval is more efficient:
def get_parameters(names: list[str]) -> dict[str, str]:
"""Retrieve multiple parameters in one call."""
response = ssm.get_parameters(
Names=names,
WithDecryption=True
)
# Convert to dict
parameters = {
param['Name']: param['Value']
for param in response['Parameters']
}
# Check for invalid parameters
invalid = response.get('InvalidParameters', [])
if invalid:
raise ValueError(f"Invalid parameters: {invalid}")
return parameters
# Usage
params = get_parameters([
'/prod/django/SECRET_KEY',
'/prod/django/DATABASE_PASSWORD',
'/prod/auth0/CLIENT_SECRET',
])
Theory: get_parameters() retrieves up to 10 parameters in a single API call. This reduces latency (one round trip vs multiple) and cost (fewer API calls).
Path-Based Retrieval¶
Retrieve all parameters under a path:
def get_parameters_by_path(path: str) -> dict[str, str]:
"""Retrieve all parameters under a path."""
paginator = ssm.get_paginator('get_parameters_by_path')
parameters = {}
for page in paginator.paginate(
Path=path,
Recursive=True,
WithDecryption=True
):
for param in page['Parameters']:
# Strip path prefix from name
key = param['Name'].replace(f"{path}/", "")
parameters[key] = param['Value']
return parameters
# Usage
params = get_parameters_by_path('/prod/django')
# Returns: {'SECRET_KEY': '...', 'DATABASE_PASSWORD': '...'}
Theory: Path-based retrieval is convenient for loading all parameters for a service. The Recursive=True option traverses nested paths. Be careful with broad paths - they can retrieve many parameters and increase startup time.
Caching Strategy¶
Problem: Fetching parameters on every request is slow and expensive.
Solution: Cache parameters in application memory at startup.
class ParameterCache:
"""Cache SSM parameters in memory."""
def __init__(self):
self._cache = {}
self._ssm = boto3.client('ssm', region_name='us-east-1')
def load_parameters(self, names: list[str]) -> None:
"""Load parameters into cache at startup."""
response = self._ssm.get_parameters(
Names=names,
WithDecryption=True
)
for param in response['Parameters']:
self._cache[param['Name']] = param['Value']
def get(self, name: str) -> str:
"""Get parameter from cache."""
if name not in self._cache:
raise KeyError(f"Parameter {name} not in cache")
return self._cache[name]
# Initialize at application startup
param_cache = ParameterCache()
param_cache.load_parameters([
'/prod/django/SECRET_KEY',
'/prod/django/DATABASE_PASSWORD',
])
# Use throughout application
SECRET_KEY = param_cache.get('/prod/django/SECRET_KEY')
Theory: Load parameters once at startup, then serve from memory. This provides: 1. Fast access - No network latency after initial load 2. Lower cost - Fewer SSM API calls 3. Fail-fast - Missing parameters crash at startup, not during requests 4. Consistent values - Parameters don't change mid-request
Trade-off: Parameter updates require application restart. This is acceptable for most Django deployments where configuration changes trigger redeployment anyway.
Production Implementation Pattern¶
Real-world implementation from the zenith project:
# poseidon/commons/config/ps_config.py
import boto3
from botocore.exceptions import ClientError
import logging
logger = logging.getLogger(__name__)
class SSMParameterManager:
"""Manages SSM parameter retrieval with caching."""
def __init__(self, ssm_client=None):
self.ssm = ssm_client or boto3.client('ssm', region_name='us-east-1')
def get_parameter(self, parameter_name: str) -> str:
"""Retrieve parameter from SSM."""
try:
response = self.ssm.get_parameter(
Name=parameter_name,
WithDecryption=True
)
return response['Parameter']['Value']
except ClientError as e:
if e.response['Error']['Code'] == 'ParameterNotFound':
raise ValueError(f"Parameter '{parameter_name}' not found in SSM.")
else:
logger.exception(f"Failed to retrieve parameter '{parameter_name}'")
raise
class PlanstoneConfig:
"""Configuration manager for production settings."""
# Whitelist of allowed parameters
_params = [
'DJANGO_SECRET_KEY',
'DATABASE_PASSWORD',
'AUTH0_CLIENT_SECRET',
'SENDGRID_API_KEY',
# ... more parameters
]
def __init__(self, ssm_client=None):
self.ssm_manager = SSMParameterManager(ssm_client=ssm_client)
def get_param(self, param_name: str) -> str:
"""Get parameter with validation."""
if param_name not in self._params:
raise ValueError(f"Unknown parameter: {param_name}")
return self.ssm_manager.get_parameter(param_name)
# Global instance
pconfig = PlanstoneConfig()
Usage in Settings:
# settings/production.py
from poseidon.commons.config.ps_config import pconfig
SECRET_KEY = pconfig.get_param('DJANGO_SECRET_KEY')
DATABASE_PASSWORD = pconfig.get_param('DATABASE_PASSWORD')
Theory: This pattern provides: 1. Parameter validation - Only whitelisted parameters can be retrieved 2. Error handling - Clear messages for missing parameters 3. Testability - Can inject mock SSM client 4. Centralization - All SSM access goes through one class 5. Logging - Failed retrievals are logged for debugging
Mermaid Diagrams¶
SSM Parameter Store Architecture¶
graph TB
subgraph "Development (LocalStack)"
D1[Docker Compose] --> D2[LocalStack Container]
D2 --> D3[SSM Mock Service]
D4[Django App] --> D3
D5[Init Script] --> D3
end
subgraph "Production (AWS)"
P1[ECS Task] --> P2[IAM Task Role]
P2 --> P3[SSM Parameter Store]
P3 --> P4[KMS Encryption]
P5[S3 Storage] --> P4
end
style D1 fill:#E3F2FD
style P1 fill:#FFEBEE
Parameter Access Flow¶
sequenceDiagram
participant App as Django Application
participant Cache as Parameter Cache
participant SSM as SSM Parameter Store
participant IAM as IAM Service
Note over App,SSM: Application Startup
App->>Cache: Initialize parameter cache
Cache->>IAM: Verify task role permissions
IAM-->>Cache: Permissions validated
Cache->>SSM: GetParameters(batch)
SSM-->>Cache: Return decrypted values
Cache->>Cache: Store in memory
Note over App,SSM: Request Processing
App->>Cache: Get parameter
Cache-->>App: Return from memory (fast)
Note over App: No SSM calls during requests
Environment-Based Configuration Decision¶
graph TD
A[Application Starts] --> B{Environment?}
B -->|Development| C[Check AWS_ENDPOINT_URL]
B -->|Production| D[Use IAM Role]
C --> E{LocalStack URL set?}
E -->|Yes| F[Connect to LocalStack]
E -->|No| G[Fall back to .env]
F --> H[Load from LocalStack SSM]
G --> I[Load from environment vars]
D --> J[Load from AWS SSM]
H --> K[Application Ready]
I --> K
J --> K
style K fill:#90EE90
Related Documentation¶
- Environment Variables - Local development configuration
- Secrets Management - Overall security strategy
- Django Settings - Settings file organization
Next Steps¶
- Set up LocalStack in your Docker Compose configuration
- Create an initialization script to populate LocalStack SSM parameters
- Implement a parameter cache class for production
- Configure IAM policies for your ECS task roles
- Document your parameter naming convention
- Create a runbook for parameter rotation procedures
SSM Best Practices
- Use hierarchical naming with environment prefixes
- Always use SecureString for secrets
- Cache parameters at startup, not per-request
- Use path-based IAM policies for flexibility
- Initialize LocalStack automatically in development
Common Pitfalls
- Fetching parameters on every request (slow, expensive)
- Using String type for secrets (no encryption)
- Forgetting WithDecryption=True (returns encrypted blob)
- Over-scoping IAM permissions (granting access to all parameters)
- Not initializing LocalStack in development (runtime failures)
Production Considerations
- Never log parameter values, even in debug mode
- Rotate secrets regularly and track rotation in parameter descriptions
- Monitor SSM API costs - batch retrievals are more efficient
- Use parameter versioning for rollback capability
- Ensure ECS tasks have both SSM and KMS permissions