Skip to main content
Template version 2.0.0 is a breaking change, and one of the changes is a data-loss hazard.
  • The template no longer creates a GVC. It deploys into the GVC you install into. The gvc.name and gvc.locations values are gone; Airflow runs in a single top-level location string.
  • Never upgrade a 1.x release onto 2.0.0 in place. A 1.x release owns the GVC it created, and Helm deletes what a chart stops declaring — the upgrade destroys that GVC and every workload, volume set and identity inside it, including your metadata database and your DAGs. Migrate to a new release instead: Migrating from 1.x.
  • KEDA autoscaling is now opt-in and defaults to false. KEDA is a GVC-level setting a chart cannot turn on for you, and turning it on against a GVC that does not have it is accepted in silence. See KEDA Autoscaling.

Overview

Apache Airflow is an open-source platform for programmatically authoring, scheduling, and monitoring workflows. This template deploys a full Airflow 3.x stack into an existing GVC using the CeleryExecutor, with Redis as the task broker, PostgreSQL as the metadata database, Celery workers at a fixed replica count or scaled by KEDA, and optional git-sync DAG delivery. The admin login and both signing keys are not template values. They come from a dictionary secret you create before installing, so they never pass through Helm and never land in the release. The web UI ships closed to the internet, because it can trigger arbitrary code and decrypt every credential stored in an Airflow Connection.
Template version 1.5.0 was also a breaking change. airflow.auth.jwtSecret, airflow.auth.fernetKey, airflow.admin.password and gitSync.auth.token were removed, airflow.auth.jwtExpirationDelta and airflow.auth.jwtRefreshThreshold were removed because Airflow 3.x ignores them, and the public web UI became closed by default. An install that still sets any removed key fails at render. If you are coming from 1.4 or earlier, read Earlier Breaking Changes as well — the Fernet key change is a data migration, not an upgrade.

What Gets Created

  • Airflow Webserver — (RELEASE_NAME-airflow-webserver): a stateful, single-replica workload running the API server, scheduler, dag-processor and triggerer in one container, serving the UI and REST API on port 8080. It is the only tier with external inbound access, and the only one that runs the boot-time GVC checks.
  • Celery Workers — (RELEASE_NAME-airflow-celery-worker): a stateful workload executing DAG tasks, at a fixed airflow.celeryWorker.replicas or scaled by KEDA on the Redis queue length.
  • Redis — (RELEASE_NAME-airflow-redis): the Celery broker, on its own persistent volume set.
  • PostgreSQL — (RELEASE_NAME-airflow-postgres): the Airflow metadata database, on its own persistent volume set.
  • Airflow Volume Set — (RELEASE_NAME-airflow-vs): a shared-filesystem volume mounted as the Airflow home by the webserver and every worker, so they all see the same DAGs and logs.
  • Config Secret — (RELEASE_NAME-airflow-config): a dictionary secret holding the metadata database username and password. It carries no key material.
  • Webserver Startup Secret — (RELEASE_NAME-airflow-webserver-startup): an opaque secret holding the webserver’s startup script, which runs the topology checks before Airflow’s own boot sequence.
  • Identity & Two Policies — An identity bound to the workloads, with reveal on exactly the secrets they mount (the database config secret, the startup script, your auth secret, and your git token secret when git-sync uses one), plus view on the one GVC you install into so the webserver can check its own topology at boot.
  • git-sync Sidecar (optional) — Polls a Git repository and syncs DAGs onto the shared volume.
This template does not create a GVC. Every resource lands in the GVC you install into, pinned to the single location you configure, so cpln workload exec, cpln logs and uninstalling all work against that GVC, and uninstalling can never delete it. Nothing Airflow-related runs in the GVC’s other locations.

Migrating from 1.x

Template versions through 1.5.0 created their own GVC — KEDA is a GVC-level setting, and the chart wanted to own it. Version 2.0.0 does not declare a GVC at all, and Helm deletes what a chart stops declaring.
An in-place upgrade from 1.x to 2.0.0 destroys the deployment. Measured on a sibling template with the guard removed: the upgrade deleted the GVC and every workload, volume set and identity inside it in 6 seconds, and reported that the release had been upgraded successfully. Reading the GVC back afterwards returned 404. Here that would take your metadata database — every Connection, Variable, DAG run and task history — and the shared volume holding your DAGs and logs.The chart ships a render-time refusal so this cannot happen by accident: any leftover gvc key in your values aborts the upgrade before Helm computes a diff or contacts the API, leaving your deployment untouched and running.
Two things this guard cannot do. An upgrade run with no values at all sees only the new chart’s defaults, has no gvc key to catch, and proceeds — so the rule matters more than the guard. And when the refusal does fire, cpln helm upgrade exits 0: nothing is destroyed, but a script chaining on success would carry on regardless. Check the output, not the exit code.
Migrate to a new release instead:
1

Choose the GVC for the new release

Create or pick a GVC that already contains the location you intend to set as location. See Prerequisites. If you plan to use KEDA, enable it on that GVC now.
2

Install 2.0.0 as a NEW release, pointing at the SAME auth secret

Use a different release name — secret names are org-wide, so a same-named release collides with the 1.x one even in another GVC. Point airflow.auth.secretName at the same auth secret the 1.x release used: the fernetKey must be identical, or every Connection and Variable you restore becomes unreadable.
3

Move the metadata database

Substitute the postgres.config.username and postgres.config.database values each release actually uses:
4

Move the DAGs

With gitSync.enabled: true there is nothing to do — the sidecar re-clones onto the new volume. Otherwise copy them off the old shared volume:
5

Verify before you uninstall anything

Force a redeployment of the new webserver so the scheduler re-reads the restored database, then confirm in the UI that your DAGs, Connections and Variables are all present. Only then uninstall the old release — that deletes the GVC 1.x created, and everything still in it.Uninstall it against the GVC you installed it into, not the one it created: that is where Helm tracks the release, and uninstalling from there takes the created GVC with it.
Values that moved or were removed in 2.0.0. Each one is named at render time rather than ignored, so an old values file produces an error instead of a silent misconfiguration:

Earlier Breaking Changes

Template version 1.4 and earlier carried the JWT signing secret, the Fernet key, the admin password and the git token as plain Helm values, and shipped a working CHANGE_ME default for the first three. All four were removed in 1.5.0 with no compatibility fallback; a values file that still sets one fails at render with a message naming its replacement.
The Fernet key cannot be rotated, so this is a migration rather than an upgrade. That key encrypts every Airflow Connection and Variable in the metadata database — the database passwords, cloud keys and API tokens your DAGs use. Changing it does not re-encrypt anything: it makes all of them permanently unreadable.If your 1.4.x install ran on the shipped CHANGE_ME default, every one of those credentials is encrypted under a value published in a public repository. There is no in-place fix. Export what you need, install 1.5.0 with a freshly generated key, re-enter the Connections and Variables by hand, and rotate every credential they held at its source — treat them as compromised.
  • The metadata database password cannot be changed in place either. POSTGRES_PASSWORD is read only when the data directory is initialized, so pointing postgres.config.password at a new value on an existing volume leaves the database on the old one and Airflow can no longer connect. Set it at first install.
  • The web UI now ships closed. If you were relying on the old 0.0.0.0/0 default, put your own CIDRs in firewallConfig.inboundAllowCIDR.
  • Two JWT knobs were removed because they did nothing. jwtExpirationDelta and jwtRefreshThreshold rendered AIRFLOW__API_AUTH__JWT_EXPIRATION_DELTA and AIRFLOW__API_AUTH__JWT_REFRESH_THRESHOLD, neither of which is an option in Airflow 3.x — tokens were issued with Airflow’s own 86400-second lifetime regardless of what those values said. The live knob is airflow.auth.jwtExpirationTime; there is no refresh-threshold equivalent.

Prerequisites

A GVC Containing the Location You Configure

A GVC must already exist, and location must be one of its locations. Extra locations in the GVC are fine — nothing Airflow-related runs in them. Check what a GVC has before installing:
The locations are under spec.staticPlacement.locationLinks. To add a missing one:
Every workload in a GVC runs in every location that GVC has, so add locations to a shared GVC deliberately.
A location the GVC does not have is not caught at install time, and it starts nothing. The platform stores the placement without validating it, so the install reports every resource created and succeeds — and then all four workloads sit at zero replicas, in every location. cpln logs returns zero lines, because no container ever runs.The signal is the per-location deployment message, on any of the four workloads:
That message is also what a correctly configured release shows for the GVC’s other locations, so read it together with the location you configured: if every location reports it, location names somewhere the GVC does not have. Symptom in one line: the release deployed and nothing started.
If a location is removed from the GVC while Airflow is already running, the webserver’s boot check reports it in the log on the next restart and keeps serving rather than taking a live install down.

One Secret Must Exist Before You Install

Its values never pass through Helm, so they never land in the release. Secrets are org-level, so no GVC flag is involved.
1

Create the auth secret

A dictionary secret holding exactly three keys — jwtSecret, fernetKey and adminPassword:
Set airflow.auth.secretName to the name you used.
2

Read the secret back later

The -o yaml is required — plain cpln secret reveal prints only a summary table, not the values:
3

Optional: create a git token secret for a private DAG repository

Only needed when gitSync.enabled is true and the repository is private. Create an opaque secret with encoding plain whose payload is a personal access token, and set gitSync.auth.secretName to its name — the sidecar receives the payload as its Git password.
Leave gitSync.auth.secretName empty for a public repository or when git-sync is off. Empty turns the credential off entirely, including its policy grant, and setting it while git-sync is disabled is rejected at render.
Generate the Fernet key once and keep it for the life of the install. It encrypts every Connection and Variable Airflow stores. A different key does not fail loudly — it simply makes all of them unreadable, and there is no way back short of re-entering each one by hand.
Create the auth secret before installing. A name pointing at a secret that does not exist installs “successfully” and then wedges: every resource reports created, the workload never becomes ready, and cpln logs returns zero lines because no container ever starts. The only diagnostic is status.versions[].message:
It names the missing secret: The secret <name> no longer exists. Workload updates are paused until the secret is added or the reference to the secret removed. The command is get-deployments — plain cpln workload get has no versions key at all. Creating the secret recovers the workload on its own, but slowly: poll for 5.5 to 10.5 minutes rather than time-boxing it (10 minutes 17 seconds measured here). cpln workload force-redeployment RELEASE_NAME-airflow-webserver --gvc GVC_NAME cuts that to roughly 90 seconds.

KEDA on the GVC

Only needed when you set keda.enabled: true. KEDA is a GVC-level setting, so this template cannot turn it on for you. Enable it on the GVC first, then install with keda.enabled: true. Edit the file the first command writes rather than composing one:
Set spec.keda.enabled: true in that file, apply it, and confirm:
Never hand-write a partial GVC file for cpln apply. It replaces a GVC’s spec rather than merging into it, so a file carrying only spec.keda wipes spec.staticPlacement.locationLinks — and every workload in that GVC loses its placement. Always start from cpln gvc get ... -o yaml-slim.

Installation

To install, follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Configuration

The default values.yaml for this template:

Location

  • location — the single location name every workload in the release is pinned to. It must already be a location of the GVC you install into.
Airflow runs in exactly one location, and that is structural rather than a simplification. The Airflow home is a shared volume set, and a shared volume set provisions one volume per location — so two locations would mean two Airflow homes that cannot see each other’s DAGs or task logs. The metadata database and the Celery broker are each a single volume bound to a single replica on top of that. There is no multi-location shape to configure.
GVC locations you did not name run nothing. Their deployment reads This workload location is deactivated because maxScale is set to 0. That is the mechanism that keeps a shared GVC safe — it is what a healthy install looks like, not a fault. It is also why a location the GVC lacks starts nothing anywhere; see Prerequisites.
Version 2.0.0 fixes a placement bug. In 1.5.0 the webserver and the Redis broker carried no per-location options at all, so on a multi-location GVC the platform’s defaults applied and the release would have run one scheduler and one broker per location — split queues, and more than one scheduler against the same metadata database. Every workload in 2.0.0 declares 0 scale by default with a single complete entry for location.

PostgreSQL Metadata Database

  • postgres.image — The PostgreSQL image.
  • postgres.resources — Reservation and limit for the database container (minCpu / maxCpu / minMemory / maxMemory).
  • postgres.config.username / postgres.config.password / postgres.config.database — The metadata database credentials. This is bundled plumbing: it serves Airflow only, is unreachable from outside the GVC, and no human ever types it, so it stays a value. Set the password at first install — the shipped change-me-airflow-db is a published placeholder, and PostgreSQL only reads it when the data directory is initialized.
  • postgres.volumeset.capacity — Database volume size in GiB (minimum 10).

Redis Broker

  • redis.image — The Redis image.
  • redis.resources — CPU and memory limit for the broker (this block exposes only a limit).
  • redis.volumeset.capacity — Broker volume size in GiB (minimum 10).

Airflow

  • airflow.webserver.image / airflow.celeryWorker.image — Images for the webserver and the Celery workers.
  • airflow.webserver.resources / airflow.celeryWorker.resources — CPU and memory limits for each tier.
  • airflow.celeryWorker.replicas — The fixed number of Celery workers. Ignored when keda.enabled is true, which hands the replica count to KEDA instead.
  • airflow.webPort — The port serving the UI and the REST API.
  • airflow.auth.secretName — Name of the prerequisite dictionary secret holding jwtSecret, fernetKey and adminPassword. It must exist before you install.
  • airflow.auth.jwtExpirationTime — API access-token lifetime in seconds, applied as AIRFLOW__API_AUTH__JWT_EXPIRATION_TIME. The shipped 86400 is Airflow’s own default.
  • airflow.admin.username — The admin login paired with the adminPassword key of your auth secret.
  • airflow.scheduler.dagDirListInterval — How often the DAG folder is rescanned, in seconds.
  • airflow.scheduler.minFileProcessInterval — Minimum interval between processing the same DAG file, in seconds.
  • airflow.celery.workerConcurrency — Tasks each Celery worker runs concurrently.
The webserver runs the API server, scheduler, dag-processor and triggerer in one container and is pinned to a single replica, so airflow.celeryWorker.replicas (or KEDA) is the only horizontal scaling knob here — you scale task execution, not scheduling.
The admin account is provisioned by Airflow’s SimpleAuthManager: the container writes a password file from the secret on every start, so the login always matches the current secret contents.
SimpleAuthManager is Airflow 3.x’s default auth manager and has no SSO or LDAP support. Front the UI with an OAuth/OIDC auth manager if you need one.

Storage

  • volumeset.airflow.capacity — Size in GiB of the shared Airflow home holding DAGs and logs (minimum 10).
The database and broker volumes are configured separately under postgres.volumeset.capacity and redis.volumeset.capacity.
The Airflow home uses a shared (NFS-style) filesystem so the webserver and every Celery worker read the same DAGs and write to the same log directory.

Access

  • firewallConfig.inboundAllowCIDR — CIDR ranges allowed to reach the webserver from the internet. The shipped [] means closed: the canonical endpoint returns 403 and the UI is reachable only over a port-forward. Add your own ranges (for example 203.0.113.0/24) to expose it.
The Airflow UI is effectively remote code execution. It triggers DAGs and can decrypt every stored Connection, and its only authentication is a single SimpleAuthManager password. That is why the closed default exists — prefer cpln port-forward over 0.0.0.0/0.
A firewall change takes roughly 30 seconds to 5 minutes to propagate (measured 20–85 seconds here). During the transition the endpoint can return 503 before it settles on 200 or 403, so re-poll rather than trusting the first response.
internalAccess controls which workloads inside the org may reach Airflow’s own workloads:
This list governs traffic between the release’s own tiers — the webserver and the workers reaching the metadata database, the broker and each other — not just traffic from your applications. The template merges its own four workloads in for you, and when keda.enabled is true it also allows the KEDA operator onto the Redis tier, whose queue length KEDA polls. So you only need to list your own clients.

DAG Delivery With git-sync

git-sync runs as a sidecar on the webserver and worker workloads, pulling a Git repository onto the shared Airflow volume and pointing AIRFLOW__CORE__DAGS_FOLDER at the synced checkout on both tiers. With git-sync off, put DAG files directly in /opt/airflow/dags on the shared volume — the dag-processor picks up a new file within seconds at the shipped dagDirListInterval: 10.

KEDA Autoscaling

KEDA scales the Celery workers on the Redis queue length, including down to zero between runs. It is off by default as of 2.0.0. With it off, workers run at the fixed airflow.celeryWorker.replicas.
Enable KEDA on the GVC before you set keda.enabled: true, in that order. KEDA is a GVC-level setting this template cannot turn on for you, and getting it wrong is accepted in silence: the workload is created without any error, reports ready: true, and simply never autoscales — it sits at keda.minScale forever. At minScale: 0 that is zero workers, and every task queues with nothing reporting a problem anywhere.The webserver checks the GVC at boot and is the only thing that surfaces it. On a fresh install it refuses to start and says so:
On an established install the same condition is a warning and Airflow keeps scheduling, so a GVC change can never take a running deployment down. The refusal also clears itself: enable KEDA on the GVC and the crash-looping webserver passes the check on its next restart, with no redeploy or reinstall needed.See KEDA on the GVC for the commands.

Wide Fan Outs Can Silently Lose Tasks

A DAG that dispatches many tasks at the same instant loses roughly a third of them, and nothing surfaces in the UI as an error — the tasks are simply marked failed, with no hostname, no start time and an empty log. A measured 24-task burst finished 7 failed and 9 succeeded, with the failures never starting at all. The cause is upstream, not anything this template configures: the Celery executor’s Redis transport is imported lazily and concurrently by several task-sending threads, and one of them observes a half-initialized module — module 'redis' has no attribute 'client' from kombu/transport/redis.py. The scheduler then marks those queued tasks failed. Until it is fixed upstream, stagger task submission rather than fanning out wide simultaneously, and count completed task instances rather than trusting the UI to flag a problem. A single task, and tasks arriving at a normal rate, are unaffected.

Connecting to Airflow

The port-forward tunnel goes through Control Plane infrastructure and is independent of the firewall, so it works while the canonical endpoint is returning 403.

First Run Sequence

1

Install with the UI closed

Keep the shipped firewallConfig.inboundAllowCIDR: [].
2

Tunnel to the UI and sign in

Open http://localhost:8080 and log in with airflow.admin.username and the adminPassword from your auth secret.
3

Expose it only if you need to

Run a Helm upgrade with your own CIDR ranges in firewallConfig.inboundAllowCIDR, then allow up to a few minutes for the firewall to propagate before testing.

API Access

Airflow 3.x issues JWT access tokens signed with your jwtSecret. Request one with the same admin credentials:
Then pass it on the v2 API:

Important Notes

  • The auth secret must exist before you install. Without it the deployment wedges with no log output at all; see Prerequisites for the one diagnostic that names it, and expect recovery to take up to about ten and a half minutes after you create it.
  • The Fernet key can never be rotated — every stored Connection and Variable is encrypted under it. Generate it once, keep it, and back it up somewhere you will still have it later.
  • A wide simultaneous fan-out silently loses roughly a third of its tasks — see Wide Fan Outs Can Silently Lose Tasks.
  • Never upgrade a 1.x release onto 2.0.0 in place — it deletes the GVC the 1.x chart created and everything in it, including your metadata database and DAGs. Migrate to a new release: Migrating from 1.x.
  • location must be a location of your GVC. If it is not, the install still succeeds and then nothing starts anywhere, with no container to produce a log line. Check cpln workload get-deployments for This workload location is deactivated because maxScale is set to 0. on every location.
  • Airflow runs in a single location by construction — the shared Airflow home provisions one volume per location, so a second location would be a second, disconnected set of DAGs and logs.
  • Enable KEDA on the GVC before setting keda.enabled: true. Getting the order wrong is accepted with no error and leaves the workers stuck at keda.minScale forever — zero workers, and silently queuing tasks, at minScale: 0.
  • Set postgres.config.password at first install. It is bundled plumbing, but the shipped placeholder is published and the value cannot be changed once the data directory exists.
  • The UI is closed by default and a firewall change takes up to a few minutes to apply, so an endpoint still returning 403 right after an upgrade is not necessarily broken.
  • The first Helm upgrade after an install re-applies resources even with identical values, restarting the webserver and the database — the UI was unavailable for about three minutes in testing. Later upgrades are clean.
  • Changing the admin password means editing the auth secret and restarting the webserver; the password file is rewritten from the secret on every container start.
  • Uninstalling deletes all three volume sets, including the DAGs and logs on the shared Airflow home. Export anything you need first.

External References

Apache Airflow Documentation

Official Apache Airflow documentation

Fernet Key and Encryption at Rest

How Airflow encrypts Connections and Variables

Airflow Security Model

What the Airflow UI and API can do, and who should reach them

CeleryExecutor

The CeleryExecutor and distributed task execution

KEDA Documentation

Event-driven autoscaling documentation

git-sync

The git-sync sidecar

Airflow Template

View the source files, default values, and chart definition