diff --git a/infrastructure/modules/ssm-parameter-wo/README.md b/infrastructure/modules/ssm-parameter-wo/README.md index 8a6777e5..d588a1d2 100644 --- a/infrastructure/modules/ssm-parameter-wo/README.md +++ b/infrastructure/modules/ssm-parameter-wo/README.md @@ -108,7 +108,7 @@ For `SecureString` callers you must also provide `value_wo_version`. `value` sti - `String` / `StringList` — non-secret configuration. Stored in state as `insecure_value` and exposed via the `value` output. - `SecureString` — secrets. **Requires `key_id` and `value_wo_version`.** The value is never stored in state or plan files, and the `value`, `raw_value` and `secure_value` outputs are always `null`. - **Choosing `value_wo_version`:** use a number that changes when the secret changes but reveals nothing about it, such as the source SSM parameter's `version` or a manually bumped revision. Do **not** derive it from a hash of the secret. -- **Console or rotation-managed values:** set `ignore_value_changes = true`. Terraform seeds the value on creation and never overwrites it. Toggling this flag replaces the resource, which re-seeds the value. The module always destroys the old parameter before creating the new one, so it never deletes a freshly created parameter. If an apply fails midway through a toggle, check the next plan: it must not show a destroy for a parameter with the same name. +- **Console or rotation-managed values:** set `ignore_value_changes = true`. Terraform seeds the value on creation and never overwrites it. Toggling this flag transfers management between two Terraform addresses for the same SSM parameter; the old address must be destroyed before the new address writes. If an apply is interrupted during a toggle, run a refreshed plan and verify the parameter will be recreated if the destroy completed before the write. - **Naming:** Parameter names are derived from context labels. When `delimiter = "/"`, names are path-style with a leading `/`. Override with `parameter_name` if custom naming is required. ## What this module does NOT do @@ -138,6 +138,7 @@ make terraform-test module=ssm-parameter-wo ``` Tests use `mock_provider` and `command = plan`, because `terraform test` cannot re-supply ephemeral inputs to the apply phase. +`make terraform-test module=ssm-parameter-wo` also checks the generated dependency graph to guard the resource handoff order when `ignore_value_changes` is toggled. diff --git a/infrastructure/modules/ssm-parameter-wo/main.tf b/infrastructure/modules/ssm-parameter-wo/main.tf index 1c46f438..fec96117 100644 --- a/infrastructure/modules/ssm-parameter-wo/main.tf +++ b/infrastructure/modules/ssm-parameter-wo/main.tf @@ -32,6 +32,11 @@ module "ssm_param_label" { resource "aws_ssm_parameter" "this" { count = module.ssm_param_label.enabled && !var.ignore_value_changes ? 1 : 0 + # Both resources share one parameter name. On a mode switch, retire the old + # address before writing through the new one so its destroy cannot delete the + # newly written parameter. + depends_on = [aws_ssm_parameter.ignore_value] + name = local.parameter_name type = var.type description = var.description @@ -51,10 +56,6 @@ resource "aws_ssm_parameter" "this" { resource "aws_ssm_parameter" "ignore_value" { count = module.ssm_param_label.enabled && var.ignore_value_changes ? 1 : 0 - # Both resources share one parameter name: this edge makes Terraform destroy the old - # address before creating the new one when ignore_value_changes is toggled (either way). - depends_on = [aws_ssm_parameter.this] - name = local.parameter_name type = var.type description = var.description diff --git a/scripts/terraform/terraform.mk b/scripts/terraform/terraform.mk index 1d844bd3..7d23ec95 100644 --- a/scripts/terraform/terraform.mk +++ b/scripts/terraform/terraform.mk @@ -53,6 +53,9 @@ terraform-test: # Run terraform test for modules with a tests directory - option mise x -- terraform -chdir="$${module_dir}" init -backend=false -input=false >/dev/null mise x -- terraform -chdir="$${module_dir}" test done + @if [[ -z "$(module)" || "$(module)" == "ssm-parameter-wo" ]]; then \ + bash tests/test-ssm-parameter-wo-ordering.sh; \ + fi _terraform: # Terraform command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], opts=[options to pass to the Terraform command, default is none/empty] dir=$(or ${dir}, ${TERRAFORM_STACK}); . scripts/terraform/terraform.lib.sh; terraform-${cmd} # 'dir' and 'opts' are accessible by the function as environment variables, if set diff --git a/tests/test-ssm-parameter-wo-ordering.sh b/tests/test-ssm-parameter-wo-ordering.sh new file mode 100644 index 00000000..61bde0fc --- /dev/null +++ b/tests/test-ssm-parameter-wo-ordering.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +module_dir="infrastructure/modules/ssm-parameter-wo" +expected='"[root] aws_ssm_parameter.this (expand)" -> "[root] aws_ssm_parameter.ignore_value (expand)"' +graph="$(mise x -- terraform -chdir="$module_dir" graph -type=plan)" + +if ! grep -Fq "$expected" <<<"$graph"; then + printf 'SSM toggle dependency is missing: %s\n' "$expected" >&2 + exit 1 +fi + +printf 'SSM ignore_value toggle dependency is present.\n'