Skip to content

Scouty the Scout Bee Deploying Scout

Two Deployment Modes

Same-cluster mode (default): Scout and the Bindy operator run in the same cluster. No extra configuration needed.

Multi-cluster mode: Scout runs on workload clusters and writes to a dedicated Queen Bee cluster cluster running Bindy. Use bindy bootstrap mc to generate credentials, then set BINDY_SCOUT_REMOTE_SECRET. See Multi-Cluster Setup below.

Bindy Scout is an optional companion controller that watches Ingress resources and LoadBalancer Services across your cluster and automatically creates ARecord CRs on behalf of application teams — without requiring them to have write access to the bindy namespace.

See the Bindy Scout guide for the full conceptual overview, including the scout bee backstory, how record naming works, and the multi-cluster roadmap.


Prerequisites

  • The main Bindy operator must already be running (Deploying Operator)
  • CRDs must be installed (Scout uses the ARecord CRD)
  • A DNSZone must exist for the zone Scout will write records into

Install

The recommended way to install Scout is with the bindy CLI. This applies all resources via server-side apply and is safe to re-run:

bindy bootstrap scout

This creates:

  • A Namespace (bindy-system by default)
  • All 12 CustomResourceDefinition objects (same set as the operator — safe if already installed)
  • A ServiceAccount (bindy-scout) in bindy-system
  • A ClusterRole / ClusterRoleBinding for cluster-wide Ingress watch, LoadBalancer Service watch, DNSZone read, and Secret read
  • A Role / RoleBinding for ARecord write access in bindy-system
  • A Deployment running the bindy scout controller

Custom namespace or version

bindy bootstrap scout --namespace my-namespace --version v0.5.0

Air-gapped / private registry

bindy bootstrap scout --registry harbor.corp.internal/bindy-mirror
This produces harbor.corp.internal/bindy-mirror/bindy:<version> instead of ghcr.io/firestoned/bindy:<version>. See the CLI reference for the full air-gapped workflow.

Preview before applying

bindy bootstrap scout --dry-run
Prints every resource as YAML without connecting to the cluster.


Gateway API

Scout watches HTTPRoute, GRPCRoute, TLSRoute and TCPRoute in addition to Ingress and LoadBalancer Services. Gateway API is not installed by default in Kubernetes, so Scout probes for each route kind separately at startup and only starts the controllers for kinds the cluster actually serves.

The probe is per kind because the route kinds did not arrive together. Gateway API ships in two channels, and graduation dates differ:

Kind Standard channel since
HTTPRoute v1.0
GRPCRoute v1.1
TLSRoute v1.5
TCPRoute v1.6

A standard-channel install older than v1.5 therefore serves HTTPRoute (and, from v1.1, GRPCRoute) but not TLSRoute or TCPRoute. That is common, since ingress implementations pin the CRD version they ship. Probing one kind and inferring the rest would leave the other controllers retrying a 404 indefinitely.

Startup logs the result:

  • All kinds present — Scout controller running — watching Ingresses, Services, HTTPRoutes, TLSRoutes, GRPCRoutes, TCPRoutes
  • Some absent — the same line listing only what is watched, followed by Gateway API CRDs not found for TLSRoute/TCPRoute; watching disabled for those kinds

No configuration is required either way. If a probe fails for a reason other than 404 — an API server briefly unavailable at startup, say — Scout assumes the kind is served rather than disabling watching for the lifetime of the pod.

The probe runs once, at startup

Installing Gateway API CRDs after Scout is running does not enable those controllers. Restart Scout to pick them up.


Configure

Scout requires one mandatory setting: the logical cluster name that is stamped on every ARecord it creates. Set it via environment variable or CLI flag.

env:
  - name: BINDY_SCOUT_CLUSTER_NAME
    value: "prod"
  - name: POD_NAMESPACE
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace
args: ["scout", "--cluster-name", "prod"]

CLI takes precedence

