Skip to content

airflow 2.0.0 — deploy into an existing GVC - #519

Merged
jacobecox merged 4 commits into
mainfrom
claude/airflow-gvc
Aug 29, 2026
Merged

airflow 2.0.0 — deploy into an existing GVC#519
jacobecox merged 4 commits into
mainfrom
claude/airflow-gvc

Conversation

@jacobecox

Copy link
Copy Markdown
Contributor

Review diff: git diff ebe78a1..HEAD -- airflow/versions/2.0.0/
(ebe78a1 is the verbatim cp -R of 1.5.0; everything real is in the commits after it.)

Why a major version

The chart no longer creates a GVC. The gate holds: the in-place helm upgrade from 1.5.0 with 1.5.0's real values file was refused at render before any resource was touched.

Two product changes, both forced by the conversion

1. Single location, by construction. A shared volumeset provisions one volume per location, so two locations would mean two Airflow homes that cannot see each other's DAGs or logs, while the metadata DB and broker are each one volume on one replica. gvc.locations therefore became a single location string, and the default installs on a one-location GVC. The scheduler, dag-processor and triggerer share the webserver container pinned to 1 replica; only Celery workers scale.

2. KEDA is now opt-in, default false — it is a GVC-level setting the chart can no longer set. This is a change from 1.x, and the reason is measured rather than cautious: a metric: keda workload in a KEDA-disabled GVC is accepted silently and sits at minScale — at 0 that is zero replicas, desiredScale: None, no versions and no message. A dead install with no diagnostic surface anywhere.

A latent multi-instance bug, fixed

In 1.5.0 the webserver and Redis carried no options at all, so on a multi-location GVC they would have started one instance per location — N schedulers, N brokers, N split queues. The 0/0 + localOptions pattern now confines every tier, verified live.

A defect testing found, and the control that found it

While running the 403 control, the boot check warned "GVC 'test-gvc' has spec.keda.enabled = false" — while cpln gvc get returned {'enabled': True} at that same moment. The real cause was a missing view grant.

A 403 body is valid, non-empty JSON, so the retry loop broke and the parse succeeded; spec.keda.enabled is absent from an error body and defaults to false, indistinguishable from a genuine false. The location check already defended against this shape; the KEDA check did not.

Fixed by gating both checks on one "the read actually succeeded" flag from the HTTP status, rather than bolting a second emptiness test onto the KEDA branch — an error body can be well-formed enough to survive an emptiness test, and only the status distinguishes it. Verified on both arms:

403 arm:  [airflow] GVC read returned HTTP 403 (attempt 1/3 … 3/3).
          [airflow] WARNING: could not read GVC 'test-gvc' (no 200 response) -- skipping
                    the GVC location and KEDA checks. If this persists, check that the
                    identity has 'view' on the GVC via the chart's GVC policy.
          occurrences of the old false message: 0

200 arm:  [airflow] GVC location check OK -- 'test-gvc' has 'aws-us-east-1'
          no KEDA complaint  -> the fix did not blunt the check it protects

Worth reading: the first attempt at that verification failed to reproduce — the policy was deleted and the workload redeployed immediately, and the read still succeeded. That is the ~4-minute authorization edge-cache. Waiting it out and redeploying produced the 403 arm. A single probe after a policy change is inconclusive in either direction, and here it would have read as "the fix does not work".

Testing

16 rows, 0 outstanding FAIL. Proven on real infrastructure: a DAG executing end to end on a Celery worker; the boot GVC read returning 200 from the real endpoint with the 403 control; both KEDA arms (hard-fail against a KEDA-off GVC, then real autoscaling once enabled); the multi-instance confinement; the wrong-location signal; and the fresh-vs-established asymmetry on both arms.

render-vs-stored 0 unexplained differences — two real backfills (recoveryPolicy: retain on the postgres and redis volume mounts) were declared in the chart, not normalized away in the checker. Drift gate fully Unchanged by the second upgrade, with the gate still clean afterwards.

Probes that settled design questions before build

  • localOptions[].autoscaling accepts a keda block, stored verbatim — the only reason KEDA and the 0/0 pattern can coexist.
  • target under metric: keda is rejected at apply (target is not allowed when metric is 'keda') and is invisible to helm template.
  • The GVC-read bound measured in the pinned image against genuinely slow failures: blackhole 6 s, accept-never-respond 6 s, 3-try loop 24 s, unbounded control still hanging at 25 s. NXDOMAIN returned 0 s and was discarded as proving nothing.

