DevOps portal — Applications¶
Apps → Deploy an app runs a container image in one of your team's namespaces. The portal
writes teams/<team>/<namespace>/apps/<app>.yaml in kube-ops, and Flux deploys it once the
merge request is merged. You need a namespace first.
The form, section by section:
Where¶
- Owning team and Namespace: the app runs in that namespace, on its cluster.
- Name: 2 to 32 lowercase letters, digits and hyphens, starting with a letter. It names the
Deployment and Service (
<app>inside the namespace). Team, namespace and name can't be changed later.
Image¶
- Repository: start typing and the portal searches Harbor (
core.harbor.jsm.systems/<project>/<image>). An image from another allowed registry is typed in full, with the registry host:docker.io/library/nginx, notnginx. The cluster acceptscore.harbor.jsm.systems,docker.ioandghcr.io. - Tag: for a Harbor image, a list of its tags, newest push first. Otherwise type it. A tag can
carry a digest:
1.4.2@sha256:….
Follow new tags¶
Harbor images can follow new tags: when CI pushes a matching tag, Flux opens a merge request in kube-ops that moves the app to it, within about 5 minutes of the push. Choose:
- Newest build: tags matching a pattern, highest number wins. The default pattern is
^v(?P<ts>\d+)$(tagsv1,v2, …); change it to match your CI, keeping a named group for the number. - Semver range: the highest tag in the range wins.
The form shows the tag the rule would deploy right now (Would deploy … now), so you can check a rule before you submit it.
What happens to the merge request depends on the environment:
| Env | Image update merge request |
|---|---|
| dev, qa | Merges by itself when its pipeline passes |
| prod | Waits for a Maintainer to merge it |
Getting semver ranges right:
- Write the range out. For a project at
0.0.2, use>=0.0.2 <1.0.0. Avoid^0.0.2: for0.0.xversions a caret means exactly0.0.2, so nothing new ever matches. - Pre-release tags are ignored (
1.3.0-rc.1,2.0.0-beta). Only release versions are picked. - Turn on tag immutability in Harbor for your project (a rule for every tag). The update writes the tag, not the image digest, so if a tag is pushed again with a different image, nothing notices.
- Your CI only pushes tags. It doesn't touch kube-ops: build, tag (
1.4.0, orv<build>for newest build) and push to Harbor. Tags that don't match the rule are ignored.
To stop following, untick Follow new tags with Change app; the tag stays where it is.
Run¶
| Field | Default | Notes |
|---|---|---|
| Port | 8080 | 1024 to 65535: containers run as non-root. The container port, Service port and health check port |
| Replicas | 1 | 0 to 10 |
| Size | S | S 50m CPU / 128Mi, M 250m / 512Mi, L 1 CPU / 2Gi, or Custom. CPU and memory are requests; memory is also the limit (no CPU limit) |
| Health check path | none: a TCP check on the port | An HTTP path such as /healthz, used for the liveness and readiness probes on the port |
Pods run with a restricted security profile: non-root, no privilege escalation, all capabilities dropped.
Config¶
Environment variables are plain name and value pairs, stored in the kube-ops file, so never put secrets here.
Secrets come from OpenBao, from a vault that is connected to the namespace (Vaults → the vault → Connections). Each secret is an env var name plus where the value is:
- Vault: a dropdown of your team's vaults connected to this namespace. If it's empty, connect a vault first.
- Item: type it, with suggestions from the vault's items.
- Key: type it, with suggestions. The portal reads the item's key names from OpenBao with your own access, names only, never values. If you can't read the item (for example a developer and a production vault), you get You can't read this item; type the key: type the key name and it works the same.
The app gets the values as a Kubernetes Secret through External Secrets. When a value changes in OpenBao, the app picks it up at the next refresh (within an hour) and restarts. Each env var name can be set once: not as both a variable and a secret.
See Secrets and, for database credentials, Requesting a database.
Expose¶
Reachable:
| Choice | Meaning |
|---|---|
| Not exposed | No route. Other pods in the namespace can still call the Service |
| Internal only | On the internal gateway: reachable inside the network and over the VPN |
| Public, through the web firewall | On the internet, through the web firewall |
| Internal and public | Both |
- Hostname: one of the namespace's hostnames, or Any name under
<domain>when the namespace has a wildcard; then type the name. See Hostnames and domains. - Path: a path prefix,
/by default. - Web firewall (public routes): Detect only (log, don't block), the default, or Block. Start in detect, check the WAF's events, then block. Once the app is public, its mode is changed with the web firewall's mode switch, not here.
- Trust the internal certificate authority: mounts the internal CA bundle and sets
SSL_CERT_FILE, for apps that call internal HTTPS services.
DNS and the certificate come automatically: see Hostnames and domains.
Sign-in¶
With a route, Require sign-in puts a Keycloak sign-in in front of the app at the gateway, so the app needs no OIDC code. Pick one of your team's confidential Keycloak clients in the same environment and a vault item for its secret; Store the client secret there copies it from Keycloak with your own access, without showing it. The client must allow the redirect URI the form shows (Add it to the client opens that change).
Signed-in requests reach the app with X-Auth-Email, X-Auth-Name and X-Auth-Subject. Who may
sign in is decided by the client's roles. Served without sign-in lists exact paths that skip
it, such as a web manifest and its icons. <path>/logout signs out.
After it's deployed¶
The app's drawer shows its status, image, route and pods.
| Status | Meaning |
|---|---|
| Deploying | A change is open, or the new version isn't rolled out yet |
| Running | Every replica is ready on the wanted image |
| Degraded | Running, but fewer replicas are ready than wanted |
| Failed | Not ready, with Flux's or Kubernetes's message |
- Deploy a tag changes only the tag.
- Change app opens the form again and rewrites the whole file in the portal's layout: defaults are written out, and comments inside the file are dropped (the header stays).
- Restart starts new pods and stops the old ones one at a time.
- Logs shows the last 500 lines (or 15 minutes) of a pod, including the previous container after a crash.
- An app has one open change at a time: wait for it to finish before the next.
A deploy waits up to 15 minutes for the rollout. If it doesn't finish, the change fails with the reason and the file stays as merged. To roll back, Deploy a tag with the previous tag; if the app follows new tags, turn that off first, or Flux moves it forward again.
Approvals¶
| Change | dev, qa | prod |
|---|---|---|
| Deploy a new app | A team owner | A team owner, then platform |
| Publish it, move its public hostname, weaken its WAF, or give it a new secret | A team owner | A team owner, then platform |
| Anything else (tag, replicas, size, env vars, health check, internal route), delete | A team owner | A team owner |
The team owner step is skipped when you're an owner yourself.
Limits¶
A portal app is one container, one route, HTTP. Anything else, such as sidecars, workers
with several containers, CronJobs or non-HTTP services, goes as plain Kubernetes YAML in the
namespace's manifests/ folder in kube-ops, within what the namespace's permissions and policies
allow.