Black Friday in 15 Tagen: Können Ihre Kunden dann bezahlen? 25 % Rabatt im ersten Jahr und kostenlose Einrichtung, bis 30. NovemberZum Angebot →

Integration

Eine neue Kundenseite in Terraform

Ein Eintrag in einer Map, ein terraform apply: Der Ping, der Checkout-Flow und das Verfügbarkeitsziel eines Kunden stehen.

Meinen API-Schlüssel holenOpenAPI-Vertrag

Vollständiges Dokument, ohne Authentifizierung, bereit für Ihren Client-Generator.

Das Ergebnis: ein Kunde, ein Eintrag

Sie betreuen mehrere Kundenseiten. Jede Seite ist ein Eintrag in einer Variablendatei. Für diesen Eintrag legt Terraform einen Ping der Startseite an, einen Flow des Checkouts bis zur Zahlungsseite und ein Verfügbarkeitsziel.

Mit den zwei Kunden unten legt das erste terraform apply sieben Ressourcen an: drei pro Kunde und einen gemeinsamen Alarm-Webhook. Ein weiterer Kunde bedeutet einen weiteren Eintrag und drei weitere Ressourcen. Niemand klickt in der Konsole.

Das vollständige Beispiel mit seinen Validierungen liegt im Repository des Providers, im Ordner examples/agency-client. Diese Seite folgt seinem Code in gekürzter Form. Die Domains example.com sind für Dokumentation reserviert: Ersetzen Sie sie durch die Seiten, die Sie überwachen dürfen.

terraform.tfvars

webhook_url = "https://hooks.example.com/pathly"

clients = {
  bakery = {
    name         = "Example Bakery"
    home_url     = "https://www.example.com/"
    product_url  = "https://www.example.com/products/sourdough"
    checkout_url = "https://www.example.com/checkout"
    severity     = "critical"
  }

  bookshop = {
    name          = "Example Bookshop"
    home_url      = "https://shop.example.com/"
    product_url   = "https://shop.example.com/books/atlas"
    checkout_url  = "https://shop.example.com/checkout"
    objective_pct = 99.5
  }
}

Anwenden

# PATHLY_API_TOKEN kommt aus Ihrem Secret-Store, nie aus einer Datei
terraform init
terraform plan
terraform apply

# Plan: 7 to add, 0 to change, 0 to destroy.

Der Code: Ping, Flow und Ziel pro Kunde

Die Variable clients ist eine Map von Objekten. Ihr Schlüssel ist ein kurzer, stabiler Bezeichner: Er wird Teil jeder Ressourcenadresse und des Kunden-Tags. Jede Ressource durchläuft diese Map mit for_each.

Der Ping ist ein pathly_scenario mit einer url: Er prüft, ob die Startseite antwortet. Der Flow ist ein pathly_scenario vom Typ browser, mit seinen Aktionen in steps. Ein echter Browser öffnet die Produktseite, legt das Produkt in den Warenkorb und öffnet die Zahlungsseite. Der erste Schritt ist immer ein goto.

Der Flow endet, sobald die Zahlungsseite angezeigt wird. Kein Schritt klickt auf einen Bezahlen-Button, und er gibt nur Testwerte ein. Die Selektoren und der erwartete Text hängen von jeder Seite ab: Prüfen Sie sie im Inspektor des Browsers, bevor Sie anwenden.

Das Verfügbarkeitsziel ist ein pathly_sla_target, das am Ping hängt. objective_pct legt das Ziel über ein gleitendes Fenster von window_days Tagen fest. warn_at_budget_ratio warnt, wenn 80 % des Fehlerbudgets verbraucht sind, solange noch Spielraum zum Handeln bleibt.

Die API gibt die Schritte eines Flow nie zurück, nur ihren Fingerabdruck (scenario_fingerprint). Der Code ist daher die einzige lesbare Kopie des Ablaufs: Halten Sie ihn im Repository, geprüft wie alles andere.

variables.tf

terraform {
  required_providers {
    pathly = {
      source  = "pathlyhq/pathly"
      version = "~> 0.1"
    }
  }
}

provider "pathly" {}

variable "clients" {
  type = map(object({
    name          = string
    home_url      = string
    product_url   = string
    checkout_url  = string
    severity      = optional(string, "major")
    objective_pct = optional(number, 99.9)
  }))
}

Ping

resource "pathly_scenario" "home" {
  for_each = var.clients

  name            = "${each.value.name} - home page"
  url             = each.value.home_url
  interval_sec    = 300
  expected_status = 200
  severity        = each.value.severity
  folder          = each.value.name
  tags            = ["terraform", "client:${each.key}"]
}

Flow

resource "pathly_scenario" "checkout" {
  for_each = var.clients

  name         = "${each.value.name} - checkout up to payment"
  type         = "browser"
  interval_sec = 3600
  severity     = each.value.severity
  folder       = each.value.name
  tags         = ["terraform", "client:${each.key}", "checkout"]

  steps = [
    {
      op  = "goto"
      url = each.value.product_url
    },
    {
      op       = "click"
      selector = "button[name=add-to-cart]"
    },
    {
      op  = "goto"
      url = each.value.checkout_url
    },
    {
      op   = "assert_text"
      text = "Payment"
    },
  ]
}

Verfügbarkeitsziel

resource "pathly_sla_target" "home" {
  for_each = var.clients

  scenario_id          = pathly_scenario.home[each.key].id
  name                 = "${each.value.name} - ${each.value.objective_pct} %"
  objective_pct        = each.value.objective_pct
  window_days          = 30
  exclude_maintenance  = true
  warn_at_budget_ratio = 0.8
}

Alarme: ein gemeinsamer Webhook, ein Tag und ein Ordner pro Kunde

Ein einziger pathly_webhook empfängt die Fehler (run.failed) und die Erholungen (run.recovered) aller Kunden. Jede Zustellung ist per HMAC mit einem Geheimnis signiert, das nur einmal, bei der Erstellung, zurückgegeben wird. Prüfen Sie diese Signatur auf der Empfängerseite, sonst kann jeder einen falschen Alarm fälschen.

Eine Zustellung nennt das Szenario mit seiner Kennung (monitorId) und seinem Namen (monitorName), nicht mit seinen Tags. Um den Kunden zu finden, vergleicht Ihr Empfänger die Kennung mit der Ausgabe scenario_ids oder liest den Anfang des Namens: Jeder Name beginnt mit dem Namen des Kunden.

Das Tag client:<Schlüssel> und der Ordner dienen dem Sortieren. Der Ordner hält die Szenarien eines Kunden in der Konsole zusammen. Das Tag filtert das Inventar, zum Beispiel mit filter_tag in der Datenquelle pathly_scenarios.

Ausgehende Webhooks sind nicht in jedem Tarif enthalten. In einem Tarif ohne Webhooks lehnt die API das Anlegen des Webhooks ab.

pathly_webhook

variable "webhook_url" {
  type = string
}

resource "pathly_webhook" "alerts" {
  url    = var.webhook_url
  events = ["run.failed", "run.recovered"]
}

scenario_ids

output "scenario_ids" {
  value = {
    for key, client in var.clients : key => {
      home     = pathly_scenario.home[key].id
      checkout = pathly_scenario.checkout[key].id
    }
  }
}

Die Szenarien eines einzelnen Kunden

data "pathly_scenarios" "bakery" {
  filter_tag = "client:bakery"
}

Geheimnisse: Schlüssel, sensible Variablen und State

Der API-Schlüssel liegt in der Umgebungsvariable PATHLY_API_TOKEN, geliefert von Ihrem Secret-Store oder von den CI-Secrets. Der provider-Block bleibt leer. Ein Schlüssel in einer .tf-Datei landet im Repository, und ein Schlüssel aus einer Terraform-Variable landet im Klartext im State.

Geben Sie dem Schlüssel die nötigen Scopes und nicht mehr: scenarios:write, alerting:write und sla:write zum Anwenden. Fügen Sie maintenance:write nur hinzu, wenn Sie Wartungsfenster im selben Code verwalten. Schreiben schließt Lesen ein. Für einen Job, der nur terraform plan ausführt, genügt ein zweiter Schlüssel mit den :read-Scopes.

Wenn der Checkout eines Kunden eine Anmeldung verlangt, läuft das Testkonto über eine als sensitive markierte Variable, getrennt von der Map clients: Terraform lässt nicht zu, dass eine sensible Map ein for_each steuert. Legen Sie dieses Konto für die Überwachung an, ohne gespeichertes Zahlungsmittel. Verwenden Sie nie das Konto eines echten Käufers.

Der Terraform-State enthält das Webhook-Geheimnis, weil die API es nur bei der Erstellung zurückgibt. Er enthält auch die Testpasswörter, die der Flow eingibt. Legen Sie ihn in ein Remote-Backend, im Ruhezustand verschlüsselt und mit eingeschränktem Zugriff. Committen Sie ihn nicht, und veröffentlichen Sie das Geheimnis nicht in einer nicht sensiblen Ausgabe.

Scopes des Schlüssels

# terraform apply   scenarios:write  alerting:write  sla:write
#                   maintenance:write   (maintenance windows only)
# terraform plan    scenarios:read   alerting:read   sla:read

Testkonten, getrennt von der Map clients

variable "test_logins" {
  type = map(object({
    username = string
    password = string
  }))
  sensitive = true
  default   = {}
}

Anmeldeschritte des Flow

  steps = [
    {
      op  = "goto"
      url = "https://shop.example.com/login"
    },
    {
      op       = "fill"
      selector = "input[name=email]"
      value    = var.test_logins[each.key].username
    },
    {
      op       = "fill"
      selector = "input[name=password]"
      value    = var.test_logins[each.key].password
    },
    {
      op       = "click"
      selector = "button[type=submit]"
    },
    # ...goto product page, add to cart, goto checkout, assert_text
  ]

Drift: was außerhalb des Codes existiert

Ein Kollege in Eile legt ein Szenario von Hand in der Konsole an. Es entgeht dem Review, und niemand weiß, dass es existiert. Die Datenquelle pathly_scenarios liest das gesamte Inventar der Organisation zurück. Die Ausgabe scenarios_outside_terraform listet auf, was Pathly enthält und der Code nicht kennt.