When both --cluster-name and BINDY_SCOUT_CLUSTER_NAME are set, the CLI flag wins.

Full configuration reference

Variable CLI flag Default Description
BINDY_SCOUT_CLUSTER_NAME --cluster-name — Required. Logical cluster name stamped on all created ARecord labels.
BINDY_SCOUT_NAMESPACE --namespace bindy-system Namespace where ARecord CRs are created.
POD_NAMESPACE — default Scout's own namespace. Always excluded from Ingress watching. Inject via downward API.
BINDY_SCOUT_EXCLUDE_NAMESPACES — — Comma-separated list of additional namespaces to skip.
BINDY_SCOUT_DEFAULT_ZONE --default-zone — Default DNS zone when no bindy.firestoned.io/zone annotation is present. With DEFAULT_IPS, Ingresses and Services only need scout-enabled: "true".
BINDY_SCOUT_DEFAULT_IPS --default-ips — Comma-separated default IP(s) used when no per-resource annotation or LB status IP is available. For shared-ingress topologies (e.g. Traefik).
BINDY_SCOUT_ALLOWED_ZONES --allowed-zones unset (every zone) Comma-separated zones Scout may publish into, each with its subzones. A source naming another zone publishes nothing. Recommended (ADR-0025).
BINDY_SCOUT_ALLOWED_IP_RANGES --allowed-ip-ranges unset (every address) Comma-separated IPv4 CIDR ranges every published address must fall in. Recommended.
BINDY_SCOUT_REMOTE_SECRET — — (Multi-cluster) Name of a Secret in the local cluster containing a kubeconfig key for the Queen Bee cluster. When set, Scout writes ARecord CRs and validates zones on the remote Bindy cluster. See Multi-Cluster Setup below.
BINDY_SCOUT_REMOTE_SECRET_NAMESPACE — Scout's own namespace (Multi-cluster) Namespace of the BINDY_SCOUT_REMOTE_SECRET. Defaults to Scout's own namespace.
RUST_LOG — info Log level: trace, debug, info, warn, error.
RUST_LOG_FORMAT — text Log format: text or json.

Scout validates its configuration at startup

Scout exits at startup, naming the setting, when a variable or flag above is set to an empty value, when a --gateway-service / BINDY_SCOUT_GATEWAY_SERVICES entry is malformed, when a default IP is not IPv4, or when an allowlist entry is malformed or the default zone or a default IP falls outside it. Unset a variable you do not need rather than setting it to "". See the Scout guide.


Multi-Cluster Setup

In multi-cluster deployments, Scout runs on workload clusters and writes ARecord CRs to the dedicated Queen Bee cluster (where the Bindy operator lives). Use bindy bootstrap mc to generate the credentials.

1. Generate credentials on the Queen Bee cluster

# Run this against the Queen Bee cluster (with Queen Bee cluster KUBECONFIG active)
bindy bootstrap mc \
  --service-account bindy-scout-remote \
  --namespace bindy-system \
  --cluster-name cluster-a \
  --allowed-zones cluster-a.example.com \
  --output scout-kubeconfig.yaml

# Apply it to the child cluster, then delete it: it holds the SA token
kubectl --context=<child-cluster> apply -f scout-kubeconfig.yaml
rm scout-kubeconfig.yaml

This: 1. Creates a ServiceAccount + Role + RoleBinding on the Queen Bee cluster (minimal: ARecord CRUD + DNSZone read) 2. Generates a kubeconfig for that service account 3. Writes a bindy.firestoned.io/remote-kubeconfig Secret to the --output file, readable only by you (mode 0600). Without --output the file is <service-account>-kubeconfig.yaml. The Secret carries the token, so it is never printed to stdout 4. With --cluster-name and --allowed-zones, registers the account in the bindy-scout-sources ConfigMap, so the bindy-scout-source-binding admission policy (install with make admission-policies-install) refuses any ARecord write by it outside that cluster name and those zones. See Binding each cluster to its names and zones

