You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ansible/deploy.yml doesn't actually use Ansible idiomatically. It's a
strictly sequential procedure — download bundle, extract, pull+merge app
packages, pull+decrypt+merge env packages, symlink switch, run ./deploy.sh, prune old releases — not a converging desired-state
playbook. "Recreate release directory" even does state: absent then
recreates unconditionally on every run, the opposite of idempotent
convergence. This matches #107's framing exactly: flightdeck is a
Capistrano-style deployment system (versioned releases, current symlink,
retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
own model — SSH in, run a sequence of tasks, switch a symlink — is what
Fabric provides for Python, the same way Capistrano provides it for Ruby.
Concrete friction with Ansible specifically
No shared-function mechanism across tasks. Each shell: task is an
independent subprocess — there's no way to define a bash function in one
task and reuse it in another without a checked-in script file. This is
the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse owner/repo@tag[:asset], resolve @latest via gh release view,
download" blocks, hand-copied because Ansible has no better answer short
of extracting a separate shell script. In Fabric this is just an
importable Python function — the duplication wouldn't exist to begin
with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
Zero unit test coverage of the deploy logic itself.encrypt-env
and load-yaml-matrix are both Python, both have real unit tests
(unittest, PyYAML). ansible/deploy.yml is YAML + Jinja + embedded
bash — the only verification it gets is --syntax-check and manually
tracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
that's not the same as testing the ref-parsing/collision-detection logic
itself). A Fabric-based deploy script would be ordinary Python, testable
the same way the rest of this repo's automation already is.
The inventory is ceremony, not a real feature.deploy-shared.yml
builds a throwaway JSON inventory via jq on every run purely to
satisfy Ansible's -i requirement — nothing here uses Ansible's actual
host-grouping/inventory capabilities.
Real, recurring cost for capabilities we don't use.ansible-core
is installed fresh via unpinned pip install on every single deploy run
(deploy-shared.yml's "Install ansible-core" step) — paying setup cost
for idempotency/convergence/templating machinery the playbook doesn't
actually lean on.
Refined direction: push-based deploy, decided during #114's design
Discussion while designing #114 (per-app vault delivery) converged on a
concrete shape for what a Fabric-based ansible/deploy.yml replacement
should actually look like — this is now the leading design, not just "port
the playbook to Python":
All ref-resolution/download logic moves to the CI runner. Parsing owner/repo@tag[:asset], resolving @latest via the GitHub API,
downloading the resolved asset — all of it runs as ordinary Python on
the GitHub Actions runner, written once, reused for the machinery
bundle, every app bundle, and every encrypted vault asset. The server
never calls gh itself and doesn't need it installed — a real
prerequisite reduction from what README currently documents.
Fabric pushes files to the server instead of the server pulling
them. Downloaded app bundles and still-encrypted vault blobs are
transferred to the server directly (SSH/SCP), rather than the server
reaching out to GitHub itself.
Decryption stays strictly server-side. The server's private age key
never leaves it, same as today. The server's only vault-related
capability is running sops decrypt on a single file it's given — a
generic, dumb, reusable primitive with no knowledge of refs, targets, or
merging.
No composition/merge logic lives on the server at all — but collision
detection still happens, in CI, before anything is pushed. SOPS's
dotenv output format only encrypts values; key names stay in
cleartext (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more
than one entry, Fabric downloads each .sops.env asset and compares key
names across them directly — no decryption needed for this check at
all. Any duplicate key fails the build right there, before anything
touches the server. Only once that check passes does Fabric push the
files and tell the server to decrypt each one and plain cat them
together into apps/{app}/.env. The server still never understands
composition; it just never sees a colliding set in the first place.
Update, superseding the below: this went further than "almost
nothing" — the session that implemented this also decided flightdeck
will have no manual administration flow at all, ever (no server SSH
console access, no local quick-start either). With that decided,
decryption and config-template rendering moved to the CI runner too
(see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives: up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
were all deleted outright, no replacements. The target host's only
dependencies are Docker and Docker Compose. deploy/deploy.py issues docker compose pull/up directly per app over SSH; there is no
server-side script layer of any kind left to describe.
Considerations against
Ansible's SSH connection handling, privilege escalation (become), and
retry semantics are battle-tested. Re-implementing that in Fabric means
re-verifying connection and privilege-escalation behavior against real
hosts, not just a drop-in rewrite.
Ansible is more broadly known in ops contexts than Fabric — a real
discoverability/onboarding tradeoff, not just a technical one.
Implemented, going further than originally scoped here (see the "Update"
note above) — ansible/ is deleted, deploy/deploy.py is the entire
deploy mechanism, verified via real unittest coverage of every module
plus a matrix trace-through of the updated targets//vaults/ schema.
No live deploy to hawkeye yet (still gated on infra prep), same
verification-rigor bar as originally proposed here.
Related
#107 (Capistrano-style framing this migration leans into), #103 (the
concrete duplication problem this resolves at the root instead of
patching — becomes moot once ref-resolution is a Python function), #114
(per-app vault delivery is the concrete feature this push-based model was
designed to carry).
Context
ansible/deploy.ymldoesn't actually use Ansible idiomatically. It's astrictly sequential procedure — download bundle, extract, pull+merge app
packages, pull+decrypt+merge env packages, symlink switch, run
./deploy.sh, prune old releases — not a converging desired-stateplaybook. "Recreate release directory" even does
state: absentthenrecreates unconditionally on every run, the opposite of idempotent
convergence. This matches #107's framing exactly: flightdeck is a
Capistrano-style deployment system (versioned releases,
currentsymlink,retained history), not a Nomad/Kubernetes-style reconciler. Capistrano's
own model — SSH in, run a sequence of tasks, switch a symlink — is what
Fabric provides for Python, the same way Capistrano provides it for Ruby.
Concrete friction with Ansible specifically
shell:task is anindependent subprocess — there's no way to define a bash function in one
task and reuse it in another without a checked-in script file. This is
the direct cause of Deduplicate release-ref resolution logic in ansible/deploy.yml #103: four near-identical ~30-line "parse
owner/repo@tag[:asset], resolve@latestviagh release view,download" blocks, hand-copied because Ansible has no better answer short
of extracting a separate shell script. In Fabric this is just an
importable Python function — the duplication wouldn't exist to begin
with, and Deduplicate release-ref resolution logic in ansible/deploy.yml #103 would become moot rather than needing its own fix.
encrypt-envand
load-yaml-matrixare both Python, both have real unit tests(
unittest,PyYAML).ansible/deploy.ymlis YAML + Jinja + embeddedbash — the only verification it gets is
--syntax-checkand manuallytracing a generated matrix through by hand (done for feat: deploy rybbit for rubykatzen.com through flightdeck itself #102/feat: move apps to targets, support env_refs as a list #110, but
that's not the same as testing the ref-parsing/collision-detection logic
itself). A Fabric-based deploy script would be ordinary Python, testable
the same way the rest of this repo's automation already is.
deploy-shared.ymlbuilds a throwaway JSON inventory via
jqon every run purely tosatisfy Ansible's
-irequirement — nothing here uses Ansible's actualhost-grouping/inventory capabilities.
ansible-coreis installed fresh via unpinned
pip installon every single deploy run(
deploy-shared.yml's "Install ansible-core" step) — paying setup costfor idempotency/convergence/templating machinery the playbook doesn't
actually lean on.
Refined direction: push-based deploy, decided during #114's design
Discussion while designing #114 (per-app vault delivery) converged on a
concrete shape for what a Fabric-based
ansible/deploy.ymlreplacementshould actually look like — this is now the leading design, not just "port
the playbook to Python":
owner/repo@tag[:asset], resolving@latestvia the GitHub API,downloading the resolved asset — all of it runs as ordinary Python on
the GitHub Actions runner, written once, reused for the machinery
bundle, every app bundle, and every encrypted vault asset. The server
never calls
ghitself and doesn't need it installed — a realprerequisite reduction from what README currently documents.
them. Downloaded app bundles and still-encrypted vault blobs are
transferred to the server directly (SSH/SCP), rather than the server
reaching out to GitHub itself.
never leaves it, same as today. The server's only vault-related
capability is running
sops decrypton a single file it's given — ageneric, dumb, reusable primitive with no knowledge of refs, targets, or
merging.
detection still happens, in CI, before anything is pushed. SOPS's
dotenv output format only encrypts values; key names stay in
cleartext (
APPS_DOMAIN=ENC[...]). So when an app'senv_refshas morethan one entry, Fabric downloads each
.sops.envasset and compares keynames across them directly — no decryption needed for this check at
all. Any duplicate key fails the build right there, before anything
touches the server. Only once that check passes does Fabric push the
files and tell the server to decrypt each one and plain
catthemtogether into
apps/{app}/.env. The server still never understandscomposition; it just never sees a colliding set in the first place.
nothing" — the session that implemented this also decided flightdeck
will have no manual administration flow at all, ever (no server SSH
console access, no local quick-start either). With that decided,
decryption and config-template rendering moved to the CI runner too
(see Consider decrypting vaults on the CI runner instead of server-side #116), and literally nothing server-side survives:
up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.shwere all deleted outright, no replacements. The target host's only
dependencies are Docker and Docker Compose.
deploy/deploy.pyissuesdocker compose pull/updirectly per app over SSH; there is noserver-side script layer of any kind left to describe.
Considerations against
become), andretry semantics are battle-tested. Re-implementing that in Fabric means
re-verifying connection and privilege-escalation behavior against real
hosts, not just a drop-in rewrite.
discoverability/onboarding tradeoff, not just a technical one.
production deploy mechanism) — needs a careful verification plan, not a
quick swap, and shouldn't block anything currently in flight (Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's
first real hawkeye deployment).
Proposal
Implemented, going further than originally scoped here (see the "Update"
note above) —
ansible/is deleted,deploy/deploy.pyis the entiredeploy mechanism, verified via real
unittestcoverage of every moduleplus a matrix trace-through of the updated
targets//vaults/schema.No live deploy to hawkeye yet (still gated on infra prep), same
verification-rigor bar as originally proposed here.
Related
#107 (Capistrano-style framing this migration leans into), #103 (the
concrete duplication problem this resolves at the root instead of
patching — becomes moot once ref-resolution is a Python function), #114
(per-app vault delivery is the concrete feature this push-based model was
designed to carry).