Secrets Manager, Parameter Store and KMS

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour on this page are current for AWS CLI v2 (2.37 line), the AWS Workload Credentials Provider 3.1.x, the AWS Secrets and Configuration Provider (ASCP) for the Kubernetes Secrets Store CSI Driver and Spring Cloud AWS 4.x (Spring Boot 4) or 3.4.x (Spring Boot 3.5), all checked on 2026-10-01; re-verify against aws --version and the linked reference pages before relying on them in a pipeline. Examples use us-east-1 and the placeholder account ID 111122223333; both are stand-ins for your own values, and every secret value below is an obvious placeholder such as <your-password>.

Every Bookshelf service that touches a database password, an API key or a signing key reads it from AWS instead of from an environment variable baked into an image or a file in Git. Three services share the work, and picking the right one is most of the design: AWS Secrets Manager stores and rotates secrets (the Aurora credentials in bookshelf/catalog/db), Systems Manager Parameter Store stores configuration, optionally encrypted (/bookshelf/), and *AWS Key Management Service (KMS) holds the encryption keys that protect both, as well as S3 buckets, EBS volumes, RDS storage and almost everything else on AWS. This page covers each service, how they fit together, and every consumption path Bookshelf uses: the Java SDK, Spring Cloud AWS, ECS task definitions, Lambda and EKS. Plain, non-secret configuration and feature flags are covered on Runtime Configuration; the IAM concepts used throughout (roles, trust and permission policies) on Identity and Access. For the Azure counterpart, see Key Vault and Secrets, where one service (Key Vault) covers what AWS splits across three.

Which service for what

Aspect Secrets Manager Parameter Store (SecureString) KMS

Purpose

Store, version and rotate secrets: database credentials, API keys, OAuth client secrets.

Store configuration hierarchies; SecureString encrypts the value with a KMS key.

Create and use cryptographic keys; the key material never leaves KMS in plaintext.

Rotation

Built in: managed rotation for RDS/Aurora master passwords and managed external secrets, or your own rotation Lambda function, on a schedule.

None. You change the value yourself (or with your own automation).

Automatic (default 365 days, 90 to 2,560 configurable) and on-demand rotation of symmetric keys.

Value size

Up to 65,536 bytes.

4 KB (standard) or 8 KB (advanced).

Encrypt takes up to 4,096 bytes of plaintext; use envelope encryption for more.

Versioning

Versions with staging labels AWSCURRENT, AWSPREVIOUS, AWSPENDING; up to 100 versions.

Numeric versions plus labels; the history is kept.

Key material versions are kept for decryption; the key ID never changes.

Cross-Region

Replicate a secret to other Regions; replicas stay in sync.

No native replication; copy with automation.

Multi-Region keys with the same key material in several Regions.

Cross-account

Resource-based policy on the secret (plus a customer managed key for the encryption).

Advanced parameters can be shared through AWS Resource Access Manager.

Key policy (and grants) allow principals from other accounts.

Pricing model

Per secret per month plus per API calls.

Standard parameters have no charge; advanced parameters are charged per parameter per month plus API calls.

Per customer managed key per month plus per request; AWS managed keys have no monthly fee.

Choose it when

The value is a credential that should rotate, be audited per read, or be replicated.

The value is configuration, or a low-risk secret, and you want a free or cheap, hierarchical store.

You need to control who can decrypt data, audit key use, or encrypt your own data directly.

A rule of thumb that fits Bookshelf: database credentials and third-party API keys go to Secrets Manager, feature toggles and endpoints go to Parameter Store (standard tier), and the keys behind both are customer managed KMS keys when you need cross-account access, a custom key policy or per-use audit, and the AWS managed keys otherwise. Newer guides often state that "Parameter Store is free, Secrets Manager is not" as the whole decision; the difference that matters is rotation, versioning with labels, cross-Region replication and per-secret resource policies, which Parameter Store does not provide.

AWS KMS

AWS KMS creates and controls cryptographic keys and performs encryption, decryption, signing and key generation inside FIPS 140-validated hardware security modules (HSMs). Applications call the KMS API; they do not hold the key. Almost every AWS service that encrypts data integrates with KMS (S3 SSE-KMS, EBS, RDS, Secrets Manager, Lambda environment variables, EKS secrets), so understanding KMS is the foundation for everything else on this page.

Key types

Category What it is

AWS owned keys

Keys that AWS creates and manages in its own accounts to protect resources by default (for example DynamoDB tables with default encryption). They are invisible to you: no key policy, no CloudTrail entries, no charge.

AWS managed keys

Keys that AWS creates in your account on behalf of a service, with aliases like aws/secretsmanager, aws/ssm, aws/s3, aws/ebs. You can view them and see their use in CloudTrail, but you cannot edit their key policy, rotate them on demand, or delete them. They rotate automatically about every year. There is no monthly fee; use requests are charged where the service says so. They are unusable from other accounts, which is why cross-account sharing needs a customer managed key.

Customer managed keys (CMKs)

Keys you create, name with an alias, control with a key policy, rotate, disable and schedule for deletion. This is the category the rest of this section covers. Each costs a monthly fee while it exists.

Within the customer managed category, the key spec and key usage decide what the key can do:

Key type Typical key usage Use it for

Symmetric (SYMMETRIC_DEFAULT, AES-256-GCM)

ENCRYPT_DECRYPT

The default and by far the most common: envelope encryption, service integrations, Secrets Manager and SecureString. The only type that supports automatic and on-demand rotation; the key material never leaves KMS unencrypted.

Asymmetric (RSA, ECC, SM2 in China)

ENCRYPT_DECRYPT, SIGN_VERIFY or KEY_AGREEMENT

Signing JWTs or documents, or encrypting data for parties outside AWS that hold only the public key. No automatic rotation.

HMAC

GENERATE_VERIFY_MAC

Message authentication codes where the secret must stay in KMS. No automatic rotation.

Post-quantum signing (ML-DSA)

SIGN_VERIFY

Long-lived signatures that must resist quantum attacks; verify the current list of key specs in the KMS developer guide.

