Secrets Manager vs SSM Parameter Store: Rotation, Retrieval, and When to Use Each
Secrets Manager and SSM Parameter Store both store secrets, but they solve different problems. Secrets Manager supports automatic rotation with Lambda. Parameter Store is cheaper for configuration values that don't rotate. Understanding retrieval patterns — SDK, environment injection, Terraform — prevents secrets from landing in plaintext where they shouldn't.
Secrets Manager vs SSM Parameter Store
Both services encrypt and store secrets. The key differences:
| | Secrets Manager | SSM Parameter Store | |---|---|---| | Cost | $0.40/secret/month + $0.05/10K API calls | Free (Standard), $0.05/advanced parameter | | Automatic rotation | Yes (built-in or custom Lambda) | No | | Cross-account sharing | Yes (resource-based policy) | Yes (via GetParameter cross-account) | | Versioning | Yes (AWSCURRENT, AWSPREVIOUS) | Yes (parameter history) | | Max size | 65KB | 4KB (Standard), 8KB (Advanced) | | Secret types | Arbitrary JSON, RDS credentials | String, StringList, SecureString |
Automatic rotation: how Secrets Manager handles credential refresh
ConceptAWS Secrets ManagerSecrets Manager rotation uses a Lambda function to update the secret in both Secrets Manager and the backend system (database, API, service). AWS provides managed Lambda rotation functions for RDS, Redshift, and ElastiCache. You write custom Lambda functions for other services.
Prerequisites
- AWS Lambda
- IAM roles
- RDS or other credential-based services
Key Points
- Rotation stages: createNewVersion → setNewVersion → testNewVersion → finishSecret.
- During rotation, AWSCURRENT and AWSPENDING versions exist simultaneously — applications retrieving AWSCURRENT are unaffected.
- RDS Proxy can read AWSCURRENT dynamically — pool connections survive rotation without application restart.
- Rotation schedule: cron expression or number of days. Enable rotation with 'rotate immediately' to test.
Storing and retrieving secrets
import boto3
import json
# Retrieve a secret at runtime (not at startup — fetched on demand with caching)
secrets_client = boto3.client('secretsmanager', region_name='us-east-1')
def get_db_credentials():
response = secrets_client.get_secret_value(
SecretId='production/db/postgres'
)
return json.loads(response['SecretString'])
def handler(event, context):
creds = get_db_credentials()
# Use creds['username'] and creds['password']
For Lambda functions calling Secrets Manager on every invocation, cache the result outside the handler:
import boto3
import json
import os
secrets_client = boto3.client('secretsmanager')
# Module-level cache — persists between warm invocations
_db_creds = None
def get_db_credentials():
global _db_creds
if _db_creds is None:
response = secrets_client.get_secret_value(
SecretId=os.environ['DB_SECRET_ARN']
)
_db_creds = json.loads(response['SecretString'])
return _db_creds
The cache is invalidated on cold start (new execution environment). During rotation, the cached secret eventually becomes stale — add a TTL to the cache and re-fetch periodically:
import time
_db_creds = None
_cache_time = 0
CACHE_TTL = 300 # refresh every 5 minutes
def get_db_credentials():
global _db_creds, _cache_time
if _db_creds is None or time.time() - _cache_time > CACHE_TTL:
response = secrets_client.get_secret_value(
SecretId=os.environ['DB_SECRET_ARN']
)
_db_creds = json.loads(response['SecretString'])
_cache_time = time.time()
return _db_creds
Terraform: reading secrets without printing them
Reading a secret version in Terraform:
data "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.arn
}
locals {
db_creds = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}
# Use the value without exposing it in terraform output
resource "aws_rds_cluster" "main" {
master_username = local.db_creds["username"]
master_password = local.db_creds["password"]
}
Security warning: Terraform stores retrieved secret values in state. Use nonsensitive() carefully:
# Don't do this in production — writes plaintext to terminal and CI logs
output "db_password" {
value = nonsensitive(local.db_creds["password"])
sensitive = false
}
# Do this instead — mark as sensitive so Terraform redacts it from output
output "db_password" {
value = local.db_creds["password"]
sensitive = true # redacted in terminal output
}
Sensitive values are still stored in Terraform state in plaintext — use remote state with encryption (S3 + SSE-KMS) and restrict access to the state file.
⚠ECS task secret injection: avoid environment variables
A common but insecure pattern is reading a secret in the ECS task definition and injecting it as an environment variable:
# Insecure: secret value visible in ECS task definition
container_definitions = jsonencode([{
name = "app"
environment = [
{ name = "DB_PASSWORD", value = "my-plaintext-password" } # don't do this
]
}])
Instead, reference Secrets Manager ARNs directly in the task definition. ECS retrieves the secret at task startup and injects it:
container_definitions = jsonencode([{
name = "app"
secrets = [
{
name = "DB_PASSWORD"
valueFrom = "${aws_secretsmanager_secret.db.arn}:password::"
# Format: <secret-arn>:<json-key>::
},
{
name = "DB_USERNAME"
valueFrom = "${aws_secretsmanager_secret.db.arn}:username::"
}
]
}])
ECS injects these as environment variables in the container, but the values never appear in the task definition JSON stored in the ECS console or API. The task execution role needs secretsmanager:GetSecretValue permission.
The same pattern works for SSM Parameter Store:
secrets = [{
name = "API_KEY"
valueFrom = "arn:aws:ssm:us-east-1:123456789012:parameter/prod/api-key"
}]
When to use Parameter Store instead
SSM Parameter Store is appropriate when:
- Configuration values don't rotate (feature flags, environment-specific configs)
- Cost matters — free tier supports thousands of parameters
- Values are non-sensitive or low-sensitivity (endpoints, feature flags)
- You need String or StringList types (not just SecureString)
# SSM Parameter Store for non-sensitive config
resource "aws_ssm_parameter" "api_endpoint" {
name = "/prod/external-api/endpoint"
type = "String"
value = "https://api.example.com"
}
resource "aws_ssm_parameter" "api_key" {
name = "/prod/external-api/key"
type = "SecureString"
value = var.api_key
key_id = aws_kms_key.ssm.arn
}
For RDS passwords, API keys for external services, and any credential that should rotate: use Secrets Manager. For application configuration that varies by environment but isn't sensitive: Parameter Store is sufficient and cheaper.
An ECS task uses secrets injection to retrieve a database password from Secrets Manager. After a rotation event, new tasks start successfully but existing tasks start failing with authentication errors. Why?
mediumSecrets Manager automatic rotation just completed. The rotated password is now in AWSCURRENT. Tasks that were running before rotation have the old password as an environment variable. New tasks injected the new password successfully.
AECS automatically restarts tasks after secret rotation
Incorrect.ECS doesn't monitor Secrets Manager for rotation events or automatically restart tasks.BECS injects secrets as environment variables at task startup — the old password is baked into running tasks. Those tasks must restart to pick up the rotated password
Correct!ECS secrets injection happens once at task launch. The secret value becomes an environment variable baked into that container's environment. Secrets Manager rotation updates the secret, but running containers are unaware — they still use the old value from their environment. Fix: use RDS Proxy, which reads AWSCURRENT dynamically and handles rotation transparently. Or add code to refresh credentials at runtime using the Secrets Manager SDK (not via environment variables). Force a deployment to restart all tasks after rotation.CSecrets Manager rotation creates a new secret ARN — the old ARN no longer resolves
Incorrect.Rotation doesn't change the secret ARN. It updates the value while keeping the same ARN. Versions are tracked with AWSCURRENT/AWSPREVIOUS stage labels.DThe task execution role lost permission to Secrets Manager after rotation
Incorrect.Rotation doesn't modify IAM policies. The task execution role's permissions are unaffected.
Hint:When does ECS inject the secret value into the container's environment?