Docs · Airframe
Skyport runbook: all of it, on freshly built clusters
How to stand up the whole Skyport demo on two new clusters: what has to be true first, what to clean up from a previous run, the order of the seven walkthroughs, and how to replay every check at the end.
Names. <dev-cluster> and <prod-cluster> are the names chosen when the clusters were built (in
the owner’s lab this time, hangar-dev and hangar-prod). Each is a record’s name: in the fleet’s
clusters.yaml (in the hub cluster repo, the dev cluster’s) and, by the one-name rule, also the cluster’s
kubectl context. Nothing in these guides assumes a particular name. Infisical projects are
<app>-<cluster>; <github-owner> is the GitHub owner of the app repos. Each cluster has its own
tenants repo, chosen when it was set up with Apron: the guides call it “the cluster’s tenants
repo”, its tenantsRepo in clusters.yaml.
Configure
Set the names once. Every guide uses them as placeholders, and tools/walkthrough-runner needs them to
replay the checks; there are no defaults. Create .walkthrough.env in your Airframe checkout (the runner
reads it from the current directory, or from --env-file PATH; real environment variables win over it):
# .walkthrough.env - your lab's names; not committed
HANGAR_GITHUB_OWNER=<github-owner>
HANGAR_CLUSTERS_FILE=<hub-cluster-repo>/clusters.yaml # the hub repo's clusters.yaml: dev = control-plane, prod = upper/workloads
# or name them directly instead of HANGAR_CLUSTERS_FILE:
# HANGAR_DEV_CLUSTER=<dev-cluster>
# HANGAR_PROD_CLUSTER=<prod-cluster>
# Contexts default to the cluster names (one-name rule); set these only if yours differ:
# HANGAR_DEV_CONTEXT=...
# HANGAR_PROD_CONTEXT=...
The GitHub owner can also come from scm.owner: at the top level of clusters.yaml if it has one, or
from an Apron cluster.yaml given with --cluster-config PATH (HANGAR_CLUSTER_CONFIG). Check the
configuration renders before going further:
tools/walkthrough-runner docs/walkthroughs/part6-gate-api.yaml --render /tmp/check.md && grep -m3 context /tmp/check.md
A missing value stops a replay with exit 2 and names the setting.
Before you start: leftovers from a previous run
A rebuilt cluster forgets everything; GitHub, GHCR and the tenants repos do not. Anything below that survives from an earlier run collides with the new one (a repo that already exists, an image tag that already exists, a tenants file that recreates an old XR). decommission-app.md is the full procedure for one app on a live cluster; with the old clusters gone, only its manual parts apply:
| Leftover | Where | What to do |
|---|---|---|
| Source repos | GitHub <github-owner>/boarding-api, flight-api, baggage-api, gate-api, checkin-api |
Delete, or rename/archive to keep history. Irreversible if deleted. Back up issues/PRs you want. |
| GitOps repos | <github-owner>/gitops-boarding-api, gitops-flight-api, gitops-baggage-api, gitops-gate-api, gitops-checkin-api, gitops-infra-skyport-broker, gitops-infra-skyport-auth |
Same. They hold the old cluster’s <old-cluster>/<env>/ folders and release records. |
| Container images | GHCR packages ghcr.io/<github-owner>/<app> for the five apps |
Delete the packages (or accept that old tags stay; new tags carry new SHAs). |
| Old tenants repos | the previous clusters’ tenants repos | Each rebuilt cluster gets a new tenants repo, so the old ones are not read again. Archive them; just make sure no new cluster’s tenantsRepo points at one (it would recreate the old XRs, with the old cluster names, at once). |
| Adoption ids | the prod cluster repo’s 10-crds-operators/crossplane/secretstore-provisioner.yaml |
No <app>-<cluster> keys from old projects. A stale key makes a new app adopt a dead project. |
| Infisical projects | the Infisical org | Starting from a fresh Infisical, there are none. If an org is reused, delete <app>-<old-cluster> for every app. |
| Dex client registrations | the old Dex servers’ sqlite volumes | Gone with the old clusters. |
| Local checkouts | your machine | Move ./boarding-api, ./flight-api, ./baggage-api, ./gate-api, ./checkin-api aside: every guide’s git clone fails into an existing directory. |
Check with:
for r in boarding-api flight-api baggage-api gate-api checkin-api; do
gh repo view <github-owner>/$r --json name -q .name 2>/dev/null && echo "still exists: $r"
gh repo view <github-owner>/gitops-$r --json name -q .name 2>/dev/null && echo "still exists: gitops-$r"
done
for r in gitops-infra-skyport-broker gitops-infra-skyport-auth; do
gh repo view <github-owner>/$r --json name -q .name 2>/dev/null && echo "still exists: $r"
done
Not yet verified: what a Bootstrap XR does when its repo already exists on GitHub (adopt it, or fail to create it). Clean first rather than finding out.
Prerequisites
The platform (once, for every part)
- Both clusters built and their root apps synced from their cluster repos (made from Apron’s template), per those repos’ and Apron’s bootstrap steps.
clusters.yamlrecords for both names (zone,roles,tenantsRepo,serviceAccountIssuer,glidepath.relaySecretName: cluster-<prod-cluster>-relay-tokenon the prod record), thecluster-registryApplication synced (manual by design), and the readiness flags flipped by a person only after checking each live (apron docs/clusters-file.md):- dev:
cicdReady: trueonce the Glidepath control plane runs;infisicalHost: true(a role: the cluster that runs Infisical);autopilotReadyonly after the netpol canary passes (not needed for Skyport). - prod:
crossplaneReady: trueonce Crossplane composes there.ApplicationEnvironmentrefuses a cluster without it (ClusterReady: False).
- dev:
- A fresh Infisical: after its first-run setup, put the new org id in
clusters.yaml’sinfisical:block (withurl: http://<dev-cluster>.local:31800/apiand the in-cluster URL). Then plant, by hand, in the control plane’s projectglidepath-<dev-cluster>(Glidepath secrets-management.md):github-application-id,github-private-key,github-installation-id,github-webhook-secret,provider-github-creds(a PAT withrepo+delete_repo: Crossplane creates and deletes the app repos),provider-infisical-creds,registry-credentials-dockerconfigjson(GHCR read/write), andrelay-token-<prod-cluster>; the same relay token in the prod cluster’s own project. Never commit or paste these values. - The GitHub App installed on every repo the apps will have (or on the whole account), with its
webhook pointing at the dev cluster’s Pipelines-as-Code endpoint and the same webhook secret as
github-webhook-secret. Check that a push reaches the cluster before part 1: when the delivery path is down, pushes trigger nothing and nothing says so (Glidepath known gap 8). - Tower is up as the
backstageapp’s prod environment. Tower (Backstage) is redeployed as an AirframeNodeJSApplicationnamedbackstage, with its prod environment on the prod cluster. That app itself is onboarded by a git PR to the dev cluster’s tenants repo (tenantsRepoinclusters.yaml), not through Tower (there is no Tower yet to use). Its deployment is prepared in jfillman/backstagedeploy/airframe/(branchairframe-app). Every step that uses Tower needs it first: the Create forms (parts 1 to 5, 7), the App Configuration and Environments tabs, the Pull Requests tab and its gate view, and all of part 7. Part 6 does not need it. - Dex on both clusters. The dev cluster’s repo installs
function-dexand theDexXRD; on the prod cluster they come from Apron’s prod profile (Dex’s XRD, composition and function). Part 5 needs both;ghcr.io/jfillman/function-dexmust be a public package. - gitsign on the machine you commit from:
git config gpg.x509.program gitsign,git config gpg.format x509,git config commit.gpgsign true, and your signing address in the cluster’s release-signing policy (releaseSigning.defaults.allowedCommitSignersin the cluster repo’s50-glidepath/glidepath-catalogvalues, orreleaseSigning.signersin itscluster.yamlfor Apron) - an admin setting since 2026-10-20, no longer each app’scicd.yaml(commit-signing.md).
The gotchas every part shares
- The scaffold’s starter
cicd.yamlwritesdeploy.environmentswith ground dev only (andtarget: aws-ecswhen the app was created with that deploy target); add flight envs as each part shows. - Merge release PRs by fast-forwarding the signed commit, never with GitHub’s button (all three
strategies create an unsigned commit and
provenancefails). - PR pipelines need the
previewlabel. configMapGeneratoroff on everyApplicationEnvironment(no config files to mount).- Python images: alpine base and
pip uninstall -y setuptools pip, or the image-scan gate fails. - Spring liveness on
/actuator/health/liveness, never/readiness. provider-helmcomponents (Redis) reportReady=Falsefor about ten minutes after any change.- One container file per repo, named
Containerfile.
Per part
| Part | Needs on the dev cluster | Needs on the prod cluster | Needs before it |
|---|---|---|---|
| 6 gate-api | platform only | platform only | nothing (the smallest app: run it first, as a smoke test of the whole path) |
| 1 boarding-api | provider-helm, Redis XRD |
the same | Tower |
| 2 flight-api | CloudNativePG, PostgreSQL XRD |
the same | part 1 (boarding-api calls it) |
| 3 skyport-broker | RabbitMQ cluster + messaging-topology operators, cert-manager, RabbitMQ XRD |
the same | parts 1 and 2 (the publisher and the first consumer) |
| 4 baggage-api | MongoDB Controllers for Kubernetes, MongoDB XRD |
the same, and a node kernel check | part 3’s broker, with app-baggage-api-<env> in allowedNamespaces |
| 5 skyport-auth | function-dex (public GHCR package), Dex XRD |
the same, from the Apron prod profile (Dex’s XRD, composition and function on prod) | parts 1, 2 and 4 on the dex-tokens app branches (enforcement code) |
| 7 checkin-api | platform only | platform only | Tower; part 6 is the same path in the terminal |
All of these operators and XRDs are in the two cluster repos (10-crds-operators/ and
20-service-catalog/idp-service-catalog); the table says what each part depends on, so you know what
to check when one fails. A quick check per cluster:
for ctx in <dev-cluster> <prod-cluster>; do
kubectl --context $ctx get xrd redis.catalog.hangar.io postgresqls.catalog.hangar.io \
rabbitmqs.catalog.hangar.io mongodbs.catalog.hangar.io 2>&1 | sed "s/^/$ctx: /"
done
for ctx in <dev-cluster> <prod-cluster>; do
kubectl --context $ctx get xrd dexes.catalog.hangar.io
kubectl --context $ctx get function function-dex
done
The order
- Part 6, gate-api (terminal). No components, no Tower: if this reaches a canary on the prod cluster, the platform works end to end (onboarding, PaC, build, ground deploy, flight env, gates, signed release, Argo CD on the prod cluster).
- Tower comes up (the
backstageapp, above) if it isn’t already. - Part 1, boarding-api + Redis, ground and flight.
- Part 2, flight-api + PostgreSQL; point boarding-api at it.
- Part 3, skyport-broker; attach flight-api (publisher) and boarding-api (consumer). List
app-baggage-api-<env>inallowedNamespacesnow. - Part 4, baggage-api + MongoDB, consuming the broker; staging needs the broker’s staging env first.
- Part 5, skyport-auth on both clusters (Dex on prod is from the Apron prod profile): the server,
a client per app, then the apps’
dex-tokenscode, boarding-api first so the gate board keeps working. From here onflight-apiandbaggage-apirefuse API calls without a token. Only boarding-api calls them; gate-api and checkin-api call no other service. - Part 7, checkin-api, through Tower.
Replaying every check
Each part has an executable walkthrough in ../walkthroughs/. With
.walkthrough.env in place (Configure), run them all:
tools/walkthrough-runner --all
or give everything on the command line:
tools/walkthrough-runner --all --clusters <hub-repo>/clusters.yaml --github-owner <github-owner>
# kubectl contexts default to the cluster names; override with --dev-context / --prod-context
Each file reports PASS/FAIL per step, then one summary line (7/7 walkthroughs replay green). Every
step is read-only. A part you haven’t done yet fails, which is the point: the replay is the definition
of done. One part alone: tools/walkthrough-runner docs/walkthroughs/part4-baggage-api.yaml --clusters ....