Platform change path  / field guide
Practitioner field guide · 8 September 2026

Changing a platform without stopping it

Zalando has kept the configuration of its production Kubernetes fleet in a public repository since 2017, and merged a change into it on the morning this guide was written. This is a decade of one retailer's compute platform read from that record: how a change is promoted across the fleet, which in-house components survived nine years and which were handed back to upstream, and what the two outages the company published say about shared fate inside a cluster.

20 primary artefacts 5 repositories 2 published incidents Evidence through September 2026 Read: 22 min
01

The territory

A company that runs its own compute platform signs up for a change stream it does not control. This guide is about the machinery that absorbs it.

The operating system underneath needs patching. The scheduler ships a new minor version several times a year and drops an API two versions later. The credential path gets deprecated by the cloud provider. The routing layer grows a feature the network team wants. None of it can stop while several hundred delivery teams keep shipping on top. So the interesting engineering question for a platform group is not which components are in the architecture diagram. It is the change path: how do you move a modification through a fleet of production environments quickly enough to stay current, and slowly enough to catch the one change that takes everything down?

Almost every organisation answers that question privately, which is why the public literature on platform engineering is mostly conference talks about the destination and almost nothing about the traffic. Zalando answers it in the open, through a decision made around 2017 that appears to have been about community goodwill and turned into something more useful: the configuration that defines its production Kubernetes clusters lives in a public repository, it is the operational article rather than a sanitised copy, and it is still being changed. The newest merge on the dev branch when this guide was compiled closed pull request 12233, on 8 September 2026.

That makes an unusual corpus available. Instead of reading what a platform team says it does, you can read the branch layout it promotes through, the decision records that argue for that layout, the pipeline definition that gates each change, the sixty-three component directories that constitute the platform today, and the pull requests that were closed without merging. Two things are missing from it, and both are stated plainly in this guide: there is no cost data, and the published failure record is four commits long and stops in June 2019.

Scope. This guide covers the platform layer at one company between 2015 and 2026: cluster lifecycle, change promotion across a fleet, the controller inventory, ingress and identity, and node provisioning. It does not cover Zalando's application architecture, its data platform, its spend, or any comparison against peers, because the session that built it could reach code hosts and nothing else. Where a claim rests on one artefact, the text says so.

17,765
Commits on the branch that defines the production fleet
63
Component directories deployed into every cluster
1.4 → 1.36
Kubernetes versions the same fleet has been carried across
4
Commits ever made to the postmortem directory, all in 2019

Figure 1 · The change path

provision a real cluster,
run conformance e2e

automatic after a wait,
if metrics stay clean

promotion pull request
opened automatically,
merged only by a human

straight to the
affected channel

Pull request
on a feature branch

dev
infrastructure cluster

alpha
infra prod, infra test,
playground

beta

stable
production clusters

Hotfix

provision a real cluster,
run conformance e2e

automatic after a wait,
if metrics stay clean

promotion pull request
opened automatically,
merged only by a human

straight to the
affected channel

Pull request
on a feature branch

dev
infrastructure cluster

alpha
infra prod, infra test,
playground

beta

stable
production clusters

Hotfix

Every configuration change enters through a pull request that provisions a real cluster, then walks four branches, gaining clusters at each step. Sources: ADR-005 and delivery.yaml.
Diagram source
02

How it is actually built

Four moving parts hold the fleet: a registry of clusters, a controller that reconciles them, a repository with one branch per risk appetite, and a belt of in-house controllers hanging off the scheduler's extension points.

Figure 2 · What was replaced, and what was not

In-house platform on AWS
CloudFormation templates,
immutable images
(deprecated, last tag 2023)

Self-managed Kubernetes
Container Linux nodes,
auto-scaling groups,
80+ clusters

Managed control planes
zalando-eks provider,
Karpenter, own Ubuntu image,
Kubernetes v1.36.3

Constant across all three:
config in git, channels,
Skipper, ingress controller,
policy controllers

In-house platform on AWS
CloudFormation templates,
immutable images
(deprecated, last tag 2023)

Self-managed Kubernetes
Container Linux nodes,
auto-scaling groups,
80+ clusters

Managed control planes
zalando-eks provider,
Karpenter, own Ubuntu image,
Kubernetes v1.36.3

Constant across all three:
config in git, channels,
Skipper, ingress controller,
policy controllers

Three eras of the same platform group. The bottom row is the set of components that spans all of them. Sources: Senza tags, the 2017 admin guide and config-defaults.yaml.
Diagram source

