Tower · Airspace

Airspace: who a change will reach, before it ships

Every service in Hangar talks to other services, databases and brokers, and serves an API someone else depends on. Airspace shows those connections as they really are, and keeps track of the API contract each deployed version carries, so you know who a change will reach before it ships.

Spec October 24, 2026Status built and live in Tower v0.99.0 to v0.102.0, Oct 23 and 24, 2026Builds on Airframe service kinds, Apron's observability stack, Glidepath's signed API specs and api-compat checkDocs Tower Airspace
What you get

Three places to look

Per service

Airspace tab

Who calls this service, what it calls, and the databases and brokers it owns, per environment. A row view for scanning, with a radar graph one toggle away.

Fleet

Map, Contracts, Drift

The whole fleet's connections, every API with the spec each environment runs and a diff between them, and every connection that disagrees with what was declared.

Release

API compatibility

Each release PR gets an api-compat check. Its verdict, the breaking changes and whether anyone uses them land on the Release Record and the Ops Wall.

The big picture

Three sources, one view

Airspace never asks you to draw a diagram by hand. It reads three things Hangar already produces: traffic the platform observes, what your environment's configuration declares and owns, and the API spec signed onto each image.

OBSERVED service podagent injected at start OTel collectorone OTLP endpoint Tempobuilds the service graph Prometheusedges, rates, latency DECLARED · COMPOSED environment values filenetworkPolicy peers (declared) · components[] (owned) Airframe / Crossplanethe databases, caches and brokers each service owns SPECS openapi.yamlin the service repo Glidepath buildsigns it onto the image image digestspec travels with it api-compatrelease PR check TOWER Airspace Strips · Radar Fleet map Contracts Drift Release Record Ops Wall
Each source answers one question. Observed: who actually talks to whom. Declared and composed: who is allowed to, and what each service owns. Specs: what contract each running version offers.
Observed traffic

Instrumented without touching your code

Hangar already knows what kind of service you run: a Node.js, Spring Boot, Python or Go application is a different Airframe kind. That kind becomes a runtime, the runtime travels with each environment to the Helm chart, and the chart asks the OpenTelemetry Operator to inject the matching agent when the pod starts.

CONFIGURATION · DECIDED ONCE PythonApplicationthe service's Airframe kind runtime: pythonwritten to its identity file environment+ the cluster's observability chart: pod annotationinject-python: observability/hangar RUNTIME · EVERY POD Operator webhookadds the agent; fail-open service + agentspans over OTLP/HTTP collector → Tempoclient/server pairs service graphin Prometheus
Nobody edits a values file to get this. The runtime comes from the kind, the cluster's observability settings come from the cluster, and both reach every Ground, Flight and preview environment that uses the platform's default chart.

What the graph contains

Calls between services, calls into databases and caches, and messages through a broker, each with a rate, a p95 latency and an error count. Traffic arriving from outside appears as user. Services are named <service>-<environment>, so dev and prod never blur together.

What gets instrumented

Node.js, Java and Python services get an agent automatically. Go services are recognised but not injected, because the Go agent needs a privileged sidecar; they can still send traces with the OpenTelemetry SDK. Every trace is kept, so a quiet connection still shows up.

Declared and owned

What each connection's state means

Observed traffic is compared with what your environment declares. Peers listed in the NetworkPolicy settings count as declared. Components your service owns, such as its PostgreSQL or Redis, come from components[] and need no declaration at all.

StateMeaningWhat to do
Declared · seenDeclared, and traffic flows.Nothing.
Seen, not declaredTraffic flows, but nothing declares it.Declare the peer, or remove the call. A NetworkPolicy built from declarations would block it.
Declared · no trafficDeclared, but silent in the window.Check whether the caller still exists. In a quiet environment this can be normal.
Via brokerMessages pass through a broker such as RabbitMQ.Nothing. Shown as a direct publisher-to-consumer edge.
OwnedA component the service's own resources create.Nothing. Known from Crossplane.
API specs

The spec travels with the image

Backstage-style API catalogs read a spec from the main branch, which says what the code will do someday. Hangar signs the spec onto the image at build time instead. Each environment then carries the exact contract of the version it is running, and dev and prod can be compared directly.

