I spent way too long passing secrets around as environment variables baked into AMIs or stuffed into .env files that inevitably ended up in version control. If you've done the same, you already know how that story ends. AWS Systems Manager Parameter Store is the fix, and it's more straightforward than I expected.
Let's get into it.
Why Parameter Store Beats .env Files
The obvious answer is security, but the real answer is auditable security. With a .env file you have no idea who read your database password last Tuesday. With Parameter Store every GetParameter call shows up in CloudTrail. You also get:
- Encryption via KMS for sensitive values (
SecureStringtype) - IAM-controlled access — your Lambda can read
/app/prod/db_password, your developer's IAM user can't - Path hierarchy that maps cleanly to environments and apps
- No more "who has the latest
.env?" conversations
SecureString vs String
Parameter Store has three types: String, StringList, and SecureString. For anything sensitive — passwords, API keys, tokens — use SecureString. It encrypts the value using a KMS key before storing it. String is fine for non-sensitive config like a region name or feature flag value.
Storing Parameters via CLI
I use a path hierarchy like /app/environment/parameter_name. It keeps things organized and makes IAM policies much cleaner.
# Store a plaintext string
aws ssm put-parameter \
--name "/myapp/prod/db_host" \
--value "mydb.cluster.us-east-1.rds.amazonaws.com" \
--type String
# Store an encrypted secret (uses your default KMS key)
aws ssm put-parameter \
--name "/myapp/prod/db_password" \
--value "s3cr3tpassword" \
--type SecureString
# Retrieve it (WithDecryption handles SecureString automatically)
aws ssm get-parameter \
--name "/myapp/prod/db_password" \
--with-decryption
Output looks like:
{
"Parameter": {
"Name": "/myapp/prod/db_password",
"Type": "SecureString",
"Value": "s3cr3tpassword",
"Version": 1
}
}
Fetching Parameters in Python with boto3
Here's the before/after that sold me on this. Before:
import os
DB_PASSWORD = os.environ['DB_PASSWORD'] # set this... how exactly?
After, using Parameter Store:
import boto3
ssm = boto3.client('ssm', region_name='us-east-1')
def get_parameter(name):
response = ssm.get_parameter(
Name=name,
WithDecryption=True
)
return response['Parameter']['Value']
DB_HOST = get_parameter('/myapp/prod/db_host')
DB_PASSWORD = get_parameter('/myapp/prod/db_password')
No environment variables to manage, no plaintext secrets in your repo, and KMS handles the decryption transparently.
Using It in a Lambda Function
In a Lambda I typically fetch parameters once outside the handler so they're cached across warm invocations:
import boto3
ssm = boto3.client('ssm', region_name='us-east-1')
# Fetch at cold-start, cached for warm invocations
DB_PASSWORD = ssm.get_parameter(
Name='/myapp/prod/db_password',
WithDecryption=True
)['Parameter']['Value']
def handler(event, context):
# DB_PASSWORD is already available here
return {'statusCode': 200}
IAM Policy for Parameter Store Access
This is where the path hierarchy pays off. I give each Lambda a minimal IAM policy scoped to its own path:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters"
],
"Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/prod/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
}
]
}
The /myapp/prod/* wildcard means this Lambda can read all prod parameters for myapp, but nothing from /myapp/staging/ or /otherapp/. Clean separation with zero extra work.
Wrapping Up
Migrating off hardcoded env vars and .env files took me an afternoon per service and I haven't looked back. The combination of path-based organization, KMS encryption, and IAM-scoped access is genuinely better in every dimension. Start with your most sensitive parameters — database passwords, API keys — and expand from there.