Aller au contenu

La supervision dans Git avec Terraform et la CI

Décrivez vos monitors, vos alertes et votre page de statut en code, faites relire chaque changement en merge request, et laissez la CI l’appliquer après la fusion. Ce tutoriel construit un projet complet de bout en bout ; le guide Terraform reste la référence de chaque attribut.

Un dépôt qui surveille une petite boutique en ligne :

  • trois monitors : le site, l’API de paiement et la base de données ;
  • un canal Discord rattaché aux trois, et un groupe « Shop » ;
  • une page de statut publique et une maintenance planifiée ;
  • un pipeline qui affiche le terraform plan dans chaque merge request et l’applique après fusion sur main.

Les exemples de code sont en anglais, comme les noms qu’ils créent : remplacez-les par les vôtres.

  • L’offre Max, pour une clé API Complète (Paramètres → Clés API). Créez aussi une clé Lecture seule : elle suffit pour plan.
  • Terraform 1.5 ou plus récent.
  • Un projet GitLab. Une version GitHub Actions figure à la fin de l’étape 6.
okstatus/
├── versions.tf # provider and backend
├── monitors.tf # the three monitors
├── alerting.tf # Discord channel and group
├── status.tf # status page and maintenance
├── variables.tf
└── .gitlab-ci.yml
versions.tf
terraform {
required_providers {
okstatus = { source = "clearcloud-fr/okstatus", version = "~> 1.8" }
}
backend "http" {} # state kept by GitLab, configured by the CI (step 5)
}
provider "okstatus" {} # the key comes from OKSTATUS_API_KEY
variables.tf
variable "db_password" {
type = string
sensitive = true
}
variable "discord_webhook" {
type = string
sensitive = true
}
monitors.tf
locals {
regions = ["fr-dc3", "de-fra", "nl-ams"]
}
resource "okstatus_monitor" "site" {
name = "Shop · website"
url = "https://shop.example.com"
interval = 60
regions = local.regions
keyword = "Add to cart"
}
resource "okstatus_monitor" "payments" {
name = "Shop · payment API"
url = "https://api.shop.example.com/health"
interval = 30
regions = local.regions
latency_threshold_ms = 800
slo_target = 99.9
}
resource "okstatus_monitor" "db" {
name = "Shop · PostgreSQL"
type = "postgres"
url = "postgres://db.shop.example.com:5432/shop"
db_query = "SELECT 1"
basic_auth_user = "okstatus_ro"
basic_auth_password = var.db_password
regions = ["fr-dc3"]
}

Avec trois régions, une alerte part quand au moins deux d’entre elles voient le service en panne. Une seule région en échec ne réveille personne.

alerting.tf
resource "okstatus_alert_channel" "discord" {
type = "discord"
config = { webhookUrl = var.discord_webhook }
}
resource "okstatus_monitor_group" "shop" {
name = "Shop"
monitor_ids = [okstatus_monitor.site.id, okstatus_monitor.payments.id, okstatus_monitor.db.id]
alert_channel_ids = [okstatus_alert_channel.discord.id]
}

Les monitors qui rejoignent le groupe reçoivent ses canaux. N’écrivez donc pas alert_channel_ids sur les monitors eux-mêmes : le prochain apply détacherait d’eux les canaux du groupe.

status.tf
resource "okstatus_status_page" "public" {
slug = "shop"
title = "Shop status"
custom_domain = "status.shop.example.com"
}
resource "okstatus_maintenance" "pg_upgrade" {
title = "PostgreSQL upgrade"
scheduled_start = "2026-11-12T02:00:00Z"
scheduled_end = "2026-11-12T03:00:00Z"
monitor_ids = [okstatus_monitor.db.id]
}

GitLab conserve l’état et le verrouille pendant un apply : rien à héberger. Le bloc backend "http" {} de l’étape 1 reste vide ; la CI lui passe l’adresse au terraform init.

Dans Paramètres → CI/CD → Variables, créez :

VariableValeurOptions
OKSTATUS_API_KEYla clé Lecture seulemasquée
OKSTATUS_API_KEY_FULLla clé Complètemasquée, protégée
TF_VAR_db_password, TF_VAR_discord_webhookles secretsmasquées, protégées

Une variable protégée n’est lue que sur main. Les merge requests calculent leur plan avec la clé en lecture seule, et seule la branche principale peut modifier vos monitors.

.gitlab-ci.yml
image:
name: hashicorp/terraform:1.9
entrypoint: [""]
variables:
TF_ADDR: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/okstatus
before_script:
- terraform init -input=false
-backend-config="address=${TF_ADDR}"
-backend-config="lock_address=${TF_ADDR}/lock"
-backend-config="unlock_address=${TF_ADDR}/lock"
-backend-config="username=gitlab-ci-token"
-backend-config="password=${CI_JOB_TOKEN}"
-backend-config="lock_method=POST"
-backend-config="unlock_method=DELETE"
stages: [plan, apply]
plan:
stage: plan
script:
- terraform plan -input=false -out=plan.tfplan
artifacts:
paths: [plan.tfplan]
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables: # the real secrets are protected: placeholders for the review plan
TF_VAR_db_password: merge-request-placeholder
TF_VAR_discord_webhook: merge-request-placeholder
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
apply:
stage: apply
variables:
OKSTATUS_API_KEY: $OKSTATUS_API_KEY_FULL
script:
- terraform apply -input=false plan.tfplan
resource_group: okstatus # one apply at a time
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Les secrets (TF_VAR_…) sont protégés : une merge request ne reçoit que des valeurs de remplacement. Son plan montre donc le mot de passe et le webhook comme modifiés ; c’est attendu, et jamais appliqué. Sur main, le job lit les vraies valeurs ; s’il en manque une, terraform plan s’arrête sur « No value for required variable » au lieu d’appliquer une valeur de remplacement.

GitHub n’héberge pas d’état Terraform : gardez-le dans un bucket compatible S3, par exemple chez Scaleway Object Storage. Remplacez le backend de versions.tf :

versions.tf
backend "s3" {
bucket = "acme-terraform-state"
key = "okstatus/terraform.tfstate"
region = "fr-par"
endpoints = { s3 = "https://s3.fr-par.scw.cloud" }
use_lockfile = true # Terraform 1.10 or later
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
}

Ajoutez la clé en lecture seule comme secret du dépôt OKSTATUS_API_KEY. Créez un environnement okstatus (Settings → Environments), limité à main, qui détient OKSTATUS_API_KEY_FULL, DB_PASSWORD et DISCORD_WEBHOOK. Les clés du bucket (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) vont dans les secrets du dépôt.

.github/workflows/okstatus.yml
name: okstatus
on:
pull_request:
push:
branches: [main]
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
env:
OKSTATUS_API_KEY: ${{ secrets.OKSTATUS_API_KEY }}
TF_VAR_db_password: pull-request-placeholder
TF_VAR_discord_webhook: pull-request-placeholder
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform plan -input=false
apply:
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment: okstatus
concurrency: okstatus
env:
OKSTATUS_API_KEY: ${{ secrets.OKSTATUS_API_KEY_FULL }}
TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
TF_VAR_discord_webhook: ${{ secrets.DISCORD_WEBHOOK }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform apply -input=false -auto-approve
  • Une région hors de votre offre échoue à l’apply, pas au plan (erreur 400). Vérifiez les régions de votre offre avant de fusionner.
  • Ce que vous n’écrivez pas n’est pas géré. Un tag ajouté dans le tableau de bord reste en place et Terraform ne le voit pas. Pour tout figer, écrivez l’attribut, même vide : tags = [].
  • Les identifiants sont en écriture seule. Un mot de passe modifié dans le tableau de bord n’apparaît pas dans le plan : changez-le dans la variable de CI.
  • Monitors existants : importez-les avec terraform import okstatus_monitor.site <id> avant de les décrire, sinon vous créez des doublons.