Two things for you

  • cpln apply REPLACES a GVC's spec — applying a file containing only spec.keda wiped locationLinks and endpointNamingFormat from test-gvc during the build. Restored and verified byte-for-byte, and now recorded in CLAUDE.md. The safe form is a -o yaml-slim round-trip. This is the opposite of the workload PATCH endpoint, which deep-merges.
  • A deliberate gap, not an oversight: if location names a location the GVC lacks, every workload gets zero replicas, so no container runs and the boot check cannot report it. The honest signal is "the release deployed and nothing started", which the README and values comment both state. Closing it would cost an unpinned guard workload for the life of the install.

Untested

The 1.x → 2.0.0 data move. Only tool availability was verified (pg_dump/pg_restore in postgres:18, tar in the airflow image, cpln workload exec --stdin); the README's steps were read against the chart but the piping itself is unproven, and both the README and briefing say so.

Briefing updated.

🤖 Generated with Claude Code

Jacob Cox and others added 4 commits August 28, 2026 17:32
Airflow was one of eleven templates that created their own GVC. It did so
because KEDA is a GVC-level setting, so the chart wanted to own the GVC to
turn it on. That is no longer allowed: a chart that creates a GVC and later
stops declaring it makes `helm upgrade` prune it, taking every workload,
volumeset and identity inside.

Three-layer defence, as in the six conversions before this one:

1. A render-time `fail` when the legacy `gvc` values key is present, tested
   against the shipped 1.5.0 values file (the real upgrade path), not just
   `--set`.
2. `defaultOptions.minScale/maxScale: 0` on all four workloads with one
   complete `localOptions` entry for the configured location, so a GVC
   location the values do not name runs nothing.
3. A boot-time GVC read in the webserver, scoped `targetKind: gvc` +
   `targetLinks` to the one install GVC, fatal on a fresh Airflow home and a
   warning on an initialised one.

KEDA becomes an opt-in defaulting to false, because the chart can no longer
enable it. Measured: a `metric: keda` workload is accepted into a GVC with
KEDA off, never autoscales, and sits at minScale -- at minScale 0 that is
zero replicas with no error anywhere. The boot check reports it.

Also in this version: `location` replaces `gvc.locations` (Airflow is
single-location by construction -- a shared volumeset provisions one volume
per location), `airflow.celeryWorker.replicas` sets the worker count with
KEDA off, `internalAccess` is new with the ownWorkloadLinks merge, and the
webserver, Redis and Postgres tiers are pinned to one replica -- before this
they carried no options at all and were eligible to scale out per GVC
location.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by the test round while running the 403 control. The warning told the user
their GVC had KEDA disabled; `cpln gvc get` at that same moment returned
`{'enabled': True}`. The real cause was a missing `view` grant.

A 403 body is valid, non-empty JSON, so the retry loop broke and the script
parsed it. `spec.keda.enabled` is absent from an error body and defaults to
False -- indistinguishable from a genuine `enabled: false`. The location check
already defended against this shape (`[ -z "$GVC_LOCS" ]` -> warn and skip); the
KEDA check had no equivalent.

Fixed by gating BOTH checks on one "the read actually succeeded" flag taken from
the HTTP status (`curl -w`), rather than bolting a second emptiness test onto the
KEDA branch: an error body can be well-formed enough to survive an emptiness
test, and only the status distinguishes it. Non-200 attempts now say so, and the
skip message names the likely cause.

Fails closed either way -- the wrong-but-safe outcome was a refusal to start --
so this is about the diagnostic, which was sending users to the GVC's KEDA
setting when the problem was the identity's grant.

Both rendered arms pass `sh -n` and `bash -n`; the script runs under an explicit
bash shebang, and the `$'\n'` extraction was verified in both shells.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lumes

`render-vs-stored` on a live install reported `recoveryPolicy: retain` backfilled
on the volume mounts of the postgres and redis workloads -- a permanent diff in
the deployment UI from the moment they are created. The webserver and celery
mounts already matched, so this was two lines, not a pattern.

Declared rather than normalized away in the checker: that list is the record of
what we have accepted, and a wrong entry blinds the gate permanently.

Verified on real infrastructure: 0 unexplained differences afterwards, and the
drift gate is fully `Unchanged` by the second no-op upgrade with the gate still
clean after both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jacobecox
jacobecox merged commit 5647f35 into main Aug 29, 2026
6 checks passed
@jacobecox
jacobecox deleted the claude/airflow-gvc branch August 29, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant