Self-hosted Gitea on a single e2-small AlmaLinux 10 VM in us-east1, serving gitea.jasonmross.dev. Runtime is podman quadlets (systemd .container/.network/.volume units). Both images are built on Debian 13: Gitea from a GPG-verified release binary, and Caddy from an xcaddy build carrying the Google Cloud DNS provider (ACME DNS-01) and the Coraza WAF with the OWASP CRS embedded. Infrastructure is a Pulumi program in Go against a GCS state backend. Cloud Build handles CI: a push trigger for images, one for infra, and a weekly scheduled rebuild. Everything Cloud Build touches is 2nd gen. Notable design decisions, each documented where it lives: - Quadlets track a floating :prod tag. AutoUpdate=registry compares digests for a tag, so a digest-pinned image silently disables auto-updates. - Git transport and LFS bypass the WAF. With the bypass removed, a plain git push returns 403 -- packfiles trip CRS reliably. - gitea:wafMode drives both SecRuleEngine and whether the fail2ban jail acting on WAF verdicts exists. Banning on detections that were never blocks would turn a tuning false positive into an nftables ban. - fail2ban bans at the nftables prerouting hook. Published container ports are DNAT'd and never traverse INPUT, where the stock actions install their rules. - The DNS zone, backup bucket, and Gitea signing secrets are not Pulumi-owned, so pulumi destroy cannot take them with it. - The podman subnet is pinned because it is what Gitea's REVERSE_PROXY_TRUSTED_PROXIES names. Three update layers: dnf5-automatic for the OS, podman-auto-update with health-gated rollback for containers, and a weekly image rebuild that gives the second layer something to pull. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49 lines
2.4 KiB
Markdown
49 lines
2.4 KiB
Markdown
# Moving CI off GitHub and onto this Gitea instance
|
|
|
|
Today the repository lives on GitHub and Cloud Build watches it through a
|
|
`cloudbuildv2` connection. The intent is to move the repository into the Gitea
|
|
instance this repo deploys. Doing that well is mostly about not building a loop
|
|
that can strand itself.
|
|
|
|
## The bootstrapping problem
|
|
|
|
**The runner that deploys the VM must not live on the VM it deploys.** If a
|
|
Gitea Actions runner on `gitea-vm` executes the infra pipeline, then any change
|
|
that restarts Gitea — a config sync, an image rollout, a reboot — kills the job
|
|
mid-flight. Worse, a bad change locks you out of the tool needed to fix it.
|
|
|
|
Two workable shapes:
|
|
|
|
1. **Keep Cloud Build as the executor.** Gitea fires a webhook; Cloud Build runs
|
|
the build. Cloud Build has no first-class Gitea trigger, so this means a small
|
|
authenticated endpoint (Cloud Run or a Cloud Function) that validates the
|
|
webhook signature and calls `cloudbuild.projects.triggers.run`. The deploy
|
|
path stays entirely outside the VM. **This is the recommended option.**
|
|
2. **A Gitea Actions runner on separate infrastructure** — a second small VM or a
|
|
Cloud Run job. Self-contained, but you now operate a runner, and the infra
|
|
pipeline still needs GCP credentials.
|
|
|
|
Either way, keep the GitHub trigger working until the replacement has
|
|
successfully deployed at least once.
|
|
|
|
## Order of operations
|
|
|
|
1. `[actions] ENABLED = true` is already set in `vm/config/app.ini.tmpl`, so no
|
|
Gitea-side config change is needed to start.
|
|
2. Mirror the repository into Gitea and verify history, LFS objects, and tags.
|
|
3. Stand up the chosen executor and prove it can run `cloudbuild/image.yaml`
|
|
end to end, including the IAP rollout step.
|
|
4. Point `gitea:githubOwner` at nothing / remove the GitHub trigger. The Pulumi
|
|
program already tolerates GitHub being unconfigured — `build.New` logs a
|
|
warning and skips the triggers — so the stack still applies cleanly.
|
|
5. Keep the GitHub repo as an archived mirror for a while. It is the cheapest
|
|
possible disaster recovery for the repo that contains the deployment.
|
|
|
|
## Do not forget
|
|
|
|
- The `cb-infra@` service account and the Pulumi state bucket are created by
|
|
`scripts/bootstrap.sh`, not by Pulumi. They survive this migration untouched.
|
|
- Backups (`gitea-backup.timer`) become considerably more important once the
|
|
repository that describes the infrastructure lives *on* the infrastructure.
|
|
Verify a restore before you cut over, not after.
|