<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Service · XB Field Notes</title><link>https://www.xbcatm.com/tags/service/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/service/index.xml" rel="self" type="application/rss+xml"/><item><title>Consul keeps a live map of healthy services</title><link>https://www.xbcatm.com/posts/consul-service-discovery-health/</link><pubDate>Thu, 06 Apr 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/consul-service-discovery-health/</guid><description>Service registration plus periodic health checks is how Consul answers where is a healthy instance and Who is down.</description><content:encoded>&lt;p&gt;A service registry is only as useful as its honesty. Consul keeps a catalog of registered instances and attaches health checks to them, so a caller asking where a healthy instance of a service lives gets an answer that changes as reality changes. Registration is trivial; the check is the whole product, because a registry that lists dead instances is worse than a static list — it actively routes traffic into a hole.&lt;/p&gt;
&lt;p&gt;The freshness of that answer is a function of two choices nobody revisits after the first week: how often the check runs, and what it actually tests. A check that runs every 30 seconds is blind for 30 seconds, and a check that only proves the process exists is blind forever.&lt;/p&gt;
&lt;h2 id="registration-and-two-levels-of-checks"&gt;Registration, and two levels of checks&lt;/h2&gt;
&lt;p&gt;An instance registers a name, an address, a port and one or more checks, through a config file or the registration API. Checks start in a &lt;code&gt;critical&lt;/code&gt; state by default, deliberately, so a service cannot enter the pool before its health has been verified at least once — an initial &lt;code&gt;passing&lt;/code&gt; status is something you opt into explicitly.&lt;/p&gt;
&lt;p&gt;Two levels of check sit on the same node. A node-level check such as &lt;code&gt;serfHealth&lt;/code&gt; carries an empty service name and describes the machine; a service-level check such as &lt;code&gt;service:web-1&lt;/code&gt; is bound to one registration. They fail independently, but the healthy-only filter requires every check on the node to pass, so a critical node-level check removes all of that machine&amp;rsquo;s services at once while a critical service check removes exactly one instance.&lt;/p&gt;
&lt;h2 id="what-a-caller-actually-receives"&gt;What a caller actually receives&lt;/h2&gt;
&lt;p&gt;The catalog answers two different questions. Unfiltered, &lt;code&gt;/v1/health/service/web&lt;/code&gt; returns every registration with each check&amp;rsquo;s status; with &lt;code&gt;passing=true&lt;/code&gt; it returns only the instances where all checks pass. DNS and SRV always answer the second way, so a resolver never sees a name it cannot use, and the passing set is the load-balancing pool.&lt;/p&gt;
&lt;p&gt;The catalog is also eventually consistent, and the mechanism is anti-entropy. Registration is immediate — the agent tells the leader — but repair is periodic: each agent reconciles its local state with the catalog on an interval of one minute for clusters up to 128 nodes, growing with cluster size, and each agent staggers its start so they do not all sync at once. A sync that fails is logged and retried next period, so a wrong catalog entry can survive quietly for up to one interval. The read path has its own trade: by default the agent forwards catalog reads to the leader using a leader lease that assumes leadership is briefly stable, which is fast and can occasionally return a stale result, while the &lt;code&gt;consistent&lt;/code&gt; mode makes the leader verify with a quorum first and &lt;code&gt;stale&lt;/code&gt; lets any server answer without checking.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The first failure is the interval itself. A check is only as fresh as its period, so a service that dies right after a passing probe stays in rotation until the next probe — 30 seconds for a 30-second check, 60 for a 60-second one. A TTL check is worse than a slow interval because it is passive: the instance reports in on its own schedule, so a dead instance with a 120-second TTL keeps its passing status for the full 120 seconds.&lt;/p&gt;
&lt;p&gt;The second failure is a check that answers the wrong question. A script that runs &lt;code&gt;pgrep&lt;/code&gt; against the process reports &lt;code&gt;passing&lt;/code&gt; for the entire duration of a deadlock — the process exists, the port is bound, and every request routed to it hangs. The catalog cannot tell that instance from a healthy one, so the failure is silent and total for every caller in the pool. A check that exercises a dependency, even imperfectly, converges on the truth within one interval; a liveness-only check never does. The direction of the error is what makes it expensive: a check that fails when it should pass removes a healthy instance and costs capacity, while a check that passes when it should fail keeps feeding traffic to something that cannot serve it, and the caller only finds out through its own error rate.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Compare the filtered and unfiltered views, and see what DNS hands out:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;consul catalog services
consul health service web
curl -s &amp;#39;http://127.0.0.1:8500/v1/health/service/web&amp;#39; | jq -r &amp;#39;.[] | &amp;#34;\(.Service.ID) \(.Checks[] | select(.Status!=&amp;#34;passing&amp;#34;) | .CheckID + &amp;#34;=&amp;#34; + .Status)&amp;#34;&amp;#39;
curl -s &amp;#39;http://127.0.0.1:8500/v1/health/service/web?passing=true&amp;#39; | jq -r &amp;#39;.[] | &amp;#34;\(.Service.ID) \(.Node.Address):\(.Service.Port)&amp;#34;&amp;#39;
dig +short SRV web.service.consul
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An instance that shows up unfiltered with a failing &lt;code&gt;CheckID&lt;/code&gt; must be absent from the &lt;code&gt;passing=true&lt;/code&gt; list and from the SRV answer; if it is still there, the check is not being evaluated at all. A node whose node-level check is critical drops out of both lists entirely, taking every service on that machine with it.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/consul/docs/concepts/service-discovery"&gt;Service discovery&lt;/a&gt;, the catalog, registration and the role of health checks, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/consul/docs/services/usage/checks"&gt;Health checks&lt;/a&gt;, check types, intervals and the default critical initial state, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/consul/api-docs/health"&gt;Health HTTP API&lt;/a&gt;, &lt;code&gt;/v1/health/service&lt;/code&gt; and the &lt;code&gt;passing&lt;/code&gt; filter, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/consul/docs/architecture/anti-entropy"&gt;Anti-entropy&lt;/a&gt;, the periodic sync interval and its failure behaviour, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Socket activation starts a service only when needed</title><link>https://www.xbcatm.com/posts/systemd-socket-activation-ka/</link><pubDate>Tue, 01 Feb 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/systemd-socket-activation-ka/</guid><description>A socket unit listens and wakes its service the moment a client connects, so idle daemons sit silent instead of burning cycles.</description><content:encoded>&lt;p&gt;The port is always ready while the process only starts when someone knocks, a neat trick for rare but heavy services. Every idle daemon costs something: resident memory, periodic wakeups, patch surface, log noise. Multiplied across dozens of seldom-used services on one host, the tax funds nothing. Socket activation inverts the deal — the kernel holds the listening socket, systemd owns it before any daemon exists, and the service process is born only when the first connection arrives carrying real work.&lt;/p&gt;
&lt;p&gt;The handoff is what makes it transparent. The client connects to the port as usual, unaware nobody is home; systemd starts the service and passes the already-accepted connection in, so not even the first client waits twice. Done right, on-demand startup is indistinguishable from always-on except on the resource bill — and on the first-hit latency, which is the one cost to budget honestly.&lt;/p&gt;
&lt;h2 id="the-listener-waits-first"&gt;The listener waits first&lt;/h2&gt;
&lt;p&gt;systemd opens the socket ahead of the service, and on the first connection starts the unit and hands it the accepted file descriptor. Configure the ListenStream once so the socket is born and bound before any daemon is up. The socket unit and service unit pair by name — foo.socket launches foo.service — and Accept=no (the default) hands the whole listening socket to one service instance, while Accept=yes spawns a per-connection instance for simple per-request handlers.&lt;/p&gt;
&lt;p&gt;This split also buys dependency-free boot ordering. Because the socket exists from early boot, clients can start in any order and simply block or retry until the service is up, instead of encoding fragile After= chains. Queued connections wait in the kernel backlog rather than failing, so boot races become queue depth rather than errors. The backlog size still deserves sizing: a flood of early clients against a slow-starting service overflows the same as any listen queue.&lt;/p&gt;
&lt;h2 id="service-wakes-on-demand"&gt;Service wakes on demand&lt;/h2&gt;
&lt;p&gt;An idle daemon can stay completely stopped, trading per-instance memory and startup cycles for a single handoff cost. Socket-activated services therefore sleep until the first request justifies running them. The pattern shines for administrative tools, legacy protocol gateways, and scheduled-but-rare workers — services whose idle-to-busy ratio is measured in days, not minutes.&lt;/p&gt;
&lt;p&gt;Idle exit completes the loop: the service stops itself after N idle seconds, so the system oscillates between zero cost and full capacity. The socket never closes, so the next client re-triggers startup. Without idle exit the process still accumulates — start on demand plus stop on idle is what converts the trick into steady-state savings.&lt;/p&gt;
&lt;h2 id="trade-speed-for-memory"&gt;Trade speed for memory&lt;/h2&gt;
&lt;p&gt;The price is a short warm-up gap on the first hit, which is fine for jobs that run rarely but wasteful on the always-hot path. Enable it where latency on the initial call is acceptable and memory savings actually matter. Quantify both sides before committing: measure cold-start time from connection to first response, and measure the resident set the daemon would otherwise hold forever. A 300ms start against 800MB saved is an easy yes; a 20-second JVM warmup against 50MB is a no.&lt;/p&gt;
&lt;p&gt;Some workloads cannot tolerate the gap no matter the savings. Health-check-driven orchestrators may mark the endpoint down during startup, load balancers may eject the slow first backend, and clients with tight timeouts fail before the service finishes warming. For those, keep the daemon running and spend the memory — or warm the service at boot and let activation cover only the long idle stretches afterwards.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The signature failure is the thundering start: a monitoring sweep opens connections to fifty sleeping services at once, all fifty start simultaneously, and the host OOMs or load-spikes into a brownout. Each activation was reasonable alone; together they stampede. Mitigate with staggered check intervals, per-service start limits, and idle-exit timeouts short enough that the steady-state overlap stays small. Test activation under the monitoring load, not just under one curious curl.&lt;/p&gt;
&lt;p&gt;The second failure is the half-adopted protocol: a service that binds the port itself at startup collides with the socket systemd already bound. It fails exactly when activated, the connection hangs, logs blame the address in use. Services must accept the passed file descriptor instead of binding; if upstream code cannot, a tiny shim that inherits the socket and execs the daemon bridges the gap.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Prove on-demand behavior end to end:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;systemctl status foo.socket foo.service
ss -ltnp | grep &amp;#39;:8080&amp;#39;
curl -m 10 localhost:8080/health; systemctl show foo.service -p ActiveState,ExecMainStartTimestamp
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Before the curl the service is inactive with the socket listening; after, timestamps show a fresh start triggered by the connection.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html"&gt;systemd.socket manual&lt;/a&gt;, Official socket unit reference, checked 2026-06-01.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html"&gt;systemd.service manual&lt;/a&gt;, service unit behavior including idle exit, checked 2026-09-05.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>NodePort publishes a service on every node's fixed port</title><link>https://www.xbcatm.com/posts/k8s-service-nodeport-exposes-a-fixed-port/</link><pubDate>Tue, 11 May 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/k8s-service-nodeport-exposes-a-fixed-port/</guid><description>NodePort makes a service reachable on a port that every node in the cluster listens on, a simple way to expose traffic before a load balancer arrives.</description><content:encoded>&lt;p&gt;A Service of type NodePort does one blunt thing: it reserves a port in the 30000-32767 range and makes every node in the cluster listen on it. &lt;code&gt;kube-proxy&lt;/code&gt; programs the same forwarding rule into the iptables or IPVS tables of every machine, so &lt;code&gt;curl &amp;lt;any-node-ip&amp;gt;:&amp;lt;nodePort&amp;gt;&lt;/code&gt; reaches the Service no matter which node answers. That is the whole appeal — external reachability with no cloud load balancer, no DNS record and no ingress controller.&lt;/p&gt;
&lt;p&gt;The cost is hidden inside the same design. Because every node answers, a packet frequently lands on a machine that is not running the pod, and the cluster then has to decide whose address the backend sees. &lt;code&gt;externalTrafficPolicy&lt;/code&gt; is that decision: &lt;code&gt;Cluster&lt;/code&gt; spreads load but rewrites the client address, &lt;code&gt;Local&lt;/code&gt; preserves it but drops traffic on any node without a local endpoint. Choosing one is unavoidable, and each choice breaks something different.&lt;/p&gt;
&lt;h2 id="a-port-every-node-owns"&gt;A port every node owns&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kubectl create service nodeport&lt;/code&gt; without &lt;code&gt;--node-port&lt;/code&gt; emits no &lt;code&gt;nodePort&lt;/code&gt; field at all; the API server allocates one from the range. The allocation is cluster-wide rather than per node, so a given number belongs to exactly one Service across the whole fleet. That is why two Services pinning 31000 cannot coexist, and also why a single firewall rule for one number is portable to every machine.&lt;/p&gt;
&lt;p&gt;Every node receives the rule whether or not it runs a backend. That is what makes the type useful — there is no node to avoid and no membership list for the caller to maintain — and it is also what makes it expensive, because the port is open on machines that may have no business accepting that traffic. When the endpoint set is empty the rule still exists and the connection is refused rather than silently dropped, which is at least honest.&lt;/p&gt;
&lt;h2 id="the-extra-hop-and-the-client-address"&gt;The extra hop and the client address&lt;/h2&gt;
&lt;p&gt;A request arriving on node B is forwarded to node A when that is where the pod lives, so the datapath is one leg longer than hitting node A directly. With the default &lt;code&gt;externalTrafficPolicy: Cluster&lt;/code&gt;, kube-proxy also source-NATs the packet to node B&amp;rsquo;s address, so the pod sees a cluster-internal address rather than the client. Anything that depends on the client address — rate limiting, geo rules, audit logs, an allow list — is then reading the wrong number.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;externalTrafficPolicy: Local&lt;/code&gt; keeps the original source address by forwarding only to endpoints on the node that received the packet. The price is availability: a node with no local replica has no working node port at all, so a three-node cluster running one replica answers on one node and drops on two. That is the trade — a correct client address and one less hop, in exchange for a node port that only works on some of the nodes.&lt;/p&gt;
&lt;h2 id="a-side-door-around-the-ingress"&gt;A side door around the ingress&lt;/h2&gt;
&lt;p&gt;A node port terminates nothing and inspects nothing. A request sent straight to &lt;code&gt;node:31000&lt;/code&gt; skips the load balancer, TLS termination, the WAF and every host or path routing rule that would have applied, then lands on the pod with whatever the caller chose to send. The pod is left making security decisions it was never written to make.&lt;/p&gt;
&lt;p&gt;Treat it as a stopgap with an owner and an expiry date. Reserve the range, pin a number only when a firewall or DNS record forces you to, and restrict who can reach the range at the network layer instead of assuming the ingress is the only way in.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The classic failure is the collision. A nodePort is a single cluster-wide resource, so the second Service that pins a number already in use is rejected with &lt;code&gt;provided port is already allocated&lt;/code&gt;, and a Service asking for an automatic port can never get one once the 2768-port range is full. Both present as a Service that exists but has no usable external port, which reads like a networking fault and is really a capacity one.&lt;/p&gt;
&lt;p&gt;The second failure is quieter. A node port is a real socket bind on every host, so anything else already listening on that number makes it dead on that machine while &lt;code&gt;kubectl get svc&lt;/code&gt; still reports a healthy Service — health checks, dashboards and the Service object all lie together. Because the same port bypasses the ingress layer, the rule that makes debugging easy also makes the cluster reachable in a way nobody is filtering. The blast radius is every node in the fleet, not the one you were looking at.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Confirm the port, the endpoints, and who else owns the port:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get svc web -o jsonpath=&amp;#39;{.spec.type} {.spec.ports[0].nodePort}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get endpoints web
kubectl get svc -A -o json | jq -r &amp;#39;.items[] | select(.spec.type==&amp;#34;NodePort&amp;#34;) | &amp;#34;\(.spec.ports[0].nodePort) \(.metadata.namespace)/\(.metadata.name)&amp;#34;&amp;#39; | sort -n | uniq -D -w5
ss -lnt | grep -E &amp;#39;:3[0-9]{4}\b&amp;#39;
curl -sS -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; &amp;#34;http://$(hostname -I | awk &amp;#39;{print $1}&amp;#39;):$(kubectl get svc web -o jsonpath=&amp;#39;{.spec.ports[0].nodePort}&amp;#39;)/&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The third line prints any nodePort claimed twice, and the fourth shows whether a process other than kube-proxy already owns that port on this host. The curl should print 200 from every node; a refusal on exactly one node means something local took the port, and a refusal everywhere means the Service has no ready endpoints.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/"&gt;Kubernetes Service&lt;/a&gt;, NodePort type, the 30000-32767 default range and cluster-wide port allocation, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/reference/networking/virtual-ips/"&gt;Virtual IPs and Service Proxies&lt;/a&gt;, kube-proxy iptables/IPVS rule programming and externalTrafficPolicy, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tutorials/services/source-ip/"&gt;Using Source IP&lt;/a&gt;, SNAT on NodePort and the client-IP consequence, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A Service gives a pod a stable virtual address</title><link>https://www.xbcatm.com/posts/k8s-service-stable-virtual-address/</link><pubDate>Tue, 09 Feb 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/k8s-service-stable-virtual-address/</guid><description>A Kubernetes Service names a label selector, and the cluster hands it a virtual cluster IP plus DNS, so clients keep talking as pods come and go.</description><content:encoded>&lt;p&gt;The selector turns the Service from a fixed Endpoint into a live view: refresh happens in the background, and a client never edits its config when pods restart.&lt;/p&gt;
&lt;h2 id="declared-then-given-an-address"&gt;Declared, then given an address&lt;/h2&gt;
&lt;p&gt;A Service turns the constant churn of pods into a stable name. You declare a selector (which pods), a port, and a protocol, and the control plane assigns a virtual IP (cluster IP) and a DNS name that stay fixed while everything behind them keeps changing. The stable name is the contract for every other resource: a Deployment, an Ingress, or another pod can address a service by name without caring which concrete pod exists right now. That indirection is what lets the runtime rearrange the membership without a client ever noticing.&lt;/p&gt;
&lt;h2 id="selector-is-the-live-membership"&gt;Selector is the live membership&lt;/h2&gt;
&lt;p&gt;The backend list a Service routes to is not a fixed set, it is rebuilt continuously from the selector the moment pods match or stop matching it. A pod that fails its readiness probe is dropped from the endpoints; a new pod that passes joins; the update happens in the background. Because membership is derived, you never hand-edit the list and you never drift: as long as the selector and the pod labels line up, the Service automatically reflects the current, healthy backends. That readme is what makes a Service ahead of static lists you would have to maintain by hand.&lt;/p&gt;
&lt;h2 id="stable-even-as-pods-churn"&gt;Stable even as pods churn&lt;/h2&gt;
&lt;p&gt;Clients talk only to the virtual address, never to an individual pod IP, which is exactly what decouples the caller from the fleet. You can scale up, scale down, replace, or kill a pod and the Service keeps answering through the survivors; the client sees no change because its target never changes. That stability is the whole reason Services allow rolling restarts and horizontal scaling without a coordinated announcement. Keep the selector precise and probe-gated, and the Service gives you a stable facade over an inherently unstable set of workers.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The pool is finite, and exhaustion is the most literal failure. Every ordinary Service takes one address from &lt;code&gt;--service-cluster-ip-range&lt;/code&gt;, so a small CIDR or leftover Services eventually reject creation with an allocation error while the pods look fine. Treat &lt;code&gt;failed to allocate IP&lt;/code&gt; as capacity accounting: delete orphaned Services before widening the range, or widening only postpones the outage.&lt;/p&gt;
&lt;p&gt;Mixing headless and ordinary Services is the next confusion. Setting &lt;code&gt;clusterIP: None&lt;/code&gt; allocates no virtual address, so DNS returns pod IPs instead of one stable VIP, and a client written for a single endpoint must suddenly balance across shifting addresses. The reverse also hurts: discovery code expecting per-pod DNS keeps hitting one VIP after someone recreates the Service with a ClusterIP. Because &lt;code&gt;clusterIP&lt;/code&gt; is effectively immutable, switching modes means recreating the Service.&lt;/p&gt;
&lt;p&gt;The last two failures live below the API. Iptables and IPVS modes program the same VIP differently, and a stale or restarting kube-proxy can leave an allocated ClusterIP resolving but blackholing traffic until rules resync. Finally, copying a ClusterIP into configs freezes a value the control plane may reassign: the address is stable only while the Service object lives. Callers must use the DNS name, and remember the VIP routes only inside the cluster.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save the following as &lt;code&gt;clusterip_pool.py&lt;/code&gt; in a scratch directory and run &lt;code&gt;python3 clusterip_pool.py&lt;/code&gt;. It models a miniature allocator over &lt;code&gt;10.96.0.0/29&lt;/code&gt; with the first address reserved, using only the standard library and changing nothing outside the process: one class hands out addresses, refuses duplicates and out-of-range requests, lets headless Services bypass the pool, and reuses released addresses, with five scenarios asserting each rule.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Simulate ClusterIP allocation from a small service CIDR.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;ipaddress&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClusterIPPool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Hand out virtual addresses; headless Services bypass the pool.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_reserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_reserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; already holds &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_reserved&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is outside the allocatable range&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is already taken&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;service CIDR exhausted: no ClusterIP left&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; holds nothing to release&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ClusterIPPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10.96.0.0/29&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;web&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;web&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;api&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10.96.0.5&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sequential Services must get distinct addresses&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;allocated:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;worker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10.96.0.5&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;conflict was silently shared&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;conflict rejected:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;evil&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10.99.0.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;out-of-range request was accepted&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;out-of-range rejected:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;headless&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;db-headless&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;headless Service must bypass the pool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;headless must not consume the pool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;headless bypass: None&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;freed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;web&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;worker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;freed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;released address must be reusable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;reused:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-&amp;gt; worker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;extra-1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;extra-2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;extra-3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;overflow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;exhaustion was not reported&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;exhaustion reported:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PASS: allocation, conflict rejection, release reuse, and exhaustion hold&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The run behaves like a healthy allocator: sequential Services get distinct addresses, a pinned request holds its address, and the deliberate double allocation is rejected instead of shared. Releasing one Service makes its address reusable, and filling the last slot proves exhaustion errors instead of recycling a live address. All assertions passed with Python 3.14.4 on macOS.&lt;/p&gt;
&lt;p&gt;The simulation checks bookkeeping only. It does not run the API server, etcd, or kube-proxy, and its tiny &lt;code&gt;/29&lt;/code&gt; stands in for a real service CIDR. Before deploying, inspect the live service CIDR, allocated ClusterIPs, and proxy rules on a node.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/posts/kubernetes-services-give-stable-homes/"&gt;Kubernetes Services give pods a stable home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/posts/k8s-service-nodeport-exposes-a-fixed-port/"&gt;NodePort publishes a service on every node&amp;rsquo;s fixed port&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/"&gt;Kubernetes Service&lt;/a&gt;, documents service semantics, checked 2026-06-01.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/reference/networking/virtual-ips/"&gt;Virtual IPs and Service Proxies&lt;/a&gt;, documents ClusterIP allocation and kube-proxy modes, checked 2026-06-01.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>