BUILD openapi.yamlcicd.yaml api.spec attest-api-specYAML → JSON, hash, sign signed attestationFulcio cert · Rekor entry image @ sha256:…attestation bound to the digest DEPLOY · EACH ENVIRONMENT RUNS ONE DIGEST devdigest B · spec 1.1.0 proddigest A · spec 1.0.0 TOWER · CONTRACTS fetch by digest → verify → show per-environment spec, and a diff dev → prod
Optional by design. No file at api.spec means the service publishes no API. A malformed file or a signing problem is reported, and the build still passes.

Turning it on

Put an OpenAPI or AsyncAPI file at the repo root as openapi.yaml. That is the default, so nothing else is needed. Name another path in cicd.yaml if you keep it elsewhere.

api:
  spec: openapi.yaml   # default; YAML or JSON
  type: openapi        # openapi | asyncapi

What is signed

The spec as JSON, its file hash, its info.title and info.version, and the format version, under the predicate type https://hangar.io/attestations/api-spec/v1. Rebuilding the same image never attaches a second copy. Tower verifies the signature the same way it verifies build provenance and SBOMs, and labels an unverified spec as unverified.

Release check

api-compat: who a change will break

When Glidepath opens a release PR, the api-compat check compares the candidate image's spec with the spec of the image already running in the target environment, not with the previous build. Skipped versions are compared correctly. It verifies both signatures, diffs the two with oasdiff, and looks up which operations are in use there.

release PRcandidate vs. running spec on candidate? yes spec on baseline? yes breaking? no no-spec no no-baseline no pass yes blockingon? no warn yes operationin use? no / unknown warn yes owneraccepted? no block yes overridden
Blocking ships switched off. Until a cluster turns it on, every breaking change is a warning. Even when it is on, a change blocks only if the operation it breaks is seen in use, and the service's owners can accept it.
VerdictWhenBlocks the merge?
passNo breaking change between the running spec and the candidate.No
warnBreaking changes found, reported only.No
blockA breaking change to an operation seen in use, with blocking switched on and no owner override.Yes, when the check is required
overriddenAn owner accepted the breaking change. Who, when and why are recorded.No
no-specThe candidate image carries no spec.No
no-baselineFirst release, or the running image has no spec yet.No
unsupportedAsyncAPI, which is not diffed yet.No

Accepting a breaking change

  1. An admin or maintainer of the service's repository comments /api-break-ok <reason> on the release PR.
  2. They comment /retest api-compat-.
  3. The check confirms their permission on GitHub again, records the override and passes. A comment from anyone else is listed as ignored.

Where the verdict shows up

On the release PR as a comment with the full report. On the Release Record as an API compatibility section. In Airspace as a "breaking change pending" item. On the Ops Wall as a Needs-Attention entry while the PR is open with a warn or block.

Guardrails

Built to fail safe

Pods always startThe agent is added by a fail-open webhook that only looks at application namespaces. If the operator is down, pods start uninstrumented and nothing else waits.
Builds never fail on a specA missing, malformed or unsigned spec is reported. The build passes.
Only verified specs countThe release check verifies both attestations before diffing. Tower shows an unverified spec as unverified, never as trusted.
Verdicts can't be forgedTower reads the machine-readable verdict only from the platform's own comment. A person posting a lookalike comment changes nothing.
Unknown never blocksIf usage in the target environment can't be read, the verdict stays a warning.
Owners decideThe override is limited to people who can already change the service's code, checked on every run, never cached.
Know the limits

What Airspace can't tell you

Operations, not fields

Traces show which operation a caller uses, not which response fields it reads. Field-level certainty needs consumer contract tests.

Quiet means unknown

A connection with no traffic in the window may be idle, not gone. Low-traffic environments produce more "no traffic" rows.

The spec is a promise

Nothing yet checks the spec against the routes a service really serves. Keep the file in step with the code.

Tower's own diff between environments uses a small rule set. The release check's oasdiff report is the authoritative one, and Tower shows it whenever a release PR has run the check.

Configuration

Who sets what

SettingWhereOwner
api.spec, api.typecicd.yaml in the service repoService team
observability.namespace, .instrumentationThe cluster's cluster-defaults.yaml, rendered by ApronPlatform team
observability.runtimeDerived from the service's Airframe kindAutomatic
apiCompat.blockWhenInUseGlidepath catalog values, per clusterPlatform team
apiCompat.usage.<cluster>.urlGlidepath catalog values: where the check reads usagePlatform team
api-compat requiredThe release guardrail registryPlatform team