AWS SSM Parameter Store
Store configuration and secrets in AWS Systems Manager Parameter Store. SecureString encryption, hierarchies, versions, standard vs advanced tiers, and cost.
Parameter Store is the part of AWS Systems Manager that holds configuration values and secrets. It encrypts SecureString parameters with KMS, versions every change, controls access through IAM, and, unlike Secrets Manager, the Standard tier costs nothing.
For most teams it should be the default place configuration lives, with Secrets Manager reserved for the specific cases that need automatic rotation.
Parameter types
| Type | Encrypted | Use it for |
|---|---|---|
String | No | Plain configuration: hostnames, feature flags, log levels |
StringList | No | Comma-separated lists, such as allowed origins |
SecureString | Yes, via KMS | Passwords, API keys, tokens |
# Plain configuration
aws ssm put-parameter \
--name /prod/payments/log-level \
--value info \
--type String
# An encrypted secret
aws ssm put-parameter \
--name /prod/payments/api-key \
--value "sk_live_xxxxxxxx" \
--type SecureString \
--key-id alias/payments-kms
Without --key-id, SecureString uses the AWS-managed alias/aws/ssm key. That works, but a customer-managed key is better for anything sensitive: you get your own key policy as a second access control layer, and key usage shows up separately in CloudTrail. The trade-off is that readers then need kms:Decrypt on that key in addition to the SSM permission. A very common cause of confusing AccessDeniedException errors.
Updating an existing parameter requires --overwrite, which is a deliberate guard against accidental clobbering:
aws ssm put-parameter --name /prod/payments/log-level --value debug --type String --overwrite
Hierarchies are the main design decision
Parameter names are paths, and getting the hierarchy right is what makes Parameter Store pleasant rather than tedious. Put the environment first:
/prod/payments/db-host
/prod/payments/db-password
/prod/payments/log-level
/staging/payments/db-host
Environment-first means one IAM statement scopes a role to exactly its own environment:
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"],
"Resource": "arn:aws:ssm:us-east-1:111122223333:parameter/prod/payments/*"
}
Note the ARN has no slash between parameter and the leading /prod. It reads parameter/prod/.... Adding one produces a policy that silently matches nothing.
It also means an application can load its whole configuration in one call:
aws ssm get-parameters-by-path \
--path /prod/payments \
--recursive \
--with-decryption \
--query 'Parameters[].{Name:Name,Value:Value}' \
--output table
--with-decryption is required for SecureString values; without it you get the ciphertext back and a confusing runtime failure.
Loading by path is the pattern to build on, because adding a parameter needs no code change:
import boto3
def load_config(path: str) -> dict:
ssm = boto3.client("ssm")
paginator = ssm.get_paginator("get_parameters_by_path")
config = {}
for page in paginator.paginate(Path=path, Recursive=True, WithDecryption=True):
for param in page["Parameters"]:
key = param["Name"].removeprefix(path).lstrip("/")
config[key] = param["Value"]
return config
config = load_config("/prod/payments")
Use the paginator. get_parameters_by_path returns at most 10 results per call, and code that ignores NextToken works fine in staging with eight parameters and truncates silently in production with thirty.
Versions and labels
Every write creates a new version, and old versions stay readable, which makes rollback trivial:
aws ssm get-parameter-history --name /prod/payments/log-level \
--query 'Parameters[].{Version:Version,Value:Value,Date:LastModifiedDate}' --output table
# Pin a specific version
aws ssm get-parameter --name /prod/payments/log-level:3
Labels give versions readable names, so a deployment can reference :live and you promote by moving the label rather than editing values:
aws ssm label-parameter-version \
--name /prod/payments/log-level --parameter-version 3 --labels live
aws ssm get-parameter --name /prod/payments/log-level:live
Standard vs advanced tier
| Standard | Advanced | |
|---|---|---|
| Cost | Free | $0.05 per parameter per month |
| Max value size | 4 KB | 8 KB |
| Parameters per account/region | 10,000 | 100,000 |
| Parameter policies (expiry, notification) | No | Yes |
| Higher throughput option | Paid | Paid |
Standard covers almost everything. Move to advanced when a value exceeds 4 KB, typically a certificate or a large JSON blob, or when you want parameter policies, which can expire a parameter automatically or emit an EventBridge notification when one hasn't changed in N days. That second policy is a genuinely useful way to catch credentials nobody has rotated.
Throughput
The default API limit is modest, and applications that read parameters on every request will hit ThrottlingException under load. The fixes, in order of preference: cache in the application, fetch by path once at start-up rather than per parameter, and only then consider paying for higher throughput.
On Lambda, the AWS Parameters and Secrets Extension is the clean answer. It runs a local caching layer so repeated reads never leave the execution environment. On ECS, reference parameters directly in the task definition:
"secrets": [
{ "name": "API_KEY", "valueFrom": "arn:aws:ssm:us-east-1:111122223333:parameter/prod/payments/api-key" }
]
Integration worth knowing
Parameter Store publishes AWS-managed public parameters, which removes a whole category of hardcoded values. The latest Amazon Linux AMI ID, for example:
aws ssm get-parameter \
--name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query Parameter.Value --output text
Terraform and CloudFormation can both resolve parameters at deploy time, so an AMI ID never needs to be pasted into a template again.
Quick reference
| Task | Command |
|---|---|
| Create/update | aws ssm put-parameter --name <n> --value <v> --type <t> [--overwrite] |
| Read one | aws ssm get-parameter --name <n> --with-decryption |
| Read a tree | aws ssm get-parameters-by-path --path <p> --recursive --with-decryption |
| History | aws ssm get-parameter-history --name <n> |
| Label a version | aws ssm label-parameter-version --name <n> --parameter-version <v> --labels <l> |
| Delete | aws ssm delete-parameter --name <n> |
| Search | aws ssm describe-parameters --parameter-filters "Key=Name,Option=Contains,Values=db" |
Key takeaways
- Standard tier is free: make Parameter Store the default and reserve Secrets Manager for values that need managed rotation.
- Design the hierarchy environment-first (
/prod/service/key) so one IAM statement scopes a role to its own environment. - The ARN is
parameter/prod/...with no extra slash; the wrong form matches nothing and gives no hint why. - A customer-managed KMS key also requires
kms:Decrypton that key, which is the usual cause of mysterious AccessDenied errors. - Always pass
--with-decryptionfor SecureString, and always paginateget-parameters-by-path. - Every write is versioned, so rollback is a read of an earlier version, and labels let you promote without editing values.
Check your understanding
Decryption failure
A role has ssm:GetParameter on the correct ARN, but reading a SecureString encrypted with a customer-managed key returns AccessDeniedException. What is missing?
Configuration that works in staging
A service loads config with get-parameters-by-path. Staging has 8 parameters and works. Production has 30 and the service starts with several values missing, but no error. Why?
Frequently Asked Questions
Is Parameter Store really free?
The Standard tier is free for storage and for standard-throughput API calls, up to 10,000 parameters per account per region. You pay only for KMS usage on SecureString operations, which is negligible for normal access patterns. The Advanced tier costs $0.05 per parameter per month and raises the value size limit to 8 KB, the count to 100,000, and adds parameter policies. Higher-throughput API access is a separate paid option in both tiers.
When should I use Secrets Manager instead?
When you need automatic rotation with a managed function, RDS, Aurora, Redshift and DocumentDB credentials being the clear cases, or cross-account access via resource policies, or values larger than 4 KB without moving to the advanced tier. Everything else belongs in Parameter Store. Putting a few hundred configuration values in Secrets Manager at $0.40 each per month costs real money for a rotation feature those values never use.
How do I read a SecureString value?
Pass --with-decryption to get-parameter or get-parameters-by-path. Without it the API returns the encrypted value and your application fails somewhere later with a confusing error. The caller needs both the SSM read permission and kms:Decrypt on the key that encrypted the parameter; with the default alias/aws/ssm key the second part is implicit, but a customer-managed key must grant it explicitly.
Why does my IAM policy for Parameter Store not work?
Check the ARN format first. It is arn:aws:ssm:<region>:<account>:parameter/prod/payments/*. There is no slash between parameter and the parameter path, so the leading slash of the name is the separator. Writing parameter//prod/... produces a policy that matches nothing and denies without explanation. The second thing to check is whether the parameter is a SecureString needing a separate kms:Decrypt grant.
Can I roll back a parameter change?
Yes. Every put-parameter creates a new version and previous versions remain readable indefinitely. Use get-parameter-history to see what changed and when, then read a specific version with --name /path/to/param:3. For a cleaner workflow, apply a label such as live to the known-good version and have deployments read :live, so promoting and rolling back are label moves rather than value edits.
How do I stop hardcoding AMI IDs?
AWS publishes public parameters under /aws/service/, including the latest AMI for each distribution. For example /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64. Read it with get-parameter, or reference it directly from CloudFormation and Terraform so templates resolve the current AMI at deploy time instead of carrying an ID that goes stale and becomes a security liability.