1. Start here
1.4 The application contract
Requirements: R8, R9, R11, R13, R14, R15, R16, R18, R21, R22, R23, R24, R26, R29, R31, R33.
What an application team has to do, and nothing more, to run on the platform in any cloud. Everything here is enforced by admission policy, a CI check or the onboarding, and each line links to the section that explains it; the reference application of section 0.3 meets all of it and contains no Azure name.
Manifests
Plain Kubernetes, in the team’s own repository, one directory per application and one per cell for what differs there (section 3.3). Only GA Kubernetes APIs, the Gateway API, External Secrets and Cilium network policy objects; no cloud CRDs (section 1.3).
| The application | Why | Enforced by |
|---|---|---|
Runs as a Deployment with replicas or HPA minReplicas of at least 2 (or 0 when switched off), topologySpreadConstraints over nodes, RollingUpdate with maxUnavailable: 0, and its own PodDisruptionBudget with maxUnavailable of at least 1 |
A release or a node drain never takes it down (section 3.4) | Admission; the PDB by CI |
Runs work that serves no request as a Job or CronJob (backoffLimit, activeDeadlineSeconds, ttlSecondsAfterFinished, concurrencyPolicy: Forbid) or, for a queue consumer, as a Deployment with at least 1 replica scaled on queue length and a graceful shutdown longer than its longest message; such a workload has no route and lives in the application’s home cell in both exposures |
Work that must not repeat runs once per environment; a cell drain finishes or restarts it (non-serving workloads) | Admission (the rollout rules apply to serving workloads only); the placement |
Declares readiness and liveness (and startup) probes and resource requests; limits stay in Git, requests are resized in place by the platform’s VPA unless the pod carries platform/rightsizing: off |
HPA, the autoscaler and the N+1 sizing work from them (section 2.3) | Admission; the VPA is platform-maintained |
Shuts down gracefully: a preStop delay and terminationGracePeriodSeconds longer than the gateway’s drain |
Connections finish during a rollout or drain (section 3.4) | Review |
| Uses an image from the environment’s registry, by digest once promotion verifies signatures; builds arm64 or multi-architecture images when it chooses those nodes | No image reaches acc or prd unsigned (section 3.2, section 2.7) | Admission (allowed images, Image Integrity); CI checks the image index against the labels |
Builds its image from the platform’s base image for its runtime with the platform’s build workflow, from a main that always builds and passes its tests; accepts that the platform rebuilds and promotes it when the base is patched |
Every application is patched on the platform’s timetable, gated by the team’s tests alone (section 1.5) | The build workflow; the registry refuses an image past its deadline |
Meets the restricted pod security standard, with readOnlyRootFilesystem: true (writable paths as emptyDir), automountServiceAccountToken: false, no hostNetwork, no privileged containers, no PersistentVolumeClaim in a stateless cell |
A compromised pod stays a pod (section 2.7) | Admission |
Puts the application’s Deployment, Service, HTTPRoute and ExternalSecrets in its namespace; writes no Ingress, Gateway, LoadBalancer, NodePort or egress policy out of the cluster |
The zone’s gateway is the platform’s (section 4.1) | Admission |
Traffic in and out
| The application | Why | Enforced by |
|---|---|---|
Publishes itself with an HTTPRoute on the zone’s Gateway, with hostnames in the zone’s domain (<name>.<zone>.<env>.<domain>); TLS, the certificate and DNS are the platform’s |
HTTPS only, HTTP/2 only and TLS 1.3 only at the edge (section 4.1) | Admission (Gateway rules) |
Serves HTTP/2 cleartext (h2c) on its route port, and declares appProtocol: kubernetes.io/h2c on that Service port; keeps HTTP/1.1 for the kubelet’s probes until Kubernetes 1.38 |
No HTTP/1.1 between a gateway and a pod (section 4.1) | Admission (h2c-upstream) and CI; the onboarding checks the image with curl --http2-prior-knowledge |
Answers GET /healthz with 200 only while it can serve, behind the gateway with its host name |
Traffic Manager and the synthetic probes leave out a cell whose probe fails (R24) | The probes |
| Expects no egress by default. A new external destination is a request: the egress agent proposes the Firewall rule from the denied flows and the team confirms it, or the team opens the pull request itself (section 5.1); the application writes no egress policy | Egress only to named destinations (R29) | The hub Firewall; admission (app-egress) |
| Listens from outside its namespace only through the zone’s gateway, HTTPS on 443 to its route; between its own pods it uses any protocol and port | One way in for every client, also the zone’s other applications (R22) | Cilium default deny (inbound only from the namespace and the zone’s gateway); admission (no LoadBalancer, NodePort or Ingress) |
| Reaches other applications, of its zone or another, only through their published names with a rule in its own rule file; another zone’s pods or nodes, a Key Vault, the instance metadata service and the stateful cluster of another zone never | Isolation (section 2.4, section 2.6) | Cilium default deny, the Firewall, the NSGs |
Identity, secrets and data
| The application | Why | Enforced by |
|---|---|---|
Authenticates to every Azure service with its workload identity (id-<zone>-<app>, the ServiceAccount’s azure.workload.identity/client-id annotation); its configuration holds endpoints and client IDs only |
No keys, connection strings or SAS tokens; local auth is disabled on the services (section 2.5) | Azure Policy on the services; the onboarding creates the identity and roles |
Takes a secret only where the target cannot use Entra ID, as an ExternalSecret against the namespace’s SecretStore key-vault, naming a secret under its own prefix <namespace>--; mounts it as a volume; never calls Key Vault itself |
Secrets reach a pod only through External Secrets Operator, separated per namespace (section 2.5) | Admission (ESO rules); Key Vault ABAC; no vault role on the application’s identity |
| Keeps its data in a PaaS service over its open protocol (PostgreSQL, MySQL, Redis, MongoDB, Kafka, S3-compatible storage) and the common subset of its features; a cloud-specific service or extension is a documented exception at onboarding | Portability of the contract (section 1.3) | Onboarding review |
| Changes its data and APIs backwards compatibly - expand → migrate → contract over separate releases | Two versions run at once during a release (section 3.4) | Review; the release train’s SLO gates |
| Asks for the stateful cluster only with a documented exception (why neither a stateless cell nor a PaaS service fits, owner, exit plan); there it terminates TLS 1.3 itself on an internal load balancer | The stateful cluster is the last option (section 3.1) | The exception label on the namespace; admission |
Choices the application makes with labels
| Pod label | Values | Effect |
|---|---|---|
platform/arch |
amd64 (default), arm64, multi |
Which nodes; multi lets the platform pick the cheapest (section 2.3) |
platform/capacity |
on-demand (default), spot |
Spot nodes when available, on-demand otherwise; only for workloads that tolerate losing replicas with 30 seconds’ notice |
platform/rightsizing |
off |
The VPA only recommends; the baseline in Git still moves by pull request |
The platform turns the labels into node selection; an application that sets selectors, tolerations or affinities for nodes itself is refused.
Observability
| The application | Why | Enforced by |
|---|---|---|
Logs JSON to stdout; emits OpenTelemetry (OTLP) to the endpoint the platform injects (OTEL_EXPORTER_OTLP_ENDPOINT) and propagates W3C traceparent; logs carry trace_id |
One Grafana Cloud stack per environment, node-local collection (section 6.2) | The injected variables; Beyla instruments without code |
| Logs no secret, token or personal data that the environment’s users may not see | Every user of a stack sees all of its data (section 6.2) | Alloy’s redaction is a safety net only |
Keeps its SLO objectives, dashboards and alerts in apps/<app>/observability/ next to its manifests |
Default SLOs come from the gateway; the team sets the targets | The IaC pipeline applies them |
Onboarding
An application starts with the request template (section 6.1): the zone,
the team’s Entra ID groups for view, edit and admin, the owner labels platform/team and platform/cost-center
and the contact annotation (section 2.6), the Azure services it needs and whether it
needs a secret, and, for an internal application, its home cell (section 3.1).
The onboarding agent turns it into the namespace, the identities, the roles and the Flux configuration; the team never
touches cluster-scoped objects. The journey around the contract - onboarding, migrating an existing application and
operating it in production - is section 1.5. Cost is reported per team from the labels, and a budget alerts the owners.