Skip to content

feat: GitHub Actions CI workflows for Terraform apply/destroy (cluster/scaleway) #29

Description

@nbrieussel

Context / Problem

Terraform for cluster/scaleway is currently applied and destroyed exclusively from a developer's
local machine via mise tasks. Moving apply/destroy into GitHub Actions enables repeatable, audited
operations — in particular an automatic nightly destroy at 16:00 UTC (18:00 CEST) to avoid idle
Scaleway billing, and a gated manual apply for controlled cluster startup.

Scope & Non-Goals

In scope:

  • terraform-apply.yml — manual only, gated behind a required-reviewer environment
  • terraform-destroy-manual.yml — manual, no gate
  • terraform-destroy-scheduled.yml — cron 0 16 * * *, fully automated, no gate

Non-goals: plan-only workflow, PR speculative plans, automatic re-apply after nightly destroy.

Acceptance Criteria

  • .github/workflows/terraform-apply.ymlworkflow_dispatch only; environment: scaleway (with required reviewers configured on that environment); exposes node_count (default 1) and bootstrap_argocd (default true) as dispatch inputs; runs terraform init + terraform apply -auto-approve in cluster/scaleway
  • .github/workflows/terraform-destroy-manual.ymlworkflow_dispatch only, no environment gate; runs terraform init + terraform destroy -auto-approve; credentials from repo-level secrets
  • .github/workflows/terraform-destroy-scheduled.ymlschedule: cron: '0 16 * * *' only (no dispatch), no gate; same credentials and commands as manual destroy; includes comment # 16:00 UTC = 18:00 CEST (summer) / 17:00 CET (winter)
  • All three workflows inject: TF_SCW_ACCESS_KEYSCW_ACCESS_KEY + AWS_ACCESS_KEY_ID; TF_SCW_SECRET_KEYSCW_SECRET_KEY + AWS_SECRET_ACCESS_KEY; AWS_DEFAULT_REGION=fr-par; AWS_ENDPOINT_URL_S3=https://s3.fr-par.scw.cloud
  • TF_VAR_infisical_client_id / TF_VAR_infisical_client_secret injected for all three (needed when bootstrap_argocd=true)
  • All action references pinned to full commit SHAs (zizmor compliance, matching repo style)
  • hashicorp/setup-terraform@v3 at terraform_version: "1.14" (matching terraform-lock.yml)
  • actionlint passes on all three workflow files

Design Notes

Gate only on apply: the scaleway GitHub Environment (with required reviewers) is referenced
exclusively by the apply workflow. Both destroy workflows are ungated — they pull credentials from
repo-level secrets (TF_SCW_ACCESS_KEY, TF_SCW_SECRET_KEY, TF_VAR_infisical_client_id,
TF_VAR_infisical_client_secret) so no environment protection rule can block them.

Credential naming: The existing SCW_ACCESS_KEY / SCW_SECRET_KEY (read-only, from
github-ci/) must NOT be reused here. The Terraform CI workflows use TF_SCW_ACCESS_KEY /
TF_SCW_SECRET_KEY (from the terraform-ci/ module — see dependency below). Both credential
pairs coexist in secrets but serve different purposes.

S3 backend in CI: AWS_DEFAULT_REGION=fr-par and
AWS_ENDPOINT_URL_S3=https://s3.fr-par.scw.cloud must be set explicitly — mise.toml injects
these locally but CI gets nothing for free.

Required-reviewer gate: requires GitHub paid plan (or public repo) for private repos. Without
it, the apply workflow still runs but the gate won't pause execution.

Cron timezone: 0 16 * * * UTC = 18:00 CEST (summer) / 17:00 CET (winter). GitHub Actions
cron does not support local timezones.

Tasks

1. Prerequisites (manual, before code)

  • 1.1 Configure scaleway GitHub Environment with at least one required reviewer (for the apply gate)
  • 1.2 Set repo-level secrets TF_SCW_ACCESS_KEY, TF_SCW_SECRET_KEY, TF_VAR_infisical_client_id, TF_VAR_infisical_client_secret (used by ungated destroy workflows); also set TF_SCW_ACCESS_KEY / TF_SCW_SECRET_KEY in the scaleway environment for the gated apply

2. Apply workflow

  • 2.1 Create .github/workflows/terraform-apply.yml with workflow_dispatch + inputs node_count (default 1), bootstrap_argocd (default true)
  • 2.2 Add environment: scaleway to the job
  • 2.3 Inject all credentials (Scaleway provider, S3 backend, Infisical TF vars)
  • 2.4 Add hashicorp/setup-terraform@v3 step (pinned SHA, terraform_version: "1.14")
  • 2.5 Add terraform init + terraform apply -auto-approve -var node_count=... -var bootstrap_argocd=...
  • 2.6 Add permissions: contents: read; pin all action SHAs

3. Manual destroy workflow

  • 3.1 Create .github/workflows/terraform-destroy-manual.yml (workflow_dispatch only, no environment block)
  • 3.2 Inject credentials from repo-level secrets; terraform init + terraform destroy -auto-approve
  • 3.3 Add permissions: contents: read; pin action SHAs

4. Scheduled destroy workflow

  • 4.1 Create .github/workflows/terraform-destroy-scheduled.yml (schedule: cron: '0 16 * * *' only)
  • 4.2 Same credential injection as manual destroy; terraform init + terraform destroy -auto-approve
  • 4.3 Add timezone comment

5. Validation

  • 5.1 Run actionlint on all three workflows
  • 5.2 Manually dispatch apply workflow; confirm required-reviewer gate fires
  • 5.3 Manually dispatch destroy workflow; confirm it runs without gate
  • 5.4 Verify or simulate scheduled destroy (gh workflow run terraform-destroy-scheduled.yml)

Dependencies

Requires #28 to be completed and secrets set before any workflow
can successfully authenticate to Scaleway and the S3 state backend.

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