From 98399b8a16c8a0f4402366fbf4e26fe12e13e880 Mon Sep 17 00:00:00 2001 From: Nicolas Brieussel Date: Fri, 19 Jun 2026 14:15:20 +0200 Subject: [PATCH] t --- .../workflows/iam_terraform-backend-role.yml | 17 ++-- .github/workflows/scaleway.yml | 8 +- .github/workflows/terraform-lock.yml | 8 +- .../{0-backend => }/.terraform.lock.hcl | 0 00-remote_state/{0-backend => }/README.md | 17 ++-- .../env/00-remote-state-backend.tfvars | 0 00-remote_state/{0-backend => }/main.tf | 0 00-remote_state/{0-backend => }/outputs.tf | 0 00-remote_state/{0-backend => }/variables.tf | 0 00-remote_state/{0-backend => }/version.tf | 0 .../{ => bootstrap}/aws/.terraform.lock.hcl | 0 01-iam/{ => bootstrap}/aws/README.md | 20 ++-- 01-iam/{ => bootstrap}/aws/iam-ci.tf | 0 01-iam/{ => bootstrap}/aws/main.tf | 0 01-iam/{ => bootstrap}/aws/outputs.tf | 0 01-iam/{ => bootstrap}/aws/variables.tf | 0 01-iam/{ => bootstrap}/aws/version.tf | 0 .../infisical/.terraform.lock.hcl | 0 01-iam/{ => bootstrap}/infisical/README.md | 10 +- .../infisical/env/01-iam-infisical.tfvars | 0 01-iam/{ => bootstrap}/infisical/main.tf | 0 01-iam/{ => bootstrap}/infisical/outputs.tf | 0 01-iam/{ => bootstrap}/infisical/variables.tf | 0 01-iam/{ => bootstrap}/infisical/version.tf | 0 .../scaleway/.terraform.lock.hcl | 0 01-iam/{ => bootstrap}/scaleway/README.md | 20 ++-- .../scaleway/env/01-iam-scaleway.tfvars | 0 01-iam/{ => bootstrap}/scaleway/main.tf | 0 01-iam/{ => bootstrap}/scaleway/outputs.tf | 0 01-iam/{ => bootstrap}/scaleway/variables.tf | 0 01-iam/{ => bootstrap}/scaleway/version.tf | 0 .../aws-state-access}/.terraform.lock.hcl | 0 .../env/00-remote-state-iam.tfvars | 0 .../ci-managed/aws-state-access}/main.tf | 0 .../ci-managed/aws-state-access}/outputs.tf | 0 .../ci-managed/aws-state-access}/variables.tf | 0 .../ci-managed/aws-state-access}/version.tf | 0 CLAUDE.md | 98 ++++++++++++------- README.md | 2 +- mise.toml | 18 ++-- modules/README.md | 11 +-- 41 files changed, 129 insertions(+), 100 deletions(-) rename 00-remote_state/{0-backend => }/.terraform.lock.hcl (100%) rename 00-remote_state/{0-backend => }/README.md (87%) rename 00-remote_state/{0-backend => }/env/00-remote-state-backend.tfvars (100%) rename 00-remote_state/{0-backend => }/main.tf (100%) rename 00-remote_state/{0-backend => }/outputs.tf (100%) rename 00-remote_state/{0-backend => }/variables.tf (100%) rename 00-remote_state/{0-backend => }/version.tf (100%) rename 01-iam/{ => bootstrap}/aws/.terraform.lock.hcl (100%) rename 01-iam/{ => bootstrap}/aws/README.md (89%) rename 01-iam/{ => bootstrap}/aws/iam-ci.tf (100%) rename 01-iam/{ => bootstrap}/aws/main.tf (100%) rename 01-iam/{ => bootstrap}/aws/outputs.tf (100%) rename 01-iam/{ => bootstrap}/aws/variables.tf (100%) rename 01-iam/{ => bootstrap}/aws/version.tf (100%) rename 01-iam/{ => bootstrap}/infisical/.terraform.lock.hcl (100%) rename 01-iam/{ => bootstrap}/infisical/README.md (89%) rename 01-iam/{ => bootstrap}/infisical/env/01-iam-infisical.tfvars (100%) rename 01-iam/{ => bootstrap}/infisical/main.tf (100%) rename 01-iam/{ => bootstrap}/infisical/outputs.tf (100%) rename 01-iam/{ => bootstrap}/infisical/variables.tf (100%) rename 01-iam/{ => bootstrap}/infisical/version.tf (100%) rename 01-iam/{ => bootstrap}/scaleway/.terraform.lock.hcl (100%) rename 01-iam/{ => bootstrap}/scaleway/README.md (86%) rename 01-iam/{ => bootstrap}/scaleway/env/01-iam-scaleway.tfvars (100%) rename 01-iam/{ => bootstrap}/scaleway/main.tf (100%) rename 01-iam/{ => bootstrap}/scaleway/outputs.tf (100%) rename 01-iam/{ => bootstrap}/scaleway/variables.tf (100%) rename 01-iam/{ => bootstrap}/scaleway/version.tf (100%) rename {00-remote_state/1-iam => 01-iam/ci-managed/aws-state-access}/.terraform.lock.hcl (100%) rename {00-remote_state/1-iam => 01-iam/ci-managed/aws-state-access}/env/00-remote-state-iam.tfvars (100%) rename {00-remote_state/1-iam => 01-iam/ci-managed/aws-state-access}/main.tf (100%) rename {00-remote_state/1-iam => 01-iam/ci-managed/aws-state-access}/outputs.tf (100%) rename {00-remote_state/1-iam => 01-iam/ci-managed/aws-state-access}/variables.tf (100%) rename {00-remote_state/1-iam => 01-iam/ci-managed/aws-state-access}/version.tf (100%) diff --git a/.github/workflows/iam_terraform-backend-role.yml b/.github/workflows/iam_terraform-backend-role.yml index 0ec0eed..f757ed4 100644 --- a/.github/workflows/iam_terraform-backend-role.yml +++ b/.github/workflows/iam_terraform-backend-role.yml @@ -1,20 +1,21 @@ name: Apply Terraform Backend Role -# Terraform GitOps for the 00-remote_state/1-iam/ root: the org-wide state-access -# role is created/synchronised from remote via the .github/actions/terraform -# composite action. Plans on PR, applies on push to main. Credentials come from -# the identity/00-ci-trust CI role assumed via OIDC — no static AWS keys. +# Terraform GitOps for the 01-iam/ci-managed/aws-state-access/ root: the org-wide +# state-access role is created/synchronised from remote via the +# .github/actions/terraform composite action. Plans on PR, applies on push to main. +# Credentials come from the 01-iam/bootstrap/aws CI role assumed via OIDC — no +# static AWS keys. on: push: branches: [main] paths: - - '00-remote_state/1-iam/**' + - '01-iam/ci-managed/aws-state-access/**' - '.github/actions/terraform/**' - '.github/workflows/iam_terraform-backend-role.yml' pull_request: paths: - - '00-remote_state/1-iam/**' + - '01-iam/ci-managed/aws-state-access/**' - '.github/actions/terraform/**' - '.github/workflows/iam_terraform-backend-role.yml' @@ -38,7 +39,7 @@ jobs: ROLE_ARN: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} run: | if [ -z "$ROLE_ARN" ]; then - echo "::error::vars.AWS_GITHUB_ACTIONS_ROLE_ARN is not set (provisioned by identity/00-ci-trust/)." + echo "::error::vars.AWS_GITHUB_ACTIONS_ROLE_ARN is not set (provisioned by 01-iam/bootstrap/aws/)." exit 1 fi @@ -49,7 +50,7 @@ jobs: - uses: ./.github/actions/terraform with: - root: 00-remote_state/1-iam + root: 01-iam/ci-managed/aws-state-access tfvars-file: 00-remote-state-iam.tfvars aws-role-arn: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} command: ${{ github.event_name == 'push' && 'apply' || 'plan' }} diff --git a/.github/workflows/scaleway.yml b/.github/workflows/scaleway.yml index 1cd407c..2be2b9b 100644 --- a/.github/workflows/scaleway.yml +++ b/.github/workflows/scaleway.yml @@ -7,9 +7,9 @@ name: Terraform — Scaleway cluster # - workflow_dispatch → up/down on demand (plan | apply | destroy) # # State R/W + lock uses the org state role (vars.AWS_TF_STATE_ROLE_ARN — -# AmazonS3FullAccess, see 00-remote_state/1-iam). Scaleway cluster lifecycle uses -# the CI API key from the `scaleway` environment (Kubernetes/VPC/PrivateNetwork -# FullAccess, see 01-iam/scaleway). Provider creds are set as job env (read by the +# AmazonS3FullAccess, see 01-iam/ci-managed/aws-state-access). Scaleway cluster +# lifecycle uses the CI API key from the `scaleway` environment (Kubernetes/VPC/ +# PrivateNetwork FullAccess, see 01-iam/bootstrap/scaleway). Provider creds are set as job env (read by the # action) — never passed as plain action inputs. No static AWS keys. on: @@ -70,7 +70,7 @@ jobs: ROLE_ARN: ${{ vars.AWS_TF_STATE_ROLE_ARN }} run: | if [ -z "$ROLE_ARN" ]; then - echo "::error::vars.AWS_TF_STATE_ROLE_ARN is not set (provisioned by 00-remote_state/1-iam)." + echo "::error::vars.AWS_TF_STATE_ROLE_ARN is not set (provisioned by 01-iam/ci-managed/aws-state-access)." exit 1 fi diff --git a/.github/workflows/terraform-lock.yml b/.github/workflows/terraform-lock.yml index 0eaee50..ee7c5d0 100644 --- a/.github/workflows/terraform-lock.yml +++ b/.github/workflows/terraform-lock.yml @@ -23,10 +23,10 @@ jobs: fail-fast: false matrix: root: - - 00-remote_state/0-backend - - 00-remote_state/1-iam - - 01-iam/aws - - 01-iam/scaleway + - 00-remote_state + - 01-iam/bootstrap/aws + - 01-iam/bootstrap/scaleway + - 01-iam/ci-managed/aws-state-access - 02-cluster/scaleway steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/00-remote_state/0-backend/.terraform.lock.hcl b/00-remote_state/.terraform.lock.hcl similarity index 100% rename from 00-remote_state/0-backend/.terraform.lock.hcl rename to 00-remote_state/.terraform.lock.hcl diff --git a/00-remote_state/0-backend/README.md b/00-remote_state/README.md similarity index 87% rename from 00-remote_state/0-backend/README.md rename to 00-remote_state/README.md index de13e06..b2582a6 100644 --- a/00-remote_state/0-backend/README.md +++ b/00-remote_state/README.md @@ -1,12 +1,13 @@ -# state/00-backend — Terraform state bucket +# 00-remote_state — Terraform state bucket A standalone Terraform root that provisions a single **AWS S3 bucket** to hold the Terraform remote state for the **whole org**. Every other root -(`cluster/local/`, `cluster/scaleway/`, `ci/10-scaleway/`, and future ones) points -its `backend "s3"` at this bucket. +(`02-cluster/local/`, `02-cluster/scaleway/`, `01-iam/bootstrap/scaleway/`, and +future ones) points its `backend "s3"` at this bucket. -It is the shared substrate every other root depends on — strata `00` of the -`state/` domain, applied by an admin. +It is the shared substrate every other root depends on — the `00-remote_state` +domain (a single-root domain, so it is flattened: the domain folder *is* the +root), applied by an admin. ## What it creates @@ -57,13 +58,13 @@ This root creates the very bucket it then stores its state in. Bootstrap order: 2. Apply once with **local state** — temporarily comment out the `backend "s3"` block in `version.tf` so the bucket gets created: ```bash - terraform -chdir=state/00-backend init - terraform -chdir=state/00-backend apply # creates the bucket (billable) + terraform -chdir=00-remote_state init + terraform -chdir=00-remote_state apply # creates the bucket (billable) ``` 3. Re-add the `backend "s3"` block and migrate the local state into the bucket it now manages: ```bash - terraform -chdir=state/00-backend init -migrate-state + terraform -chdir=00-remote_state init -migrate-state ``` After that, this root's own state lives at `state-backend/terraform.tfstate` diff --git a/00-remote_state/0-backend/env/00-remote-state-backend.tfvars b/00-remote_state/env/00-remote-state-backend.tfvars similarity index 100% rename from 00-remote_state/0-backend/env/00-remote-state-backend.tfvars rename to 00-remote_state/env/00-remote-state-backend.tfvars diff --git a/00-remote_state/0-backend/main.tf b/00-remote_state/main.tf similarity index 100% rename from 00-remote_state/0-backend/main.tf rename to 00-remote_state/main.tf diff --git a/00-remote_state/0-backend/outputs.tf b/00-remote_state/outputs.tf similarity index 100% rename from 00-remote_state/0-backend/outputs.tf rename to 00-remote_state/outputs.tf diff --git a/00-remote_state/0-backend/variables.tf b/00-remote_state/variables.tf similarity index 100% rename from 00-remote_state/0-backend/variables.tf rename to 00-remote_state/variables.tf diff --git a/00-remote_state/0-backend/version.tf b/00-remote_state/version.tf similarity index 100% rename from 00-remote_state/0-backend/version.tf rename to 00-remote_state/version.tf diff --git a/01-iam/aws/.terraform.lock.hcl b/01-iam/bootstrap/aws/.terraform.lock.hcl similarity index 100% rename from 01-iam/aws/.terraform.lock.hcl rename to 01-iam/bootstrap/aws/.terraform.lock.hcl diff --git a/01-iam/aws/README.md b/01-iam/bootstrap/aws/README.md similarity index 89% rename from 01-iam/aws/README.md rename to 01-iam/bootstrap/aws/README.md index ec4d196..c782449 100644 --- a/01-iam/aws/README.md +++ b/01-iam/bootstrap/aws/README.md @@ -1,12 +1,13 @@ -# identity/00-ci-trust — CI identity & governance +# 01-iam/bootstrap/aws — CI identity & governance A standalone Terraform root that provisions **keyless GitHub-OIDC → AWS** access for this repo's CI: an IAM OIDC identity provider, plus an IAM role GitHub Actions assumes (via short-lived OIDC tokens) to read/write the Terraform **remote state on AWS S3** — **no static AWS keys in GitHub secrets**. -This is **not** under `cluster/` — it provisions no cluster. It's a CI-platform -concern, kept as its own root so its state and blast radius stay small. +This is **not** under `02-cluster/` — it provisions no cluster. It's the trust +anchor of the `01-iam` domain (`bootstrap/` = human-applied), kept as its own +root so its state and blast radius stay small. ## Why OIDC here (and why Scaleway still uses a static key) @@ -18,7 +19,7 @@ GitHub secrets. Scaleway resources (Kapsule, VPC) are a different story: **Scaleway IAM is not an OIDC relying party** ([feature request](https://feature-request.scaleway.com/posts/761/oidc-provider-for-external-ci-cd)), -so those keep using a scoped, static Scaleway API key (see `ci/10-scaleway/`). +so those keep using a scoped, static Scaleway API key (see `01-iam/bootstrap/scaleway/`). OIDC here covers **only** the AWS/S3 side. Revisit Scaleway OIDC if/when Scaleway ships it. @@ -79,8 +80,8 @@ inline in [`iam-ci.tf`](./iam-ci.tf)): Verify the guardrails with the IAM policy simulator, e.g.: ```bash -ROLE=$(terraform -chdir=identity/00-ci-trust output -raw role_arn) -B=$(terraform -chdir=identity/00-ci-trust output -raw permissions_boundary_arn) +ROLE=$(terraform -chdir=01-iam/bootstrap/aws output -raw role_arn) +B=$(terraform -chdir=01-iam/bootstrap/aws output -raw permissions_boundary_arn) # CreateRole without our boundary -> explicitDeny aws iam simulate-principal-policy --policy-source-arn "$ROLE" \ --action-names iam:CreateRole \ @@ -99,8 +100,9 @@ aws iam simulate-principal-policy --policy-source-arn "$ROLE" \ ## Apply ```bash -mise run ci-trust-plan # terraform init && plan — review first -mise run ci-trust-apply # terraform apply (creates IAM resources) +terraform -chdir=01-iam/bootstrap/aws init +terraform -chdir=01-iam/bootstrap/aws plan # review first +terraform -chdir=01-iam/bootstrap/aws apply # creates IAM resources ``` > Never `terraform apply`/`destroy` here without explicit approval. @@ -113,7 +115,7 @@ variable (an ARN is a public identifier, not a secret). Set it once: ```bash gh variable set AWS_GITHUB_ACTIONS_ROLE_ARN \ --repo IntegratedDynamic/infrastructure \ - --body "$(terraform -chdir=identity/00-ci-trust output -raw role_arn)" + --body "$(terraform -chdir=01-iam/bootstrap/aws output -raw role_arn)" ``` Workflows then assume the role with `aws-actions/configure-aws-credentials`: diff --git a/01-iam/aws/iam-ci.tf b/01-iam/bootstrap/aws/iam-ci.tf similarity index 100% rename from 01-iam/aws/iam-ci.tf rename to 01-iam/bootstrap/aws/iam-ci.tf diff --git a/01-iam/aws/main.tf b/01-iam/bootstrap/aws/main.tf similarity index 100% rename from 01-iam/aws/main.tf rename to 01-iam/bootstrap/aws/main.tf diff --git a/01-iam/aws/outputs.tf b/01-iam/bootstrap/aws/outputs.tf similarity index 100% rename from 01-iam/aws/outputs.tf rename to 01-iam/bootstrap/aws/outputs.tf diff --git a/01-iam/aws/variables.tf b/01-iam/bootstrap/aws/variables.tf similarity index 100% rename from 01-iam/aws/variables.tf rename to 01-iam/bootstrap/aws/variables.tf diff --git a/01-iam/aws/version.tf b/01-iam/bootstrap/aws/version.tf similarity index 100% rename from 01-iam/aws/version.tf rename to 01-iam/bootstrap/aws/version.tf diff --git a/01-iam/infisical/.terraform.lock.hcl b/01-iam/bootstrap/infisical/.terraform.lock.hcl similarity index 100% rename from 01-iam/infisical/.terraform.lock.hcl rename to 01-iam/bootstrap/infisical/.terraform.lock.hcl diff --git a/01-iam/infisical/README.md b/01-iam/bootstrap/infisical/README.md similarity index 89% rename from 01-iam/infisical/README.md rename to 01-iam/bootstrap/infisical/README.md index 1745ee1..01f7496 100644 --- a/01-iam/infisical/README.md +++ b/01-iam/bootstrap/infisical/README.md @@ -1,11 +1,11 @@ -# 01-iam/infisical — GitHub Actions → Infisical (OIDC) +# 01-iam/bootstrap/infisical — GitHub Actions → Infisical (OIDC) A standalone Terraform root that sets up **keyless GitHub-OIDC → Infisical**: a dedicated Infisical machine identity that GitHub Actions authenticates as by presenting its short-lived OIDC token — no long-lived Infisical secret stored in CI. -This is the keyless counterpart to [`01-iam/scaleway`](../scaleway) (which has to +This is the keyless counterpart to [`01-iam/bootstrap/scaleway`](../scaleway) (which has to use a static API key because Scaleway IAM isn't an OIDC relying party). Infisical **is**, so we get the real keyless flow here. @@ -48,9 +48,9 @@ with it instead of any static secret. ## Apply ```bash -terraform -chdir=01-iam/infisical init -terraform -chdir=01-iam/infisical plan -var-file=env/01-iam-infisical.tfvars -terraform -chdir=01-iam/infisical apply -var-file=env/01-iam-infisical.tfvars +terraform -chdir=01-iam/bootstrap/infisical init +terraform -chdir=01-iam/bootstrap/infisical plan -var-file=env/01-iam-infisical.tfvars +terraform -chdir=01-iam/bootstrap/infisical apply -var-file=env/01-iam-infisical.tfvars ``` > Never `apply`/`destroy` here without explicit approval. diff --git a/01-iam/infisical/env/01-iam-infisical.tfvars b/01-iam/bootstrap/infisical/env/01-iam-infisical.tfvars similarity index 100% rename from 01-iam/infisical/env/01-iam-infisical.tfvars rename to 01-iam/bootstrap/infisical/env/01-iam-infisical.tfvars diff --git a/01-iam/infisical/main.tf b/01-iam/bootstrap/infisical/main.tf similarity index 100% rename from 01-iam/infisical/main.tf rename to 01-iam/bootstrap/infisical/main.tf diff --git a/01-iam/infisical/outputs.tf b/01-iam/bootstrap/infisical/outputs.tf similarity index 100% rename from 01-iam/infisical/outputs.tf rename to 01-iam/bootstrap/infisical/outputs.tf diff --git a/01-iam/infisical/variables.tf b/01-iam/bootstrap/infisical/variables.tf similarity index 100% rename from 01-iam/infisical/variables.tf rename to 01-iam/bootstrap/infisical/variables.tf diff --git a/01-iam/infisical/version.tf b/01-iam/bootstrap/infisical/version.tf similarity index 100% rename from 01-iam/infisical/version.tf rename to 01-iam/bootstrap/infisical/version.tf diff --git a/01-iam/scaleway/.terraform.lock.hcl b/01-iam/bootstrap/scaleway/.terraform.lock.hcl similarity index 100% rename from 01-iam/scaleway/.terraform.lock.hcl rename to 01-iam/bootstrap/scaleway/.terraform.lock.hcl diff --git a/01-iam/scaleway/README.md b/01-iam/bootstrap/scaleway/README.md similarity index 86% rename from 01-iam/scaleway/README.md rename to 01-iam/bootstrap/scaleway/README.md index 36b348d..47b9a40 100644 --- a/01-iam/scaleway/README.md +++ b/01-iam/bootstrap/scaleway/README.md @@ -1,12 +1,13 @@ -# ci/10-scaleway — Scaleway CI identity +# 01-iam/bootstrap/scaleway — Scaleway CI identity A standalone Terraform root that provisions the **Scaleway identity GitHub Actions uses to authenticate to Scaleway**. First real consumer: a smoke-test workflow that lists Object Storage buckets; the Terraform CI/CD pipeline itself is a separate, later concern. -This is **not** under `cluster/` — it provisions no cluster. It's a CI-platform -concern, kept as its own root so its state and blast radius stay small. +This is **not** under `02-cluster/` — it provisions no cluster. It's a +`01-iam/bootstrap/` trust anchor (human-applied), kept as its own root so its +state and blast radius stay small. ## Why a static key and not OIDC @@ -58,8 +59,9 @@ Same as the other roots: ## Apply ```bash -mise run scaleway-ci-plan # terraform init && plan — review first -mise run scaleway-ci-apply # terraform apply (billable: creates an IAM key) +terraform -chdir=01-iam/bootstrap/scaleway init +terraform -chdir=01-iam/bootstrap/scaleway plan # review first +terraform -chdir=01-iam/bootstrap/scaleway apply # billable: creates an IAM key ``` > Never `terraform apply`/`destroy` here without explicit approval. @@ -77,12 +79,12 @@ them into your shell history or echo them**: # SCW_ACCESS_KEY is a public identifier, exposed as a Terraform output: gh secret set SCW_ACCESS_KEY \ --repo IntegratedDynamic/infrastructure \ - --body "$(terraform -chdir=ci/10-scaleway output -raw access_key)" + --body "$(terraform -chdir=01-iam/bootstrap/scaleway output -raw access_key)" # SCW_SECRET_KEY is sensitive — pipe it from the API key resource without printing: gh secret set SCW_SECRET_KEY \ --repo IntegratedDynamic/infrastructure \ - --body "$(terraform -chdir=ci/10-scaleway state show -no-color scaleway_iam_api_key.github_ci \ + --body "$(terraform -chdir=01-iam/bootstrap/scaleway state show -no-color scaleway_iam_api_key.github_ci \ | awk '/secret_key/ {print $3; exit}' | tr -d '\"')" ``` @@ -94,7 +96,7 @@ from stdin.) The smoke-test workflow (`.github/workflows/scaleway-auth-check.yml`) runs `scw object bucket list region=fr-par` against the key. It triggers on any PR -that touches `ci/10-scaleway/**` or the workflow itself, so you can validate before +that touches `01-iam/bootstrap/scaleway/**` or the workflow itself, so you can validate before merge. It needs two **repo variables** (public identifiers, not secrets — the scw CLI @@ -122,7 +124,7 @@ The API key lives entirely in this root's state. - **On demand** — force it early with: ```bash - terraform -chdir=ci/10-scaleway apply -replace=scaleway_iam_api_key.github_ci + terraform -chdir=01-iam/bootstrap/scaleway apply -replace=scaleway_iam_api_key.github_ci ``` Either way the key material changes, so **re-run the `gh secret set` steps above** diff --git a/01-iam/scaleway/env/01-iam-scaleway.tfvars b/01-iam/bootstrap/scaleway/env/01-iam-scaleway.tfvars similarity index 100% rename from 01-iam/scaleway/env/01-iam-scaleway.tfvars rename to 01-iam/bootstrap/scaleway/env/01-iam-scaleway.tfvars diff --git a/01-iam/scaleway/main.tf b/01-iam/bootstrap/scaleway/main.tf similarity index 100% rename from 01-iam/scaleway/main.tf rename to 01-iam/bootstrap/scaleway/main.tf diff --git a/01-iam/scaleway/outputs.tf b/01-iam/bootstrap/scaleway/outputs.tf similarity index 100% rename from 01-iam/scaleway/outputs.tf rename to 01-iam/bootstrap/scaleway/outputs.tf diff --git a/01-iam/scaleway/variables.tf b/01-iam/bootstrap/scaleway/variables.tf similarity index 100% rename from 01-iam/scaleway/variables.tf rename to 01-iam/bootstrap/scaleway/variables.tf diff --git a/01-iam/scaleway/version.tf b/01-iam/bootstrap/scaleway/version.tf similarity index 100% rename from 01-iam/scaleway/version.tf rename to 01-iam/bootstrap/scaleway/version.tf diff --git a/00-remote_state/1-iam/.terraform.lock.hcl b/01-iam/ci-managed/aws-state-access/.terraform.lock.hcl similarity index 100% rename from 00-remote_state/1-iam/.terraform.lock.hcl rename to 01-iam/ci-managed/aws-state-access/.terraform.lock.hcl diff --git a/00-remote_state/1-iam/env/00-remote-state-iam.tfvars b/01-iam/ci-managed/aws-state-access/env/00-remote-state-iam.tfvars similarity index 100% rename from 00-remote_state/1-iam/env/00-remote-state-iam.tfvars rename to 01-iam/ci-managed/aws-state-access/env/00-remote-state-iam.tfvars diff --git a/00-remote_state/1-iam/main.tf b/01-iam/ci-managed/aws-state-access/main.tf similarity index 100% rename from 00-remote_state/1-iam/main.tf rename to 01-iam/ci-managed/aws-state-access/main.tf diff --git a/00-remote_state/1-iam/outputs.tf b/01-iam/ci-managed/aws-state-access/outputs.tf similarity index 100% rename from 00-remote_state/1-iam/outputs.tf rename to 01-iam/ci-managed/aws-state-access/outputs.tf diff --git a/00-remote_state/1-iam/variables.tf b/01-iam/ci-managed/aws-state-access/variables.tf similarity index 100% rename from 00-remote_state/1-iam/variables.tf rename to 01-iam/ci-managed/aws-state-access/variables.tf diff --git a/00-remote_state/1-iam/version.tf b/01-iam/ci-managed/aws-state-access/version.tf similarity index 100% rename from 00-remote_state/1-iam/version.tf rename to 01-iam/ci-managed/aws-state-access/version.tf diff --git a/CLAUDE.md b/CLAUDE.md index aba490e..4df6e7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,48 +34,68 @@ The `.terraform.lock.hcl` in each root must cover **both** `darwin_arm64` (local `mise run lock` is equivalent to: ```bash -terraform -chdir=state/00-backend providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=state/10-access providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=identity/00-ci-trust providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=ci/10-scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=cluster/local providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=cluster/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=00-remote_state providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/bootstrap/aws providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/bootstrap/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/bootstrap/infisical providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/ci-managed/aws-state-access providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=02-cluster/local providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=02-cluster/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 ``` Commit the updated lock files alongside the version change. ## Architecture -Terraform roots are organized by **domain** (top-level folder = what the root is -responsible for) with a **numeric strata prefix** on each root (apply order / -blast-radius / who applies it: `00` = admin-applied foundation, `10`+ = built on -top). The shared S3 state bucket (`state/00-backend`) holds every root's remote -state. +Terraform roots are organized by **domain** — the top-level folder is a numeric +**pseudo-ID** for what that domain owns (`00-remote_state`, `01-iam`, +`02-cluster`). The number encodes apply order / blast-radius across domains +(`00` first, built on by `01`, then `02`). The shared S3 state bucket +(`00-remote_state`) holds every root's remote state. + +Within a domain, sub-folders split roots by the **second axis: lifecycle / who +applies them** — `bootstrap/` = human/admin-applied trust anchors (rare changes, +need admin creds), `ci-managed/` = roots minted BY the CI (GitOps, capped by the +permissions boundary). A domain with a single root is flattened (the domain +folder *is* the root, e.g. `00-remote_state`). ``` -modules/ # reusable Terraform modules (empty for now) -state/ # domain: Terraform state - 00-backend/ # shared AWS S3 bucket holding every root's remote state (admin-applied) - 10-access/ # org-wide read-only state-access IAM role (created BY CI) -identity/ # domain: CI identity & governance - 00-ci-trust/ # GitHub OIDC provider + role-creator role + permissions boundary + CI grant -ci/ # domain: CI access - 10-scaleway/ # Scaleway IAM identity (static API key) GitHub Actions authenticates with -cluster/ # domain: the Kubernetes platform (de-facto strata 20) - local/ # minikube — local dev and debugging. Local backend (local files) - scaleway/ # Scaleway Kapsule cluster + ArgoCD bootstrap (homelab; WIP) +modules/ # reusable Terraform modules (empty for now) +00-remote_state/ # domain: Terraform state backend — the shared AWS + # S3 bucket holding every root's remote state (admin-applied) +01-iam/ # domain: IAM identities & grants + bootstrap/ # human-applied trust anchors + aws/ # GitHub-OIDC → AWS: OIDC provider + role-creator role + # + permissions boundary + CI grant + scaleway/ # Scaleway CI identity (IAM app + project policy + static API key) + infisical/ # Infisical CI identity (keyless GitHub-OIDC → Infisical) + ci-managed/ # minted BY the CI, capped by the boundary + aws-state-access/ # org-wide tf-state-access role (the first CI-minted role) +02-cluster/ # domain: the Kubernetes platform + local/ # minikube — local dev and debugging. Local backend (local files) + scaleway/ # Scaleway Kapsule cluster + ArgoCD bootstrap (homelab; WIP) ``` -**Backend keys are decoupled from paths.** Each root keeps its original -`workspace_key_prefix` (e.g. `state/10-access` still uses prefix `s3-lister-role`) -so the by-domain restructure was a pure move with no state migration. Don't -"fix" a prefix to match its path unless you also migrate the state. +**The dependency spine** runs strictly forward: `00-remote_state` (bucket) → +`01-iam/bootstrap/aws` (the trust anchor that lets CI apply anything) → +`01-iam/ci-managed/*` (roles the anchor mints) → `02-cluster/*`. `bootstrap/aws` +is the root of trust — nothing CI-applied can exist before it. + +**Backend keys are decoupled from paths.** Each root pins its own +`workspace_key_prefix` in `version.tf` (e.g. `01-iam/ci-managed/aws-state-access` +still uses prefix `s3-lister-role`), and the workspace name comes from the +`env/.tfvars` filename — **neither is tied to the directory**. The +restructure was a pure `git mv` with no state migration. Don't "fix" a prefix or +rename a tfvars file to match its new path unless you also migrate the state +(renaming the tfvars file changes the workspace, hence the state key). This is +why some workspace names look dated (e.g. `aws-state-access` still uses the +`00-remote-state-iam` workspace). -### `cluster/*` +### `02-cluster/*` Terraform here is only a **one-time bootstrapper** — everything after ArgoCD is up lives in the `gitops` repo. The cluster internal state nor status will be reflected in the terraform state. -### `cluster/local/` +### `02-cluster/local/` Warning : This environment expect you an accessible local kubernetes cluster access, likely configured within your ~/.kube/config. This is automatically handled via `mise run dev` @@ -84,27 +104,31 @@ Two-step, one-time bootstrap: 2. Deploy **ArgoCD** via Helm with the admin bcrypt password hash from Infisical (pre-hashed to prevent Terraform drift). 3. Deploy the **argocd-apps bootstrap** Application, pointing ArgoCD at `https://github.com/IntegratedDynamic/gitops.git`. ArgoCD then self-manages all further cluster state from that separate GitOps repo. -### `cluster/scaleway/` +### `02-cluster/scaleway/` Same bootstrap pattern as `local/`, but with the Kapsule cluster + node pool (`DEV1-M`, min=0/max=3) instead. -### `state/00-backend/` +### `00-remote_state/` The shared org S3 bucket holding **every** root's remote state (built on `terraform-aws-modules/s3-bucket`: versioning, SSE, public-access block, TLS-only). Chicken-and-egg: its own state lives in the bucket it creates (one-time local-state bootstrap — see its README). Applied by an admin. -### `state/10-access/` +### `01-iam/ci-managed/aws-state-access/` + +Org-wide Terraform-state **access** IAM **role created BY the CI** (the first role minted by `01-iam/bootstrap/aws`'s role-creator rather than by a human). Named `tf-state-access`, it grants `AmazonS3FullAccess` — **read/write on the state bucket plus the state lock** — so every state-touching workflow assumes it for `plan` AND `apply`/`destroy` alike (e.g. the `scaleway` workflow), wired via `vars.AWS_TF_STATE_ROLE_ARN`. Assumable org-wide via two trust doors: AWS principals in the org (`aws:PrincipalOrgID`) and GitHub Actions in the org via OIDC (`repo:IntegratedDynamic/*`). Applied by CI (`iam_terraform-backend-role.yml`). + +### `01-iam/bootstrap/scaleway/` -Org-wide Terraform-state **access** IAM **role created BY the CI** (the first role minted by `identity/00-ci-trust`'s role-creator rather than by a human). Named `tf-state-access`, it grants `AmazonS3FullAccess` — **read/write on the state bucket plus the state lock** — so every state-touching workflow assumes it for `plan` AND `apply`/`destroy` alike (e.g. the `scaleway` workflow), wired via `vars.AWS_TF_STATE_ROLE_ARN`. Assumable org-wide via two trust doors: AWS principals in the org (`aws:PrincipalOrgID`) and GitHub Actions in the org via OIDC (`repo:IntegratedDynamic/*`). Applied by CI (`iam_terraform-backend-role.yml`). +Standalone root that stands up the **Scaleway IAM identity GitHub Actions uses to authenticate to Scaleway**: a dedicated IAM application + a project-scoped policy (`Kubernetes`/`VPC`/`PrivateNetworks` FullAccess + `IPAMReadOnly`, enough for CI to create/destroy the Kapsule cluster) + an API key, with the key written into Infisical. GitHub secrets (`SCW_ACCESS_KEY` / `SCW_SECRET_KEY`) are still set manually via `gh secret set`. Keyless GitHub-OIDC → Scaleway is a non-goal — blocked upstream (Scaleway IAM is not an OIDC relying party). See `01-iam/bootstrap/scaleway/README.md`. -### `ci/10-scaleway/` +### `01-iam/bootstrap/infisical/` -Standalone root that stands up the **Scaleway IAM identity GitHub Actions uses to authenticate to Scaleway**: a dedicated IAM application + a project-scoped policy (`Kubernetes`/`VPC`/`PrivateNetworks` FullAccess + `IPAMReadOnly`, enough for CI to create/destroy the Kapsule cluster) + an API key, with the key written into Infisical. GitHub secrets (`SCW_ACCESS_KEY` / `SCW_SECRET_KEY`) are still set manually via `gh secret set`. Keyless GitHub-OIDC → Scaleway is a non-goal — blocked upstream (Scaleway IAM is not an OIDC relying party). See `ci/10-scaleway/README.md`. +The **keyless GitHub-OIDC → Infisical** counterpart to `bootstrap/scaleway`: a Infisical machine identity + OIDC auth trusting GitHub Actions tokens, so the composite action can mint a short-lived Infisical token (no static Infisical secret) to read the secrets cluster bootstraps need. See `01-iam/bootstrap/infisical/README.md`. -### `identity/00-ci-trust/` +### `01-iam/bootstrap/aws/` -The CI **identity & governance foundation**: **keyless GitHub-OIDC → AWS** access, built on the `terraform-aws-modules/iam` modules. An OIDC provider + a role (`github-actions-terraform`) GitHub Actions assumes via short-lived tokens (trust scoped to `repo:IntegratedDynamic/infrastructure:*`). The role grant (`tf-managed-ci`) gives Terraform-state R/W on the state bucket **plus privilege-escalation-safe IAM role management** — i.e. it is the role that **creates other CI roles** (e.g. `state/10-access`). Applied locally by an admin; `role_arn` is wired to CI via `vars.AWS_GITHUB_ACTIONS_ROLE_ARN`. See `identity/00-ci-trust/README.md`. +The CI **identity & governance foundation**: **keyless GitHub-OIDC → AWS** access, built on the `terraform-aws-modules/iam` modules. An OIDC provider + a role (`github-actions-terraform`) GitHub Actions assumes via short-lived tokens (trust scoped to `repo:IntegratedDynamic/infrastructure:*`). The role grant (`tf-managed-ci`) gives Terraform-state R/W on the state bucket **plus privilege-escalation-safe IAM role management** — i.e. it is the role that **creates other CI roles** (e.g. `01-iam/ci-managed/aws-state-access`). Applied locally by an admin; `role_arn` is wired to CI via `vars.AWS_GITHUB_ACTIONS_ROLE_ARN`. See `01-iam/bootstrap/aws/README.md`. -**Permissions-boundary contract (repo-wide):** any `aws_iam_role` that the CI applies **must** set `permissions_boundary` (= the `permissions_boundary_arn` output, `tf-managed-boundary`) and `path` (= the `managed_path` output, `/tf-managed///`), or the apply is rejected by the CI grant's conditions. Set both via the root's `env/.tfvars` (see the Terraform workspaces convention below). The boundary caps every CI-created role to "admin minus a hardened deny-list" so a role-creating role can't escalate. Rationale is documented inline in `identity/00-ci-trust/iam-ci.tf`. +**Permissions-boundary contract (repo-wide):** any `aws_iam_role` that the CI applies **must** set `permissions_boundary` (= the `permissions_boundary_arn` output, `tf-managed-boundary`) and `path` (= the `managed_path` output, `/tf-managed///`), or the apply is rejected by the CI grant's conditions. Set both via the root's `env/.tfvars` (see the Terraform workspaces convention below). The boundary caps every CI-created role to "admin minus a hardened deny-list" so a role-creating role can't escalate. Rationale is documented inline in `01-iam/bootstrap/aws/iam-ci.tf`. ## Conventions diff --git a/README.md b/README.md index 4d00bd4..20da191 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This infrastructure contains the actual ScalePack infrastructure. -All remote based environment states are stored in the S3 bucket provisioned by `./state/00-backend`. Check `version.tf` files from any root module in this repository for an example, stored under eu-west-3 region. +All remote based environment states are stored in the S3 bucket provisioned by `./00-remote_state`. Check `version.tf` files from any root module in this repository for an example, stored under eu-west-3 region. ## Forking diff --git a/mise.toml b/mise.toml index 39b0375..046867d 100644 --- a/mise.toml +++ b/mise.toml @@ -26,13 +26,13 @@ kubectl wait --for=condition=Ready nodes --all --timeout=120s """ [tasks.local-init] -dir = "02_cluster/local" +dir = "02-cluster/local" run = """ terraform init """ [tasks.local-apply] -dir = "02_cluster/local" +dir = "02-cluster/local" run = """ ws=$(terraform workspace show) if [ "$ws" = "default" ]; then @@ -60,11 +60,11 @@ run = "minikube delete" [tasks.lock] description = "Re-generate all .terraform.lock.hcl files for darwin_arm64 + linux_amd64" run = """ -terraform -chdir=00-remote_state/0-backend providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=00-remote_state/1-iam providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=01-iam/aws providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=01-iam/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=01-iam/infisical providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=02-cluster/local providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=02-cluster/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=00-remote_state providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/bootstrap/aws providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/bootstrap/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/bootstrap/infisical providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=01-iam/ci-managed/aws-state-access providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=02-cluster/local providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=02-cluster/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 """ diff --git a/modules/README.md b/modules/README.md index 44f177f..9ce6302 100644 --- a/modules/README.md +++ b/modules/README.md @@ -4,10 +4,9 @@ Reusable Terraform modules shared across the roots in this repo. Empty for now — the roots currently consume community modules (`terraform-aws-modules/*`) directly. When a pattern is duplicated across roots -(e.g. a Scaleway CI IAM identity used by both `ci/10-scaleway/` and a future -`ci/11-scaleway-tf/`), factor it out into a module here and reference it with a -relative `source = "../../modules/"`. +(e.g. an IAM identity used by more than one root), factor it out into a module +here and reference it with a relative `source = "../../modules/"`. -Roots (the deployable configs) live under the domain folders `state/`, -`identity/`, `ci/`, and `cluster/`; this folder holds only non-deployable, -reusable building blocks. +Roots (the deployable configs) live under the numeric domain folders +`00-remote_state/`, `01-iam/`, and `02-cluster/`; this folder holds only +non-deployable, reusable building blocks.