Infrastructure as Code (cp.yaml)
This example manages a complete e-commerce platform entirely through cp.yaml files committed to the repository. Every infrastructure change goes through Git — no imperative cpctl deploy calls in the day-to-day workflow. CI/CD applies changes automatically via cpctl iac up.
| Service | Role |
|---|---|
storefront-api | REST API (Postgres, Redis) |
storefront-web | Customer-facing frontend |
storefront-admin | Internal admin dashboard |
storefront-worker | Background order processor |
storefront-db | Managed PostgreSQL |
storefront-cache | Managed Redis |
Repository layout
storefront/
├── api/
├── web/
├── admin/
├── worker/
└── environments/
├── staging/
│ └── cp.yaml ← staging infra definition
└── production/
└── cp.yaml ← production infra definitionEach environment has its own cp.yaml. Secrets (database URLs, API keys) are never stored in cp.yaml — they are injected separately via cpctl env set and stored in the platform's secret store.
The cp.yaml files
environments/staging/cp.yaml
yaml
version: "1"
databases:
- name: storefront-db-staging
engine: postgres
size_gb: 10
- name: storefront-cache-staging
engine: redis
size_gb: 2
services:
- name: storefront-api-staging
image: ghcr.io/your-org/storefront-api:staging
replicas: 1
env:
NODE_ENV: staging
PORT: "4000"
- name: storefront-web-staging
image: ghcr.io/your-org/storefront-web:staging
replicas: 1
env:
NODE_ENV: staging
PORT: "3000"
API_URL: https://storefront-api-staging-<hash>.cpctl.app
- name: storefront-admin-staging
image: ghcr.io/your-org/storefront-admin:staging
replicas: 1
env:
NODE_ENV: staging
PORT: "5000"
API_URL: https://storefront-api-staging-<hash>.cpctl.app
- name: storefront-worker-staging
image: ghcr.io/your-org/storefront-worker:staging
replicas: 1
env:
NODE_ENV: staging
health_check:
type: none # worker doesn't serve HTTP — skip readiness probeenvironments/production/cp.yaml
yaml
version: "1"
databases:
- name: storefront-db
engine: postgres
size_gb: 50
- name: storefront-cache
engine: redis
size_gb: 10
services:
- name: storefront-api
image: ghcr.io/your-org/storefront-api:latest
replicas: 3
env:
NODE_ENV: production
PORT: "4000"
API_URL: https://api.storefront.example.com
autoscaling:
min: 3
max: 20
cpu: 70
- name: storefront-web
image: ghcr.io/your-org/storefront-web:latest
replicas: 2
env:
NODE_ENV: production
PORT: "3000"
API_URL: https://api.storefront.example.com
autoscaling:
min: 2
max: 10
cpu: 65
- name: storefront-admin
image: ghcr.io/your-org/storefront-admin:latest
replicas: 1
env:
NODE_ENV: production
PORT: "5000"
API_URL: https://api.storefront.example.com
- name: storefront-worker
image: ghcr.io/your-org/storefront-worker:latest
replicas: 2
env:
NODE_ENV: production
health_check:
type: none # worker doesn't serve HTTP — skip readiness probeThe health_check: type: none field tells the platform not to apply a TCP readiness probe. Worker-style services that don't bind a port would otherwise stay in starting indefinitely. For an imperative deploy the equivalent flag is --no-probe:
bash
cpctl deploy --image your-org/worker:latest --name my-worker --no-probeStep 1 — First-time staging bootstrap
The --file flag lets you run iac commands from the repo root without cd.
bash
# Preview what will be created
cpctl iac up --dry-run --file environments/staging/cp.yaml
# Planned changes:
# + create database storefront-db-staging engine=postgres size_gb=10
# + create database storefront-cache-staging engine=redis size_gb=2
# + create storefront-api-staging image=ghcr.io/.../storefront-api:staging replicas=1
# + create storefront-web-staging image=ghcr.io/.../storefront-web:staging replicas=1
# + create storefront-admin-staging image=ghcr.io/.../storefront-admin:staging replicas=1
# + create storefront-worker-staging image=ghcr.io/.../storefront-worker:staging replicas=1
# health_check: type=none
#
# Dry-run mode — no changes applied.
# Apply — databases are created first, then services
cpctl iac up --yes --file environments/staging/cp.yaml
# ✓ Created database storefront-db-staging
# ✓ Created database storefront-cache-staging
# ✓ Created storefront-api-staging
# ✓ Created storefront-web-staging
# ✓ Created storefront-admin-staging
# ✓ Created storefront-worker-stagingStep 2 — Inject secrets (one-time per environment)
Secrets are never in cp.yaml. Set them once; the platform persists them across all future redeploys. iac up merges env — it never wipes keys set outside cp.yaml.
bash
DB_URL=$(cpctl db url storefront-db-staging)
REDIS_URL=$(cpctl db url storefront-cache-staging)
cpctl env set storefront-api-staging \
DATABASE_URL="$DB_URL" \
REDIS_URL="$REDIS_URL" \
STRIPE_SECRET_KEY=<your-stripe-test-key> \
STRIPE_WEBHOOK_SECRET=<your-webhook-secret> \
JWT_SECRET=$(openssl rand -hex 32)
cpctl env set storefront-worker-staging \
DATABASE_URL="$DB_URL" \
REDIS_URL="$REDIS_URL"
cpctl env set storefront-admin-staging \
ADMIN_SECRET=$(openssl rand -hex 16)
# Rolling restart to pick up secrets
cpctl redeploy storefront-api-staging
cpctl redeploy storefront-worker-staging
cpctl redeploy storefront-admin-stagingStep 3 — Verify staging
bash
cpctl machine list
# ┌──────────────────────────┬───────────────────────────────────────┬─────────┬──────────┬─────────┬──────────────────────────────────────────────────┐
# │ NAME │ IMAGE │ STATUS │ REPLICAS │ REGION │ URL │
# ├──────────────────────────┼───────────────────────────────────────┼─────────┼──────────┼─────────┼──────────────────────────────────────────────────┤
# │ storefront-api-staging │ ghcr.io/your-org/storefront-api:... │ running │ 1 / 1 │ eu-west │ https://storefront-api-staging-<hash>.cpctl.app │
# │ storefront-web-staging │ ghcr.io/your-org/storefront-web:... │ running │ 1 / 1 │ eu-west │ https://storefront-web-staging-<hash>.cpctl.app │
# │ storefront-admin-staging │ ghcr.io/your-org/storefront-admin:... │ running │ 1 / 1 │ eu-west │ https://storefront-admin-staging-<hash>.cpctl.app│
# │ storefront-worker-staging│ ghcr.io/your-org/storefront-worker:...│ running │ 1 / 1 │ eu-west │ — │
# └──────────────────────────┴───────────────────────────────────────┴─────────┴──────────┴─────────┴──────────────────────────────────────────────────┘
curl https://storefront-api-staging-<hash>.cpctl.app/health
# {"ok":true,"db":"connected","redis":"connected"}
cpctl quota
# ┌──────────┬─────────┬──────────┬─────┐
# │ RESOURCE │ USED │ LIMIT │ PCT │
# ├──────────┼─────────┼──────────┼─────┤
# │ CPU │ 400m │ 2000m │ 20% │
# │ Memory │ 512 MiB │ 4096 MiB │ 12% │
# │ Storage │ — │ 50 GiB │ — │
# └──────────┴─────────┴──────────┴─────┘Step 4 — Snapshot live state as cp.yaml (optional)
If services are already running and you want to bring them under IaC, generate the initial cp.yaml from the live cluster. --no-secrets omits env keys whose names contain SECRET, KEY, PASSWORD, or TOKEN.
bash
cpctl iac init --no-secrets --file environments/staging/cp.yaml
# Snapshotting live services...
# ✓ Wrote environments/staging/cp.yaml
git add environments/staging/cp.yaml
git commit -m "chore: snapshot staging state into cp.yaml"
git pushThe generated file includes replicas, non-secret env vars, and any active autoscaling config.
Step 5 — Production bootstrap (one-time)
bash
cpctl iac up --dry-run --file environments/production/cp.yaml
# Planned changes:
# + create database storefront-db engine=postgres size_gb=50
# + create database storefront-cache engine=redis size_gb=10
# + create storefront-api image=ghcr.io/.../storefront-api:latest replicas=3
# autoscaling: min=3 max=20 cpu=70%
# + create storefront-web image=ghcr.io/.../storefront-web:latest replicas=2
# autoscaling: min=2 max=10 cpu=65%
# + create storefront-admin image=ghcr.io/.../storefront-admin:latest replicas=1
# + create storefront-worker image=ghcr.io/.../storefront-worker:latest replicas=2
# health_check: type=none
#
# Dry-run mode — no changes applied.
cpctl iac up --yes --file environments/production/cp.yamlInject production secrets the same way as staging, then attach custom domains (DNS CNAME target: cpctl.app):
bash
cpctl domain add storefront-api api.storefront.example.com
cpctl domain add storefront-web storefront.example.com
cpctl domain add storefront-admin admin.storefront.example.comStep 6 — Day-to-day: promote a new image
The standard change flow:
- CI builds and pushes a new image tag
- Update the image tag in
cp.yaml - Open a PR → merge to
main - CI runs
cpctl iac up— only changed services are updated
Example: API v1.3.0 → v1.4.0
diff
- name: storefront-api
- image: ghcr.io/your-org/storefront-api:v1.3.0
+ image: ghcr.io/your-org/storefront-api:v1.4.0
replicas: 3bash
git add environments/production/cp.yaml
git commit -m "deploy: storefront-api v1.4.0"
git push origin main
# CI output:
# Planned changes:
# ~ update storefront-api image=ghcr.io/.../v1.3.0→v1.4.0
#
# ✓ Updated storefront-apiEverything else in cp.yaml — replicas, env, autoscaling — is unchanged.
Step 7 — Add a feature flag via env drift
Non-secret env vars can live in cp.yaml so they are version-controlled. Add a new key to the env: block and merge. iac up patches only the diff — secrets injected via cpctl env set are untouched.
diff
- name: storefront-api
env:
NODE_ENV: production
PORT: "4000"
API_URL: https://api.storefront.example.com
+ FEATURE_NEW_CHECKOUT: "true"bash
# CI output:
# Planned changes:
# ~ update storefront-api env(changed)
#
# ✓ Updated storefront-apiStep 8 — Scale up for a traffic spike
Edit cp.yaml and merge — the change is tracked in Git with author and timestamp. No imperative cpctl scale commands.
diff
- name: storefront-api
- replicas: 3
+ replicas: 8
autoscaling:
min: 3
- max: 20
+ max: 50
cpu: 70
- name: storefront-web
- replicas: 2
+ replicas: 4bash
# CI output:
# Planned changes:
# ~ update storefront-api replicas=3→8 autoscaling: min=3 max=50 cpu=70%
# ~ update storefront-web replicas=2→4
#
# ✓ Updated storefront-api
# ✓ Updated storefront-webScale back down after the event — edit, commit, merge.
Step 9 — Drift detection
--dry-run shows whether live state has drifted from cp.yaml. Run it on a schedule (e.g. nightly) to catch manual changes made outside of Git.
bash
cpctl iac up --dry-run --file environments/production/cp.yaml
# Clean — no drift:
# → Everything is up to date — no changes needed.
# Drift detected (e.g. someone ran cpctl scale storefront-api --replicas 12):
# Planned changes:
# ~ update storefront-api replicas=12→3Step 10 — Roll back a bad deploy
Roll back is a Git revert — no platform-specific commands needed.
bash
git revert HEAD
git push origin main
# CI output:
# Planned changes:
# ~ update storefront-api image=ghcr.io/.../v1.4.0→v1.3.0
#
# ✓ Updated storefront-apiStep 11 — Tear down staging to save quota
cpctl iac down destroys every service and database listed in cp.yaml. Useful for turning off staging overnight.
bash
cpctl iac down --yes --file environments/staging/cp.yaml
# Resources to destroy:
# - service storefront-api-staging
# - service storefront-web-staging
# - service storefront-admin-staging
# - service storefront-worker-staging
# - database storefront-db-staging (postgres)
# - database storefront-cache-staging (redis)
#
# ✓ Deleted service storefront-api-staging
# ✓ Deleted service storefront-web-staging
# ✓ Deleted service storefront-admin-staging
# ✓ Deleted service storefront-worker-staging
# ✓ Deleted database storefront-db-staging
# ✓ Deleted database storefront-cache-staging
# Bring it back up any time
cpctl iac up --yes --file environments/staging/cp.yamlCI/CD integration (GitHub Actions)
yaml
# .github/workflows/deploy-production.yml
name: Deploy Production
on:
push:
branches: [main]
paths:
- environments/production/cp.yaml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cpctl
run: curl -fsSL https://install.computeportal.io | sh
- name: Plan
env:
CP_API_TOKEN: ${{ secrets.CP_API_TOKEN }}
run: |
cpctl login --token "$CP_API_TOKEN"
cpctl iac up --dry-run --file environments/production/cp.yaml
- name: Apply
env:
CP_API_TOKEN: ${{ secrets.CP_API_TOKEN }}
run: cpctl iac up --yes --file environments/production/cp.yamlyaml
# .github/workflows/deploy-staging.yml
name: Deploy Staging
on:
push:
branches: [staging]
paths:
- environments/staging/cp.yaml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cpctl
run: curl -fsSL https://install.computeportal.io | sh
- name: Apply staging infrastructure
env:
CP_API_TOKEN: ${{ secrets.CP_API_TOKEN }}
run: |
cpctl login --token "$CP_API_TOKEN"
cpctl iac up --yes --file environments/staging/cp.yamlyaml
# .github/workflows/drift-check.yml
name: Drift Check
on:
schedule:
- cron: "0 3 * * *" # 03:00 UTC nightly
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cpctl
run: curl -fsSL https://install.computeportal.io | sh
- name: Check production drift
env:
CP_API_TOKEN: ${{ secrets.CP_API_TOKEN }}
run: |
cpctl login --token "$CP_API_TOKEN"
cpctl iac up --dry-run --file environments/production/cp.yamlWith this setup:
- Merging to
staging→ staging environment updates automatically - Merging to
main→ production environment updates automatically - Every infrastructure change has a Git commit, author, and timestamp
- Rollback is
git revert— no platform-specific commands needed - Nightly drift check surfaces manual changes made outside of Git
What lives in cp.yaml vs. what doesn't
| Concern | cp.yaml | cpctl env set |
|---|---|---|
| Image tag | ✓ | — |
| Replica count | ✓ | — |
| Autoscaling policy | ✓ | — |
| Health check type | ✓ | — |
| Feature flags | ✓ | — |
| Database URLs | — | ✓ (secret) |
| API keys / tokens | — | ✓ (secret) |
| JWT secrets | — | ✓ (secret) |
| Webhook signing keys | — | ✓ (secret) |
