Deploying apps
A KubeNest cluster is a standard Kubernetes cluster with a tested platform layer on it. Deploy to it
however you already deploy to Kubernetes — kubectl, helm, your own CI, Flux, someone else’s
ArgoCD. None of that needs KubeNest involved, and if your team likes its pipeline, keep it.
What follows is the app layer, for teams who would rather not build one.
What the platform gives your workloads
Worth knowing before you deploy anything, because it changes what you have to configure:
| What you get | |
|---|---|
| Ingress | Traefik with Gateway API. Nothing to install, nothing to choose |
| TLS | cert-manager issues and renews. Certificates are not something you think about again |
| DNS | A working hostname per exposed component, even with no domain of your own |
| Storage | A default StorageClass. PVCs bind without configuration |
| Backup | Velero, with weekly verified restore drills once a target is set |
Local storage is node-local. A pod using a Local PV LVM volume is bound to the node holding it; if that node is lost, restore the volume from backup. Workloads needing storage that survives node loss need an external storage system.
The file
An app is described by a kubenest.yaml that lives in your repository, next to the code it
deploys. kubenest init writes a starting one.
name: my-api
project: acme-prod
components:
api:
image: myorg/my-api:v1.2.0
port: 8000
replicas: 2
expose: api.acme.com
env:
APP_ENV: production
DATABASE_URL: ${postgres.dsn}
STRIPE_KEY: ${secret:STRIPE_KEY}
resources:
cpu: 500m
memory: 512Mi
worker:
image: myorg/my-api:v1.2.0
command: [celery, worker, -A, tasks]
env:
DATABASE_URL: ${postgres.dsn}
postgres:
addon: postgres@16
storage: 20GiThree components, one of them a database, connection strings wired between them. That is the whole description — no Deployment, no Service, no Ingress, no Secret, no Helm values.
The file is reviewable and diffable and belongs in the repo. What is deployed is what was merged.
Deploy it
kubenest deployproject acme-prod on cluster prod-1
postgres addon postgres@16 creating → running ok 42s
api workload myorg/my-api:v1.2.0 deploying → running ok 31s
worker workload myorg/my-api:v1.2.0 deploying → running ok 28s
api https://api.acme.com
deployed in 1m14s · deploy 7 · `kubenest rollback` to undoThe order is worked out from the wiring: api and worker read ${postgres.dsn}, so Postgres goes
first and the others wait for its exports. You do not declare dependencies; using a value is the
declaration.
Deploying again applies the difference. Unchanged components are not touched.
kubenest deploy --dry-run # show what would change, touch nothing
kubenest deploy --wait=false # return as soon as it is acceptedExposing a component
expose gives a component a public URL.
expose: api.acme.com # a hostname you own
expose: true # let the platform pick oneWith expose: true and no domain configured on the cluster, you get
<component>.<app>.<node-ip>.sslip.io — a name that resolves through a public wildcard resolver,
with a real Let’s Encrypt certificate issued over HTTP-01. No DNS to configure and no service of
ours in the path.
It is a working URL, and it is not one you would print. Once you have a domain:
kubenest cluster set-domain --cluster prod-1 acme.comEvery expose: true component moves to <component>.<app>.acme.com, certificates reissue, and the
sslip.io names keep serving while DNS propagates.
Wiring components together
${component.export} reads a value another component publishes. The platform resolves it on the
cluster, at deploy time, into the consuming container’s environment.
env:
DATABASE_URL: ${postgres.dsn}
CACHE_URL: ${redis.url}No password passes through the control plane to get there, and nothing is written into your repo. See Addons for what each addon type publishes.
To read from a shared database that lives outside this app, name it explicitly:
env:
DATABASE_URL: ${addon:shared-postgres.dsn}And ${secret:NAME} reads a value you set out of band — see Secrets.
Health checks
A component that declares a port gets a readiness probe on it, and a rolling update that waits for
ready before moving on. You do not have to write anything: a container that starts and immediately
crashes will not replace the pods that are serving.
When the default guess is wrong — a slow boot, a health endpoint that is not / — say so:
components:
api:
image: myorg/my-api:v1.2.0
port: 8000
healthcheck:
path: /healthz
interval: 10s
timeout: 2s
start_period: 30s
retries: 3start_period is the grace before failures count, which is the one people usually need: a service
that takes forty seconds to warm its cache is not unhealthy at second five.
File reference
| Key | Meaning |
|---|---|
name | The app’s name. Unique within its project |
project | Which project to deploy into. --project overrides it |
cluster | Pin the whole app to a cluster. Defaults to the project’s |
components.<name>.image | A prebuilt image |
components.<name>.addon | type@version from the catalog — makes this an addon, not a workload |
components.<name>.port | The port the container listens on. Implies a readiness probe |
components.<name>.expose | true for a generated hostname, or a hostname you own |
components.<name>.replicas | Default 1 |
components.<name>.env | Literals, or ${component.export} / ${addon:name.export} / ${secret:NAME} references |
components.<name>.command / args | Override the entrypoint |
components.<name>.resources | cpu and memory requests |
components.<name>.storage | Volume size, for addons and stateful workloads |
components.<name>.healthcheck | Override the probe: path, interval, timeout, start_period, retries |
Day to day
None of these touch the file. They act on what is deployed.
kubenest status # this app: components, health, URLs, current deploy
kubenest logs api -f # follow one component
kubenest logs -f # follow everything, interleaved
kubenest open api # open the URL
kubenest exec api -- sh # a shell in a running containerScale one component without redeploying the rest:
kubenest scale api=4Pause to stop compute without losing anything. Workloads go to zero replicas; addons keep running, so databases survive. Resume restores the counts pause recorded.
kubenest pause
kubenest resumeRoll back. Every change is a numbered deploy with an author and a diff.
kubenest deploys 7 now alice api: myorg/my-api:v1.1.0 → v1.2.0
6 2 days ago alice added component worker
5 6 days ago bob api: replicas 2 → 3kubenest rollback # to the deploy before this one
kubenest rollback --to 5 # to a specific one
kubenest diff --to 5 # what that would change, firstA rollback is a new forward deploy of an earlier spec, so history stays linear.
Rolling back an app containing addons rolls back those addons’ chart versions too. An older chart is
a Helm downgrade, and many database charts run migrations on upgrade that do not reverse.
kubenest diff --to before rolling back anything stateful.
Delete it. Addon volumes go with it, so detach anything you want to keep first.
kubenest destroySecrets
Values you do not want in the file, and do not want in your repo:
kubenest secret set api STRIPE_KEY=sk_live_...
kubenest secret list apiThe value goes through the hub to the agent, which writes a Kubernetes Secret in the project’s
namespace. It is never in your repository, never in the file, and never returned by the API —
list shows names only.
Reference one from the file:
env:
STRIPE_KEY: ${secret:STRIPE_KEY}Addon-generated credentials never need any of this. They are resolved on the cluster through
${postgres.dsn} and are never typed by anyone.
This is not the secrets profile. That profile installs sealed-secrets, for teams who keep
their own Kubernetes manifests in Git and need encrypted values in them. If KubeNest is deploying
your app, you do not need it — and the sealing key it makes so important stays optional.
Projects
A project is a namespace with KubeNest metadata. Everything you deploy lives in one, and an app belongs to exactly one project on exactly one cluster.
kubenest project create acme-prod --cluster prod-1
kubenest project listSplitting a workload across clusters is two apps, in two projects, with two files. There is no per-component cluster targeting: a value produced on one cluster cannot be wired into a container on another, and pretending otherwise would mean a control-plane path we deliberately do not have.
Environments and promotion
Staging and production are two projects, usually on two clusters.
You deploy to staging from the file. You do not deploy to production from a file — you promote what passed:
kubenest deploy --project acme-stagingapi workload myorg/my-api:v1.2.0 deploying → running ok 31s
api https://api.staging.acme.com
deployed in 31s · deploy 12Then, once you believe it:
kubenest promote --from acme-staging --to acme-prodpromoting deploy 12 from acme-staging
api myorg/my-api@sha256:9f2c1e… unchanged from staging
postgres postgres@16 unchanged from staging
worker myorg/my-api@sha256:9f2c1e… unchanged from staging
promoted · deploy 34 on acme-prod · `kubenest rollback` to undoThe difference matters more than it looks. A promotion moves the exact spec that ran — image
digests, resolved configuration, the lot — rather than re-rendering the file. So production runs the
artefact you tested, not whatever myorg/my-api:v1.2.0 happens to point at by the time you get
there, and not whatever the file says after someone merged to main in between.
Per-environment differences that are genuinely different — replica counts, resource limits, a staging database that is smaller — are set on the target project and survive promotion:
kubenest scale api=6 --project acme-prodkubenest diff --from acme-staging --to acme-prod shows what promoting would change, before it
changes it.
From CI
kubenest login is interactive. In a pipeline, use a token instead — revocable, and scoped to a
project:
- uses: kubenest/deploy-action@v1
with:
token: ${{ secrets.KUBENEST_TOKEN }}
project: acme-stagingOr without the action, anywhere that can run a binary:
kubenest login --token "$KUBENEST_TOKEN"
kubenest deploy --project acme-staging --waitYour CI keeps doing what it already does — build, test, push an image. Deploying is the last step,
and --wait makes the job fail when the deploy does.
Without the file
Everything here is also in the console — creating apps, deploying, scaling, logs, rollback, promotion. The two are peers, not a tool and its lesser sibling, and both drive the same API.
The file is what we recommend, because it is the one you can review in a pull request, commit alongside the code it deploys, and revert. The console is better when you are looking rather than changing, and better when the person who needs to do something does not have a terminal.
Next: Addons and templates · Day 2