Simplify bootstrap inputs to YAML config

This commit is contained in:
Haitao Pan 2025-12-24 14:28:19 +08:00
parent dd2afb0e13
commit 6943ab67fc
13 changed files with 88 additions and 200 deletions

View File

@ -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. - **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. - **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 ## 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. - **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. - **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. - **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
```

View File

@ -1,22 +1,27 @@
locals { locals {
config_root = coalesce(var.config_root, abspath("${path.module}/../../../../../gitops")) bootstrap_config_path = abspath(var.bootstrap_config_path)
bootstrap_config_path = coalesce( config_root = dirname(dirname(dirname(local.bootstrap_config_path)))
var.bootstrap_config_path,
"${local.config_root}/config/accounts/bootstrap.yaml"
)
bootstrap = yamldecode(file(local.bootstrap_config_path)) bootstrap = yamldecode(file(local.bootstrap_config_path))
config_account_name = coalesce(var.account_name, local.bootstrap.account_name) config_account_name = local.bootstrap.account_name
config_region = coalesce(var.region, local.bootstrap.region) config_region = local.bootstrap.region
config_role_name = coalesce(var.role_name, local.bootstrap.iam.role_name) config_role_name = local.bootstrap.iam.role_name
config_terraform_user = coalesce(var.terraform_user_name, local.bootstrap.iam.terraform_user_name) config_terraform_user = local.bootstrap.iam.terraform_user_name
environment = coalesce(try(local.bootstrap.environment, null), try(local.bootstrap.iam.environment, null), "bootstrap") environment = coalesce(try(local.bootstrap.environment, null), try(local.bootstrap.iam.environment, null), "bootstrap")
extra_tags = try(local.bootstrap.tags, {}) extra_tags = try(local.bootstrap.tags, {})
role_name = coalesce(var.existing_role_name, local.config_role_name) create_role = try(local.bootstrap.iam.create_role, true)
terraform_user_name = coalesce(var.existing_user_name, local.config_terraform_user) existing_role_name = try(local.bootstrap.iam.existing_role_name, null)
state_bucket_name = coalesce(var.state_bucket_name, try(local.bootstrap.state.bucket_name, null)) existing_role_arn = try(local.bootstrap.iam.existing_role_arn, null)
lock_table_name = coalesce(var.state_lock_table_name, try(local.bootstrap.state.dynamodb_table_name, 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 { locals {

View File

@ -56,7 +56,7 @@ data "aws_iam_policy_document" "terraform_deploy_assume_role" {
} }
resource "aws_iam_role" "terraform_deploy_role" { resource "aws_iam_role" "terraform_deploy_role" {
count = var.create_role ? 1 : 0 count = local.create_role ? 1 : 0
name = local.role_name name = local.role_name
assume_role_policy = data.aws_iam_policy_document.terraform_deploy_assume_role.json 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" { 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" name = "${local.role_name}-bootstrap-minimal"
role = aws_iam_role.terraform_deploy_role[0].id 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" { 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 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) # IAM User for Terraform (AK/SK)
# ---------------------------------------- # ----------------------------------------
resource "aws_iam_user" "terraform_user" { resource "aws_iam_user" "terraform_user" {
count = var.create_user ? 1 : 0 count = local.create_user ? 1 : 0
name = local.terraform_user_name name = local.terraform_user_name
} }
@ -126,7 +126,7 @@ data "aws_iam_policy_document" "terraform_user" {
} }
resource "aws_iam_user_policy" "terraform_user_policy" { 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" name = "${local.terraform_user_name}-iac-policy"
user = aws_iam_user.terraform_user[0].name user = aws_iam_user.terraform_user[0].name

View File

@ -1,10 +1,10 @@
output "iam_role_arn" { 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" description = "The ARN of the role assumed by Terraform"
} }
output "terraform_user_name" { 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" description = "Terraform IAM User"
} }

View File

@ -11,18 +11,16 @@ terraform {
} }
locals { locals {
gitops_repo_root = get_env( tg_root = get_env("TG_ROOT", get_parent_terragrunt_dir())
"GITOPS_REPO_ROOT", repo_root = abspath("${local.tg_root}/../../../../../")
abspath("${get_parent_terragrunt_dir()}/../../../../../gitops") gitops_root = "${local.repo_root}/gitops"
)
config_root = "${local.gitops_repo_root}/config" tf_config_env = trimspace(get_env("TF_CONFIG", ""))
bootstrap_config_path = get_env( bootstrap_config_path = local.tf_config_env != "" ? (
"GITOPS_BOOTSTRAP_CONFIG", startswith(local.tf_config_env, "/") ? local.tf_config_env : abspath("${local.repo_root}/${local.tf_config_env}")
"${local.config_root}/accounts/bootstrap.yaml" ) : abspath("${local.gitops_root}/${get_env("GITOPS_BOOTSTRAP_CONFIG", "config/bootstrap.yaml")}")
)
} }
inputs = { inputs = {
bootstrap_config_path = local.bootstrap_config_path bootstrap_config_path = local.bootstrap_config_path
config_root = local.gitops_repo_root
} }

View File

@ -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" { variable "bootstrap_config_path" {
description = "Path to the bootstrap account configuration YAML" description = "Path to the bootstrap account configuration YAML"
type = string type = string
default = null
}
variable "config_root" { validation {
description = "Local path to the gitops repository root." condition = var.bootstrap_config_path != null && trimspace(var.bootstrap_config_path) != ""
type = string error_message = "Set bootstrap_config_path (TF_CONFIG) to the bootstrap YAML file path."
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"]
} }

View File

@ -1,14 +1,9 @@
locals { locals {
config_root = coalesce(var.config_root, abspath("${path.module}/../../../../../gitops")) bootstrap_config_path = abspath(var.bootstrap_config_path)
bootstrap_config_path = coalesce(
var.bootstrap_config_path,
"${local.config_root}/config/accounts/bootstrap.yaml"
)
bootstrap = yamldecode(file(local.bootstrap_config_path)) bootstrap = yamldecode(file(local.bootstrap_config_path))
dynamodb_table_name = coalesce(var.table_name, local.bootstrap.state.dynamodb_table_name) dynamodb_table_name = local.bootstrap.state.dynamodb_table_name
region = coalesce(var.region, local.bootstrap.region) region = local.bootstrap.region
environment = try(local.bootstrap.environment, "bootstrap") environment = try(local.bootstrap.environment, "bootstrap")
tags = try(local.bootstrap.tags, {}) tags = try(local.bootstrap.tags, {})
} }

View File

@ -11,18 +11,16 @@ terraform {
} }
locals { locals {
gitops_repo_root = get_env( tg_root = get_env("TG_ROOT", get_parent_terragrunt_dir())
"GITOPS_REPO_ROOT", repo_root = abspath("${local.tg_root}/../../../../../")
abspath("${get_parent_terragrunt_dir()}/../../../../../gitops") gitops_root = "${local.repo_root}/gitops"
)
config_root = "${local.gitops_repo_root}/config" tf_config_env = trimspace(get_env("TF_CONFIG", ""))
bootstrap_config_path = get_env( bootstrap_config_path = local.tf_config_env != "" ? (
"GITOPS_BOOTSTRAP_CONFIG", startswith(local.tf_config_env, "/") ? local.tf_config_env : abspath("${local.repo_root}/${local.tf_config_env}")
"${local.config_root}/accounts/bootstrap.yaml" ) : abspath("${local.gitops_root}/${get_env("GITOPS_BOOTSTRAP_CONFIG", "config/bootstrap.yaml")}")
)
} }
inputs = { inputs = {
bootstrap_config_path = local.bootstrap_config_path bootstrap_config_path = local.bootstrap_config_path
config_root = local.gitops_repo_root
} }

View File

@ -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" { variable "bootstrap_config_path" {
description = "Path to the bootstrap account configuration YAML" description = "Path to the bootstrap account configuration YAML"
type = string type = string
default = null
}
variable "config_root" { validation {
description = "Local path to the gitops repository root." condition = var.bootstrap_config_path != null && trimspace(var.bootstrap_config_path) != ""
type = string error_message = "Set bootstrap_config_path (TF_CONFIG) to the bootstrap YAML file path."
default = null }
} }

View File

@ -3,11 +3,12 @@ locals {
bootstrap = yamldecode(file(local.bootstrap_config_path)) bootstrap = yamldecode(file(local.bootstrap_config_path))
bucket_name = coalesce(var.bucket_name, local.bootstrap.state.bucket_name) bucket_name = local.bootstrap.state.bucket_name
region = coalesce(var.region, local.bootstrap.region) region = local.bootstrap.region
environment = try(local.bootstrap.environment, "bootstrap") environment = try(local.bootstrap.environment, "bootstrap")
tags = try(local.bootstrap.tags, {}) tags = try(local.bootstrap.tags, {})
bucket_arn = var.create_bucket ? aws_s3_bucket.state[0].arn : data.aws_s3_bucket.existing[0].arn create_bucket = try(local.bootstrap.state.create_bucket, true)
bucket_id = var.create_bucket ? aws_s3_bucket.state[0].id : data.aws_s3_bucket.existing[0].id 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
} }

View File

@ -1,5 +1,5 @@
resource "aws_s3_bucket" "state" { resource "aws_s3_bucket" "state" {
count = var.create_bucket ? 1 : 0 count = local.create_bucket ? 1 : 0
bucket = local.bucket_name bucket = local.bucket_name
tags = merge( tags = merge(
@ -12,12 +12,12 @@ resource "aws_s3_bucket" "state" {
} }
data "aws_s3_bucket" "existing" { data "aws_s3_bucket" "existing" {
count = var.create_bucket ? 0 : 1 count = local.create_bucket ? 0 : 1
bucket = local.bucket_name bucket = local.bucket_name
} }
resource "aws_s3_bucket_versioning" "versioning" { resource "aws_s3_bucket_versioning" "versioning" {
count = var.create_bucket ? 1 : 0 count = local.create_bucket ? 1 : 0
bucket = local.bucket_id bucket = local.bucket_id
versioning_configuration { versioning_configuration {
@ -26,7 +26,7 @@ resource "aws_s3_bucket_versioning" "versioning" {
} }
resource "aws_s3_bucket_server_side_encryption_configuration" "sse" { 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 bucket = local.bucket_id
rule { rule {
@ -37,7 +37,7 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "sse" {
} }
resource "aws_s3_bucket_public_access_block" "block" { resource "aws_s3_bucket_public_access_block" "block" {
count = var.create_bucket ? 1 : 0 count = local.create_bucket ? 1 : 0
bucket = local.bucket_id bucket = local.bucket_id
block_public_acls = true block_public_acls = true

View File

@ -7,18 +7,16 @@ terraform {
} }
locals { locals {
gitops_repo_root = get_env( tg_root = get_env("TG_ROOT", get_parent_terragrunt_dir())
"GITOPS_REPO_ROOT", repo_root = abspath("${local.tg_root}/../../../../../")
abspath("${get_parent_terragrunt_dir()}/../../../../../gitops") gitops_root = "${local.repo_root}/gitops"
)
config_root = "${local.gitops_repo_root}/config" tf_config_env = trimspace(get_env("TF_CONFIG", ""))
bootstrap_config_path = get_env( bootstrap_config_path = local.tf_config_env != "" ? (
"GITOPS_BOOTSTRAP_CONFIG", startswith(local.tf_config_env, "/") ? local.tf_config_env : abspath("${local.repo_root}/${local.tf_config_env}")
"${local.config_root}/accounts/bootstrap.yaml" ) : abspath("${local.gitops_root}/${get_env("GITOPS_BOOTSTRAP_CONFIG", "config/bootstrap.yaml")}")
)
} }
inputs = { inputs = {
bootstrap_config_path = local.bootstrap_config_path bootstrap_config_path = local.bootstrap_config_path
config_root = local.gitops_repo_root
} }

View File

@ -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" { variable "bootstrap_config_path" {
description = "Path to the bootstrap account configuration YAML" description = "Path to the bootstrap account configuration YAML"
type = string type = string
validation { validation {
condition = var.bootstrap_config_path != null && trimspace(var.bootstrap_config_path) != "" 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
}