Skip to content

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 set command that fixes it. Everything before that point is applied, and running apply again finishes the job. The one exception is a key listed under generate: 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. --prune removes 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 in anyport 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 fileNot in the file
Apps: image or source, ports, env, resources, autoscaling, probes, volumes, sidecars, pull secret, which projects may reach itThe cluster: pass --cluster, or let a new project land on the org default
Managed services with their configurationSecret values: set with anyport secret set or in the console
Secrets by name and the keys the project reads, and the keys apply may generateWhether a project or app is paused
Tasks: schedule, command, the app whose image and env they borrowPreview 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.