Configuration as code

Embed
Preview of the card
<a href="https://forumulate.com/p/watchtower/docs/as-code"><img src="https://forumulate.com/api/public/orgs/watchtower/spaces/docs/pages/3042c1df-31b6-4f02-9eb7-89800704dd6d/social-card.svg?v=3e7ae33eec8b" alt="Configuration as code" width="600" height="315"></a>

Manage a project's configuration from files with wtctl, or from Terraform. Both use the same kinds and the same identity key: metadata.name, per kind, per project.

Manifests

apiVersion: watchtower.dev/v1
kind: Monitor
metadata:
  name: api-health
spec:
  env_id: env_1
  kind: http
  interval_s: 60
  config: { url: https://example.com/health }

spec is the API request body of the kind without its name. Several documents can share a file (---).

Kind

API collection

Name field

Notes

AlertRule

alert-rules

name

RuleGroup

rule-groups/{name}

name

Prometheus group body. The API cannot list groups, so every apply sends it and the server reports "unchanged".

Routing

routing

n/a

One tree per project. The spec is the tree. It cannot be deleted or pruned.

Receiver

receivers

name

secrets values are ${ENV} references. Omitted secrets keep the stored ones.

Template

templates

name

MaintenanceWindow

maintenance-windows

name

Silence

silences

comment

A silence has no name, so its comment is its identity.

Slo

slos

name

Monitor

monitors

name

CheckSecret

check-secrets/{name}

name

value is a ${ENV} reference. Created when absent; the stored value is never read back, so a rotated value is not re-sent by apply (use terraform apply after changing the reference, or delete the secret and apply).

Schedule

oncall/schedules

name

EscalationPolicy

escalation-policies

name

IncidentType

incident-types

name

Workflow

workflows

name

Dashboard

dashboards

title

ServiceCatalog

catalog/services/{service}

service

Not pruned: services are also discovered from telemetry.

AgentConfig

agent-configs

name

Pipeline

pipelines

name

spec: signal, description?, config (stages), tests?. A change adds a version (its unit tests must pass) and activates it; the signal cannot change.

HealthToken

health-tokens

name

Created once. The token is printed once on stderr (and kept as the sensitive token attribute in Terraform). Existing tokens are never changed.

Commands

wtctl apply  -f <dir|file> --project <id> [--dry-run] [--prune]
wtctl diff   -f <dir|file> --project <id> [--prune]      # exit 1 when anything differs
wtctl export --project <id> [--kinds AlertRule,Monitor] [--out <dir>]
wtctl completion bash|zsh|fish|powershell

--api and --key default to $WT_API and $WT_API_KEY; --project to $WT_PROJECT.

  • apply creates what is missing and updates what differs. A second apply with unchanged files changes nothing. Only the fields a manifest names are compared, so server-side defaults never show as changes.

  • --prune also deletes live resources of the kinds present in the files that no manifest names. Without it nothing is ever deleted.

  • export writes manifests to stdout (or one file per resource below --out) that apply accepts unchanged.

Secrets

Secret fields (Receiver.secrets.*, CheckSecret.value) accept only ${ENV} references. A literal value is rejected before anything is sent, and an unset variable is an error. export writes references such as ${WT_CHECKSECRET_API_TOKEN_VALUE}, never values.

Terraform

provider "watchtower" {
  endpoint = "https://watchtower.example.com" # or $WT_API
  api_key  = var.api_key                      # or $WT_API_KEY
  project  = "prj_..."                        # default project, or $WT_PROJECT
}

resource "watchtower_monitor" "api" {
  name = "api-health"
  spec = jsonencode({ env_id = "env_1", kind = "http", interval_s = 60, config = { url = "https://example.com/health" } })
}

Every kind is a resource named watchtower_<snake_case_kind> with project (optional), name and spec. Import by name: terraform import watchtower_monitor.api prj_.../api-health (or just api-health with a default project). Drift in fields named by spec shows as a changed spec.

Generated client

go/wtapi is generated from apps/web/openapi.json (go generate ./wtapi: a small preprocessor lowers OpenAPI 3.1 to 3.0 and keeps the config-resource paths, then a pinned oapi-codegen runs). wtctl and the provider use its server URL, auth hook and transport.

Phase 6 resources

watchtower_attribute_registry (name = attribute key, spec carries signal), watchtower_archive_config (name = signal; credentials take ${ENV} references only, never read back) and watchtower_fleet_config (create-only; destroy only forgets it, there is no DELETE route), watchtower_retention_rule (name = the rule's priority, spec carries signal, days, match), watchtower_change_source (name = provider kind; the one-time secret lands in the sensitive token attribute and is never read back; no in-place update) and the org-scoped watchtower_ip_allowlist (name = scope org|api_keys|ingest|sessions; destroy resets it to disabled), watchtower_org_sso (singleton) and watchtower_secret_store (credential takes ${ENV} references only, never read back; replace to change). For org-scoped resources the project attribute holds the org id; the provider takes a default org (or $WT_ORG); import with <org>/<name>. Uncovered families and reasons: go/terraform/resources/coverage_exempt.txt.

Provider docs and releases

make tf-docs regenerates go/terraform/docs/ with tfplugindocs (MPL-2.0). scripts/release-provider.sh <version> builds terraform-provider-watchtower_<version>_<os>_<arch>.zip for each target, writes SHA256SUMS and signs it with $WT_GPG_KEY.

Updated