dev-k8s │ staging │ prod ⊘ │ mwaa-sit atc · airflow 3
────────────────────────────────────────────────────────────────────────────
◉ dev-k8s k8s ▸ demo-cluster ▸ airflow-demo ▸ :54312 ● 78ms v3.1.0
★ watching 5 ✗ failing: vendor_feed_import
────────────────────────────────────────────────────────────────────────────
DAG SCHED LAST 20 RUNS LAST Ø
ingest_orders_to_warehouse @hourly ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ ✓ 12m ago 8m42s
sync_customers_to_crm 0 2 * * * ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▒ ◐ running 9m15s
vendor_feed_import @hourly ▇▇▇▇▇▇▇▇▇▇▇▇▚▇▇▇▇▇▇▚ ✗ 9m ago 7m36s
rebuild_search_index @daily ▇▇▇▇·▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ ✓ 4h ago 22m04s
export_finance_extract 0 6 * * 1-5 ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ ✓ 31m ago 1m08s
────────────────────────────────────────────────────────────────────────────
tab next env 1-9 jump a add f unstar ⏎ runs t trigger q quit
The problem
Production Airflow rarely sits on a public URL. It runs inside a private EKS cluster,
or on AWS MWAA with the webserver access mode set to private. Checking whether last
night's DAG failed then means a manual kubectl port-forward, a bastion host,
a VPN, or an AWS console round-trip — every single time.
atc collapses that into one terminal session. It owns the tunnel, mints and renews the token, and keeps the connection path — which environment, reached by which mechanism, at what latency — on screen at all times, so you never trigger a DAG against the wrong environment.
Quickstart
No AWS account, no cluster, no config file, no network:
uvx airflow-atc --fake
That launches the full TUI against a deterministic fake environment. Tabs, the watch list, run history, the empty-log finding and the trigger confirmation all behave exactly as they do against a real Airflow. It is also what the entire test suite runs on.
Install it properly
# as a tool on your PATH
uv tool install airflow-atc
# or with pip
pip install airflow-atc
atc configure # discovery wizard
atc # go
Requirements: Python 3.11+. For k8s profiles you need
kubectl on your PATH and a working context. For mwaa profiles you
need AWS credentials resolvable through the normal chain (profile, environment, SSO,
instance role).
What you get
🛰️ The connection path, always on screen
k8s ▸ cluster ▸ namespace ▸ :port ● 78ms v3.1.0 sits above everything.
You can never be unsure which environment you are pointed at.
🔌 A tunnel that heals itself
One kubectl port-forward for the whole session. If it dies, atc notices,
rebuilds it, and drops the token bound to the dead socket.
🔑 MWAA auth that falls back
Tries mwaa:InvokeRestApi; on a permissions gap falls back to
CreateWebLoginToken. Automatic, and pinnable per profile.
🗂️ Tabs, not reconnects
One tab per environment with its own connection, watch list and cursor. Tabs connect lazily — starting atc never dials everything you configured.
⭐ O(favourites), not O(DAGs)
The main screen is a starred watch list. A real environment has hundreds of DAGs; rendering all of them costs one call each and tells you nothing.
🕵️ An empty log is a finding
When Airflow returns nothing for a failed task, atc says so — and on Kubernetes reads the log off the scheduler's PVC instead of showing a blank pane.
🛑 Read-only, marked before you touch it
readonly = true puts ⊘ on the tab and refuses the trigger key
outright. The one write path sits behind a confirmation modal.
🧪 Tests that need no credentials
Everything the UI consumes goes through an 8-method provider protocol, so the whole suite — headless UI tests included — runs with no AWS, cluster or network.
Configure
Run atc configure. It discovers your kubectl contexts and namespaces, your
AWS profiles and your MWAA environments, and writes
~/.config/atc/config.toml. Running atc with no configuration at all
launches the same wizard, so there is no "file not found" dead end.
Or write the config by hand
default = "dev-k8s"
[profiles.dev-k8s]
type = "k8s"
context = "YOUR-KUBE-CONTEXT"
namespace = "YOUR-AIRFLOW-NAMESPACE"
service = "airflow-api-server-cluster-ip-service"
port = 8080
secret = "airflow-secret"
[profiles.mwaa-prd]
type = "mwaa"
environment = "YOUR-MWAA-ENV-NAME"
region = "ap-southeast-2"
aws_profile = "YOUR-AWS-PROFILE"
strategy = "auto" # auto | invoke_rest_api | web_login_token
readonly = true # ⊘ on the tab; `t` is refused outright
No secrets go in this file. Kubernetes admin passwords are read from a cluster secret at runtime; AWS credentials come from the standard chain.
How it is built
Everything the UI consumes goes through one 8-method AirflowProvider
protocol. That single seam is why --fake exists, why the headless UI tests need
no credentials, and why supporting a new Airflow deployment shape means writing one file
rather than touching the TUI.
tui/app.py · tui/render.py Textual screens + a pure renderer
│
▼ AirflowProvider (8 methods)
core/provider.py RestApiProvider · Airflow 3 · /api/v2
│
├── providers/k8s.py self-healing port-forward, JWT, PVC log fallback
├── providers/mwaa.py InvokeRestApi → CreateWebLoginToken fallback
└── providers/fake.py --fake, and the whole test suite
The dependency direction is strictly one-way: tui → core → providers.
core/models.py imports nothing from the project, and tui/render.py
imports only rich and the models — which is what makes the formatting layer
unit-testable on its own.
FAQ
Does atc support Airflow 2?
No. atc is Airflow 3 only and speaks /api/v2. An Airflow 2 environment is
refused by name rather than failing obscurely. There are no Airflow 2 fallbacks by design.
Can atc break my production environment?
There is exactly one call that mutates a remote Airflow: trigger(). It is
gated by a confirmation modal and refused outright on profiles marked
readonly = true. Everything else is read-only.
Does it replace the Airflow web UI?
No, and it does not try to. There is no graph view, no Gantt chart, no DAG authoring. atc is for the operating loop: what is failing, what did the log say, run it again.
Where do my credentials go?
Nowhere. Kubernetes admin passwords are read from a cluster secret at runtime, AWS
credentials come from the standard chain, and MWAA web sessions are cached in memory for
12 hours. config.toml holds no secrets.
Is it safe to run against production?
Mark the profile readonly = true. The tab then carries a ⊘
marker before you are looking at it, and the trigger key is refused rather than
confirmed.
More
| Resource | What is there |
|---|---|
| GitHub repository | Source, issues, releases |
| PyPI · airflow-atc | Released wheels and sdists |
| README 简体中文 | Full Chinese documentation |
| llms.txt | Structured summary for LLMs and AI agents |
| CONTRIBUTING.md | Layer boundaries and the FakeProvider testing rule |
| CHANGELOG.md | What changed, per release |