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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions