Skip to content

feat: provision Terraform CI IAM identity (terraform-ci/) for cluster management #28

Description

@nbrieussel

Context / Problem

The existing github-ci/ Scaleway IAM application has only ObjectStorageReadOnly permissions —
sufficient for the bucket-listing smoke test, but insufficient to run terraform apply/destroy on
cluster/scaleway (K8s clusters, node pools, VPC networks, Terraform state R/W). A dedicated,
scoped identity is required as a prerequisite for the Terraform CI workflows.

Note on OIDC: Scaleway IAM does not support OIDC as a relying party today
(feature request: https://feature-request.scaleway.com/posts/761/oidc-provider-for-external-ci-cd).
This implementation follows the same static-key + time-rotation pattern as github-ci/.
Revisit if/when Scaleway ships OIDC support.

Scope & Non-Goals

In scope: new terraform-ci/ root module; IAM application + least-privilege policy; API key
with time-rotation; Infisical secret storage; mise.toml tasks; gh secret set documentation;
smoke test that confirms state bucket access.

Non-goals: modifying github-ci/, automating gh secret set, OIDC.

Acceptance Criteria

  • terraform-ci/ root module exists (version.tf, variables.tf, main.tf, outputs.tf, README.md, .terraform.lock.hcl)
  • State key is terraform-ci/terraform.tfstate in the shared S3 bucket (independent from github-ci/ and cluster/scaleway/)
  • scaleway_iam_application.terraform_ci provisioned (name "terraform-ci")
  • IAM policy grants exactly ObjectStorageReadWrite + KubernetesFullAccess + VPCFullAccess, all scoped to var.project_id
  • scaleway_iam_api_key.terraform_ci driven by time_rotating (default 365 days, var.api_key_rotation_days)
  • Key written to Infisical /ci folder as TF_SCW_ACCESS_KEY (ID) and TF_SCW_SECRET_KEY (secret, sensitive = true)
  • terraform-ci added to terraform-lock.yml CI matrix
  • mise.toml tasks terraform-ci-init, terraform-ci-plan, terraform-ci-apply added; terraform-ci added to lock task
  • README documents gh secret set TF_SCW_ACCESS_KEY / TF_SCW_SECRET_KEY for the scaleway GitHub Environment and as repo-level secrets (for the ungated destroy workflows)
  • Smoke test job authenticates with the new key and successfully lists the Terraform state bucket (confirms ObjectStorageReadWrite)

Design Notes

Why a new root instead of extending github-ci/: independent state and blast radius. Revoking
the terraform-ci key does not affect the read-only CI key, and vice versa.

Scaleway permission sets for cluster/scaleway:

Permission set Why
ObjectStorageReadWrite R/W the S3 state backend
KubernetesFullAccess scaleway_k8s_cluster + scaleway_k8s_pool
VPCFullAccess scaleway_vpc_private_network

Kubernetes/Helm providers authenticate via the cluster kubeconfig (not Scaleway IAM) — no
additional IAM permission needed for them. Infisical provider uses its own credentials
(TF_VAR_infisical_*) — no IAM coverage needed.

S3 backend env vars in CI (must be set by the consuming workflows):

  • AWS_ACCESS_KEY_ID = TF_SCW_ACCESS_KEY value
  • AWS_SECRET_ACCESS_KEY = TF_SCW_SECRET_KEY value
  • AWS_DEFAULT_REGION = fr-par
  • AWS_ENDPOINT_URL_S3 = https://s3.fr-par.scw.cloud

Secret naming: TF_SCW_ACCESS_KEY / TF_SCW_SECRET_KEY — distinct from the read-only
SCW_ACCESS_KEY / SCW_SECRET_KEY to preserve permission separation.

Smoke test: add a second job to the existing scaleway-auth-check.yml, triggered on PRs
touching terraform-ci/**, reusing the scaleway/action-scw pattern.

Tasks

1. Root module scaffold

  • 1.1 Create terraform-ci/version.tf — copy github-ci/version.tf; set workspace_key_prefix = "terraform-ci"; keep providers scaleway, infisical, time
  • 1.2 Create terraform-ci/variables.tf — copy github-ci/variables.tf (project_id, api_key_rotation_days, infisical_*)
  • 1.3 Create terraform-ci/nico.auto.tfvars (gitignored, same pattern as github-ci/)
  • 1.4 Run terraform init in terraform-ci/; confirm backend initializes cleanly

2. IAM resources (main.tf)

  • 2.1 scaleway_iam_application.terraform_ci (name "terraform-ci")
  • 2.2 scaleway_iam_policy.terraform_ci — 3 rules: ObjectStorageReadWrite, KubernetesFullAccess, VPCFullAccess, all project_ids = [var.project_id]
  • 2.3 time_rotating.api_key (rotation_days = var.api_key_rotation_days)
  • 2.4 scaleway_iam_api_key.terraform_ci (expires_at = time_rotating.api_key.rotation_rfc3339, default_project_id = var.project_id)
  • 2.5 infisical_secret_folder data source or resource (reuse /ci); infisical_secret.tf_scw_access_key + infisical_secret.tf_scw_secret_key (sensitive = true)

3. Outputs

  • 3.1 outputs.tf — expose access_key (non-sensitive ID only; secret never output)

4. mise.toml + lock

  • 4.1 Add terraform-ci-init, terraform-ci-plan, terraform-ci-apply tasks to mise.toml
  • 4.2 Add terraform -chdir=terraform-ci providers lock -platform=darwin_arm64 -platform=linux_amd64 to the lock task
  • 4.3 Run mise run lock for terraform-ci; commit .terraform.lock.hcl
  • 4.4 Add terraform-ci to the matrix in .github/workflows/terraform-lock.yml

5. README + smoke test

  • 5.1 Write terraform-ci/README.md — purpose, why static key, what it creates, credentials, apply steps, gh secret set commands (environment scaleway + repo-level), rotation procedure
  • 5.2 Add list-state-bucket-terraform-ci job to .github/workflows/scaleway-auth-check.yml using TF_SCW_ACCESS_KEY / TF_SCW_SECRET_KEY; trigger on PRs touching terraform-ci/**

6. Apply + wire secrets (manual, post-merge)

  • 6.1 mise run terraform-ci-plan → review → mise run terraform-ci-apply
  • 6.2 gh secret set TF_SCW_ACCESS_KEY + TF_SCW_SECRET_KEY in scaleway environment and as repo-level secrets
  • 6.3 Run smoke test to confirm state bucket access

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions