<?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>All Apps in Practice on The Nested Lab</title>
    <link>https://thenestedlab.com/series/all-apps-in-practice/</link>
    <description>Recent content in All Apps in Practice 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/series/all-apps-in-practice/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>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>
