Config as code
A project can live in a file. anyport.yaml names the apps, managed services, secrets (by name
and key), scheduled tasks, variables and domains a project is made of, and anyport apply makes
a project match it. The same file serves development, staging and production: where it runs is
an argument to apply, not a line in the file.
apiVersion: anyport.dev/v1alpha1
kind: Project
name: demo
environment: staging
variables:
REGION: eu
secrets:
- name: creds
keys: [API_KEY]
- name: session
generate: [APP_SECRET]
services:
- name: db
type: postgres
config:
version: "17"
apps:
- name: api
image: ghcr.io/acme/api:1.4.0
replicas: 2
ports:
- { name: http, port: 8080 }
env:
- { name: REGION, value: "${{ REGION }}" }
- { name: DATABASE_URL, fromService: { service: db, key: DATABASE_URL } }
- { name: API_KEY, fromSecret: { secret: creds, key: API_KEY } }
probes:
readiness: { path: /healthz, port: 8080 }
domains:
- host: api.acme.dev
publicPorts:
- { port: 20017, target: 50051 }
- name: web
source:
repo: acme/web
branch: main
ports: [{ port: 3000 }]
tasks:
- name: nightly
app: api
command: ["python", "manage.py", "cleanup"]
schedule: "0 3 * * *"
The full format is published as a JSON Schema at /schema/anyport.json.
Point your editor at it with a # yaml-language-server: $schema=https://anyport.dev/schema/anyport.json
line at the top of the file and it completes and checks every key. Unknown keys are refused, not
ignored: a misspelled setting that silently did nothing is the failure this exists to prevent.
publicPorts opens raw TCP or UDP ports beside the hostnames (see Domains).
via is left out on purpose: the console picks what the cluster serves for free, and on a
managed cloud where a load balancer costs money it refuses to guess until you write
via: loadBalancer or via: nodePort.
Start from a project you have
anyport project export demo -o anyport.yaml
writes a running project as a file. That is the fastest way to adopt this, and it is also how the format is kept honest: everything the console can build has to come back out of an export, and a test in the codebase fails when a setting exists that the file cannot say.
See what would change
anyport plan -f anyport.yaml
Project demo on prod (update)
ACTION RESOURCE DETAIL
blocked secret/creds secret "creds" lacks API_KEY; set it first: anyport secret set creds API_KEY=… -p demo
update app/api env, replicas
create task/nightly
unmanaged app/legacy in the project but not in the file; left alone (pass prune to remove)
noop service/db
0 to create, 1 to update, 0 to remove, 1 unchanged, 1 in the project but not in the file.
Has to happen first:
- secret "creds" lacks API_KEY; set it first: anyport secret set creds API_KEY=… -p demo
Nothing is changed by plan. The rows are what apply would do, in the file's own vocabulary.
Apply
anyport apply -f anyport.yaml
anyport apply -f anyport.yaml --cluster staging # a new project lands here
apply shows the plan, asks, and then works in dependency order: the project, then its managed
services, then apps, tasks and domains. Three rules decide what it will and will not do:
- Secret values never enter the file, so apply never writes a secret. A file declares which
secrets and keys the project needs; apply checks they exist and stops if one does not, naming
the
anyport secret setcommand that fixes it. Everything before that point is applied, and runningapplyagain finishes the job. The one exception is a key listed undergenerate: a signing key or a session secret needs a value, not a particular one, so apply mints a random one the first time and never overwrites it. - Apply never deletes by omission. A resource in the project that the file does not mention
is reported as unmanaged and left alone.
--pruneremoves unmanaged apps, tasks and domains. Managed services are never removed by apply, prune or not: an uninstall destroys data and has its own confirmation in the console and inanyport service uninstall. - What the file owns, it owns completely. A key removed from an app in the file is removed from the app. Settings the file does not carry (whether a project is paused, for one) are not touched.
In the console
Project → Settings → Config as code shows the file for a project, to copy or download, and takes a pasted file: the same plan, then apply. Projects → New project → Create from file starts a project from one, with the cluster chosen in the dialog.
Kept in sync from the repository
Once an anyport.yaml is on the branch an app deploys from, the console applies it on every
push that changes it, before the build that push triggers. So a pull request that changes the
file and the code together deploys both, in that order, and the repository is the place the
project is described. The rules are the ones above: nothing is deleted by omission (a push is
never a prune), and a missing secret stops the apply where it is, with the reason in the
repository's deployment log next to the build.
The file has to name the project the app deploys into; a file that names another project is left alone, so two apps in one repository deploying into two projects cannot rewrite each other's. In a monorepo the file is looked for in the app's build directory first, then at the root.
A pull request's preview gets the pull request's own file: applied into the preview when it is created, and on every push after. Hostnames and public ports in the file stay with the base project, since a preview is reached on generated hostnames of its own, and the preview's name and variables are the preview engine's.
What goes in the file, and what does not
| In the file | Not in the file |
|---|---|
| Apps: image or source, ports, env, resources, autoscaling, probes, volumes, sidecars, pull secret, which projects may reach it | The cluster: pass --cluster, or let a new project land on the org default |
| Managed services with their configuration | Secret values: set with anyport secret set or in the console |
| Secrets by name and the keys the project reads, and the keys apply may generate | Whether a project or app is paused |
| Tasks: schedule, command, the app whose image and env they borrow | Preview settings, which live on the base project in the console |
Variables and their use as ${{ NAME }} | Anything a build produced: a source-built app is described by its repository, never by the image its last build pushed |
| Domains, on the app that serves them |
A source-built app carries source instead of image. Applying it creates the app and its build
from the repository the same way "deploy from GitHub" does, and a push to the branch keeps it
current from then on.