diff --git a/.github/workflows/aws-auth-check.yml b/.github/workflows/aws-auth-check.yml deleted file mode 100644 index 49f4b72..0000000 --- a/.github/workflows/aws-auth-check.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: AWS Auth Check - -# Validates the aws-github-oidc/ IAM role end to end: assume it via GitHub OIDC -# (no static AWS keys) and list S3 buckets. This is a smoke test, not part of any -# pipeline yet. -# -# Runs on PRs that touch the AWS OIDC identity so it can be validated before merge. - -on: - pull_request: - paths: - - '.github/workflows/aws-auth-check.yml' - - 'aws-github-oidc/**' - -permissions: - contents: read - -jobs: - list-buckets: - runs-on: ubuntu-latest - permissions: - id-token: write # mint the OIDC token exchanged for temporary AWS creds - contents: read - steps: - # Fail fast and clearly if the role ARN variable isn't set, instead of - # letting configure-aws-credentials emit a confusing assume-role error. - - name: Assert role ARN is configured - env: - 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. Apply aws-github-oidc/ and set it (see aws-github-oidc/README.md)." - exit 1 - fi - - - name: Configure AWS credentials via OIDC - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 - with: - role-to-assume: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} - aws-region: eu-west-3 - - # List the state bucket specifically (s3:ListBucket on that bucket) rather - # than `aws s3 ls` with no arg, which needs account-wide s3:ListAllMyBuckets - # — the CI grant is intentionally scoped to the state bucket only. - - name: List the Terraform state bucket - run: aws s3 ls s3://id-terraform-state20260612164136440800000001 diff --git a/.github/workflows/iam_terraform-backend-role.yml b/.github/workflows/iam_terraform-backend-role.yml new file mode 100644 index 0000000..7681912 --- /dev/null +++ b/.github/workflows/iam_terraform-backend-role.yml @@ -0,0 +1,75 @@ +name: Apply Terraform Backend Role + +# Terraform GitOps for the 00-remote_state/1-iam/ root: the org-wide S3 state +# access role is created/synchronised from remote. 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. + +on: + push: + branches: [main] + paths: + - '00-remote_state/1-iam/**' + - '.github/workflows/iam_terraform-backend-role.yml' + pull_request: + paths: + - '00-remote_state/1-iam/**' + - '.github/workflows/iam_terraform-backend-role.yml' + +permissions: + contents: read + +jobs: + terraform: + runs-on: ubuntu-latest + permissions: + id-token: write # mint the OIDC token exchanged for temporary AWS creds + contents: read + steps: + # Fail fast and clearly if creds aren't set, instead of letting the + # tooling emit a confusing assume-role error. + - name: Assert credentials are present + env: + 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. Set it to the github-actions-terraform role ARN (provisioned by identity/00-ci-trust/)." + exit 1 + fi + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + # Keep in sync with mise.toml [tools] terraform version. + terraform_version: "1.14" + + - name: Assume the CI role via OIDC + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + with: + role-to-assume: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} + aws-region: eu-west-3 + + - name: Terraform plan + if: github.event_name == 'pull_request' + env: + TF_CHDIR: "00-remote_state/1-iam" + TF_ENVIRONMENT: "00-remote-state-iam.tfvars" + run: | + set -euo pipefail + terraform -chdir="${TF_CHDIR}" init -input=false + terraform -chdir="${TF_CHDIR}" workspace select "${TF_ENVIRONMENT%.*}" + terraform -chdir="${TF_CHDIR}" plan -input=false -var-file="env/${TF_ENVIRONMENT}" + + - name: Terraform apply + if: github.event_name == 'push' + env: + TF_CHDIR: "00-remote_state/1-iam" + TF_ENVIRONMENT: "00-remote-state-iam.tfvars" + run: | + set -euo pipefail + terraform -chdir="${TF_CHDIR}" init -input=false + terraform -chdir="${TF_CHDIR}" workspace select "${TF_ENVIRONMENT%.*}" + terraform -chdir="${TF_CHDIR}" apply -auto-approve -input=false -var-file="env/${TF_ENVIRONMENT}" diff --git a/.github/workflows/s3-lister-role.yml b/.github/workflows/s3-lister-role.yml deleted file mode 100644 index d057c80..0000000 --- a/.github/workflows/s3-lister-role.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Apply s3-lister-role - -# Terraform GitOps for the s3-lister-role/ root: the role is created/synchronised -# from the remote. Runs `apply` on every trigger (PRs included) so the CI role -# actually creates the role via OIDC. Credentials come from the aws-github-oidc -# CI role assumed via OIDC — no static AWS keys. - -on: - push: - branches: [main] - paths: - - 's3-lister-role/**' - - '.github/workflows/s3-lister-role.yml' - pull_request: - paths: - - 's3-lister-role/**' - - '.github/workflows/s3-lister-role.yml' - -permissions: - contents: read - -jobs: - terraform: - runs-on: ubuntu-latest - permissions: - id-token: write # mint the OIDC token exchanged for temporary AWS creds - contents: read - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 - with: - # Keep in sync with mise.toml [tools] terraform version. - terraform_version: "1.14" - - - name: Configure AWS credentials via OIDC - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 - with: - role-to-assume: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} - aws-region: eu-west-3 - - # Workspace + variables are derived from s3-lister-role/workspace/*.tfvars. - # apply on every run (PRs included) — the point of this PR is to watch the - # CI role actually create the role via OIDC, not just plan it. - - uses: ./.github/actions/terraform - with: - root: s3-lister-role - apply: "true" diff --git a/.github/workflows/scaleway-auth-check.yml b/.github/workflows/scaleway-auth-check.yml deleted file mode 100644 index b6b0d1e..0000000 --- a/.github/workflows/scaleway-auth-check.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Scaleway Auth Check - -# Validates the github-ci/ Scaleway IAM key end to end: authenticate with the -# scoped API key from GitHub secrets and list the fr-par Object Storage buckets. -# This is a smoke test, not part of any pipeline yet. -# -# Runs on PRs that touch the CI identity so it can be validated before merge. - -on: - pull_request: - paths: - - '.github/workflows/scaleway-auth-check.yml' - - 'github-ci/**' - -permissions: - contents: read - -jobs: - list-buckets: - runs-on: ubuntu-latest - # Reference a dedicated environment so secret usage is scoped to it - # (satisfies zizmor's secrets-outside-env audit, and gives a place to attach - # protection rules later). Auto-created on first run with no protection. - environment: scaleway - steps: - # Fail fast and clearly if the secrets aren't set, instead of letting the - # CLI emit a confusing auth error (or worse, appear to pass). - - name: Assert credentials are present - env: - SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }} - SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }} - run: | - if [ -z "$SCW_ACCESS_KEY" ] || [ -z "$SCW_SECRET_KEY" ]; then - echo "::error::SCW_ACCESS_KEY and/or SCW_SECRET_KEY are not set. Run 'gh secret set' (see github-ci/README.md)." - exit 1 - fi - - - name: List fr-par Object Storage buckets - uses: scaleway/action-scw@2e34a1eb35cf3cac627f24643a101fea269cbd83 # v0.0.3 - with: - version: v2.41.0 - access-key: ${{ secrets.SCW_ACCESS_KEY }} - secret-key: ${{ secrets.SCW_SECRET_KEY }} - # Public identifiers (not credentials) the scw CLI wants even for a - # project-scoped key. Repo variables, not secrets. - default-organization-id: ${{ vars.SCW_DEFAULT_ORGANIZATION_ID }} - default-project-id: ${{ vars.SCW_DEFAULT_PROJECT_ID }} - args: object bucket list region=fr-par diff --git a/.github/workflows/scaleway-plan.yml b/.github/workflows/scaleway-plan.yml index d6ade5b..daec1e9 100644 --- a/.github/workflows/scaleway-plan.yml +++ b/.github/workflows/scaleway-plan.yml @@ -18,13 +18,14 @@ name: Scaleway Plan (via s3-lister role) on: pull_request: paths: - - 'cluster/scaleway/**' - - 's3-lister-role/**' + - '02-cluster/scaleway/**' + - '01-iam/aws/**' - '.github/workflows/scaleway-plan.yml' workflow_dispatch: permissions: contents: read + id-token: write # This is required for requesting the JWT jobs: plan: @@ -36,10 +37,8 @@ jobs: id-token: write # mint the OIDC token exchanged for temporary AWS creds contents: read env: - # Select the existing remote workspace without `workspace new` (a write the - # read-only role can't perform). The default workspace would collide with - # the state bucket's own state at the bucket root. - TF_WORKSPACE: infrastructure + INFISICAL_MACHINE_IDENTITY_ID: ${{ vars.INFISICAL_MACHINE_IDENTITY_ID }} + # Scaleway provider creds for the refresh. Keys are secrets; the public # org/project IDs are repo variables (not secrets). SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }} @@ -48,10 +47,10 @@ jobs: SCW_DEFAULT_PROJECT_ID: ${{ vars.SCW_DEFAULT_PROJECT_ID }} # The infisical provider authenticates eagerly even with bootstrap_argocd # off (the gated data source still pulls the provider into the graph), so - # it needs valid machine-identity creds just to configure. With - # bootstrap_argocd=false no secret is actually read. - TF_VAR_infisical_client_id: ${{ secrets.INFISICAL_CLIENT_ID }} - TF_VAR_infisical_client_secret: ${{ secrets.INFISICAL_CLIENT_SECRET }} + # it needs to configure successfully — here via keyless GitHub-OIDC (see + # the "Mint Infisical OIDC token" step). With bootstrap_argocd=false no + # secret is actually read. + steps: # Fail fast and clearly if creds/vars aren't set, instead of letting the # tooling emit a confusing assume-role or provider auth error. @@ -60,11 +59,11 @@ jobs: ROLE_ARN: ${{ vars.AWS_S3_LISTER_ROLE_ARN }} run: | if [ -z "$ROLE_ARN" ]; then - echo "::error::vars.AWS_S3_LISTER_ROLE_ARN is not set. Set it to the s3-lister role ARN (see s3-lister-role/README.md)." + echo "::error::vars.AWS_S3_LISTER_ROLE_ARN is not set. Set it to the s3-lister role ARN (provisioned by state/10-access)." exit 1 fi if [ -z "$SCW_ACCESS_KEY" ] || [ -z "$SCW_SECRET_KEY" ]; then - echo "::error::SCW_ACCESS_KEY and/or SCW_SECRET_KEY are not set (see github-ci/README.md)." + echo "::error::SCW_ACCESS_KEY and/or SCW_SECRET_KEY are not set (see ci/10-scaleway/README.md)." exit 1 fi @@ -85,8 +84,25 @@ jobs: role-to-assume: ${{ vars.AWS_S3_LISTER_ROLE_ARN }} aws-region: eu-west-3 + # Mint a GitHub OIDC JWT for the Infisical machine identity and stash it in + # $INFISICAL_AUTH_JWT (the provider's default token env var). audience must + # match the identity's boundAudiences. This is the keyless counterpart to + # the universal-auth client_id/secret used locally — no static Infisical + # secret in CI. core.getIDToken is the official @actions/core way to mint + # the token (it masks the value automatically). + - name: Mint Infisical OIDC token + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const jwt = await core.getIDToken('https://github.com/IntegratedDynamic') + core.exportVariable('INFISICAL_AUTH_JWT', jwt) + - name: Terraform plan (read-only role -> no lock, no apply) + env: + TF_CHDIR: "02-cluster/scaleway/" + TF_ENVIRONMENT: "02-cluster-staging.tfvars" run: | - terraform -chdir=cluster/scaleway init -input=false - terraform -chdir=cluster/scaleway plan \ - -lock=false -input=false -var bootstrap_argocd=false + set -euo pipefail + terraform -chdir="${TF_CHDIR}" init -input=false + terraform -chdir="${TF_CHDIR}" workspace select "${TF_ENVIRONMENT%.*}" + terraform -chdir="${TF_CHDIR}" destroy -auto-approve -input=false -var-file="env/${TF_ENVIRONMENT}" diff --git a/.github/workflows/terraform-lock.yml b/.github/workflows/terraform-lock.yml index 435069e..0eaee50 100644 --- a/.github/workflows/terraform-lock.yml +++ b/.github/workflows/terraform-lock.yml @@ -23,12 +23,11 @@ jobs: fail-fast: false matrix: root: - - terraform-state-bucket - - cluster/local - - cluster/scaleway - - github-ci - - aws-github-oidc - - s3-lister-role + - 00-remote_state/0-backend + - 00-remote_state/1-iam + - 01-iam/aws + - 01-iam/scaleway + - 02-cluster/scaleway steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/terraform-state-bucket/.terraform.lock.hcl b/00-remote_state/0-backend/.terraform.lock.hcl similarity index 100% rename from terraform-state-bucket/.terraform.lock.hcl rename to 00-remote_state/0-backend/.terraform.lock.hcl diff --git a/terraform-state-bucket/README.md b/00-remote_state/0-backend/README.md similarity index 88% rename from terraform-state-bucket/README.md rename to 00-remote_state/0-backend/README.md index 5ca8999..de13e06 100644 --- a/terraform-state-bucket/README.md +++ b/00-remote_state/0-backend/README.md @@ -1,13 +1,12 @@ -# terraform-state-bucket +# state/00-backend — 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/`, `github-ci/`, and future ones) points +(`cluster/local/`, `cluster/scaleway/`, `ci/10-scaleway/`, and future ones) points its `backend "s3"` at this bucket. -This is intentionally **not** under `cluster/` because it provisions no cluster; -it is the shared substrate the other roots depend on. It lives at the repo root -as its own root module. +It is the shared substrate every other root depends on — strata `00` of the +`state/` domain, applied by an admin. ## What it creates @@ -58,13 +57,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=terraform-state-bucket init - terraform -chdir=terraform-state-bucket apply # creates the bucket (billable) + terraform -chdir=state/00-backend init + terraform -chdir=state/00-backend 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=terraform-state-bucket init -migrate-state + terraform -chdir=state/00-backend 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/0-backend/env/00-remote-state-backend.tfvars new file mode 100644 index 0000000..b400580 --- /dev/null +++ b/00-remote_state/0-backend/env/00-remote-state-backend.tfvars @@ -0,0 +1,3 @@ +bucket_prefix = "id-terraform-state" +region = "eu-west-3" +noncurrent_version_expiration_days = 10 diff --git a/terraform-state-bucket/main.tf b/00-remote_state/0-backend/main.tf similarity index 100% rename from terraform-state-bucket/main.tf rename to 00-remote_state/0-backend/main.tf diff --git a/terraform-state-bucket/outputs.tf b/00-remote_state/0-backend/outputs.tf similarity index 100% rename from terraform-state-bucket/outputs.tf rename to 00-remote_state/0-backend/outputs.tf diff --git a/terraform-state-bucket/variables.tf b/00-remote_state/0-backend/variables.tf similarity index 100% rename from terraform-state-bucket/variables.tf rename to 00-remote_state/0-backend/variables.tf diff --git a/terraform-state-bucket/version.tf b/00-remote_state/0-backend/version.tf similarity index 70% rename from terraform-state-bucket/version.tf rename to 00-remote_state/0-backend/version.tf index d40a006..c54997f 100644 --- a/terraform-state-bucket/version.tf +++ b/00-remote_state/0-backend/version.tf @@ -4,10 +4,13 @@ terraform { backend "s3" { bucket = "id-terraform-state20260612164136440800000001" region = "eu-west-3" + # Prefix kept as "state-backend" (≠ this root's path state/00-backend/) on + # purpose: the state key is decoupled from the directory, so the repo + # restructure was a pure move with zero state migration. workspace_key_prefix = "state-backend" - key = "terraform.tfstate" - encrypt = true - use_lockfile = true + key = "terraform.tfstate" + encrypt = true + use_lockfile = true } required_providers { diff --git a/s3-lister-role/.terraform.lock.hcl b/00-remote_state/1-iam/.terraform.lock.hcl similarity index 100% rename from s3-lister-role/.terraform.lock.hcl rename to 00-remote_state/1-iam/.terraform.lock.hcl diff --git a/s3-lister-role/workspace/infrastructure.tfvars b/00-remote_state/1-iam/env/00-remote-state-iam.tfvars similarity index 90% rename from s3-lister-role/workspace/infrastructure.tfvars rename to 00-remote_state/1-iam/env/00-remote-state-iam.tfvars index 139f287..d3d9450 100644 --- a/s3-lister-role/workspace/infrastructure.tfvars +++ b/00-remote_state/1-iam/env/00-remote-state-iam.tfvars @@ -8,7 +8,7 @@ region = "eu-west-3" role_name = "s3-lister" -# Repo-scoped path + boundary required by the CI grant (aws-github-oidc/). Must +# Repo-scoped path + boundary required by the CI grant (identity/00-ci-trust/). Must # match the grant's pins exactly or the apply is denied. role_path = "/tf-managed/IntegratedDynamic/infrastructure/" permissions_boundary_arn = "arn:aws:iam::503577850357:policy/tf-managed-boundary" diff --git a/s3-lister-role/main.tf b/00-remote_state/1-iam/main.tf similarity index 83% rename from s3-lister-role/main.tf rename to 00-remote_state/1-iam/main.tf index d63fade..51041b2 100644 --- a/s3-lister-role/main.tf +++ b/00-remote_state/1-iam/main.tf @@ -1,10 +1,10 @@ # An org-wide, read-only S3-lister IAM role — the first role created BY the CI # rather than by a human. It is created/updated when a push to main touches this # root (see .github/workflows/s3-lister-role.yml): GitHub Actions assumes the -# aws-github-oidc CI role and runs `terraform apply`. +# identity/00-ci-trust CI role and runs `terraform apply`. # # Two things are mandatory for the CI role to be ALLOWED to create this (see -# aws-github-oidc/iam-ci.tf): the role must sit under the managed `path` and +# identity/00-ci-trust/iam-ci.tf): the role must sit under the managed `path` and # carry the `permissions_boundary`. Omit either and the apply is denied. # # Trust: two doors, both org-scoped, that the module ORs together. @@ -27,7 +27,7 @@ module "s3_lister" { name = var.role_name use_name_prefix = false path = var.role_path - description = "Org-wide read-only S3 lister. Created by CI; capped by the tf-managed permissions boundary." + description = "Org-wide Terraform-state S3 access (read/write + lock). Created by CI; capped by the tf-managed permissions boundary." permissions_boundary = var.permissions_boundary_arn @@ -51,11 +51,9 @@ module "s3_lister" { }] } } - - # Same access the original GitHub Action had: list/read S3. The boundary clamps - # it anyway, but S3 read is well within the ceiling. + policies = { - S3ReadOnly = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" + S3FullAccess = "arn:aws:iam::aws:policy/AmazonS3FullAccess" } tags = { diff --git a/s3-lister-role/outputs.tf b/00-remote_state/1-iam/outputs.tf similarity index 100% rename from s3-lister-role/outputs.tf rename to 00-remote_state/1-iam/outputs.tf diff --git a/s3-lister-role/variables.tf b/00-remote_state/1-iam/variables.tf similarity index 87% rename from s3-lister-role/variables.tf rename to 00-remote_state/1-iam/variables.tf index d23ca96..23b0668 100644 --- a/s3-lister-role/variables.tf +++ b/00-remote_state/1-iam/variables.tf @@ -10,7 +10,7 @@ variable "role_name" { default = "s3-lister" } -# IAM path every CI-managed role must sit under. The CI grant (aws-github-oidc/) +# IAM path every CI-managed role must sit under. The CI grant (identity/00-ci-trust/) # only allows role creation under this path WITH the boundary below — so both # must be set or the apply is rejected. In CI this is fed automatically from the # repo slug: TF_VAR_role_path=/tf-managed/${{ github.repository }}/ (see the @@ -22,10 +22,10 @@ variable "role_path" { } # The permissions boundary that caps this role. Value is the -# `permissions_boundary_arn` output of the aws-github-oidc/ root. Required by the -# CI grant's conditions. +# `permissions_boundary_arn` output of the identity/00-ci-trust/ root. Required by +# the CI grant's conditions. variable "permissions_boundary_arn" { - description = "ARN of the permissions boundary to attach (aws-github-oidc output)." + description = "ARN of the permissions boundary to attach (identity/00-ci-trust output)." type = string default = "arn:aws:iam::503577850357:policy/tf-managed-boundary" } diff --git a/s3-lister-role/version.tf b/00-remote_state/1-iam/version.tf similarity index 56% rename from s3-lister-role/version.tf rename to 00-remote_state/1-iam/version.tf index 7cd85b6..bf41b20 100644 --- a/s3-lister-role/version.tf +++ b/00-remote_state/1-iam/version.tf @@ -1,10 +1,13 @@ terraform { # Remote state on the shared org S3 bucket, under this root's own key prefix. - # In CI the GitHub OIDC role (aws-github-oidc/) provides the credentials; it is + # In CI the GitHub OIDC role (identity/00-ci-trust/) provides the credentials; it is # granted R/W on this bucket, so `init`/`plan`/`apply` work without static keys. backend "s3" { - bucket = "id-terraform-state20260612164136440800000001" - region = "eu-west-3" + bucket = "id-terraform-state20260612164136440800000001" + region = "eu-west-3" + # Prefix kept as "s3-lister-role" (≠ this root's path state/10-access/) on + # purpose: the state key is decoupled from the directory, so the repo + # restructure was a pure move with zero state migration. workspace_key_prefix = "s3-lister-role" key = "terraform.tfstate" encrypt = true diff --git a/aws-github-oidc/.terraform.lock.hcl b/01-iam/aws/.terraform.lock.hcl similarity index 100% rename from aws-github-oidc/.terraform.lock.hcl rename to 01-iam/aws/.terraform.lock.hcl diff --git a/aws-github-oidc/README.md b/01-iam/aws/README.md similarity index 91% rename from aws-github-oidc/README.md rename to 01-iam/aws/README.md index 018e42c..ec4d196 100644 --- a/aws-github-oidc/README.md +++ b/01-iam/aws/README.md @@ -1,4 +1,4 @@ -# aws-github-oidc +# identity/00-ci-trust — 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 @@ -18,8 +18,8 @@ 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 `terraform-ci/` / -`github-ci/`). OIDC here covers **only** the AWS/S3 side. Revisit Scaleway OIDC +so those keep using a scoped, static Scaleway API key (see `ci/10-scaleway/`). +OIDC here covers **only** the AWS/S3 side. Revisit Scaleway OIDC if/when Scaleway ships it. All resources are built from the [`terraform-aws-modules/iam`](https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest) @@ -79,8 +79,8 @@ inline in [`iam-ci.tf`](./iam-ci.tf)): Verify the guardrails with the IAM policy simulator, e.g.: ```bash -ROLE=$(terraform -chdir=aws-github-oidc output -raw role_arn) -B=$(terraform -chdir=aws-github-oidc output -raw permissions_boundary_arn) +ROLE=$(terraform -chdir=identity/00-ci-trust output -raw role_arn) +B=$(terraform -chdir=identity/00-ci-trust 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 +99,8 @@ aws iam simulate-principal-policy --policy-source-arn "$ROLE" \ ## Apply ```bash -mise run aws-github-oidc-plan # terraform init && plan — review first -mise run aws-github-oidc-apply # terraform apply (creates IAM resources) +mise run ci-trust-plan # terraform init && plan — review first +mise run ci-trust-apply # terraform apply (creates IAM resources) ``` > Never `terraform apply`/`destroy` here without explicit approval. @@ -113,7 +113,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=aws-github-oidc output -raw role_arn)" + --body "$(terraform -chdir=identity/00-ci-trust output -raw role_arn)" ``` Workflows then assume the role with `aws-actions/configure-aws-credentials`: diff --git a/aws-github-oidc/iam-ci.tf b/01-iam/aws/iam-ci.tf similarity index 100% rename from aws-github-oidc/iam-ci.tf rename to 01-iam/aws/iam-ci.tf diff --git a/aws-github-oidc/main.tf b/01-iam/aws/main.tf similarity index 96% rename from aws-github-oidc/main.tf rename to 01-iam/aws/main.tf index bedc664..feab64a 100644 --- a/aws-github-oidc/main.tf +++ b/01-iam/aws/main.tf @@ -4,7 +4,7 @@ # GitHub secrets for the S3 backend. # # This covers ONLY the AWS/S3 side. Scaleway resources (Kapsule, VPC) still use a -# static Scaleway API key (github-ci/), because Scaleway IAM is not an OIDC +# static Scaleway API key (ci/10-scaleway/), because Scaleway IAM is not an OIDC # relying party — see README.md. # OIDC identity provider for GitHub Actions. One per AWS account; this is the diff --git a/aws-github-oidc/outputs.tf b/01-iam/aws/outputs.tf similarity index 100% rename from aws-github-oidc/outputs.tf rename to 01-iam/aws/outputs.tf diff --git a/aws-github-oidc/variables.tf b/01-iam/aws/variables.tf similarity index 100% rename from aws-github-oidc/variables.tf rename to 01-iam/aws/variables.tf diff --git a/aws-github-oidc/version.tf b/01-iam/aws/version.tf similarity index 73% rename from aws-github-oidc/version.tf rename to 01-iam/aws/version.tf index f60c11b..8434a9b 100644 --- a/aws-github-oidc/version.tf +++ b/01-iam/aws/version.tf @@ -2,8 +2,11 @@ terraform { # Remote state on the shared org S3 bucket (same bucket every other root uses), # under this root's own key so its state/blast-radius stay isolated. backend "s3" { - bucket = "id-terraform-state20260612164136440800000001" - region = "eu-west-3" + bucket = "id-terraform-state20260612164136440800000001" + region = "eu-west-3" + # Prefix kept as "aws-github-oidc" (≠ this root's path identity/00-ci-trust/) + # on purpose: the state key is decoupled from the directory, so the repo + # restructure was a pure move with zero state migration. workspace_key_prefix = "aws-github-oidc" key = "terraform.tfstate" encrypt = true diff --git a/01-iam/infisical/.terraform.lock.hcl b/01-iam/infisical/.terraform.lock.hcl new file mode 100644 index 0000000..38031e0 --- /dev/null +++ b/01-iam/infisical/.terraform.lock.hcl @@ -0,0 +1,26 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/infisical/infisical" { + version = "0.16.30" + constraints = "~> 0.16" + hashes = [ + "h1:6XTllezN1ZbgFyxQfLumAwonXMyQl4KdgQLQx+vdWOc=", + "h1:MxCBGCIQUhX/FTSuJsxAwFR5ybakkSxORv259Fbl5Wc=", + "zh:07924c8210300cf3e58c044ca820188644c648c9777d0f3e5ebdf87ae3529aa1", + "zh:0d728a71c0b47358815b5cf13e6070f512ebda61268c6d6e89906d6c169cab53", + "zh:1f8a327f819382d2d0d59f6d4c24fb1a87038efab9f62934519ac3474633b65a", + "zh:397e937a685a68a72779d1ba123f5b291bea5aa61a0fe60adbf56ce3ef4c387c", + "zh:405ee9b531ef8b3402fea060970bea1c31c274d1ff0e1da075721466ca0999fd", + "zh:4ca7f0182b0f7ac08f91803bdb7dd3996e98c2e46a1f14caef8c02c4f54fdf66", + "zh:6c3ed4b716899ca321fdf0a687df24519f5872343cb651af234c2ccfc0a26be8", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:a04c0a7a3f2daf2e940d3c53f15fc5d923ae9a9e916d0cdfb30c7c4e810d1df0", + "zh:aed0de407ef288a606552c719f517296292c9044d8e314c5b162cb31baf744d9", + "zh:b559fbb532566b9baa3ac06c7061405d274e6d4708cf18180d0b66cee0e79179", + "zh:cd284e2b0e037d124d25ed2f4a5d57279fa39864314a35b06e36888ef78d5511", + "zh:e3b549abcbb2f6d5a03f8244826facce647a67d8ce0cb6a65dd19f7b6cf2a227", + "zh:e9258e0fb4ae3ad7fab80609ba3cf46d9a890ecf1619f54eaa661ebb4f61ffdd", + "zh:ff5e17795828ff8f5c32eec0ae7c9746b26b51e803e8fb28f7131a98cc1fc301", + ] +} diff --git a/01-iam/infisical/README.md b/01-iam/infisical/README.md new file mode 100644 index 0000000..1745ee1 --- /dev/null +++ b/01-iam/infisical/README.md @@ -0,0 +1,87 @@ +# 01-iam/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 +use a static API key because Scaleway IAM isn't an OIDC relying party). Infisical +**is**, so we get the real keyless flow here. + +## How the flow works + +1. A workflow run requests an OIDC token from GitHub (`permissions: id-token: write`). +2. The workflow logs in to Infisical with that token + the identity ID. +3. Infisical verifies the token against GitHub's JWKS (`oidc_discovery_url`), + checks `iss` (`bound_issuer`), `aud` (`bound_audiences`) and `sub` + (`bound_claims.sub` = `repo:IntegratedDynamic/infrastructure:*`), then returns + a short-lived Infisical access token (`access_token_ttl`, default 600s). +4. With that token the workflow reads secrets from the **Platform** project (the + identity is granted the read-only `viewer` project role). + +## What it creates + +- `infisical_identity.github_actions` — the CI identity (org-level role + `no-access`; real permissions come from the project membership, not org-wide). +- `infisical_identity_oidc_auth.github_actions` — the OIDC trust: GitHub issuer + + discovery URL, bound audience, and `sub` scoped to this one repo (any + branch/PR/tag). +- `infisical_project_identity.github_actions` — grants the identity the `viewer` + role on the Platform project so CI can read its secrets. + +## The bootstrap chicken-and-egg + +Creating an OIDC identity still requires an authenticated provider, so this root +authenticates with a **universal-auth machine identity** (`client_id` / +`client_secret` in `default.auto.tfvars`, gitignored, per-developer). That +bootstrap identity is the seed; once this OIDC identity exists, CI authenticates +with it instead of any static secret. + +## Credentials + +- **Infisical** provider — universal-auth machine identity from + `*.auto.tfvars`. Host defaults to `https://app.infisical.com`. +- **S3 state backend** — AWS-style env vars (injected by `mise.toml`'s `[env]` + block like the other roots). + +## 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 +``` + +> Never `apply`/`destroy` here without explicit approval. + +After apply, `terraform output identity_id` is the principal GitHub Actions logs +in as — wire it into the workflow (e.g. as a repo variable). + +## Wiring a workflow (example) + +```yaml +permissions: + id-token: write + contents: read +steps: + - uses: Infisical/secrets-action@v1 + with: + method: oidc + identity-id: ${{ vars.INFISICAL_IDENTITY_ID }} # = output identity_id + project-slug: platform-p-qc1 + env-slug: staging + secret-path: /ci +``` + +The OIDC token's default `aud` is `https://github.com/IntegratedDynamic` +(matching `github_oidc_audience`); if the workflow requests a custom audience, +update that variable to match or login is rejected. + +## Trust scope / revocation + +- Trust is pinned to `repo:IntegratedDynamic/infrastructure:*` — only workflows + in this repo can authenticate. Narrow it further (e.g. a single branch) by + tightening `bound_claims.sub`. +- To revoke access, destroy the identity (or remove the project membership to + drop project access while keeping the identity). diff --git a/01-iam/infisical/env/01-iam-infisical.tfvars b/01-iam/infisical/env/01-iam-infisical.tfvars new file mode 100644 index 0000000..bece347 --- /dev/null +++ b/01-iam/infisical/env/01-iam-infisical.tfvars @@ -0,0 +1,8 @@ +org_id="73133541-f1c4-40d5-93f5-a5e073ab0264" +project_id="7ecb6ed4-058a-46cd-ac9f-7e792469cf0f" +project_role_slug="viewer" + +github_org="IntegratedDynamic" +github_repo="infrastructure" +github_oidc_audience="https://github.com/IntegratedDynamic" +access_token_ttl=600 diff --git a/01-iam/infisical/main.tf b/01-iam/infisical/main.tf new file mode 100644 index 0000000..4455286 --- /dev/null +++ b/01-iam/infisical/main.tf @@ -0,0 +1,56 @@ +# GitHub Actions → Infisical, keyless via OIDC. +# +# A workflow run gets a short-lived OIDC token from GitHub; Infisical verifies it +# against GitHub's JWKS and trades it for an Infisical access token — so CI holds +# no long-lived Infisical credential. This is the keyless counterpart to +# 01-iam/scaleway's static API key. +# +# Bootstrap note: creating this identity still needs the universal-auth machine +# identity wired into the provider (default.auto.tfvars). That bootstrap identity +# is the chicken-and-egg seed; once this OIDC identity exists, CI uses it instead. + +resource "infisical_identity" "github_actions" { + name = "github-actions-oidc" + org_id = var.org_id + + # Org-level role is no-access on purpose: this identity draws its actual + # permissions from the project membership below, never org-wide. + role = "no-access" +} + +resource "infisical_identity_oidc_auth" "github_actions" { + identity_id = infisical_identity.github_actions.id + + # GitHub's OIDC provider. The discovery URL serves the JWKS Infisical uses to + # verify the token signature; bound_issuer must equal the token's `iss` claim. + oidc_discovery_url = "https://token.actions.githubusercontent.com" + bound_issuer = "https://token.actions.githubusercontent.com" + + # The `aud` GitHub stamps on the token. With no audience requested in the + # workflow, GitHub defaults it to the repository-owner URL + # (https://github.com/); set var.github_oidc_audience to match whatever + # the workflow requests. + bound_audiences = [var.github_oidc_audience] + + # Scope trust to any workflow in this one repo (any branch / PR / tag), + # mirroring the AWS root's repo:/:* trust. bound_claims values may be + # glob patterns — bound_subject is an exact match, which we don't want here. + bound_claims = { + sub = "repo:${var.github_org}/${var.github_repo}:*" + } + + # Short-lived CI token — long enough for a job, no longer. + access_token_ttl = var.access_token_ttl +} + +# Grant the identity access to the Platform project so CI can read its secrets. +resource "infisical_project_identity" "github_actions" { + project_id = var.project_id + identity_id = infisical_identity.github_actions.id + + roles = [ + { + role_slug = var.project_role_slug + }, + ] +} diff --git a/01-iam/infisical/outputs.tf b/01-iam/infisical/outputs.tf new file mode 100644 index 0000000..a8be3b0 --- /dev/null +++ b/01-iam/infisical/outputs.tf @@ -0,0 +1,11 @@ +# The identity ID is the principal GitHub Actions logs in as (the OIDC login call +# takes identityId). Public identifier — safe to surface and wire into CI. +output "identity_id" { + description = "Infisical identity ID GitHub Actions authenticates as via OIDC." + value = infisical_identity.github_actions.id +} + +output "oidc_auth_id" { + description = "ID of the OIDC auth configuration attached to the identity." + value = infisical_identity_oidc_auth.github_actions.id +} diff --git a/01-iam/infisical/variables.tf b/01-iam/infisical/variables.tf new file mode 100644 index 0000000..ecc5762 --- /dev/null +++ b/01-iam/infisical/variables.tf @@ -0,0 +1,43 @@ +variable "org_id" { + description = "Infisical organization ID the GitHub Actions identity is created in." + type = string + default = "73133541-f1c4-40d5-93f5-a5e073ab0264" +} + +variable "project_id" { + description = "Infisical project (workspace) ID the identity is granted access to. Defaults to the Platform project." + type = string + default = "7ecb6ed4-058a-46cd-ac9f-7e792469cf0f" +} + +variable "project_role_slug" { + description = "Project role granted to the GitHub Actions identity. 'viewer' = read-only secret access." + type = string + default = "viewer" +} + +# Trust is scoped to exactly one repo: repo:/:* . Only workflows in +# this repo can present an OIDC token Infisical will accept. +variable "github_org" { + description = "GitHub organization that owns the repo allowed to authenticate." + type = string + default = "IntegratedDynamic" +} + +variable "github_repo" { + description = "GitHub repository whose workflows may authenticate (sub claim is scoped to it)." + type = string + default = "infrastructure" +} + +variable "github_oidc_audience" { + description = "Expected `aud` claim on the GitHub OIDC token. Defaults to GitHub's repository-owner default audience." + type = string + default = "https://github.com/IntegratedDynamic" +} + +variable "access_token_ttl" { + description = "Lifetime (seconds) of the Infisical access token CI receives after OIDC login." + type = number + default = 600 +} diff --git a/01-iam/infisical/version.tf b/01-iam/infisical/version.tf new file mode 100644 index 0000000..d105afd --- /dev/null +++ b/01-iam/infisical/version.tf @@ -0,0 +1,26 @@ +terraform { + # Remote state on the shared org S3 bucket (same bucket every other root uses), + # under this root's own key so its state/blast-radius stay isolated. Fresh + # prefix for a brand-new root — no state to migrate. + backend "s3" { + bucket = "id-terraform-state20260612164136440800000001" + region = "eu-west-3" + workspace_key_prefix = "infisical-github-oidc" + key = "terraform.tfstate" + encrypt = true + use_lockfile = true + } + + required_providers { + infisical = { + source = "infisical/infisical" + version = "~> 0.16" + } + } +} + +# The provider authenticates with a universal-auth machine identity (the +# bootstrap identity). Its client_id / client_secret come from *.auto.tfvars +# (per-developer, gitignored — see default.auto.tfvars). Host defaults to +# https://app.infisical.com. +provider "infisical" {} diff --git a/github-ci/.terraform.lock.hcl b/01-iam/scaleway/.terraform.lock.hcl similarity index 100% rename from github-ci/.terraform.lock.hcl rename to 01-iam/scaleway/.terraform.lock.hcl diff --git a/github-ci/README.md b/01-iam/scaleway/README.md similarity index 91% rename from github-ci/README.md rename to 01-iam/scaleway/README.md index 770288a..36b348d 100644 --- a/github-ci/README.md +++ b/01-iam/scaleway/README.md @@ -1,4 +1,4 @@ -# github-ci +# ci/10-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 @@ -58,8 +58,8 @@ Same as the other roots: ## Apply ```bash -mise run github-ci-plan # terraform init && plan — review first -mise run github-ci-apply # terraform apply (billable: creates an IAM key) +mise run scaleway-ci-plan # terraform init && plan — review first +mise run scaleway-ci-apply # terraform apply (billable: creates an IAM key) ``` > Never `terraform apply`/`destroy` here without explicit approval. @@ -77,12 +77,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=github-ci output -raw access_key)" + --body "$(terraform -chdir=ci/10-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=github-ci state show -no-color scaleway_iam_api_key.github_ci \ + --body "$(terraform -chdir=ci/10-scaleway state show -no-color scaleway_iam_api_key.github_ci \ | awk '/secret_key/ {print $3; exit}' | tr -d '\"')" ``` @@ -94,7 +94,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 `github-ci/**` or the workflow itself, so you can validate before +that touches `ci/10-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 +122,7 @@ The API key lives entirely in this root's state. - **On demand** — force it early with: ```bash - terraform -chdir=github-ci apply -replace=scaleway_iam_api_key.github_ci + terraform -chdir=ci/10-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/scaleway/env/01-iam-scaleway.tfvars new file mode 100644 index 0000000..654e22e --- /dev/null +++ b/01-iam/scaleway/env/01-iam-scaleway.tfvars @@ -0,0 +1,6 @@ +infisical_workspace_id="7ecb6ed4-058a-46cd-ac9f-7e792469cf0f" +infisical_env_slug="staging" +infisical_folder_path="/ci" + +project_id="6283c05b-a4c7-4f83-a75f-83adad236d54" +api_key_rotation_days=365 diff --git a/github-ci/main.tf b/01-iam/scaleway/main.tf similarity index 72% rename from github-ci/main.tf rename to 01-iam/scaleway/main.tf index 58584d8..c878efc 100644 --- a/github-ci/main.tf +++ b/01-iam/scaleway/main.tf @@ -1,23 +1,17 @@ -# Dedicated, least-privilege identity for GitHub Actions to authenticate to -# Scaleway. The keyless GitHub-OIDC -> Scaleway flow isn't possible yet (Scaleway -# IAM is not an OIDC relying party — see README), so we use Scaleway's supported -# pattern: a scoped, independently-revocable API key consumed from GH secrets. - -resource "scaleway_iam_application" "github_ci" { +resource "scaleway_iam_application" "this" { name = "github-ci" description = "GitHub Actions CI for the IntegratedDynamic/infrastructure repo (managed by terraform: github-ci/)." } -# Least privilege: read-only Object Storage, scoped to a single project. The -# future Terraform-CI identity gets its own broader policy (out of scope here). -resource "scaleway_iam_policy" "github_ci" { +# Dummy permission for the IAM Scaleway User. TBD +resource "scaleway_iam_policy" "this" { name = "github-ci-object-storage-ro" description = "Read-only Object Storage for the GitHub Actions CI application, project-scoped." - application_id = scaleway_iam_application.github_ci.id + application_id = scaleway_iam_application.this.id rule { project_ids = [var.project_id] - permission_set_names = ["ObjectStorageReadOnly"] + permission_set_names = ["VPCFullAccess", "KubernetesFullAccess", "PrivateNetworksFullAccess", "IPAMReadOnly"] } } @@ -30,8 +24,8 @@ resource "time_rotating" "api_key" { rotation_days = var.api_key_rotation_days } -resource "scaleway_iam_api_key" "github_ci" { - application_id = scaleway_iam_application.github_ci.id +resource "scaleway_iam_api_key" "this" { + application_id = scaleway_iam_application.this.id description = "Consumed from GitHub Actions secrets (SCW_ACCESS_KEY / SCW_SECRET_KEY)." # Bakes the project into the key so `scw object bucket list` resolves the right @@ -57,7 +51,7 @@ resource "infisical_secret_folder" "ci" { resource "infisical_secret" "scw_access_key" { name = "SCW_ACCESS_KEY" - value = scaleway_iam_api_key.github_ci.access_key + value = scaleway_iam_api_key.this.access_key env_slug = var.infisical_env_slug workspace_id = var.infisical_workspace_id folder_path = infisical_secret_folder.ci.path @@ -65,7 +59,7 @@ resource "infisical_secret" "scw_access_key" { resource "infisical_secret" "scw_secret_key" { name = "SCW_SECRET_KEY" - value = scaleway_iam_api_key.github_ci.secret_key + value = scaleway_iam_api_key.this.secret_key env_slug = var.infisical_env_slug workspace_id = var.infisical_workspace_id folder_path = infisical_secret_folder.ci.path diff --git a/github-ci/outputs.tf b/01-iam/scaleway/outputs.tf similarity index 76% rename from github-ci/outputs.tf rename to 01-iam/scaleway/outputs.tf index 45e42cf..aa51c35 100644 --- a/github-ci/outputs.tf +++ b/01-iam/scaleway/outputs.tf @@ -1,11 +1,11 @@ output "application_id" { description = "IAM application ID backing the GitHub Actions CI identity." - value = scaleway_iam_application.github_ci.id + value = scaleway_iam_application.this.id } # The access key is a public identifier (like an AWS access key ID), so it's safe # to surface. The secret half is never output — read it from Infisical or state. output "access_key" { description = "SCW_ACCESS_KEY for the CI identity (public identifier)." - value = scaleway_iam_api_key.github_ci.access_key + value = scaleway_iam_api_key.this.access_key } diff --git a/github-ci/variables.tf b/01-iam/scaleway/variables.tf similarity index 82% rename from github-ci/variables.tf rename to 01-iam/scaleway/variables.tf index 5a3c3b5..e14d310 100644 --- a/github-ci/variables.tf +++ b/01-iam/scaleway/variables.tf @@ -1,14 +1,3 @@ -# Infisical universal-auth machine identity (per-developer, from nico.auto.tfvars). -# Used to write the generated API key back into Infisical. -variable "infisical_client_id" { - type = string -} - -variable "infisical_client_secret" { - type = string - sensitive = true -} - variable "infisical_workspace_id" { description = "Infisical project (workspace) ID the CI secrets are written to." type = string diff --git a/01-iam/scaleway/version.tf b/01-iam/scaleway/version.tf new file mode 100644 index 0000000..8b19c08 --- /dev/null +++ b/01-iam/scaleway/version.tf @@ -0,0 +1,42 @@ +terraform { + backend "s3" { + bucket = "id-terraform-state20260612164136440800000001" + region = "eu-west-3" + # Prefix kept as "github-ci" (≠ this root's path ci/10-scaleway/) on purpose: + # the state key is decoupled from the directory, so the repo restructure was + # a pure move with zero state migration. + workspace_key_prefix = "github-ci" + key = "terraform.tfstate" + encrypt = true + use_lockfile = true + } + + + + required_providers { + scaleway = { + source = "scaleway/scaleway" + version = "~> 2.0" + } + infisical = { + source = "infisical/infisical" + version = "~> 0.16" + } + time = { + source = "hashicorp/time" + version = "~> 0.12" + } + } +} + +# Creds, region and project_id come from the scw CLI config (like the other roots). +provider "scaleway" {} +# Without `auth.oidc`, infisical will not try to consume OIDC environment variables, even if present, and will only look for generic auth environment variables. +provider "infisical" { + auth = { + ## Uncomment `universal` and comment `oidc` when running terraform locally, . + ## By default, even with `INFISICAL_UNIVERSAL_AUTH_CLIENT_XXXX` environment variable, due to `auth.oidc` being present, infisical provider expect OIDC configuration, and nothing else. + # universal = {} + oidc = {} + } +} diff --git a/cluster/local/.terraform.lock.hcl b/02-cluster/local/.terraform.lock.hcl similarity index 100% rename from cluster/local/.terraform.lock.hcl rename to 02-cluster/local/.terraform.lock.hcl diff --git a/02-cluster/local/env/02-cluster-local.tfvars b/02-cluster/local/env/02-cluster-local.tfvars new file mode 100644 index 0000000..bd4c3c1 --- /dev/null +++ b/02-cluster/local/env/02-cluster-local.tfvars @@ -0,0 +1,8 @@ +# This repo follow GitOps convention & standard. +# Which means even `local` environment do get it's own official environment file. +# Treat this file as an example file for any local environment + + +# infisical_client_id = "" +# infisical_client_secret = "" +gitops_revision = "main" diff --git a/cluster/local/main.tf b/02-cluster/local/main.tf similarity index 100% rename from cluster/local/main.tf rename to 02-cluster/local/main.tf diff --git a/cluster/local/variables.tf b/02-cluster/local/variables.tf similarity index 100% rename from cluster/local/variables.tf rename to 02-cluster/local/variables.tf diff --git a/cluster/local/version.tf b/02-cluster/local/version.tf similarity index 100% rename from cluster/local/version.tf rename to 02-cluster/local/version.tf diff --git a/cluster/scaleway/.terraform.lock.hcl b/02-cluster/scaleway/.terraform.lock.hcl similarity index 100% rename from cluster/scaleway/.terraform.lock.hcl rename to 02-cluster/scaleway/.terraform.lock.hcl diff --git a/02-cluster/scaleway/argocd.tf b/02-cluster/scaleway/argocd.tf new file mode 100644 index 0000000..460d37c --- /dev/null +++ b/02-cluster/scaleway/argocd.tf @@ -0,0 +1,82 @@ +data "infisical_secrets" "this" { + env_slug = "staging" + workspace_id = "7ecb6ed4-058a-46cd-ac9f-7e792469cf0f" // project ID + folder_path = "/" +} + +resource "helm_release" "argocd" { + name = "argocd" + namespace = "argocd" + create_namespace = true + + repository = "https://argoproj.github.io/argo-helm" + chart = "argo-cd" + version = "9.4.17" + + # Fail fast (under the 5m default) if ArgoCD doesn't come up. Transient blips + # (e.g. quay.io 502s) are absorbed by retrying the apply (see mise scaleway-up). + timeout = 240 + + depends_on = [scaleway_k8s_pool.default] + + set_sensitive = [{ + name = "configs.secret.argocdServerAdminPassword" + # ArgoCD expects a bcrypt() hash here. bcrypt() would regenerate on every + # run, so we store the hash directly in Infisical to avoid spurious diffs. + value = data.infisical_secrets.this.secrets["ArgoCD_admin_encrypted"].value + }] + + values = [</dev/null || true + kubectl config rename-context "${scaleway_k8s_cluster.this.name}-${local.cluster_uuid}" "${var.cluster_name}" + EOT + } +} diff --git a/02-cluster/scaleway/outputs.tf b/02-cluster/scaleway/outputs.tf new file mode 100644 index 0000000..9bac614 --- /dev/null +++ b/02-cluster/scaleway/outputs.tf @@ -0,0 +1,3 @@ +output "cluster_id" { + value = scaleway_k8s_cluster.this.id +} diff --git a/02-cluster/scaleway/variables.tf b/02-cluster/scaleway/variables.tf new file mode 100644 index 0000000..6d64d53 --- /dev/null +++ b/02-cluster/scaleway/variables.tf @@ -0,0 +1,46 @@ +# Infisical provide many way to authenticate. +# You can use `infisical_client_id` and `infisical_client_secret` when running terraform locally +# Or use `infisical_oidc_identity_id` when OIDC integration is available. +variable "infisical_client_id" { + type = string + default = "" +} + +variable "infisical_client_secret" { + type = string + default = "" + sensitive = true +} + +variable "infisical_oidc_identity_id" { + description = "Infisical OIDC machine-identity ID. When set, the provider authenticates via GitHub-OIDC; when empty, via universal auth." + type = string + default = "" +} + +variable "k8s_version" { + type = string + default = "1.35" +} + +variable "node_count" { + type = number + default = 1 +} + +variable "cluster_name" { + description = "The cluster name" + type = string + default = "scaleway-homelab" +} + +variable "gitops_revision" { + type = string + default = "main" +} + +variable "update_kubeconfig" { + type = bool + default = false + description = "Set to true when using locally to automatically update you ~/.kube/config. Require `kubectl` and `scw` installed & configured." +} diff --git a/02-cluster/scaleway/version.tf b/02-cluster/scaleway/version.tf new file mode 100644 index 0000000..22034b8 --- /dev/null +++ b/02-cluster/scaleway/version.tf @@ -0,0 +1,64 @@ +terraform { + # Remote state in the org-wide bucket (state-backend/). Creds: see mise.toml. + backend "s3" { + bucket = "id-terraform-state20260612164136440800000001" + region = "eu-west-3" + workspace_key_prefix = "cluster/scaleway" + key = "terraform.tfstate" + encrypt = true + use_lockfile = true + } + + required_providers { + scaleway = { + source = "scaleway/scaleway" + version = "~> 2.0" + } + local = { + source = "hashicorp/local" + version = "~> 2.0" + } + null = { + source = "hashicorp/null" + version = "~> 3.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.0" + } + helm = { + source = "hashicorp/helm" + version = "~> 3.0" + } + infisical = { + source = "infisical/infisical" + version = "~> 0.16" + } + } +} + +provider "scaleway" {} + +provider "infisical" { + + auth = { + ## Uncomment `universal` and comment `oidc` when running terraform locally. + ## By default, even with `INFISICAL_UNIVERSAL_AUTH_CLIENT_XXXX` environment variable, due to `auth.oidc` being present, infisical provider expect OIDC configuration, and nothing else. + # universal = {} + oidc = {} + } +} + +provider "kubernetes" { + host = scaleway_k8s_cluster.this.kubeconfig[0].host + token = scaleway_k8s_cluster.this.kubeconfig[0].token + cluster_ca_certificate = base64decode(scaleway_k8s_cluster.this.kubeconfig[0].cluster_ca_certificate) +} + +provider "helm" { + kubernetes = { + host = scaleway_k8s_cluster.this.kubeconfig[0].host + token = scaleway_k8s_cluster.this.kubeconfig[0].token + cluster_ca_certificate = base64decode(scaleway_k8s_cluster.this.kubeconfig[0].cluster_ca_certificate) + } +} diff --git a/CLAUDE.md b/CLAUDE.md index faf3896..684d372 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,33 +34,44 @@ The `.terraform.lock.hcl` in each root must cover **both** `darwin_arm64` (local `mise run lock` is equivalent to: ```bash -terraform -chdir=terraform-state-bucket 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=github-ci providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=aws-github-oidc providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=s3-lister-role providers lock -platform=darwin_arm64 -platform=linux_amd64 +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 ``` Commit the updated lock files alongside the version change. ## Architecture -Several Terraform root modules. One (./terraform-state-bucket) manage the shared terraform state bucket; any -others cloud-based environments will store it's data on it. +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-state-bucket/ # shared AWS S3 bucket holding every root's remote state -cluster/ - local/ # minikube — local dev and debugging. Using local backend (e.g. local files) - scaleway/ # Scaleway Kapsule cluster + ArgoCD bootstrap (homelab; WIP, not yet wired into mise) -github-ci/ # Scaleway IAM identity GitHub Actions authenticates with -aws-github-oidc/ # keyless GitHub-OIDC -> AWS role for CI (Terraform state + bounded IAM) +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) ``` -Otherwise, this repo, for now, is an agregate of terraform root modules without specific structure yet. +**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. -### `./clusters/*` +### `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. @@ -77,15 +88,23 @@ Two-step, one-time bootstrap: Same bootstrap pattern as `local/`, but with the Kapsule cluster + node pool (`DEV1-M`, min=0/max=3) instead. -### `github-ci/` +### `state/00-backend/` -Standalone root (not under `cluster/` — provisions no cluster) that stands up the **Scaleway IAM identity GitHub Actions uses to authenticate to Scaleway**: a dedicated IAM application + a least-privilege policy (`ObjectStorageReadOnly`, project-scoped) + 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 `github-ci/README.md`. +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. -### `aws-github-oidc/` +### `state/10-access/` -Standalone root (provisions no cluster) for **keyless GitHub-OIDC → AWS** CI 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**. Applied locally by an admin; `role_arn` is wired to CI via `vars.AWS_GITHUB_ACTIONS_ROLE_ARN`. See `aws-github-oidc/README.md`. +Org-wide, read-only S3-lister IAM **role created BY the CI** (the first role minted by `identity/00-ci-trust`'s role-creator rather than by a human). Assumable org-wide via two trust doors: AWS principals in the org (`aws:PrincipalOrgID`) and GitHub Actions in the org via OIDC (`repo:IntegratedDynamic/*`). Its true responsibility is **read access to the Terraform state** — used e.g. by the `scaleway-plan` workflow. Applied by CI (`s3-lister-role.yml`). -**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 `workspace/.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 `aws-github-oidc/iam-ci.tf`. +### `ci/10-scaleway/` + +Standalone root that stands up the **Scaleway IAM identity GitHub Actions uses to authenticate to Scaleway**: a dedicated IAM application + a least-privilege policy (`ObjectStorageReadOnly`, project-scoped) + 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`. + +### `identity/00-ci-trust/` + +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`. + +**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 `workspace/.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`. ## Conventions @@ -95,4 +114,4 @@ Standalone root (provisions no cluster) for **keyless GitHub-OIDC → AWS** CI a **PRs**: After each commit + push on a branch, create a draft PR if none exists. Title: `: description`. Body: context, changes, linked issues (`Closes #123`), test instructions. Use [Conventional Comments](https://conventionalcomments.org/) in reviews (`praise`, `nitpick`, `suggestion`, `issue`, `todo`, `question`, `thought`). -**Terraform workspaces**: a root that runs through CI declares its workspaces declaratively in a `workspace/` folder — one `workspace/.tfvars` per workspace. The **filename is the terraform workspace name** (so state lands at `//`, isolated per root) and the **file contents are that workspace's variable values**. The reusable composite action `.github/actions/terraform` loops over these files, doing `workspace select -or-create ` + `plan`/`apply -var-file=workspace/.tfvars`. This replaces the old reliance on a local, gitignored `.terraform/environment` (invisible to CI — a `default`-workspace run collides on the state key). `s3-lister-role/` is the reference example. +**Terraform workspaces**: a root that runs through CI declares its workspaces declaratively in a `workspace/` folder — one `workspace/.tfvars` per workspace. The **filename is the terraform workspace name** (so state lands at `//`, isolated per root) and the **file contents are that workspace's variable values**. The reusable composite action `.github/actions/terraform` loops over these files, doing `workspace select -or-create ` + `plan`/`apply -var-file=workspace/.tfvars`. This replaces the old reliance on a local, gitignored `.terraform/environment` (invisible to CI — a `default`-workspace run collides on the state key). `state/10-access/` is the reference example. diff --git a/README.md b/README.md index fd8a8c6..4d00bd4 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 under `./terraform-state-bucket` bucket. 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 `./state/00-backend`. Check `version.tf` files from any root module in this repository for an example, stored under eu-west-3 region. ## Forking diff --git a/cluster/scaleway/main.tf b/cluster/scaleway/main.tf deleted file mode 100644 index cb2c7b0..0000000 --- a/cluster/scaleway/main.tf +++ /dev/null @@ -1,171 +0,0 @@ -resource "scaleway_vpc_private_network" "cluster" {} - -resource "scaleway_k8s_cluster" "homelab" { - name = "homelab" - version = "1.35" - - auto_upgrade { - enable = true - maintenance_window_start_hour = 2 - maintenance_window_day = "any" - } - cni = "cilium" - type = "kapsule" - private_network_id = scaleway_vpc_private_network.cluster.id - - # Alright for homelab, might not be true for production stuff - delete_additional_resources = true - - tags = ["homelab", "terraform"] -} - -resource "scaleway_k8s_pool" "default" { - cluster_id = scaleway_k8s_cluster.homelab.id - name = "default" - node_type = "DEV1-M" - size = var.node_count - min_size = 0 - max_size = 3 - autoscaling = false - autohealing = true - - lifecycle { - create_before_destroy = true - } -} - -# Without waiting for at least one pool, the cluster status is `pool_required` -# and the DNS entry for the API server is not yet resolvable. -resource "null_resource" "kubeconfig" { - depends_on = [scaleway_k8s_pool.default] - triggers = { - host = scaleway_k8s_cluster.homelab.kubeconfig[0].host - token = scaleway_k8s_cluster.homelab.kubeconfig[0].token - cluster_ca_certificate = scaleway_k8s_cluster.homelab.kubeconfig[0].cluster_ca_certificate - } -} - -resource "local_file" "kubeconfig" { - depends_on = [null_resource.kubeconfig] - content = scaleway_k8s_cluster.homelab.kubeconfig[0].config_file - filename = pathexpand("~/.kube/scaleway-homelab.yaml") - file_permission = "0600" -} - -# Scaleway regional IDs are "/"; the scw CLI wants the bare UUID, -# and the generated kubeconfig context is named "-". -locals { - cluster_uuid = split("/", scaleway_k8s_cluster.homelab.id)[1] -} - -# Optional local DevX: merge this cluster into ~/.kube/config via the Scaleway -# CLI (instead of juggling the standalone file above). Opt-in via -# install_kubeconfig = true in your *.auto.tfvars. `scw k8s kubeconfig install` -# has no rename flag, so we rename the context cleanly with kubectl afterwards. -resource "null_resource" "install_kubeconfig" { - count = var.install_kubeconfig ? 1 : 0 - - triggers = { - cluster_id = scaleway_k8s_cluster.homelab.id - context_name = var.kubeconfig_context_name - } - - depends_on = [null_resource.kubeconfig] - - provisioner "local-exec" { - command = <<-EOT - scw k8s kubeconfig install ${local.cluster_uuid} - # Override any pre-existing context with the target name, then rename. - kubectl config delete-context "${var.kubeconfig_context_name}" 2>/dev/null || true - kubectl config rename-context "${scaleway_k8s_cluster.homelab.name}-${local.cluster_uuid}" "${var.kubeconfig_context_name}" - EOT - } -} - -# ── ArgoCD bootstrap (toggle with var.bootstrap_argocd) ───────────────────── - -data "infisical_secrets" "this" { - count = var.bootstrap_argocd ? 1 : 0 - env_slug = "staging" - workspace_id = "7ecb6ed4-058a-46cd-ac9f-7e792469cf0f" // project ID - folder_path = "/" -} - -resource "helm_release" "argocd" { - count = var.bootstrap_argocd ? 1 : 0 - name = "argocd" - namespace = "argocd" - create_namespace = true - - repository = "https://argoproj.github.io/argo-helm" - chart = "argo-cd" - version = "9.4.17" - - # Fail fast (under the 5m default) if ArgoCD doesn't come up. Transient blips - # (e.g. quay.io 502s) are absorbed by retrying the apply (see mise scaleway-up). - timeout = 240 - - depends_on = [local_file.kubeconfig] - - set_sensitive = [{ - name = "configs.secret.argocdServerAdminPassword" - # ArgoCD expects a bcrypt() hash here. bcrypt() would regenerate on every - # run, so we store the hash directly in Infisical to avoid spurious diffs. - value = data.infisical_secrets.this[0].secrets["ArgoCD_admin_encrypted"].value - }] - - values = [<)" >&2 + exit 1 +fi +terraform apply -auto-approve -var-file="env/${ws}.tfvars" +""" [tasks.dev] description = "Full local dev environment (minikube + ArgoCD)" @@ -45,90 +54,17 @@ mise run local-apply description = "Reset local minikube cluster" run = "minikube delete" -# ── Scaleway (homelab) ────────────────────────────────────────────────────── - -[tasks.scaleway-provision] -description = "Provision the Scaleway cluster only (no ArgoCD). Use for the first from-scratch apply." -dir = "cluster/scaleway" -run = "terraform init && terraform apply -auto-approve -var bootstrap_argocd=false" - -[tasks.scaleway-up] -description = "Provision the Scaleway cluster + bootstrap ArgoCD" -dir = "cluster/scaleway" -run = """ -terraform init -n=0 -until terraform apply -auto-approve; do - n=$((n + 1)) - if [ "$n" -ge 3 ]; then echo "terraform apply failed after $n attempts"; exit 1; fi - echo "apply attempt $n failed (likely a transient registry/API blip); retrying in 15s..." - sleep 15 -done -""" - -[tasks.scaleway-pause] -description = "Scale Scaleway node pool to 0 (free tier)" -dir = "cluster/scaleway" -run = "terraform apply -auto-approve -var='node_count=0'" - -[tasks.scaleway-resume] -description = "Scale Scaleway node pool back to 1" -dir = "cluster/scaleway" -run = "terraform apply -auto-approve -var='node_count=1'" - -[tasks.scaleway-apply] -description = "Plain terraform apply on the Scaleway root" -dir = "cluster/scaleway" -run = "terraform apply" - -[tasks.scaleway-destroy] -description = "Tear down the Scaleway cluster (terraform destroy)" -dir = "cluster/scaleway" -run = "terraform destroy" - -# ── GitHub CI identity (github-ci/) ───────────────────────────────────────── - -[tasks.github-ci-init] -description = "terraform init on the github-ci root" -dir = "github-ci" -run = "terraform init" - -[tasks.github-ci-plan] -description = "terraform plan on the github-ci root" -dir = "github-ci" -run = "terraform init && terraform plan" - -[tasks.github-ci-apply] -description = "terraform apply on the github-ci root (provisions the Scaleway CI IAM key)" -dir = "github-ci" -run = "terraform apply" - -# ── AWS GitHub OIDC identity (aws-github-oidc/) ────────────────────────────── - -[tasks.aws-github-oidc-init] -description = "terraform init on the aws-github-oidc root" -dir = "aws-github-oidc" -run = "terraform init" - -[tasks.aws-github-oidc-plan] -description = "terraform plan on the aws-github-oidc root" -dir = "aws-github-oidc" -run = "terraform init && terraform plan" - -[tasks.aws-github-oidc-apply] -description = "terraform apply on the aws-github-oidc root (provisions the GitHub OIDC provider + IAM role)" -dir = "aws-github-oidc" -run = "terraform apply" # ── Provider locks ─────────────────────────────────────────────────────────── [tasks.lock] description = "Re-generate all .terraform.lock.hcl files for darwin_arm64 + linux_amd64" run = """ -terraform -chdir=terraform-state-bucket 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=github-ci providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=aws-github-oidc providers lock -platform=darwin_arm64 -platform=linux_amd64 -terraform -chdir=s3-lister-role providers lock -platform=darwin_arm64 -platform=linux_amd64 +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 """ diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 0000000..44f177f --- /dev/null +++ b/modules/README.md @@ -0,0 +1,13 @@ +# modules/ + +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/"`. + +Roots (the deployable configs) live under the domain folders `state/`, +`identity/`, `ci/`, and `cluster/`; this folder holds only non-deployable, +reusable building blocks.