A complete demonstration of the OpenJustice OK platform engineering stack, featuring:
- Hub & Spoke Identity - Centralized Workload Identity Federation
- NixOS + Container - Immutable infrastructure with versioned artifacts
- GitOps Deployment - Explicit SHA-based promotions via PRs
- Multi-Environment - Dev (auto-deploy) and Prod (approval gates)
┌─────────────┐ ┌─────────────┐
│ Dev │────────▶│ Prod │
│ (auto) │ │ (manual) │
└─────────────┘ └─────────────┘
│ │
▼ ▼
hubspoke- hubspoke-
demo-dev demo-prod
This project separates infrastructure into two layers:
- Landing Zone (Base Infrastructure) - Provisioned manually once
- Application Zone (App + Updates) - Automated via CI/CD
Step 1: Provision Landing Zone
Run this locally to create the base infrastructure:
cd infra
tofu init -backend-config="bucket=hubspoke-demo-dev-tfstate"
tofu apply -var-file="dev.tfvars" -var="image_version=initial"This creates:
- GCP API enablements (Artifact Registry, Cloud Run, etc.)
- GCS bucket for NixOS images
- Artifact Registry repository
- IAM permissions for the CI/CD service account
Step 2: Build Initial Image (Optional)
The first CI/CD run will fail because Cloud Run expects an image that doesn't exist yet. You can either:
- Let the first CI/CD run fail, then re-run it after the image is pushed
- Manually build and push an initial image:
nix run .#container.copyToDockerDaemon
docker tag hubspoke-demo:latest us-central1-docker.pkg.dev/hubspoke-demo-dev-b87d/repo/hubspoke-demo:initial
skopeo copy docker-daemon:us-central1-docker.pkg.dev/hubspoke-demo-dev-b87d/repo/hubspoke-demo:initial \
docker://us-central1-docker.pkg.dev/hubspoke-demo-dev-b87d/repo/hubspoke-demo:initialStep 3: Enable CI/CD
After the landing zone is ready, pushes to main will automatically:
- Build container and GCE images via Nix
- Push to Artifact Registry and GCS
- Deploy to dev environment via OpenTofu
# Enter development environment
nix develop
# Build artifacts
mise run build
# Deploy locally (requires GCP auth)
mise run deploy-localGET /- Hello message with version and environment infoGET /healthz- Health check (returns 200 OK)GET /status- Detailed system statusPOST /echo- Echo endpoint
Trigger: Push to main branch (excluding infra/prod.tfvars)
Flow:
- CI builds Nix artifacts with git SHA
- Container pushed to
us-central1-docker.pkg.dev/.../hubspoke-demo:$SHA - GCE image pushed to GCS
- OpenTofu applies with
-var="image_version=$SHA" - Cloud Run updated with new container
Trigger: Pull request modifying infra/prod.tfvars merged to main
Flow:
- Create PR updating
infra/prod.tfvarswith SHA from dev - CODEOWNERS approval required (@brancengregory)
- Merge triggers deployment
- OpenTofu applies with version from
prod.tfvars
All environment configuration is in version-controlled tfvars files:
infra/dev.tfvars- Dev environment (image_version overridden by CI)infra/prod.tfvars- Production (protected by CODEOWNERS, version pinned in git)
| Workflow | Trigger | Purpose |
|---|---|---|
CI/CD: Dev |
Push to main (excl. prod.tfvars) | Build + deploy to dev |
Release: Production |
Push to main changing prod.tfvars | Deploy to prod |
CI |
Pull request | Validate tofu syntax |
-
GitHub Secrets:
GCP_WIF_PROVIDER- Workload Identity Provider resource name
-
Branch Protection:
- Enable "Require pull request reviews"
- Enable "Require review from Code Owners"
-
CODEOWNERS: Already configured to protect
infra/prod.tfvars -
Initial Landing Zone: Must be provisioned manually (see Initial Setup above)
Built on openjusticeok/tofu-modules v0.6.0+ with:
- Hub & Spoke WIF (single global identity pool)
- Versioned artifacts (SHA-based immutable deployments)
- Dual deployment (Cloud Run container + GCE VM)
- Separate state buckets per environment
MIT