Skip to main content

Overview

Apache Airflow is an open-source platform for programmatically authoring, scheduling and monitoring workflows. This template deploys an Airflow 3.x stack using the CeleryExecutor: one webserver workload running the API server, scheduler, dag-processor and triggerer, Celery workers that execute tasks, Redis as the task broker and PostgreSQL as the metadata database. Workers run at a fixed replica count or, optionally, are scaled by KEDA on the queue length; DAGs are placed on a shared volume or delivered by an optional git-sync sidecar. The admin password, the JWT signing secret and the Fernet key are not template values. They come from a dictionary secret you create before installing, so they never pass through Helm. The web UI ships closed to the internet: it can trigger arbitrary code and decrypt every credential stored in an Airflow Connection, so you reach it over a port-forward until you choose to expose it.
This template deploys into an existing GVC; it does not create one. Every resource is pinned to the single location named by the location value, which must already be a location of that GVC. Other locations the GVC has are left alone: nothing Airflow-related starts in them. There is no multi-location mode, because the shared Airflow home provisions one volume per location and the database and broker are each a single volume.

What Gets Created

Prerequisites

Two secrets must exist before you install, and the GVC must already have the location you configure. Secrets are org-level, so no GVC flag is involved.
1

Confirm the GVC has the location you will configure

location must name a location the GVC already has. Read the current list under spec.staticPlacement.locationLinks:
Set location to one of those names (the default is aws-us-east-1). To add a location, see the GVC reference — every workload in a GVC runs in every location it has, so add locations to a shared GVC deliberately.
2

Create the auth secret

A dictionary secret holding exactly three keys — jwtSecret, fernetKey and adminPassword:
Set airflow.auth.secretName to this name.
3

Read the secret back later

Pass -o yaml — a bare cpln secret reveal prints only a summary table, not the values:
4

Create the database credentials secret

A dictionary secret holding username, password and database. The bundled PostgreSQL initializes from it, and the webserver and workers connect with it:
Set its name in postgres.config.credentialsSecretName. Secret names are org-wide, so give each Airflow release its own secret. The three values are part of Airflow’s connection URLs, so use letters, digits, - and _ only. PostgreSQL reads the secret only when its data is first initialized — see Rotating the Database Password.
5

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

Only when gitSync.enabled is true and the repository is private. An opaque secret with encoding plain whose payload is a personal access token; the sidecar sends it as the Git password:
Set gitSync.auth.secretName to this name. Leave it empty for a public repository or when git-sync is off — setting it while git-sync is disabled is rejected at render.
6

Optional: enable KEDA on the GVC

Only when you set keda.enabled: true. KEDA is a GVC-level setting this template cannot turn on for you; see Keda. Export the GVC, set spec.keda.enabled: true in the exported file, apply it and read it back:
Always start from the exported file. cpln apply replaces a GVC’s spec rather than merging into it, so a hand-written file carrying only spec.keda removes the GVC’s locations.
Create the auth and database credentials secrets before installing, or the deployment wedges silently. A secretName pointing at a secret that does not exist installs “successfully” and then never starts. No container runs, so cpln logs returns zero lines. The one place the reason appears is status.versions[].message:
The command is get-deployments — plain cpln workload get has no versions key. Once the secret exists the workload recovers on its own, slowly; force a redeployment to skip the wait:
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 makes all of them unreadable, and there is no re-encrypt path short of re-entering each one by hand. Back the secret up somewhere you will still have it later.

Installation

Install from the marketplace registry, naming the location your GVC has and the two secrets you created:

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

Location

Every workload in the release is pinned here. GVC locations you do not name run nothing — their deployments read This workload location is deactivated because maxScale is set to 0., which is what a healthy install looks like, not a fault.

PostgreSQL Metadata Database

The metadata database credentials come from your prerequisite dictionary secret (see Prerequisites), named by postgres.config.credentialsSecretName. PostgreSQL reads them only when the data directory is first initialized, so editing the secret afterwards does not change the database password — see Rotating the Database Password.

Redis Broker

Airflow

The webserver runs the API server, scheduler, dag-processor and triggerer in one container pinned to a single replica, so airflow.celeryWorker.replicas (or KEDA) is the only horizontal scaling knob: you scale task execution, not scheduling. The admin account is provisioned by Airflow’s SimpleAuthManager, which has no SSO or LDAP support; the webserver rewrites its password file from the secret on every container start.

Storage

The Airflow home is a shared filesystem mounted at /opt/airflow by the webserver and every Celery worker, so they read the same DAGs and write to the same log directory. The database and broker volumes are sized separately under postgres.volumeset.capacity and redis.volumeset.capacity.

Access

firewallConfig.inboundAllowCIDR applies to the webserver only. With the shipped [] the canonical endpoint returns 403 and the UI is reachable only over a port-forward; a change takes up to a few minutes to take effect. internalAccess controls which workloads inside the org may reach Airflow’s own workloads: Under workload-list you only list your own clients: the template merges in the webserver, workers, database and broker so they can keep reaching each other, and allows the KEDA operator onto the broker when keda.enabled is true.

DAG Delivery With git-sync

When enabled, a git-sync sidecar runs on the webserver and worker workloads, clones the repository under /opt/airflow/dags-sync on the shared volume, and the DAGs folder on both tiers points at the synced checkout (plus gitSync.subPath when set). gitSync.repo and gitSync.branch are required when git-sync is on.

KEDA Autoscaling

KEDA scales the Celery workers on the Redis queue length, including down to zero between runs. With it off, workers run at the fixed airflow.celeryWorker.replicas.
Enable KEDA on the GVC before you set keda.enabled: true. Getting the order wrong is accepted without any error: the worker workload is created, never autoscales, and sits at keda.minScale forever — at minScale: 0 that is zero workers, and every task queues with nothing reporting a problem. On a fresh install the webserver checks the GVC at boot and refuses to start with [airflow] FATAL: keda.enabled is true in your values, but GVC ... has spec.keda.enabled = false; on an established install the same condition is logged as a warning and Airflow keeps running. Enabling KEDA on the GVC clears the refusal on the webserver’s next restart. See Prerequisites for the commands.

Connecting

Verify the webserver is serving through the tunnel:

First Run

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. There is no setup wizard; the admin account already exists.
3

Expose it only if you need to

Run a Helm upgrade with your own CIDR ranges in firewallConfig.inboundAllowCIDR, then allow a few minutes for the firewall change to take effect before testing the canonical endpoint.

API Access

Airflow 3.x issues JWT access tokens signed with your jwtSecret. Request one with the admin credentials and pass it on the v2 API:
Tokens expire after airflow.auth.jwtExpirationTime seconds.

Operations

Deploying DAGs

With git-sync off, DAG files go in /opt/airflow/dags on the shared volume; the dag-processor picks up a new file within airflow.scheduler.dagDirListInterval seconds. Copy a local folder in through the webserver container (--stdin is required, and the archive is base64-encoded because cpln workload exec is not binary-safe):
With gitSync.enabled: true, commit DAGs to the configured repository and branch instead; the sidecar syncs them every gitSync.period.

Backing Up

The chart schedules no backups and no volume set snapshots. The metadata database — every Connection, Variable, DAG run and task history — is the one thing to back up; the broker is transient and DAGs live in your repository or on the shared volume. Two options: Take a snapshot of the database volume set:
Or dump the database with pg_dump, which exists in the postgres:18 image. The dump is base64-encoded in transit because cpln workload exec corrupts binary output:
Back up the auth secret too: the Fernet key inside it is the only way to read the dump’s Connections and Variables.

Restoring a Backup

This restore path has not been exercised end to end against this template. The commands are built from the chart’s configuration (container postgresql, credentials in POSTGRES_USER / POSTGRES_DB) and the tools present in the pinned images, but verify them on a throwaway release before relying on them.
Restore a pg_dump archive into the metadata database, then restart the webserver and workers so they re-read it. Stop triggering DAG runs while the restore runs:
The restored Connections and Variables are readable only under the Fernet key that encrypted them, so the release must point at the same auth secret the backup was taken under. Restoring a volume set snapshot in place (cpln volumeset snapshot restore) is not verified for this template.

Rotating the Admin Password

The webserver rewrites its SimpleAuthManager password file from the secret on every container start, so changing adminPassword and restarting the webserver is enough. A secret change is not picked up by a running replica, so redeploy after changing it. Export the secret, edit only adminPassword (leave fernetKey untouched), re-apply it and confirm the change:
Then restart the webserver:
jwtSecret can be rotated the same way — redeploy both the webserver and RELEASE_NAME-airflow-celery-worker, and every outstanding API token becomes invalid. Never change fernetKey.

Rotating the Database Password

PostgreSQL keeps the password it was initialized with, and a running workload keeps the values it started with, so a rotation takes three steps in order:
  1. Change it in the database (DB_USERNAME and DB_NAME are the secret’s username and database):
  2. Update the password key of the secret: cpln secret edit SECRET_NAME.
  3. Right away, redeploy the webserver and the workers, and wait until get-deployments shows a higher version number for each, alone and ready. Do not redeploy the database: PostgreSQL reads the secret only when it is first initialized, so restarting it only adds downtime.
Connections Airflow already has open keep working after ALTER ROLE; new ones fail until the redeploy finishes, so rotate in a quiet window. Without step 3 the old password stays in use indefinitely: updating a secret does not restart the workloads that read it.

Upgrading from 1.4.x or Earlier

Template version 1.5.0 removed the credentials that 1.4.x took as plain Helm values. Each removed key is rejected at render with a message naming its replacement, so an old values file fails loudly instead of being silently ignored. There is no in-place path from 1.4.x to 2.0.0 — follow Migrating from 1.x, carrying only the new keys:
If your 1.4.x install ran on the shipped CHANGE_ME Fernet key, every stored Connection and Variable is encrypted under a value published in a public repository. The key cannot be rotated, so there is no in-place fix: export what you need, install with a freshly generated key, re-enter the Connections and Variables by hand, and rotate every credential they held at its source.

Migrating from 1.x

Template versions through 1.5.0 created their own GVC. Version 2.0.0 deploys into an existing GVC and does not declare one — and Helm deletes what a chart stops declaring.
Never run a plain helm upgrade on a 1.x release onto 2.0.0. The upgrade drops the GVC from the manifest and deletes it, with every workload, volume set and identity inside — your metadata database and your DAGs — while reporting success. The chart refuses to render when your values still carry the gvc key, and the refusal leaves the running release untouched:
An upgrade run with no values at all has no gvc key to catch and proceeds, so the rule matters more than the guard. To keep the GVC, follow Upgrading In Place from 1.5.0. The alternative is a new release against an existing GVC, as below.
1

Pick the GVC and location for the new release

Choose a GVC that already has the location you will set as location, and enable KEDA on it first if you plan to use it. See Prerequisites.
2

Install 2.0.0 as a new release against the same auth secret

Use a different release name — policy and 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 restored Connection and Variable becomes unreadable. With 2.1.0 or later, also point postgres.config.credentialsSecretName at a new database credentials secret (see Prerequisites).
3

Move the metadata database

Dump from the old release’s database workload, in the GVC the 1.x chart created, and restore into the new one. Both go through base64 because cpln workload exec is not binary-safe:
--no-owner --no-acl let the restore succeed when the new database credentials secret uses a different username from the old release. This data move has not been exercised end to end; see Restoring a Backup.
4

Move the DAGs

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

Verify, then uninstall the old release

Force a redeployment of the new webserver so the scheduler re-reads the restored database, and confirm in the UI that your DAGs, Connections and Variables are present. Only then uninstall the 1.x release — against the GVC you installed it into, where Helm tracks it, not the GVC it created. That uninstall deletes the created GVC and everything still in it.
Values that moved or were removed in 2.0.0:

Upgrading In Place from 1.5.0

Template versions through 1.5.0 created their own GVC, and 2.1.0 deploys into an existing one. A plain cpln helm upgrade across that change deletes the GVC the 1.x release created, with every workload, volume set and identity inside it. These steps mark that GVC “keep” first, so the upgrade leaves it and everything in it in place. It covers a 1.5.0 release (upgrade an older 1.x to 1.5.0 first) that runs in one location; with more than one entry in gvc.locations, use Migrating from 1.x instead. Create the database credentials secret under a new name first, holding the values the database runs with today. 1.5.0 keeps them in RELEASE_NAME-airflow-config, and the upgrade deletes that secret:
YOUR_CURRENT_DATABASE is postgres.config.database of the 1.5.0 release, airflow unless you changed it. Then mark the GVC and upgrade:
  • OLD_GVC_NAME is the GVC the 1.x release created (its gvc.name), not the GVC you ran the original install against.
  • The upgrade output must include Skipped deletion of /org/ORG_NAME/gvc/OLD_GVC_NAME. If it does not, stop: the GVC was not protected.
values-new.yaml is values-old.yaml with these changes:
If values-old.yaml has no gvc key, the release ran on 1.5.0’s defaults: OLD_GVC_NAME is airflow and location is aws-eu-central-1. Keep keda.enabled as 1.5.0 had it: the GVC it created has KEDA on exactly when that was true, and true against a GVC with KEDA off stops the webserver from starting. The upgrade restarts PostgreSQL, Redis, the webserver and the workers, so Airflow is unavailable for a few minutes. A workload can wait with is not allowed to reveal the secret in get-deployments while access to the new secrets propagates; it clears by itself (see Upgrading from 2.0.0). Afterwards the GVC belongs to you, not the release: cpln helm uninstall leaves it in place, and you delete it with cpln gvc delete OLD_GVC_NAME. Details: the keep-gvc script documentation.

Upgrading from 2.0.0

Template version 2.1.0 removed postgres.config.username, postgres.config.password and postgres.config.database: the metadata database credentials are now a prerequisite secret. Carrying the old keys forward fails the render. 2.0.0 created that secret itself (RELEASE_NAME-airflow-config), so this upgrade deletes it, and the chart refuses that name for the new secret. Before upgrading, create a secret under a new name holding the values your database was initialized with — not new ones, or Airflow loses its database connection:
YOUR_CURRENT_DATABASE is postgres.config.database of the 2.0.0 release — airflow unless you changed it; the old secret does not hold it. Then upgrade with postgres.config.credentialsSecretName set to the new name and the three old keys removed. An upgrade that passes no values at all uses the default name my-airflow-db-credentials, so create the secret under that name instead if you upgrade that way. The upgrade restarts PostgreSQL, the webserver and the workers (they now read the new secret), so Airflow is briefly unavailable. The new version of a workload can also wait for several minutes, with get-deployments reporting that it is not allowed to reveal the secret, while access to the new secret propagates; the previous version keeps running meanwhile, and it clears by itself (or wait a few minutes, then cpln workload force-redeployment the waiting workload).

Scaling and Availability

  • Workers scale; the webserver does not. Raise airflow.celeryWorker.replicas for a fixed pool, or enable KEDA to scale on queue length (see KEDA Autoscaling). The webserver, database and broker are each a single replica on a single volume.
  • 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. Run a separate release per location if you need Airflow in two places.
  • The first Helm upgrade after an install can restart the bundled database and the webserver even when nothing changed. Expect a brief interruption of the UI; later upgrades with unchanged values are clean.
  • A wide simultaneous fan-out can fail tasks without starting them. The Celery executor’s Redis transport in this worker image has an upstream race when many tasks are dispatched at the same instant; the affected tasks are marked failed with no hostname, no start time and an empty log. Stagger task submission rather than fanning out wide at once.

Troubleshooting

Symptom: every resource reports created, no workload becomes ready, cpln logs is empty.Cause: the auth secret (or the git token secret) does not exist, so no container ever starts.Fix: read the deployment message, which names the missing secret:
Create the secret exactly as in Prerequisites, then force a redeployment rather than waiting for the automatic recovery.
Symptom: the install succeeded, all four workloads sit at zero replicas, and cpln workload get-deployments shows that message for every location.Cause: location names a location the GVC does not have. The platform stores the placement without validating it.Fix: compare location with spec.staticPlacement.locationLinks from cpln gvc get GVC_NAME -o yaml, then either add the location to the GVC or upgrade the release with a location the GVC already has. Seeing the message on the GVC’s other locations while the configured one runs is normal.
Symptom: the webserver crash-loops on a fresh install with that line in cpln logs.Cause: keda.enabled: true without KEDA enabled on the GVC. The worker workload was accepted anyway and will never autoscale.Fix: enable KEDA on the GVC as in Prerequisites; the webserver passes the check on its next restart. Or set keda.enabled: false and use airflow.celeryWorker.replicas.
Symptom: keda.enabled: true, the worker workload is healthy at keda.minScale (zero workers at minScale: 0), queued tasks never run, nothing is logged.Cause: KEDA is not enabled on the GVC, and the install was onto an existing Airflow home, so the webserver only warned instead of refusing to start. Under internalAccess.type: workload-list the template already allows the KEDA operator onto the broker, so that is not the cause.Fix: read spec.keda.enabled with cpln gvc get GVC_NAME -o yaml and enable it as in Prerequisites.
Symptom: several tasks of a wide DAG run are marked failed with an empty log, no hostname and no start time, while the rest succeed.Cause: an upstream race in the Celery executor’s Redis transport (module 'redis' has no attribute 'client' from kombu/transport/redis.py) when many tasks are dispatched at the same instant.Fix: stagger task submission, and count completed task instances rather than relying on the UI to flag the failures.
Symptom: upgrading a 1.x release to 2.0.0 fails at render with the message quoted in Migrating from 1.x.Cause: your values still carry the 1.x gvc key. The refusal is deliberate; a plain upgrade would delete the GVC the 1.x chart created.Fix: do not remove the key and retry. Follow Upgrading In Place from 1.5.0, or install 2.0.0 as a new release and move the data as described in Migrating from 1.x.
Symptom: the webserver is ready but its public URL returns 403.Cause: firewallConfig.inboundAllowCIDR is [] (the default), or a recent change has not taken effect yet.Fix: use the port-forward from First Run, or add your CIDR ranges and allow a few minutes for the change to apply.
Symptom: after editing the password key of the database credentials secret and redeploying, the webserver and workers fail to authenticate against PostgreSQL.Cause: PostgreSQL reads POSTGRES_PASSWORD only when the data directory is initialized, so the database still has the old password while Airflow’s connection string carries the new one.Fix: change the password in the database first, as described in Rotating the Database Password, or put the old value back in the secret and redeploy the webserver and workers.
Symptom: upgrading a 2.0.0 release fails at render with airflow 2.1.0: postgres.config.username, postgres.config.password and postgres.config.database were REMOVED.Cause: your values still carry the 2.0.0 plaintext database credentials.Fix: create the database credentials secret holding your current values and remove the old keys — see Upgrading from 2.0.0.

Important Notes

  • Create the auth and database credentials secrets before installing. Without either the deployment wedges with no log output; cpln workload get-deployments is the one place the missing secret is named.
  • The Fernet key can never be rotated. Generate it once, keep it, and back it up with the auth secret.
  • Never run a plain helm upgrade on a 1.x release onto 2.0.0 — it deletes the GVC the 1.x chart created and everything in it. See Upgrading In Place from 1.5.0 or Migrating from 1.x.
  • location must be a location of your GVC. Otherwise the install succeeds and nothing starts anywhere.
  • Enable KEDA on the GVC before setting keda.enabled: true, or the workers sit at keda.minScale forever with no error.
  • Rotating the database password needs ALTER ROLE plus a forced redeployment of the webserver and workers — editing the secret alone changes nothing; see Rotating the Database Password.
  • The UI is closed by default. Reach it with cpln port-forward; a firewall change takes a few minutes to apply.
  • The first Helm upgrade after an install can restart the database and webserver even with unchanged values; expect a brief interruption.
  • Changing the admin password means editing the secret and forcing a webserver redeployment; see Rotating the Admin Password.
  • Uninstalling deletes all three volume sets, including the DAGs and logs on the shared Airflow home. Back up the metadata database first; see Backing Up.

External References

Apache Airflow Documentation

Official Apache Airflow documentation

Fernet Key and Encryption at Rest

How Airflow encrypts Connections and Variables

Airflow Public API Authentication

Requesting and using JWT access tokens

CeleryExecutor

The CeleryExecutor and distributed task execution

KEDA Documentation

Event-driven autoscaling documentation

Airflow Template

View the source files, default values, and chart definition