Airframe quickstart · part 1 of 7
Build boarding-api, the gate board of Skyport: a small NodeJS service that looks up a flight's gate and caches the answer in Redis. You will create it, deploy it to a ground environment on the dev cluster, give it a Redis cache, promote it to a flight environment on the prod cluster, and watch a canary roll out.
Placeholders. <dev-cluster> and <prod-cluster> are the clusters' names in the fleet's clusters.yaml, which are also their kubectl contexts (for example hangar-dev and hangar-prod). <github-owner> owns the app repos.
Where things live.
| Application | InfraService | |
|---|---|---|
| XRs (the app, its environments) | app-<name>-cicd | infra-<name>-cicd |
| An environment's namespace | app-<name>-<env> | infra-<name>-<env> |
| Deploy repo | gitops-<name> | gitops-infra-<name> |
Four habits.
.tekton/ PR. Glidepath opens one after onboarding and after every cicd.yaml change. No merge, no pipeline.git pull after the pipeline runs. The deploy stage commits the image to glidepath/envs/<env>.release.yaml on main.build.platforms unset. The dev cluster is arm64 and the prod cluster amd64; the image is built for both.provenance gate checks the app commit it promotes. Push a gitsign-signed commit to main (or fast-forward one). GitHub's merge button works only when a platform admin has turned on the GitHub-merge path for the app.Tower → Create → NodeJSApplication
| Field | Value |
|---|---|
| Name | boarding-api |
| Dev cluster | <dev-cluster> |
| Description | Skyport gate board |
| Node version | 20 |
| Package manager | npm |
| Port | 8080 |
| Visibility | private |
The dev cluster must be its exact registry name. Any other value gives the app a secret store nothing reads, and its pods stay in CreateContainerConfigError.
Submitting opens a PR on the dev cluster's tenants repo. Merge it.
kubectl --context <dev-cluster> get nodejsapplication boarding-api -n app-boarding-api-cicd shows SYNCED True and READY True.
Crossplane creates two repos: boarding-api (a hello-world app, Containerfile and starter cicd.yaml) and gitops-boarding-api.
Glidepath opens one PR on each repo. They add the generated .tekton/ pipelines and glidepath/envs/dev.yaml. Merge both, from GitHub or Tower's Pull Requests tab.
Merging the source repo's PR starts the first build, which deploys the hello-world app to dev.
Pipelines on pull requests run only after you add the preview label to the PR.
The real boarding-api lives in this repo under examples/skyport/boarding-api/. Copy it over the scaffold:
git clone https://github.com/jfillman/airframe.git /tmp/airframe git clone https://github.com/<github-owner>/boarding-api.git && cd boarding-api cp -R /tmp/airframe/examples/skyport/boarding-api/. .
Keep the scaffolded Containerfile. Try it locally:
npm install && npm test
PORT=8080 node index.js # open http://localhost:8080The header says cache: memory: with no Redis configured, the app keeps its cache in the process.
The example code already contains everything later parts switch on (the flight-api client, the broker consumer, the token client). Each piece stays off until its environment variable is set.
| Endpoint | Purpose |
|---|---|
GET / | The gate board: flight lookup, boarding-pass scans, version tally |
GET /api/boarding/{flight} | Boarding info for a flight such as AC123, cached for 3 minutes |
POST /api/boarding/{flight}/scan | Count one boarding pass |
GET /api/whoami | Version, pod and cache mode |
GET /healthz | Liveness and readiness |
Onboarding already wrote a cicd.yaml that builds and deploys to dev. Turn on the unit tests, since the code you copied has a test.sh:
apiVersion: platform/v1
kind: PipelineConfig
build:
agent: nodejs-20
unitTest:
enabled: true # runs ./test.sh
deploy:
strategy: rollout
environments: # every environment once, in promotion order
- { name: dev, tier: ground }
pipelines:
ci:
trigger: { source: git, event: push, branch: main }
steps:
- stage: build
- stage: deploy
env: devCommit, push, and merge the .tekton/ PR the change opens. The pipeline builds the image and the deploy stage commits it to glidepath/envs/dev.release.yaml. Pull before your next push.
kubectl --context <dev-cluster> get pods -n app-boarding-api-dev kubectl --context <dev-cluster> port-forward -n app-boarding-api-dev svc/boarding-api 8080:8080
Open http://localhost:8080 and look up AC123 twice: the second answer is a cache hit. Press Scan a boarding pass a few times. The count jumps around because each of the two pods keeps its own.
Redis is a component: an entry under components: in an environment's values. The chart turns it into a Redis XR in the app's namespace. fromComponent hands its connection details to the app.
envName: dev
components:
- type: redis
name: cache
spec: { size: small, persistence: false }
env:
- { name: REDIS_URL, fromComponent: { name: cache, output: url } }
- { name: REDIS_PASSWORD, fromComponent: { name: cache, output: password } }Push it. No build runs; Argo CD applies the change and the pods restart.
The gate board header says cache: redis, and each scan adds exactly one, whichever pod answers.
The Redis XR reports Ready=False for about ten minutes after a change, though the pods are fine. Check the pods instead of waiting on the XR.
Add the flight environment and a release stage to cicd.yaml:
deploy:
strategy: rollout
environments:
- { name: dev, tier: ground }
- { name: staging, tier: flight, cluster: <prod-cluster> }
pipelines:
ci:
trigger: { source: git, event: push, branch: main }
steps:
- stage: build
- stage: deploy
env: dev
- stage: release
env: stagingCommit, push, and merge the .tekton/ PR.
Tower → Create → ApplicationEnvironment
| Field | Value |
|---|---|
| Name | boarding-api-<prod-cluster>-staging |
| Namespace | app-boarding-api-cicd |
| Owner | your Backstage group, e.g. group:default/<your-team> |
| App name | boarding-api |
| Cluster | <prod-cluster> (must be an upper cluster; a dev cluster is rejected) |
| Env | staging |
| configMapGenerator | off |
Merge the PR it opens. Crossplane writes a starter values.yaml to gitops-boarding-api/<prod-cluster>/staging/, and the prod cluster's own Argo CD creates the empty namespace app-boarding-api-staging.
kubectl --context <dev-cluster> describe applicationenvironment boarding-api-<prod-cluster>-staging -n app-boarding-api-cicd shows ClusterReady: True and AppResolved: True.
Tower → boarding-api → Environments → Add environment does steps 6 and 7 in one go. It opens both PRs; merge the environment request first. Part 7 uses it.
Tower → boarding-api → App Configuration → staging. Check the environment named at the top before you save.
| Section | Value |
|---|---|
| Deployment | On |
| Scaling | Replicas 4 (a canary splits by pod count, so 4 gives 25% steps) |
| Resources | Requests 100m / 128Mi, limits 500m / 256Mi |
| Service | Port name http, container port 8080 |
| Health checks | Liveness and readiness: HTTP GET /healthz on 8080 |
| Canary steps | Keep the default |
Save opens a PR on gitops-boarding-api, checked against the chart's schema. Merge it. There is no image field: the release sets the image.
Push a signed commit to main. After build and deploy to dev, the release stage opens a PR on gitops-boarding-api that writes <prod-cluster>/staging/release.yaml. Wait for its gates (image-promotion runs last), then merge it.
kubectl --context <prod-cluster> get rollout boarding-api -n app-boarding-api-staging is Healthy, or look in Tower.
The gate board polls /api/whoami twice a second and draws a bar of which version answered.
Port-forward to staging and open the board: one bar, 100% of the current version.
kubectl --context <prod-cluster> port-forward -n app-boarding-api-staging svc/boarding-api 8080:8080
"version" in package.json (to 2.0.0, say), commit signed, push, and merge the new release PR.Watch the steps. With four replicas the bar moves from a quarter of the new version, to half, to all of it.
kubectl --context <prod-cluster> argo rollouts get rollout boarding-api -n app-boarding-api-staging --watch
kubectl --context <prod-cluster> argo rollouts promote boarding-api -n app-boarding-api-staging # if a step is pausedPress Reset tally between steps to see the current mix. The split is by pod count, because the chart has no traffic router.
rollout.strategy: blueGreen. Port-forward to boarding-api-preview to see the new version before promotion..tekton/ PR after onboarding and after every cicd.yaml change.glidepath/envs/<env>.yaml); flight environments need an ApplicationEnvironment and change only through PRs on gitops-<app>.cicd.yaml declares.redis, postgresql, rabbitmq, mongodb, dex) go under components:; read their outputs with fromComponent.envName, never env (that is the container env-var list).