ObservabilityLXXXIV · Configuration as CodeConfigAsCode
Alertmanager Config as Code
What you'll learn
- Structure alertmanager.yml with the route tree as code, separating global, route, receivers, and templates
- Use amtool check-config in CI to gate merges to the alertmanager config
- Run amtool config routes test to confirm Alertmanager is running the live config
- Externalise receiver credentials (Slack webhook URL, PagerDuty routing key) from YAML into mounted secrets
- Diagnose the four most common Alertmanager routing failures: continue defaults, mute intervals, missing receivers, secret drift
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A high-error-rate page fires for the payment-service team at 02:14.
The receiver list says payments-pager. The Slack channel says
#payments-incidents. The page goes out to the on-call rotation
and to a Slack channel the team no longer monitors, because
someone changed the route.matchers.team from team=payments to
team=payments|operations two months ago and forgot to update the
receiver. The continue: true on the parent route meant the alert
was routed to both children. The on-call rotation got the page; the
team Slack never read it.
This is the failure shape that alertmanager.yml as code exists to prevent. The routing tree is the operative definition of “who gets paged on what”. A change to that tree is a change to human behaviour, and it goes through review like any other production code.
What it is
alertmanager.yml is the configuration file Alertmanager reads
at startup and on every SIGHUP. It declares four operational
contracts:
- global — defaults that every receiver inherits (SMTP, Slack, PagerDuty, OpsGenie, VictorOps, webhook).
- templates — Go-templated notification bodies reused across receivers.
- route — the routing tree, a parent node with a list of
child routes. Each route has matchers, a receiver, and
routing-control flags (
continue,group_by,group_wait,group_interval,repeat_interval,mute_time_intervals). - receivers — the named destinations: Slack, PagerDuty, email, webhook, and so on.
The file is YAML. Alertmanager parses it with the same loader
whether at startup, on SIGHUP, or via amtool check-config. Any
schema mistake that amtool rejects will also fail Alertmanager
on reload.
Why a sysadmin cares
The alertmanager config is the loudest part of the on-call experience. When it is wrong:
- Pages go to the wrong rotation. A matcher that catches
team=operationsdoes not matchteam=ops(Alertmanager does not support substring matchers without a regex). - Pages are duplicated.
continue: trueon a parent route and a default child that always matches sends every alert to two receivers. - Pages are silenced forever.
mute_time_intervals:referencing a time interval name that does not exist produces a routing error that Alertmanager logs and ignores, leaving the alert actively muted without a workable on-call path. - Pages never reach the destination. A
slack_configs.api_urlthat points at a webhook revoked two quarters ago produces silent failure.
Each of these is preventable. Each is amplified when the file is not under configuration management.
How it works
The mental model is “alerts come in, the routing tree sends them out”:
Prometheus
|
| active alerts (group, label-set, since)
|
v
Alertmanager
|
| route: tree walk top to bottom
| +-- matcher check on each child
| +-- continue: true? keep walking siblings
| +-- otherwise stop at first match
|
v
Receiver (slack_configs, pagerduty_configs, ...)
|
v
Pager / Slack / Email / Webhook
Two details follow. First, Alertmanager’s default for continue
is false. If a route matches and continue: false (the default),
sibling routes are not evaluated. To fan out to multiple teams,
each sibling must explicitly state continue: true or be
configured separately.
Second, Alertmanager does not enforce a unique-receiver constraint. Multiple routes may point at the same receiver. The grouping, deduplication, and silencing logic deduplicate alert fingerprints, not deliveries; if the same alert reaches two sibling receivers and both fire, the on-call gets two pages.
How to configure it
A working alertmanager.yml for a small production tree:
global:
resolve_timeout: 5m
slack_api_url_file: /etc/alertmanager/secrets/slack-default.url
templates:
- default.tmpl
route:
receiver: default
group_by: ['alertname', 'cluster']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity="critical"
receiver: pager-rotation
group_wait: 10s # critical pages faster
repeat_interval: 1h
continue: true
- matchers:
- team="payments"
receiver: payments-slack
- matchers:
- team="platform"
- severity!="info"
receiver: platform-pager-and-slack
continue: true
routes:
- matchers:
- severity="info"
receiver: platform-slack-only
receivers:
- name: default
slack_configs:
- channel: '#observability-default'
send_resolved: true
- name: pager-rotation
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/pagerduty.key
send_resolved: true
severity: critical
slack_configs:
- channel: '#ops-pager'
send_resolved: true
- name: payments-slack
slack_configs:
- channel: '#payments-incidents'
send_resolved: true
- name: platform-pager-and-slack
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/platform-pagerduty.key
slack_configs:
- channel: '#platform-incidents'
send_resolved: true
- name: platform-slack-only
slack_configs:
- channel: '#platform-info'
inhibit_rules:
- source_matchers:
- severity="critical"
target_matchers:
- severity="warning"
equal: ['alertname', 'cluster', 'service']
Notes on the choices:
slack_api_url_fileandrouting_key_fileare the credentials surface. The file is mode 0400 and owned by thealertmanageruser; the contents are read at reload. No plaintext secrets in Git.continue: trueon the critical route lets it continue to the team-specific child so that the team also gets a Slack notification in parallel. Default isfalse.repeat_interval: 4hon the top route withrepeat_interval: 1hon the critical child escalates critical alerts faster.inhibit_rulessuppress the warning alert when a critical alert fires for the sameserviceandalertname. Useful for “API errors critical” silencing “API errors warning”.- The
templatesdirectory lives at/etc/alertmanager/. The path is supplied as--config.file=/etc/alertmanager/alertmanager.ymland the templates dir is relative to that path.
The CI gate:
.PHONY: alertmanager
alertmanager:
amtool check-config alertmanager/alertmanager.yml
How to validate it
Three validations across the CI/deploy lifecycle.
# 1. CI: file parses with the runtime loader
amtool check-config alertmanager/alertmanager.yml
# 2. Prod: Alertmanager is using the loaded config
curl -fsS http://alertmanager-prod-01:9093/-/ready
curl -fsS http://alertmanager-prod-01:9093/api/v2/status \
| jq '.data.config.original' -r
# 3. Prod: a routing test against the live Alertmanager
amtool config routes test \
--alertmanager.url=http://alertmanager-prod-01:9093 \
--config.file=alertmanager/alertmanager.yml \
<<EOF
[
{
"labels": {
"alertname": "HighErrorRate",
"severity": "critical",
"team": "payments",
"cluster": "prod-eu-west-1"
}
}
]
EOF
amtool config routes test walks the live routing tree with a
synthetic alert, returning which receiver(s) would fire. The
output is a JSON list of matched routes. A test that returns the
empty list when it should return a team-specific receiver is a
routing regression that the CI gate should catch.
How it can fail
Six concrete failure modes appear repeatedly.
continue: truefan-out. A new sibling withcontinue: truesends every alert to the new receiver as well as the existing one. The on-call gets two pages. The fix is to model “page only” paths and “page plus Slack” paths as separate receivers with explicit routing, and to lintcontinueflags in CI.- Matcher case sensitivity. Alertmanager matchers are
case-sensitive.
severity=criticaldoes not matchseverity=Critical. The fix is a CI rule that asserts the metric-side labels are normalised at emission. - Receiver name typo. A route points at
payments-pagerbut the receivers block declarespayments-pagerduty. The YAML loader accepts the name; Alertmanager logs “no receivers configured for route”; the alert is dropped. The fix isamtool check-configplus a CI assertion that everyroute[*].receiverexists inreceivers[*].name. mute_time_intervals:referencing an undefined interval. A route points at atime_intervals.business-hoursblock that does not exist. Alertmanager logs the error and treats the interval as empty (mute never). The fix is a CI assertion that interval names exist.- Webhook URL changes silently. The Slack webhook is
rotated; the secret file is updated; the deployment that
should pick it up does not. Slack rejects the post with
invalid_auth. The fix is a CI smoke test that posts a synthetic test alert and asserts the response from each configured receiver. inhibit_rulesover-broad. An inhibit rule with emptytarget_matcherssilences everything. The fix is a CI rule that asserts every inhibit rule has at least onetarget_matchersentry.
Security implications
The alertmanager config is a credentials store in disguise. The discipline:
- No credentials in YAML. Use
*_file(api_url_file, routing_key_file) for every credential, with the file mode 0400 and owned by thealertmanageruser. - Webhook URLs and PagerDuty keys are high-value secrets. They grant the ability to page internal users or post to internal channels.
- The
-/readyandapi/v2/statusendpoints expose the loaded configuration. Anyone who can reach them can see the routing tree. Bind Alertmanager to a private listener and front it with auth. inhibit_rulesdo not move across teams. A team that adds an inhibit rule against another team’s alerts can silence them silently. The fix is a code-review rule that inhibits rules must be reviewed by both teams’ CODEOWNERS.
Performance implications
Alertmanager’s cost grows with the rate of incoming alerts and the size of the alert label set, not with the size of the routing tree. The big knobs:
group_by— wider grouping means fewer notifications; coarser aggregation loses detail.group_wait— short wait means faster notifications; long wait means fewer.group_interval— short interval means more notifications; long interval means fewer.repeat_interval— short repeat means more noise; long repeat means longer to escalation.
The routing tree itself is O(routes) per alert. A tree of 100 routes per alert matches most production teams; the cost is not the bottleneck. The bottleneck is the rate of incoming active alerts during a real incident, which can be in the tens of thousands.
Production guidance
- One
alertmanager.ymlper cluster, in Git. amtool check-configin CI on every change.amtool config routes testfor every critical alert in CI, asserted to hit a PagerDuty receiver.- All credentials in
*_filepaths. No plaintext anywhere. - Test silence rules and time-interval rules in CI.
- Run Alertmanager as a cluster (replicas) with a gossip layer for HA; the file is the source of truth, the cluster is the runtime.
- Reload Alertmanager with
SIGHUPafter every deploy. CI smoke test that the loaded config matches Git.
Verification
You should now be able to answer:
- What does
continue: trueon a parent route do, and why does the default offalserequire care when modelling “page and Slack” paths? - Why is
slack_api_url_filethe right posture for the Slack webhook URL, and what file mode should the credentials file have? - What is the failure shape when a route matcher is case-sensitive and the alert label is emitted in a different case?
- What does
amtool config routes testdo, and what should it be asserted to produce for every critical alert label set?
Quiz
Knowledge check · 8 questions
Q1. Which CLI command validates alertmanager.yml against the runtime schema?
Q2. What does `continue: true` on a route do?
Q3. amtool check-config exits non-zero for YAML schema errors but not for routing-tree logic mistakes.
Q4. Which of these are valid receiver types in alertmanager.yml?
Q5. Name the CLI command that ships with Alertmanager and validates alertmanager.yml against the runtime schema.
Q6. Where do the annotations rendered as the notification body originate?
Q7. What is the production posture for the Slack webhook URL in alertmanager.yml?
Q8. Alertmanager loads its routing tree directly from a rules/ directory under /etc/alertmanager.
Passing score: 75%. Answers are checked in this browser.