The shape is easiest to see from the oldest decision record in the repository. ADR-001 settles where the current state of a cluster is written down, and the answer is neither the cluster nor the repository: the Cluster Lifecycle Manager stores three version strings in the status field of a Cluster record in a separate Cluster Registry, called next_version, current_version and last_version. Each is the git commit of the channel concatenated with a hash of that cluster's own configuration. The record explains the encoding choice in one word, KISS, and lists the consequence that matters: because last_version is the last working version, a rollback is a version string, not a restore procedure.

That gives the reconciliation loop its shape. The lifecycle manager polls two sources, the registry for which clusters exist and which channel each one follows, and the repository for what that channel currently says. It is described as reentrant, so it can be killed at any point and will continue a cluster update from where it left off, because all state is in the registry and in git. Nothing durable lives in the process. That single property is what makes a fleet upgrade restartable, and it is the reason this design survived a decade of Kubernetes releases while the components inside the clusters were replaced underneath it.

What the clusters contain has changed almost completely. The admin guide still in the repository describes the 2017 arrangement: clusters provisioned with an adapted kube-aws, nodes running CoreOS Container Linux, a master auto-scaling group of two nodes, three worker groups split one per availability zone, and more than eighty production clusters. Read the configuration defaults today and none of the specifics survive. Nodes are provisioned by Karpenter with an explicit exclusion list of instance families, the machine image is Zalando's own Ubuntu 22.04 build carrying Kubernetes v1.36.3, and a second provider named zalando-eks sits alongside the original one, with its own control plane logging, IP family and IAM proxy settings.

The migration to managed control planes is visible in the configuration rather than announced in it. There is a skipper_eks_migration_enabled flag, and a sibling cluster concept: when a cluster has a sibling, the ingress layer publishes an additional service so traffic can be served across the pair while workloads move. Read alongside pull requests from 2026 that set the Karpenter maximum pods per node for EKS clusters, the reading is straightforward and worth stating as an inference rather than a report: after roughly nine years of operating its own control planes, the platform group is moving clusters onto the cloud provider's managed offering, in place, one sibling pair at a time.

Figure 3 · Reference architecture of the fleet

One cluster

Fleet control

CloudFormation + manifests

non-disruptive rolling update

Cluster Registry
which clusters, which channel,
three version strings

Cluster Lifecycle Manager
reentrant reconcile loop

Config repository
dev / alpha / beta / stable

Control plane
self-managed or zalando-eks

Nodes
Karpenter, own Ubuntu AMI

Controller belt
ingress, IAM, downscaler,
janitor, stackset, metrics

One cluster

Fleet control

CloudFormation + manifests

non-disruptive rolling update

Cluster Registry
which clusters, which channel,
three version strings

Cluster Lifecycle Manager
reentrant reconcile loop

Config repository
dev / alpha / beta / stable

Control plane
self-managed or zalando-eks

Nodes
Karpenter, own Ubuntu AMI

Controller belt
ingress, IAM, downscaler,
janitor, stackset, metrics

Reconstructed from ADR-001, the lifecycle manager README and cluster/manifests. The dashed provider is the newer of the two and does not yet run everywhere.
Diagram source

The layer worth naming is the third box inside the cluster. Sixty-three directories in cluster/manifests deploy into every cluster, and they divide cleanly into two kinds. Some are upstream or cloud-vendor software taken as-is: CoreDNS, kube-proxy, flannel, Prometheus, metrics-server, the vertical pod autoscaler, the EBS and EFS CSI drivers, the AWS load balancer controller, the NVIDIA device plugin, Karpenter. The rest are controllers Zalando wrote, and every one of them sits at an extension point the platform already offered rather than replacing a core component: an ingress controller that reconciles AWS load balancers from Ingress and RouteGroup objects, an IAM controller and proxy that turn pod identity into cloud credentials, a downscaler that turns non-production workloads off out of hours, a janitor that deletes what nobody owns, a stackset controller that expresses a deployment as a set of versioned stacks, a metrics adapter that lets any metric drive autoscaling, and a pod disruption budget controller that stops rolling node updates from taking a service below its floor.

