Terraform module to create Amazon Secrets Manager resources with comprehensive input validation and advanced features.
AWS Secrets Manager helps you protect secrets needed to access your applications, services, and IT resources. The service enables you to easily rotate, manage, and retrieve database credentials, API keys, and other secrets throughout their lifecycle.
- ✅ Input Validation: Comprehensive validation for all variables to prevent configuration errors
- ✅ Type Safety: Strongly typed variables with structured object definitions
- ✅ Secret Rotation: Built-in support for automatic secret rotation with Lambda functions
- ✅ Cross-Region Replication: Support for replicating secrets across AWS regions
- ✅ KMS Encryption: Support for customer-managed KMS keys
- ✅ Resource Policies: Attach custom IAM policies to secrets
- ✅ Native Policy Replication: AWS Secrets Manager replicates resource policies to replica regions automatically
- ✅ Flexible Secret Types: Support for plain text, key/value pairs, and binary secrets
| Component | Minimum version | Notes |
|---|---|---|
| Terraform | >= 1.11.0 |
Required for ephemeral resources and write-only arguments. |
| AWS provider | >= 6.50.0 |
Required so the module can use aws_secretsmanager_secret_version.secret_string_wo / secret_string_wo_version safely. Version 6.50.0 includes Secrets Manager fixes for final-plan consistency, creation eventual consistency, empty version_stages, and switching between secret_string and secret_string_wo. |
The module declares the AWS provider minimum at the root, so all usage modes use the same compatibility policy. Ephemeral mode additionally requires setting secret_string_wo_version >= 1 for every managed secret value.
Check the examples folder where you can see the complete compilation of snippets.
module "secrets-manager-1" {
source = "lgallard/secrets-manager/aws"
secrets = {
secret-1 = {
description = "My secret 1"
recovery_window_in_days = 7
secret_string = "This is an example"
},
secret-2 = {
description = "My secret 2"
recovery_window_in_days = 7
secret_string = "This is another example"
}
}
tags = {
Owner = "DevOps team"
Environment = "dev"
Terraform = true
}
}module "secrets-manager-2" {
source = "lgallard/secrets-manager/aws"
secrets = {
secret-kv-1 = {
description = "This is a key/value secret"
secret_key_value = {
key1 = "value1"
key2 = "value2"
}
recovery_window_in_days = 7
},
secret-kv-2 = {
description = "Another key/value secret"
secret_key_value = {
username = "user"
password = "topsecret"
}
tags = {
app = "web"
}
recovery_window_in_days = 7
},
}
tags = {
Owner = "DevOps team"
Environment = "dev"
Terraform = true
}
}module "secrets-manager-3" {
source = "lgallard/secrets-manager/aws"
secrets = {
secret-binary-1 = {
description = "This is a binary secret"
secret_binary = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDt4TcI58h4G0wR+GcDY+0VJR10JNvG92jEKGaKxeMaOkfsXflVGsYXbfVBBCG/n3uHtTse7baYLB6LWQAuYWL1SHJVhhTQ7pPiocFWibAvJlVo1l7qJEDu2OxKpWEleCE+p3ufNXAy7v5UFO7EOnj0Zg6R3F/MiAWbQnaEHcYzNtogyC24YBecBLrBXZNi1g0AN1hM9k+3XvWUYTf9vPv8LIWnqo7y4Q2iEGWWurf37YFl1LzX4mG/Co+Vfe5TlZSe2YPMYWlw0ZKaKvwzInRR6dPMAflo3ABzlduiIbSdp110uGqB8i2M8eGXNDxR7Ni4nnLWnT9r1cpWhXWP6pAG4Xg8+x7+PIg/pgjgJNmsURw+jPD6+hkCw2Vz16EIgkC2b7lj0V6J4LncUoRzU/1sAzCQ4tspy3SKBUinYoxbDvXleF66FHEjfparnvNwfslBx0IJjG2uRwuX6zrsNIsGF1stEjz+eyAOtFV4/wRjRcCNDZvl1ODzIvwf8pAWddE= lgallard@server1"
recovery_window_in_days = 7
},
secret-binary-2 = {
name = "secret-binary-2"
description = "Another binary secret"
secret_binary = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCzc818NSC6oJYnNjVWoF43+IuQpqc3WyS8BWZ50uawK5lY/aObweX2YiXPv2CoVvHUM0vG7U7BDBvNi2xwsT9n9uT27lcVQsTa8iDtpyoeBhcj3vJ60Jd04UfoMP7Og6UbD+KGiaqQ0LEtMXq6d3i619t7V0UkaJ4MXh2xl5y3bV4zNzTXdSScJnvMFfjLW0pJOOqltLma3NQ9ILVdMSK2Vzxc87T+h/jp0VuUAX4Rx9DqmxEU/4JadXmow/BKy69KVwAk/AQ8jL7OwD2YAxlMKqKnOsBJQF27YjmMD240UjkmnPlxkV8+g9b2hA0iM5GL+5MWg6pPUE0BYdarCmwyuaWYhv/426LnfHTz9UVC3y9Hg5c4X4I6AdJJUmarZXqxnMe9jJiqiQ+CAuxW3m0gIGsEbUul6raG73xFuozlaXq3J+kMCVW24eG2i5fezgmtiysIf/dpcUo+YLkX+U8jdMQg9IwCY0bf8XL39kwJ7u8uWU8+7nMcS9VQ5llVVMk= lgallard@server2"
recovery_window_in_days = 7
tags = {
app = "web"
}
}
}
tags = {
Owner = "DevOps team"
Environment = "dev"
Terraform = true
}
}module "secrets-manager-kms" {
source = "lgallard/secrets-manager/aws"
secrets = {
database-credentials = {
description = "Database credentials encrypted with customer KMS key"
secret_key_value = {
username = "admin"
password = "super-secret-password"
host = "db.example.com"
port = "5432"
}
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
recovery_window_in_days = 7
}
}
tags = {
Environment = "production"
KMSEncrypted = "true"
}
}data "aws_iam_policy_document" "secret_policy" {
statement {
sid = "AllowApplicationAccess"
effect = "Allow"
principals {
type = "AWS"
identifiers = ["arn:aws:iam::123456789012:role/MyApplicationRole"]
}
actions = ["secretsmanager:GetSecretValue"]
resources = ["*"]
}
}
module "secrets-manager-policy" {
source = "lgallard/secrets-manager/aws"
secrets = {
app-config = {
description = "Application configuration with resource policy"
secret_string = jsonencode({
api_key = "secret-api-key"
config = "production-config"
})
policy = data.aws_iam_policy_document.secret_policy.json
recovery_window_in_days = 14
}
}
}module "secrets-manager-replication" {
source = "lgallard/secrets-manager/aws"
secrets = {
global-config = {
description = "Global configuration replicated across regions"
secret_string = "global-configuration-data"
replica_regions = {
"us-west-2" = "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
"eu-west-1" = "arn:aws:kms:eu-west-1:123456789012:key/87654321-4321-4321-4321-210987654321"
}
force_overwrite_replica_secret = true
recovery_window_in_days = 7
}
}
tags = {
ReplicationEnabled = "true"
GlobalResource = "true"
}
}AWS Secrets Manager replication copies encrypted secret data and metadata, including resource policies, to replica regions. Configure policy on the primary regular or rotating secret; AWS propagates that policy to replicas automatically.
module "secrets-manager-replication" {
source = "lgallard/secrets-manager/aws"
secrets = {
global-config = {
description = "Global configuration replicated across regions"
secret_string = "global-configuration-data"
policy = data.aws_iam_policy_document.secret_policy.json
replica_regions = {
"us-west-2" = "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
"eu-west-1" = "arn:aws:kms:eu-west-1:123456789012:key/87654321-4321-4321-4321-210987654321"
}
}
}
}Policy replication is asynchronous. AWS rejects direct resource-policy updates against replica secrets; update the primary secret policy instead. If a replica must diverge, promote it to a standalone secret first.
Note: Rotating secrets (
rotate_secrets) supportreplica_regionsusing the same input shape as regularsecrets. If you previously setreplica_regionsunderrotate_secrets, version 1.2.0 and later manage those replicas instead of ignoring that attribute.Migration note for 1.2.0 users:
replicate_policy,secret_replica_policies, androtate_secret_replica_policieshave been removed because direct policy writes to replicas fail. Removereplicate_policy = truefrom module inputs and remove references to the removed outputs. If Terraform had already created any replica policy resources in state, the next apply will destroy them; this is expected because AWS manages replica policies from the primary secret.
If you need to configure lifecycle rules for your secrets (such as prevent_destroy, create_before_destroy, or ignore_changes), you must configure them on individual resources since lifecycle blocks cannot be used with module calls.
Here are the recommended approaches:
# Create secret directly with lifecycle protection
resource "aws_secretsmanager_secret" "critical_secret" {
name = "critical-database-password"
description = "Critical database password - protected from accidental deletion"
lifecycle {
prevent_destroy = true
}
tags = {
Environment = "production"
Critical = "true"
}
}
resource "aws_secretsmanager_secret_version" "critical_secret_version" {
secret_id = aws_secretsmanager_secret.critical_secret.id
secret_string = "super-secret-password"
lifecycle {
ignore_changes = [secret_string]
}
}# Use module for convenience, then protect specific secrets separately
module "secrets-manager" {
source = "lgallard/secrets-manager/aws"
secrets = {
app-config = {
description = "Application configuration"
secret_string = "app-configuration-data"
}
}
}
# Create additional protected secret with lifecycle rules
resource "aws_secretsmanager_secret" "protected_secret" {
name = "critical-database-password"
description = "Critical database password - protected from accidental deletion"
lifecycle {
prevent_destroy = true
create_before_destroy = true
}
tags = {
Environment = "production"
Critical = "true"
}
}
resource "aws_secretsmanager_secret_version" "protected_version" {
secret_id = aws_secretsmanager_secret.protected_secret.id
secret_string = "super-secret-password"
lifecycle {
ignore_changes = [secret_string]
}
}This approach follows Terraform's requirement that lifecycle blocks only contain literal values and can only be used on resource blocks, not module calls.
If you need to rotate your secrets, use rotate_secrets map to define them. The lambda function must exist and have the right permissions to rotate secrets in AWS Secrets Manager:
module "secrets-manager-rotation" {
source = "lgallard/secrets-manager/aws"
rotate_secrets = {
database-password = {
description = "Database password rotated every 30 days"
secret_string = "initial-password"
rotation_lambda_arn = "arn:aws:lambda:us-east-1:123456789012:function:rotate-secret"
automatically_after_days = 30
recovery_window_in_days = 15
},
api-key = {
description = "API key rotated weekly"
secret_string = "initial-api-key"
rotation_lambda_arn = "arn:aws:lambda:us-east-1:123456789012:function:rotate-secret"
automatically_after_days = 7
recovery_window_in_days = 7
}
}
tags = {
Owner = "DevOps team"
Environment = "dev"
Terraform = true
}
}
# Lambda function for rotation (example)
# AWS templates available at https://github.com/aws-samples/aws-secrets-manager-rotation-lambdas
module "rotate_secret_lambda" {
source = "spring-media/lambda/aws"
version = "5.2.0"
filename = "secrets_manager_rotation.zip"
function_name = "rotate-secret"
handler = "secrets_manager_rotation.lambda_handler"
runtime = "python3.9"
source_code_hash = filebase64sha256("${path.module}/secrets_manager_rotation.zip")
environment = {
variables = {
SECRETS_MANAGER_ENDPOINT = "https://secretsmanager.us-east-1.amazonaws.com"
}
}
}
resource "aws_lambda_permission" "allow_secret_manager_call_lambda" {
function_name = module.rotate_secret_lambda.function_name
statement_id = "AllowExecutionSecretManager"
action = "lambda:InvokeFunction"
principal = "secretsmanager.amazonaws.com"
}You can define different types of secrets (string, key/value, or binary) in the same secrets or rotate_secrets map:
module "secrets-manager-mixed" {
source = "lgallard/secrets-manager/aws"
secrets = {
plain-text-secret = {
description = "A plain text secret"
recovery_window_in_days = 7
secret_string = "This is a plain text secret"
}
key-value-secret = {
description = "A key/value secret for database credentials"
secret_key_value = {
username = "dbuser"
password = "dbpassword"
host = "database.example.com"
port = "5432"
database = "myapp"
}
recovery_window_in_days = 14
tags = {
Type = "database"
}
}
binary-secret = {
description = "SSH private key"
secret_binary = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC..."
recovery_window_in_days = 30
tags = {
Type = "ssh-key"
}
}
}
tags = {
Owner = "DevOps team"
Environment = "dev"
Terraform = true
}
}This module includes comprehensive input validation to prevent configuration errors:
- Must contain only alphanumeric characters, hyphens, underscores, periods, forward slashes, at signs, plus signs, and equal signs
- Must be between 1 and 512 characters long
- Examples:
my-secret,app/config,db_credentials
- Must be 0 (immediate deletion) or between 7-30 days
- Default: 30 days
- Must be between 1-365 days for
automatically_after_days - Default: 30 days
- Only
AWSCURRENTandAWSPENDINGare valid - Example:
["AWSCURRENT"]
- Must be valid KMS key ARN, alias, or key ID format
- Examples:
- ARN:
arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012 - Alias:
alias/my-key - Key ID:
12345678-1234-1234-1234-123456789012
- ARN:
- Keys cannot start with
aws:(case insensitive) - Keys must be 1-128 characters long
- Values must be 256 characters or less
Error: Secret names must contain only alphanumeric characters, hyphens, underscores, periods, forward slashes, at signs, plus signs, and equal signs.
Solution: Check your secret names for invalid characters. Only use: a-z, A-Z, 0-9, -, _, ., /, @, +, =
Error: Recovery window must be 0 (for immediate deletion) or between 7 and 30 days.
Solution: Set recovery_window_in_days to either 0 or a value between 7 and 30.
Error: KMS key ID must be a valid KMS key ARN, alias, or key ID format.
Solution: Ensure your KMS key follows one of these formats:
arn:aws:kms:region:account:key/key-idalias/key-aliaskey-id(UUID format)
Error: All rotate_secrets must have a valid rotation_lambda_arn specified.
Solution: When using rotate_secrets, always provide a valid rotation_lambda_arn. The Lambda function must exist and have proper permissions.
Error: Tag keys cannot start with 'aws:' (case insensitive).
Solution: Remove any tags that start with aws:, AWS:, or any case variation.
-
Large Numbers of Secrets: If you have many secrets, consider splitting them across multiple module instances to improve Terraform performance.
-
Cross-Region Replication: Be aware that replication increases costs and may impact performance. Only replicate secrets that truly need global availability.
-
Rotation Frequency: More frequent rotations increase Lambda costs and API calls. Balance security requirements with operational costs.
-
Least Privilege Access: Use resource policies to grant minimal required permissions.
-
Encryption: Always use KMS encryption for sensitive secrets, preferably with customer-managed keys.
-
Monitoring: Enable CloudTrail logging for Secrets Manager API calls to monitor access patterns.
-
Rotation: Implement regular rotation for database credentials and API keys.
-
Version Management: Use version stages appropriately and avoid storing multiple active versions unnecessarily.
Issue #13 highlighted the fact that changing the secrets order will recreate the secrets (for example, adding a new secret in the top of the list o removing a secret that is not the last one). The suggested approach to tackle this issue was to use for_each to iterate over a map of secrets.
Version 0.5.0 has this implementation, but it's not backward compatible. Therefore you must migrate your Terraform code and the objects in the tfstate.
Before 0.5.0 your secrets were defined as list as follow:
secrets = [
{
name = "secret-1"
description = "My secret 1"
recovery_window_in_days = 7
secret_string = "This is an example"
},
]
After version 0.5.0 you have to define you secrets as a map:
secrets = {
secret-1 = {
description = "My secret 1"
recovery_window_in_days = 7
secret_string = "This is an example"
},
}
Notice that the map key is the name of the secret, thefore a name field is not needed anymore.
To avoid recreating your already deploy secrets you can rename or move the object in the tfstate file as follow:
terraform state mv 'module.secrets-manager-1.aws_secretsmanager_secret_version.sm-sv['0']' 'module.secrets-manager-1.aws_secretsmanager_secret_version.sm-sv["secret-1"]'
Another option is to use a script to iterate over your secrets. In the migration-scripts folder you'll find a couple of scripts that can be used as a starting point.
For example, to migrate a module named secrets-manager-1 run the script as follow:
$ ./secret_list_to_map.sh secrets-manager-1
Move "module.secrets-manager-1.aws_secretsmanager_secret.sm[0]" to "module.secrets-manager-1.aws_secretsmanager_secret.sm[\"secret-1\"]"
Successfully moved 1 object(s).
Move "module.secrets-manager-1.aws_secretsmanager_secret_version.sm-sv[0]" to "module.secrets-manager-1.aws_secretsmanager_secret_version.sm-sv[\"secret-1\"]"
Successfully moved 1 object(s).
Move "module.secrets-manager-1.aws_secretsmanager_secret.sm[1]" to "module.secrets-manager-1.aws_secretsmanager_secret.sm[\"secret-2\"]"
Successfully moved 1 object(s).
Move "module.secrets-manager-1.aws_secretsmanager_secret_version.sm-sv[1]" to "module.secrets-manager-1.aws_secretsmanager_secret_version.sm-sv[\"secret-2\"]"
Successfully moved 1 object(s).
| Name | Version |
|---|---|
| terraform | >= 1.11.0 |
| aws | >= 6.50.0 |
| Name | Version |
|---|---|
| aws | 6.53.0 |
No modules.
| Name | Type |
|---|---|
| aws_secretsmanager_secret.rsm | resource |
| aws_secretsmanager_secret.sm | resource |
| aws_secretsmanager_secret_rotation.rsm-sr | resource |
| aws_secretsmanager_secret_version.rsm-sv | resource |
| aws_secretsmanager_secret_version.rsm-svu | resource |
| aws_secretsmanager_secret_version.sm-sv | resource |
| aws_secretsmanager_secret_version.sm-svu | resource |
| aws_secretsmanager_secret.existing | data source |
| aws_secretsmanager_secret_version.existing | data source |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| automatically_after_days | Specifies the number of days between automatic scheduled rotations of the secret. Must be between 1 and 365 days. Example: 30 | number |
30 |
no |
| default_tags | Default tags to apply to all resources. These are merged with resource-specific tags. Example: { Environment = "prod", ManagedBy = "terraform" } | map(string) |
{} |
no |
| ephemeral | Enable ephemeral resources and write-only arguments to prevent sensitive data from being stored in state. Requires Terraform >= 1.11. When enabled, secret values use write-only arguments (_wo) and are not persisted to state. Example: true | bool |
false |
no |
| existing_secrets | Map of existing secret names or ARNs to import as data sources. Useful for referencing secrets created outside this module. Example: { existing_secret = "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret" } | map(string) |
{} |
no |
| recovery_window_in_days | Specifies the number of days that AWS Secrets Manager waits before it can delete the secret. This value can be 0 to force deletion without recovery or range from 7 to 30 days. Example: 7 | number |
30 |
no |
| rotate_secrets | Map of secrets to keep and rotate in AWS Secrets Manager. Each secret must include rotation_lambda_arn. Set replica_regions to replicate rotating secrets across regions. AWS Secrets Manager replicates secret metadata, including resource policies, to replicas automatically. Example: { mysecret = { description = "My secret", secret_string = "secret-value", rotation_lambda_arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function" } } | any |
{} |
no |
| secrets | Map of secrets to keep in AWS Secrets Manager. Set replica_regions to replicate secrets across regions. AWS Secrets Manager replicates secret metadata, including resource policies, to replicas automatically. Example: { mysecret = { description = "My secret", secret_string = "secret-value" } } | any |
{} |
no |
| tags | Key-value map of user-defined tags attached to the secret. Keys cannot start with 'aws:'. Example: { Environment = "prod", Owner = "team" } | any |
{} |
no |
| unmanaged | Terraform must ignore secrets lifecycle. Using this option you can initialize the secrets and rotate them outside Terraform, avoiding other users changing or rotating secrets by subsequent Terraform runs. Example: true | bool |
false |
no |
| version_stages | List of version stages to be handled. Valid values are 'AWSCURRENT' and 'AWSPENDING'. Kept as null for backwards compatibility. Example: ["AWSCURRENT"] | list(string) |
null |
no |
| Name | Description |
|---|---|
| all_secret_arns | List of all secret ARNs (both regular and rotating) for easy reference in IAM policies. |
| existing_secret_versions | Map of existing secret versions with their current values and metadata. |
| existing_secrets | Map of existing secrets referenced as data sources with their complete attributes. |
| rotate_secret_arns | Map of rotating secret names to their ARNs. Use these ARNs to grant permissions or reference rotating secrets in IAM policies and other AWS resources. |
| rotate_secret_ids | Map of rotating secret names to their resource IDs. Use these IDs to reference rotating secrets in other Terraform resources. |
| rotate_secret_versions | Map of managed rotating secret versions with their ARNs and version information. |
| rotate_secrets | Complete map of rotating secrets with all attributes including ARNs, names, KMS keys, descriptions, replica information, and rotation information. |
| secret_arns | Map of secret names to their ARNs. Use these ARNs to grant permissions or reference secrets in IAM policies and other AWS resources. |
| secret_ids | Map of secret names to their resource IDs. Use these IDs to reference secrets in other Terraform resources. |
| secret_rotations | Map of secret rotation configurations with Lambda ARN and rotation schedule information. |
| secret_versions | Map of managed secret versions with their ARNs and version information. |
| secrets | Complete map of regular secrets with all attributes including ARNs, names, KMS keys, descriptions, and replica information. |
| secrets_by_name | Map of actual secret names to their ARNs, useful for referencing secrets by their AWS names rather than Terraform keys. |
