<?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>Kubernetes on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/kubernetes/</link>
    <description>Recent content in Kubernetes on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 16 Sep 2026 08:40:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/kubernetes/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Seven demo apps, one request: deploying a showcase stack via VCF Automation</title>
      <link>https://thenestedlab.com/posts/demo-apps-via-vcfa/</link>
      <pubDate>Wed, 16 Sep 2026 08:40:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/demo-apps-via-vcfa/</guid>
      <description>KubeDoom, KubeInvaders, kube-ops-view, Pac-Man with persistent MongoDB, podinfo, Goldpinger and a Prometheus stack — deployed onto a VCFA-provisioned VKS cluster in a tenant VPC, each behind its own NSX VIP. The proper tenanted path (not the supervisor shortcut), what the platform does for free, and the traps.</description>
      <content:encoded><![CDATA[<p>Every platform needs a demo stack — something that looks alive on a
projector and quietly exercises every layer underneath. This is the lab&rsquo;s:
seven apps on a VKS cluster that VCF Automation provisioned, in a tenant
VPC, each app behind its own load-balancer VIP.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">kubedoom</span>       <span class="mf">192.168</span><span class="o">.</span><span class="mf">144.20</span><span class="p">:</span><span class="mi">5900</span>   <span class="p">(</span><span class="n">VNC</span> <span class="err">—</span> <span class="n">yes</span><span class="p">,</span> <span class="n">it</span> <span class="n">kills</span> <span class="n">pods</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">kubeinvaders</span>   <span class="mf">192.168</span><span class="o">.</span><span class="mf">144.21</span>
</span></span><span class="line"><span class="cl"><span class="n">kube</span><span class="o">-</span><span class="n">ops</span><span class="o">-</span><span class="n">view</span>  <span class="mf">192.168</span><span class="o">.</span><span class="mf">144.22</span>
</span></span><span class="line"><span class="cl"><span class="n">pacman</span>         <span class="mf">192.168</span><span class="o">.</span><span class="mf">144.23</span>        <span class="p">(</span><span class="o">+</span> <span class="n">MongoDB</span> <span class="n">on</span> <span class="n">a</span> <span class="n">PVC</span> <span class="err">—</span> <span class="n">persistent</span> <span class="n">high</span> <span class="n">scores</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">podinfo</span>        <span class="mf">192.168</span><span class="o">.</span><span class="mf">144.24</span>
</span></span><span class="line"><span class="cl"><span class="n">goldpinger</span>     <span class="mf">192.168</span><span class="o">.</span><span class="mf">144.25</span>        <span class="p">(</span><span class="n">DaemonSet</span> <span class="n">incl</span><span class="o">.</span> <span class="n">control</span> <span class="n">plane</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">prometheus</span>     <span class="p">(</span><span class="ow">in</span><span class="o">-</span><span class="n">cluster</span><span class="p">:</span> <span class="n">KSM</span> <span class="o">+</span> <span class="n">node</span><span class="o">-</span><span class="n">exporter</span><span class="p">,</span> <span class="mi">11</span><span class="o">/</span><span class="mi">11</span> <span class="n">targets</span> <span class="n">up</span><span class="p">)</span>
</span></span></code></pre></div><p><img alt="Three of the seven live on their VIPs — KubeInvaders, kube-ops-view, Pac-Man — and the whole set as VCF Operations sees it" loading="lazy" src="/images/demo-apps-grid.jpg">
<em>Captured from the VIPs the platform handed out. KubeDoom is VNC-only and podinfo is an API, so they sit this one out; the Ops topology tile shows all seven by name.</em></p>
<h2 id="the-path-that-matters-tenanted-not-shortcut">The path that matters: tenanted, not shortcut</h2>
<p>The first version of this stack was deployed <em>against the supervisor</em> —
an admin kubeconfig, a vSphere namespace, <code>kubectl apply</code>. It worked and
it was wrong, for the reason the <a href="/posts/vks-kubectl-vs-vcfa-all-apps/">previous
post</a> spells out: nothing about it
was <em>provided</em> to anyone. So it was torn down (seven apps, cluster, VPC and
VIPs gone in about seven minutes) and rebuilt the proper way:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">dev-01 org → default-project → SupervisorNamespace (class large, region f06, VPC default-f06)
</span></span><span class="line"><span class="cl">          → VKS cluster vks-demo01 → seven apps
</span></span></code></pre></div><p>All created <strong>through VCF Automation</strong> — the CCI API for the namespace and
cluster, then the apps via the cluster&rsquo;s own kubeconfig. Two auth facts
worth writing down, because they cost a cycle each:</p>
<ul>
<li>A <strong>provider</strong> service account reaches the cloud API only. CCI
(<code>/cci/kubernetes/apis/...</code>) rejects provider tokens with 401 — it needs
an <strong>org-scoped</strong> service account.</li>
<li>Device-flow login uses the service account&rsquo;s own UUID as <code>client_id</code>
(not its software ID), against the tenant endpoint
<code>/oauth/tenant/&lt;org&gt;/device_authorization</code>.</li>
</ul>
<p><img alt="VCF Operations topology: the cluster with its apps named" loading="lazy" src="/images/ui/u9-ops-vks-topology.jpg"></p>
<h2 id="what-the-platform-does-for-free">What the platform does for free</h2>
<p>Each app is an ordinary Deployment + <code>Service</code> of type <code>LoadBalancer</code>. The
supervisor turns each service into an NSX VPC LB virtual server and hands
back a VIP from the org&rsquo;s external block. No ingress controller, no
MetalLB, no port-forwarding — seven services, seven VIPs, done. Pac-Man&rsquo;s
MongoDB asks for a PVC and gets a vSAN-backed volume through the CSI the
supervisor already installed. Goldpinger runs as a DaemonSet across every
node including the control plane and draws the node-to-node mesh live.</p>
<p>That&rsquo;s three platform services (LB, storage, networking) exercised by apps
that know nothing about VCF.</p>
<h2 id="the-traps">The traps</h2>
<p><strong>Supervisor refuses cluster-scoped RBAC — even to admin.</strong> Demo apps
that need ClusterRoles (kube-ops-view, Goldpinger, KubeDoom) <em>must</em> live
on a guest cluster. You cannot run them as vSphere Pods on the supervisor.</p>
<p><strong>PodSecurity <code>restricted</code> is the VKS 1.35 default.</strong> Half the demo set
runs as root. Symptom: Deployment shows <code>0 UP-TO-DATE</code>, ReplicaSet exists,
zero pods, events say <code>FailedCreate</code>. Fix: label the namespace
<code>pod-security.kubernetes.io/enforce=privileged</code> — and mention in the demo
that you did, because it&rsquo;s a teaching moment.</p>
<p><strong>node-exporter without <code>hostNetwork</code>.</strong> The VPC fabric blocks pod → node
IP scrapes, so the stock DaemonSet&rsquo;s <code>hostNetwork: true</code> doesn&rsquo;t help;
run it as a normal pod and let Prometheus scrape it in-cluster.</p>
<p><strong>Docker Hub is flaky from behind a proxy.</strong> TLS handshake timeouts put
pods into kubelet&rsquo;s image-pull backoff. Deleting the stuck pods bypasses
the backoff; a Harbor proxy-cache project fixes it properly.</p>
<p><strong>Pod CIDR shadowing.</strong> The stock <code>192.168.0.0/16</code> pod range hid the
org&rsquo;s <code>192.168.144.0/21</code> external block <em>from inside the cluster</em> — apps
couldn&rsquo;t reach their neighbours&rsquo; VIPs. Pod CIDR is now <code>172.16.0.0/16</code>.</p>
<h2 id="automation-notes">Automation notes</h2>
<p>The whole stack is a checkbox on the lab&rsquo;s catalog item that deploys a
supervisor: <code>installDemoApps</code> creates the cluster (newest compatible
Kubernetes release, newest built-in ClusterClass, auto-detected), applies
the seven apps, and reports their URLs in the deployment summary. Run
integrated on a fresh environment: 16.7 minutes to <code>CREATE_SUCCESSFUL</code>.
An immediate re-run: 3.3 minutes, all steps idempotent — which is the
number I actually care about, because it means a broken demo is a re-run,
not a rebuild.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>A demo stack sounds like a toy. It&rsquo;s actually the fastest way to make a
platform <em>legible</em> to people who don&rsquo;t read YAML: a customer watches a
request become a cluster, watches seven services get their own addresses,
opens one and plays it. Everything underneath — self-service Kubernetes,
load balancing, persistent storage, isolation — is being exercised in a way
a non-technical stakeholder can see working. The same stack is what we
put in front of a new team on day one, and the same idempotent deploy is
what makes it safe to demonstrate live: if it breaks on stage, it re-runs
in three minutes.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Demo apps that need cluster-scoped RBAC <strong>must</strong> run on a guest cluster;
the supervisor won&rsquo;t grant it, even to admin.</li>
<li>Build the stack through the <strong>tenanted path</strong> (org SA → CCI → namespace
→ cluster → apps). Same apps, but now they&rsquo;re provided, quota&rsquo;d and
visible in Ops.</li>
<li>VKS 1.35: <code>restricted</code> PodSecurity by default. Label the namespace and
say why.</li>
<li><code>LoadBalancer</code> per app = NSX VIP per app. No ingress needed for a demo.</li>
<li>Pick a pod CIDR that doesn&rsquo;t overlap the VPC external block.</li>
<li>Make the deploy idempotent; a demo that re-runs in 3 minutes is one you
can afford to break on stage.</li>
</ul>
<p><em>Previously: <a href="/posts/vks-kubectl-vs-vcfa-all-apps/">one VKS cluster, two ways</a>.
The Pac-Man instance here is the one from <a href="/posts/whats-a-vpc-with-pacman/">What&rsquo;s a VPC?</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own.</em></p>
]]></content:encoded>
    </item>
    <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>
    <item>
      <title>One VKS cluster, two ways: kubectl vs VCF Automation All Apps</title>
      <link>https://thenestedlab.com/posts/vks-kubectl-vs-vcfa-all-apps/</link>
      <pubDate>Wed, 16 Sep 2026 06:50:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/vks-kubectl-vs-vcfa-all-apps/</guid>
      <description>I built the same VKS cluster twice on the same supervisor: once with kubectl apply, once as a VCF Automation All Apps request. The Cluster object is identical. Everything around it isn&amp;rsquo;t — and the payoff table is what you get for free the second way: catalog, quota class, org RBAC, VCF Operations visibility.</description>
      <content:encoded><![CDATA[<p>The <code>Cluster</code> manifest is the same. That&rsquo;s the point of this post, and
also the punchline: <strong>VKS is VKS</strong> whichever door you walk through. What
differs is everything wrapped around the cluster — who can ask for it,
what limits it, who can see it, and how it shows up in operations tooling.</p>
<p>So: two clusters, one supervisor, one ClusterClass, two paths.</p>
<h2 id="path-a-kubectl-the-way-weve-always-done-it">Path A: kubectl, the way we&rsquo;ve always done it</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">kubectl vsphere login --server &lt;supervisor&gt; --tanzu-kubernetes-cluster-namespace demo
</span></span><span class="line"><span class="cl">kubectl apply -f cluster.yaml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cluster.x-k8s.io/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Cluster</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">clusterNetwork</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="l">pods, services, serviceDomain }  </span><span class="w"> </span><span class="c"># set all three — see below</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">topology</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">builtin-generic-v3.6.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">classNamespace</span><span class="p">:</span><span class="w"> </span><span class="l">vmware-system-vks-public         </span><span class="w"> </span><span class="c"># the gotcha</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="l">v1.35.5+vmware.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">controlPlane</span><span class="p">:</span><span class="w"> </span>{<span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">workers</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="l">one node pool, 2 replicas }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">variables</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">vmClass, storageClass ]</span><span class="w">
</span></span></span></code></pre></div><p>Fifteen minutes later: a cluster. Requires a vSphere namespace that
somebody (an admin) created, with a content library attached, a VM class
assigned and quota set — all in the vSphere Client, by hand.</p>
<h2 id="path-b-the-same-manifest-as-a-catalog-request">Path B: the same manifest, as a catalog request</h2>
<p>In All Apps the cluster is a <code>CCI.Supervisor.Resource</code> inside a blueprint,
sitting next to a <code>CCI.Supervisor.Namespace</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">CCI.Supervisor.Namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">generateName</span><span class="p">:</span><span class="w"> </span><span class="l">${input.name}-</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="l">large                     </span><span class="w"> </span><span class="c"># quota comes from the class</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">regionName</span><span class="p">:</span><span class="w"> </span><span class="l">f06</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">vpcName</span><span class="p">:</span><span class="w"> </span><span class="l">${input.vpc}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contentSources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">f06-vks-lib01, type</span><span class="p">:</span><span class="w"> </span><span class="l">ContentLibrary}]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cluster</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">CCI.Supervisor.Resource</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">${resource.namespace.id}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;the SAME Cluster object as above&gt;</span><span class="w">
</span></span></span></code></pre></div><p>Publish it, and a tenant user requests it from a tile:</p>
<p><img alt="VCFA: VKS cluster list as the tenant sees it" loading="lazy" src="/images/ui/u6a-vks-cluster-list.jpg">
<img alt="VCFA: cluster detail" loading="lazy" src="/images/ui/u6-vks-cluster-detail.jpg"></p>
<p>Fifteen minutes later: a cluster. Byte-identical <code>Cluster</code> object.</p>
<h2 id="what-path-b-adds-for-free">What path B adds for free</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>kubectl</th>
					<th>All Apps request</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Who can create</td>
					<td>anyone with a kubeconfig to that namespace</td>
					<td>org users with catalog entitlement (org RBAC)</td>
			</tr>
			<tr>
					<td>Namespace</td>
					<td>pre-created by an admin, by hand</td>
					<td>created by the request, from a <strong>class</strong></td>
			</tr>
			<tr>
					<td>Quota</td>
					<td>set per namespace in the vSphere Client</td>
					<td>inherited from the namespace class (<code>small</code>/<code>medium</code>/<code>large</code>)</td>
			</tr>
			<tr>
					<td>Content library</td>
					<td>admin attaches manually</td>
					<td><code>contentSources</code> on the blueprint</td>
			</tr>
			<tr>
					<td>Networking</td>
					<td>whatever the namespace has</td>
					<td>pinned to a tenant VPC by <code>vpcName</code></td>
			</tr>
			<tr>
					<td>Sizing choices</td>
					<td>edit YAML</td>
					<td>form inputs with enums (worker count, VM class)</td>
			</tr>
			<tr>
					<td>Record</td>
					<td><code>kubectl get cluster</code></td>
					<td>a <strong>deployment</strong> with inputs, owner, history, day-2 actions</td>
			</tr>
			<tr>
					<td>Visibility</td>
					<td>supervisor only</td>
					<td>VCFA inventory <strong>and</strong> VCF Operations</td>
			</tr>
	</tbody>
</table>
<p>That last row is the one operations teams care about:</p>
<p><img alt="VCF Operations: the VKS cluster object with gauges and time series" loading="lazy" src="/images/ui/u8-ops-vks-summary.jpg">
<img alt="VCF Operations: topology view — the cluster and the apps on it, by name" loading="lazy" src="/images/ui/u9-ops-vks-topology.jpg"></p>
<p>The VCFA-deployed cluster appears in Ops&rsquo; object model — supervisor →
namespace → cluster → nodes → the workloads running on it — and its demo
apps show up by name in the topology tab. The kubectl-built cluster is
<em>also</em> visible to Ops (it&rsquo;s the same supervisor, after all), but it has no
deployment, no owner, no request history and no quota lineage. It&rsquo;s a
thing that exists, not a thing that was <em>provided</em>.</p>
<h2 id="the-four-gotchas-in-order-of-how-much-time-they-cost">The four gotchas, in order of how much time they cost</h2>
<p>Both paths share the same four traps on VKS 1.35 / VCF 9.1:</p>
<ol>
<li><strong>A VCFA-created namespace has no content library.</strong> Zero
<code>VirtualMachineImage</code>s → no cluster possible. <code>contentSources</code> in the
blueprint, or attach by hand.</li>
<li><strong><code>ClusterClass</code> lives in <code>vmware-system-vks-public</code>.</strong> It 404s from
the workload namespace unless the spec sets
<code>topology.classNamespace</code>.</li>
<li><strong>The default Quick Start namespace is 1000M CPU / 1000Mi.</strong> Unusable
for a cluster. Use a real class: <code>small</code> 10000M/10000Mi, <code>medium</code>
20000M, <code>large</code> 40000M.</li>
<li><strong>VKS 1.35 enforces PodSecurity <code>restricted</code> by default.</strong> Demo apps
that run as root get a ReplicaSet and <em>no pods</em>; the events say
<code>FailedCreate</code>. Label the app namespace
<code>pod-security.kubernetes.io/enforce=privileged</code> (or fix the apps).</li>
</ol>
<p>Plus one that only shows up later: set <code>clusterNetwork.serviceDomain</code>
explicitly. It&rsquo;s immutable after create, and a cluster without it produces
a service DNS name that some add-ons build wrongly (<code>....svc.</code> with no
domain). And pick a pod CIDR that doesn&rsquo;t shadow your VPC&rsquo;s external range
— the stock <code>192.168.0.0/16</code> hid the org&rsquo;s <code>192.168.144.0/21</code> from inside
the cluster.</p>
<h2 id="so-which-one">So which one?</h2>
<p>Use <strong>kubectl</strong> when you&rsquo;re the platform team proving something on a
supervisor, or debugging. Use <strong>All Apps</strong> the moment a second person needs
a cluster: the request form is the interface, the namespace class is the
guardrail, the deployment is the audit trail, and Ops sees it as a
provided service rather than a stray object.</p>
<p>The cluster&rsquo;s the same either way. The <em>service</em> isn&rsquo;t.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>The business case for the second path is governance without friction.
Development teams get Kubernetes clusters on request; the platform team
gets quotas, ownership, RBAC and a monitoring view of every cluster for
free. That&rsquo;s the difference between a managed Kubernetes <em>service</em> and a
collection of clusters nobody can account for — and it&rsquo;s typically the
gap that stops organisations offering Kubernetes broadly at all. Everything
the developers touch stays standard Kubernetes; the control lands around
it, not on it.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>The <code>Cluster</code> object is identical across paths — All Apps wraps it, it
doesn&rsquo;t change it.</li>
<li>What All Apps adds: catalog RBAC, class-based quota, library attach,
VPC pinning, deployment history, and a place in the Ops object model.</li>
<li>Four traps on 1.35: no default library, <code>classNamespace</code>, tiny default
quota, PodSecurity <code>restricted</code>.</li>
<li>Set <code>serviceDomain</code> and a non-shadowing pod CIDR at create; both are
immutable.</li>
</ul>
<p><em>Next in All Apps in Practice: <a href="/series/all-apps-in-practice/">the demo apps that live on this
cluster</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
