Airframe quickstart · part 1 of 7

boarding-api

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.

boarding-api: from a Tower form to a canaryA Tower form opens a request PR; the dev cluster creates the app and its repos; each push runs the pipeline, which deploys to the ground environment and opens a release PR for the flight environment on the prod cluster.DEV CLUSTERPROD CLUSTERTower formCreate → NodeJStenants reporequest PRNodeJSApplicationapp-boarding-api-cicdboarding-api repocode · cicd.yaml · envs/Glidepath pipelinebuild → deploy → releaseGROUNDapp-boarding-api-devRollout · Redisgitops-boarding-apivalues · release.yamlFLIGHTstagingapp-boarding-api-stagingSUBMITMERGECREATESPUSHDEPLOYRELEASE PRARGO CDGround, then flight.Ground: your source repo, deployedon every push to main.Flight: a governed environment,changed only through PRs.
Before you start

The rules every part uses

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.

ApplicationInfraService
XRs (the app, its environments)app-<name>-cicdinfra-<name>-cicd
An environment's namespaceapp-<name>-<env>infra-<name>-<env>
Deploy repogitops-<name>gitops-infra-<name>

Four habits.

  1. Merge the .tekton/ PR. Glidepath opens one after onboarding and after every cicd.yaml change. No merge, no pipeline.
  2. git pull after the pipeline runs. The deploy stage commits the image to glidepath/envs/<env>.release.yaml on main.
  3. Leave build.platforms unset. The dev cluster is arm64 and the prod cluster amd64; the image is built for both.
  4. Release signed commits. The release's 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.
01

Create the app

Tower → Create → NodeJSApplication

FieldValue
Nameboarding-api
Dev cluster<dev-cluster>
DescriptionSkyport gate board
Node version20
Package managernpm
Port8080
Visibilityprivate

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.

Check

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.

02

Merge the onboarding PRs

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.

Note

Pipelines on pull requests run only after you add the preview label to the PR.

03

Pull in the code

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:8080

The 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.

EndpointPurpose
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}/scanCount one boarding pass
GET /api/whoamiVersion, pod and cache mode
GET /healthzLiveness and readiness
04

GroundDeploy to dev

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:

cicd.yaml
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: dev

Commit, 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.

Check
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.

05

GroundAdd a Redis cache

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.

glidepath/envs/dev.yaml
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.

Check

The gate board header says cache: redis, and each scan adds exactly one, whichever pod answers.

Note

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.

06

FlightDeclare staging

Add the flight environment and a release stage to cicd.yaml:

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: staging

Commit, push, and merge the .tekton/ PR.

07

FlightCreate the environment

Tower → Create → ApplicationEnvironment

FieldValue
Nameboarding-api-<prod-cluster>-staging
Namespaceapp-boarding-api-cicd
Owneryour Backstage group, e.g. group:default/<your-team>
App nameboarding-api
Cluster<prod-cluster> (must be an upper cluster; a dev cluster is rejected)
Envstaging
configMapGeneratoroff

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.

Check

kubectl --context <dev-cluster> describe applicationenvironment boarding-api-<prod-cluster>-staging -n app-boarding-api-cicd shows ClusterReady: True and AppResolved: True.

Tip

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.

08

FlightConfigure staging

Tower → boarding-api → App Configuration → staging. Check the environment named at the top before you save.

SectionValue
DeploymentOn
ScalingReplicas 4 (a canary splits by pod count, so 4 gives 25% steps)
ResourcesRequests 100m / 128Mi, limits 500m / 256Mi
ServicePort name http, container port 8080
Health checksLiveness and readiness: HTTP GET /healthz on 8080
Canary stepsKeep 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.

09

FlightRelease

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.

Check

kubectl --context <prod-cluster> get rollout boarding-api -n app-boarding-api-staging is Healthy, or look in Tower.

10

Watch a canary

The gate board polls /api/whoami twice a second and draws a bar of which version answered.

  1. 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
  2. Bump "version" in package.json (to 2.0.0, say), commit signed, push, and merge the new release PR.
  3. 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 paused

Press Reset tally between steps to see the current mix. The split is by pod count, because the chart has no traffic router.

Remember