The origin of the key material can be KMS itself (the default), imported material you supply, a CloudHSM key store (a single-tenant HSM cluster you own, for compliance that requires dedicated hardware) or an external key store (the key stays in an HSM outside AWS). CloudHSM and external key stores add cost and operational burden and are only worth it when a regulation names them; Bookshelf uses the default origin.

Key policies, IAM policies and grants

Three mechanisms authorise key use, and they combine differently from most AWS services:

  • The key policy is the primary access control and the only one that is mandatory. Unlike an S3 bucket policy, a KMS key with no statement allowing the account root principal cannot be managed by IAM at all. The default key policy created by the console contains a statement that gives the account root kms:*, which is what delegates authorisation to IAM policies in the same account.

  • IAM policies grant KMS permissions to roles and users, but only if the key policy permits IAM to do so (the root statement above) or names the principal directly.

  • Grants are programmatic, temporary, narrowly scoped permissions added to a key without editing its key policy. AWS services use them when you create an encrypted resource (an EBS volume attached to an instance, an RDS instance); applications use them for delegation, passing a specific operation set to a specific principal, with optional constraints on the encryption context. Grants are eventually consistent (use the grant token to use one immediately) and are removed by retiring or revoking them.

A key policy for the Bookshelf data key alias/bookshelf-data that separates administrators (whose statement grants no kms:Decrypt) from users (the catalog-api task role and the Secrets Manager service acting for it):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableIamPoliciesInThisAccount",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "KeyAdministrators",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/bookshelf-key-admin" },
      "Action": [
        "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*",
        "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:TagResource",
        "kms:UntagResource", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion",
        "kms:RotateKeyOnDemand"
      ],
      "Resource": "*"
    },
    {
      "Sid": "UseTheKeyOnlyThroughSecretsManagerAndSsm",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/catalog-api-task-role" },
      "Action": [ "kms:Decrypt", "kms:DescribeKey" ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": [
            "secretsmanager.us-east-1.amazonaws.com",
            "ssm.us-east-1.amazonaws.com"
          ]
        }
      }
    }
  ]
}

The kms:ViaService condition means the role can use the key only when the request comes from Secrets Manager or Systems Manager on its behalf, never by calling kms:Decrypt directly. The root statement is what keeps the key manageable if the administrator role is deleted; removing it is how people lock themselves out of a key, and then need AWS Support to recover it. Do not put user-supplied data in the key policy; see Key policies in AWS KMS.

Envelope encryption

Encrypt accepts at most 4,096 bytes, and sending large payloads to a remote service is slow. KMS and the AWS Encryption SDK therefore use envelope encryption: ask KMS for a unique data key, encrypt the data locally with the plaintext copy, discard it, and store the encrypted copy of the data key next to the ciphertext. To decrypt, send the stored encrypted data key to KMS (Decrypt), receive the plaintext data key and decrypt locally. Only the small data key ever travels to KMS, and each object gets its own key.

sequenceDiagram participant App as Application participant KMS as AWS KMS participant Store as Storage (S3, database) Note over App,KMS: Encrypt App->>KMS: GenerateDataKey (key ID, AES_256, encryption context) KMS-->>App: Plaintext data key + encrypted data key App->>App: Encrypt data locally with the plaintext data key App->>App: Discard the plaintext data key App->>Store: Ciphertext + encrypted data key Note over App,KMS: Decrypt Store-->>App: Ciphertext + encrypted data key App->>KMS: Decrypt (encrypted data key, same encryption context) KMS-->>App: Plaintext data key App->>App: Decrypt the data locally and discard the key

The encryption context is a set of non-secret key/value pairs (for example app=bookshelf and tenant=<tenant-id>) that is cryptographically bound to the ciphertext, logged in CloudTrail in plaintext and required again on decryption. Use it to tie a ciphertext to its purpose and to write policies such as "decrypt only when kms:EncryptionContext:app equals `bookshelf`". AWS services do this for you: S3, EBS, RDS and Secrets Manager all use envelope encryption internally with their own data keys under your KMS key.

Create a key

In the AWS Management Console

  1. Open Key Management Service (KMS) and choose the correct Region in the selector; keys are Regional.

  2. Choose Customer managed keys > Create key.

  3. Key type Symmetric, Key usage Encrypt and decrypt. Under Advanced options, leave Key material origin as KMS and choose Single-Region key (or Multi-Region key, see Multi-Region keys).

  4. Set the alias bookshelf-data (aliases are stored as alias/bookshelf-data), a description and tags.

  5. Define key administrative permissions (choose bookshelf-key-admin) and Define key usage permissions (choose catalog-api-task-role). The console writes a key policy from these choices that you can review on the last step and edit.

  6. Finish. Open the key and enable Key rotation on its Key rotation tab.

With the AWS CLI

export AWS_REGION=us-east-1

KEY_ID=$(aws kms create-key \
  --description "Bookshelf data key" \
  --key-spec SYMMETRIC_DEFAULT --key-usage ENCRYPT_DECRYPT \
  --policy file://key-policy.json \
  --tags TagKey=app,TagValue=bookshelf \
  --query KeyMetadata.KeyId --output text)

aws kms create-alias --alias-name alias/bookshelf-data --target-key-id "$KEY_ID"

# Small payload: direct encrypt / decrypt with an encryption context
aws kms encrypt --key-id alias/bookshelf-data \
  --plaintext fileb://plain.txt \
  --encryption-context app=bookshelf \
  --query CiphertextBlob --output text | base64 --decode > cipher.bin
aws kms decrypt --ciphertext-blob fileb://cipher.bin \
  --encryption-context app=bookshelf \
  --query Plaintext --output text | base64 --decode

# Envelope encryption: get a data key (Plaintext for local use, CiphertextBlob to store)
aws kms generate-data-key --key-id alias/bookshelf-data --key-spec AES_256 \
  --encryption-context app=bookshelf

aws kms decrypt needs no --key-id for symmetric keys because the key identity is embedded in the ciphertext (pass it anyway to make the policy intent explicit). Reference: the AWS CLI Command Reference — kms and Create a KMS key.

To delegate a narrow permission without touching the key policy, add and later retire a grant:

aws kms create-grant --key-id alias/bookshelf-data \
  --grantee-principal arn:aws:iam::111122223333:role/orders-api-task-role \
  --operations Decrypt GenerateDataKey \
  --constraints EncryptionContextSubset={app=bookshelf}
aws kms list-grants --key-id alias/bookshelf-data
aws kms revoke-grant --key-id alias/bookshelf-data --grant-id <grant-id>

Key rotation

Rotation replaces the key material while the key ID, alias and policy stay the same; ciphertext made under older material still decrypts, because KMS keeps every version. Only symmetric keys with KMS-generated material support it; asymmetric, HMAC and imported-material keys do not, and you rotate those by creating a new key and updating the alias.

# Automatic rotation, every 365 days (the default; allowed range is 90 to 2,560 days)
aws kms enable-key-rotation --key-id alias/bookshelf-data --rotation-period-in-days 365
aws kms get-key-rotation-status --key-id alias/bookshelf-data

# Rotate right now (for example after a suspected exposure)
aws kms rotate-key-on-demand --key-id alias/bookshelf-data

Rotation does not re-encrypt existing data: old ciphertexts keep working and new encryptions use the new material. Automatic rotations are included in the key price; on-demand rotations are charged like API requests. See Rotate AWS KMS keys.

Multi-Region keys

By default a KMS key exists in one Region, and ciphertext from us-east-1 cannot be decrypted by a different key in eu-west-1. A multi-Region key is a set of interoperable keys with the same key ID (prefixed mrk-) and the same key material in different Regions, so data encrypted in one Region can be decrypted in another without a cross-Region call. Typical uses are global applications, disaster recovery, DynamoDB global tables, Aurora global databases, and Secrets Manager replicas. Each replica has its own key policy, alias and grants, and a single primary key controls shared properties such as rotation.

KEY_ID=$(aws kms create-key --multi-region --description "Bookshelf data key (MRK)" \
  --query KeyMetadata.KeyId --output text)
aws kms replicate-key --key-id "$KEY_ID" --replica-region us-west-2 \
  --region us-east-1 --policy file://key-policy.json

Multi-Region keys are not a way to share one key across Regions' IAM boundaries: you still manage each replica’s policy. See Multi-Region keys in AWS KMS.

Disable and delete a key

Deleting a KMS key destroys its key material and makes everything encrypted under it unrecoverable (a deleted symmetric key cannot be recreated, even from the same material). Because of that, AWS never deletes a key immediately: you schedule deletion with a mandatory waiting period of 7 to 30 days (default 30; the real deletion can take up to 24 hours longer). While the key is PendingDeletion it cannot be used for any cryptographic operation, and you can cancel the deletion at any time before the period ends; afterwards the key, its aliases and metadata are gone. If you are unsure, disable the key instead (reversible).

aws kms disable-key --key-id alias/bookshelf-data                       # reversible
aws kms schedule-key-deletion --key-id "$KEY_ID" --pending-window-in-days 7
aws kms describe-key --key-id "$KEY_ID" \
  --query "KeyMetadata.[KeyState,DeletionDate]" --output text
aws kms cancel-key-deletion --key-id "$KEY_ID"                          # key stays Disabled
aws kms enable-key --key-id "$KEY_ID"                                   # then re-enable it

Create a CloudWatch alarm on use attempts during the waiting period (any Decrypt of a pending key is evidence that something still needs it). A multi-Region primary key cannot be deleted until all its replicas are; a replica can be deleted at any time. See Delete an AWS KMS key.

AWS Secrets Manager

A secret is a named container for a value (a string up to 64 KB, usually JSON such as {"username":"catalog","password":"<your-password>"}) plus metadata, versions and the KMS key that encrypts it. Each change creates a new version; versions are tagged with staging labels: AWSCURRENT is what readers get by default, AWSPENDING marks a version being prepared during rotation, and AWSPREVIOUS is the one before AWSCURRENT. A secret holds up to 100 versions, and unlabeled versions older than 24 hours are removed when the count exceeds that. Secrets are Regional resources whose ARN ends with a six-character random suffix, for example arn:aws:secretsmanager:us-east-1:111122223333:secret:bookshelf/catalog/db-AbCdEf.

Create and read a secret

In the AWS Management Console

  1. Open AWS Secrets Manager (check the Region) and choose Store a new secret.

  2. Choose the Secret type: Credentials for Amazon RDS database, Credentials for other database, Other type of secret (key/value or plaintext) or Managed external secret (see Managed external secrets). For bookshelf/catalog/db choose the RDS type, enter the username and password and select the bookshelf-catalog Aurora cluster.

  3. Encryption key: keep aws/secretsmanager (the AWS managed key) or choose alias/bookshelf-data (required for cross-account sharing).

  4. Secret name: bookshelf/catalog/db, description, tags; optionally add a Resource permissions policy and Replicate secret to another Region.

  5. Configure rotation (optional now), review, Store.

  6. To read it back: open the secret > Retrieve secret value. Every read is a GetSecretValue call recorded in CloudTrail under your identity.

With the AWS CLI

