Dans les articles précédents de la série TheForge, nous avons vu comment déployer un cluster GKE + VPC + ArgoCD sur GCP via Terraform.
Cette stack est fonctionnelle mais elle a ses limites : le state Terraform est local, l’authentification GCP repose sur gcloud et il n’y a aucun workflow CI/CD.
Comment passer d’un workflow PoC local à une configuration CI/CD complète applicable en production ?
- Authentification GCP via Workload Identity Federation (WIF) pour GitHub Actions
- Remote state Terraform sur GCS
- Workflows GitHub Actions
Workload Identity Federation
Workload Identity Federation (WIF) est une fonctionnalité de GCP qui permet à des applications externes (comme GitHub Actions) d’obtenir des identités temporaires pour accéder aux ressources GCP, sans avoir besoin de stocker des clés JSON de service account.
Variables shell à exporter
Remplacez les valeurs par celles de votre projet GCP et de votre repo GitHub.
export PROJECT_ID="deft-accord-496812-k9"
export REGION="europe-west1"
export GITHUB_ORG="Bernedotcom2312"
export GITHUB_REPO="theforge-infra"
export SA_NAME="github-actions-terraform"
export SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
export WIF_POOL="github-wif-pool"
export WIF_PROVIDER="github-actions-provider"
export TF_STATE_BUCKET="theforge-infra-tfstate-${PROJECT_ID}"
Activer les APIs GCP nécessaires
Dans la suite de cet article nous allons avoir besoin d’activer plusieurs APIs GCP pour que Terraform et GitHub Actions puissent interagir avec GCP, notamment l’IAM, GKE, Compute Engine et Cloud Storage.
gcloud services enable cloudresourcemanager.googleapis.com iam.googleapis.com \
iamcredentials.googleapis.com sts.googleapis.com container.googleapis.com \
compute.googleapis.com storage.googleapis.com --project="${PROJECT_ID}"
Créer le bucket GCS pour le state Terraform
gcloud storage buckets create "gs://${TF_STATE_BUCKET}" \
--project="${PROJECT_ID}" --location="${REGION}" \
--uniform-bucket-level-access --public-access-prevention
gcloud storage buckets update "gs://${TF_STATE_BUCKET}" --versioning
Créer le Service Account
gcloud iam service-accounts create "${SA_NAME}" \
--project="${PROJECT_ID}" \
--display-name="GitHub Actions Terraform SA"
Attribuer les rôles IAM minimaux
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SA_EMAIL}" --role="roles/container.admin"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SA_EMAIL}" --role="roles/compute.networkAdmin"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SA_EMAIL}" --role="roles/viewer"
gcloud storage buckets add-iam-policy-binding "gs://${TF_STATE_BUCKET}" \
--member="serviceAccount:${SA_EMAIL}" --role="roles/storage.objectAdmin"
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
--project="${PROJECT_ID}" \
--member="serviceAccount:${SA_EMAIL}" --role="roles/iam.serviceAccountUser"
Créer le pool WIF et le provider OIDC
GCP recommande de créer un pool WIF par environnement (dev, staging, prod) pour limiter l’accès aux ressources GCP. Dans cet exemple, nous allons créer un pool WIF unique pour le repo GitHub.
gcloud iam workload-identity-pools create "${WIF_POOL}" \
--project="${PROJECT_ID}" --location="global" \
--display-name="GitHub Actions WIF Pool"
gcloud iam workload-identity-pools providers create-oidc "${WIF_PROVIDER}" \
--project="${PROJECT_ID}" --location="global" \
--workload-identity-pool="${WIF_POOL}" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'"
L’attribute-condition est ce qui garantit que seul mon organisation GitHub peut s’authentifier, pas n’importe qui avec un token OIDC GitHub.
Lier le SA au pool
export WIF_POOL_NAME=$(gcloud iam workload-identity-pools describe "${WIF_POOL}" \
--project="${PROJECT_ID}" --location="global" --format="value(name)")
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
--project="${PROJECT_ID}" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/${WIF_POOL_NAME}/attribute.repository/${GITHUB_ORG}/${GITHUB_REPO}"
Récupérer la valeur du provider WIF pour GitHub
gcloud iam workload-identity-pools providers describe "${WIF_PROVIDER}" \
--project="${PROJECT_ID}" --location="global" \
--workload-identity-pool="${WIF_POOL}" --format="value(name)"
Configurer les variables GitHub
| Variable | Valeur |
|---|---|
GCP_PROJECT_ID | deft-accord-496812-k9 |
GCP_REGION | europe-west1 |
GCP_SA_EMAIL | github-actions-terraform@deft-accord-496812-k9.iam.gserviceaccount.com |
GCP_WIF_PROVIDER | sortie de la commande 1.7 |
TF_STATE_BUCKET | theforge-infra-tfstate-deft-accord-496812-k9 |

Créer l’environnement GitHub production
Settings → Environments → New environment → production
- Required reviewers : soi-même (ou le lead)
- Deployment branches :
mainuniquement

Migration du state vers GCS
Maintenant que les pré-requis de gestion de droits sont en place, nous allons migrer le state Terraform local vers GCS.
Créer backend.tf
terraform {
backend "gcs" {
bucket = "lenomdevotrebucket"
prefix = "terraform/state"
}
}
Migrer le state local → GCS
gcloud auth application-default login
terraform init -migrate-state # répondre "yes" à la question de migration
terraform plan -var-file=terraform.tfvars # vérifier que tout fonctionne, il ne doit pas y avoir de changement
rm terraform.tfstate terraform.tfstate.backup
git add backend.tf
git commit -m "chore: migrate Terraform state backend to GCS"
git push origin main
Workflows GitHub Actions
Dernière étape : mettre en place les workflows GitHub Actions pour CI/CD pour valider notre configuration dans une PR et appliquer les changements sur main après approbation.
Fichiers à créer
.github/
└── workflows/
├── terraform-ci.yml (PR → validate + plan commenté)
└── terraform-cd.yml (merge main → apply)
terraform-ci.yml
name: Terraform CI
on:
pull_request:
branches:
- main
paths:
- "**.tf"
- "**.tfvars"
- ".github/workflows/terraform-ci.yml"
permissions:
contents: read
id-token: write
pull-requests: write
jobs:
terraform-ci:
name: Validate & Plan
runs-on: ubuntu-26.04
defaults:
run:
shell: bash
steps:
- name: Setup
uses: ./.github/actions/terraform-setup
with:
terraform_wrapper: 'true'
- name: Terraform fmt
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform init
id: init
run: |
terraform init \
-backend-config="bucket=${{ vars.TF_STATE_BUCKET }}" \
-input=false
- name: Terraform validate
id: validate
run: terraform validate -no-color
- name: Setup TFLint
uses: terraform-linters/setup-tflint@v6.2.2
with:
tflint_version: v0.63.1
- name: TFLint init
run: tflint --init
- name: TFLint run
id: tflint
run: tflint --format=compact --no-color
continue-on-error: true
- name: Checkov scan
id: checkov
uses: bridgecrewio/checkov-action@v12.1347.0
with:
directory: .
framework: terraform
output_format: cli
soft_fail: false
continue-on-error: true
- name: Terraform plan
id: plan
run: |
terraform plan \
-var="project_id=${{ vars.GCP_PROJECT_ID }}" \
-var="region=${{ vars.GCP_REGION }}" \
-out=plan.tfplan \
-no-color \
-input=false 2>&1 | tee plan-output.txt
echo "exitcode=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
continue-on-error: true
- name: Upload plan artifact
if: steps.plan.outcome == 'success'
uses: actions/upload-artifact@v7.0.1
with:
name: plan-${{ github.sha }}
path: plan.tfplan
retention-days: 1
Si vous avez suivi mon article précédent sur les pre-commit pour terraform voici les contrôles que je vais appliquer : terraform fmt, terraform validate, tflint et checkov pour la sécurité.
terraform-cd.yml
name: Terraform CD
on:
push:
branches:
- main
paths:
- "**.tf"
- "**.tfvars"
- ".github/workflows/terraform-cd.yml"
permissions:
contents: read
id-token: write
actions: read
jobs:
terraform-apply:
name: Apply to production
runs-on: ubuntu-26.04
environment: production
defaults:
run:
shell: bash
steps:
- name: Setup
uses: ./.github/actions/terraform-setup
- name: Terraform init
run: |
terraform init \
-backend-config="bucket=${{ vars.TF_STATE_BUCKET }}" \
-input=false
- name: Find CI run for this commit
id: find-ci-run
uses: actions/github-script@v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'terraform-ci.yml',
head_sha: context.sha,
status: 'success',
per_page: 1,
});
const run = data.workflow_runs[0];
if (!run) core.setFailed(`No successful CI run found for ${context.sha}`);
core.setOutput('run-id', String(run.id));
- name: Download plan artifact
uses: actions/download-artifact@v8.0.1
with:
name: plan-${{ github.sha }}
run-id: ${{ steps.find-ci-run.outputs.run-id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Terraform apply
run: |
terraform apply \
-auto-approve \
-input=false \
plan.tfplan
Ce fichier sera déclenché uniquement sur le merge de la branche main et après approbation de l’environnement production, c’est le seul endroit où j’autorise le terraform apply pour respecter les principes GitOps
Vérification
Pour valider la chaine compplète il faut désormais appliquer un changement mineur (ex: ajout d’un tag sur une ressource).
Vous devriez avoir un résultat semblable à celui-ci dans la PR :

Une fois merged, vous devriez voir le workflow terraform-cd.yml s’exécuter et appliquer les changements sur GCP :

Il suffira ensuite d’approuver le déploiement dans l’environnement production pour que le terraform apply soit exécuté.
Conclusion
Nous avons vu comment passer d’un workflow Terraform local à une configuration CI/CD complète sur GCP applicable en entreprise en respectant les principes GitOps et en optimisant la boucle de feedback pour les développeurs.
L’ensemble du code est disponible sur mon repo GitHub : the-forge-infra.

