Skip to content

Kubernetes Auto-Discovery

Overview

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.

HTTP GET checks only

The operator currently creates plain HTTP GET checks. The request method, custom request headers, expected status code, and authentication are not yet mapped from the Ingress. If you need any of those, configure the check manually in the UptimeHunt UI after it is discovered.

Prerequisites

  • A Kubernetes cluster (the operator is installed with Helm).
  • An UptimeHunt API token, created under Settings → API Tokens. This token determines which UptimeHunt account the discovered checks belong to.

Installation

Create a values file, then install with Helm:

values.yaml
saas:
  url: https://app.uptimehunt.io/api/v1
  token: uh_xxxxxxxxxxxxxxxxxxxx   # from Settings → API Tokens

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:

Annotation Description
enabled Opt in/out ("true" / "false"; meaning depends on the discovery mode)
name Override the check's display name
group Target project/group (defaults to the namespace name)
interval Check interval — whole minutes ("5") or a duration ("90s", "2m")
path Force a single probe path for all hosts on this Ingress
scheme Force http or https when TLS auto-detection isn't right
icon Custom 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"

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 value Default Description
saas.url UptimeHunt API base URL, e.g. https://app.uptimehunt.io/api/v1
saas.token API token (or use saas.existingSecret to reference a Secret)
discovery.mode annotation-only annotation-only or all
discovery.defaultIntervalMinutes 3 Check interval when not overridden by annotation
discovery.resyncInterval 10m How often desired state is periodically re-applied (Go duration) — see Continuous Reconciliation
discovery.ingressClassAllow / ingressClassDeny [] Ingress class filters
watch.scope cluster RBAC + 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.enabled false Expose 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).
  • The API token is stored in a Kubernetes Secret. Use a dedicated token so it can be rotated or revoked independently from Settings → API Tokens.