# Deploy hrafn (fleet browser-fetch service) to a Docker host. # # Single source of truth for "how hrafn is deployed" — CI and manual runs # take the same path. Modelled on nevermore's playbook, with two deliberate # differences noted below. # # Usage (from this repo's root): # elway --playbook playbooks/deploy.yaml # # Requires a build-context tarball at dist/hrafn-context.tgz. The CI # workflow builds it; for a manual run, build it the same way: # mkdir -p dist && tar czf dist/hrafn-context.tgz \ # Dockerfile compose.yaml pyproject.toml README.md src # # DIFFERENCE 1 — tarball instead of per-file upload steps. nevermore # enumerates every source file as its own upload step. That is explicit, # but it fails OPEN: add a new src/hrafn/*.py and forget the matching # step, and the deploy silently ships without it. hrafn's build context # is a whole package, so it travels as one archive and cannot go partial. # # DIFFERENCE 2 — the image is tagged with the commit SHA, not a fixed # `v1`. Before this, nothing on the host could answer "what is running"; # now the image tag IS the answer, and rolling back is retagging. # # NOT DEPLOYED BY THIS PLAYBOOK: .env. It is host-owned, 0600, and holds # the bearer token. Uploading it would either clobber the live token or # leak it into the repo. It is provisioned once by hand from the vault # item `ana-docker/hrafn/bearer-token`. vars: compose_dir: /opt/docker/compose/hrafn # Overridden by CI with the triggering commit. `manual` marks a # hand-run deploy so an un-provenanced image is obvious on the host. hrafn_sha: manual steps: # ── preconditions ─────────────────────────────────────────────────── - name: Verify compose dir exists + writable shell: test -w {{ compose_dir }} changed_when: "false" - name: Verify .env is present and 0600 (deploy must not create it) shell: | test -f {{ compose_dir }}/.env || { echo "MISSING .env — provision it from vault item ana-docker/hrafn/bearer-token"; exit 1; } perms=$(stat -c %a {{ compose_dir }}/.env) test "$perms" = "600" || { echo "REFUSING: .env is $perms, expected 600 (it holds the bearer token)"; exit 1; } changed_when: "false" - name: Verify traefik-net exists (consumers reach hrafn on it) shell: docker network inspect traefik-net >/dev/null 2>&1 changed_when: "false" # ── ship the build context ────────────────────────────────────────── - name: Upload build context upload: src: dist/hrafn-context.tgz dest: "{{ compose_dir }}/.hrafn-context.tgz" mode: "0600" - name: Unpack build context # Overwrites tracked files in place; leaves .env and any host-only # state alone because the archive does not contain them. shell: tar xzf {{ compose_dir }}/.hrafn-context.tgz -C {{ compose_dir }} && rm -f {{ compose_dir }}/.hrafn-context.tgz # ── build + start ─────────────────────────────────────────────────── - name: Build image tagged with the commit shell: cd {{ compose_dir }} && HRAFN_TAG={{ hrafn_sha }} docker compose build - name: Start/replace the container on the new image shell: cd {{ compose_dir }} && HRAFN_TAG={{ hrafn_sha }} docker compose up -d - name: Record what is deployed # The provenance fix. Anyone on the box can now answer "what commit # is this" without asking the person who last rsync'd. shell: | printf 'sha=%s\ndeployed_at=%s\n' "{{ hrafn_sha }}" "$(date -Is)" > {{ compose_dir }}/.deployed chmod 644 {{ compose_dir }}/.deployed verify: - name: Container reports healthy # start_period is 20s and the interval is 30s, so allow two cycles # before calling it a failure rather than a slow start. shell: | # elway's variable regex only matches an identifier starting with a # letter or underscore, so docker's .State Go template below passes # through untouched and needs no escaping. for i in $(seq 1 20); do s=$(docker inspect hrafn --format '{{.State.Health.Status}}' 2>/dev/null || echo missing) [ "$s" = "healthy" ] && { echo "healthy after ${i}0s"; exit 0; } sleep 10 done echo "still $s after 200s"; docker logs --tail 40 hrafn; exit 1 changed_when: "false" - name: Readiness endpoint answers on the shared network # Proves consumers can actually reach it, not just that the process # is up — hrafn has no published host port, so this has to run from # inside traefik-net. shell: > docker run --rm --network traefik-net curlimages/curl:latest -sf -m 10 -o /dev/null -w '%{http_code}\n' http://hrafn:8080/readyz changed_when: "false" - name: Deployed SHA matches what we just shipped shell: grep -q "^sha={{ hrafn_sha }}$" {{ compose_dir }}/.deployed changed_when: "false"