GCP Compute Engine Backend
Each Run is one Compute Engine instance booted from the project’s Golden Image (a GCE custom image), configured via a startup-script, and self-deleted on command exit — or self-stopped instead, when launched with --retain, for post-mortem inspection. It follows the same one-VM-per-Run shape as AWS: full Compose Contract (host Docker daemon, real bridge networking, privileged-capable). Attach is over an IAP TCP tunnel — no public IP, no internet-facing SSH.
This document covers what the backend provisions and how its attach / lifecycle / cost specifics work.
Prerequisites
Section titled “Prerequisites”gcloudCLI installed and authenticated (gcloud auth login). The authenticated account is the Run’s Owner.- A GCP project with billing enabled, and
terraform(>= 1.10) on PATH. - OS Login at the org/project level (Terraform enables the role binding; the developer principal is mapped to a POSIX user automatically).
afk attachrelies on it. - IAP — the developer principal needs
roles/iap.tunnelResourceAccessor(granted by the module). The IAP TCP forwarding range35.235.240.0/20is the only ingress allowed to Run VMs. - The required APIs (compute, iap, secretmanager, artifactregistry, firestore, logging, cloudfunctions, cloudscheduler, run, cloudbuild) — enabled by
apis.tfatafk provision.
End-to-end flow
Section titled “End-to-end flow”afk init --provider gcp [--region <region>]resolves the project from your active gcloud config (gcloud config set project <id>first), creates the Terraform state GCS bucket (afk-tf-state-<project_id>), copies the Terraform module intoterraform/gcp/, rendersbackend.tf, scaffolds.afk.envand thegcpblock inafk.config.json(zone + machine-type defaults you can edit), and gitignores.afk.env. (Region defaults tous-central1; zone/machine type live in the config block, not flags.)afk provision(orterraform applyfromterraform/gcp/) enables the APIs, creates the VPC + subnet + Cloud NAT + IAP firewall, theafk-vminstance service account, theafk-developercustom role, the Firestore database + indexes, the Artifact Registry repo, the artifacts bucket, and the history-reconcile Cloud Function + Cloud Scheduler trigger. It binds theafk-developerrole to your active gcloud principal.afk golden buildboots a short-lived builder VM, pre-pullsgcp.cachedImages, snapshots its disk into a GCE custom image labelledafk-golden=true, and deletes the builder.afk runbuilds + pushes the agent image to Artifact Registry (<region>-docker.pkg.dev/<project>/afk/...), reads/lintsafk.compose.yml, then callscompute.instances.createagainst the Golden Image with a templated startup-script — no external IP, labelledafk-owner/afk-run-id/afk-branch/afk-sha/afk-managed=true, withscheduling.max_run_durationset from--timeoutandinstance_termination_action=DELETE. Capacity is Spot by default (provisioning-model=SPOT);--on-demandselectsSTANDARD.DELETEcovers both a preemption and the duration cap, so by default a Spot reclaim and a clean exit end the same way.--retain(On-Demand only) sets the termination action toSTOPand self-stops instead, preserving the disk for post-mortemafk attach— see Run lifecycle.- The VM boots: the startup-script authenticates Docker to Artifact Registry (
gcloud auth configure-docker), pulls the agent image, resolvessecret:<name>vars from Secret Manager, writes the compose file, and runs the stack. The CLI-injected entrypoint clones the repo at the ref into/workspaceand runs the command. - On command exit — at the VM level, not inside the container (mirroring how AWS self-terminates from
user_data, not the entrypoint) — the startup-script uploads Session Artifacts to GCS, writes the completion row to Firestore, then runsgcloud compute instances delete "$(hostname)" --zone=<zone> --quiet. Theafk-vmSA can delete only afk-managed VMs. GCE’sscheduling.max_run_durationis the backstop if the VM never reaches that step.
afk.config.json — the gcp block
Section titled “afk.config.json — the gcp block”{ "backend": "gcp", "gcp": { "projectId": "my-project", "region": "us-central1", "zone": "us-central1-a", "defaultMachineType": "e2-standard-4", "allowedMachineTypes": [ "e2-medium", "e2-standard-2", "e2-standard-4", "n2-standard-4" ], "cachedImages": ["postgres:16", "redis:7"] }}defaultMachineTypeis used whenafk runis called without--machine-type.allowedMachineTypesis the whitelist the CLI validates--machine-typeagainst (and which theafk-developerIAM condition mirrors where expressible).cachedImagesis the sidecar pre-pull list baked into the Golden Image byafk golden build.
What Terraform provisions
Section titled “What Terraform provisions”Run once per GCP project/team.
Networking
Section titled “Networking”- A dedicated VPC + regional subnet with
private_ip_google_accessso VMs reach Google APIs without an external IP. - Run VMs get no external IP. The only ingress firewall allows
tcp:22from IAP’s35.235.240.0/20; an explicit low-priority deny covers everything else. - A Cloud Router + Cloud NAT for egress (image pulls, GitHub clone, the gcloud self-delete call).
Identity
Section titled “Identity”-
An
afk-vmservice account attached to every Run VM. Grants: Artifact Registry reader; Secret ManagersecretAccessorscoped toafk-*secrets via an IAM condition; LogginglogWriter; StorageobjectCreatoron the artifacts bucket; and a custom self-reclaim role holding onlycompute.instances.delete+compute.instances.stop(a Run reclaims itself — delete, or stop when retained). Nothing else. -
An
afk-developercustom role bound to developer principals:compute.instances.create(conditioned on the AFK subnet + machine-type whitelist where IAM conditions allow),compute.instances.delete,compute.instances.start/stop(resume + re-park a retained Run on attach),iam.serviceAccountUseronafk-vmonly (the PassRole analogue — a developer can attach only theafk-vmSA),roles/iap.tunnelResourceAccessor, androles/compute.osLogin. -
An
afk-sweeperSA for the Cloud Function:compute.instances.list/get+roles/datastore.user, pluscompute.instances.deleteto reap retained VMs past the retention window. Delete is project-wide (theafk-retainlabel isn’t an IAM-condition attribute — same gap noted below); the Function only ever deletes stoppedafk-managed+afk-retaininstances, enforced in code.IAM-condition limitation. GCP IAM conditions cannot inspect arbitrary GCE instance labels at create/delete time (labels are request-body fields, not condition attributes). The label-based rules AWS expressed with
ec2:ResourceTagconditions —afk-owner == caller, golden-imageafk-golden=true,afk-managed=true, and owner-scoped attach — are therefore enforced CLI-side: the CLI stamps the labels, validates the image/owner/machine-type beforeinstances.create, and resolves the Run via history (Owner-scoped) before tunnelling. Theiam.tfcomments mark each gap.
Storage / state
Section titled “Storage / state”- Terraform state lives in a GCS bucket (
afk-tf-state-<project_id>), versioned. (Created byafk initfirst to avoid the chicken-and-egg; the module also declares it.) - Firestore (Native mode) holds Run history in the
afk-runscollection, with two composite indexes reproducing the DynamoDB GSIs:(owner ASC, started_at DESC)and(repo ASC, started_at DESC). - A Session Artifacts GCS bucket (
afk-artifacts-<project_id>,force_destroy, uniform access, public access prevented, lifecycle expiry at 30 days). The Run VM uploads declared artifacts togs://<bucket>/<repo>/<runId>/session-artifacts/before self-reclaiming (the VM SA hasobjectCreatoronly). Collection mechanics are Backend-neutral; see Session Artifacts.
Not created by Terraform
Section titled “Not created by Terraform”- The Golden Image — built by
afk golden build. - Artifact Registry image tags — pushed by the CLI on
afk build. - Secrets — created by
afk secrets putin Secret Manager (afk-<name>).
Secrets
Section titled “Secrets”Stored in Secret Manager as afk-<name>. The startup-script resolves references at boot via the VM’s afk-vm SA (secretAccessor, IAM-conditioned to the afk-* prefix) and exports them into the compose stack. Values never appear in instance metadata, labels, or logs.
The Docker gcplogs driver is injected per compose service (labelled runId + service). afk logs <run-id> reads via gcloud logging read with a jsonPayload.container.metadata.afk-run=… filter (gcplogs writes container labels under the JSON payload’s container.metadata, not at the entry’s top-level labels); default = the main service, --service <name> = one service, --all = every service. --follow polls the logging API.
Attach
Section titled “Attach”afk attach <run-id> opens an IAP TCP tunnel + OS Login SSH session (gcloud compute ssh --tunnel-through-iap), then docker execs into the main service’s container.
- No inbound networking, no managed SSH keys — IAP terminates the authenticated tunnel and OS Login maps your Google principal to a POSIX user on the VM.
- Gated by
roles/iap.tunnelResourceAccessor; Owner-scoping (only your own Runs) is enforced CLI-side via history lookup, since instance-label conditions aren’t expressible in IAM. --service <name>exec’s into a sidecar;--hostdrops to the VM’s host shell (exposes the Docker socket — use deliberately).- Post-mortem (retained Runs). Attaching a finished Run launched with
--retainresumes it: the CLI starts the stopped instance (gcloud compute sshretries until sshd is back), then — because the container has exited — commits its final filesystem and drops you into a shell from that image;--hostgives the host shell instead. On detach the instance is stopped again; the reconcile Function reclaims it at the retention period. See--retainunder Run lifecycle.
Adding teammates
Section titled “Adding teammates”afk provision binds the afkDeveloper custom role onto the active gcloud principal only. To onboard another developer, bind an existing GCP principal to the same role — GCP doesn’t create principals, so you pass the canonical IAM member string explicitly:
afk team add alice --principal user:alice@example.comafk team add ci --principal serviceAccount:ci@my-proj.iam.gserviceaccount.comafk team lsafk team rm aliceWhat this does:
addbinds the project-levelafkDeveloperrole onto--principal. That role already carries the permissions the developer needs (compute.instances.create/deleteon afk-managed labels,iam.serviceAccountUseronafk-vm,iap.tunnelResourceAccessor,compute.osLogin) — defined once at provision time and reused across team members.lsenumerates the principals bound to the role.rmunbinds the principal from the role.
The caller of
afk team add/rmneeds permission to edit project IAM (resourcemanager.projects.setIamPolicy) — typically project Owner or a custom IAM-admin role. Without it the IAM bind call fails and the developer was never added.
Run lifecycle and reclaim
Section titled “Run lifecycle and reclaim”- A Run’s lifetime equals its main service container’s lifetime. On exit the startup-script writes the completion row, uploads Session Artifacts, flushes logs, then self-reclaims the instance with
gcloud compute instances delete. Theafk-vmSA can delete (and, for retained Runs, stop) only afk-managed VMs. - Timeout backstop is native GCE: every instance is created with
scheduling.max_run_duration(from--timeout, default capped bymax_run_timeout_hours) andinstance_termination_actionset toDELETE(orSTOPfor a retained Run). If the agent crashes before the startup-script reaches self-reclaim, GCE applies that action when the duration elapses. --retain(post-mortem inspection). A Run launched with--retainis labelledafk-retain=true, has its termination action set toSTOP, and self-stops instead of deleting on exit — preserving the boot disk (and exited containers) for laterafk attach(see Attach).--retainimplies On-Demand (STANDARD): a Spot VM cannot be stopped without losing its disk. A retained instance is reclaimed byafk killor by the reconcile Function once it is older than the retention period (retention_days, default 7). Opt-in because a stopped instance still bills for its boot disk.- The history-reconcile Cloud Function (Cloud Scheduler, every 5 min) rewrites orphaned
RUNNINGFirestore rows toSTOPPEDfor Runs whose VM has vanished, and reaps retained VMs (afk-retain+ stopped) older than the retention window — the GCP analogue of the AWS sweeper’s reaper. - The CLI does not stay resident after
afk run; a dead laptop doesn’t affect the Run.
Run state and querying
Section titled “Run state and querying”afk ls→compute.instances.listfiltered by labels (afk-managed=true,afk-owner).afk ls --alldrops the owner filter (requires broader IAM).afk historyreads the Firestoreafk-runscollection.
- VPC + Cloud NAT (small hourly + per-GB), Firestore (free tier covers low Run volume), the always-min-0 Cloud Function + Cloud Scheduler, Artifact Registry storage: low baseline.
- Per-Run: Compute Engine billed per second at the chosen machine type, boot disk, Cloud Logging ingest, NAT egress. Spot by default (
provisioning-model=SPOT, 60–91% off standard) —--on-demandopts up toSTANDARDfor interruption-resistance on long Runs. A Spot reclaim DELETEs the VM mid-Run, same as a clean exit; the reconcile Cloud Function flips the orphaned history row. (Modern Spot VMs supportmax_run_duration+instance_termination_action=DELETE; this is not the legacy preemptible product.) A--retainRun (On-Demand) instead stops on exit — its boot disk keeps billing while stopped until reclaimed at the retention period, which is why retention is opt-in.
Teardown
Section titled “Teardown”afk destroy # dry-run: prints what would be deletedafk destroy --yes # terraform destroy + golden images, Artifact Registry # images, Secret Manager secrets, and the TF state bucket