From b89600ea70e766c57b24b7e32cdd4563506169b5 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Sat, 30 May 2026 23:35:02 -0400 Subject: [PATCH] feat(bootstrap): dedicated AWS state backend + scoped IAM role via terraform-aws-template terragrunt plan against this stack was failing with S3 403 because state was pointing at the terraform-proxmox bucket (cross-stack) and the day-to-day terraform IAM user lacks ListBucket on that bucket. Rather than widen the existing user's S3 grants across stacks, follow the canonical terraform-aws-template pattern: one bucket per stack, one scoped IAM role per stack, operator assumes the role with MFA via aws-vault. What lands - bootstrap/ subdirectory containing a thin module call to dryvist/terraform-aws-template pinned to commit c85894b (v0.1.0) with project = github. Yields S3 bucket tfstate-github- (AES256, versioning, public-access-block, TLS-only policy, 90-day noncurrent expiry, S3 native locking) and IAM role tf-github with combined OIDC + MFA-gated AssumeRole trust. github_org / github_repo / operator_user_arns are required vars supplied via gitignored terraform.tfvars - no identities in source tree. GitHub Actions OIDC provider also created (account-wide singleton; import path documented if it pre-exists). - bootstrap/README.md walks the operator through one-time apply, state migration into the new bucket at key _bootstrap/terraform.tfstate, ~/.aws/config profile addition, verification. - terragrunt.hcl switched from terraform-proxmox-state-useast2- to tfstate-github-, key to github/terraform.tfstate, dropped dynamodb_table (S3 native lock only). No state to migrate since no apply has ever run against this stack. - versions.tf bumped required_version to >= 1.10 (for use_lockfile). - AGENTS.md adds a State backend section: bucket-per-stack rationale, operator identity flow, aws-vault profile naming convention, future CI via OIDC, and the rule never run this stack with elevated bootstrap creds. - README.md replaces the manual tofu init -backend-config snippet with aws-vault exec tf-github -- terragrunt and points to bootstrap/README.md for first-time setup. - .pre-commit-config.yaml adds --hook-config=--retry-once-with-cleanup to terraform_validate, the documented antonbabenko/pre-commit-terraform escape hatch for module sources pinned to commit SHAs. Without it the hook fails on first call after a module-SHA bump because the prior .terraform/modules/ install no longer matches; with it the hook auto-cleans and re-inits before the validate retry. Cost impact: free. S3 for a small state file plus a few noncurrent versions sits well under any free-tier ceiling. AES256 SSE-S3 (no KMS). S3 native locking (no DynamoDB). IAM role + inline policies + OIDC provider are all free. Verification - tofu validate in both root and bootstrap -> green - pre-commit run --all-files from a cold .terraform/ -> green - Checkov CKV_TF_1 satisfied by pinning the template module source to commit c85894b3667cc753a3d5ac07b50e9a7be9302331 (v0.1.0 tag's resolved commit), not the tag name - Commit GPG-signed - No identities in source tree Assisted-by: Claude --- .pre-commit-config.yaml | 8 ++ AGENTS.md | 52 +++++++++ README.md | 42 +++++--- bootstrap/README.md | 164 +++++++++++++++++++++++++++++ bootstrap/main.tf | 43 ++++++++ bootstrap/outputs.tf | 29 +++++ bootstrap/providers.tf | 3 + bootstrap/terraform.tfvars.example | 20 ++++ bootstrap/variables.tf | 62 +++++++++++ bootstrap/versions.tf | 25 +++++ terragrunt.hcl | 28 +++-- versions.tf | 12 ++- 12 files changed, 460 insertions(+), 28 deletions(-) create mode 100644 bootstrap/README.md create mode 100644 bootstrap/main.tf create mode 100644 bootstrap/outputs.tf create mode 100644 bootstrap/providers.tf create mode 100644 bootstrap/terraform.tfvars.example create mode 100644 bootstrap/variables.tf create mode 100644 bootstrap/versions.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56c0be2..6c41962 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,14 @@ repos: hooks: - id: terraform_fmt - id: terraform_validate + # `terraform_validate` fails on first run when a module source is + # pinned to a new commit SHA and `.terraform/modules/` still holds + # the previous install. `--retry-once-with-cleanup` makes the hook + # purge `.terraform/` and re-init on first validate failure, then + # re-run validate — the documented escape hatch from + # antonbabenko/pre-commit-terraform for SHA-pinned module sources. + args: + - --hook-config=--retry-once-with-cleanup=true - id: terraform_tflint args: - --args=--only=terraform_deprecated_interpolation diff --git a/AGENTS.md b/AGENTS.md index e7087ce..134df09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,58 @@ any operator who runs `tofu apply` without overrides). Enforce explicitly: tofu apply -var markdown_lint_enforcement=active ``` +## State backend + +State for the org-rulesets stack lives in **its own dedicated S3 bucket +and is gated by its own scoped IAM role.** Cross-stack state-bucket +sharing is not used; every Terraform stack in this account gets its own +bucket via the `terraform-aws-template` module, on the principle that +"a misapply against this repo cannot reach another stack's state and +its IAM grants don't widen as new stacks come online." + +| Component | Value | +| --- | --- | +| State bucket | `tfstate-github-` (us-east-2) | +| State key | `github/terraform.tfstate` | +| Bootstrap state key | `_bootstrap/terraform.tfstate` (same bucket) | +| Lock | S3 native (`use_lockfile = true` — no DynamoDB) | +| Encryption | AES256 / SSE-S3 (no KMS) | +| IAM role | `tf-github` — scoped to that one bucket only | + +Identity flow: + +1. Operator's underlying IAM user (e.g. `terraform`) sits in `~/.aws/config` + as a `source_profile`. MFA is required on this user — the role's + trust policy denies sessions without `aws:MultiFactorAuthPresent`. +2. `aws-vault exec tf-github -- ` calls AWS STS to assume + `tf-github`. aws-vault prompts for MFA once per session and caches + the STS credentials. +3. The STS credentials reach `tofu` / `terragrunt` via environment + variables. The `aws` provider in the github provider's wire-up + has no work — the github provider uses `GITHUB_TOKEN`, not AWS — but + the backend's S3 access does, and it's scoped to the one bucket. + +The aws-vault profile name (`tf-github`) **matches the role name** by +convention. Sibling stacks bootstrapped via `terraform-aws-template` +follow the same pattern: profile name = `tf-` = role name. + +Future CI uses GitHub OIDC instead of MFA AssumeRole. The role's trust +policy already accepts `repo:/` on push to +the default branch and on pull_request events — no operator user +involvement. `.github/workflows/terragrunt.yml` is not in this repo +yet; when added, it uses `aws-actions/configure-aws-credentials@v4` +with `role-to-assume = arn:aws:iam:::role/tf-github`. + +**Never** run this stack with the elevated bootstrap credentials +(`iam-user` or any admin identity). Those are only for one-time +`bootstrap/` applies. All ongoing operations — `terragrunt init`, +`plan`, `apply` — go through `aws-vault exec tf-github`. + +First-time setup walkthrough lives in +[`bootstrap/README.md`](bootstrap/README.md). Re-run only when the +template's pinned ref bumps or the role / bucket configuration +intentionally changes. + ## Cost policy **Never apply a policy or enable a feature that costs money unless the diff --git a/README.md b/README.md index 418f026..5fbaffe 100644 --- a/README.md +++ b/README.md @@ -64,37 +64,53 @@ upstream via `data "http"`, not committed as a local template. ## Requirements -- **OpenTofu** (>= 1.6) and the `integrations/github` provider, pinned in - `versions.tf`. The dev shell supplies the toolchain via direnv: +- **OpenTofu** (>= 1.10 for S3 native locking) and the + `integrations/github` provider, pinned in `versions.tf`. Dev shell via + direnv: ```bash git clone git@github.com:dryvist/terraform-github.git - cd terraform-github && direnv allow # provides tofu, terraform, terragrunt + cd terraform-github && direnv allow # provides tofu, terraform, terragrunt, aws-vault ``` +- **AWS state backend bootstrapped.** The dedicated state bucket + (`tfstate-github-`) and scoped IAM role (`tf-github`) must + exist. First-time setup runs once with elevated AWS admin creds — + see [`bootstrap/README.md`](bootstrap/README.md). After bootstrap, all + ongoing operations use the scoped role via `aws-vault`. + +- **`aws-vault` profile `tf-github`** in `~/.aws/config`. Profile shape + is documented in `bootstrap/README.md` → "Hand off to the operator". + Verify with `aws-vault exec tf-github -- aws sts get-caller-identity` + before running terragrunt. + - **`GITHUB_TOKEN` with `admin:org`** (the ORG_ADMIN token tier, - `gh-claude-org-admin`) to create or modify org rulesets. The provider reads it - from the environment. -- **S3 state backend** access — bucket / key / region supplied at init (see Usage). + `gh-claude-org-admin`) for apply. The github provider reads it from + the environment; the default `DRYVIST` tier is read-only on org + rulesets and will `403` on apply. ## Usage -State lives in S3 (org convention). Backend values (bucket / key / region) are -supplied at init — never committed, because the bucket name embeds the AWS -account ID: +Daily flow (after bootstrap): ```bash -tofu init -backend-config=bucket= \ - -backend-config=key=terraform-github/terraform.tfstate \ - -backend-config=region=us-east-2 +aws-vault exec tf-github -- terragrunt init # one-time per worktree +aws-vault exec tf-github -- terragrunt plan +aws-vault exec tf-github -- terragrunt apply ``` -Validation needs no backend or credentials: +`terragrunt.hcl` resolves the state bucket name from +`get_aws_account_id()` at runtime, so no account identifier is committed. + +Validation only (no backend, no credentials): ```bash tofu init -backend=false && tofu validate ``` +First-time setup (one-time, elevated AWS creds): see +[`bootstrap/README.md`](bootstrap/README.md). + **Rolling out a rule safely.** Org-wide enforcement can block merges everywhere at once. For `markdown_lint_enforcement` (legacy default `evaluate`), use the dry-run gate before enforcing: diff --git a/bootstrap/README.md b/bootstrap/README.md new file mode 100644 index 0000000..fb8028d --- /dev/null +++ b/bootstrap/README.md @@ -0,0 +1,164 @@ +# bootstrap + +One-time provisioning of the AWS state backend (S3 bucket) and scoped IAM +role (`tf-github`) that the parent terraform-github stack uses for all +subsequent operations. Calls the canonical +[dryvist/terraform-aws-template](https://github.com/dryvist/terraform-aws-template) +module at the pinned `v0.1.0` ref. + +Run this **once**, with elevated AWS admin credentials. After the apply +succeeds and state is migrated into the new bucket, this directory is +effectively read-only — re-runs only happen if the template's pinned ref +bumps or if the IAM role / bucket need a documented configuration change. + +```mermaid +graph LR + EU["Elevated AWS user
(temporary, admin)"] -->|tofu apply
local state| BS[bootstrap/] + BS -->|creates| OIDC[aws_iam_openid_
connect_provider.github] + BS -->|calls module| MOD["terraform-aws-template v0.1.0"] + MOD -->|creates| BKT["S3 bucket
tfstate-github-<account>"] + MOD -->|creates| ROLE["IAM role tf-github"] + ROLE -.->|trust + MFA| OP["IAM user terraform
(operator)"] + ROLE -.->|trust OIDC| GHA["GitHub Actions
repo:<org>/<repo>"] +``` + +## Requirements + +Run `aws sts get-caller-identity` from this directory's shell. The ARN must +be an admin / elevated user with permission to: + +- Create / read S3 buckets and bucket policies +- Create IAM roles, inline policies, and OIDC providers +- Read OIDC providers (for the import path if one already exists) + +The operator IAM user that goes into `operator_user_arns` must have MFA +enabled in IAM. The role's trust policy enforces `aws:MultiFactorAuthPresent`, +so MFA-less sessions cannot AssumeRole regardless of the operator's other +permissions. + +Tooling: + +- OpenTofu ≥ 1.10 (for `use_lockfile`) +- `aws` CLI ≥ 2.x + +## Usage + +End-to-end flow: one-time bootstrap apply, then state migration, then a +one-time `~/.aws/config` profile addition. After that, the operator runs the +parent stack via `aws-vault exec tf-github -- terragrunt …`. + +### 1. Apply the bootstrap + +```bash +cd bootstrap +cp terraform.tfvars.example terraform.tfvars # then edit with real values +tofu init +tofu apply +``` + +Expected resources on a clean account: + +| Resource | What it is | +| --- | --- | +| `aws_iam_openid_connect_provider.github` | Account-wide GitHub Actions OIDC provider | +| `module.state_backend.aws_s3_bucket.state` | `tfstate-github-` (AES256, versioning, public-access-blocked, TLS-only, 90-day noncurrent expiry) | +| `module.state_backend.aws_iam_role.terraform` | `tf-github` with combined trust (OIDC for the repo + MFA AssumeRole from the operator user) | +| `module.state_backend.aws_iam_role_policy.state` | Inline policy scoped to the new bucket only | + +**OIDC provider already exists?** If another stack created it, the apply +errors with `EntityAlreadyExists`. Import once, then re-run apply: + +```bash +tofu import aws_iam_openid_connect_provider.github \ + arn:aws:iam:::oidc-provider/token.actions.githubusercontent.com +tofu apply +``` + +### 2. Migrate state into the new bucket + +After the first apply, the bootstrap state is still local +(`bootstrap/terraform.tfstate`). Move it into the bucket it just created: + +1. Capture the backend config from outputs: + + ```bash + tofu output -raw backend_config + ``` + +2. In `versions.tf`, uncomment the `backend "s3"` block and replace the + `` placeholder in the bucket name with your real account id + (or paste the entire block from step 1's output). + +3. Migrate: + + ```bash + tofu init -migrate-state + ``` + + Confirm `yes` when prompted. The local `terraform.tfstate` lifts into + `s3://tfstate-github-/_bootstrap/terraform.tfstate`. Delete + the local file once migration succeeds; the gitignored backup is fine + to keep until you trust the migration. + +### 3. Hand off to the operator + +Once the role exists, the operator (whose IAM user ARN was in +`operator_user_arns`) needs a matching `aws-vault` profile. Add to +`~/.aws/config`: + +```ini +[profile tf-github] +role_arn = arn:aws:iam:::role/tf-github +source_profile = +mfa_serial = arn:aws:iam:::mfa/ +region = us-east-2 +``` + +`` is whatever profile holds the operator IAM +user's static keys (e.g. `terraform`). `` matches the +user portion of `operator_user_arns[i]`. The MFA serial is found at +IAM → Users → operator → Security credentials → "Assigned MFA device". + +Verify: + +```bash +aws-vault exec tf-github -- aws sts get-caller-identity +``` + +The returned ARN should be `arn:aws:sts:::assumed-role/tf-github/`, +not the operator user. + +### 4. Run the parent stack + +From the repo root (one level up): + +```bash +aws-vault exec tf-github -- terragrunt init +aws-vault exec tf-github -- terragrunt plan +aws-vault exec tf-github -- terragrunt apply +``` + +`terragrunt.hcl` at the root is already configured to point at +`tfstate-github-` under key `github/terraform.tfstate`. No +further edits required after the bootstrap. + +### 5. What to do (or not) on subsequent changes + +- Bumping the template's pinned ref → edit `module.state_backend.source`, run + `tofu plan` to see the diff, apply if benign. +- Adding more operator users → append ARNs to `operator_user_arns` in + `terraform.tfvars`, apply. The role's trust policy updates in place. +- Adding `.github/workflows/terragrunt.yml` to the parent stack → no + bootstrap change. The role already trusts `repo:/` + on push to `main` and on pull_request. +- Wanting to widen `branch_pattern` → edit `terraform.tfvars`, apply. +- Wanting to widen the role's permissions → DON'T. Template is scoped to + one bucket on purpose. New AWS responsibilities = new role from the + template, not policy creep on this one. + +## Cost + +Free for this size. See the parent stack's +[`AGENTS.md` cost-policy section](../AGENTS.md#cost-policy) for the +matrix. S3 storage for one tiny state file + a few noncurrent versions is +~$0/month, no KMS, no DynamoDB. diff --git a/bootstrap/main.tf b/bootstrap/main.tf new file mode 100644 index 0000000..7a3fefd --- /dev/null +++ b/bootstrap/main.tf @@ -0,0 +1,43 @@ +# GitHub Actions OIDC provider — prerequisite for the IAM role's OIDC +# trust statement. Account-wide singleton: one provider per AWS account +# serves every GitHub repo that needs OIDC. If another stack in this +# account already created this provider, this resource's create will +# fail with "EntityAlreadyExists"; resolve by importing once: +# +# tofu import aws_iam_openid_connect_provider.github \ +# arn:aws:iam:::oidc-provider/token.actions.githubusercontent.com +# +# Then re-run apply. Subsequent applies are no-ops on this resource. +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] +} + +# State backend + scoped IAM role, provisioned via the canonical template. +# Project name is intentionally short ("github") — the template derives +# the bucket name as `tfstate-${project}-${account}` and the role name +# as `tf-${project}`, so the final names are `tfstate-github-` +# and `tf-github`. The consuming repo's terragrunt.hcl points its state +# at this bucket under key `github/terraform.tfstate`; the bootstrap's +# own state lives at `_bootstrap/terraform.tfstate` after migration. +# +# depends_on ensures the OIDC provider exists before the role's trust +# policy references it. +module "state_backend" { + # Pinned to the commit SHA that v0.1.0 points to, per CKV_TF_1: module + # sources from a git URL should pin to an immutable commit, not a tag + # name (tags can be force-pushed). The trailing comment records the + # human-readable tag this SHA materialized from so future bumps are + # traceable. Update both together when bumping. + source = "git::https://github.com/dryvist/terraform-aws-template.git?ref=c85894b3667cc753a3d5ac07b50e9a7be9302331" # v0.1.0 + + project = "github" + github_org = var.github_org + github_repo = var.github_repo + branch_pattern = var.branch_pattern + aws_region = var.aws_region + + operator_user_arns = var.operator_user_arns + + depends_on = [aws_iam_openid_connect_provider.github] +} diff --git a/bootstrap/outputs.tf b/bootstrap/outputs.tf new file mode 100644 index 0000000..15fb3e7 --- /dev/null +++ b/bootstrap/outputs.tf @@ -0,0 +1,29 @@ +output "state_bucket" { + description = "S3 bucket the consuming repo writes its state to." + value = module.state_backend.state_bucket +} + +output "state_bucket_arn" { + description = "ARN of the state bucket." + value = module.state_backend.state_bucket_arn +} + +output "tf_role_arn" { + description = "IAM role ARN the operator assumes (via aws-vault + MFA) and CI assumes (via GitHub OIDC) to run terraform-github." + value = module.state_backend.tf_role_arn +} + +output "aws_region" { + description = "Region where the state bucket lives." + value = module.state_backend.aws_region +} + +output "state_key_prefix" { + description = "Prefix the consuming repo writes its state objects under." + value = module.state_backend.state_key_prefix +} + +output "backend_config" { + description = "Ready-to-paste `terraform { backend \"s3\" {} }` block for the consuming repo's backend configuration. Use this to fill the commented-out backend block in `versions.tf` before running `tofu init -migrate-state`." + value = module.state_backend.backend_config +} diff --git a/bootstrap/providers.tf b/bootstrap/providers.tf new file mode 100644 index 0000000..c9d7ccb --- /dev/null +++ b/bootstrap/providers.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = var.aws_region +} diff --git a/bootstrap/terraform.tfvars.example b/bootstrap/terraform.tfvars.example new file mode 100644 index 0000000..0060fe5 --- /dev/null +++ b/bootstrap/terraform.tfvars.example @@ -0,0 +1,20 @@ +# Copy to `terraform.tfvars` (gitignored) and fill in the placeholders. +# `terraform.tfvars` carries identities (GitHub org login, repo name, +# operator IAM user ARN) — those values stay out of git per the +# no-identities-in-source rule. The example file ships with placeholders +# so the shape is obvious; it doesn't disclose anything. + +github_org = "" +github_repo = "" + +# IAM user ARN(s) allowed to assume `tf-github` from a local shell. +# Each entry must match `arn:aws:iam:::user/`. +# Operator must have MFA enabled — the role's trust policy requires +# `aws:MultiFactorAuthPresent = true`. +operator_user_arns = [ + "arn:aws:iam:::user/", +] + +# Optional overrides (defaults below match this stack's convention): +# aws_region = "us-east-2" +# branch_pattern = "main" diff --git a/bootstrap/variables.tf b/bootstrap/variables.tf new file mode 100644 index 0000000..9a3b2cc --- /dev/null +++ b/bootstrap/variables.tf @@ -0,0 +1,62 @@ +variable "github_org" { + description = <<-EOT + GitHub org login that owns the consuming repo. Pinned into the + OIDC trust subject of the bootstrapped role. Required, no default — + operator supplies via tfvars or `-var` at apply time so the source + tree stays identity-free. + EOT + type = string + + validation { + condition = length(var.github_org) > 0 + error_message = "github_org must be a non-empty GitHub org login." + } +} + +variable "github_repo" { + description = <<-EOT + Name of the consuming repo. Pinned into the OIDC trust subject so the + role can only be assumed by GitHub Actions runs of this specific repo. + Required, no default. + EOT + type = string + + validation { + condition = length(var.github_repo) > 0 + error_message = "github_repo must be a non-empty repo name." + } +} + +variable "operator_user_arns" { + description = <<-EOT + IAM user ARNs allowed to assume the bootstrapped role with MFA from + local dev shells. Required, no default — operator supplies via + tfvars at apply time. Empty list disables operator AssumeRole + entirely (CI-only operation). + EOT + type = list(string) + + validation { + condition = alltrue([for a in var.operator_user_arns : can(regex("^arn:aws:iam::[0-9]{12}:user/", a))]) + error_message = "Each operator_user_arns entry must be an IAM user ARN (arn:aws:iam:::user/)." + } +} + +variable "aws_region" { + description = <<-EOT + Region for the state bucket. Matches the existing org convention + (us-east-2) used by sibling state buckets in this account. + EOT + type = string + default = "us-east-2" +} + +variable "branch_pattern" { + description = <<-EOT + Branch name pattern (StringLike against the OIDC sub claim) that + CI is allowed to assume the role from on push events. Default + `main` matches the consuming repo's default branch. + EOT + type = string + default = "main" +} diff --git a/bootstrap/versions.tf b/bootstrap/versions.tf new file mode 100644 index 0000000..7f9e207 --- /dev/null +++ b/bootstrap/versions.tf @@ -0,0 +1,25 @@ +terraform { + required_version = ">= 1.10" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + # Backend stays commented for the first apply (no S3 bucket exists yet). + # After `tofu apply` materializes the bucket below, uncomment and run + # `tofu init -migrate-state` to lift the local bootstrap state into the + # bucket it just created. From then on the bootstrap state lives at + # `_bootstrap/terraform.tfstate` in the same bucket as the main rulesets + # state (which lives at `github/terraform.tfstate`). + # + # backend "s3" { + # bucket = "tfstate-github-" + # key = "_bootstrap/terraform.tfstate" + # region = "us-east-2" + # use_lockfile = true + # encrypt = true + # } +} diff --git a/terragrunt.hcl b/terragrunt.hcl index 7541e1d..f13fbe6 100644 --- a/terragrunt.hcl +++ b/terragrunt.hcl @@ -1,16 +1,24 @@ -# Terragrunt configuration for terraform-github (dryvist org governance). +# Terragrunt configuration for the org governance stack. # -# Governance state is small and has no SOPS/Doppler/deployment.json layers — -# this file only wires the shared S3 remote state backend. The github provider -# reads GITHUB_TOKEN from the environment (ORG_ADMIN tier to apply). +# State backend is this stack's own dedicated S3 bucket, provisioned once by +# `bootstrap/` calling the terraform-aws-template module. Bucket and role +# naming follow the template's formula (`tfstate-${project}-${account}` and +# `tf-${project}` with `project = "github"`). The bucket name embeds the AWS +# account id, resolved at runtime via `get_aws_account_id()` so no account +# identifier is committed. +# +# Apply requires assumed-role STS credentials from the `tf-github` role +# (operator: `aws-vault exec tf-github`; CI: `aws-actions/configure-aws-credentials@v4` +# with the role ARN). The github provider reads `GITHUB_TOKEN` separately — +# ORG_ADMIN tier to apply org-level rulesets. terraform { source = "." } -# Remote state backend configuration using S3 (org convention). -# The bucket name embeds the AWS account id, so it is resolved at runtime -# rather than committed. +# Remote state backend. +# S3 native locking (`use_lockfile = true`) — no DynamoDB table needed. +# Requires OpenTofu/Terraform >= 1.10 (declared in versions.tf). remote_state { backend = "s3" generate = { @@ -18,13 +26,13 @@ remote_state { if_exists = "overwrite_terragrunt" } config = { - bucket = "terraform-proxmox-state-useast2-${get_aws_account_id()}" - key = "terraform-github/terraform.tfstate" + bucket = "tfstate-github-${get_aws_account_id()}" + key = "github/terraform.tfstate" region = "us-east-2" encrypt = true use_lockfile = true - # Retry configuration for transient S3 failures + # Retry configuration for transient S3 failures. max_retries = 5 } } diff --git a/versions.tf b/versions.tf index cec21cc..24a70e7 100644 --- a/versions.tf +++ b/versions.tf @@ -1,5 +1,7 @@ terraform { - required_version = ">= 1.6.0" + # >= 1.10 required for S3 native locking (`use_lockfile = true` in + # terragrunt.hcl). Earlier versions ignore the option and run unlocked. + required_version = ">= 1.10" required_providers { github = { @@ -8,9 +10,9 @@ terraform { } } - # Remote state in S3 (org convention). Backend values are supplied at init - # time (bucket / key / region) via `-backend-config` or terragrunt and are - # never hardcoded here, because the bucket name embeds the AWS account ID. - # See README.md → "State". Use `tofu init -backend=false` for validation only. + # Remote state in S3. Backend values come from terragrunt.hcl + # (`tfstate-github-` / `github/terraform.tfstate` / us-east-2) + # so no bucket name is committed here. Use `tofu init -backend=false` + # for validation-only runs. backend "s3" {} }