Skip to content

feat(alerting): Grafana deadman heartbeat -> AWS watcher (#560) - #25

Merged
aatchison merged 1 commit into
mainfrom
feat/560-deadman-heartbeat-20260623-101530
Aug 19, 2026
Merged

aatchison merged 1 commit into
mainfrom
feat/560-deadman-heartbeat-20260623-101530

Conversation

@aatchison

Copy link
Copy Markdown
Contributor

Summary

The Grafana side of the alerting deadman/heartbeat (issue https://github.com/thunderbird/platform-infrastructure/issues/560). A heartbeat that stops is what pages: while the alerting stack is healthy this rule fires continuously and POSTs to an off-cluster AWS watcher; if Grafana, the ruler, the VictoriaMetrics datasource, or shared01 egress is down, the heartbeat stops and the watcher pages.

The AWS-native watcher half (Lambda + EventBridge Scheduler + DynamoDB + PagerDuty) is a separate PR: https://github.com/thunderbird/platform-infrastructure/pull/572

Refs https://github.com/thunderbird/platform-infrastructure/issues/560

Changes

  • terraform/deadman.tf (new):
    • grafana_rule_group.deadman_heartbeat — query A is vector(1) executed against the VictoriaMetrics datasource by UID (var.prometheus_datasource_uid), reduced (B) and thresholded > 0 (C). Not a pure in-process __expr__ node — a datasource-traversing query is required (plan B1) so a stopped heartbeat proves ruler + datasource + delivery. Labels deadman=true, severity=heartbeat (deliberately NOT page|critical, so it never hits the real PD route). Reuses the core_services folder.
    • grafana_contact_point.deadman_ingest — webhook POSTing each heartbeat to the watcher ingest URL with a bearer token. URL + token read from Secrets Manager mzla/shared-services/grafana-terraform-deadman-url, named under the grafana-terraform* prefix so the Atlantis ECS task role reads it with no IAM change (plan B2).
  • terraform/alerting.tf: a nested deadman=true route added to the existing grafana_notification_policy.root singleton — a sibling of the page|critical and warning/ticket routes. The root tree and existing routes are unchanged (plan B3: the singleton is not re-created; the notification policy root is otherwise untouched). Tight timers (group_wait=0s, group/repeat=1m).

terraform fmt clean.

Atlantis plan expectations

When this plans: 1 sensitive data-source read; create grafana_contact_point.deadman_ingest + grafana_rule_group.deadman_heartbeat; the notification policy shows the imported baseline plus the new deadman=true route with 0 destroys of the existing page|critical/warning/ticket routes.

The webhook auth attribute names (authorization_scheme / authorization_credentials) should be confirmed against the installed Grafana provider version during terraform validate / autoplan — webhook auth attributes vary by provider release.

Sequencing / not applied

Nothing is applied — no atlantis apply. The heartbeat begins firing on apply, so build the watcher (PR #572) and populate mzla/shared-services/grafana-terraform-deadman-url before applying this, or accept benign delivery failures until the ingest URL exists. Recommended order: watcher up → secret populated → apply this.

Human step

Create the dedicated PagerDuty Grafana Deadman - Platform Infrastructure service + Events API v2 routing key (used by the watcher, not by this repo). Documented in the watcher PR / docs/observability.md.

🤖 Generated with Claude Code

@aatchison
aatchison requested a review from a team as a code owner June 23, 2026 17:21
@mzla-atlantis

mzla-atlantis Bot commented Jun 23, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Failed: This project is currently locked by an unapplied plan from pull #24. To continue, delete the lock from #24 or apply that plan and merge the pull request.

Once the lock is released, comment atlantis plan here to re-plan.

@aatchison
aatchison force-pushed the feat/560-deadman-heartbeat-20260623-101530 branch from 53e74d7 to de0fb4c Compare August 18, 2026 16:42
@mzla-atlantis

mzla-atlantis Bot commented Aug 18, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Failed: This project is currently locked by an unapplied plan from pull #36. To continue, delete the lock from #36 or apply that plan and merge the pull request.

Once the lock is released, comment atlantis plan here to re-plan.

@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis plan

1 similar comment
@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis plan

@mzla-atlantis

mzla-atlantis Bot commented Aug 18, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Error

cannot run "plan": the default workspace at path terraform is currently locked for this pull request by "plan".
Wait until the previous command is complete and try again

@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis plan

@mzla-atlantis

mzla-atlantis Bot commented Aug 18, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Error

cannot run "plan": the default workspace at path terraform is currently locked for this pull request by "plan".
Wait until the previous command is complete and try again

@mzla-atlantis

mzla-atlantis Bot commented Aug 18, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Error

Show Output
running 'sh -c' '/home/atlantis/.atlantis/bin/terraform1.15.8 plan -input=false -refresh -out "/home/atlantis/.atlantis/repos/thunderbird/platform-grafana/25/default/terraform/grafana-default.tfplan"' in '/home/atlantis/.atlantis/repos/thunderbird/platform-grafana/25/default/terraform': exit status 1
╷
│ Warning: Deprecated Parameter
│ 
│ The parameter "dynamodb_table" is deprecated. Use parameter "use_lockfile"
│ instead.
╵
data.aws_secretsmanager_secret_version.grafana: Reading...
data.aws_secretsmanager_secret_version.pagerduty_routing_key: Reading...
data.aws_secretsmanager_secret_version.deadman_ingest: Reading...
data.aws_secretsmanager_secret_version.grafana: Read complete after 0s [id=mzla/shared-services/grafana-terraform|AWSCURRENT]
data.aws_secretsmanager_secret_version.pagerduty_routing_key: Read complete after 0s [id=mzla/shared-services/pagerduty-routing-key|AWSCURRENT]
grafana_folder.kubernetes: Refreshing state... [id=0:dfhbgu7yr6nlsc]
grafana_contact_point.pagerduty_platform_infra: Refreshing state... [id=0:pagerduty-platform-infra]
grafana_rule_group.appointment_edge_public_alb_cert_expiry: Refreshing state... [id=0:efvkkljbxrugwe:appointment-edge-public-alb-cert-expiry]
grafana_folder.bitergia: Refreshing state... [id=0:bfilh3aq4kh6oa]
grafana_folder.keycloak: Refreshing state... [id=0:dfhbgu8xprhtsb]
grafana_folder.twenty: Refreshing state... [id=0:afjpnfj8kg4qoe]
grafana_folder.appointment: Refreshing state... [id=0:efvkkljbxrugwe]
grafana_dashboard.appointment_cloudfront_edge: Refreshing state... [id=0:appointment-cloudfront-edge]
grafana_folder.victorialogs: Refreshing state... [id=0:cftic1vmdaolca]
grafana_folder.core_services: Refreshing state... [id=0:bfhbgu9i6fjswa]
grafana_folder.victoriametrics: Refreshing state... [id=0:ffhbgu8hreubka]
grafana_data_source.cloudwatch_tb_dev: Refreshing state... [id=1:cfjasvpsecqo0c]
grafana_data_source.cloudwatch_workloads: Refreshing state... [id=1:efhwxlv825vcwc]
grafana_folder.argocd: Refreshing state... [id=0:efhbgu7k4a2o0b]
grafana_folder.kargo: Refreshing state... [id=0:ffvjq5wczs934e]
grafana_folder.traefik: Refreshing state... [id=0:efhbguai6gv7ka]
grafana_data_source.cloudwatch_shared_control_plane: Refreshing state... [id=1:efhx0itiz8t1cd]
grafana_data_source.cloudwatch_tb_prod: Refreshing state... [id=1:dfjasvpsjcjcwf]
grafana_folder.teleport: Refreshing state... [id=0:afhbgu9xft7uob]
grafana_folder.discourse: Refreshing state... [id=0:efld6rtum1xxcc]
grafana_data_source.cloudwatch_shared: Refreshing state... [id=1:cfhwxlv6bqepse]
grafana_contact_point.pagerduty_platform_infra_low: Refreshing state... [id=0:pagerduty-platform-infra-low]
grafana_dashboard.bitergia_infrastructure: Refreshing state... [id=0:bitergia-infrastructure]
grafana_dashboard.twenty_overview: Refreshing state... [id=0:twenty-overview]
grafana_dashboard.k8s_namespace_breakdown: Refreshing state... [id=0:k8s-namespace-breakdown]
grafana_dashboard.k8s_persistent_volumes: Refreshing state... [id=0:k8s-persistent-volumes]
grafana_dashboard.k8s_cluster_overview: Refreshing state... [id=0:k8s-cluster-overview]
grafana_dashboard.k8s_pod_container_resources: Refreshing state... [id=0:k8s-pod-container-resources]
grafana_dashboard.vm_victorialogs: Refreshing state... [id=0:vm-victorialogs]
grafana_dashboard.k8s_coredns: Refreshing state... [id=0:k8s-coredns]
grafana_rule_group.keycloak_dr_backup: Refreshing state... [id=1:dfhbgu8xprhtsb:keycloak-dr-backup]
grafana_rule_group.catalog_kubernetes: Refreshing state... [id=1:dfhbgu7yr6nlsc:catalog-kubernetes-paging]
grafana_dashboard.vm_vmagent: Refreshing state... [id=0:vm-vmagent]
grafana_dashboard.thundermail_ticket_spike_monitor: Refreshing state... [id=0:thundermail-ticket-spike-monitor]
grafana_rule_group.euc1_cert_expiry: Refreshing state... [id=1:bfhbgu9i6fjswa:cert-expiry-euc1]
grafana_dashboard.bamboohr_cal_sync: Refreshing state... [id=0:bamboohr-cal-sync]
grafana_rule_group.catalog_keycloak: Refreshing state... [id=1:dfhbgu8xprhtsb:catalog-keycloak-paging]
grafana_dashboard.keycloak_overview: Refreshing state... [id=0:keycloak-overview]
grafana_dashboard.external_dns: Refreshing state... [id=0:external-dns]
grafana_dashboard.vm_cluster_overview: Refreshing state... [id=0:vm-cluster-overview]
grafana_rule_group.catalog_eso: Refreshing state... [id=0:bfhbgu9i6fjswa:catalog-eso-paging]
grafana_dashboard.cert_manager: Refreshing state... [id=0:cert-manager]
grafana_dashboard.external_secrets_operator: Refreshing state... [id=0:external-secrets-operator]
grafana_dashboard.aws_load_balancer_controller: Refreshing state... [id=0:aws-load-balancer-controller]
grafana_rule_group.catalog_victoriametrics: Refreshing state... [id=0:ffhbgu8hreubka:catalog-victoriametrics-paging]
grafana_rule_group.catalog_vmauth: Refreshing state... [id=0:ffhbgu8hreubka:catalog-vmauth-paging]
grafana_rule_group.tailscale: Refreshing state... [id=1:bfhbgu9i6fjswa:tailscale-operator]
grafana_rule_group.catalog_victorialogs: Refreshing state... [id=0:cftic1vmdaolca:catalog-victorialogs-paging]
grafana_dashboard.argocd_application_overview: Refreshing state... [id=0:argocd-application-overview]
grafana_rule_group.catalog_argocd: Refreshing state... [id=1:efhbgu7k4a2o0b:catalog-argocd-paging]
grafana_dashboard.argocd_operational_overview: Refreshing state... [id=0:argocd-operational-overview]
grafana_dashboard.traefik_overview: Refreshing state... [id=0:traefik-overview]
grafana_rule_group.catalog_traefik: Refreshing state... [id=0:efhbguai6gv7ka:catalog-traefik-paging]
grafana_rule_group.catalog_kargo: Refreshing state... [id=0:ffvjq5wczs934e:catalog-kargo-verification]
grafana_rule_group.euc1_federation_ha: Refreshing state... [id=0:dfhbgu8xprhtsb:euc1-federation-ha-dr]
grafana_dashboard.discourse_overview: Refreshing state... [id=0:discourse-overview]
grafana_dashboard.teleport_sessions_connections: Refreshing state... [id=0:teleport-sessions-connections]
grafana_dashboard.teleport_backend_audit: Refreshing state... [id=0:teleport-backend-audit]
grafana_rule_group.catalog_teleport: Refreshing state... [id=0:afhbgu9xft7uob:catalog-teleport-paging]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform planned the following actions, but then encountered a problem:

  # grafana_dashboard.appointment_cloudfront_edge will be destroyed
  # (because grafana_dashboard.appointment_cloudfront_edge is not in configuration)
  - resource "grafana_dashboard" "appointment_cloudfront_edge" {
      - config_json  = jsonencode(
            {
              - annotations          = {
                  - list = [
                      - {
                          - builtIn    = 1
                          - datasource = {
                              - type = "datasource"
                              - uid  = "grafana"
                            }
                          - enable     = true
                          - hide       = true
                          - iconColor  = "rgba(0, 211, 255, 1)"
                          - name       = "Annotations & Alerts"
                          - target     = {
                              - limit    = 100
                              - matchAny = false
                              - tags     = []
                              - type     = "dashboard"
                            }
                          - type       = "dashboard"
                        },
                    ]
                }
              - description          = "CloudFront edge for appointment.tb-dev.thunderbird.dev. All AWS/CloudFront metrics are us-east-1 only, so every query overrides the region on the eu-central-1-default tb-dev CloudWatch datasource. Cache Hit Rate and Origin Latency require the per-distribution MonitoringSubscription (platform-infrastructure #826, Pulumi half) and are empty until it exists."
              - editable             = true
              - fiscalYearStartMonth = 0
              - graphTooltip         = 1
              - links                = [
                  - {
                      - asDropdown  = false
                      - icon        = "external link"
                      - includeVars = false
                      - keepTime    = false
                      - tags        = []
                      - targetBlank = true
                      - title       = "Issue #826 — appointment edge observability"
                      - tooltip     = ""
                      - type        = "link"
                      - url         = "https://github.com/thunderbird/platform-infrastructure/issues/826"
                    },
                ]
              - panels               = [
                  - {
                      - gridPos = {
                          - h = 6
                          - w = 24
                          - x = 0
                          - y = 0
                        }
                      - options = {
                          - code    = {
                              - language        = "plaintext"
                              - showLineNumbers = false
                              - showMiniMap     = false
                            }
                          - content = <<-EOT
                                ### Appointment CloudFront edge — tb-dev (account 718959508124)
                                
                                CloudFront is a global service: **all `AWS/CloudFront` metrics are published in us-east-1 only**. Every query here overrides the datasource region to `us-east-1` (the `CloudWatch Logs — tb-dev01` datasource defaults to `eu-central-1`).
                                
                                **Panels that are expected to be empty until their prerequisite lands**
                                
                                - **Cache Hit Rate** and **Origin Latency** are CloudFront *additional* metrics. They do not exist until an `aws.cloudfront.MonitoringSubscription` is enabled on the distribution — the Pulumi half of https://github.com/thunderbird/platform-infrastructure/issues/826. Empty here means "not subscribed", not "broken".
                                - **Every** panel needs `cloudwatch:GetMetricData` on the cross-account `mzla-tb-dev-grafana-cloudwatch` role, which today grants `logs:*` only. Same issue, same Pulumi half.
                                - The distribution itself is created by https://github.com/thunderbird/platform-infrastructure/pull/844 (draft). Until it applies, the **Distribution** picker above is empty and every panel shows no data.
                                - The **Distribution** picker is a `dimension_values` query, which needs `cloudwatch:ListMetrics` (not just `GetMetricData`) on that same role. Its default value and its filter regex are both rendered from the Terraform variable `appointment_distribution_id`, so once that is set the dashboard opens on the appointment distribution even if `ListMetrics` is missing, and it can never silently default to some other distribution in the account.
                                
                                Cache hit rate is the success metric for this edge: a caching distribution that is not caching is a pure extra hop, i.e. a latency penalty. Read it **per cache behaviour** — the deliberately-uncached `/api/*` and `/fxa*` behaviours drag the distribution-wide aggregate below any naive ">90%" target.
                            EOT
                          - mode    = "markdown"
                        }
                      - title   = "Prerequisites and how to read this dashboard"
                      - type    = "text"
                    },
                  - {
                      - collapsed = false
                      - gridPos   = {
                          - h = 1
                          - w = 24
                          - x = 0
                          - y = 6
                        }
                      - panels    = []
                      - title     = "Traffic"
                      - type      = "row"
                    },
                  - {
                      - datasource  = {
                          - type = "cloudwatch"
                          - uid  = "cfjasvpsecqo0c"
                        }
                      - description = "Total viewer requests served by the distribution (all edge locations). CloudFront publishes only Sum for this metric."
                      - fieldConfig = {
                          - defaults  = {
                              - color    = {
                                  - mode = "palette-classic"
                                }
                              - custom   = {
                                  - drawStyle         = "line"
                                  - fillOpacity       = 10
                                  - lineInterpolation = "smooth"
                                  - lineWidth         = 2
                                  - showPoints        = "never"
                                }
                              - mappings = []
                              - min      = 0
                              - unit     = "short"
                            }
                          - overrides = []
                        }
                      - gridPos     = {
                          - h = 8
                          - w = 12
                          - x = 0
                          - y = 7
                        }
                      - options     = {
                          - legend  = {
                              - calcs       = [
                                  - "lastNotNull",
                                  - "max",
                                ]
                              - displayMode = "table"
                              - placement   = "bottom"
                              - showLegend  = true
                            }
                          - tooltip = {
                              - mode = "multi"
                              - sort = "desc"
                            }
                        }
                      - targets     = [
                          - {
                              - datasource       = {
                                  - type = "cloudwatch"
                                  - uid  = "cfjasvpsecqo0c"
                                }
                              - dimensions       = {
                                  - DistributionId = "$distribution_id"
                                  - Region         = "Global"
                                }
                              - expression       = ""
                              - id               = ""
                              - legendFormat     = "Requests"
                              - matchExact       = true
                              - metricEditorMode = 0
                              - metricName       = "Requests"
                              - metricQueryType  = 0
                              - namespace        = "AWS/CloudFront"
                              - period           = "300"
                              - queryMode        = "Metrics"
                              - refId            = "A"
                              - region           = "us-east-1"
                              - statistic        = "Sum"
                            },
                        ]
                      - title       = "Requests"
                      - type        = "timeseries"
                    },
                  - {
                      - datasource  = {
                          - type = "cloudwatch"
                          - uid  = "cfjasvpsecqo0c"
                        }
                      - description = "Bytes downloaded by viewers for GET/HEAD/OPTIONS requests. CloudFront publishes only Sum for this metric."
                      - fieldConfig = {
                          - defaults  = {
                              - color    = {
                                  - mode = "palette-classic"
                                }
                              - custom   = {
                                  - drawStyle         = "line"
                                  - fillOpacity       = 10
                                  - lineInterpolation = "smooth"
                                  - lineWidth         = 2
                                  - showPoints        = "never"
                                }
                              - mappings = []
                              - min      = 0
                              - unit     = "bytes"
                            }
                          - overrides = []
                        }
                      - gridPos     = {
                          - h = 8
                          - w = 12
                          - x = 12
                          - y = 7
                        }
                      - options     = {
                          - legend  = {
                              - calcs       = [
                                  - "lastNotNull",
                                  - "max",
                                ]
                              - displayMode = "table"
                              - placement   = "bottom"
                              - showLegend  = true
                            }
                          - tooltip = {
                              - mode = "multi"
                              - sort = "desc"
                            }
                        }
                      - targets     = [
                          - {
                              - datasource       = {
                                  - type = "cloudwatch"
                                  - uid  = "cfjasvpsecqo0c"
                                }
                              - dimensions       = {
                                  - DistributionId = "$distribution_id"
                                  - Region         = "Global"
                                }
                              - expression       = ""
                              - id               = ""
                              - legendFormat     = "BytesDownloaded"
                              - matchExact       = true
                              - metricEditorMode = 0
                              - metricName       = "BytesDownloaded"
                              - metricQueryType  = 0
                              - namespace        = "AWS/CloudFront"
                              - period           = "300"
                              - queryMode        = "Metrics"
                              - refId            = "A"
                              - region           = "us-east-1"
                              - statistic        = "Sum"
                            },
                        ]
                      - title       = "Bytes Downloaded"
                      - type        = "timeseries"
                    },
                  - {
                      - collapsed = false
                      - gridPos   = {
                          - h = 1
                          - w = 24
                          - x = 0
                          - y = 15
                        }
                      - panels    = []
                      - title     = "Errors"
                      - type      = "row"
                    },
                  - {
                      - datasource  = {
                          - type = "cloudwatch"
                          - uid  = "cfjasvpsecqo0c"
                        }
                      - description = "Percentage (0-100) of viewer requests answered with a 4xx. Alerted on at > 25% for 15m, gated on more than 50 requests in the bucket (severity=warning, Slack only) in terraform/alerting-appointment-edge.tf — that rule is what restores parity with the legacy prod distribution, which got a 4xxErrorRate alarm for free from tb_pulumi."
                      - fieldConfig = {
                          - defaults  = {
                              - color    = {
                                  - mode = "palette-classic"
                                }
                              - custom   = {
                                  - drawStyle         = "line"
                                  - fillOpacity       = 10
                                  - lineInterpolation = "smooth"
                                  - lineWidth         = 2
                                  - showPoints        = "never"
                                }
                              - mappings = []
                              - max      = 100
                              - min      = 0
                              - unit     = "percent"
                            }
                          - overrides = []
                        }
                      - gridPos     = {
                          - h = 8
                          - w = 12
                          - x = 0
                          - y = 16
                        }
                      - options     = {
                          - legend  = {
                              - calcs       = [
                                  - "lastNotNull",
                                  - "max",
                                ]
                              - displayMode = "table"
                              - placement   = "bottom"
                              - showLegend  = true
                            }
                          - tooltip = {
                              - mode = "multi"
                              - sort = "desc"
                            }
                        }
                      - targets     = [
                          - {
                              - datasource       = {
                                  - type = "cloudwatch"
                                  - uid  = "cfjasvpsecqo0c"
                                }
                              - dimensions       = {
                                  - DistributionId = "$distribution_id"
                                  - Region         = "Global"
                                }
                              - expression       = ""
                              - id               = ""
                              - legendFormat     = "4xxErrorRate"
                              - matchExact       = true
                              - metricEditorMode = 0
                              - metricName       = "4xxErrorRate"
                              - metricQueryType  = 0
                              - namespace        = "AWS/CloudFront"
                              - period           = "300"
                              - queryMode        = "Metrics"
                              - refId            = "A"
                              - region           = "us-east-1"
                              - statistic        = "Average"
                            },
                        ]
                      - title       = "4xx Error Rate"
                      - type        = "timeseries"
                    },
                  - {
                      - datasource  = {
                          - type = "cloudwatch"
                          - uid  = "cfjasvpsecqo0c"
                        }
                      - description = "Percentage (0-100) of viewer requests answered with a 5xx. Alerted on at > 5% for 15m, gated on more than 50 requests in the bucket so a single error in near-zero tb-dev traffic cannot trip it (severity=warning, Slack only) in terraform/alerting-appointment-edge.tf."
                      - fieldConfig = {
                          - defaults  = {
                              - color    = {
                                  - mode = "palette-classic"
                                }
                              - custom   = {
                                  - drawStyle         = "line"
                                  - fillOpacity       = 10
                                  - lineInterpolation = "smooth"
                                  - lineWidth         = 2
                                  - showPoints        = "never"
                                }
                              - mappings = []
                              - max      = 100
                              - min      = 0
                              - unit     = "percent"
                            }
                          - overrides = []
                        }
                      - gridPos     = {
                          - h = 8
                          - w = 12
                          - x = 12
                          - y = 16
                        }
                      - options     = {
                          - legend  = {
                              - calcs       = [
                                  - "lastNotNull",
                                  - "max",
                                ]
                              - displayMode = "table"
                              - placement   = "bottom"
                              - showLegend  = true
                            }
                          - tooltip = {
                              - mode = "multi"
                              - sort = "desc"
                            }
                        }
                      - targets     = [
                          - {
                              - datasource       = {
                                  - type = "cloudwatch"
                                  - uid  = "cfjasvpsecqo0c"
                                }
                              - dimensions       = {
                                  - DistributionId = "$distribution_id"
                                  - Region         = "Global"
                                }
                              - expression       = ""
                              - id               = ""
                              - legendFormat     = "5xxErrorRate"
                              - matchExact       = true
                              - metricEditorMode = 0
                              - metricName       = "5xxErrorRate"
                              - metricQueryType  = 0
                              - namespace        = "AWS/CloudFront"
                              - period           = "300"
                              - queryMode        = "Metrics"
                              - refId            = "A"
                              - region           = "us-east-1"
                              - statistic        = "Average"
                            },
                        ]
                      - title       = "5xx Error Rate"
                      - type        = "timeseries"
                    },
                  - {
                      - collapsed = false
                      - gridPos   = {
                          - h = 1
                          - w = 24
                          - x = 0
                          - y = 24
                        }
                      - panels    = []
                      - title     = "Cache and origin — requires the MonitoringSubscription"
                      - type      = "row"
                    },
                  - {
                      - datasource  = {
                          - type = "cloudwatch"
                          - uid  = "cfjasvpsecqo0c"
                        }
                      - description = "Percentage (0-100) of viewer requests served from the edge cache. THE success metric for this distribution. REQUIRES the per-distribution CloudFront MonitoringSubscription (an *additional* metric). This panel is legitimately empty until the Pulumi half of https://github.com/thunderbird/platform-infrastructure/issues/826 lands — it is not a collection failure."
                      - fieldConfig = {
                          - defaults  = {
                              - color    = {
                                  - mode = "palette-classic"
                                }
                              - custom   = {
                                  - drawStyle         = "line"
                                  - fillOpacity       = 10
                                  - lineInterpolation = "smooth"
                                  - lineWidth         = 2
                                  - showPoints        = "never"
                                }
                              - mappings = []
                              - max      = 100
                              - min      = 0
                              - unit     = "percent"
                            }
                          - overrides = []
                        }
                      - gridPos     = {
                          - h = 8
                          - w = 12
                          - x = 0
                          - y = 25
                        }
                      - options     = {
                          - legend  = {
                              - calcs       = [
                                  - "lastNotNull",
                                  - "max",
                                ]
                              - displayMode = "table"
                              - placement   = "bottom"
                              - showLegend  = true
                            }
                          - tooltip = {
                              - mode = "multi"
                              - sort = "desc"
                            }
                        }
                      - targets     = [
                          - {
                              - datasource       = {
                                  - type = "cloudwatch"
                                  - uid  = "cfjasvpsecqo0c"
                                }
                              - dimensions       = {
                                  - DistributionId = "$distribution_id"
                                  - Region         = "Global"
                                }
                              - expression       = ""
                              - id               = ""
                              - legendFormat     = "CacheHitRate"
                              - matchExact       = true
                              - metricEditorMode = 0
                              - metricName       = "CacheHitRate"
                              - metricQueryType  = 0
                              - namespace        = "AWS/CloudFront"
                              - period           = "300"
                              - queryMode        = "Metrics"
                              - refId            = "A"
                              - region           = "us-east-1"
                              - statistic        = "Average"
                            },
                        ]
                      - title       = "Cache Hit Rate"
                      - type        = "timeseries"
                    },
                  - {
                      - datasource  = {
                          - type = "cloudwatch"
                          - uid  = "cfjasvpsecqo0c"
                        }
                      - description = "Time from CloudFront receiving a request to returning the last byte to the edge for origin-bound requests. Only percentile statistics are valid for this metric. REQUIRES the per-distribution CloudFront MonitoringSubscription (an *additional* metric). This panel is legitimately empty until the Pulumi half of https://github.com/thunderbird/platform-infrastructure/issues/826 lands — it is not a collection failure."
                      - fieldConfig = {
                          - defaults  = {
                              - color    = {
                                  - mode = "palette-classic"
                                }
                              - custom   = {
                                  - drawStyle         = "line"
                                  - fillOpacity       = 10
                                  - lineInterpolation = "smooth"
                                  - lineWidth         = 2
                                  - showPoints        = "never"
                                }
                              - mappings = []
                              - min      = 0
                              - unit     = "ms"
                            }
                          - overrides = []
                        }
                      - gridPos     = {
                          - h = 8
                          - w = 12
                          - x = 12
                          - y = 25
                        }
                      - options     = {
                          - legend  = {
                              - calcs       = [
                                  - "lastNotNull",
                                  - "max",
                                ]
                              - displayMode = "table"
                              - placement   = "bottom"
                              - showLegend  = true
                            }
                          - tooltip = {
                              - mode = "multi"
                              - sort = "desc"
                            }
                        }
                      - targets     = [
                          - {
                              - datasource       = {
                                  - type = "cloudwatch"
                                  - uid  = "cfjasvpsecqo0c"
                                }
                              - dimensions       = {
                                  - DistributionId = "$distribution_id"
                                  - Region         = "Global"
                                }
                              - expression       = ""
                              - id               = ""
                              - legendFormat     = "p50"
                              - matchExact       = true
                              - metricEditorMode = 0
                              - metricName       = "OriginLatency"
                              - metricQueryType  = 0
                              - namespace        = "AWS/CloudFront"
                              - period           = "300"
                              - queryMode        = "Metrics"
                              - refId            = "A"
                              - region           = "us-east-1"
                              - statistic        = "p50"
                            },
                          - {
                              - datasource       = {
                                  - type = "cloudwatch"
                                  - uid  = "cfjasvpsecqo0c"
                                }
                              - dimensions       = {
                                  - DistributionId = "$distribution_id"
                                  - Region         = "Global"
                                }
                              - expression       = ""
                              - id               = ""
                              - legendFormat     = "p90"
                              - matchExact       = true
                              - metricEditorMode = 0
                              - metricName       = "OriginLatency"
                              - metricQueryType  = 0
                              - namespace        = "AWS/CloudFront"
                              - period           = "300"
                              - queryMode        = "Metrics"
                              - refId            = "B"
                              - region           = "us-east-1"
                              - statistic        = "p90"
                            },
                        ]
                      - title       = "Origin Latency (p50 / p90)"
                      - type        = "timeseries"
                    },
                ]
              - schemaVersion        = 39
              - tags                 = [
                  - "appointment",
                  - "cloudfront",
                  - "tb-dev",
                  - "edge",
                ]
              - templating           = {
                  - list = [
                      - {
                          - current    = {
                              - selected = false
                              - text     = ""
                              - value    = ""
                            }
                          - datasource = {
                              - type = "cloudwatch"
                              - uid  = "cfjasvpsecqo0c"
                            }
                          - definition = "dimension_values(us-east-1,AWS/CloudFront,Requests,DistributionId)"
                          - hide       = 0
                          - includeAll = false
                          - label      = "Distribution"
                          - multi      = false
                          - name       = "distribution_id"
                          - options    = []
                          - query      = {
                              - attributeName    = ""
                              - dimensionFilters = {}
                              - dimensionKey     = "DistributionId"
                              - ec2Filters       = {}
                              - instanceID       = ""
                              - metricName       = "Requests"
                              - namespace        = "AWS/CloudFront"
                              - queryType        = "dimensionValues"
                              - refId            = "CloudWatchVariableQueryEditor-VariableQuery"
                              - region           = "us-east-1"
                              - resourceType     = ""
                              - tags             = {}
                            }
                          - refresh    = 1
                          - regex      = ""
                          - sort       = 1
                          - type       = "query"
                        },
                    ]
                }
              - time                 = {
                  - from = "now-24h"
                  - to   = "now"
                }
              - timepicker           = {}
              - timezone             = ""
              - title                = "Appointment — CloudFront Edge (tb-dev)"
              - uid                  = "appointment-cloudfront-edge"
            }
        ) -> null
      - dashboard_id = 44 -> null
      - folder       = "efvkkljbxrugwe" -> null
      - id           = "0:appointment-cloudfront-edge" -> null
      - org_id       = "0" -> null
      - uid          = "appointment-cloudfront-edge" -> null
      - url          = "https://grafana.pi.thunderbird.net/d/appointment-cloudfront-edge/appointment-e28094-cloudfront-edge-tb-dev" -> null
      - version      = 1 -> null
    }

  # grafana_folder.appointment will be destroyed
  # (because grafana_folder.appointment is not in configuration)
  - resource "grafana_folder" "appointment" {
      - id                           = "0:efvkkljbxrugwe" -> null
      - org_id                       = "0" -> null
      - prevent_destroy_if_not_empty = false -> null
      - title                        = "Appointment" -> null
      - uid                          = "efvkkljbxrugwe" -> null
      - url                          = "https://grafana.pi.thunderbird.net/dashboards/f/efvkkljbxrugwe/appointment" -> null
        # (1 unchanged attribute hidden)
    }

  # grafana_rule_group.appointment_edge_public_alb_cert_expiry will be destroyed
  # (because grafana_rule_group.appointment_edge_public_alb_cert_expiry is not in configuration)
  - resource "grafana_rule_group" "appointment_edge_public_alb_cert_expiry" {
      - disable_provenance = true -> null
      - folder_uid         = "efvkkljbxrugwe" -> null
      - id                 = "0:efvkkljbxrugwe:appointment-edge-public-alb-cert-expiry" -> null
      - interval_seconds   = 600 -> null
      - name               = "appointment-edge-public-alb-cert-expiry" -> null
      - org_id             = "1" -> null

      - rule {
          - annotations                     = {
              - "description" = "ACM certificate d324b88c-174e-4264-81af-a017c981b1a1 (eu-central-1, minted by the AWS Load Balancer Controller for the LIVE PUBLIC appointment Ingress on mzla-eks-tb-dev01, host appointment.tb-dev.thunderbird.dev) has fewer than 21 days to expiry, so managed renewal has not completed. This is NOT the CloudFront origin cert — see AppointmentOriginAlbCertExpiring for that one. It matters because it co-holds the single Route53 DNS-validation CNAME that the us-east-1 CloudFront viewer cert renews against, so one missing record breaks renewal on both (platform-infrastructure #816): check that validation CNAME first, and check that an Ingress still serves appointment.tb-dev.thunderbird.dev, because the LB Controller garbage-collects that record if none does. If this fired as NoData instead, the certificate ARN pinned in terraform/alerting-appointment-edge.tf has stopped publishing DaysToExpiry: the load balancer controller has almost certainly re-minted the cert under a new ARN, and the pin must be updated before this rule means anything again."
              - "runbook_url" = "https://github.com/thunderbird/platform-infrastructure/issues/826"
              - "summary"     = "appointment tb-dev PUBLIC ALB certificate expires in < 21 days, or is no longer being measured"
            } -> null
          - condition                       = "C" -> null
          - exec_err_state                  = "OK" -> null
          - for                             = "1h0m0s" -> null
          - is_paused                       = false -> null
          - labels                          = {
              - "cluster"  = "mzla-eks-tb-dev01"
              - "service"  = "appointment"
              - "severity" = "warning"
            } -> null
          - missing_series_evals_to_resolve = 0 -> null
          - name                            = "AppointmentPublicAlbCertExpiring" -> null
          - no_data_state                   = "Alerting" -> null
          - uid                             = "dfvkkljlsdfk0b" -> null
            # (1 unchanged attribute hidden)

          - data {
              - datasource_uid = "cfjasvpsecqo0c" -> null
              - model          = jsonencode(
                    {
                      - datasource       = {
                          - type = "cloudwatch"
                          - uid  = "cfjasvpsecqo0c"
                        }
                      - dimensions       = {
                          - CertificateArn = "arn:aws:acm:eu-central-1:718959508124:certificate/d324b88c-174e-4264-81af-a017c981b1a1"
                        }
                      - expression       = ""
                      - id               = ""
                      - matchExact       = true
                      - metricEditorMode = 0
                      - metricName       = "DaysToExpiry"
                      - metricQueryType  = 0
                      - namespace        = "AWS/CertificateManager"
                      - period           = "86400"
                      - queryMode        = "Metrics"
                      - refId            = "A"
                      - region           = "eu-central-1"
                      - statistic        = "Minimum"
                    }
                ) -> null
              - ref_id         = "A" -> null
                # (1 unchanged attribute hidden)

              - relative_time_range {
                  - from = 172800 -> null
                  - to   = 0 -> null
                }
            }
          - data {
              - datasource_uid = "__expr__" -> null
              - model          = jsonencode(
                    {
                      - datasource = {
                          - type = "__expr__"
                          - uid  = "__expr__"
                        }
                      - expression = "A"
                      - reducer    = "last"
                      - refId      = "B"
                      - type       = "reduce"
                    }
                ) -> null
              - ref_id         = "B" -> null
                # (1 unchanged attribute hidden)

              - relative_time_range {
                  - from = 172800 -> null
                  - to   = 0 -> null
                }
            }
          - data {
              - datasource_uid = "__expr__" -> null
              - model          = jsonencode(
                    {
                      - conditions = [
                          - {
                              - evaluator = {
                                  - params = [
                                      - 21,
                                    ]
                                  - type   = "lt"
                                }
                              - operator  = {
                                  - type = "and"
                                }
                              - query     = {
                                  - params = [
                                      - "C",
                                    ]
                                }
                              - reducer   = {
                                  - params = []
                                  - type   = "last"
                                }
                              - type      = "query"
                            },
                        ]
                      - datasource = {
                          - type = "__expr__"
                          - uid  = "__expr__"
                        }
                      - expression = "B"
                      - refId      = "C"
                      - type       = "threshold"
                    }
                ) -> null
              - ref_id         = "C" -> null
                # (1 unchanged attribute hidden)

              - relative_time_range {
                  - from = 172800 -> null
                  - to   = 0 -> null
                }
            }
        }
    }

  # grafana_rule_group.deadman_heartbeat will be created
  + resource "grafana_rule_group" "deadman_heartbeat" {
      + disable_provenance = true
      + folder_uid         = "bfhbgu9i6fjswa"
      + id                 = (known after apply)
      + interval_seconds   = 30
      + name               = "deadman-heartbeat"

      + rule {
          + annotations    = {
              + "description" = "Emits a heartbeat to the AWS-native deadman watcher every cycle. If this stops, the watcher pages on-call. Do NOT pause or delete this rule (issue 560). Runbook: docs/observability.md '## Alerting & Paging' -> Deadman."
              + "runbook_url" = "https://github.com/thunderbird/platform-infrastructure/blob/main/docs/observability.md#deadman--self-monitoring"
              + "summary"     = "Grafana alerting deadman heartbeat (always firing while healthy)"
            }
          + condition      = "C"
          + exec_err_state = "Error"
          + is_paused      = false
          + labels         = {
              + "deadman"  = "true"
              + "severity" = "heartbeat"
            }
          + name           = "GrafanaAlertingDeadmanHeartbeat"
          + no_data_state  = "OK"
          + uid            = (known after apply)

          + data {
              + datasource_uid = "P4169E866C3094E38"
              + model          = jsonencode(
                    {
                      + datasource = {
                          + type = "prometheus"
                          + uid  = "P4169E866C3094E38"
                        }
                      + expr       = "vector(1)"
                      + instant    = true
                      + refId      = "A"
                    }
                )
              + ref_id         = "A"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
          + data {
              + datasource_uid = "__expr__"
              + model          = jsonencode(
                    {
                      + datasource = {
                          + type = "__expr__"
                          + uid  = "__expr__"
                        }
                      + expression = "A"
                      + reducer    = "last"
                      + refId      = "B"
                      + type       = "reduce"
                    }
                )
              + ref_id         = "B"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
          + data {
              + datasource_uid = "__expr__"
              + model          = jsonencode(
                    {
                      + conditions = [
                          + {
                              + evaluator = {
                                  + params = [
                                      + 0,
                                    ]
                                  + type   = "gt"
                                }
                              + operator  = {
                                  + type = "and"
                                }
                              + query     = {
                                  + params = [
                                      + "C",
                                    ]
                                }
                              + reducer   = {
                                  + params = []
                                  + type   = "last"
                                }
                              + type      = "query"
                            },
                        ]
                      + datasource = {
                          + type = "__expr__"
                          + uid  = "__expr__"
                        }
                      + expression = "B"
                      + refId      = "C"
                      + type       = "threshold"
                    }
                )
              + ref_id         = "C"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
        }
    }

Plan: 1 to add, 0 to change, 3 to destroy.
╷
│ Error: reading Secrets Manager Secret Version (mzla/shared-services/grafana-terraform-deadman-url|AWSCURRENT): couldn't find resource
│ 
│   with data.aws_secretsmanager_secret_version.deadman_ingest,
│   on deadman.tf line 25, in data "aws_secretsmanager_secret_version" "deadman_ingest":
│   25: data "aws_secretsmanager_secret_version" "deadman_ingest" {
│ 
╵

@aatchison
aatchison force-pushed the feat/560-deadman-heartbeat-20260623-101530 branch from de0fb4c to 5e6930d Compare August 18, 2026 16:48
@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis plan

@mzla-atlantis

mzla-atlantis Bot commented Aug 18, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Error

cannot run "plan": the default workspace at path terraform is currently locked for this pull request by "plan".
Wait until the previous command is complete and try again

@mzla-atlantis

mzla-atlantis Bot commented Aug 18, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Error

Show Output
running 'sh -c' '/home/atlantis/.atlantis/bin/terraform1.15.8 plan -input=false -refresh -out "/home/atlantis/.atlantis/repos/thunderbird/platform-grafana/25/default/terraform/grafana-default.tfplan"' in '/home/atlantis/.atlantis/repos/thunderbird/platform-grafana/25/default/terraform': exit status 1
╷
│ Warning: Deprecated Parameter
│ 
│ The parameter "dynamodb_table" is deprecated. Use parameter "use_lockfile"
│ instead.
╵
data.aws_secretsmanager_secret_version.grafana: Reading...
data.aws_secretsmanager_secret_version.pagerduty_routing_key: Reading...
data.aws_secretsmanager_secret_version.deadman_ingest: Reading...
data.aws_secretsmanager_secret_version.pagerduty_routing_key: Read complete after 1s [id=mzla/shared-services/pagerduty-routing-key|AWSCURRENT]
data.aws_secretsmanager_secret_version.grafana: Read complete after 1s [id=mzla/shared-services/grafana-terraform|AWSCURRENT]
grafana_folder.twenty: Refreshing state... [id=0:afjpnfj8kg4qoe]
grafana_folder.victorialogs: Refreshing state... [id=0:cftic1vmdaolca]
grafana_folder.appointment: Refreshing state... [id=0:efvkkljbxrugwe]
grafana_folder.bitergia: Refreshing state... [id=0:bfilh3aq4kh6oa]
grafana_folder.kargo: Refreshing state... [id=0:ffvjq5wczs934e]
grafana_folder.victoriametrics: Refreshing state... [id=0:ffhbgu8hreubka]
grafana_folder.teleport: Refreshing state... [id=0:afhbgu9xft7uob]
grafana_folder.traefik: Refreshing state... [id=0:efhbguai6gv7ka]
grafana_folder.core_services: Refreshing state... [id=0:bfhbgu9i6fjswa]
grafana_folder.argocd: Refreshing state... [id=0:efhbgu7k4a2o0b]
grafana_data_source.cloudwatch_shared_control_plane: Refreshing state... [id=1:efhx0itiz8t1cd]
grafana_folder.discourse: Refreshing state... [id=0:efld6rtum1xxcc]
grafana_data_source.cloudwatch_tb_prod: Refreshing state... [id=1:dfjasvpsjcjcwf]
grafana_folder.keycloak: Refreshing state... [id=0:dfhbgu8xprhtsb]
grafana_folder.kubernetes: Refreshing state... [id=0:dfhbgu7yr6nlsc]
grafana_data_source.cloudwatch_shared: Refreshing state... [id=1:cfhwxlv6bqepse]
grafana_data_source.cloudwatch_workloads: Refreshing state... [id=1:efhwxlv825vcwc]
grafana_data_source.cloudwatch_tb_dev: Refreshing state... [id=1:cfjasvpsecqo0c]
grafana_contact_point.pagerduty_platform_infra_low: Refreshing state... [id=0:pagerduty-platform-infra-low]
grafana_contact_point.pagerduty_platform_infra: Refreshing state... [id=0:pagerduty-platform-infra]
grafana_dashboard.traefik_overview: Refreshing state... [id=0:traefik-overview]
grafana_dashboard.bitergia_infrastructure: Refreshing state... [id=0:bitergia-infrastructure]
grafana_rule_group.catalog_victorialogs: Refreshing state... [id=0:cftic1vmdaolca:catalog-victorialogs-paging]
grafana_rule_group.catalog_kargo: Refreshing state... [id=0:ffvjq5wczs934e:catalog-kargo-verification]
grafana_dashboard.vm_victorialogs: Refreshing state... [id=0:vm-victorialogs]
grafana_rule_group.catalog_argocd: Refreshing state... [id=1:efhbgu7k4a2o0b:catalog-argocd-paging]
grafana_dashboard.teleport_backend_audit: Refreshing state... [id=0:teleport-backend-audit]
grafana_rule_group.catalog_teleport: Refreshing state... [id=0:afhbgu9xft7uob:catalog-teleport-paging]
grafana_rule_group.catalog_victoriametrics: Refreshing state... [id=0:ffhbgu8hreubka:catalog-victoriametrics-paging]
grafana_dashboard.vm_vmagent: Refreshing state... [id=0:vm-vmagent]
grafana_dashboard.vm_cluster_overview: Refreshing state... [id=0:vm-cluster-overview]
grafana_rule_group.catalog_vmauth: Refreshing state... [id=0:ffhbgu8hreubka:catalog-vmauth-paging]
grafana_rule_group.tailscale: Refreshing state... [id=1:bfhbgu9i6fjswa:tailscale-operator]
grafana_dashboard.twenty_overview: Refreshing state... [id=0:twenty-overview]
grafana_dashboard.argocd_application_overview: Refreshing state... [id=0:argocd-application-overview]
grafana_dashboard.argocd_operational_overview: Refreshing state... [id=0:argocd-operational-overview]
grafana_dashboard.teleport_sessions_connections: Refreshing state... [id=0:teleport-sessions-connections]
grafana_rule_group.catalog_traefik: Refreshing state... [id=0:efhbguai6gv7ka:catalog-traefik-paging]
grafana_rule_group.catalog_eso: Refreshing state... [id=0:bfhbgu9i6fjswa:catalog-eso-paging]
grafana_dashboard.external_dns: Refreshing state... [id=0:external-dns]
grafana_rule_group.euc1_cert_expiry: Refreshing state... [id=1:bfhbgu9i6fjswa:cert-expiry-euc1]
grafana_dashboard.external_secrets_operator: Refreshing state... [id=0:external-secrets-operator]
grafana_dashboard.thundermail_ticket_spike_monitor: Refreshing state... [id=0:thundermail-ticket-spike-monitor]
grafana_dashboard.bamboohr_cal_sync: Refreshing state... [id=0:bamboohr-cal-sync]
grafana_dashboard.cert_manager: Refreshing state... [id=0:cert-manager]
grafana_dashboard.keycloak_overview: Refreshing state... [id=0:keycloak-overview]
grafana_rule_group.keycloak_dr_backup: Refreshing state... [id=1:dfhbgu8xprhtsb:keycloak-dr-backup]
grafana_dashboard.aws_load_balancer_controller: Refreshing state... [id=0:aws-load-balancer-controller]
grafana_dashboard.discourse_overview: Refreshing state... [id=0:discourse-overview]
grafana_rule_group.euc1_federation_ha: Refreshing state... [id=0:dfhbgu8xprhtsb:euc1-federation-ha-dr]
grafana_rule_group.catalog_keycloak: Refreshing state... [id=1:dfhbgu8xprhtsb:catalog-keycloak-paging]
grafana_rule_group.appointment_edge_public_alb_cert_expiry: Refreshing state... [id=0:efvkkljbxrugwe:appointment-edge-public-alb-cert-expiry]
grafana_dashboard.k8s_pod_container_resources: Refreshing state... [id=0:k8s-pod-container-resources]
grafana_dashboard.k8s_persistent_volumes: Refreshing state... [id=0:k8s-persistent-volumes]
grafana_rule_group.catalog_kubernetes: Refreshing state... [id=1:dfhbgu7yr6nlsc:catalog-kubernetes-paging]
grafana_dashboard.k8s_cluster_overview: Refreshing state... [id=0:k8s-cluster-overview]
grafana_dashboard.appointment_cloudfront_edge: Refreshing state... [id=0:appointment-cloudfront-edge]
grafana_dashboard.k8s_coredns: Refreshing state... [id=0:k8s-coredns]
grafana_dashboard.k8s_namespace_breakdown: Refreshing state... [id=0:k8s-namespace-breakdown]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform planned the following actions, but then encountered a problem:

  # grafana_rule_group.deadman_heartbeat will be created
  + resource "grafana_rule_group" "deadman_heartbeat" {
      + disable_provenance = true
      + folder_uid         = "bfhbgu9i6fjswa"
      + id                 = (known after apply)
      + interval_seconds   = 30
      + name               = "deadman-heartbeat"

      + rule {
          + annotations    = {
              + "description" = "Emits a heartbeat to the AWS-native deadman watcher every cycle. If this stops, the watcher pages on-call. Do NOT pause or delete this rule (issue 560). Runbook: docs/observability.md '## Alerting & Paging' -> Deadman."
              + "runbook_url" = "https://github.com/thunderbird/platform-infrastructure/blob/main/docs/observability.md#deadman--self-monitoring"
              + "summary"     = "Grafana alerting deadman heartbeat (always firing while healthy)"
            }
          + condition      = "C"
          + exec_err_state = "Error"
          + is_paused      = false
          + labels         = {
              + "deadman"  = "true"
              + "severity" = "heartbeat"
            }
          + name           = "GrafanaAlertingDeadmanHeartbeat"
          + no_data_state  = "OK"
          + uid            = (known after apply)

          + data {
              + datasource_uid = "P4169E866C3094E38"
              + model          = jsonencode(
                    {
                      + datasource = {
                          + type = "prometheus"
                          + uid  = "P4169E866C3094E38"
                        }
                      + expr       = "vector(1)"
                      + instant    = true
                      + refId      = "A"
                    }
                )
              + ref_id         = "A"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
          + data {
              + datasource_uid = "__expr__"
              + model          = jsonencode(
                    {
                      + datasource = {
                          + type = "__expr__"
                          + uid  = "__expr__"
                        }
                      + expression = "A"
                      + reducer    = "last"
                      + refId      = "B"
                      + type       = "reduce"
                    }
                )
              + ref_id         = "B"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
          + data {
              + datasource_uid = "__expr__"
              + model          = jsonencode(
                    {
                      + conditions = [
                          + {
                              + evaluator = {
                                  + params = [
                                      + 0,
                                    ]
                                  + type   = "gt"
                                }
                              + operator  = {
                                  + type = "and"
                                }
                              + query     = {
                                  + params = [
                                      + "C",
                                    ]
                                }
                              + reducer   = {
                                  + params = []
                                  + type   = "last"
                                }
                              + type      = "query"
                            },
                        ]
                      + datasource = {
                          + type = "__expr__"
                          + uid  = "__expr__"
                        }
                      + expression = "B"
                      + refId      = "C"
                      + type       = "threshold"
                    }
                )
              + ref_id         = "C"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
╷
│ Error: reading Secrets Manager Secret Version (mzla/shared-services/grafana-terraform-deadman-url|AWSCURRENT): couldn't find resource
│ 
│   with data.aws_secretsmanager_secret_version.deadman_ingest,
│   on deadman.tf line 25, in data "aws_secretsmanager_secret_version" "deadman_ingest":
│   25: data "aws_secretsmanager_secret_version" "deadman_ingest" {
│ 
╵

Adds the Grafana side of the alerting deadman/heartbeat:

- terraform/deadman.tf: a grafana_rule_group `deadman-heartbeat` whose query A
  is `vector(1)` executed AGAINST the VictoriaMetrics datasource by UID (not a
  pure in-process __expr__ node) so a stopped heartbeat proves ruler +
  datasource + delivery (plan B1). Labels deadman=true, severity=heartbeat
  (deliberately NOT page|critical). Plus a webhook contact point `deadman-ingest`
  that POSTs each heartbeat to the off-cluster AWS watcher's ingest URL with a
  bearer token; URL+token read from Secrets Manager
  mzla/shared-services/grafana-terraform-deadman-url (named under the
  grafana-terraform* prefix so the Atlantis task role reads it with NO IAM
  change — plan B2).
- terraform/alerting.tf: a nested deadman=true route added to the existing
  grafana_notification_policy.root singleton (sibling of the page|critical and
  warning/ticket routes — NOT re-created; existing routes preserved). Tight
  timers (group_wait 0s, group/repeat 1m). The notification policy root is NOT
  otherwise modified.

terraform fmt clean. The AWS-native watcher is in platform-infrastructure
pulumi/environments/grafana-deadman/ (separate PR).

Nothing applied (no atlantis apply). Sequence: build+populate the watcher
ingest secret first, then apply this.

Refs thunderbird/platform-infrastructure#560

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aatchison
aatchison force-pushed the feat/560-deadman-heartbeat-20260623-101530 branch from 5e6930d to 9f85d06 Compare August 19, 2026 05:31
@mzla-atlantis

mzla-atlantis Bot commented Aug 19, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Failed: This project is currently locked by an unapplied plan from pull #38. To continue, delete the lock from #38 or apply that plan and merge the pull request.

Once the lock is released, comment atlantis plan here to re-plan.

@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis plan

@mzla-atlantis

mzla-atlantis Bot commented Aug 19, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Failed: This project is currently locked by an unapplied plan from pull #38. To continue, delete the lock from #38 or apply that plan and merge the pull request.

Once the lock is released, comment atlantis plan here to re-plan.

@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis plan

@mzla-atlantis

mzla-atlantis Bot commented Aug 19, 2026

Copy link
Copy Markdown

Ran Plan for dir: terraform workspace: default

Plan Failed: This project is currently locked by an unapplied plan from pull #38. To continue, delete the lock from #38 or apply that plan and merge the pull request.

Once the lock is released, comment atlantis plan here to re-plan.

@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis plan

@mzla-atlantis

mzla-atlantis Bot commented Aug 19, 2026

Copy link
Copy Markdown

Ran Plan for project: grafana dir: terraform workspace: default

Show Output
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
~ update in-place

Terraform will perform the following actions:

  # grafana_contact_point.deadman_ingest will be created
+ resource "grafana_contact_point" "deadman_ingest" {
      + disable_provenance = true
      + id                 = (known after apply)
      + name               = "deadman-ingest"

      + webhook {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
    }

  # grafana_notification_policy.root will be updated in-place
~ resource "grafana_notification_policy" "root" {
        id                 = "0:policy"
        # (7 unchanged attributes hidden)

      + policy {
          + contact_point   = "deadman-ingest"
          + continue        = false
          + group_by        = [
              + "alertname",
            ]
          + group_interval  = "1m"
          + group_wait      = "0s"
          + repeat_interval = "1m"

          + matcher {
              + label = "deadman"
              + match = "="
              + value = "true"
            }
        }

        # (4 unchanged blocks hidden)
    }

  # grafana_rule_group.deadman_heartbeat will be created
+ resource "grafana_rule_group" "deadman_heartbeat" {
      + disable_provenance = true
      + folder_uid         = "bfhbgu9i6fjswa"
      + id                 = (known after apply)
      + interval_seconds   = 30
      + name               = "deadman-heartbeat"

      + rule {
          + annotations    = {
              + "description" = "Emits a heartbeat to the AWS-native deadman watcher every cycle. If this stops, the watcher pages on-call. Do NOT pause or delete this rule (issue 560). Runbook: docs/observability.md '## Alerting & Paging' -> Deadman."
              + "runbook_url" = "https://github.com/thunderbird/platform-infrastructure/blob/main/docs/observability.md#deadman--self-monitoring"
              + "summary"     = "Grafana alerting deadman heartbeat (always firing while healthy)"
            }
          + condition      = "C"
          + exec_err_state = "Error"
          + is_paused      = false
          + labels         = {
              + "deadman"  = "true"
              + "severity" = "heartbeat"
            }
          + name           = "GrafanaAlertingDeadmanHeartbeat"
          + no_data_state  = "OK"
          + uid            = (known after apply)

          + data {
              + datasource_uid = "P4169E866C3094E38"
              + model          = jsonencode(
                    {
                      + datasource = {
                          + type = "prometheus"
                          + uid  = "P4169E866C3094E38"
                        }
                      + expr       = "vector(1)"
                      + instant    = true
                      + refId      = "A"
                    }
                )
              + ref_id         = "A"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
          + data {
              + datasource_uid = "__expr__"
              + model          = jsonencode(
                    {
                      + datasource = {
                          + type = "__expr__"
                          + uid  = "__expr__"
                        }
                      + expression = "A"
                      + reducer    = "last"
                      + refId      = "B"
                      + type       = "reduce"
                    }
                )
              + ref_id         = "B"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
          + data {
              + datasource_uid = "__expr__"
              + model          = jsonencode(
                    {
                      + conditions = [
                          + {
                              + evaluator = {
                                  + params = [
                                      + 0,
                                    ]
                                  + type   = "gt"
                                }
                              + operator  = {
                                  + type = "and"
                                }
                              + query     = {
                                  + params = [
                                      + "C",
                                    ]
                                }
                              + reducer   = {
                                  + params = []
                                  + type   = "last"
                                }
                              + type      = "query"
                            },
                        ]
                      + datasource = {
                          + type = "__expr__"
                          + uid  = "__expr__"
                        }
                      + expression = "B"
                      + refId      = "C"
                      + type       = "threshold"
                    }
                )
              + ref_id         = "C"

              + relative_time_range {
                  + from = 600
                  + to   = 0
                }
            }
        }
    }

Plan: 2 to add, 1 to change, 0 to destroy.
  • ▶️ To apply this plan, comment:
    atlantis apply -p grafana
  • 🚮 To delete this plan and lock, click here
  • 🔁 To plan this project again, comment:
    atlantis plan -p grafana

Plan: 2 to add, 1 to change, 0 to destroy.


  • ⏩ To apply all unapplied plans from this Pull Request, comment:
    atlantis apply
  • 🚮 To delete all plans and locks from this Pull Request, comment:
    atlantis unlock

@aatchison

Copy link
Copy Markdown
Contributor Author

atlantis apply

@mzla-atlantis

mzla-atlantis Bot commented Aug 19, 2026

Copy link
Copy Markdown

Ran Apply for project: grafana dir: terraform workspace: default

Show Output
╷
│ Warning: Deprecated Parameter
│ 
│ The parameter "dynamodb_table" is deprecated. Use parameter "use_lockfile"
│ instead.
╵
grafana_rule_group.deadman_heartbeat: Creating...
grafana_contact_point.deadman_ingest: Creating...
grafana_contact_point.deadman_ingest: Creation complete after 0s [id=0:deadman-ingest]
grafana_notification_policy.root: Modifying... [id=0:policy]
grafana_rule_group.deadman_heartbeat: Creation complete after 0s [id=0:bfhbgu9i6fjswa:deadman-heartbeat]
grafana_notification_policy.root: Modifications complete after 0s [id=0:policy]

Apply complete! Resources: 2 added, 1 changed, 0 destroyed.

@aatchison
aatchison merged commit 7aad9b3 into main Aug 19, 2026
9 checks passed
@aatchison
aatchison deleted the feat/560-deadman-heartbeat-20260623-101530 branch August 19, 2026 05:50
@mzla-atlantis

mzla-atlantis Bot commented Aug 19, 2026

Copy link
Copy Markdown

Locks and plans deleted for the projects and workspaces modified in this pull request:

  • dir: terraform workspace: default

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants