{"id":"l-dk-5-3","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Tune notes-api’s rolling update so releases never reduce capacity, watch and roll back a bad rollout with kubectl, and package the manifests as a Helm chart with per-environment values.","html":"<p>The release that taught me to respect rollouts went out on a Friday at 4 p.m. (I know). The new version had a typo in an environment variable name, so it couldn’t reach the database. The Deployment had no readiness probe and the default rollout settings, so Kubernetes happily replaced every working Pod with one that returned 500s. It took six minutes to notice and forty seconds to roll back. This lesson is about making the first part impossible and the second part routine.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">How a rolling update works</h2>\n<p>When you change anything in a Deployment’s Pod template, typically the image, the Deployment creates a <strong>new ReplicaSet</strong> for the new template and starts shifting replicas from old to new. Two settings pace it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">k8s/deployment.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">spec</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  revisionHistoryLimit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  minReadySeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  progressDeadlineSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">300</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  strategy</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">RollingUpdate</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    rollingUpdate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      maxSurge</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      maxUnavailable</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span></span></code></pre></div>\n<ul>\n<li><strong><code dir=\"ltr\">maxSurge</code></strong>: how many Pods above the desired count may exist during the update. With 4 replicas and <code dir=\"ltr\">maxSurge: 1</code>, there can be 5 Pods at once.</li>\n<li><strong><code dir=\"ltr\">maxUnavailable</code></strong>: how many below the desired count are tolerated. At <code dir=\"ltr\">0</code>, capacity never drops.</li>\n<li><strong><code dir=\"ltr\">minReadySeconds</code></strong>: a new Pod must stay ready this long before it counts as available. It catches versions that pass readiness and then crash ten seconds in.</li>\n<li><strong><code dir=\"ltr\">progressDeadlineSeconds</code></strong>: if the rollout makes no progress for this long, the Deployment reports it as failed.</li>\n</ul>\n<p>The defaults are 25% surge and 25% unavailable. I set <code dir=\"ltr\">maxUnavailable: 0</code> on every user-facing service: an old Pod is removed only after a new Pod is ready. Combined with the readiness probe from lesson 5.1, a broken release can’t take traffic away from a working one. It stalls with one unready new Pod while the old ones serve.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 240\" role=\"img\" aria-labelledby=\"l-dk-5-3-t1\"><defs><marker id=\"arrow-l-dk-5-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-dk-5-3-t1\">Four steps of a rolling update for 4 replicas. Each step adds one new-version Pod, waits for it to be ready, then removes one old-version Pod, so at least 4 ready Pods always exist.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"40\">start</text>\n  <rect class=\"d-box\" x=\"120\" y=\"22\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"190\" y=\"22\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"260\" y=\"22\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"330\" y=\"22\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-strong\" x=\"20\" y=\"95\">surge</text>\n  <rect class=\"d-box\" x=\"120\" y=\"77\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"190\" y=\"77\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"260\" y=\"77\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"330\" y=\"77\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-success\" x=\"400\" y=\"77\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"480\" y=\"96\">new Pod ready</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"150\">swap</text>\n  <rect class=\"d-box\" x=\"190\" y=\"132\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"260\" y=\"132\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"330\" y=\"132\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-success\" x=\"400\" y=\"132\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"480\" y=\"151\">one old Pod removed</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"205\">done</text>\n  <rect class=\"d-box-success\" x=\"120\" y=\"187\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-success\" x=\"190\" y=\"187\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-success\" x=\"260\" y=\"187\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-success\" x=\"330\" y=\"187\" width=\"60\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"480\" y=\"206\">old ReplicaSet at 0</text>\n  <text class=\"d-label-muted\" x=\"20\" y=\"234\">grey: 1.4.0 · green: 1.5.0 · repeat until all replaced</text>\n</svg><figcaption>Rolling update with maxSurge 1 and maxUnavailable 0</figcaption></figure>\n<p>The other strategy, <code dir=\"ltr\">Recreate</code>, deletes all old Pods before starting new ones. Use it only when two versions truly can’t run side by side, and accept the downtime.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Deploy, watch, roll back</h2>\n<p>Change the image in <code dir=\"ltr\">k8s/deployment.yaml</code> and apply, then watch:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> apply</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> k8s/deployment.yaml</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollout</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> status</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> deployment/notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --timeout=5m</span></span></code></pre></div>\n<p><code dir=\"ltr\">rollout status</code> waits until the rollout completes or fails, and exits non-zero on failure, so CI can use it as the “did the deploy work?” step. If it fails, or your dashboards turn red after it succeeds:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollout</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> history</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> deployment/notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollout</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> undo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> deployment/notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollout</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> undo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> deployment/notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --to-revision=7</span></span></code></pre></div>\n<p>Every old ReplicaSet (up to <code dir=\"ltr\">revisionHistoryLimit</code>) is a ready-made rollback target. <code dir=\"ltr\">undo</code> scales the previous one back up using the same rolling update rules, so rolling back is as safe as rolling forward and takes seconds. To make <code dir=\"ltr\">history</code> readable, record why each revision exists: <code dir=\"ltr\">kubectl annotate deployment/notes-api kubernetes.io/change-cause=&quot;1.5.0: bulk export&quot;</code>.</p>\n<p>After an undo, the cluster runs 1.4.0 and Git still says 1.5.0. Revert the commit straight away, or the next routine apply redeploys the bug.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Assuming rollback undoes everything</span></p>\n<p><code dir=\"ltr\">rollout undo</code> swaps Pods. It doesn’t touch your database. If 1.5.0 renamed a column, 1.4.0 comes back to a schema it doesn’t understand, and your rollback becomes a second outage. Make schema changes in expand-and-contract steps: add the new column, deploy code that writes both, backfill, switch reads, and only drop the old column a release later. Then any two adjacent versions can run against the same database, which rolling updates require anyway.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Packaging with Helm</h2>\n<p>notes-api’s <code dir=\"ltr\">k8s/</code> folder is now six files, and staging needs different replicas, resources and image tags from production. <strong>Helm</strong> packages manifests into a <strong>chart</strong>: templates with placeholders, plus a <code dir=\"ltr\">values.yaml</code> of defaults that each environment overrides.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Text</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code>charts/notes-api/\n  Chart.yaml\n  values.yaml\n  templates/\n    deployment.yaml\n    service.yaml\n    hpa.yaml\n    configmap.yaml</code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">charts/notes-api/templates/deployment.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      containers</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          resources</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">            {{- </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">toYaml .Values.resources | nindent 12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }}</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">charts/notes-api/values-prod.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  repository</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ghcr.io/skylane/notes-api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  tag</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"1.5.1\"</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">resources</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  requests</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">cpu</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">250m</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">memory</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">256Mi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  limits</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">memory</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">512Mi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span></code></pre></div>\n<p>The commands mirror what you did with kubectl:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"6\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">helm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> lint</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> charts/notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">helm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> template</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> charts/notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> charts/notes-api/values-prod.yaml</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">helm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> upgrade</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --install</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> charts/notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> charts/notes-api/values-prod.yaml</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">helm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> history</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">helm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollback</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 4</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes</span></span></code></pre></div>\n<p><code dir=\"ltr\">helm template</code> renders the YAML locally so you can review it. <code dir=\"ltr\">upgrade --install</code> installs the first time and upgrades after that, recording a numbered <strong>release revision</strong> each time. <code dir=\"ltr\">helm rollback</code> re-applies an earlier revision’s complete set of manifests, ConfigMap included, which is broader than <code dir=\"ltr\">kubectl rollout undo</code>. Helm 4, released in late 2025, renamed some flags; for example <code dir=\"ltr\">--atomic</code>, which rolls back automatically when an upgrade fails, is now <code dir=\"ltr\">--rollback-on-failure</code>.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Start with plain manifests</span></p>\n<p>Helm earns its place once you have several environments or want to consume charts others publish, such as a metrics stack. For one service in one environment, plain YAML plus <code dir=\"ltr\">kubectl diff</code> is easier to read and debug. Templates you can’t render in your head slow down every incident.</p>\n</aside>\n<p>You can now ship, watch and reverse a release with confidence. The last lesson gets traffic into the cluster properly and assembles the debugging playbook for when something still goes wrong.</p>\n","toc":[{"id":"sec-1","text":"How a rolling update works","level":2},{"id":"sec-2","text":"Deploy, watch, roll back","level":2},{"id":"sec-3","text":"Packaging with Helm","level":2}],"takeaways":["A rolling update scales a new ReplicaSet up and the old one down, paced by <code dir=\"ltr\">maxSurge</code> and <code dir=\"ltr\">maxUnavailable</code> and gated by readiness.","With <code dir=\"ltr\">maxUnavailable</code> at 0, a release whose Pods never become ready stalls harmlessly while the old version keeps serving.","<code dir=\"ltr\">kubectl rollout undo</code> switches back to the previous ReplicaSet in seconds, but update Git too or the next apply redeploys the bad version.","Rolling back code doesn’t roll back the database, so schema changes must work with both the old and the new version.","Helm packages manifests as templates plus values; <code dir=\"ltr\">helm upgrade --install</code> deploys, and <code dir=\"ltr\">helm rollback</code> returns to an earlier release revision."],"further":[{"title":"Deployments (rolling update and rollback)","url":"https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-back-a-deployment"},{"title":"Performing a Rolling Update","url":"https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/"},{"title":"kubectl rollout","url":"https://kubernetes.io/docs/reference/kubectl/generated/kubectl_rollout/"}],"quiz":[{"q":"<p>notes-api runs 4 replicas with <code dir=\"ltr\">maxSurge 1</code> and <code dir=\"ltr\">maxUnavailable 0</code>. Version 1.5.0’s Pods crash on start. What do users experience?</p>","options":[{"text":"Nothing; the rollout stalls with one failing new Pod while all 4 old Pods keep serving.","why":"Correct. With no unavailability allowed, an old Pod is only removed after a new one is ready, which never happens."},{"text":"A full outage, because the old ReplicaSet is deleted first.","why":"That describes the <code dir=\"ltr\">Recreate</code> strategy. Rolling updates keep the old ReplicaSet until the new one is ready."},{"text":"About a quarter of requests fail until someone intervenes.","why":"That could happen with <code dir=\"ltr\">maxUnavailable</code> above zero and no readiness probe. Here the failing Pod never receives traffic."},{"text":"Kubernetes rolls back to 1.4.0 automatically after the deadline.","why":"Kubernetes marks the rollout as failed after <code dir=\"ltr\">progressDeadlineSeconds</code>, but it doesn’t roll back on its own."}],"answer":0},{"q":"<p>You ran <code dir=\"ltr\">kubectl rollout undo deployment/notes-api</code> during an incident and service recovered. What must happen next?</p>","options":[{"text":"Nothing; the rollback is permanent.","why":"The cluster is fixed, but the manifest in Git still names the bad image, and the next deploy brings it back."},{"text":"Delete the old ReplicaSet so it can’t be used again.","why":"Old ReplicaSets are what make rollbacks possible. Keep them."},{"text":"Restart all Pods to clear cached state.","why":"The rollback already replaced the Pods."},{"text":"Change the image in Git (or revert the commit) so the next <code dir=\"ltr\">kubectl apply</code> doesn’t redeploy the broken version.","why":"Correct. Git and the cluster must agree. Fix the source of truth before anyone runs the next deploy."}],"answer":3},{"q":"<p>Release 1.5.0 renames the database column <code dir=\"ltr\">body</code> to <code dir=\"ltr\">content</code> in a migration. 1.5.0 has a bug and you roll back to 1.4.0. What happens?</p>","options":[{"text":"The rollback also reverts the migration automatically.","why":"Kubernetes knows nothing about your database. Only the Pods change."},{"text":"1.4.0 starts failing, because it still queries <code dir=\"ltr\">body</code>, which no longer exists.","why":"Correct. That’s why schema changes should be expand-then-contract (add new, migrate, remove old later), so adjacent versions both work."},{"text":"Postgres renames the column back when old queries arrive.","why":"Databases don’t undo schema changes in response to queries."},{"text":"Nothing breaks, because 1.4.0’s image includes its own schema.","why":"Images hold code, not database state. The schema lives in Postgres."}],"answer":1},{"q":"<p>You want to see the exact Kubernetes YAML a Helm chart will produce for production before installing it. Which command?</p>","options":[{"text":"<code dir=\"ltr\">helm history notes-api</code>","why":"That lists past releases of an installed chart, not rendered manifests."},{"text":"<code dir=\"ltr\">helm rollback notes-api 0</code>","why":"That changes the cluster; it doesn’t preview anything."},{"text":"<code dir=\"ltr\">helm template notes-api ./charts/notes-api -f values-prod.yaml</code>","why":"Correct. It renders the templates with your values locally and prints the YAML, so you can read or diff it."},{"text":"<code dir=\"ltr\">kubectl get all -n notes</code>","why":"That shows what’s already running, not what the chart would create."}],"answer":2}],"exercise":{"kind":"fill","language":"bash","template":"# Wait for the 1.5.0 rollout to finish (fails after 5 minutes)\nkubectl rollout {{0}} deployment/notes-api --timeout=5m\n\n# It's failing in production: list revisions, then go back one\nkubectl rollout {{1}} deployment/notes-api\nkubectl rollout {{2}} deployment/notes-api\n\n# On the Helm-managed staging cluster, return to release revision 4\nhelm {{3}} notes-api 4 -n notes","blanks":[{"answers":["status"]},{"answers":["history"]},{"answers":["undo"]},{"answers":["rollback"]}],"title":"Watch it, then take it back","prompt":"<p>Release 1.5.0 is rolling out. Fill in the blanks to wait for the rollout, list the Deployment’s revisions, roll back to the previous one, and roll the Helm-managed staging release back to revision 4.</p>\n","hints":["All three <code dir=\"ltr\">kubectl rollout</code> subcommands are single words: one waits, one lists, one reverses.","Helm’s command for returning to an earlier release has the same name as the concept."],"explanation":"<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollout</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> status</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> deployment/notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --timeout=5m</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollout</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> history</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> deployment/notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollout</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> undo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> deployment/notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">helm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rollback</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 4</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes</span></span></code></pre></div>\n<p><code dir=\"ltr\">status</code> blocks until the rollout finishes and exits non-zero if it fails, which makes it a good CI gate. <code dir=\"ltr\">undo</code> scales the previous ReplicaSet back up with the same rolling-update rules. <code dir=\"ltr\">helm rollback</code> re-applies all manifests from release revision 4. Afterwards, revert the change in Git so the next deploy doesn’t bring 1.5.0 back.</p>\n","checkLabels":{}},"words":722,"readMinutes":3}