One SA per child cluster

Use a unique --service-account per child cluster for independent credential revocation:

bindy bootstrap mc --service-account bindy-scout-remote-cluster-a -o cluster-a.yaml
kubectl --context=cluster-a apply -f cluster-a.yaml && rm cluster-a.yaml

2. Enable remote mode on the Scout Deployment

Add BINDY_SCOUT_REMOTE_SECRET to the Scout Deployment on the child cluster:

env:
  - name: BINDY_SCOUT_REMOTE_SECRET
    value: "bindy-scout-remote-kubeconfig"

Scout will load the kubeconfig from that Secret and write all ARecord CRs to the Queen Bee cluster instead of the local cluster.

See the Scout guide — Multi-Cluster Mode for the full architecture diagram, per-cluster SA strategy, and RBAC details.


Verify

Check that the Scout pod is running:

kubectl get pods -n bindy-system -l app.kubernetes.io/component=scout

Expected output:

NAME                           READY   STATUS    RESTARTS   AGE
bindy-scout-6d4b9f7c8d-r2kp4   1/1     Running   0          30s

Check Scout logs:

kubectl logs -n bindy-system -l app.kubernetes.io/component=scout -f

You should see Scout announce which namespaces it is watching and confirm it is ready.


Test

Annotate an Ingress in any non-excluded namespace:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: my-app-ns
  annotations:
    bindy.firestoned.io/scout-enabled: "true"
    bindy.firestoned.io/zone: "example.com"
spec:
  rules:
    - host: my-app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

Annotate a LoadBalancer Service in any non-excluded namespace:

apiVersion: v1
kind: Service
metadata:
  name: my-grpc-api
  namespace: my-app-ns
  annotations:
    bindy.firestoned.io/scout-enabled: "true"
    bindy.firestoned.io/zone: "example.com"
spec:
  type: LoadBalancer
  selector:
    app: my-grpc-api
  ports:
    - port: 9090
      targetPort: 9090

Annotate an HTTPRoute attached to a Gateway:

IP required

HTTPRoute has no LoadBalancer status. Supply an IP via bindy.firestoned.io/ip or set BINDY_SCOUT_DEFAULT_IPS.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
  namespace: my-app-ns
  annotations:
    bindy.firestoned.io/scout-enabled: "true"
    bindy.firestoned.io/zone: "example.com"
    bindy.firestoned.io/ip: "203.0.113.10"
spec:
  parentRefs:
    - name: my-gateway
      namespace: gateway-ns
  hostnames:
    - my-app.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: my-app
          port: 80

Annotate a TLSRoute for TCP/TLS workloads:

IP required

TLSRoute has no LoadBalancer status. Supply an IP via bindy.firestoned.io/ip or set BINDY_SCOUT_DEFAULT_IPS.

apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
  name: my-grpc-api
  namespace: my-app-ns
  annotations:
    bindy.firestoned.io/scout-enabled: "true"
    bindy.firestoned.io/zone: "example.com"
    bindy.firestoned.io/ip: "203.0.113.11"
spec:
  parentRefs:
    - name: my-gateway
      namespace: gateway-ns
  hostnames:
    - my-grpc-api.example.com
  rules:
    - backendRefs:
        - name: my-grpc-api
          port: 9090

Within seconds, Scout creates an ARecord in bindy-system for each DNS name, named scout-<cluster>-<dns-name>-<zone>-<hash> (see Record names). Resources that publish the same name share one record. Find records by label, not by name:

# All Scout-managed records
kubectl get arecords -n bindy-system -l bindy.firestoned.io/managed-by=scout

# Records one resource contributes to: kind is Ingress, Service, HTTPRoute, GRPCRoute, TLSRoute or TCPRoute
kubectl get arecords -n bindy-system -l \
  "contributor.scout.bindy.firestoned.io/$(printf '%s\0%s\0%s' <kind> <namespace> <name> | sha256sum | cut -c1-16)"

Next Steps