Eine leere Liste bedeutet, dass der Code alles beschreibt. Ein Szenario, das dort auftaucht, behandeln Sie auf eine von zwei Arten: Sie importieren es in den Code, oder Sie löschen es. Wenn andere Terraform-Repositories einen Teil der Organisation verwalten, grenzen Sie das Inventar mit filter_tag oder filter_folder ein.

Planen Sie jede Nacht ein terraform plan in der CI, mit der Option -detailed-exitcode. Der Exit-Code 0 sagt, dass sich nichts bewegt hat. Der Exit-Code 2 sagt, dass der Plan nicht leer ist: Ein im Code beschriebenes Attribut wurde in der Konsole geändert, oder die Liste der Szenarien außerhalb des Codes hat sich geändert. Der Job schlägt fehl und meldet es Ihnen.

Dieser nächtliche Job liest nur: Geben Sie ihm einen Schlüssel mit den :read-Scopes. Er braucht außerdem Zugriff auf das State-Backend und dieselben Variablen wie der Job, der den Code anwendet.

Zwei Grenzen sollten Sie kennen. Ein Attribut, das im Code fehlt, behält den in der Konsole gesetzten Wert, ohne im Plan zu erscheinen. Die Schritte eines Flow lassen sich über die API nicht zurücklesen. Schreiben Sie daher jedes Attribut in den Code, das Sie festhalten wollen.

scenarios_outside_terraform

data "pathly_scenarios" "all" {
  depends_on = [pathly_scenario.home, pathly_scenario.checkout]
}

output "scenarios_outside_terraform" {
  value = [
    for s in data.pathly_scenarios.all.scenarios : {
      id     = s.id
      name   = s.name
      folder = s.folder
    }
    if !contains(
      concat(
        [for home in pathly_scenario.home : home.id],
        [for checkout in pathly_scenario.checkout : checkout.id],
      ),
      s.id,
    )
  ]
}

GitHub Actions — nächtlicher Plan

name: pathly-drift
on:
  schedule:
    - cron: "0 3 * * *"

jobs:
  plan:
    runs-on: ubuntu-latest
    env:
      PATHLY_API_TOKEN: ${{ secrets.PATHLY_API_TOKEN_READ }}
      TF_VAR_webhook_url: ${{ secrets.PATHLY_WEBHOOK_URL }}
      TF_VAR_test_logins: ${{ secrets.PATHLY_TEST_LOGINS }}
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: false
      - run: terraform init -input=false
      - run: terraform plan -input=false -lock=false -detailed-exitcode

Kunden hinzufügen, ändern, entfernen oder importieren

Einen Kunden hinzufügen: ein weiterer Eintrag in der Map, ein Pull Request, ein apply. Der Plan zeigt drei anzulegende Ressourcen und berührt sonst nichts. Genau das prüfen Sie im Review.

Einen Kunden ändern: Bearbeiten Sie seinen Eintrag. Eine neue URL, ein Schweregrad oder ein Ziel werden an Ort und Stelle aktualisiert. Benennen Sie den Schlüssel nicht leichtfertig um: Er ist Teil der Ressourcenadressen. Terraform würde die Szenarien zerstören und neu anlegen, ohne ihre Laufhistorie. Wenn Sie ihn umbenennen müssen, deklarieren Sie einen moved-Block pro Ressource.

Einen Kunden entfernen: Löschen Sie seinen Eintrag. Der Plan zeigt drei zu zerstörende Ressourcen. Zerstörte Szenarien nehmen ihre Laufhistorie mit: Exportieren Sie vorher, was der Kunde behalten muss.

Eine von Hand angelegte Seite importieren: Fügen Sie zuerst ihren Eintrag zur Map hinzu und importieren Sie dann das bestehende Szenario an der Adresse dieses Eintrags, mit terraform import pathly_scenario.<Name> <UUID des Szenarios>. Die Kennung ist eine UUID, ablesbar in der URL des Szenarios in der Konsole. Ohne Import würde Terraform ein zweites Szenario neben dem ersten anlegen.

Nach dem Import eines Flow überschreibt das nächste apply die Schritte mit denen des Codes, da die API sie nicht zurückgibt. Ein Verfügbarkeitsziel, das in der Konsole bereits für dieses Szenario gesetzt ist, wird vom ersten apply übernommen, ohne Konfliktfehler.

Ein weiterer Kunde

clients = {
  # ...

  cycles = {
    name         = "Example Cycles"
    home_url     = "https://store.example.org/"
    product_url  = "https://store.example.org/bikes/city"
    checkout_url = "https://store.example.org/checkout"
  }
}

Import über die Kommandozeile

terraform import 'pathly_scenario.home["cycles"]' 00000000-0000-4000-8000-000000000000
terraform import 'pathly_scenario.checkout["cycles"]' 11111111-1111-4111-8111-111111111111
terraform plan

Oder ein import-Block, mit dem Rest geprüft

import {
  to = pathly_scenario.home["cycles"]
  id = "00000000-0000-4000-8000-000000000000"
}