AWS DOP-C02: Parameter Store hierarchies, versions, labels and tiers, and what a local emulator will not tell you
The path chooses what you read, the label chooses which version, and the tier chooses what you are allowed to store.
The problem
An online shop runs in two environments, dev and prod. Each needs a database host and a database password, and the values differ between them. A deployment pipeline has to read every setting for prod in one call, because one call per value means the pipeline carries a list of names that drifts out of date. It must also roll a changed value forward and back without anyone editing the pipeline definition. The password has to stay encrypted, and only callers allowed to decrypt it should see it. Now and then a value, such as a long certificate, is larger than usual.
AWS Systems Manager Parameter Store, a managed key-value store for configuration data and secrets, covers all four needs. The DOP-C02 exam guide names it under Task Statement 1.1, "Implement CI/CD pipelines" (CI/CD is continuous integration and continuous delivery), as part of the skill "Managing build and deployment secrets", and under Task Statement 2.1, where Systems Manager is one of the configuration management services a candidate must choose between. AWS Secrets Manager, named next to it in the guide, is a separate service not covered here. Exam questions describe a pipeline like this one and ask which call, flag, tier or label makes it work.
Every command below is AWS CLI (command line interface) 2.38.5 run against a local AWS emulator (moto 5.2.4.dev0) on 127.0.0.1 with throwaway credentials and no AWS account. The emulator diverged from the documented service in three places, and each one is pointed out where it appears.
The idea, in plain words
A parameter is a named value, and slashes in the name make a hierarchy that one call can read as a subtree. Every change creates a new numbered version, and a label is a movable name for one version, so a pipeline can read name:label while an operator moves the label. A SecureString encrypts its value through KMS (AWS Key Management Service) and decrypts it only on request. Tiers, called Standard and Advanced, decide the maximum value size, the number of parameters allowed, and whether parameter policies (rules such as an expiration time) are permitted. In one line: the path chooses what you read, the label chooses which version, and the tier chooses what you are allowed to store.
Hierarchies and GetParametersByPath
A hierarchy is, in the documentation's words, "a parameter name that includes a path that you define by using forward slashes (/)". A name can have at most 15 levels. Paths matter because /shop/prod/... and /shop/dev/... share a shape, so a pipeline needs to know only the environment prefix; otherwise it must carry every individual name.
The operation that reads a subtree is GetParametersByPath, which the CLI exposes as get-parameters-by-path. It takes a Path, a Recursive flag, MaxResults and NextToken. MaxResults accepts 1 to 10, and results are best effort: a page can hold anywhere from zero to MaxResults items, and a NextToken in the response tells the caller to ask again. The path is the parameter name without its last part, because "the last part of the parameter name can't be in the path".
The lab created four parameters with put-parameter: /shop/prod/db/host, /shop/prod/db/password (a SecureString), /shop/prod/log-level and /shop/dev/db/host. A small shell function printed each response's Version and Tier, and all four reported Version,1,Tier,Standard. The lab then read /shop/prod twice.
$ aws ssm get-parameters-by-path --path /shop/prod --query 'Parameters[].Name' --output text
/shop/prod/log-level
[exit 0]
$ aws ssm get-parameters-by-path --path /shop/prod --recursive --query 'Parameters[].[Name,Type,Value]' --output text
/shop/prod/db/host String db.prod.internal
/shop/prod/db/password SecureString kms:alias/aws/ssm:pr0d-s3cret
/shop/prod/log-level String warn
[exit 0]
Without --recursive, the call returned only /shop/prod/log-level, the one parameter directly under the path; the two under /shop/prod/db sit a level deeper. The second call, with --recursive, returned all three parameters in the prod subtree and nothing from /shop/dev, which is the separation the naming scheme is meant to give. The [exit 0] lines, echoed by the lab script, show that the shortfall in the first call is silent.
The last-part rule also showed up in the lab:
$ aws ssm get-parameters-by-path --path /shop/prod/db/host --query 'length(Parameters)' --output text
0
[exit 0]
Passing a full parameter name as the path returned 0 parameters and exit status 0, not an error. A single known name is read with get-parameter instead.
On the exam: a stem that says one call must return all settings for an environment points to GetParametersByPath with a path and the recursive option. Watch the documentation's Important note on access: a user allowed on /a can read /a/b through a recursive call on /a, even with an explicit IAM (Identity and Access Management) deny on /a/b. A deny on one deep parameter does not hide it from someone who holds the parent path.
Symptom: the pipeline reads /shop/prod and gets only log-level, without the database settings.
Cause: GetParametersByPath reads one level unless Recursive is set, as steps 2a and 2b of the lab showed.
Fix: pass --recursive and handle NextToken, because the service returns at most 10 parameters per call.
SecureString parameters and KMS
Parameter Store has three parameter types: String, StringList (items separated by commas) and SecureString. A SecureString is a parameter with a plaintext name and an encrypted value, and Parameter Store uses KMS to encrypt and decrypt it. If no KeyId is given, the system uses the AWS managed key with the alias aws/ssm. A customer managed key can be named through KeyId instead; only symmetric keys are supported.
A standard SecureString (at most 4096 bytes) is encrypted directly under the KMS key, while an advanced one uses envelope encryption with a data key generated per value. The permissions follow: kms:Encrypt to write a standard SecureString, kms:GenerateDataKey to write an advanced one, and kms:Decrypt to read either. The encryption context is a key named PARAMETER_ARN whose value is the parameter's ARN (Amazon Resource Name). Access control policies cannot be set on the default aws/ssm key, so key-level control needs a customer managed key.
A read returns the value encrypted unless the caller sets WithDecryption, which is --with-decryption in the CLI. The flag is ignored for String and StringList parameters. The lab read the same SecureString both ways. The first line below is from the read without the flag (step 2b), and the second is from the read with it (step 2c):
/shop/prod/db/password SecureString kms:alias/aws/ssm:pr0d-s3cret
/shop/prod/db/password pr0d-s3cret
The first line is where the emulator parts from the documented service. The documented response for a SecureString read without decryption is ciphertext, such as "Value": "AQECAHgnOkMROh5LaLXkA4j0+vYi6tmM17Lg". The emulator returned the plaintext with a prefix naming the key alias, kms:alias/aws/ssm:pr0d-s3cret. The emulator therefore demonstrates the flag, since the decrypted read returned pr0d-s3cret, but it does not demonstrate encryption. The lab defined no IAM or key policies, so everything said here about KMS permissions comes from the documentation alone.
On the exam: a build role that must read a SecureString needs two grants. The documentation's example policy allows ssm:GetParameter* on arn:aws:ssm:us-east-1:111122223333:parameter/ITParameters/* and kms:Decrypt on one customer managed key. Because decrypting needs kms:Decrypt, an SSM-only grant cannot return plaintext, and scoping the SSM action to a path keeps the role from reading unrelated parameters.
Symptom: a build step receives a long base64-looking string instead of the password.
Cause: the value is a SecureString and the call omitted WithDecryption, so the encrypted value came back as documented.
Fix: pass --with-decryption and grant kms:Decrypt on the key to that role, with ssm:GetParameter* scoped to the path.
Versions and labels
A version is a numbered snapshot of a parameter's value. PutParameter has an Overwrite option that defaults to false, so putting to an existing name fails with ParameterAlreadyExists. With overwrite, Parameter Store creates a new version and increments the number. It keeps the 100 most recent versions and deletes the oldest when another arrives. If the oldest version carries a label, it refuses and returns ParameterMaxVersionLimitExceeded instead.
A label is a user-defined alias for one version, and GetParameter accepts name:version or name:label as the name. Labels exist so that a pipeline reading name:known-good never changes while the operator moves the label from one version to another. A pipeline that hard-codes name:2 has to be edited for every promotion. The rules for labels are strict:
- A version holds at most 10 labels.
- A label can be attached to only one version of a given parameter, but it can be moved.
- A label cannot be attached at creation, only to an existing version.
- A label has at most 100 characters, drawn from letters, numbers, periods, hyphens and underscores.
- A label cannot begin with a number, "aws" or "ssm".
A label that breaks a rule is not attached, and the system lists it under InvalidLabels. A request for a missing version "fails. It doesn't fall back to the latest or default value".
The lab first tried to overwrite without the flag, then with it, then read the parameter through four selectors (lines trimmed, none altered):
### 3a overwrite without --overwrite
aws: [ERROR]: An error occurred (ParameterAlreadyExists) when calling the PutParameter operation: The parameter already exists. To overwrite this value, set the overwrite option in the request to true.
[exit 254]
### 3b with --overwrite: a new version
Version,2,Tier,Standard
[exit 0]
### 3c read by version selector
None 2 db2.prod.internal
[/shop/prod/db/host exit 0]
None 1 db.prod.internal
[/shop/prod/db/host:1 exit 0]
None 2 db2.prod.internal
[/shop/prod/db/host:2 exit 0]
aws: [ERROR]: An error occurred (ParameterVersionNotFound) when calling the GetParameter operation: Systems Manager could not find version 7 of /shop/prod/db/host. Verify the version and try again.
[/shop/prod/db/host:7 exit 254]
The put without --overwrite failed with ParameterAlreadyExists and exit status 254, and the put with it created version 2. Selector :1 returned the old host and :2 the new one. Selector :7 failed with ParameterVersionNotFound, with no fallback to the latest value. The None column is the second emulator divergence: the documented Selector field echoes the requested version or label.
The labels came next: attach known-good to version 1, read by label, move it to version 2, then print the history as version and labels:
### 4a label version 1 as known-good, read by label
{
"InvalidLabels": [],
"ParameterVersion": 1
}
None 1 db.prod.internal
### 4b move the label to version 2
{
"InvalidLabels": [],
"ParameterVersion": 2
}
1
2 known-good
The empty InvalidLabels list means the label was applied, and reading /shop/prod/db/host:known-good returned version 1. After a second label-parameter-version call naming version 2, the history shows the label on version 2 only; no separate removal step was needed. Rolling back is the same call with --parameter-version 1, and the pipeline's read of name:known-good never changes, which is the whole reason to use a label rather than a version number.
Three bad labels came back like this:
{
"InvalidLabels": [
"2026-q4",
"aws-prod",
"ssm-x"
],
"ParameterVersion": 2
}
[exit 0]
The call succeeded with exit status 0 while attaching none of the three labels. Finally, get-parameters-by-path --path /shop --recursive with a Label filter of known-good returned /shop/prod/db/host 2, so the filter finds which parameters under a path carry a label.
On the exam: name:label is the answer when a pipeline must follow a promotion without edits, and ParameterVersionNotFound is the answer when a stem names a version that does not exist. A stem that attaches a label while creating the parameter is a trap, because labels attach only to an existing version.
Symptom: label-parameter-version exits 0, but reads of name:label fail and the label is missing from the history.
Cause: the label broke a naming rule, such as starting with a number or with "aws" or "ssm", and came back in InvalidLabels (lab step 4c).
Fix: check InvalidLabels in the response, not only the exit code, and rename the label.
Tiers and parameter policies
A tier is the class of a parameter, and it sets storage attributes. Standard is the default and suits most configuration data.
| Standard | Advanced | |
|---|---|---|
| Maximum parameters per account and Region | 10,000 | 100,000 |
| Maximum value size | 4 KB | 8 KB |
| Parameter policies | Not supported | Supported |
| Sharing across AWS accounts | Not supported | Supported |
| Changing tier | Upgradeable | Not downgradeable |
| Cost | No additional charge | Charges apply |
Standard can be upgraded to Advanced at any time, but Advanced cannot revert: the value would be truncated from 8 KB to 4 KB, policies would be lost, and the tiers encrypt differently. To return to Standard, delete and recreate the parameter. The account's default tier setting can be Standard, Advanced, or Intelligent-Tiering, which picks Advanced only when a request needs it: the value is over 4 KB, the request uses a parameter policy, or more than 10,000 parameters already exist in the Region. Tiers are also separate from throughput, which controls request rate.
Policies come in three types. Expiration deletes the parameter after it expires. ExpirationNotification starts an Amazon CloudWatch Events event about the expiration. NoChangeNotification starts an event if a parameter has not been modified for a set period. A parameter can carry at most 10 policies, and because policies need Advanced, a policy request on a Standard parameter requires the upgrade.
The lab tried the limits. Step 5a stored 5,000 x characters as a Standard String, 5b stored the same value with --tier Advanced, and 5c put a small value to the Advanced parameter with --tier Standard --overwrite:
Version,1,Tier,Standard
[exit 0]
Version,1,Tier,Advanced
[exit 0]
Version,2,Tier,Standard
[exit 0]
/shop/prod/big-adv Standard 2
This is the third divergence. A 5,000-byte value exceeds the documented 4 KB Standard limit, yet 5a succeeded, and 5c moved an Advanced parameter back to Standard, confirmed by the describe call. The documented service allows neither, so this step shows only that the emulator does not enforce the rules; every tier rule above comes from the documentation.
On the exam: a value over 4 KB, a need for parameter policies, or more than 10,000 parameters each point to Advanced. Intelligent-Tiering as the default tier setting is the answer when a team wants Advanced chosen automatically only for requests that need it.
Symptom: an emulator-based pipeline test passes, then the real put of a large certificate file fails.
Cause: the value exceeds the Standard tier's 4 KB, and the emulator did not enforce the limit (lab step 5a).
Fix: set Tier Advanced for that parameter, or set the default tier to Intelligent-Tiering, and test size and tier rules against the real service.
Putting it together
The pipeline from the problem comes together in this order:
- Name parameters under
/shop/<env>/...so each environment is a subtree. - Read an environment with
get-parameters-by-path --recursive --with-decryption, and follow NextToken until it is absent. - Promote by moving a label such as
known-goodto the new version, with the pipeline readingname:label. - Roll back by moving the label back to the earlier version.
- Keep secrets as SecureString, and give each role
kms:Decrypton the key plusssm:GetParameter*scoped to its path. - Choose Advanced only where size or policies require it.
| Exam cue | Answer |
|---|---|
| Read all parameters for one environment in a single call | GetParametersByPath with Path and Recursive |
| Value comes back as unreadable ciphertext | Set WithDecryption and grant kms:Decrypt |
| Promote or roll back without editing the pipeline | Read name:label and move the label |
| Create the parameter and label it in one step | Not possible, labels attach to an existing version |
| Value larger than 4 KB, or a parameter policy | Advanced tier, or Intelligent-Tiering as the default |
| Return an Advanced parameter to Standard | Delete it and recreate it |
| Deny one deep parameter but allow its parent path | Does not hide it from recursive reads of the parent |
The lab verified the hierarchy reads, the overwrite rule, version and label selectors, InvalidLabels and the Label filter. It did not verify real KMS encryption or permissions, tier limits, the downgrade refusal, policies, NextToken paging or throughput.
Check yourself
/orders/prod and receives only two of the eight parameters stored under that prefix. Which option is missing, and what must the caller also handle if the subtree has more than 10 parameters?ssm:GetParameter* on /orders/prod/* but receives an encrypted string for the database password. What two changes fix it?kms:Decrypt on the KMS key protecting the SecureString. Without the permission, the decryption call cannot succeed.label-parameter-version with the label 2026-q4. The command exits 0, but the pipeline's read of name:2026-q4 fails. What happened, and how would the operator have caught it?/a and has an explicit IAM deny on the parameter /a/b. Can the user still see /a/b?/a can return /a/b despite the explicit deny, so access should be granted on the narrowest path.Sources
- AWS Certified DevOps Engineer - Professional Exam Guide (DOP-C02)
- AWS Systems Manager User Guide, Working with parameter hierarchies in Parameter Store
- AWS Systems Manager API Reference, GetParametersByPath
- AWS Systems Manager User Guide, Working with parameter labels in Parameter Store
- AWS Systems Manager API Reference, PutParameter
- AWS Systems Manager User Guide, AWS KMS encryption for Parameter Store SecureString parameters
- AWS Systems Manager User Guide, Choosing parameter tiers in Parameter Store
- AWS Systems Manager API Reference, GetParameter