diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e4e7da..7f8a62b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.96.1 + rev: v1.100.0 hooks: - id: terraform_fmt - id: terraform_wrapper_module_for_each diff --git a/README.md b/README.md index 67b91ad..d2f92cf 100644 --- a/README.md +++ b/README.md @@ -123,16 +123,15 @@ Examples codified under the [`examples`](https://github.com/terraform-aws-module | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 5.0 | -| [random](#requirement\_random) | >= 3.0 | +| [terraform](#requirement\_terraform) | >= 1.11 | +| [aws](#requirement\_aws) | >= 6.0 | +| [random](#requirement\_random) | >= 3.7 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 5.0 | -| [random](#provider\_random) | >= 3.0 | +| [aws](#provider\_aws) | >= 6.0 | ## Modules @@ -147,7 +146,6 @@ No modules. | [aws_secretsmanager_secret_rotation.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_rotation) | resource | | [aws_secretsmanager_secret_version.ignore_changes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | | [aws_secretsmanager_secret_version.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | -| [random_password.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs @@ -157,7 +155,7 @@ No modules. | [block\_public\_policy](#input\_block\_public\_policy) | Makes an optional API call to Zelkova to validate the Resource Policy to prevent broad access to your secret | `bool` | `null` | no | | [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | | [create\_policy](#input\_create\_policy) | Determines whether a policy will be created | `bool` | `false` | no | -| [create\_random\_password](#input\_create\_random\_password) | Determines whether a random password will be generated | `bool` | `false` | no | +| [create\_random\_password](#input\_create\_random\_password) | Determines whether an ephemeral random password will be generated for `secret_string_wo` | `bool` | `false` | no | | [description](#input\_description) | A description of the secret | `string` | `null` | no | | [enable\_rotation](#input\_enable\_rotation) | Determines whether secret rotation is enabled | `bool` | `false` | no | | [force\_overwrite\_replica\_secret](#input\_force\_overwrite\_replica\_secret) | Accepts boolean value to specify whether to overwrite a secret with the same name in the destination Region | `bool` | `null` | no | @@ -166,15 +164,19 @@ No modules. | [name](#input\_name) | Friendly name of the new secret. The secret name can consist of uppercase letters, lowercase letters, digits, and any of the following characters: `/_+=.@-` | `string` | `null` | no | | [name\_prefix](#input\_name\_prefix) | Creates a unique name beginning with the specified prefix | `string` | `null` | no | | [override\_policy\_documents](#input\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | -| [policy\_statements](#input\_policy\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `map(any)` | `{}` | no | +| [policy\_statements](#input\_policy\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage |
map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
}))
| `null` | no | | [random\_password\_length](#input\_random\_password\_length) | The length of the generated random password | `number` | `32` | no | | [random\_password\_override\_special](#input\_random\_password\_override\_special) | Supply your own list of special characters to use for string generation. This overrides the default character list in the special argument | `string` | `"!@#$%&*()-_=+[]{}<>:?"` | no | | [recovery\_window\_in\_days](#input\_recovery\_window\_in\_days) | 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. The default value is `30` | `number` | `null` | no | -| [replica](#input\_replica) | Configuration block to support secret replication | `map(any)` | `{}` | no | +| [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | +| [replica](#input\_replica) | Configuration block to support secret replication |
map(object({
kms_key_id = optional(string)
region = optional(string) # will default to the key name
}))
| `null` | no | +| [rotate\_immediately](#input\_rotate\_immediately) | Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window. The rotation schedule is defined in `rotation_rules` | `bool` | `null` | no | | [rotation\_lambda\_arn](#input\_rotation\_lambda\_arn) | Specifies the ARN of the Lambda function that can rotate the secret | `string` | `""` | no | -| [rotation\_rules](#input\_rotation\_rules) | A structure that defines the rotation configuration for this secret | `map(any)` | `{}` | no | -| [secret\_binary](#input\_secret\_binary) | Specifies binary data that you want to encrypt and store in this version of the secret. This is required if `secret_string` is not set. Needs to be encoded to base64 | `string` | `null` | no | -| [secret\_string](#input\_secret\_string) | Specifies text data that you want to encrypt and store in this version of the secret. This is required if `secret_binary` is not set | `string` | `null` | no | +| [rotation\_rules](#input\_rotation\_rules) | A structure that defines the rotation configuration for this secret |
object({
automatically_after_days = optional(number)
duration = optional(string)
schedule_expression = optional(string)
})
| `null` | no | +| [secret\_binary](#input\_secret\_binary) | Specifies binary data that you want to encrypt and store in this version of the secret. This is required if `secret_string` or `secret_string_wo` is not set. Needs to be encoded to base64 | `string` | `null` | no | +| [secret\_string](#input\_secret\_string) | Specifies text data that you want to encrypt and store in this version of the secret. This is required if `secret_binary` or `secret_string_wo` is not set | `string` | `null` | no | +| [secret\_string\_wo](#input\_secret\_string\_wo) | Specifies text data that you want to encrypt and store in this version of the secret. This is required if `secret_binary` or `secret_string` is not set | `string` | `null` | no | +| [secret\_string\_wo\_version](#input\_secret\_string\_wo\_version) | Used together with `secret_string_wo` to trigger an update. Increment this value when an update to `secret_string_wo` is required | `string` | `null` | no | | [source\_policy\_documents](#input\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s | `list(string)` | `[]` | no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | | [version\_stages](#input\_version\_stages) | Specifies a list of staging labels that are attached to this version of the secret. A staging label must be unique to a single version of the secret | `list(string)` | `null` | no | diff --git a/examples/complete/README.md b/examples/complete/README.md index de91e6c..24672fb 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -28,20 +28,21 @@ Note that this example may create resources which will incur monetary charges on | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 5.0 | +| [terraform](#requirement\_terraform) | >= 1.11 | +| [aws](#requirement\_aws) | >= 6.0 | +| [random](#requirement\_random) | >= 3.7 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 5.0 | +| [aws](#provider\_aws) | >= 6.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [lambda](#module\_lambda) | terraform-aws-modules/lambda/aws | ~> 6.0 | +| [lambda](#module\_lambda) | terraform-aws-modules/lambda/aws | ~> 8.0 | | [secrets\_manager](#module\_secrets\_manager) | ../.. | n/a | | [secrets\_manager\_disabled](#module\_secrets\_manager\_disabled) | ../.. | n/a | | [secrets\_manager\_rotate](#module\_secrets\_manager\_rotate) | ../.. | n/a | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 4c2be4e..8fef555 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -101,7 +101,7 @@ module "secrets_manager_rotate" { engine = "mariadb", host = "mydb.cluster-123456789012.us-east-1.rds.amazonaws.com", username = "Bill", - password = "ThisIsMySuperSecretString12356!" + password = "ThisIsMySuperSecretString12356!", dbname = "mydb", port = 3306 }) @@ -152,13 +152,13 @@ data "aws_iam_policy_document" "this" { module "lambda" { source = "terraform-aws-modules/lambda/aws" - version = "~> 6.0" + version = "~> 8.0" function_name = local.name description = "Example Secrets Manager secret rotation lambda function" handler = "function.lambda_handler" - runtime = "python3.10" + runtime = "python3.13" timeout = 60 memory_size = 512 source_path = "${path.module}/function.py" diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index ddfcb0e..23d9fe0 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,10 +1,14 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.11" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.0" + version = ">= 6.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.7" } } } diff --git a/main.tf b/main.tf index 53e12f9..deeff12 100644 --- a/main.tf +++ b/main.tf @@ -6,6 +6,8 @@ resource "aws_secretsmanager_secret" "this" { count = var.create ? 1 : 0 + region = var.region + description = var.description force_overwrite_replica_secret = var.force_overwrite_replica_secret kms_key_id = var.kms_key_id @@ -14,11 +16,11 @@ resource "aws_secretsmanager_secret" "this" { recovery_window_in_days = var.recovery_window_in_days dynamic "replica" { - for_each = var.replica + for_each = var.replica != null ? var.replica : {} content { - kms_key_id = try(replica.value.kms_key_id, null) - region = try(replica.value.region, replica.key) + kms_key_id = replica.value.kms_key_id + region = coalesce(replica.value.region, replica.key) } } @@ -36,18 +38,18 @@ data "aws_iam_policy_document" "this" { override_policy_documents = var.override_policy_documents dynamic "statement" { - for_each = var.policy_statements + for_each = var.policy_statements != null ? var.policy_statements : {} content { - sid = try(statement.value.sid, null) - actions = try(statement.value.actions, null) - not_actions = try(statement.value.not_actions, null) - effect = try(statement.value.effect, null) - resources = try(statement.value.resources, null) - not_resources = try(statement.value.not_resources, null) + sid = statement.value.sid + actions = statement.value.actions + not_actions = statement.value.not_actions + effect = statement.value.effect + resources = statement.value.resources + not_resources = statement.value.not_resources dynamic "principals" { - for_each = try(statement.value.principals, []) + for_each = statement.value.principals != null ? statement.value.principals : [] content { type = principals.value.type @@ -56,7 +58,7 @@ data "aws_iam_policy_document" "this" { } dynamic "not_principals" { - for_each = try(statement.value.not_principals, []) + for_each = statement.value.not_principals != null ? statement.value.not_principals : [] content { type = not_principals.value.type @@ -65,7 +67,7 @@ data "aws_iam_policy_document" "this" { } dynamic "condition" { - for_each = try(statement.value.conditions, []) + for_each = statement.value.condition != null ? statement.value.condition : [] content { test = condition.value.test @@ -80,9 +82,11 @@ data "aws_iam_policy_document" "this" { resource "aws_secretsmanager_secret_policy" "this" { count = var.create && var.create_policy ? 1 : 0 - secret_arn = aws_secretsmanager_secret.this[0].arn - policy = data.aws_iam_policy_document.this[0].json + region = var.region + block_public_policy = var.block_public_policy + policy = data.aws_iam_policy_document.this[0].json + secret_arn = aws_secretsmanager_secret.this[0].arn } ################################################################################ @@ -92,19 +96,27 @@ resource "aws_secretsmanager_secret_policy" "this" { resource "aws_secretsmanager_secret_version" "this" { count = var.create && !(var.enable_rotation || var.ignore_secret_changes) ? 1 : 0 - secret_id = aws_secretsmanager_secret.this[0].id - secret_string = var.create_random_password ? random_password.this[0].result : var.secret_string - secret_binary = var.secret_binary - version_stages = var.version_stages + region = var.region + + secret_id = aws_secretsmanager_secret.this[0].id + secret_binary = var.secret_binary + secret_string = var.secret_string + secret_string_wo = var.create_random_password ? ephemeral.random_password.this[0].result : var.secret_string_wo + secret_string_wo_version = var.create_random_password ? coalesce(var.secret_string_wo_version, 0) : var.secret_string_wo_version + version_stages = var.version_stages } resource "aws_secretsmanager_secret_version" "ignore_changes" { count = var.create && (var.enable_rotation || var.ignore_secret_changes) ? 1 : 0 - secret_id = aws_secretsmanager_secret.this[0].id - secret_string = var.create_random_password ? random_password.this[0].result : var.secret_string - secret_binary = var.secret_binary - version_stages = var.version_stages + region = var.region + + secret_id = aws_secretsmanager_secret.this[0].id + secret_binary = var.secret_binary + secret_string = var.secret_string + secret_string_wo = var.create_random_password ? ephemeral.random_password.this[0].result : var.secret_string_wo + secret_string_wo_version = var.create_random_password ? coalesce(var.secret_string_wo_version, 0) : var.secret_string_wo_version + version_stages = var.version_stages lifecycle { ignore_changes = [ @@ -115,7 +127,7 @@ resource "aws_secretsmanager_secret_version" "ignore_changes" { } } -resource "random_password" "this" { +ephemeral "random_password" "this" { count = var.create && var.create_random_password ? 1 : 0 length = var.random_password_length @@ -130,15 +142,18 @@ resource "random_password" "this" { resource "aws_secretsmanager_secret_rotation" "this" { count = var.create && var.enable_rotation ? 1 : 0 + region = var.region + + rotate_immediately = var.rotate_immediately rotation_lambda_arn = var.rotation_lambda_arn dynamic "rotation_rules" { - for_each = [var.rotation_rules] + for_each = var.rotation_rules != null ? [var.rotation_rules] : [] content { - automatically_after_days = try(rotation_rules.value.automatically_after_days, null) - duration = try(rotation_rules.value.duration, null) - schedule_expression = try(rotation_rules.value.schedule_expression, null) + automatically_after_days = rotation_rules.value.automatically_after_days + duration = rotation_rules.value.duration + schedule_expression = rotation_rules.value.schedule_expression } } diff --git a/variables.tf b/variables.tf index b4b915a..510b0ad 100644 --- a/variables.tf +++ b/variables.tf @@ -4,6 +4,12 @@ variable "create" { default = true } +variable "region" { + description = "Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration" + type = string + default = null +} + variable "tags" { description = "A map of tags to add to all resources" type = map(string) @@ -52,8 +58,11 @@ variable "recovery_window_in_days" { variable "replica" { description = "Configuration block to support secret replication" - type = map(any) - default = {} + type = map(object({ + kms_key_id = optional(string) + region = optional(string) # will default to the key name + })) + default = null } ################################################################################ @@ -80,8 +89,28 @@ variable "override_policy_documents" { variable "policy_statements" { description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" - type = map(any) - default = {} + type = map(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string) + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + values = list(string) + variable = string + }))) + })) + default = null } variable "block_public_policy" { @@ -100,14 +129,27 @@ variable "ignore_secret_changes" { default = false } +variable "secret_binary" { + description = "Specifies binary data that you want to encrypt and store in this version of the secret. This is required if `secret_string` or `secret_string_wo` is not set. Needs to be encoded to base64" + type = string + default = null +} + variable "secret_string" { - description = "Specifies text data that you want to encrypt and store in this version of the secret. This is required if `secret_binary` is not set" + description = "Specifies text data that you want to encrypt and store in this version of the secret. This is required if `secret_binary` or `secret_string_wo` is not set" type = string default = null } -variable "secret_binary" { - description = "Specifies binary data that you want to encrypt and store in this version of the secret. This is required if `secret_string` is not set. Needs to be encoded to base64" +variable "secret_string_wo" { + description = "Specifies text data that you want to encrypt and store in this version of the secret. This is required if `secret_binary` or `secret_string` is not set" + type = string + default = null + ephemeral = true +} + +variable "secret_string_wo_version" { + description = "Used together with `secret_string_wo` to trigger an update. Increment this value when an update to `secret_string_wo` is required" type = string default = null } @@ -119,7 +161,7 @@ variable "version_stages" { } variable "create_random_password" { - description = "Determines whether a random password will be generated" + description = "Determines whether an ephemeral random password will be generated for `secret_string_wo`" type = bool default = false } @@ -146,6 +188,12 @@ variable "enable_rotation" { default = false } +variable "rotate_immediately" { + description = "Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window. The rotation schedule is defined in `rotation_rules`" + type = bool + default = null +} + variable "rotation_lambda_arn" { description = "Specifies the ARN of the Lambda function that can rotate the secret" type = string @@ -154,6 +202,10 @@ variable "rotation_lambda_arn" { variable "rotation_rules" { description = "A structure that defines the rotation configuration for this secret" - type = map(any) - default = {} + type = object({ + automatically_after_days = optional(number) + duration = optional(string) + schedule_expression = optional(string) + }) + default = null } diff --git a/versions.tf b/versions.tf index d7b00bc..23d9fe0 100644 --- a/versions.tf +++ b/versions.tf @@ -1,14 +1,14 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.11" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.0" + version = ">= 6.0" } random = { source = "hashicorp/random" - version = ">= 3.0" + version = ">= 3.7" } } } diff --git a/wrappers/main.tf b/wrappers/main.tf index ef2db77..42612fa 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -15,15 +15,19 @@ module "wrapper" { name = try(each.value.name, var.defaults.name, null) name_prefix = try(each.value.name_prefix, var.defaults.name_prefix, null) override_policy_documents = try(each.value.override_policy_documents, var.defaults.override_policy_documents, []) - policy_statements = try(each.value.policy_statements, var.defaults.policy_statements, {}) + policy_statements = try(each.value.policy_statements, var.defaults.policy_statements, null) random_password_length = try(each.value.random_password_length, var.defaults.random_password_length, 32) random_password_override_special = try(each.value.random_password_override_special, var.defaults.random_password_override_special, "!@#$%&*()-_=+[]{}<>:?") recovery_window_in_days = try(each.value.recovery_window_in_days, var.defaults.recovery_window_in_days, null) - replica = try(each.value.replica, var.defaults.replica, {}) + region = try(each.value.region, var.defaults.region, null) + replica = try(each.value.replica, var.defaults.replica, null) + rotate_immediately = try(each.value.rotate_immediately, var.defaults.rotate_immediately, null) rotation_lambda_arn = try(each.value.rotation_lambda_arn, var.defaults.rotation_lambda_arn, "") - rotation_rules = try(each.value.rotation_rules, var.defaults.rotation_rules, {}) + rotation_rules = try(each.value.rotation_rules, var.defaults.rotation_rules, null) secret_binary = try(each.value.secret_binary, var.defaults.secret_binary, null) secret_string = try(each.value.secret_string, var.defaults.secret_string, null) + secret_string_wo = try(each.value.secret_string_wo, var.defaults.secret_string_wo, null) + secret_string_wo_version = try(each.value.secret_string_wo_version, var.defaults.secret_string_wo_version, null) source_policy_documents = try(each.value.source_policy_documents, var.defaults.source_policy_documents, []) tags = try(each.value.tags, var.defaults.tags, {}) version_stages = try(each.value.version_stages, var.defaults.version_stages, null) diff --git a/wrappers/versions.tf b/wrappers/versions.tf index d7b00bc..23d9fe0 100644 --- a/wrappers/versions.tf +++ b/wrappers/versions.tf @@ -1,14 +1,14 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.11" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.0" + version = ">= 6.0" } random = { source = "hashicorp/random" - version = ">= 3.0" + version = ">= 3.7" } } }