<?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>Explainer on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/explainer/</link>
    <description>Recent content in Explainer on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 16 Sep 2026 08:30:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/explainer/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>What&#39;s a VPC? Let Pac-Man explain</title>
      <link>https://thenestedlab.com/posts/whats-a-vpc-with-pacman/</link>
      <pubDate>Wed, 16 Sep 2026 08:30:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/whats-a-vpc-with-pacman/</guid>
      <description>Part 0 of the Pod Papers: an NSX VPC explained with a running game. Private by default, one deliberate door out — and a self-inflicted outage that taught me five green layers can hide one wrong integer.</description>
      <content:encoded><![CDATA[<p>Before this series gets into trunk subnets and binding maps, it&rsquo;s worth
spending ten minutes on the thing everything else stands on: <strong>what an NSX
VPC actually is</strong> from the tenant&rsquo;s chair. No slides. A game of Pac-Man.</p>
<p>The lab has Pac-Man running twice on VCF 9.1 — once on a VKS cluster I
built by hand with <code>kubectl</code>, once on a cluster deployed through VCF
Automation&rsquo;s catalog. Both live inside VPCs. Both were reachable when I
started:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">http://192.168.144.15/  -&gt;  &lt;title&gt;Pacman in HTML 5 Canvas
</span></span><span class="line"><span class="cl">http://192.168.144.23/  -&gt;  &lt;title&gt;Pacman in HTML 5 Canvas
</span></span></code></pre></div><figure class="nl-video">
  <video autoplay loop muted playsinline controls preload="metadata" style="aspect-ratio:710 / 610">
    <source src="/images/pacman-vip-23.mp4" type="video/mp4">
  </video>
  <figcaption>Pac-Man, live at <code>192.168.144.23</code> — a VIP on the VPC load balancer. The only door in.</figcaption>
</figure>

<h2 id="a-vpc-is-a-private-universe-with-a-door-policy">A VPC is a private universe with a door policy</h2>
<p>Think of an NSX VPC as a tenant&rsquo;s own routed network space: its own
subnets, its own gateway, its own address plan — carved out by the tenant,
not filed as a ticket with the network team. Three rules define it:</p>
<ol>
<li><strong>Private by default.</strong> A <code>Private</code> subnet is reachable only from inside
the same VPC. Nobody outside can route to it — not other tenants, not
other VPCs in the same org, not the corporate network.</li>
<li><strong>Your addresses are your business.</strong> Because private subnets aren&rsquo;t
advertised anywhere, two VPCs can use <em>identical</em> CIDRs. (This is the
superpower the rest of the series is built on.)</li>
<li><strong>Every door out is deliberate.</strong> Traffic leaves via the transit
gateway — SNAT&rsquo;d — or arrives via a <strong>LoadBalancer VIP</strong> from an
external block the provider allocated. Nothing is exposed by accident.</li>
</ol>
<p>Pac-Man&rsquo;s pods sit on a private subnet. The only reason <code>192.168.144.23</code>
answers is a Kubernetes <code>Service</code> of type <code>LoadBalancer</code>, which NSX turns
into a VIP on the VPC&rsquo;s load balancer. So let&rsquo;s remove the door.</p>
<h2 id="before-close-the-door">Before: close the door</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">$ kubectl patch svc pacman -n pacman -p &#39;{&#34;spec&#34;:{&#34;type&#34;:&#34;ClusterIP&#34;}}&#39;
</span></span><span class="line"><span class="cl">service/pacman patched
</span></span><span class="line"><span class="cl">NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
</span></span><span class="line"><span class="cl">pacman   ClusterIP   10.106.219.213   &lt;none&gt;        80/TCP    4d13h
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$ curl -m 5 http://192.168.144.23/
</span></span><span class="line"><span class="cl">curl: timed out / unreachable
</span></span></code></pre></div><p>The pods are running. The service exists. The game is fine — <em>for anything
inside the VPC</em>. From my desk it&rsquo;s simply gone. That&rsquo;s the whole VPC model
in one <code>curl</code>: the boundary isn&rsquo;t a firewall rule somebody wrote, it&rsquo;s the
absence of a route.</p>
<h2 id="after-open-it-again">After: open it again</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">$ kubectl patch svc pacman -n pacman -p &#39;{&#34;spec&#34;:{&#34;type&#34;:&#34;LoadBalancer&#34;}}&#39;
</span></span><span class="line"><span class="cl">service/pacman patched
</span></span><span class="line"><span class="cl">NAME     TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)        AGE
</span></span><span class="line"><span class="cl">pacman   LoadBalancer   10.106.219.213   192.168.144.23   80:31467/TCP   4d13h
</span></span></code></pre></div><p>Same VIP handed straight back. NSX programmed a virtual server and pool on
the VPC LB; the supervisor stitched it to the cluster&rsquo;s NodePort. Door open.</p>
<p>And then the game <em>didn&rsquo;t load</em>.</p>
<h2 id="the-outage-i-gave-myself-this-is-the-useful-bit">The outage I gave myself (this is the useful bit)</h2>
<p>Everything was green:</p>
<ul>
<li><code>kubectl get svc</code> — LoadBalancer, VIP assigned</li>
<li><code>kubectl get endpoints</code> — pod IPs present</li>
<li>NSX — virtual server up, pool members healthy</li>
<li><code>iptables</code> on the node — NodePort rules identical to a working
neighbour service</li>
</ul>
<p>Five layers, all green, and <code>curl</code> hung. The control experiment was the
manually-built cluster&rsquo;s Pac-Man at <code>.15</code>, untouched throughout, still
playing.</p>
<p>The cause: my &ldquo;harmless&rdquo; <code>ClusterIP</code> patch earlier had included a <code>ports</code>
list. <code>kubectl patch</code> with a merge patch <strong>replaces arrays</strong>, it doesn&rsquo;t
merge them — and my array said <code>targetPort: 80</code>. Pac-Man listens on
<strong>8080</strong>. Every layer above was faithfully forwarding traffic to a port
nothing was listening on, and every layer reported success because <em>its</em>
job was done.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">$ kubectl patch svc pacman -n pacman --type=json \
</span></span><span class="line"><span class="cl">    -p &#39;[{&#34;op&#34;:&#34;replace&#34;,&#34;path&#34;:&#34;/spec/ports/0/targetPort&#34;,&#34;value&#34;:8080}]&#39;
</span></span><span class="line"><span class="cl">service/pacman patched
</span></span><span class="line"><span class="cl">$ curl -s http://192.168.144.23/ | grep -o &#39;&lt;title&gt;.*&lt;/title&gt;&#39;
</span></span><span class="line"><span class="cl">&lt;title&gt;Pacman in HTML 5 Canvas&lt;/title&gt;
</span></span></code></pre></div><p>Instant recovery. The diagnosis walked the entire paravirtual chain —
VIP → supervisor <code>VirtualMachineService</code> → NSX VS/pool → NodePort
<code>iptables</code> → pod — and it&rsquo;s exactly the walk you&rsquo;ll need one day:</p>
<table>
	<thead>
			<tr>
					<th>Layer</th>
					<th>Check</th>
					<th>What &ldquo;green&rdquo; hides</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>VIP</td>
					<td><code>kubectl get svc</code> EXTERNAL-IP</td>
					<td>nothing about the backend</td>
			</tr>
			<tr>
					<td>NSX LB</td>
					<td>virtual server + pool status</td>
					<td>pool health is TCP to the <em>NodePort</em>, not the pod</td>
			</tr>
			<tr>
					<td>Endpoints</td>
					<td><code>kubectl get endpoints</code></td>
					<td>it lists pod IP:<strong>targetPort</strong> — read the number</td>
			</tr>
			<tr>
					<td>Node</td>
					<td><code>iptables -t nat -L KUBE-SERVICES</code></td>
					<td>rules can be perfect and point at the wrong port</td>
			</tr>
			<tr>
					<td>Pod</td>
					<td><code>kubectl exec ... ss -ltn</code></td>
					<td>the only place the truth lives</td>
			</tr>
	</tbody>
</table>
<p>Bonus find on the way: kube-proxy <em>and</em> Antrea on that cluster had dropped
their API watches days earlier (<code>http2: client connection lost</code>) and never
re-established informers until restarted. It didn&rsquo;t cause this outage, but
it&rsquo;s the kind of thing you only find when you&rsquo;re forced to look.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>If you run a platform for more than one team, this is the feature you&rsquo;ve
been asking the network team for. A VPC gives each team, project or customer
its own private network space — created by them, in minutes, with nothing
reachable from outside until they publish it. Security teams like it for the
same reason developers do: exposure is a deliberate, auditable act, not a
side effect of plugging something in.</p>
<p>What organisations do with it once they have it:</p>
<ul>
<li><strong>Per-team sandboxes</strong> that can&rsquo;t see each other, provisioned without a
ticket.</li>
<li><strong>Partner or supplier environments</strong> isolated from the corporate estate
but hosted on the same platform.</li>
<li><strong>Multi-tenant hosting</strong> — service providers and internal IT alike — with
isolation enforced by topology rather than a growing pile of firewall rules.</li>
</ul>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>A VPC&rsquo;s boundary is <strong>the absence of a route</strong>, not a rule. <code>Private</code>
subnets are unreachable from outside by construction — which is also why
identical CIDRs across VPCs just work.</li>
<li>A <code>LoadBalancer</code> service is the <em>deliberate</em> door: NSX VIP from the
external block, programmed per service. Flip the type and the door
closes with nothing else to clean up.</li>
<li><code>kubectl patch</code> (merge) <strong>replaces <code>spec.ports</code></strong>, it doesn&rsquo;t merge it.
Patch a single field with <code>--type=json</code>, or don&rsquo;t include the array.</li>
<li>Five green layers can hide one wrong integer. Keep a <em>working control</em>
(here: the untouched <code>.15</code> instance) and compare layer by layer.</li>
<li>Read <code>kubectl get endpoints</code> as <code>IP:targetPort</code> — the port is the part
people skim.</li>
</ul>
<p><em>Next in the Pod Papers: <a href="/posts/nested-esxi-nsx-vpc/">nested ESXi inside a VPC</a> —
where &ldquo;private by default&rdquo; meets a host that fakes its own MAC address.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Output captured live, trimmed for length,
never edited for outcome — including the outage.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
