The Anyport Team
Config as code for a project: what the file should not own
Putting a project in a YAML file is the easy half. The decisions that matter are what the file must never contain: secret values, the cluster, and the permission to delete. Why each of those is a rule rather than a default.
Every deploy platform ends up with a file. Heroku had app.json, Render has render.yaml, Railway has railway.json, Fly has fly.toml. The file exists because a dashboard is a fine place to make a change and a poor place to remember one: three months later nobody can say why the worker has two replicas, and the pull request that would have said so was never written, because the change was a click.
Writing the format is a week of work. Deciding what the file is allowed to say is where the design happens, and most of that design is about what to leave out.
One file, every field
The first decision is whether the file describes the project or is the project. If it is the project, every setting the console can change has to be expressible in it, or the two drift the first time someone edits in the dashboard. That sounds like a burden and is in fact the useful constraint: a test that walks every field of the app spec and fails when one has no line in the file keeps the format honest for as long as the product exists. An export that comes back out of a running project and parses to the same thing is the same guarantee from the other side.
The alternative, a file that covers the common cases, is a file that is right until the first uncommon case, after which it is a second place to look.
Secret values never go in
Obvious in principle, broken constantly in practice, usually by a template that ships with DATABASE_PASSWORD: changeme. The file lives in a repository. A repository is cloned, forked, cached by CI and read by whichever tool has read access to the organisation. A value written there once is in every one of those places, and rotating it means finding all of them.
So the file names secrets, and names the keys the app reads, and stops there. What that costs is that applying a file cannot be one step on a fresh project, because the values have to arrive from somewhere else first. The honest way to handle it is to let the apply stop at the missing key, print the exact command that supplies it, and finish on the next run. An apply that quietly created the app with an empty variable would be worse than one that refused.
There is one exception worth making. A session secret or a signing key needs a value, not a particular one. For those the file can say "generate this", and the platform mints a random value the first time and never overwrites it. That is a different thing from a database password, which has to match something on the other end.
Where it runs is an argument
The same file has to serve development, staging and production, or it is three files that drift. Which means the place a project runs cannot be a line in it. The cluster is an argument to apply, and everything that follows from the cluster (which storage class, which public address, what a load balancer costs there) is answered at apply time rather than pinned in the repository.
A related choice: an app built from source goes into the file as its repository, branch and build settings, never as the image its last build produced. That image is one commit's output. A file that named it would be pinned to a moment, and the next push would make it wrong.
What leaving something out should mean
Terraform made "the file is the whole truth" the expectation: a resource missing from the file is a resource to destroy. That is the right rule for infrastructure one team owns end to end. It is the wrong default for a project file, for two reasons.
The first is that the file is edited by people thinking about one app. Someone adding a cron task, or removing a stale environment variable, is not deciding whether the legacy service a colleague deployed last quarter should continue to exist. If the apply deletes it, the file has taken a decision nobody made.
The second is that some things in a project hold data. An app can be recreated from its image. A database cannot be recreated from its config. So the rule has two levels. A resource in the project that the file does not mention is reported and left alone, and removing it takes an explicit flag. A managed service is never removed by an apply at all, flag or no flag, because uninstalling it destroys data and deserves its own confirmation, by a person, with the word "delete" on the button.
The cost is that the file is not a complete description of the project until someone runs the prune. That is worth paying: a file that can only add and update is one a push can safely apply.
Show the plan, in the file's own words
A file you can apply is a file you will eventually apply by accident. The plan is what makes that survivable: a diff between what the file says and what the project is, before anything moves. The detail that matters is the vocabulary. A plan that says a ConfigMap will be replaced has translated the file into the platform's internals and handed the translation back. A plan that says app/api: env, replicas is in the words the person wrote.
Once a plan exists, applying from the repository on push becomes reasonable. A pull request that changes the file and the code together deploys both, in that order. Nothing is deleted by omission, because a push is never a prune. A missing secret stops the apply where it is, with the reason next to the build it would have preceded.
How this works in Anyport
An Anyport project can live in an anyport.yaml: its apps, managed services, secrets by name and key, scheduled tasks, variables and domains. anyport project export writes a running project as one, and a test in the codebase fails when a setting exists that the file cannot say. anyport plan -f shows what applying would change, in the file's own vocabulary; anyport apply -f does it, in dependency order, with the cluster as an argument.
The rules above are the ones it enforces. Apply never writes a secret value and stops naming the anyport secret set command that does, with generate for the keys that only need a value. Nothing is deleted by omission; --prune removes unmanaged apps, tasks and domains, and managed services are never removed by apply at all. The same plan and apply run in the console under Project → Settings → Config as code, and once the file is on the branch an app deploys from, every push that changes it applies it before the build. The format is a published JSON Schema, and an unknown key is refused rather than ignored.
The documentation has the full format and the plan output as it renders. The decision this post is about comes before any of it: which of the things your project is made of should a file be allowed to change, and which should it only be allowed to name.