Hardcoded secrets in application code get into git repositories, environment variables in task definitions get exposed in AWS Console screenshots, and plaintext credentials in configuration files get leaked in incident reports. As someone who has reviewed codebases where AWS keys were committed to git and had the uncomfortable conversation that follows, I learned what the right approach actually looks like and how to implement it without a major project. Today I’ll share all of it.
This article includes affiliate links. We may earn a commission at no extra cost to you.

The Two Services You’ll Actually Use
AWS Secrets Manager stores and rotates credentials. It supports automatic rotation for RDS passwords, which means your database password can rotate on a schedule without any application code changes — Secrets Manager updates the secret, and the next time your application fetches it, it gets the new value. Secrets Manager costs $0.40 per secret per month plus $0.05 per 10,000 API calls. For most applications, the cost is negligible.
AWS Systems Manager Parameter Store stores configuration values, including encrypted parameters (SecureString). Parameter Store is free for standard parameters (up to 10,000 parameters, 4KB max). Advanced parameters with higher throughput and larger values cost $0.05 per parameter per month. For non-rotating secrets and configuration that doesn’t need rotation, Parameter Store is cost-effective and integrates with most AWS services.
The general guidance: use Secrets Manager when you need rotation. Use Parameter Store for static secrets, API keys, and configuration values. That’s what makes the two-service distinction endearing to anyone who’s tried to use only one for everything — each is genuinely designed for a different use case.
Fetching Secrets at Runtime (Not Startup)
The common mistake is fetching all secrets at application startup and storing them in memory for the application lifetime. This is better than hardcoding, but it means a rotated secret causes errors until the application restarts. The better pattern is to cache the secret with a TTL and refresh it:
import boto3
import time
_secrets_cache = {}
_cache_ttl = 300 # 5 minutes
def get_secret(secret_name: str) -> str:
now = time.time()
cached = _secrets_cache.get(secret_name)
if cached and now - cached['timestamp'] < _cache_ttl:
return cached['value']
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId=secret_name)
value = response['SecretString']
_secrets_cache[secret_name] = {'value': value, 'timestamp': now}
return value
This pattern handles rotation gracefully: when the secret rotates, the next cache refresh picks up the new value within 5 minutes. Adjust the TTL based on how quickly you need rotation to take effect.
ECS Integration
ECS supports injecting secrets directly into container environment variables via the task definition’s secrets field. The container sees the value as a plain environment variable — no SDK code needed to fetch it:
{
"containerDefinitions": [{
"name": "app",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT:secret:prod/db/password"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:ssm:us-east-1:ACCOUNT:parameter/prod/api/key"
}
]
}]
}
The task execution role needs secretsmanager:GetSecretValue or ssm:GetParameters permission. The secret is fetched at container startup — so this pattern works for static secrets but not rotating ones. For rotating credentials, use the SDK approach above.
What Not to Do
Don’t put secrets in environment variables in CloudFormation or Terraform templates if those templates are in git. Don’t store secrets in S3 without encryption. Don’t log secrets — check that your logging configuration filters out headers like Authorization and field names like password before writing to CloudWatch. Probably should have led with this section, honestly, because these aren’t theoretical concerns — they show up regularly in security audits and post-mortems, and they’re entirely preventable.
Leave a Reply