# Generate a strong password locally on AWS's side; never put it in shell history
DB_PASSWORD=$(aws secretsmanager get-random-password --password-length 32 \
  --exclude-characters '"@/\' --require-each-included-type \
  --query RandomPassword --output text)

aws secretsmanager create-secret \
  --name bookshelf/catalog/db \
  --description "Bookshelf catalog database credentials" \
  --kms-key-id alias/bookshelf-data \
  --secret-string "{\"username\":\"catalog\",\"password\":\"$DB_PASSWORD\",\"host\":\"<cluster-endpoint>\",\"port\":5432,\"dbname\":\"catalog\"}" \
  --tags Key=app,Value=bookshelf

aws secretsmanager get-secret-value --secret-id bookshelf/catalog/db \
  --query SecretString --output text
aws secretsmanager describe-secret --secret-id bookshelf/catalog/db
aws secretsmanager list-secret-version-ids --secret-id bookshelf/catalog/db

To change the value write a new version (the new version becomes AWSCURRENT and the old one AWSPREVIOUS), and read a specific version by label or ID:

aws secretsmanager put-secret-value --secret-id bookshelf/catalog/db \
  --secret-string file://new-secret.json
aws secretsmanager get-secret-value --secret-id bookshelf/catalog/db \
  --version-stage AWSPREVIOUS

Keep sustained writes below about one PutSecretValue per 10 minutes per secret; faster updates create versions faster than Secrets Manager prunes them. Reference: the AWS CLI Command Reference — secretsmanager and Create an AWS Secrets Manager secret.

Rotation

Rotation changes the credential in both Secrets Manager and the service that checks it, using a four-step state machine run by a Lambda rotation function that Secrets Manager invokes:

sequenceDiagram participant SM as Secrets Manager participant Fn as Rotation Lambda participant DB as Database SM->>Fn: createSecret Fn->>SM: Store new password as AWSPENDING SM->>Fn: setSecret Fn->>DB: Set the new password for the user SM->>Fn: testSecret Fn->>DB: Connect with the AWSPENDING credentials SM->>Fn: finishSecret Fn->>SM: Move AWSCURRENT to the new version (old becomes AWSPREVIOUS)

Three rotation strategies exist, in order of preference:

  • Managed rotation (RDS, Aurora, Redshift, DocumentDB master user): let the database service own the secret. Create the instance or cluster with --manage-master-user-password and the secret is created, encrypted and rotated by the service (every 7 days by default) with no Lambda function in your account. Older guides still hand-build a rotation function for the master user; for new clusters this is no longer needed.

  • Rotation function for application users or other stores: Secrets Manager provides templates for the supported databases, in single-user and alternating-users strategies. The alternating-users strategy switches between two database users so there is no moment when the credential in use is invalid, which suits applications that hold long-lived connection pools.

  • Managed external secrets (Managed external secrets) for supported SaaS partners.

# Master password managed by the service (RDS / Aurora)
aws rds create-db-cluster --db-cluster-identifier bookshelf-catalog \
  --engine aurora-postgresql --master-username catalog_admin \
  --manage-master-user-password --master-user-secret-kms-key-id alias/bookshelf-data

# Application user: attach your rotation Lambda and a schedule
aws secretsmanager rotate-secret --secret-id bookshelf/catalog/db \
  --rotation-lambda-arn arn:aws:lambda:us-east-1:111122223333:function:bookshelf-rotate-db \
  --rotation-rules AutomaticallyAfterDays=30

# Rotate immediately, and inspect the result
aws secretsmanager rotate-secret --secret-id bookshelf/catalog/db
aws secretsmanager describe-secret --secret-id bookshelf/catalog/db \
  --query "[RotationEnabled,LastRotatedDate,NextRotationDate]"

The rotation function runs in your VPC when the database is private, so it needs a route to the Secrets Manager endpoint (a VPC interface endpoint or a NAT gateway, see VPC Networking) and permission to call Secrets Manager and decrypt the key. Applications must re-read the secret after a rotation (see the consumption patterns below); a connection pool that caches the old password for its lifetime fails at the next reconnect. Details are in Rotate AWS Secrets Manager secrets and the databases' own pages: Amazon RDS and Amazon Aurora.

Managed external secrets

Managed external secrets are a secret type, introduced in 2025, that stores and automatically rotates credentials for supported integration partners (SaaS vendors that publish a predefined secret format). Rotation runs without a Lambda function in your account, the partner defines the secret’s required JSON shape, and the full Secrets Manager feature set (IAM and resource policies, replication, CloudTrail, EventBridge) applies. In the console it is the Managed external secret option in Store a new secret; the partner list and programmatic creation steps are in Using Secrets Manager managed external secrets. Use it for partner API keys instead of hand-writing one more rotation function.

Resource policies

Like an S3 bucket policy, a resource-based policy attached to a secret says who may use it, which is how you share a secret across accounts, or add a guard rail on top of IAM. It is evaluated together with the caller’s identity policies; for a caller in another account both must allow the request, and the secret must use a customer managed key whose key policy also allows that account.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPartnerAccountRead",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::<reporting-account-id>:role/reporting-reader" },
      "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret" ],
      "Resource": "*"
    },
    {
      "Sid": "DenyEveryoneElseOutsideTheOrganization",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "secretsmanager:*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": { "aws:PrincipalOrgID": "o-exampleorgid" },
        "ArnNotEquals": {
          "aws:PrincipalArn": [
            "arn:aws:iam::<reporting-account-id>:role/reporting-reader",
            "arn:aws:iam::111122223333:role/bookshelf-key-admin"
          ]
        }
      }
    }
  ]
}
aws secretsmanager put-resource-policy --secret-id bookshelf/catalog/db \
  --resource-policy file://secret-policy.json --block-public-policy
aws secretsmanager validate-resource-policy --resource-policy file://secret-policy.json
aws secretsmanager get-resource-policy --secret-id bookshelf/catalog/db

Resource in a secret’s resource policy is always "*", meaning "this secret". Test a draft with validate-resource-policy (it flags public access and syntax problems) before attaching it; a wrong deny can lock out even your administrators. See Attach a permissions policy to a secret.

Replication

A secret can be replicated to other Regions for disaster recovery or to serve workloads close to users. Each replica is a read-only copy, encrypted with a KMS key you choose in the destination Region, kept in sync with the primary (including rotation results). A replica is billed as a secret. To make a replica an independent writable secret (failover), promote it.

aws secretsmanager replicate-secret-to-regions --secret-id bookshelf/catalog/db \
  --add-replica-regions Region=us-west-2,KmsKeyId=alias/bookshelf-data-west
aws secretsmanager describe-secret --secret-id bookshelf/catalog/db \
  --query ReplicationStatus

# In a disaster: make the replica a standalone secret
aws secretsmanager stop-replication-to-replica --secret-id bookshelf/catalog/db \
  --region us-west-2

Use a multi-Region KMS key (Multi-Region keys) to keep the encryption consistent, or a per-Region key if the Regions' compliance rules differ. Reference: Replicate AWS Secrets Manager secrets to other Regions.

Events and audit

Every Secrets Manager and KMS call is recorded by CloudTrail, including who read GetSecretValue and the KMS Decrypt it triggered, which is the main reason to prefer Secrets Manager over a plain parameter for credentials. Secrets Manager also publishes events to EventBridge (rotation start/success/failure, value changes, deletion scheduled) that you can route to SNS or a Lambda function:

aws events put-rule --name bookshelf-secret-changes --event-pattern '{
  "source": ["aws.secretsmanager"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": { "eventName": ["PutSecretValue", "UpdateSecret", "DeleteSecret", "RotationFailed"] }
}'

Monitoring and alerting patterns are on Monitoring and Observability. See Monitor AWS Secrets Manager secrets.

Deleting a secret: the recovery window

delete-secret does not remove a secret immediately. It marks the secret for deletion with a recovery window of 7 to 30 days (default 30) during which it cannot be read, its name cannot be reused, and restore-secret brings it back; afterwards it is deleted permanently. --force-delete-without-recovery skips the window and is immediate and irreversible, which is appropriate only for throwaway secrets (a test environment’s, or one created by mistake).

aws secretsmanager delete-secret --secret-id bookshelf/catalog/db --recovery-window-in-days 7
aws secretsmanager restore-secret --secret-id bookshelf/catalog/db
aws secretsmanager delete-secret --secret-id bookshelf/tmp --force-delete-without-recovery

Replicas must be removed (remove-regions-from-replication) before the primary can be deleted. A common outage is deleting a secret that a running service still reads: the task or pod keeps working until its next restart or cache refresh, then fails. Check describe-secret and CloudTrail GetSecretValue activity first.

Parameter Store SecureString

Systems Manager Parameter Store is a hierarchical key/value store (/bookshelf/prod/catalog/page-size) of three types: String, StringList and SecureString. A SecureString value is encrypted with a KMS key (the AWS managed alias/aws/ssm unless you pass --key-id) and decrypted when read with --with-decryption. Parameters are versioned, can carry labels and tags, and can be read by path to load a whole application’s configuration with one call.

Tier Limits Notes

Standard (default)

Up to 10,000 parameters per account and Region; value up to 4 KB.

No additional charge. No parameter policies.

Advanced

Up to 100,000 parameters; value up to 8 KB.

Charged per advanced parameter per month. Adds parameter policies (expiration, no-change notification) and sharing across accounts. A standard parameter can be promoted to advanced, but an advanced parameter cannot be changed back to standard; delete and recreate it.

Separately, a throughput setting controls the request rate (default throughput versus higher throughput, which is charged per interaction); tiers control storage attributes, throughput controls rate.

In the AWS Management Console

  1. Open AWS Systems Manager > Application Management > Parameter Store (check the Region) and choose Create parameter.

  2. Name: /bookshelf/prod/catalog/internal-api-key; Tier: Standard (or Advanced).

  3. Type: SecureString; KMS key source: My current account, then the AWS managed key or a customer managed key.

  4. Value: the value (for a real secret, paste it from your secret store; never type secrets into screenshots or tickets). Create parameter.

  5. Browse the hierarchy under My parameters; use History to see versions.

With the AWS CLI

aws ssm put-parameter --name /bookshelf/prod/catalog/page-size \
  --type String --value 25 --tier Standard
aws ssm put-parameter --name /bookshelf/prod/catalog/internal-api-key \
  --type SecureString --key-id alias/bookshelf-data \
  --value '<your-api-key>' --tier Standard

# One parameter, decrypted
aws ssm get-parameter --name /bookshelf/prod/catalog/internal-api-key \
  --with-decryption --query Parameter.Value --output text

# Everything for the catalog service, recursively and decrypted
aws ssm get-parameters-by-path --path /bookshelf/prod/catalog \
  --recursive --with-decryption \
  --query "Parameters[].[Name,Type]" --output table

# Update (a new version is created) and label
aws ssm put-parameter --name /bookshelf/prod/catalog/page-size --type String \
  --value 50 --overwrite

Parameter Store can also read a Secrets Manager secret through the reserved path /aws/reference/secretsmanager/<secret-name>, so one ssm:GetParameter call (with --with-decryption) works for both stores; a service that has only Parameter Store integration, such as an older CloudFormation or Lambda feature, can still get a rotated secret that way. Use IAM permissions on ssm:GetParameter and secretsmanager:GetSecretValue for that path.

Consuming secrets from applications

flowchart LR subgraph AWS["AWS"] SM["Secrets Manager
bookshelf/catalog/db"] PS["Parameter Store
/bookshelf/*"] KMS["KMS key
alias/bookshelf-data"] SM --- KMS PS --- KMS end SDK["Java SDK or
Spring Cloud AWS
(task role, Pod Identity)"] -->|GetSecretValue / GetParameter| SM SDK --> PS LAM["Lambda function
+ Parameters and Secrets extension
localhost:2773"] -->|cached reads| SM ECS["ECS task
secrets in task definition
(execution role)"] -->|resolved at task start| SM EKS["EKS pod
Secrets Store CSI Driver + ASCP
or External Secrets Operator"] -->|Pod Identity role| SM

Whatever the path, the identity is the one that already runs the code (ECS task or execution role, Lambda execution role, EKS Pod Identity role) and the IAM policy lists exactly the secrets and the key that identity needs; long-lived access keys never appear in the container. A least-privilege identity policy for the catalog-api task role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadCatalogSecret",
      "Effect": "Allow",
      "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret" ],
      "Resource": "arn:aws:secretsmanager:us-east-1:111122223333:secret:bookshelf/catalog/*"
    },
    {
      "Sid": "ReadCatalogParameters",
      "Effect": "Allow",
      "Action": [ "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath" ],
      "Resource": "arn:aws:ssm:us-east-1:111122223333:parameter/bookshelf/prod/catalog*"
    },
    {
      "Sid": "DecryptWithTheBookshelfKey",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:us-east-1:111122223333:key/<key-id>",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": [
            "secretsmanager.us-east-1.amazonaws.com",
            "ssm.us-east-1.amazonaws.com"
          ]
        }
      }
    }
  ]
}

Two details that cause real incidents: the secret ARN in a policy ends with a random suffix, so use a trailing or a name prefix (bookshelf/catalog/) rather than the exact ARN, which would not match after the secret is recreated; and the kms:Decrypt permission is required in addition to the Secrets Manager permission whenever the secret uses a customer managed key (it is not needed for the AWS managed aws/secretsmanager key within the same account).

Java SDK, with caching

AWS SDK for Java 2.x with the default credential chain; SecretsManagerClient calls cost money and add latency, so cache the value and refresh it:

package com.example.bookshelf.config;

import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;

import java.time.Duration;
import java.time.Instant;

public class DbSecretProvider {
    private final SecretsManagerClient client = SecretsManagerClient.create();
    private final Duration ttl = Duration.ofMinutes(5);
    private volatile String cached;
    private volatile Instant loadedAt = Instant.MIN;

    public String dbSecretJson() {
        if (cached == null || Instant.now().isAfter(loadedAt.plus(ttl))) {
            cached = client.getSecretValue(GetSecretValueRequest.builder()
                    .secretId("bookshelf/catalog/db").build()).secretString();
            loadedAt = Instant.now();
        }
        return cached;
    }
}

For production, prefer the maintained caching library (com.amazonaws.secretsmanager:aws-secretsmanager-caching-java, whose 2.x line uses the SDK v2) with its SecretCache and SecretCacheConfiguration, which also handles version stages and refresh jitter, and read the AWS SDK for Java 2.x Get a Secrets Manager secret value using Java. Never log the secret JSON.

Spring Cloud AWS

Bookshelf’s catalog-api is a Spring Boot service, so it imports secrets and parameters straight into the Environment with Spring Cloud AWS (4.x for Spring Boot 4, 3.4.x for Spring Boot 3.5), using the credentials of the task role. The mechanics of spring.config.import, profiles and precedence are on Configuration and Profiles; the Bookshelf-specific lines are:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.awspring.cloud</groupId>
      <artifactId>spring-cloud-aws-dependencies</artifactId>
      <version>${spring.cloud.aws.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-starter-secrets-manager</artifactId>
  </dependency>
  <dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-starter-parameter-store</artifactId>
  </dependency>
</dependencies>
spring:
  config:
    import:
      - "aws-secretsmanager:bookshelf/catalog/db"
      - "optional:aws-parameterstore:/bookshelf/prod/catalog/"
  datasource:
    url: jdbc:postgresql://${host}:${port}/${dbname}
    username: ${username}
    password: ${password}

The keys of the secret’s JSON (username, password, host, port, dbname) become properties, so the datasource above resolves from the secret; Parameter Store entries under the prefix become properties named by their last path segments. Spring Cloud AWS reads values at start-up; to pick up a rotated password at run time enable its reload support (spring.cloud.aws.secretsmanager.reload.strategy, see the Spring Cloud AWS reference) or restart the service after rotation. The optional: prefix keeps local development working without AWS credentials.

ECS and Fargate

In a task definition, secrets entries (not environment) tell the ECS agent to fetch the value when the task starts and inject it as an environment variable. The task execution role (not the task role) needs the read permissions, because ECS resolves them before your code runs. See ECS and Fargate for the roles themselves.

{
  "family": "catalog-api",
  "executionRoleArn": "arn:aws:iam::111122223333:role/catalog-api-execution-role",
  "taskRoleArn": "arn:aws:iam::111122223333:role/catalog-api-task-role",
  "containerDefinitions": [{
    "name": "catalog-api",
    "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/bookshelf/catalog-api:1.0.0",
    "secrets": [
      {
        "name": "DB_PASSWORD",
        "valueFrom": "arn:aws:secretsmanager:us-east-1:111122223333:secret:bookshelf/catalog/db-AbCdEf:password::"
      },
      {
        "name": "DB_USERNAME",
        "valueFrom": "arn:aws:secretsmanager:us-east-1:111122223333:secret:bookshelf/catalog/db-AbCdEf:username::"
      },
      {
        "name": "PAGE_SIZE",
        "valueFrom": "arn:aws:ssm:us-east-1:111122223333:parameter/bookshelf/prod/catalog/page-size"
      }
    ]
  }]
}

The format for Secrets Manager is secret-arn:json-key:version-stage:version-id: the trailing :: means "`AWSCURRENT`, latest version", and omitting the :password:: suffix injects the whole JSON string. Parameter Store entries use the parameter ARN, or just the name when the parameter is in the same Region. The execution role needs secretsmanager:GetSecretValue, ssm:GetParameters and kms:Decrypt on the keys involved (the policy above, attached to the execution role). The value is resolved once, at task start: after a rotation (or a new put-secret-value) start a new deployment with aws ecs update-service --cluster bookshelf --service catalog-api --force-new-deployment so the tasks restart and re-read, or let the application read the secret itself through the SDK. Reference: Pass Secrets Manager secrets through Amazon ECS environment variables and Pass Systems Manager parameters.

Lambda

Lambda’s own environment variables are encrypted but not rotated, so credentials usually come from a secret at run time. Add the AWS Parameters and Secrets Lambda Extension as a layer: it runs a local HTTP cache on port 2773 that calls Secrets Manager or Parameter Store with the function’s execution role, caches results (TTL up to 300 seconds, default 300) and spares your function an SDK dependency. The same local-endpoint design is also available as the AWS Workload Credentials Provider (formerly named the Secrets Manager Agent), a standalone binary for containers, EC2 and Lambda, which uses the same port and the X-Aws-Parameters-Secrets-Token header.

# Resolve the current extension layer ARN for the Region (x86_64; use .../arm64/latest for Graviton)
LAYER_ARN=$(aws ssm get-parameter \
  --name /aws/service/aws-parameters-and-secrets-lambda-extension/x86/latest \
  --query Parameter.Value --output text)

aws lambda update-function-configuration --function-name notifier \
  --layers "$LAYER_ARN" \
  --environment "Variables={SECRETS_MANAGER_TTL=300,PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL=INFO}"

The function then calls the local endpoint with the session token as the header (the extension rejects requests without it; this is its SSRF protection):

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:2773/secretsmanager/get?secretId=bookshelf%2Fcatalog%2Fdb"))
    .header("X-Aws-Parameters-Secrets-Token", System.getenv("AWS_SESSION_TOKEN"))
    .build();
String json = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString()).body();

Parameter Store values use /systemsmanager/parameters/get?name=<url-encoded-name>&withDecryption=true. The execution role needs the same secretsmanager:GetSecretValue / ssm:GetParameter and kms:Decrypt statements as above. With SnapStart the AWS_SESSION_TOKEN variable is not always set; read the token from the SDK credential provider chain instead. Because the extension has no cache invalidation, a rotated secret can be stale for up to the TTL; lower SECRETS_MANAGER_TTL or retry once after an authentication failure. See AWS Lambda, and the Use Secrets Manager secrets in Lambda functions page.

EKS

Kubernetes Secret objects are only base64-encoded; on EKS, the recommended pattern keeps the source of truth in Secrets Manager and brings values into pods in one of two ways. Both authenticate with EKS Pod Identity (see EKS Clusters and Deploying Microservices on EKS) so that only the service account that needs the secret can read it.

Secrets Store CSI Driver with the AWS Secrets and Configuration Provider (ASCP) mounts secrets as files in a volume and can sync them to a Kubernetes Secret:

helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
  --namespace kube-system --set syncSecret.enabled=true --set enableSecretRotation=true

helm repo add aws-secrets-manager https://aws.github.io/secrets-store-csi-driver-provider-aws
helm install secrets-provider-aws aws-secrets-manager/secrets-store-csi-driver-provider-aws \
  --namespace kube-system

aws eks create-pod-identity-association --cluster-name bookshelf-eks \
  --namespace bookshelf --service-account catalog-api \
  --role-arn arn:aws:iam::111122223333:role/catalog-api-pod-role

The role’s trust policy uses the Pod Identity principal and the same permission policy as above:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "pods.eks.amazonaws.com" },
    "Action": [ "sts:AssumeRole", "sts:TagSession" ]
  }]
}
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: bookshelf-catalog-db
  namespace: bookshelf
spec:
  provider: aws
  parameters:
    usePodIdentity: "true"
    objects: |
      - objectName: "bookshelf/catalog/db"
        objectType: "secretsmanager"
        jmesPath:
          - path: username
            objectAlias: db-username
          - path: password
            objectAlias: db-password
  secretObjects:
    - secretName: catalog-db
      type: Opaque
      data:
        - objectName: db-username
          key: username
        - objectName: db-password
          key: password
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-api
  namespace: bookshelf
spec:
  replicas: 2
  selector:
    matchLabels: { app: catalog-api }
  template:
    metadata:
      labels: { app: catalog-api }
    spec:
      serviceAccountName: catalog-api
      containers:
        - name: catalog-api
          image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/bookshelf/catalog-api:1.0.0
          volumeMounts:
            - name: secrets
              mountPath: /mnt/secrets-store
              readOnly: true
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef: { name: catalog-db, key: password }
      volumes:
        - name: secrets
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: bookshelf-catalog-db

The mount (and the synced Secret) exist only while a pod that mounts the volume runs. With enableSecretRotation the driver re-reads the secret periodically and updates the files, but environment variables from a synced Secret are fixed at pod start, so mount the file and re-read it, or restart the Deployment. Private clusters need a VPC endpoint for AWS STS and for Secrets Manager. See Use ASCP with Pod Identity for Amazon EKS and the Kubernetes primer in Orchestration: Swarm Mode and Kubernetes.

External Secrets Operator (ESO) is the alternative: a CNCF-hosted controller that polls Secrets Manager or Parameter Store and writes ordinary Kubernetes Secret objects, so pods consume them with the usual secretKeyRef or envFrom and no volume. It uses the default credential chain, so it works with the Pod Identity or IRSA role of the operator itself:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: aws-secrets-manager
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: catalog-db
  namespace: bookshelf
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: catalog-db
  data:
    - secretKey: password
      remoteRef:
        key: bookshelf/catalog/db
        property: password

Choose ASCP when you want values to exist only as files in running pods and to keep Kubernetes Secret objects out of etcd; choose ESO when you want GitOps-friendly declarative ExternalSecret objects, support for several backends (Vault, Azure Key Vault, Google Secret Manager) or a cluster-wide store. Both bill you for Secrets Manager API calls, so set a sensible refresh interval. Runtime configuration (ConfigMaps) is on Runtime Configuration; the Spring-side view of Kubernetes Secrets is on Configuration and Profiles.

GitHub Actions secrets or Secrets Manager

CI pipelines authenticate to AWS with OIDC federation and a short-lived role (see Identity and Access and Docker CI/CD with GitHub Actions), so no AWS key needs to be stored in GitHub at all. Keep secrets that only the pipeline needs (a registry token, a signing passphrase) in GitHub Actions secrets, and secrets that the running application needs in Secrets Manager; a workflow that must read one can do so after assuming its role, for example with the aws-actions/aws-secretsmanager-get-secrets action or aws secretsmanager get-secret-value (mask the value with ::add-mask:: before it can reach a log). The CI/CD page is Developer Tools and DevOps.

Certificates and CloudHSM, in brief

AWS Certificate Manager (ACM) issues and renews public TLS certificates for free for use with integrated services (CloudFront, ALB, API Gateway, and others) and issues private certificates from AWS Private CA for internal mTLS, with a separate monthly CA charge. Public certificates validate through DNS (a CNAME in the hosted zone) and renew automatically; certificates for CloudFront must be requested in us-east-1. See CloudFront and CDN Delivery, Load Balancers and Gateways and Route 53, DNS and ACM Certificates. AWS CloudHSM provides single-tenant HSM clusters you control (FIPS 140-3 Level 3) when a standard requires dedicated hardware or you need to run your own PKCS#11/JCE software; most workloads are well served by KMS, which is multi-tenant but built on validated HSMs.

Pricing, limits and cost drivers

Prices change and vary by Region; they are deliberately not repeated here. Check the Secrets Manager pricing, KMS pricing and Systems Manager pricing pages (checked 2026-10-01) before sizing. The cost drivers are:

  • Secrets Manager: a monthly fee for every secret (including each replica and each secret for a service that creates one, such as a managed master password), plus a charge per 10,000 API calls. A recently deleted secret is billed until the recovery window ends. Cache reads (SDK cache, Lambda extension, CSI driver with a sensible rotation interval) to cut calls. A new account usually has a free trial for each new secret; confirm on the pricing page.

  • Parameter Store: standard parameters carry no storage charge and the default throughput is free; advanced parameters are charged per parameter per month, and higher throughput per interaction.

  • KMS: a monthly fee for each customer managed key (each multi-Region replica counts as a key, and each key version is not charged), plus a charge per 10,000 requests; AWS managed keys have no monthly fee. A busy S3 bucket with SSE-KMS can generate millions of requests; S3 Bucket Keys (see S3 Security and Static Hosting) reduce the KMS call volume dramatically.

Key quotas to design around (all Regional; verify in Service Quotas):

Item Limit

Secrets Manager secrets per Region

500,000 (not adjustable)

Secret value size

65,536 bytes

Versions per secret

100

GetSecretValue request rate

10,000 per second per Region

Control-plane writes (create, put, update)

50 per second per Region combined

Resource policy size

20,480 characters

Parameter Store parameters

10,000 (standard) / 100,000 (advanced) per account and Region

SecureString value size

4 KB (standard) / 8 KB (advanced)

KMS Encrypt plaintext

4,096 bytes

KMS deletion waiting period

7 to 30 days

Secrets Manager recovery window

7 to 30 days

KMS request quotas are shared across all uses of a key in the account and Region; very high-volume encryption should use envelope encryption with cached data keys rather than a KMS call per record.

Troubleshooting

Symptom Likely cause What to check

AccessDeniedException on GetSecretValue

The identity lacks secretsmanager:GetSecretValue, the ARN pattern does not match (the six-character suffix), or a resource policy denies it.

Policy simulator, the exact ARN from describe-secret, the secret’s resource policy and any SCP.

AccessDeniedException mentioning kms:Decrypt, or KMSInvalidStateException

The secret or parameter uses a customer managed key and the identity cannot decrypt it, or the key is disabled or pending deletion.

aws kms describe-key for the state, the key policy and the kms:ViaService condition, and cross-account grants; the AWS managed key cannot be used from another account.

ECS task stops with ResourceInitializationError: unable to pull secrets or registry auth

The execution role cannot read the secret/parameter or key, or the task has no network path to Secrets Manager, Systems Manager or KMS.

The execution role policy, a NAT gateway or VPC endpoints for secretsmanager, ssm and kms, and the ARN format in valueFrom.

Application still uses the old password after rotation

The value is cached or injected once (ECS, env vars, a connection pool, Lambda extension TTL).

Force a new deployment, shorten the cache TTL, use the alternating-users rotation strategy, and re-read on authentication failure.

Rotation fails with RotationFailed

The rotation function cannot reach the database or Secrets Manager endpoint, lacks permissions, or the database user cannot change its own password.

The function’s CloudWatch Logs, the security groups, a Secrets Manager VPC endpoint, and the AWSPENDING version.

InvalidRequestException: You can’t create this secret because a secret with this name is already scheduled for deletion

Names cannot be reused during the recovery window.

restore-secret, or wait, or delete with --force-delete-without-recovery for a throwaway secret.

Pod stays in ContainerCreating with a CSI mount error

The ASCP or driver is not installed, the Pod Identity association is missing, usePodIdentity is not set, or the namespace/service-account does not match.

kubectl describe pod, the provider pod logs in kube-system, and aws eks list-pod-identity-associations --cluster-name bookshelf-eks.

ThrottlingException or TooManyRequestsException

Too many GetSecretValue or KMS calls (a cold-start burst, a tight loop, no caching).

Cache, stagger start-up, add retry with exponential backoff and jitter, and request a quota increase only after caching.

ValidationException or ParameterNotFound from get-parameter

Wrong name (leading /, case), wrong Region, or a SecureString read without --with-decryption.

aws ssm describe-parameters, the Region in AWS_REGION, and the full path.

Locked out of a key

The key policy has no root statement and the only named principal was deleted.

Contact AWS Support; keep the root statement and a break-glass role in every key policy.

Clean up

Secrets and keys carry monthly fees while they exist, but both have deliberate safety nets that make "delete" a two-step operation. Remove the resources in dependency order:

# 1. Stop consumers first (ECS service, Lambda function, EKS pod associations)
aws eks delete-pod-identity-association --cluster-name bookshelf-eks \
  --association-id <association-id>

# 2. Remove replicas, then schedule the secret for deletion (recovery window 7 to 30 days)
aws secretsmanager remove-regions-from-replication --secret-id bookshelf/catalog/db \
  --remove-replica-regions us-west-2
aws secretsmanager delete-secret --secret-id bookshelf/catalog/db --recovery-window-in-days 7
# Throwaway secrets only: immediate and irreversible
aws secretsmanager delete-secret --secret-id bookshelf/tmp --force-delete-without-recovery

# 3. Delete parameters
aws ssm delete-parameters --names /bookshelf/prod/catalog/page-size \
  /bookshelf/prod/catalog/internal-api-key

# 4. Delete the key last: remove its alias, then schedule deletion (7 to 30 days)
aws kms delete-alias --alias-name alias/bookshelf-data
aws kms schedule-key-deletion --key-id "$KEY_ID" --pending-window-in-days 7

Order matters: do not schedule a key’s deletion while any secret, parameter, bucket, volume or database still uses it; those resources become unreadable when the waiting period ends. Until the recovery window ends a secret, and until the waiting period ends a key, can still be restored (restore-secret, cancel-key-deletion), and the monthly fee for a key is charged while it exists, so confirm on the KMS pricing page whether a PendingDeletion key is billed. If you used CloudFormation, aws cloudformation delete-stack follows the resource’s DeletionPolicy, and for secrets and keys the template’s retention settings control this behaviour. Remove any SecretProviderClass, ExternalSecret and CSI/ESO Helm releases with helm uninstall, and delete the CloudWatch alarms and EventBridge rules created above (aws events delete-rule --name bookshelf-secret-changes, after removing its targets).