<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Service-Discovery | Ronak Nathani</title><link>https://ronaknathani.com/tag/service-discovery/</link><atom:link href="https://ronaknathani.com/tag/service-discovery/index.xml" rel="self" type="application/rss+xml"/><description>Service-Discovery</description><generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>en-us</language><copyright>© 2026 Ronak Nathani</copyright><lastBuildDate>Tue, 07 Jul 2026 11:00:00 -0400</lastBuildDate><image><url>https://ronaknathani.com/img/avatar.jpg</url><title>Service-Discovery</title><link>https://ronaknathani.com/tag/service-discovery/</link></image><item><title>A Sync Loop Is Not a Handshake: Keeping External Systems in Lockstep with Kubernetes</title><link>https://ronaknathani.com/blog/2026/07/keeping-external-systems-in-lockstep-with-kubernetes/</link><pubDate>Tue, 07 Jul 2026 11:00:00 -0400</pubDate><guid>https://ronaknathani.com/blog/2026/07/keeping-external-systems-in-lockstep-with-kubernetes/</guid><description>&lt;p&gt;&amp;ldquo;How do you keep an external system in lockstep with what Kubernetes knows?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Kubernetes is the source of truth for a lot of state. Which pods exist, which ones are ready to serve traffic, which ones are going away. Plenty of systems outside the cluster need that same state. A service discovery registry is the obvious one. It needs to know the exact set of endpoints that can take traffic right now.&lt;/p&gt;
&lt;p&gt;The usual answer is a controller. Watch the objects you care about, push the changes to the external system, reconcile on drift. It works most of the time. The problem is the &lt;strong&gt;&amp;ldquo;most of the time&amp;rdquo;&lt;/strong&gt; part. A plain sync loop is best-effort and eventually consistent. When it falls behind, or the thing it writes to is down, Kubernetes keeps moving and the external system is left behind.&lt;/p&gt;
&lt;p&gt;This post is about closing that gap. I&amp;rsquo;ll use service discovery as the running example because it makes the failure concrete. But the real subject is two coordination primitives Kubernetes gives you,
&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-readiness-gate" target="_blank" rel="noopener"&gt;readiness gates&lt;/a&gt; and
&lt;a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/" target="_blank" rel="noopener"&gt;finalizers&lt;/a&gt;, and how they let an external system participate in the pod lifecycle instead of chasing it.&lt;/p&gt;
&lt;h2 id="the-baseline-mirror-endpointslices"&gt;The baseline: mirror EndpointSlices&lt;/h2&gt;
&lt;p&gt;Kubernetes has a
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/" target="_blank" rel="noopener"&gt;&lt;code&gt;Service&lt;/code&gt;&lt;/a&gt;, which is an abstraction over a group of pods selected by a label. A controller in the control plane watches Pods and Services and maintains
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/" target="_blank" rel="noopener"&gt;&lt;code&gt;EndpointSlice&lt;/code&gt;&lt;/a&gt; objects for each Service. An EndpointSlice is a list of endpoints backing the Service: IP, port, pod name, and readiness and termination state. It&amp;rsquo;s updated as pods come and go.&lt;/p&gt;
&lt;p&gt;Say you have a service discovery system that lives outside the cluster, and clients ask it for the endpoints of a given app. You need those endpoints to match reality. The Kubernetes-native way to feed it is a controller that watches EndpointSlices and pushes the endpoints into the external registry.&lt;/p&gt;
&lt;div class="sd-baseline"&gt;
&lt;style&gt;
.sd-baseline { margin: 1.7em auto; max-width: 720px; }
.sd-baseline svg { width: 100%; height: auto; display: block; }
&lt;/style&gt;
&lt;svg viewBox="0 0 760 150" xmlns="http://www.w3.org/2000/svg" role="img"
aria-label="The Kubernetes-native baseline: a sync controller watches pods and EndpointSlices from the API server and writes endpoints to the external registry."&gt;
&lt;defs&gt;
&lt;filter id="sb-shadow" x="-2%" y="-2%" width="104%" height="104%"&gt;
&lt;feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.08"/&gt;
&lt;/filter&gt;
&lt;marker id="sb-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"&gt;
&lt;path d="M0 0L8 4L0 8Z" fill="#475569"/&gt;
&lt;/marker&gt;
&lt;/defs&gt;
&lt;rect x="12" y="54" width="176" height="76" rx="13" fill="#fbfcfe" stroke="#e2e8f0" stroke-width="1.5" filter="url(#sb-shadow)"/&gt;
&lt;text x="100" y="97" text-anchor="middle" font-size="15.5" font-weight="650" fill="#0a2540" font-family="system-ui, -apple-system, sans-serif"&gt;k8s API server&lt;/text&gt;
&lt;rect x="292" y="54" width="176" height="76" rx="13" fill="#f6f5ff" stroke="#d7d5ff" stroke-width="1.5" filter="url(#sb-shadow)"/&gt;
&lt;text x="380" y="97" text-anchor="middle" font-size="15.5" font-weight="650" fill="#4c46d6" font-family="system-ui, -apple-system, sans-serif"&gt;sync controller&lt;/text&gt;
&lt;rect x="572" y="54" width="176" height="76" rx="13" fill="#fbfcfe" stroke="#e2e8f0" stroke-width="1.5" filter="url(#sb-shadow)"/&gt;
&lt;text x="660" y="97" text-anchor="middle" font-size="15.5" font-weight="650" fill="#0a2540" font-family="system-ui, -apple-system, sans-serif"&gt;external registry&lt;/text&gt;
&lt;line x1="192" y1="92" x2="286" y2="92" stroke="#475569" stroke-width="2" marker-end="url(#sb-arrow)"/&gt;
&lt;text x="239" y="30" text-anchor="middle" font-size="12.5" fill="#5d6b82" font-family="system-ui, -apple-system, sans-serif"&gt;watches&lt;/text&gt;
&lt;text x="239" y="46" text-anchor="middle" font-size="12.5" fill="#5d6b82" font-family="system-ui, -apple-system, sans-serif"&gt;EndpointSlices&lt;/text&gt;
&lt;line x1="472" y1="92" x2="566" y2="92" stroke="#475569" stroke-width="2" marker-end="url(#sb-arrow)"/&gt;
&lt;text x="519" y="30" text-anchor="middle" font-size="12.5" fill="#5d6b82" font-family="system-ui, -apple-system, sans-serif"&gt;writes endpoints&lt;/text&gt;
&lt;text x="519" y="46" text-anchor="middle" font-size="12.5" fill="#5d6b82" font-family="system-ui, -apple-system, sans-serif"&gt;to the registry&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;p&gt;EndpointSlices are the right primitive here when the rollout controller shares fate with the EndpointSlice controller. In vanilla Kubernetes they do: the Deployment controller and the EndpointSlice controller both live in the kube-controller-manager, the same binary. If it&amp;rsquo;s down, nothing rolls out and nothing updates endpoints, so a rollout can&amp;rsquo;t march ahead while endpoint updates quietly fall behind. They fail together. The trouble starts when responsibility is split: one system drives the rollout, and a separate, independent component owns the service discovery endpoints. Now the two can fail on their own, and a rollout can outrun the endpoints.&lt;/p&gt;
&lt;h2 id="where-the-gap-is"&gt;Where the gap is&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s walk through a rollout. Assume the sync controller is unhealthy at the wrong moment, or the external registry is refusing writes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A rollout starts. New pods come up. Their containers pass their readiness probes and are ready to serve, so kubelet marks the pods &lt;code&gt;Ready&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The controller managing the pods sees ready pods and makes progress: it deletes old pods and creates more new ones.&lt;/li&gt;
&lt;li&gt;The rollout finishes. From Kubernetes&amp;rsquo; view, everything is healthy. Desired state reached.&lt;/li&gt;
&lt;li&gt;The sync controller was down the whole time (or the registry wasn&amp;rsquo;t accepting writes), so the external registry never learned about the new endpoints. It still holds the endpoints for pods that no longer exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now the deploy is &amp;ldquo;done&amp;rdquo; and there&amp;rsquo;s nothing for clients to reach. Traffic is black-holed against a rollout that Kubernetes considers a success. Nobody gets paged for a failed deployment, because it didn&amp;rsquo;t fail. The rollout succeeded, and the registry has stale data.&lt;/p&gt;
&lt;p&gt;The root cause is that &lt;strong&gt;pod readiness and registration are decoupled&lt;/strong&gt;. The pod was marked &lt;code&gt;Ready&lt;/code&gt; and the rollout moved forward with no acknowledgement that its endpoint had been written to the external registry. Pod readiness says &amp;ldquo;the container can serve traffic.&amp;rdquo; It says nothing about whether the system that routes traffic to it has that endpoint yet.&lt;/p&gt;
&lt;p&gt;So the fix isn&amp;rsquo;t a more reliable sync loop. A sync loop can and will fall behind. The fix is to make readiness depend on registration, so a rollout can&amp;rsquo;t move forward until the external registry acknowledges it received the endpoint of the new healthy pod.&lt;/p&gt;
&lt;h2 id="the-fix"&gt;The fix&lt;/h2&gt;
&lt;p&gt;Concretely, that&amp;rsquo;s a controller that watches pods instead of EndpointSlices. It uses a readiness gate and a finalizer to participate in the pod lifecycle, so the external registry stays in lockstep with Kubernetes.&lt;/p&gt;
&lt;div class="sd-baseline"&gt;
&lt;style&gt;
.sd-baseline { margin: 1.7em auto; max-width: 760px; }
.sd-baseline svg { width: 100%; height: auto; display: block; }
&lt;/style&gt;
&lt;svg viewBox="0 0 820 260" xmlns="http://www.w3.org/2000/svg" role="img"
aria-label="The controller watches pods, clears the readiness gate and registers the endpoint on the way up (green), and de-registers the endpoint and removes the finalizer on the way down (amber)."&gt;
&lt;defs&gt;
&lt;filter id="sfd-shadow" x="-2%" y="-2%" width="104%" height="104%"&gt;
&lt;feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.08"/&gt;
&lt;/filter&gt;
&lt;marker id="sfd-slate" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"&gt;&lt;path d="M0 0L8 4L0 8Z" fill="#475569"/&gt;&lt;/marker&gt;
&lt;marker id="sfd-green" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"&gt;&lt;path d="M0 0L8 4L0 8Z" fill="#15b67a"/&gt;&lt;/marker&gt;
&lt;marker id="sfd-amber" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"&gt;&lt;path d="M0 0L8 4L0 8Z" fill="#e8910a"/&gt;&lt;/marker&gt;
&lt;/defs&gt;
&lt;rect x="16" y="30" width="150" height="215" rx="13" fill="#fbfcfe" stroke="#e2e8f0" stroke-width="1.5" filter="url(#sfd-shadow)"/&gt;
&lt;text x="91" y="143" text-anchor="middle" font-size="15" font-weight="650" fill="#0a2540" font-family="system-ui, -apple-system, sans-serif"&gt;k8s API server&lt;/text&gt;
&lt;rect x="335" y="30" width="150" height="215" rx="13" fill="#f6f5ff" stroke="#d7d5ff" stroke-width="1.5" filter="url(#sfd-shadow)"/&gt;
&lt;text x="410" y="143" text-anchor="middle" font-size="15" font-weight="650" fill="#4c46d6" font-family="system-ui, -apple-system, sans-serif"&gt;sync controller&lt;/text&gt;
&lt;rect x="654" y="30" width="150" height="215" rx="13" fill="#fbfcfe" stroke="#e2e8f0" stroke-width="1.5" filter="url(#sfd-shadow)"/&gt;
&lt;text x="729" y="143" text-anchor="middle" font-size="15" font-weight="650" fill="#0a2540" font-family="system-ui, -apple-system, sans-serif"&gt;external registry&lt;/text&gt;
&lt;line x1="335" y1="64" x2="172" y2="64" stroke="#475569" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#sfd-slate)"/&gt;
&lt;text x="250" y="50" text-anchor="middle" font-size="12.5" fill="#5d6b82" font-family="system-ui, -apple-system, sans-serif"&gt;watches pods&lt;/text&gt;
&lt;line x1="335" y1="112" x2="172" y2="112" stroke="#15b67a" stroke-width="2" marker-end="url(#sfd-green)"/&gt;
&lt;text x="250" y="88" text-anchor="middle" font-size="12.5" fill="#0f8a5b" font-family="system-ui, -apple-system, sans-serif"&gt;clears readiness gate&lt;/text&gt;
&lt;text x="250" y="104" text-anchor="middle" font-size="12.5" fill="#0f8a5b" font-family="system-ui, -apple-system, sans-serif"&gt;after registration&lt;/text&gt;
&lt;line x1="485" y1="112" x2="648" y2="112" stroke="#15b67a" stroke-width="2" marker-end="url(#sfd-green)"/&gt;
&lt;text x="570" y="88" text-anchor="middle" font-size="12.5" fill="#0f8a5b" font-family="system-ui, -apple-system, sans-serif"&gt;registers endpoint&lt;/text&gt;
&lt;text x="570" y="104" text-anchor="middle" font-size="12.5" fill="#0f8a5b" font-family="system-ui, -apple-system, sans-serif"&gt;once containers ready&lt;/text&gt;
&lt;line x1="485" y1="210" x2="648" y2="210" stroke="#e8910a" stroke-width="2" marker-end="url(#sfd-amber)"/&gt;
&lt;text x="570" y="186" text-anchor="middle" font-size="12.5" fill="#b3700a" font-family="system-ui, -apple-system, sans-serif"&gt;de-registers endpoint&lt;/text&gt;
&lt;text x="570" y="202" text-anchor="middle" font-size="12.5" fill="#b3700a" font-family="system-ui, -apple-system, sans-serif"&gt;once containers stop&lt;/text&gt;
&lt;line x1="335" y1="210" x2="172" y2="210" stroke="#e8910a" stroke-width="2" marker-end="url(#sfd-amber)"/&gt;
&lt;text x="250" y="186" text-anchor="middle" font-size="12.5" fill="#b3700a" font-family="system-ui, -apple-system, sans-serif"&gt;removes finalizer&lt;/text&gt;
&lt;text x="250" y="202" text-anchor="middle" font-size="12.5" fill="#b3700a" font-family="system-ui, -apple-system, sans-serif"&gt;after de-registration&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;h3 id="gate-ready-on-registration-with-a-readiness-gate"&gt;Gate &amp;ldquo;Ready&amp;rdquo; on registration with a readiness gate&lt;/h3&gt;
&lt;p&gt;A container has a
&lt;a href="https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/#readiness-probe" target="_blank" rel="noopener"&gt;&lt;code&gt;readinessProbe&lt;/code&gt;&lt;/a&gt;, and kubelet uses it to set the pod&amp;rsquo;s &lt;code&gt;ContainersReady&lt;/code&gt; condition. Normally the pod&amp;rsquo;s &lt;code&gt;Ready&lt;/code&gt; condition just follows &lt;code&gt;ContainersReady&lt;/code&gt;. A
&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-readiness-gate" target="_blank" rel="noopener"&gt;pod readiness gate&lt;/a&gt; changes that rule. When a pod declares a readiness gate, the pod is &lt;code&gt;Ready&lt;/code&gt; only when every container is ready &lt;strong&gt;and&lt;/strong&gt; every readiness gate condition is &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The gate is a custom condition that you own. You declare it in the pod spec, and flip it to &lt;code&gt;True&lt;/code&gt; in the pod&amp;rsquo;s status when the desired condition is met:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: v1
kind: Pod
metadata:
name: web-1
spec:
readinessGates:
- conditionType: discovery.example.com/registered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our service discovery example, the same sync controller clears the readiness gate. Instead of watching EndpointSlices, it watches pods. When a pod&amp;rsquo;s &lt;code&gt;ContainersReady&lt;/code&gt; condition goes &lt;code&gt;True&lt;/code&gt;, it registers that pod&amp;rsquo;s endpoint with the external system. Only once registration succeeds does it set the gate condition to &lt;code&gt;True&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;status:
conditions:
- type: ContainersReady
status: &amp;quot;True&amp;quot;
- type: discovery.example.com/registered
status: &amp;quot;True&amp;quot;
- type: Ready
status: &amp;quot;True&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pod is &lt;code&gt;Ready&lt;/code&gt; only after all three line up. The &lt;code&gt;Ready&lt;/code&gt; condition now carries an extra guarantee: the external system has accepted this endpoint.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see what that does to the failure above. If the controller is down, or the registry is refusing writes, the gate condition stays &lt;code&gt;False&lt;/code&gt;, so the new pods never become &lt;code&gt;Ready&lt;/code&gt;. The rollout can&amp;rsquo;t count them as &lt;code&gt;Available&lt;/code&gt;, so it can&amp;rsquo;t complete. It may still take down a few old pods up to &lt;code&gt;maxUnavailable&lt;/code&gt;, but it stalls there and eventually trips its progress deadline (&lt;code&gt;ProgressDeadlineExceeded&lt;/code&gt;) and is marked failed. That&amp;rsquo;s the outcome we want. A deploy that can&amp;rsquo;t register its endpoints should fail loudly instead of quietly reporting success. The readiness gate turns &amp;ldquo;the external system is unavailable&amp;rdquo; into back-pressure on the rollout.&lt;/p&gt;
&lt;p&gt;The
&lt;a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/pod_readiness_gate/" target="_blank" rel="noopener"&gt;AWS Load Balancer Controller&lt;/a&gt; does exactly this: it injects a pod readiness gate so a pod isn&amp;rsquo;t marked &lt;code&gt;Ready&lt;/code&gt; until its target is registered and healthy in the ALB or NLB target group. That keeps a rolling update from tearing down the old pods before the new ones can actually take traffic. Same primitive, pointed at a different external system.&lt;/p&gt;
&lt;h3 id="gate-deleted-on-cleanup-with-a-finalizer"&gt;Gate &amp;ldquo;Deleted&amp;rdquo; on cleanup with a finalizer&lt;/h3&gt;
&lt;p&gt;Registration is only half the story. The other half is deletion, and it has the same decoupling problem in reverse.&lt;/p&gt;
&lt;p&gt;When you delete a pod, Kubernetes doesn&amp;rsquo;t remove it right away. The API server sets &lt;code&gt;metadata.deletionTimestamp&lt;/code&gt; to a time in the future: the moment you asked, plus the pod&amp;rsquo;s grace period. That timestamp is a deadline, not a tombstone. It can be shortened if the pod drains early, but never pushed further out. The pod moves to &lt;code&gt;Terminating&lt;/code&gt;, and kubelet runs graceful shutdown: any &lt;code&gt;preStop&lt;/code&gt; hook, then &lt;code&gt;SIGTERM&lt;/code&gt;, then &lt;code&gt;SIGKILL&lt;/code&gt; if the container outlives the grace period. Kubelet stops the containers, but it doesn&amp;rsquo;t delete the pod object. Once the containers are gone, it sends the API server a final delete, and the API server is what removes the object from the datastore.&lt;/p&gt;
&lt;p&gt;Without coordination, here&amp;rsquo;s the race. The pod object can be gone from the API before the sync controller manages to clean up the external registry. If the controller or the registry is down at that moment, the pod disappears and a stale endpoint is left behind. Now we need an out-of-band garbage collector to find and clean up orphaned records, which is its own source of bugs.&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/" target="_blank" rel="noopener"&gt;Finalizers&lt;/a&gt; help close this gap. A finalizer is a key on an object that blocks its removal until the key is taken off. As long as the finalizers list is non-empty, deletion is blocked. While the finalizer is present, the pod object stays around with its &lt;code&gt;deletionTimestamp&lt;/code&gt; set, and the API server won&amp;rsquo;t remove it.&lt;/p&gt;
&lt;p&gt;The finalizer only holds the pod &lt;em&gt;object&lt;/em&gt; in the API. It doesn&amp;rsquo;t keep the containers running: kubelet still drains and kills them on the grace period, finalizer or not. What it buys you is a guarantee that the object sticks around long enough for the de-registration to run.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;apiVersion: v1
kind: Pod
metadata:
name: web-1
finalizers:
- discovery.example.com/deregistered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both the finalizer and the readiness gate have to be on the pod from the start. So whatever creates the pods needs to put both in place up front, either in the pod template it stamps out or through a mutating admission webhook that injects them.&lt;/p&gt;
&lt;p&gt;Now the controller has two jobs across the teardown, and the order matters:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When the &lt;code&gt;deletionTimestamp&lt;/code&gt; first appears, stop the registry from handing this endpoint to new clients. Exactly how depends on the system: a health-aware registry can mark it unhealthy or draining, another one may only support removing it. The point is to cut off new traffic without pulling the pod out from under its in-flight requests.&lt;/li&gt;
&lt;li&gt;Wait until the pod has actually drained, then remove the endpoint for good and take the finalizer off.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For step 2 you need a signal that the containers are gone. For a normal pod, that&amp;rsquo;s every entry in &lt;code&gt;status.containerStatuses&lt;/code&gt; sitting in a &lt;code&gt;terminated&lt;/code&gt; state. There are edge cases: init containers and restartable sidecars show up there too, and a pod that never started or whose node went unreachable may never report &lt;code&gt;terminated&lt;/code&gt;. The controller needs a fallback for those, a timeout or a force path, or the finalizer will keep the pod object in the API forever.&lt;/p&gt;
&lt;p&gt;The finalizer is what makes step 2 safe. The object can&amp;rsquo;t be reaped until de-registration has succeeded, so a failed de-registration or an unhealthy controller holds the pod instead of leaking a record. The cleanup happens before Kubernetes removes the object, so you don&amp;rsquo;t need a separate job chasing leaks.&lt;/p&gt;
&lt;p&gt;The two gates bracket the pod&amp;rsquo;s life. A new pod can&amp;rsquo;t take traffic until the registry has it, and a finished pod can&amp;rsquo;t be reaped until the registry has let it go. The rollout waits on the registry at the front, cleanup waits on it at the back.&lt;/p&gt;
&lt;div class="sd-lifecycle-embed"&gt;
&lt;style&gt;
.sd-lifecycle-embed { margin: 1.6em 0; }
.sd-lifecycle-embed iframe { width:100%; height:516px; border:0; display:block; }
@media (max-width: 560px) { .sd-lifecycle-embed iframe { height:660px; } }
&lt;/style&gt;
&lt;iframe src="https://ronaknathani.com/sd-lifecycle.html?v=6" title="Step-by-step animation of how a readiness gate and a finalizer coordinate an external service discovery registry across the pod lifecycle, with the controller writing to the pod and the registry" loading="lazy" scrolling="no"&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2 id="the-tradeoff-rollouts-now-depend-on-the-registry"&gt;The tradeoff: rollouts now depend on the registry&lt;/h2&gt;
&lt;p&gt;None of this is free. A plain sync loop is best-effort, but it never blocks a deploy. With readiness gates and finalizers, both rollouts and teardown take a hard dependency on the controller and the external registry. If either is unhealthy, rollouts stall and pods pile up in &lt;code&gt;Terminating&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For service discovery that&amp;rsquo;s the desired failure. A pod that can&amp;rsquo;t be registered shouldn&amp;rsquo;t take traffic, and a pod that can&amp;rsquo;t be de-registered shouldn&amp;rsquo;t vanish and leave a dangling endpoint. So reach for this when a stale record actually causes harm, not by default.&lt;/p&gt;
&lt;p&gt;A couple of caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Readiness can flap after registration.&lt;/strong&gt; If a running pod&amp;rsquo;s &lt;code&gt;ContainersReady&lt;/code&gt; goes back to &lt;code&gt;False&lt;/code&gt;, the controller should mark that endpoint unhealthy in the registry, the same move as the start of a graceful teardown, so clients stop routing to it. If the registry distinguishes health and registration, don&amp;rsquo;t de-register it: the pod may recover, and you want it back without churn. If the controller or the registry is unhealthy at that instant, the registry keeps advertising a stale endpoint until things recover, so clients still need to tolerate an endpoint that errors and retry another one. Coordination makes the system resilient but doesn&amp;rsquo;t remove the need for clients to be defensive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finalizers can wedge deletion.&lt;/strong&gt; The flip side of &amp;ldquo;the pod can&amp;rsquo;t disappear until cleanup succeeds&amp;rdquo; is that a broken controller can leave pods stuck in terminating. That&amp;rsquo;s a deliberate choice: a stuck delete you can see and alert on beats a silent leak you can&amp;rsquo;t. But it means the controller that owns the finalizer is now on the critical path for teardown, and you should treat it that way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watching pods is heavier than watching EndpointSlices.&lt;/strong&gt; EndpointSlices aggregate up to 100 endpoints per object, so the baseline controller watched relatively few of them. This one watches pods, one object each, and pods churn on every status update, not just readiness changes. The informer cache is much larger and the event stream much busier. On a large fleet, that&amp;rsquo;s real load to size for.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-pattern"&gt;The pattern&lt;/h2&gt;
&lt;p&gt;Service discovery is just the example. The pattern generalizes to any external system that has to stay in lockstep with Kubernetes data.&lt;/p&gt;
&lt;p&gt;A sync loop is a one-way push. It sends updates and hopes they land, and Kubernetes never waits for an answer. A readiness gate and a finalizer turn that push into a handshake: a pod isn&amp;rsquo;t &lt;code&gt;Ready&lt;/code&gt; until registration succeeds, and it isn&amp;rsquo;t deleted until de-registration does. Kubernetes waits for the acknowledgement. That&amp;rsquo;s the difference between eventually consistent and in lockstep.&lt;/p&gt;</description></item></channel></rss>