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.
Was Sie bauen
Abschnitt betitelt „Was Sie bauen“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 planin jedem Merge Request zeigt und ihn nach dem Merge aufmainanwendet.
Die Codebeispiele sind auf Englisch, wie die Namen, die sie anlegen: Ersetzen Sie sie durch Ihre eigenen.
Bevor Sie beginnen
Abschnitt betitelt „Bevor Sie beginnen“- 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.
1. Das Repository
Abschnitt betitelt „1. Das Repository“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.ymlterraform { 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_KEYvariable "db_password" { type = string sensitive = true}
variable "discord_webhook" { type = string sensitive = true}2. Die Monitore
Abschnitt betitelt „2. Die Monitore“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.
3. Alarmkanal und Gruppe
Abschnitt betitelt „3. Alarmkanal und Gruppe“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.
4. Statusseite und Wartung
Abschnitt betitelt „4. Statusseite und Wartung“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]}5. Der Terraform-State
Abschnitt betitelt „5. Der Terraform-State“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.
6. Die Pipeline
Abschnitt betitelt „6. Die Pipeline“Legen Sie unter Settings → CI/CD → Variables an:
| Variable | Wert | Optionen |
|---|---|---|
OKSTATUS_API_KEY | der Schlüssel mit Zugriff Nur Lesen | maskiert |
OKSTATUS_API_KEY_FULL | der Schlüssel mit Zugriff Voll | maskiert, geschützt |
TF_VAR_db_password, TF_VAR_discord_webhook | die Secrets | maskiert, 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.
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_BRANCHDie 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.
Mit GitHub Actions
Abschnitt betitelt „Mit GitHub Actions“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:
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.
name: okstatuson: 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-approveDie Fallstricke
Abschnitt betitelt „Die Fallstricke“- Eine Region außerhalb Ihres Tarifs scheitert beim
apply, nicht beimplan(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.
Weiterführend
Abschnitt betitelt „Weiterführend“- Terraform-Leitfaden: alle Monitortypen, private Agenten, Import.
- Fehlerbudget in Grafana: das hier gesetzte
slo_targetverfolgen. - Heartbeat-Monitore: die Cronjobs desselben Projekts überwachen.