Zum Inhalt springen

Monitoring als Code mit Terraform und CI

Beschreiben Sie Ihre Monitore, Alarme und Ihre Statusseite als Code, lassen Sie jede Änderung im Merge Request prüfen, und überlassen Sie das Anwenden nach dem Merge der CI. Dieses Tutorial baut ein vollständiges Projekt von Anfang bis Ende; der Terraform-Leitfaden bleibt die Referenz für jedes Attribut.

Ein Repository, das einen kleinen Onlineshop überwacht:

  • drei Monitore: die Website, die Zahlungs-API und die Datenbank;
  • einen Discord-Kanal für alle drei und eine Gruppe „Shop“;
  • eine öffentliche Statusseite und ein geplantes Wartungsfenster;
  • eine Pipeline, die den terraform plan in jedem Merge Request zeigt und ihn nach dem Merge auf main anwendet.

Die Codebeispiele sind auf Englisch, wie die Namen, die sie anlegen: Ersetzen Sie sie durch Ihre eigenen.

  • Der Max-Tarif, für einen API-Schlüssel mit Zugriff Voll (Einstellungen → API-Schlüssel). Legen Sie auch einen Schlüssel mit Zugriff Nur Lesen an: Er genügt für plan.
  • Terraform 1.5 oder neuer.
  • Ein GitLab-Projekt. Eine Version für GitHub Actions steht am Ende von Schritt 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"]
}

Mit drei Regionen geht ein Alarm raus, wenn mindestens zwei davon den Dienst als ausgefallen sehen. Eine einzelne fehlerhafte Region weckt niemanden.

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]
}

Monitore, die der Gruppe beitreten, erhalten ihre Kanäle. Schreiben Sie alert_channel_ids daher nicht auf die Monitore selbst: Das nächste apply würde die Kanäle der Gruppe von ihnen lösen.

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 speichert den State und sperrt ihn während eines apply: Sie müssen nichts hosten. Der Block backend "http" {} aus Schritt 1 bleibt leer; die CI übergibt ihm die Adresse beim terraform init.

Legen Sie unter Settings → CI/CD → Variables an:

VariableWertOptionen
OKSTATUS_API_KEYder Schlüssel mit Zugriff Nur Lesenmaskiert
OKSTATUS_API_KEY_FULLder Schlüssel mit Zugriff Vollmaskiert, geschützt
TF_VAR_db_password, TF_VAR_discord_webhookdie Secretsmaskiert, geschützt

Eine geschützte Variable wird nur auf main gelesen. Merge Requests berechnen ihren Plan mit dem Nur-Lesen-Schlüssel, und nur der Hauptzweig kann Ihre Monitore ändern.

.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

Die Secrets (TF_VAR_…) sind geschützt: Ein Merge Request erhält nur Platzhalter. Sein Plan zeigt daher Passwort und Webhook als geändert; das ist erwartet und wird nie angewendet. Auf main liest der Job die echten Werte; fehlt einer, bricht terraform plan mit „No value for required variable“ ab, statt einen Platzhalter anzuwenden.

GitHub speichert keinen Terraform-State: Legen Sie ihn in einen S3-kompatiblen Bucket, zum Beispiel bei Scaleway Object Storage. Ersetzen Sie das Backend in 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
}

Hinterlegen Sie den Nur-Lesen-Schlüssel als Repository-Secret OKSTATUS_API_KEY. Legen Sie ein Environment okstatus an (Settings → Environments), auf main beschränkt, mit OKSTATUS_API_KEY_FULL, DB_PASSWORD und DISCORD_WEBHOOK. Die Bucket-Schlüssel (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) gehören in die Repository-Secrets.

.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
  • Eine Region außerhalb Ihres Tarifs scheitert beim apply, nicht beim plan (Fehler 400). Prüfen Sie die Regionen Ihres Tarifs vor dem Merge.
  • Was Sie nicht schreiben, wird nicht verwaltet. Ein im Dashboard hinzugefügter Tag bleibt, und Terraform sieht ihn nicht. Um alles festzuschreiben, schreiben Sie das Attribut, auch leer: tags = [].
  • Zugangsdaten sind nur schreibbar. Ein im Dashboard geändertes Passwort erscheint nicht im Plan: Ändern Sie es in der CI-Variable.
  • Bestehende Monitore: Importieren Sie sie mit terraform import okstatus_monitor.site <id>, bevor Sie sie beschreiben, sonst entstehen Duplikate.