Call it the controller belt. Its defining property is that no item in it forks the platform. The belt is bolted to interfaces the scheduler publishes, which is why the same belt survived the move from Container Linux to a custom image, from auto-scaling groups to Karpenter, and now from self-managed control planes to a managed one. Three of these components have outlived their original context entirely: Skipper, the layer-seven proxy that does the in-cluster routing, states in its README that it handles 350,000 route definitions and daily traffic between 500,000 and 7 million requests per second across more than one hundred production clusters, and Patroni, written at Zalando to run PostgreSQL clusters, is now described in its own README as having started as a fork of Governor and no longer carries the company's name.

Two ingress controllers, on purpose

The in-house controller that programs AWS load balancers is still deployed in 2026, with flags for network load balancer zone affinity, ALPN policy and container network interface target mode. The cloud vendor's own load balancer controller sits beside it. Both are present because the fleet currently spans two providers.

Evidence: ingress-controller deployment

Identity moved in-house, not out

The 2017 guide documents kube2iam, a third-party daemon, for giving pods a cloud role. The 2026 manifest set carries a Zalando-written IAM controller and an IAM proxy alongside it. This is the one place where the company took work back rather than handing it over, and the credential path is exactly where a general solution stops fitting a specific organisation.

Evidence: 2017 admin guide, 2026 manifests

Policy encoded as controllers

The downscaler, the janitor, the pod disruption budget controller and the stackset controller do not add capability to Kubernetes. They encode company policy about cost, ownership, availability floors and what a deployment means, in a form the scheduler enforces continuously rather than a wiki page nobody reads.

Evidence: cluster/manifests, checked 2026-09-08

03

The decisions that matter

Five decision records, one pipeline definition and a set of closed pull requests, read for the option that lost and the condition that would bring it back.

Decision: where does a fleet's risk appetite live?

Chosen
  • Configuration channels are git branches. A cluster names a branch in the registry, and merging into that branch updates every cluster following it.
  • ADR-003 chose it because promotion, hotfixing and experimentation all become operations the team already knows how to perform.
Rejected
  • A configuration service with per-cluster settings and a release concept of its own.
  • ADR-001 gives the reason obliquely: the version format is deliberately opaque to every system except the provisioner, so the registry never learns what a release is.
Flips when
  • Clusters stop being interchangeable. Branch channels work because the fleet is homogeneous and a cluster is described by a small set of configuration items; the moment clusters need genuinely different component sets, a branch per risk appetite becomes a merge conflict per cluster.

Decision: what is allowed to gate a platform change?

Chosen
  • Every pull request provisions a real cluster on the cloud provider, runs the end-to-end suite including the official conformance tests, reports to the request and deletes the cluster afterwards.
  • ADR-005 additionally requires the first post-merge target to be an existing cluster with applications running on it, precisely so an update is tested on a used cluster and not only a fresh one.
Rejected
  • Testing against the cluster registry, which would have been simpler.
  • ADR-005 says they decided against polluting the registry, so test clusters are created by calling the lifecycle manager directly.
Flips when
  • Cluster creation stops being cheap or reproducible. This gate is affordable only because a cluster is a CloudFormation stack plus a manifest set; where provisioning takes hours or needs manual approval, the same idea has to shrink to a long-lived staging fleet, and you lose the guarantee that creation itself works.

Decision: build the component or take the upstream one?

Chosen
  • Build where the component encodes company policy: ingress to cloud load balancer mapping, pod identity, out-of-hours downscaling, ownership expiry, disruption budgets, the deployment abstraction.
  • Nine years on, the in-house ingress controller is still deployed and still gaining flags.
Rejected
  • Building the mechanism. Node provisioning, autoscaling of pods on metrics, block and file storage drivers and the machine image bootstrap were all ceded to upstream or the vendor as those projects matured.
  • The clearest case is node provisioning, now Karpenter with a Zalando exclusion list rather than a Zalando autoscaler.
Flips when
  • Upstream ships the policy hook you were compensating for. The test is whether your component would still exist if the platform grew one configuration field: if yes, it is policy and you keep it; if no, it is a loan against someone else's roadmap and you should plan its repayment.
DecisionChosenRejected or replacedStated reasonEvidence
Where cluster state livesThree version strings in the registry recordMultiple typed fields in the registry schemaKeeps the provisioner's format out of the registry schema, and keeps it human readable for debuggingADR-001
How many channelsThree, named dev, alpha and stableAn unbounded set of per-cluster pinsAnswers the questions ADR-003 left open about promotion and controlADR-005
Promotion authorityAutomatic to alpha after a wait, human approval into stableFully automatic promotion to productionADR-005 requires the final pull request to carry additional human approvalADR-005
Node operating systemZalando-built Ubuntu 22.04 image, Kubernetes v1.36.3CoreOS Container LinuxNot stated in the corpus; the 2017 guide praises Container Linux and the 2026 configuration no longer references itconfig-defaults.yaml
Instance selectionKarpenter with an excluded families listAMD-based c5a, m5a, r5a and six further families, excluded by defaultNot stated; the exclusion is expressed as configuration rather than argued in a recordconfig-defaults.yaml
Control plane ownershipA second provider, zalando-eks, with sibling clusters for migrationContinuing to self-manage every control planeNot stated in the corpus; inferred from the provider switch, the migration flags and 2026 pull requests scoped to EKS clustersconfig-defaults.yaml

Figure 4 · Build it or rent it

No

Yes

Forks

Plugs in

Would the component
still be needed if the
platform grew one
configuration field?

Rent it.
Mechanism: node provisioning,
storage drivers, metrics server

Does it plug into a
published extension point,
or fork a core component?

Do not build.
You will carry the fork
across every upgrade

Build it.
Policy: ingress mapping, pod identity,
downscaling, ownership expiry,
disruption floors

No

Yes

Forks

Plugs in

Would the component
still be needed if the
platform grew one
configuration field?

Rent it.
Mechanism: node provisioning,
storage drivers, metrics server

Does it plug into a
published extension point,
or fork a core component?

Do not build.
You will carry the fork
across every upgrade

Build it.
Policy: ingress mapping, pod identity,
downscaling, ownership expiry,
disruption floors

The test that separates the components this platform kept for nine years from the ones it handed back. Derived from the inventory in cluster/manifests; the sources state the outcomes, not this rule.
Diagram source
The drift worth noticing

ADR-005 decided on three channels. The pipeline definition triggers on four branch names, and the branch listing shows dev, alpha, beta and stable all updated on 8 September 2026, plus two standing promotion branches called dev-to-alpha and beta-to-stable. The record was never amended. This is the ordinary fate of decision records, and it is an argument for reading the pipeline next to the record rather than instead of it: the record tells you what was argued, the pipeline tells you what is true.

04

What broke in production

The company published exactly two postmortems, both in the first half of 2019, both about the same underlying mistake in two different disguises.

Postmortem

One application took the cluster's name resolution down

AssumptionThat cluster DNS was infrastructure, sized from historical usage, and that an application could not affect it. CoreDNS ran with a memory limit of 100Mi set from observed consumption.
What happenedCalls from an aggregation service to one minor downstream dependency began timing out. The service returned 404s, clients retried, and the request spike became a DNS query spike. The application is a Node.js service with no DNS caching, running with the default search domain configuration, so one external lookup costs ten queries. CoreDNS pods hit the memory limit, were killed together, and could not recover under continued load. The result was a total DNS outage for the entire cluster.
Blast radiusAll web product and outfit pages of the fashion store returned a high rate of errors to customers for over an hour on 7 January 2019. Internal monitoring for the cluster was also down, because it needed to resolve external names to push metrics and raise alerts.
FixImmediately, raising the memory limit from 100Mi to 2000Mi by hand. Structurally, running CoreDNS fronted by dnsmasq on every node, so the resolver stops being a cluster-wide singleton, plus alerting that runs from outside the cluster.
Design ruleAny dependency that every pod calls on every request is a shared-fate component, and the correct response to saturation is locality rather than headroom. The postmortem states the target directly: the blast radius of an application increasing its query rate should be isolated to a node, ideally to a pod. Give it more memory and you have bought time; give it per-node locality and you have changed the failure class.
Postmortem

A default rate limit became the capacity ceiling

AssumptionThat the node agent would report pod status promptly, and that anything depending on pod IP addresses being published would therefore work.
What happenedContinuous delivery builds could not obtain cloud credentials, because the credential daemon needs the pod IP address, which the node agent publishes. The agent ships with a request rate limit to the API server of five per second. After a rescaling left one node available for builder pods, rapid pod creation and deletion pushed the agent behind, and status updates were delayed by minutes.
Blast radiusBuilds on the in-house delivery platform failed for almost an hour on 11 June 2019. Customer traffic was unaffected; the engineering organisation could not ship.
FixScaling the cluster up by hand to spread the builder pods, then raising the agent's rate limit from the default of five to fifty as a global configuration change. The pull request describes the default as ridiculously low, causing delays of five minutes or more when twenty pods start at once. It merged on 21 June 2019 and reached the stable channel on 25 June.
Design ruleA platform default is a capacity assumption made by someone who has never seen your workload. Churn, not size, is what exposes it, so the review question for any adopted component is which of its defaults were chosen for a cluster far smaller than yours, and what the symptom looks like when one binds. Here the symptom appeared three layers away, in a credential daemon, which is why nobody suspected a rate limit.

Read together, both incidents are the same failure class in different clothing: a component that everything depends on was configured for the average case, and the average case was not the failure case. Neither was a bug in the software. The DNS outage came from a memory limit derived from historical usage, and the build outage from an upstream default nobody had revisited. Both fixes changed a number; only one of them, the move to a per-node resolver, changed the architecture. That distinction is the most useful thing an architect can take from this pair.

The third failure class in this corpus is not written up anywhere, because it is a class of near-miss that the change path is designed to absorb. It is visible instead in the closed pull requests. One from March 2025 is titled as an update of Karpenter to a specific version with the parenthetical note that this should fail, which is a deliberate negative test of the gate rather than a change anyone wanted merged. Others, such as a February 2026 attempt to enable a new instance family for the default node pool and a March 2026 proposal to disable AMD instance types in production by default, were opened, tested and closed without merging. The record of what a platform team decided not to do is thin everywhere in public engineering writing; here it is a filter on a pull request list.

What the failure record does not contain

The postmortem directory has received four commits in its life, between 4 February and 29 June 2019, and none since. Zalando has run this fleet for another seven years and has certainly had incidents; it has simply stopped publishing them, while continuing to publish every configuration change. Treat the two write-ups here as a sample of one year's operational reality, not as the decade's. If you are evaluating a platform approach on public evidence, this asymmetry is the trap: the change record survives because it is a by-product of doing the work, and the failure record does not, because writing it is separate work that no pipeline enforces.

Figure 5 · How one service's retries became a cluster outage

MonitoringCoreDNS (clusterwide)Minor dependencyAggregation serviceClientMonitoringCoreDNS (clusterwide)Minor dependencyAggregation serviceClientmemory limit 100Mireachedpage requestcalltimeout404retry storm10 queries per lookup, no cachepods OOM killed togethererrors for over 1 hourcannot resolve, cannotpage
MonitoringCoreDNS (clusterwide)Minor dependencyAggregation serviceClientMonitoringCoreDNS (clusterwide)Minor dependencyAggregation serviceClientmemory limit 100Mireachedpage requestcalltimeout404retry storm10 queries per lookup, no cachepods OOM killed togethererrors for over 1 hourcannot resolve, cannotpage
The January 2019 path from a single slow dependency to total loss of name resolution, including the monitoring that could not report it. Reconstructed from the postmortem.
Diagram source
05

Numbers you can plan against

Everything quantitative in the corpus, with the date attached, because three of these figures are stale and the page should say which.

MetricValueKindContextAs ofSource
Production clustersmore than 80MeasuredStated in the admin guide during the early migration2017 eraAdmin guide
Clusters operated by the lifecycle manager200+ClaimedREADME, whose upgrade claim stops at Kubernetes v1.24v1.24 eraLifecycle manager README
Clusters running the layer-seven proxymore than 100ClaimedSkipper README, undated in the document itselfChecked 2026-09-08Skipper README
Route definitions in the proxy350,000ClaimedShop frontend routingChecked 2026-09-08Skipper README
Daily request rate through the proxy500k to 7M per secondClaimedServing 15,000 ingress objects and 3,750 route groupsChecked 2026-09-08Skipper README
Unit cost of routingunder 5 cents per million requestsClaimedThe single cost figure in the entire corpus, method not publishedChecked 2026-09-08Skipper README
Kubernetes minor versions crossed in place32Derivedv1.4 named as the oldest cluster's starting point, v1.36.3 built into the current node image2026-09-08config-defaults.yaml
Staleness of the lifecycle manager README12 minor versionsDerivedv1.24 claimed there against v1.36.3 in the configuration2026-09-08README
Node agent request rate limit5 raised to 50 per secondMeasuredDefault caused delays over five minutes when twenty pods start together2019-06-21Pull request 2247
Resolver memory limit at failure100Mi raised to 2000MiMeasuredSet from historical usage, killed under a query spike2019-01-07Postmortem
Promotion time, merge to stable4 daysMeasuredThe node agent fix, merged 21 June and stable on 25 June 20192019-06Pull request 2247
Platform components per cluster63MeasuredDirectory count under cluster/manifests2026-09-08Manifests
Read these carefully

Only four rows here are measurements from an incident or a merged change. The cluster counts and the proxy figures are self-reported in README files that carry no date, and the routing cost figure names no method, no denominator and no accounting boundary, so it is a claim rather than a benchmark. The two derived rows are arithmetic on version strings and are labelled as such. What nobody has published anywhere in this corpus: the current cluster count, node count or spend, the promotion interval actually configured between channels, and any incident frequency after 2019. If you plan against this platform's numbers, plan against the four measured rows and treat the rest as direction.

06

The evidence wall

Every source behind this page, graded and dated. Two tiers are absent and the absence is explained below rather than hidden.

Postmortem Zalando2019-02

Total DNS outage in Kubernetes cluster

A complete incident write-up with timeline, six contributing factors, lessons and action items. Names the memory limit, the query amplification from the default search domain setting, and the monitoring path that shared the failure domain.

Carry forwardCluster-wide singletons need locality, not headroom; the target is a blast radius of one node.
github.com/zalando-incubator/kubernetes-on-aws
Postmortem Zalando2019-06

Build errors of the continuous delivery platform

Short and unusually precise: a default rate limit on the node agent, exposed by pod churn on a single node, surfacing as a credential failure three components away. Links the permanent fix as a configuration change.

Carry forwardAudit the defaults of adopted components for the cluster size they assume, and expect the symptom to appear far from the cause.
github.com/zalando-incubator/kubernetes-on-aws
Decision record ZalandoAccepted

ADR-001: store cluster versions in the Cluster Registry

Defines the three version strings and argues for an opaque concatenated format so the registry never learns the provisioner's internals. Lists rollback via the last working version as a consequence rather than a feature.

Carry forwardA fleet reconciler is restartable only if every piece of its state is in a store it does not own.
docs/dev-guide/arch/cluster-lifecycle/adr-001.rst
Decision record ZalandoUndated

ADR-003: organize cluster versions in branches

Maps configuration channels onto git branches and draws the promotion graph. Ends with three explicit open questions about when promotion happens, how it is triggered and what approval it needs, which the later record answers.

Carry forwardA decision record that ships its unresolved questions is more useful than one that pretends completeness.
adr-003-organize-cluster-versions.rst
Decision record ZalandoUndated

ADR-005: how to use channels

Settles three channels, requires end-to-end tests on a freshly provisioned cluster per pull request, and puts a human approval on the final promotion. Also states that the first cluster updated after merge must be one with applications already running.

Carry forwardTest the update on a used cluster, not only a new one; upgrade bugs live in accumulated state.
adr-005-channels-usage.rst
Source Zalando2026-09-08

delivery.yaml, the pipeline that gates every change

Shows the build triggering on dev, alpha, beta and stable plus pull requests, and a step that creates a cluster on the cloud provider for end-to-end tests using a named lifecycle-manager role.

Carry forwardWhere a record and a pipeline disagree, the pipeline is the architecture.
delivery.yaml
Source Zalando2026-09-08

cluster/config-defaults.yaml

Around 1,540 lines of fleet defaults: Karpenter feature gates and excluded instance families, a second provider named zalando-eks, control plane logging and IP family settings, proxy migration flags, and the machine image carrying Kubernetes v1.36.3.

Carry forwardA defaults file is the most honest architecture document a platform has, because it is executed.
cluster/config-defaults.yaml
Source Zalando2026-09-08

cluster/manifests, sixty-three component directories

The whole platform inventory in one listing: upstream components taken as-is next to in-house controllers for ingress, identity, downscaling, cleanup, disruption budgets, stacksets and metrics adaptation.

Carry forwardSort your own platform inventory into mechanism and policy; only the policy half justifies in-house code.
cluster/manifests
Source Zalando2026-09-08

The in-house ingress controller, still deployed

Deployment arguments show network load balancer zone affinity, ALPN policy, deregistration delay, container network interface target mode and a pod label selector pointing at the proxy. Nine years of accumulated operational detail.

Carry forwardLongevity in a platform component correlates with how much local policy it encodes, not with how clever it is.
ingress-controller/deployment.yaml
Source Zalando2026-09-08

Branch listing of the configuration repository

dev, alpha, beta and stable all updated on the same day, alongside standing dev-to-alpha and beta-to-stable promotion branches and several component update branches named after container images.

