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.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.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: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.Create the auth secret
jwtSecret, fernetKey and adminPassword:airflow.auth.secretName to this name.Read the secret back later
-o yaml — a bare cpln secret reveal prints only a summary table, not the values:Create the database credentials secret
username, password and database. The bundled PostgreSQL initializes from it, and the webserver and workers connect with it: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.Optional: create a git token secret for a private DAG repository
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: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.Optional: enable KEDA on the GVC
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: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.Installation
Install from the marketplace registry, naming the location your GVC has and the two secrets you created:UI
CLI
Terraform
Pulumi
Configuration
Location
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
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
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
/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:
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
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
airflow.celeryWorker.replicas.
Connecting
First Run
Install with the UI closed
firewallConfig.inboundAllowCIDR: [].Tunnel to the UI and sign in
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.Expose it only if you need to
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 yourjwtSecret. Request one with the admin credentials and pass it on the v2 API:
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):
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:pg_dump, which exists in the postgres:18 image. The dump is base64-encoded in transit because cpln workload exec corrupts binary output:
Restoring a Backup
Restore apg_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:
cpln volumeset snapshot restore) is not verified for this template.
Rotating the Admin Password
The webserver rewrites itsSimpleAuthManager 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:
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:-
Change it in the database (
DB_USERNAMEandDB_NAMEare the secret’susernameanddatabase): -
Update the
passwordkey of the secret:cpln secret edit SECRET_NAME. -
Right away, redeploy the webserver and the workers, and wait until
get-deploymentsshows 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.
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: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.Pick the GVC and location for the new release
location, and enable KEDA on it first if you plan to use it. See Prerequisites.Install 2.0.0 as a new release against the same auth secret
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).Move the metadata database
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.Move the DAGs
gitSync.enabled: true there is nothing to do — the sidecar clones onto the new volume. Otherwise copy them off the old shared volume:Verify, then uninstall the old release
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 plaincpln 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_NAMEis the GVC the 1.x release created (itsgvc.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:
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 removedpostgres.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.replicasfor 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
The install succeeded but nothing starts and cpln logs returns no lines
The install succeeded but nothing starts and cpln logs returns no lines
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:Every location reports This workload location is deactivated because maxScale is set to 0
Every location reports This workload location is deactivated because maxScale is set to 0
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.Webserver log shows FATAL: keda.enabled is true in your values, but GVC has spec.keda.enabled = false
Webserver log shows FATAL: keda.enabled is true in your values, but GVC has spec.keda.enabled = false
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.Workers never scale and tasks stay queued with no error
Workers never scale and tasks stay queued with no error
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.Tasks fail without ever starting after a large fan-out
Tasks fail without ever starting after a large fan-out
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.helm upgrade prints the gvc values key was REMOVED
helm upgrade prints the gvc values key was REMOVED
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.The canonical endpoint returns 403
The canonical endpoint returns 403
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.Airflow cannot connect to the metadata database after changing the database secret
Airflow cannot connect to the metadata database after changing the database secret
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.Upgrade fails because postgres.config.username, password and database were REMOVED
Upgrade fails because postgres.config.username, password and database were REMOVED
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-deploymentsis 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 upgradeon 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. locationmust 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 atkeda.minScaleforever with no error. - Rotating the database password needs
ALTER ROLEplus 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.