Black Friday en 15 días: ¿podrán pagar sus clientes? -25 % el primer año y puesta en marcha gratuita, hasta el 30 de noviembreVer la oferta →

Integración

Un nuevo sitio cliente en Terraform

Una entrada en un mapa, un terraform apply: el Ping, el Flow del proceso de compra y el objetivo de disponibilidad de un cliente quedan listos.

Obtener mi clave de APIContrato OpenAPI

Documento completo, sin autenticación, listo para su generador de clientes.

El resultado: un cliente, una entrada

Usted mantiene varios sitios de clientes. Cada sitio es una entrada en un archivo de variables. Para esa entrada, Terraform crea un Ping de la página de inicio, un Flow del proceso de compra hasta la página de pago y un objetivo de disponibilidad.

Con los dos clientes de abajo, el primer terraform apply crea siete recursos: tres por cliente y un webhook de alertas común. Un cliente más es una entrada más y tres recursos más. Nadie hace clic en la consola.

El ejemplo completo, con sus validaciones, está en el repositorio del provider, carpeta examples/agency-client. Esta página sigue su código en versión corta. Los dominios example.com están reservados para la documentación: sustitúyalos por los sitios que tiene derecho a supervisar.

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

Aplicar

# PATHLY_API_TOKEN viene de su gestor de secretos, nunca de un archivo
terraform init
terraform plan
terraform apply

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

El código: Ping, Flow y objetivo por cliente

La variable clients es un mapa de objetos. Su clave es un identificador corto y estable: forma parte de la dirección de cada recurso y de la etiqueta del cliente. Cada recurso recorre ese mapa con for_each.

El Ping es un pathly_scenario con una url: comprueba que la página de inicio responde. El Flow es un pathly_scenario de tipo browser, con sus acciones en steps. Un navegador real abre la página de producto, añade el producto al carrito y abre la página de pago. El primer paso siempre es un goto.

El Flow se detiene cuando aparece la página de pago. Ningún paso hace clic en un botón de pago, y solo escribe valores de prueba. Los selectores y el texto esperado dependen de cada sitio: compruébelos en el inspector del navegador antes de aplicar.

El objetivo de disponibilidad es un pathly_sla_target ligado al Ping. objective_pct fija el objetivo sobre una ventana móvil de window_days días. warn_at_budget_ratio avisa cuando se ha consumido el 80 % del presupuesto de error, mientras queda margen para actuar.

La API nunca devuelve los pasos de un Flow, solo su huella (scenario_fingerprint). El código es, por tanto, la única copia legible del recorrido: guárdelo en el repositorio, revisado como todo lo demás.

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

Objetivo de disponibilidad

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
}

Alertas: un webhook común, una etiqueta y una carpeta por cliente

Un solo pathly_webhook recibe los fallos (run.failed) y las recuperaciones (run.recovered) de todos los clientes. Cada envío va firmado con HMAC mediante un secreto que se devuelve una sola vez, al crearlo. Verifique esa firma en el lado receptor; de lo contrario, cualquiera puede falsificar una alerta.

El envío nombra el escenario por su identificador (monitorId) y por su nombre (monitorName), no por sus etiquetas. Para encontrar al cliente, su receptor compara el identificador con la salida scenario_ids, o lee el principio del nombre: cada nombre empieza por el del cliente.

La etiqueta client:<clave> y la carpeta sirven para ordenar. La carpeta agrupa los escenarios de un cliente en la consola. La etiqueta filtra el inventario, por ejemplo con filter_tag en la fuente de datos pathly_scenarios.

Los webhooks salientes no forman parte de todos los planes. En un plan que no los incluye, la API rechaza la creación del webhook.

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

Los escenarios de un solo cliente

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

Secretos: clave, variables sensibles y estado

La clave de API vive en la variable de entorno PATHLY_API_TOKEN, que proporciona su gestor de secretos o los secretos de la CI. El bloque provider queda vacío. Una clave escrita en un archivo .tf acaba en el repositorio, y una clave leída de una variable de Terraform acaba en claro en el estado.

Dé a la clave los alcances necesarios y nada más: scenarios:write, alerting:write y sla:write para aplicar. Añada maintenance:write solo si gestiona ventanas de mantenimiento en el mismo código. Escribir implica leer. Para una tarea que solo ejecuta terraform plan, basta una segunda clave con los alcances :read.

Si el proceso de compra de un cliente exige iniciar sesión, la cuenta de prueba pasa por una variable marcada como sensitive, separada del mapa clients: Terraform no permite que un mapa sensible dirija un for_each. Cree esa cuenta para la supervisión, sin medio de pago guardado. No use nunca la cuenta de un comprador real.

El estado de Terraform contiene el secreto del webhook, porque la API solo lo devuelve al crearlo. También contiene las contraseñas de prueba que escribe el Flow. Guárdelo en un backend remoto, cifrado en reposo y con acceso restringido. No lo suba al repositorio ni publique el secreto en una salida no sensible.

Alcances de la clave

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

Cuentas de prueba, separadas del mapa clients

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

Pasos de inicio de sesión del 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
  ]

Deriva: lo que existe fuera del código

Un compañero con prisa crea un escenario a mano en la consola. Se escapa de la revisión y nadie sabe que existe. La fuente de datos pathly_scenarios vuelve a leer todo el inventario de la organización. La salida scenarios_outside_terraform enumera lo que Pathly contiene y el código ignora.

Una lista vacía significa que el código lo describe todo. Un escenario que aparece en ella se trata de dos maneras: lo importa al código o lo elimina. Si otros repositorios de Terraform gestionan parte de la organización, reduzca el inventario con filter_tag o filter_folder.

Programe un terraform plan cada noche en la CI, con la opción -detailed-exitcode. El código de salida 0 indica que nada se ha movido. El código 2 indica que el plan no está vacío: un atributo descrito en el código cambió en la consola, o cambió la lista de escenarios fuera del código. La tarea falla y le avisa.

Esta tarea nocturna solo lee: dele una clave con los alcances :read. También necesita acceso al backend del estado y las mismas variables que la tarea que aplica el código.

Dos límites que conviene conocer. Un atributo ausente del código conserva el valor fijado en la consola, sin aparecer en el plan. Los pasos de un Flow no se pueden releer por la API. Escriba, por tanto, en el código cada atributo que quiera mantener.

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 — plan nocturno

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

Añadir, modificar, retirar o importar un cliente

Añadir un cliente: una entrada más en el mapa, una pull request, un apply. El plan anuncia tres recursos por crear y no toca nada más. Ese es el control que hay que hacer en la revisión.

Modificar un cliente: cambie su entrada. Una nueva URL, una severidad o un objetivo se actualizan en el sitio. No cambie el nombre de la clave sin precaución: forma parte de la dirección de los recursos. Terraform destruiría los escenarios y los volvería a crear, sin su historial de ejecuciones. Si debe renombrarla, declare un bloque moved por recurso.

Retirar un cliente: elimine su entrada. El plan anuncia tres recursos por destruir. Los escenarios destruidos se llevan su historial de ejecuciones: exporte antes lo que el cliente deba conservar.

Importar un sitio creado a mano: añada primero su entrada al mapa y luego importe el escenario existente en la dirección de esa entrada, con terraform import pathly_scenario.<nombre> <UUID del escenario>. El identificador es un UUID, visible en la URL del escenario en la consola. Sin la importación, Terraform crearía un segundo escenario junto al primero.

Tras importar un Flow, el siguiente apply reescribe los pasos con los del código, ya que la API no los devuelve. Un objetivo de disponibilidad ya definido en la consola para ese escenario se adopta en el primer apply, sin error de conflicto.

Un cliente más

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

Importar desde la línea de comandos

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

O un bloque import, revisado con lo demás

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