From 6943ab67fcc5da8fc6092ef7690bfbc6085d68c5 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 24 Dec 2025 14:28:19 +0800 Subject: [PATCH] Simplify bootstrap inputs to YAML config --- .../aws-cloud/bootstrap/README.md | 17 +++- .../aws-cloud/bootstrap/identity/locals.tf | 31 ++++--- .../aws-cloud/bootstrap/identity/main.tf | 12 +-- .../aws-cloud/bootstrap/identity/outputs.tf | 4 +- .../bootstrap/identity/terragrunt.hcl | 18 ++-- .../aws-cloud/bootstrap/identity/variables.tf | 92 +------------------ .../aws-cloud/bootstrap/lock/locals.tf | 11 +-- .../aws-cloud/bootstrap/lock/terragrunt.hcl | 18 ++-- .../aws-cloud/bootstrap/lock/variables.tf | 22 +---- .../aws-cloud/bootstrap/state/locals.tf | 9 +- .../aws-cloud/bootstrap/state/main.tf | 10 +- .../aws-cloud/bootstrap/state/terragrunt.hcl | 18 ++-- .../aws-cloud/bootstrap/state/variables.tf | 26 +----- 13 files changed, 88 insertions(+), 200 deletions(-) diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/README.md b/terraform-hcl-standard/aws-cloud/bootstrap/README.md index 57f05fdd..1bfaae2f 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/README.md +++ b/terraform-hcl-standard/aws-cloud/bootstrap/README.md @@ -21,7 +21,7 @@ Terragrunt `run-all` handles the ordering; no manual sequencing is required. - **Data plane**: S3 bucket enforces AES256 SSE, public access block, and versioning. DynamoDB enables server-side encryption and PITR for forensic recovery. - **Control plane**: IAM policies are externalized in `identity/policies/*.json` and rendered via `aws_iam_policy_document` to keep Terraform code lean and auditable. -- **Config source of truth**: The GitOps repo (`https://github.com/cloud-neutral-workshop/gitops.git`) stores `config/accounts/bootstrap.yaml`, defining canonical names, regions, and tags. Terragrunt reads it via `GITOPS_REPO_ROOT` (defaults to `../gitops` relative to this repo). Clone that repository locally or set `GITOPS_REPO_ROOT` to your desired path to keep configuration and modules separated. You can also override the config file path with `GITOPS_BOOTSTRAP_CONFIG` (for example, `config/xzerolab/sit/aws-cloud/account/bootstrap.yaml` inside the GitOps repo). +- **Config source of truth**: Provide the bootstrap YAML path via `TF_CONFIG` (absolute or repo-root relative). When unset, Terragrunt defaults to `gitops/${GITOPS_BOOTSTRAP_CONFIG:-config/bootstrap.yaml}` relative to the repo root inferred from `TG_ROOT` (`terraform-hcl-standard/aws-cloud/bootstrap`). ## How to Run with Terragrunt @@ -70,3 +70,18 @@ Document the teardown in your change log for auditability. - **Idempotent automation**: All configurations are declarative, version-controlled, and runnable via Terragrunt without manual steps. - **Auditability**: Policies live in external JSON files; DynamoDB PITR and S3 versioning preserve history for compliance. - **Portability**: Inputs are read from YAML configuration and Terragrunt dependencies, making the stack reusable across accounts and regions. + +Optional YAML fields supported by the bootstrap modules: + +```yaml +state: + create_bucket: true +iam: + create_role: true + existing_role_name: null + existing_role_arn: null + create_user: true + existing_user_name: null + managed_policy_arns: + - arn:aws:iam::aws:policy/AdministratorAccess +``` diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/identity/locals.tf b/terraform-hcl-standard/aws-cloud/bootstrap/identity/locals.tf index a2bb8183..4bcb4982 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/identity/locals.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/identity/locals.tf @@ -1,22 +1,27 @@ locals { - config_root = coalesce(var.config_root, abspath("${path.module}/../../../../../gitops")) - bootstrap_config_path = coalesce( - var.bootstrap_config_path, - "${local.config_root}/config/accounts/bootstrap.yaml" - ) + bootstrap_config_path = abspath(var.bootstrap_config_path) + config_root = dirname(dirname(dirname(local.bootstrap_config_path))) bootstrap = yamldecode(file(local.bootstrap_config_path)) - config_account_name = coalesce(var.account_name, local.bootstrap.account_name) - config_region = coalesce(var.region, local.bootstrap.region) - config_role_name = coalesce(var.role_name, local.bootstrap.iam.role_name) - config_terraform_user = coalesce(var.terraform_user_name, local.bootstrap.iam.terraform_user_name) + config_account_name = local.bootstrap.account_name + config_region = local.bootstrap.region + config_role_name = local.bootstrap.iam.role_name + config_terraform_user = local.bootstrap.iam.terraform_user_name environment = coalesce(try(local.bootstrap.environment, null), try(local.bootstrap.iam.environment, null), "bootstrap") extra_tags = try(local.bootstrap.tags, {}) - role_name = coalesce(var.existing_role_name, local.config_role_name) - terraform_user_name = coalesce(var.existing_user_name, local.config_terraform_user) - state_bucket_name = coalesce(var.state_bucket_name, try(local.bootstrap.state.bucket_name, null)) - lock_table_name = coalesce(var.state_lock_table_name, try(local.bootstrap.state.dynamodb_table_name, null)) + create_role = try(local.bootstrap.iam.create_role, true) + existing_role_name = try(local.bootstrap.iam.existing_role_name, null) + existing_role_arn = try(local.bootstrap.iam.existing_role_arn, null) + role_name = coalesce(local.existing_role_name, local.config_role_name) + + create_user = try(local.bootstrap.iam.create_user, true) + existing_user_name = try(local.bootstrap.iam.existing_user_name, null) + terraform_user_name = coalesce(local.existing_user_name, local.config_terraform_user) + + state_bucket_name = try(local.bootstrap.state.bucket_name, null) + lock_table_name = try(local.bootstrap.state.dynamodb_table_name, null) + managed_policy_arns = try(local.bootstrap.iam.managed_policy_arns, ["arn:aws:iam::aws:policy/AdministratorAccess"]) } locals { diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/identity/main.tf b/terraform-hcl-standard/aws-cloud/bootstrap/identity/main.tf index 05c9c919..546ac2c4 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/identity/main.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/identity/main.tf @@ -56,7 +56,7 @@ data "aws_iam_policy_document" "terraform_deploy_assume_role" { } resource "aws_iam_role" "terraform_deploy_role" { - count = var.create_role ? 1 : 0 + count = local.create_role ? 1 : 0 name = local.role_name assume_role_policy = data.aws_iam_policy_document.terraform_deploy_assume_role.json @@ -87,7 +87,7 @@ data "aws_iam_policy_document" "terraform_deploy_inline" { } resource "aws_iam_role_policy" "terraform_deploy_role_policy" { - count = var.create_role ? 1 : 0 + count = local.create_role ? 1 : 0 name = "${local.role_name}-bootstrap-minimal" role = aws_iam_role.terraform_deploy_role[0].id @@ -95,17 +95,17 @@ resource "aws_iam_role_policy" "terraform_deploy_role_policy" { } resource "aws_iam_role_policy_attachment" "terraform_deploy_role_managed" { - count = var.create_role ? length(var.managed_policy_arns) : 0 + count = local.create_role ? length(local.managed_policy_arns) : 0 role = aws_iam_role.terraform_deploy_role[0].name - policy_arn = var.managed_policy_arns[count.index] + policy_arn = local.managed_policy_arns[count.index] } # # IAM User for Terraform (AK/SK) # ---------------------------------------- resource "aws_iam_user" "terraform_user" { - count = var.create_user ? 1 : 0 + count = local.create_user ? 1 : 0 name = local.terraform_user_name } @@ -126,7 +126,7 @@ data "aws_iam_policy_document" "terraform_user" { } resource "aws_iam_user_policy" "terraform_user_policy" { - count = var.create_user ? 1 : 0 + count = local.create_user ? 1 : 0 name = "${local.terraform_user_name}-iac-policy" user = aws_iam_user.terraform_user[0].name diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/identity/outputs.tf b/terraform-hcl-standard/aws-cloud/bootstrap/identity/outputs.tf index 001b9148..d80ed085 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/identity/outputs.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/identity/outputs.tf @@ -1,10 +1,10 @@ output "iam_role_arn" { - value = var.create_role ? aws_iam_role.terraform_deploy_role[0].arn : var.existing_role_arn + value = local.create_role ? aws_iam_role.terraform_deploy_role[0].arn : local.existing_role_arn description = "The ARN of the role assumed by Terraform" } output "terraform_user_name" { - value = var.create_user ? aws_iam_user.terraform_user[0].name : local.terraform_user_name + value = local.create_user ? aws_iam_user.terraform_user[0].name : local.terraform_user_name description = "Terraform IAM User" } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/identity/terragrunt.hcl b/terraform-hcl-standard/aws-cloud/bootstrap/identity/terragrunt.hcl index b6f1a1d6..2959a69a 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/identity/terragrunt.hcl +++ b/terraform-hcl-standard/aws-cloud/bootstrap/identity/terragrunt.hcl @@ -11,18 +11,16 @@ terraform { } locals { - gitops_repo_root = get_env( - "GITOPS_REPO_ROOT", - abspath("${get_parent_terragrunt_dir()}/../../../../../gitops") - ) - config_root = "${local.gitops_repo_root}/config" - bootstrap_config_path = get_env( - "GITOPS_BOOTSTRAP_CONFIG", - "${local.config_root}/accounts/bootstrap.yaml" - ) + tg_root = get_env("TG_ROOT", get_parent_terragrunt_dir()) + repo_root = abspath("${local.tg_root}/../../../../../") + gitops_root = "${local.repo_root}/gitops" + + tf_config_env = trimspace(get_env("TF_CONFIG", "")) + bootstrap_config_path = local.tf_config_env != "" ? ( + startswith(local.tf_config_env, "/") ? local.tf_config_env : abspath("${local.repo_root}/${local.tf_config_env}") + ) : abspath("${local.gitops_root}/${get_env("GITOPS_BOOTSTRAP_CONFIG", "config/bootstrap.yaml")}") } inputs = { bootstrap_config_path = local.bootstrap_config_path - config_root = local.gitops_repo_root } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/identity/variables.tf b/terraform-hcl-standard/aws-cloud/bootstrap/identity/variables.tf index cc2e871c..ede024fa 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/identity/variables.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/identity/variables.tf @@ -1,93 +1,9 @@ -variable "region" { - description = "AWS region" - type = string - default = null -} - -variable "account_name" { - type = string - description = "Which account configuration to load (e.g., dev)" - default = null -} - -variable "create_role" { - description = "Whether to create the Terraform deploy IAM role" - type = bool - default = true - - validation { - condition = var.create_role || (var.existing_role_arn != null && var.existing_role_name != null) - error_message = "existing_role_name and existing_role_arn must be provided when create_role is false." - } -} - -variable "existing_role_name" { - description = "Existing IAM role name to reference when create_role is false" - type = string - default = null -} - -variable "existing_role_arn" { - description = "Existing IAM role ARN to reference when create_role is false" - type = string - default = null -} - -variable "role_name" { - type = string - description = "IAM role name to create (e.g., TerraformDeployRole-Dev)" - default = null -} - -variable "existing_user_name" { - description = "Existing IAM username to reference when create_user is false" - type = string - default = null -} - -variable "terraform_user_name" { - type = string - description = "IAM username for Terraform IAC runner" - default = null -} - -variable "create_user" { - description = "Whether to create the IAM user for Terraform" - type = bool - default = true - - validation { - condition = var.create_user || var.existing_user_name != null - error_message = "existing_user_name must be provided when create_user is false." - } -} - -variable "state_bucket_name" { - description = "Name of the Terraform state bucket (overrides bootstrap config when provided)" - type = string - default = null -} - -variable "state_lock_table_name" { - description = "Name of the DynamoDB state lock table (overrides bootstrap config when provided)" - type = string - default = null -} - variable "bootstrap_config_path" { description = "Path to the bootstrap account configuration YAML" type = string - default = null -} -variable "config_root" { - description = "Local path to the gitops repository root." - type = string - default = null -} - -variable "managed_policy_arns" { - description = "List of managed policy ARNs to attach to the Terraform deploy role" - type = list(string) - default = ["arn:aws:iam::aws:policy/AdministratorAccess"] + validation { + condition = var.bootstrap_config_path != null && trimspace(var.bootstrap_config_path) != "" + error_message = "Set bootstrap_config_path (TF_CONFIG) to the bootstrap YAML file path." + } } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/lock/locals.tf b/terraform-hcl-standard/aws-cloud/bootstrap/lock/locals.tf index d6134d40..a3aab69e 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/lock/locals.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/lock/locals.tf @@ -1,14 +1,9 @@ locals { - config_root = coalesce(var.config_root, abspath("${path.module}/../../../../../gitops")) - bootstrap_config_path = coalesce( - var.bootstrap_config_path, - "${local.config_root}/config/accounts/bootstrap.yaml" - ) - + bootstrap_config_path = abspath(var.bootstrap_config_path) bootstrap = yamldecode(file(local.bootstrap_config_path)) - dynamodb_table_name = coalesce(var.table_name, local.bootstrap.state.dynamodb_table_name) - region = coalesce(var.region, local.bootstrap.region) + dynamodb_table_name = local.bootstrap.state.dynamodb_table_name + region = local.bootstrap.region environment = try(local.bootstrap.environment, "bootstrap") tags = try(local.bootstrap.tags, {}) } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/lock/terragrunt.hcl b/terraform-hcl-standard/aws-cloud/bootstrap/lock/terragrunt.hcl index ecfd94d4..eac8b74b 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/lock/terragrunt.hcl +++ b/terraform-hcl-standard/aws-cloud/bootstrap/lock/terragrunt.hcl @@ -11,18 +11,16 @@ terraform { } locals { - gitops_repo_root = get_env( - "GITOPS_REPO_ROOT", - abspath("${get_parent_terragrunt_dir()}/../../../../../gitops") - ) - config_root = "${local.gitops_repo_root}/config" - bootstrap_config_path = get_env( - "GITOPS_BOOTSTRAP_CONFIG", - "${local.config_root}/accounts/bootstrap.yaml" - ) + tg_root = get_env("TG_ROOT", get_parent_terragrunt_dir()) + repo_root = abspath("${local.tg_root}/../../../../../") + gitops_root = "${local.repo_root}/gitops" + + tf_config_env = trimspace(get_env("TF_CONFIG", "")) + bootstrap_config_path = local.tf_config_env != "" ? ( + startswith(local.tf_config_env, "/") ? local.tf_config_env : abspath("${local.repo_root}/${local.tf_config_env}") + ) : abspath("${local.gitops_root}/${get_env("GITOPS_BOOTSTRAP_CONFIG", "config/bootstrap.yaml")}") } inputs = { bootstrap_config_path = local.bootstrap_config_path - config_root = local.gitops_repo_root } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/lock/variables.tf b/terraform-hcl-standard/aws-cloud/bootstrap/lock/variables.tf index b6e9536c..ede024fa 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/lock/variables.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/lock/variables.tf @@ -1,23 +1,9 @@ -variable "table_name" { - description = "DynamoDB table name for Terraform state lock" - type = string - default = null -} - -variable "region" { - description = "AWS region" - type = string - default = null -} - variable "bootstrap_config_path" { description = "Path to the bootstrap account configuration YAML" type = string - default = null -} -variable "config_root" { - description = "Local path to the gitops repository root." - type = string - default = null + validation { + condition = var.bootstrap_config_path != null && trimspace(var.bootstrap_config_path) != "" + error_message = "Set bootstrap_config_path (TF_CONFIG) to the bootstrap YAML file path." + } } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/state/locals.tf b/terraform-hcl-standard/aws-cloud/bootstrap/state/locals.tf index 1bc2be9d..5e9bb36b 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/state/locals.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/state/locals.tf @@ -3,11 +3,12 @@ locals { bootstrap = yamldecode(file(local.bootstrap_config_path)) - bucket_name = coalesce(var.bucket_name, local.bootstrap.state.bucket_name) - region = coalesce(var.region, local.bootstrap.region) + bucket_name = local.bootstrap.state.bucket_name + region = local.bootstrap.region environment = try(local.bootstrap.environment, "bootstrap") tags = try(local.bootstrap.tags, {}) - bucket_arn = var.create_bucket ? aws_s3_bucket.state[0].arn : data.aws_s3_bucket.existing[0].arn - bucket_id = var.create_bucket ? aws_s3_bucket.state[0].id : data.aws_s3_bucket.existing[0].id + create_bucket = try(local.bootstrap.state.create_bucket, true) + bucket_arn = local.create_bucket ? aws_s3_bucket.state[0].arn : data.aws_s3_bucket.existing[0].arn + bucket_id = local.create_bucket ? aws_s3_bucket.state[0].id : data.aws_s3_bucket.existing[0].id } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/state/main.tf b/terraform-hcl-standard/aws-cloud/bootstrap/state/main.tf index 7985e0ac..8a82d9f0 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/state/main.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/state/main.tf @@ -1,5 +1,5 @@ resource "aws_s3_bucket" "state" { - count = var.create_bucket ? 1 : 0 + count = local.create_bucket ? 1 : 0 bucket = local.bucket_name tags = merge( @@ -12,12 +12,12 @@ resource "aws_s3_bucket" "state" { } data "aws_s3_bucket" "existing" { - count = var.create_bucket ? 0 : 1 + count = local.create_bucket ? 0 : 1 bucket = local.bucket_name } resource "aws_s3_bucket_versioning" "versioning" { - count = var.create_bucket ? 1 : 0 + count = local.create_bucket ? 1 : 0 bucket = local.bucket_id versioning_configuration { @@ -26,7 +26,7 @@ resource "aws_s3_bucket_versioning" "versioning" { } resource "aws_s3_bucket_server_side_encryption_configuration" "sse" { - count = var.create_bucket ? 1 : 0 + count = local.create_bucket ? 1 : 0 bucket = local.bucket_id rule { @@ -37,7 +37,7 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "sse" { } resource "aws_s3_bucket_public_access_block" "block" { - count = var.create_bucket ? 1 : 0 + count = local.create_bucket ? 1 : 0 bucket = local.bucket_id block_public_acls = true diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/state/terragrunt.hcl b/terraform-hcl-standard/aws-cloud/bootstrap/state/terragrunt.hcl index 8697098a..b0446eed 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/state/terragrunt.hcl +++ b/terraform-hcl-standard/aws-cloud/bootstrap/state/terragrunt.hcl @@ -7,18 +7,16 @@ terraform { } locals { - gitops_repo_root = get_env( - "GITOPS_REPO_ROOT", - abspath("${get_parent_terragrunt_dir()}/../../../../../gitops") - ) - config_root = "${local.gitops_repo_root}/config" - bootstrap_config_path = get_env( - "GITOPS_BOOTSTRAP_CONFIG", - "${local.config_root}/accounts/bootstrap.yaml" - ) + tg_root = get_env("TG_ROOT", get_parent_terragrunt_dir()) + repo_root = abspath("${local.tg_root}/../../../../../") + gitops_root = "${local.repo_root}/gitops" + + tf_config_env = trimspace(get_env("TF_CONFIG", "")) + bootstrap_config_path = local.tf_config_env != "" ? ( + startswith(local.tf_config_env, "/") ? local.tf_config_env : abspath("${local.repo_root}/${local.tf_config_env}") + ) : abspath("${local.gitops_root}/${get_env("GITOPS_BOOTSTRAP_CONFIG", "config/bootstrap.yaml")}") } inputs = { bootstrap_config_path = local.bootstrap_config_path - config_root = local.gitops_repo_root } diff --git a/terraform-hcl-standard/aws-cloud/bootstrap/state/variables.tf b/terraform-hcl-standard/aws-cloud/bootstrap/state/variables.tf index 3f64dd5f..ede024fa 100644 --- a/terraform-hcl-standard/aws-cloud/bootstrap/state/variables.tf +++ b/terraform-hcl-standard/aws-cloud/bootstrap/state/variables.tf @@ -1,33 +1,9 @@ -variable "bucket_name" { - description = "S3 bucket name for Terraform state" - type = string - default = null -} - -variable "region" { - description = "AWS region" - type = string - default = null -} - variable "bootstrap_config_path" { description = "Path to the bootstrap account configuration YAML" type = string validation { condition = var.bootstrap_config_path != null && trimspace(var.bootstrap_config_path) != "" - error_message = "Set bootstrap_config_path to the GitHub Action environment input that points to the bootstrap YAML file." + error_message = "Set bootstrap_config_path (TF_CONFIG) to the bootstrap YAML file path." } } - -variable "config_root" { - description = "Local path to the gitops repository root." - type = string - default = null -} - -variable "create_bucket" { - description = "Whether to create the Terraform state bucket. Set to false to use an existing bucket." - type = bool - default = true -}