Carry forwardPromotion implemented as a long-lived pull request gives you an audit trail and a place to argue, for free.
branches/all
Source Zalando2019-06-21

Pull request 2247: increase QPS for kubelet and controller manager

The structural fix from the June 2019 incident, with the author's description of the default as ridiculously low. Merged on 21 June and promoted through alpha and beta to stable by 25 June.

Carry forwardA four-day path from merge to production is a design output of the channel model, and it is measurable.
pull/2247
Source Zalando2025-03

A pull request opened in order to fail

Titled as an update of the node provisioner to a specific version with the note that this should fail. Closed without merging. Evidence that the gate is itself tested rather than assumed.

Carry forwardProve your change gate rejects a known-bad change on a schedule, or you are trusting an untested control.
pull/9078
Source Zalando2026-03

A closed proposal to disable AMD instance types by default

Opened and closed without merging in March 2026, in the same period as several other node provisioning experiments. The exclusion list in the defaults file shows which of these arguments ended up as configuration.

Carry forwardClosed pull requests are the only public record of what a platform team decided not to do.
pull/10710
Source Zalando2019

Commit history of the postmortem directory

Four commits in total, between 4 February and 29 June 2019, by two authors. Nothing since, while the repository as a whole took a change today.

Carry forwardPublication practices decay unless a pipeline enforces them; the artefacts that survive are the ones produced as a by-product of the work.
commits/dev/docs/postmortems
Case study Zalando2017 era

Running Kubernetes in Production

The team's own operational account from the early migration: more than eighty production clusters, one cluster per cloud account, an adapted provisioning tool, Container Linux nodes, and five stated goals including no manual operations and no pet clusters. Now a historical document contradicted by the configuration beside it.

Carry forwardDate every platform document at the top, or it will be read as current for a decade.
docs/admin-guide/kubernetes-in-production.rst
Source ZalandoChecked 2026-09-08

Cluster Lifecycle Manager README

States that the component has been developed internally since January 2017, operates more than two hundred clusters, and has carried the oldest cluster from Kubernetes v1.4 to v1.24 by continuous update. Describes the reentrant design and non-disruptive rolling node updates.

Carry forwardIn-place continuous upgrade is achievable for a fleet, and the enabling property is that the reconciler holds no state of its own.
cluster-lifecycle-manager README
Source ZalandoChecked 2026-09-08

Skipper, the layer-seven proxy

Self-reported production figures: 350,000 route definitions, more than one hundred production clusters, daily traffic between 500,000 and 7 million requests per second, 15,000 ingress objects and 3,750 route groups, at a stated cost below five cents per million requests.

Carry forwardThe one published unit cost in this corpus comes from a README, with no method attached; treat vendor-style numbers from your own teams the same way.
github.com/zalando/skipper
Source CommunityChecked 2026-09-08

Patroni, after the handover

The PostgreSQL high-availability template written at Zalando now describes itself as formerly known as Zalando's Patroni and as a fork of Governor, with no company ownership language and support for several coordination stores.

Carry forwardGeneric mechanism can be given away and keeps working; company policy cannot, which is the same line that decides build versus rent.
github.com/zalando/patroni
Source Zalando2023-03-27

Senza, from the platform that came before

The command line tool of the pre-Kubernetes in-house platform, carrying a shouted notice that the project is not actively maintained any more. Its final tag is dated March 2023, years after the successor platform was in production.

Carry forwardReplaced platforms do not stop; budget for the overlap, which here ran to several years.
zalando-stups/senza tags
Source ZalandoChecked 2026-09-08

The deprecation notice on the previous platform

The tool that generated and executed CloudFormation templates for the pre-Kubernetes platform still describes what it does at length, under a single shouted line saying the project is not actively maintained any more. No date, no successor named, no migration guidance.

Carry forwardA deprecation notice without a date and a successor is an unanswered question for whoever finds it in five years; write both.
github.com/zalando-stups/senza
Two tiers are missing

There are no papers and no conference talks in this wall, and no engineering blog posts from any organisation. That is a property of the session rather than of the topic: the network policy in force reached code hosts and nothing else, so material on video platforms, conference sites and company blogs could not be fetched, and this skill forbids citing anything not retrieved. The lifecycle manager README names a 2018 conference talk about exactly this system, which is the obvious next thing to watch and is deliberately not cited here. Nor is there any independent measurement or peer comparison; every number above comes from the company that runs the system.

07

Build a miniature, then productionise it

Six rungs. The line between exercise and production sits between rungs three and four, where the change gate stops being advisory.

Put one environment's configuration in a repository

Take one non-production environment and move every component it runs into a single directory of manifests plus one defaults file. Nothing clever, no promotion yet.

Done when: you can delete the environment and rebuild it from the repository alone.  Teaches: which parts of your platform exist only in someone's shell history.

Add a reconciler that holds no state

Write the smallest loop that reads a list of environments and the repository, computes a version string from both, applies the difference and records the version it reached plus the last one that worked.

Done when: killing the loop mid-apply and restarting it converges without human help.  Teaches: why the version string is a concatenation and why rollback is a string rather than a procedure.

Split the repository into two channels

Create a second branch, point one environment at each, and promote by merge. Write down, as a record, who may promote and after how long.

Done when: a change reaches the second environment only via the first.  Teaches: that risk appetite is a property of an environment, not of a change.

Make every change provision a real environment

Wire the pull request to create an environment from scratch, run the platform's own conformance suite plus one test per policy controller you rely on, and destroy it.

Done when: a deliberately broken change is rejected without a human noticing it.  Teaches: the cost of the gate, which is the number that decides whether this model is available to you at all.

Move the first post-merge target onto a used environment

Point the first automatic update at an environment that has real workloads and has been upgraded before, not a fresh one, and watch service level indicators across the update rather than the apply exit code.

Done when: an upgrade that passes on a clean environment and fails on a used one is caught here.  Teaches: that upgrade defects live in accumulated state.

Move one alarm outside the failure domain

Take the single alarm that says the environment is serving traffic and run it from somewhere that shares no dependency with the environment, name resolution included. Then run a game day that kills the cluster-wide resolver.

Done when: the outside alarm fires before anyone reports the outage.  Teaches: the January 2019 lesson at first hand, for the price of one afternoon.

08

Keep hunting

This page was built without a single engineering blog post. These are the queries and paths that produced it, which work on any organisation that operates in the open.

Find the decisions and the failures inside a repository

  • path:docs/postmortems
  • path:docs/dev-guide/arch filename:adr
  • "this should fail" is:pr is:closed is:unmerged
  • filename:delivery.yaml OR filename:config-defaults.yaml

Date the architecture without a blog post

  • /commits/<branch>/<path> for the first and last commit to a directory
  • /branches/all to see which channels are still moving
  • /tags on the superseded project, for the real end-of-life date
  • grep the defaults file for a version string, then diff it against every README claim
09

References

  1. Zalando, kubernetes-on-aws, production cluster configuration GitHub. Checked 2026-09-08.
  2. Total DNS outage in Kubernetes cluster Postmortem, committed 4 February 2019. Checked 2026-09-08.
  3. Build errors of the continuous delivery platform Postmortem, committed 28 June 2019. Checked 2026-09-08.
  4. Commit history of docs/postmortems GitHub, four commits between February and June 2019. Checked 2026-09-08.
  5. ADR-001: store cluster versions in Cluster Registry Decision record, status accepted. Checked 2026-09-08.
  6. ADR-003: organize cluster versions in branches Decision record. Checked 2026-09-08.
  7. ADR-005: how to use channels Decision record. Checked 2026-09-08.
  8. delivery.yaml, the change pipeline GitHub, schema version dated 2017-09-20. Checked 2026-09-08.
  9. cluster/config-defaults.yaml GitHub. Checked 2026-09-08.
  10. cluster/manifests component inventory GitHub. Checked 2026-09-08.
  11. In-house ingress controller deployment GitHub. Checked 2026-09-08.
  12. Running Kubernetes in Production Zalando admin guide, early migration era. Checked 2026-09-08.
  13. Branch listing of the configuration repository GitHub. Checked 2026-09-08.
  14. Pull request 2247, increase QPS for kubelet and controller manager Merged 21 June 2019, stable 25 June 2019. Checked 2026-09-08.
  15. Pull request 9078, an update opened in order to fail Closed unmerged, March 2025. Checked 2026-09-08.
  16. Pull request 10710, disable AMD instance types in production by default Closed unmerged, March 2026. Checked 2026-09-08.
  17. Cluster Lifecycle Manager GitHub README. Checked 2026-09-08.
  18. Skipper, HTTP router and reverse proxy GitHub README. Checked 2026-09-08.
  19. Patroni, PostgreSQL high availability template GitHub README. Checked 2026-09-08.
  20. Senza release tags GitHub, final tag 27 March 2023. Checked 2026-09-08.