UptimeHunt Docs
Probes

Kubernetes Auto-Discovery

Automatically turn Kubernetes Ingress resources into UptimeHunt monitoring checks using the auto-discovery operator.

Kubernetes Auto-Discovery

The UptimeHunt Kubernetes auto-discovery operator runs inside your cluster and automatically turns your Ingress resources into UptimeHunt monitoring checks. Deploy it once with an API token and every public endpoint your cluster exposes is monitored — no need to add each URL by hand, and new services are picked up as soon as their Ingress is created.

It is designed to be safe by default: it only ever adds and updates checks, and never deletes them automatically (a disappearing endpoint is often a misconfiguration you'd want to be alerted about, not silently un-monitored).

How It Works

For every Ingress it is allowed to see, the operator:

  1. Reads the hostnames and paths from spec.rules.
  2. Determines HTTP vs HTTPS from spec.tls (a host covered by TLS — including wildcard certificates — is monitored over HTTPS).
  3. Creates one HTTP check per unique host + path combination.
  4. Keeps those checks in sync as the Ingress changes (hosts/paths added, annotations updated).

Discovered checks appear in your UptimeHunt account grouped into a project named after the Kubernetes namespace (by default), tagged as Managed in the UI.

Wildcard hosts are skipped

A wildcard host such as *.example.com is not a concrete, probeable URL, so no check is created for it. Use a specific hostname instead.

Only Exact and Prefix paths are monitored

The operator creates checks only for ingress paths whose pathType is Exact or Prefix. ImplementationSpecific paths — which may contain controller-specific regex or rewrite syntax rather than a literal URL path — are skipped, because their effective probe URL can't be determined reliably. Use an Exact/Prefix path (or the path annotation) if you need such an endpoint monitored.

GET by default; method and expected status are annotation-driven

The operator creates plain HTTP GET checks by default. The method annotation overrides the request method (e.g. POST, to monitor a webhook receiver), and expect-status overrides which status codes count as healthy. Custom request headers and authentication are not yet mapped from the Ingress — configure those manually in the UptimeHunt UI after the check is discovered.

Recreated Ingresses are adopted, not duplicated

Deleting and re-applying an Ingress (a new UID for the same host/path — common in GitOps/CI reconciles) does not mint a duplicate check or orphan the old one. See Ingress-Recreation Adoption below.

Ingress-Recreation Adoption

Each discovered check is stamped with an external_id of <ingress-uid>|<host>|<path> so the dashboard can upsert it idempotently. Because that id embeds the Ingress's Kubernetes UID, a naive implementation would break the moment an Ingress is deleted and re-applied — a GitOps sync, a Helm upgrade that recreates the object, or a CI reconcile all mint a new UID for the same logical host/path, which would otherwise look like a brand-new check to monitor and leave the old one behind as an orphan.

Instead, when a desired check's external_id matches no existing service, the operator looks for a managed service with the same <host>|<path> tail under a different UID and re-keys it in place — a PATCH of just the external_id field. The existing check keeps its:

  • Service id
  • Check history
  • Incident links

No duplicate is created, and nothing needs to be manually re-linked.

Only a genuinely gone Ingress is adopted

Adoption only claims a check whose owning Ingress is verifiably gone from the cluster. Two Ingresses that are simultaneously live and happen to share one host/path — the standard nginx-ingress stable + canary pair, for example — are never mistaken for orphans: each keeps its own check, and neither ever steals the other's. If several deleted incarnations of an Ingress share the same host/path, the most-recently-modified enabled one is adopted, and a warning is logged.

Re-keying never resumes a paused check — a check you've paused stays paused across an adoption.

The required RBAC/API capability is covered by the same Automation service-token preset used for everything else this operator does (see Prerequisites): re-keying uses PATCH /services/{id} under the service.edit capability, so no additional token permission is needed.

Prerequisites

  • A Kubernetes cluster (the operator is installed with Helm).
  • An UptimeHunt organization service token, minted under Settings → Tokens → Service tokens with the automation preset. This determines which organization the discovered checks belong to.

Use an org service token, not a personal token

A personal API token works, but an organization service token is the right credential for an operator: it is pinned to exactly one organization, its capabilities are scoped to the monitoring inventory (no member-management or SSO access), and it survives team changes. See Service Account / Organization Tokens.

Installation

Create a values file, then install with Helm. Store the token in a Kubernetes Secret and reference it with existingSecret to keep it out of version control (see Service Account Tokens — Kubernetes):

values.yaml
saas:
  url: https://app.uptimehunt.io/api/v1
  existingSecret: uptimehunt-token   # Kubernetes Secret name
  existingSecretKey: token           # key inside the Secret

discovery:
  mode: annotation-only            # or "all"

watch:
  scope: cluster                   # or "namespace"
helm install uptimehunt-kubediscovery \
  oci://ohcr.io/uptimehunt/autodiscovery/kubernetes/charts/uptimehunt-kubediscovery \
  --version 0.1.0 \
  --namespace uptimehunt-kubediscovery --create-namespace \
  -f values.yaml

Pin a released version

The OCI chart is published only when a version is tagged in the repository: the release pipeline packages the chart with its version and appVersion set to the tag, and the matching operator image is pushed as :<version>. Always pass --version <X.Y.Z> with a released version (replace the 0.1.0 placeholder above) — an unpinned or latest install is not guaranteed to resolve until a release exists.

Discovery Modes

Choose how the operator decides which Ingresses to monitor with discovery.mode:

Only Ingresses explicitly opted in are monitored:

kubectl annotate ingress my-app monitor.uptimehunt.io/enabled=true

Best for selectively monitoring a few endpoints, and the safest default for a shared cluster.

Every discoverable Ingress is monitored automatically. Opt an individual Ingress out with:

kubectl annotate ingress internal-only monitor.uptimehunt.io/enabled=false

Best when you want comprehensive coverage of everything the cluster exposes.

You can also restrict which ingress classes are considered, to keep internal-only ingresses out:

values.yaml
discovery:
  ingressClassAllow: ["public"]      # only these classes
  # or
  ingressClassDeny: ["internal"]     # everything except these

Annotations

Fine-tune individual checks with annotations under the monitor.uptimehunt.io/ prefix:

AnnotationDescription
enabledOpt in/out ("true" / "false"; meaning depends on the discovery mode)
nameOverride the check's display name
groupTarget project/group (defaults to the namespace name)
intervalCheck interval — whole minutes ("5") or a duration ("90s", "2m")
pathForce a single probe path for all hosts on this Ingress
schemeForce http or https when TLS auto-detection isn't right
methodHTTP method for the probe (default GET). Set POST (etc.) to monitor a webhook receiver that only answers POST — a GET there returns 404/405.
expect-statusHTTP status codes that count as healthy, replacing the default "2xx/3xx is up" rule with an exact-match assertion. Two forms — see Expect Status: Per-Path Codes below: a bare comma-separated list (e.g. "403") applies to every path on the Ingress; a ;-separated per-path map (e.g. "/hook=403;/health=200,204;500") sets different codes for different paths, with an optional bare entry as the default for any unmapped path.
iconCustom icon URL for the check. By default the UI shows the monitored site's favicon; set this (or edit it in the UI) to override.
allow-removal"true" lets the operator delete this Ingress's checks when paths are removed or it's opted out (see Check Removal). Can be set at the namespace level and overridden per-Ingress with "false".

Example:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    monitor.uptimehunt.io/enabled: "true"
    monitor.uptimehunt.io/name: "My App (production)"
    monitor.uptimehunt.io/interval: "1m"
    monitor.uptimehunt.io/path: "/healthz"

To monitor a webhook receiver the way the upstream provider hits it — a POST that's rejected with 403 when unsigned (meaning the endpoint is alive and verifying signatures) — probe it with POST and treat 403 as healthy:

metadata:
  annotations:
    monitor.uptimehunt.io/path: "/api/webhook/slack/interactivity"
    monitor.uptimehunt.io/method: "POST"
    monitor.uptimehunt.io/expect-status: "403"

Expect Status: Per-Path Codes

A single Ingress often exposes several paths that need different healthy-status rules — a webhook receiver that only accepts signed POST requests (403 = healthy) alongside a plain /health endpoint (200/204 = healthy). expect-status supports two forms, and stays backward compatible with every existing config:

  • Legacy / bare list — a plain comma-separated list of codes, e.g. "403,404", applies to every path discovered on the Ingress. Existing configs using this form are unaffected.
  • Per-path map — a ;-separated list of path=codes entries, e.g. "/hook=403;/health=200,204;500", sets the healthy codes independently per path (paths are normalized before matching). An entry with no = (like the trailing 500 above) is a bare default applied to any discovered path that isn't otherwise mapped.
metadata:
  annotations:
    monitor.uptimehunt.io/expect-status: "/hook=403;/health=200,204;500"

In this example: the /hook check treats 403 as healthy, /health treats 200 or 204 as healthy, and any other path on the Ingress falls back to treating 500 as healthy.

A malformed entry (bad path, non-numeric code, etc.) is logged and skipped — it never fails the whole reconcile; the rest of the annotation's entries still apply.

Namespace-Level Defaults

The same annotations can be set on a Namespace. Every Ingress in that namespace inherits them as defaults, which individual Ingress annotations override. This is handy for applying a setting across many services at once:

# Monitor everything in this namespace at a 5-minute interval
kubectl annotate namespace production \
  monitor.uptimehunt.io/enabled=true \
  monitor.uptimehunt.io/interval=5

An Ingress in production is then monitored every 5 minutes unless it sets its own monitor.uptimehunt.io/interval.

Cluster scope only

Namespace inheritance reads the cluster-scoped Namespace object, so it is available only when watch.scope: cluster (where the operator has permission to read Namespaces). Under watch.scope: namespace the operator uses Ingress annotations only, regardless of the watch.namespaces filter — see Watch Scope.

The precedence is:

Ingress annotation  →  Namespace annotation  →  operator default

Stopping Monitoring of a Check

To silence a discovered check — for example one of several produced by a single Ingress — use Stop monitoring (Ignore) on that check in the UptimeHunt UI. The operator will not re-enable or recreate an ignored check, so the change sticks across reconciles.

Ignore, don't delete

Deleting a managed check in the UI alone does not stop monitoring: the operator re-creates it on the next reconcile (see Continuous Reconciliation). Ignoring the check is the correct, durable way to stop monitoring it.

Check Removal

The operator never deletes checks automatically. If an Ingress (or one of its hosts/paths) disappears, its checks are left in place so a misconfiguration or outage keeps alerting rather than going quiet.

To let the operator prune an Ingress's checks when its paths change or it is opted out, set monitor.uptimehunt.io/allow-removal: "true". This can be set:

  • Per-Ingress, affecting only that Ingress, or
  • At the namespace level, where every Ingress in the namespace inherits it. An individual Ingress can opt back out with monitor.uptimehunt.io/allow-removal: "false".

Namespace-level allow-removal is broad

Setting allow-removal: "true" on a Namespace permits the operator to delete checks for every Ingress in that namespace. Every removal the operator performs is logged. Prefer per-Ingress allow-removal, or ignore a check, when you want to stop monitoring without granting cluster-wide deletion.

To stop monitoring a single check without enabling deletion, use Stop monitoring in the UI instead.

Watch Scope

watch.scope controls the operator's RBAC and whether namespace-annotation inheritance is available. watch.namespaces is an independent filter that narrows which namespaces are watched — it applies in both scopes.

Uses a ClusterRole and can read Namespaces, so namespace-level defaults are enabled.

  • watch.namespaces: [] — watch Ingresses in all namespaces.
  • watch.namespaces: ["team-a", "team-b"] — watch only those namespaces, while RBAC stays cluster-wide and inheritance stays enabled.
watch:
  scope: cluster
  namespaces: []          # or a subset to narrow the watch

Uses namespaced Role/RoleBindings only — no ClusterRole is created — so it fits multi-tenant or least-privilege setups. The operator cannot read Namespaces, so namespace-annotation inheritance is not available.

  • watch.namespaces: ["team-a", "team-b"] — watch only those namespaces (a Role/RoleBinding is created in each).
  • watch.namespaces: [] — watch only the operator's own (release) namespace.
watch:
  scope: namespace
  namespaces: ["team-a", "team-b"]

Continuous Reconciliation

Beyond reacting to Kubernetes events, the operator periodically re-applies the desired state on a timer (discovery.resyncInterval, default 10m), so any drift between the cluster and your UptimeHunt account is corrected even without new events.

Re-applies are idempotent and preserve user-controlled fields: a check's enabled state, its ignored (Stop monitoring) state, and an icon you set in the UI (when there is no icon annotation) all survive across reconciles.

Configuration Reference

Helm valueDefaultDescription
saas.urlUptimeHunt API base URL, e.g. https://app.uptimehunt.io/api/v1
saas.tokenAPI token (or use saas.existingSecret to reference a Secret)
discovery.modeannotation-onlyannotation-only or all
discovery.defaultIntervalMinutes3Check interval when not overridden by annotation
discovery.resyncInterval10mHow often desired state is periodically re-applied (Go duration) — see Continuous Reconciliation
discovery.ingressClassAllow / ingressClassDeny[]Ingress class filters
watch.scopeclusterRBAC + namespace-inheritance mode: cluster (ClusterRole, inheritance enabled) or namespace (Roles only, inheritance disabled)
watch.namespaces[]Namespace filter applied in both scopes. Empty: all namespaces in cluster scope, or the operator's own namespace in namespace scope
metrics.enabledfalseExpose the controller-runtime metrics endpoint (off by default; scraping requires extra RBAC and a ServiceMonitor/PodMonitor not shipped by the chart)

Security

  • The operator is read-only on your Ingresses — it never modifies them.
  • It needs only get/list/watch on Ingresses (and, for cluster-wide installs, on Namespaces).
  • Use a service account token with the automation preset (minted via Settings → Tokens → Service tokens), stored in a Kubernetes Secret. A service account token is preferred over a personal token because it is org-pinned, cannot gain SSO or member-management capabilities, and survives staff changes. Rotate it independently by creating a new token, updating the Secret, and revoking the old token.

On this page