<?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 on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/all-apps/</link>
    <description>Recent content in All-Apps 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/all-apps/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>A datacenter in a catalog tile: nested ESXi pods via VCF Automation All Apps</title>
      <link>https://thenestedlab.com/posts/nested-esxi-via-vcfa-all-apps/</link>
      <pubDate>Wed, 16 Sep 2026 07:40:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/nested-esxi-via-vcfa-all-apps/</guid>
      <description>The whole isolated pod — namespace, trunk subnets, binding maps, two dual-NIC nested ESXi hosts with an ISO attached, SSH/HTTPS VIPs — as one VCF Automation blueprint, published to the catalog. Anatomy of the blueprint, the ordering it enforces, and the three things it can&amp;rsquo;t express.</description>
      <content:encoded><![CDATA[<p>Everything in this series so far was built with <code>kubectl</code> and API calls.
That proves the platform. It doesn&rsquo;t make a <em>product</em>. This post turns the
pod into a <strong>catalog item</strong>: fill in a name, pick a VPC, click Request, and
a few minutes later there&rsquo;s a datacenter-in-miniature with two SSH prompts
waiting.</p>
<p><img alt="VCFA catalog: the nested-esxi-pod tile" loading="lazy" src="/images/ui/u1-catalog-tile.jpg"></p>
<h2 id="all-apps-in-one-paragraph">All Apps in one paragraph</h2>
<p>VCF Automation 9.1 has two provisioning models side by side. <strong>VM Apps</strong> is
the classic Aria Automation path — cloud templates through an IaaS engine
that drives vCenter. <strong>All Apps</strong> is the supervisor-native path: the
blueprint composes Kubernetes objects (a Supervisor Namespace, VM Service
VMs, NSX subnets, VKS clusters) and the vSphere Supervisor&rsquo;s controllers
reconcile them. A blueprint is <code>formatVersion: 2</code>; its resources are
<code>CCI.Supervisor.Namespace</code> and <code>CCI.Supervisor.Resource</code> — the latter is
literally &ldquo;here&rsquo;s a manifest, apply it in that namespace.&rdquo;</p>
<p>That makes the blueprint a <em>composition</em> of the manifests from the earlier
posts, with two additions: inputs, and <code>dependsOn</code>.</p>
<h2 id="the-blueprint-section-by-section">The blueprint, section by section</h2>
<h3 id="inputs--the-form">Inputs — the form</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">podName</span><span class="p">:</span><span class="w">  </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="nt">nested-pod, pattern</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;^[a-z0-9]([-a-z0-9]*[a-z0-9])?$&#39;</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="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, description</span><span class="p">:</span><span class="w"> </span><span class="l">Must exist and be Realized before deploying.}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">esxOva</span><span class="p">:</span><span class="w">   </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="l">vmi-61bb062ddfc506b79}  </span><span class="w"> </span><span class="c"># Nested ESXi 9.1 appliance</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">isoImage</span><span class="p">:</span><span class="w"> </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="l">vmi-39f562e2ae9e9c501}  </span><span class="w"> </span><span class="c"># the ISO to attach</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vmClass</span><span class="p">:</span><span class="w">  </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="nt">best-effort-large, enum</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">best-effort-large, best-effort-xlarge, best-effort-2xlarge]}</span><span class="w">
</span></span></span></code></pre></div><p><img alt="The request form" loading="lazy" src="/images/ui/u2-request-form.jpg"></p>
<h3 id="the-namespace--with-libraries-attached">The namespace — with libraries attached</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><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.podName}-       </span><span class="w"> </span><span class="c"># NOT name — new namespaces get a suffix</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></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.vpcName}             </span><span class="w"> </span><span class="c"># pins the namespace to the pod&#39;s VPC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">storageClasses</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">vSAN Default Storage Policy, limit</span><span class="p">:</span><span class="w"> </span><span class="l">400000Mi}]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">zones</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">domain-c9, cpuLimit</span><span class="p">:</span><span class="w"> </span><span class="nt">40000M, memoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l">64000Mi, ...}]</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></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">ISO, 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">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></code></pre></div><p><code>contentSources</code> is the line that closes the gap a lot of first attempts
hit: a VCFA-created namespace has <strong>no content library</strong>, so there are no
<code>VirtualMachineImage</code>s and nothing can be deployed. Declaring the libraries
here attaches them at creation.</p>
<h3 id="the-topology--ordered-on-purpose">The topology — ordered on purpose</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">snTrunk</span><span class="p">:</span><span class="w">   </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">CCI.Supervisor.Resource, properties</span><span class="p">:</span><span class="w"> </span>{<span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">${resource.namespace.id}, manifest: &lt;Subnet sn-trunk&gt;}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">snMgmt</span><span class="p">:</span><span class="w">    </span>{<span class="nt">dependsOn</span><span class="p">:</span><span class="w"> </span><span class="nt">[snTrunk], ...  manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;Subnet sn-mgmt&gt;}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">snVmotion</span><span class="p">:</span><span class="w"> </span>{<span class="nt">dependsOn</span><span class="p">:</span><span class="w"> </span><span class="nt">[snMgmt],  ...  manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;Subnet sn-vmotion&gt;}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">bmMgmt</span><span class="p">:</span><span class="w">    </span>{<span class="nt">... manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;SubnetConnectionBindingMap sn-mgmt -&gt; sn-trunk, vlan 1610&gt;}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">bmVmotion</span><span class="p">:</span><span class="w"> </span>{<span class="nt">... manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;SubnetConnectionBindingMap sn-vmotion -&gt; sn-trunk, vlan 1611&gt;}</span><span class="w">
</span></span></span></code></pre></div><p>The <code>dependsOn</code> chain is the whole reason <a href="/posts/three-datacenters-one-ip-plan/">every pod has identical
CIDRs</a>: fresh VPCs realize subnets
in creation order, and the blueprint fixes that order.</p>
<p><img alt="Blueprint canvas and YAML side by side" loading="lazy" src="/images/ui/u3-blueprint-canvas-yaml.jpg"></p>
<h3 id="the-hosts--dual-nic-iso-attached-bootstrapped-by-ovf">The hosts — dual-NIC, ISO attached, bootstrapped by OVF</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">esx01</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">dependsOn</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">bmMgmt]                   </span><span class="w"> </span><span class="c"># no point booting before VLAN 1610 exists</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">manifest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">VirtualMachine               </span><span class="w"> </span><span class="c"># vmoperator.vmware.com/v1alpha5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><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">hardware</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cdrom</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">... the ISO, declared, connected ... ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">network</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">interfaces</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">eth0 -&gt; sn-trunk, eth1 -&gt; sn-trunk ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">bootstrap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">vAppConfig</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">guestinfo.hostname / ipaddress / vlan / ... ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">wait</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fields</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="nt">status.powerState, value</span><span class="p">:</span><span class="w"> </span><span class="l">PoweredOn}]</span><span class="w">
</span></span></span></code></pre></div><p>(Abridged — the full resource carries the image references, VM class,
guest ID and the complete <code>guestinfo</code> set.)</p>
<p>Two vNICs, both on the trunk — <a href="/series/the-vpc-pod-papers/">the nested equivalent of a VCF host&rsquo;s two
pNICs</a>. The ISO rides along as a declarative
CD-ROM. And the <code>wait</code> block makes the deployment&rsquo;s <em>completion</em> mean
something: the request doesn&rsquo;t finish until the host is powered on.</p>
<h3 id="the-doors--one-vip-per-host">The doors — one VIP per host</h3>
<p>A <code>VirtualMachineService</code> of type <code>LoadBalancer</code> per host, selecting it by
label and publishing 22 and 443, and a blueprint <strong>output</strong> that reads the
VIP back out of the service&rsquo;s status:</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">outputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">esx01Ssh</span><span class="p">:</span><span class="w"> </span>{<span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;ssh root@${resource.esx01Access.object.status.loadBalancer.ingress[0].ip}&#34;</span>}<span class="w">
</span></span></span></code></pre></div><p>The outputs surface in the deployment view — the requester gets the SSH
command, not a scavenger hunt.</p>
<p><img alt="Deployment topology after a successful request" loading="lazy" src="/images/ui/u4-deployment-topology.jpg"></p>
<p><img alt="Request → deployment in progress → complete" loading="lazy" src="/images/u7-catalog-request-flow.gif">
<em>The request flow, end to end.</em></p>
<h2 id="what-the-blueprint-cannot-express-yet">What the blueprint cannot express (yet)</h2>
<p>Three cluster-scoped objects have <strong>no blueprint resource type</strong>, and they
must exist <em>before</em> the request — <a href="/posts/the-lb-that-must-exist-first/">in this order</a>:</p>
<ol>
<li><code>VPC</code> — <code>privateIPs: 172.30.0.0/16</code>, same in every pod</li>
<li><code>VPCAttachment</code> — connectivity profile with the service gateway; the LB
creation fails loudly without it</li>
<li><code>LoadBalancer</code> — silently, permanently required before the namespace</li>
</ol>
<p>Today that&rsquo;s a short script or a runbook step per pod. The honest framing:
the blueprint is the <em>pod</em>; the VPC is the <em>tenancy</em>, and tenancy is
still created one layer up. I&rsquo;d expect that layer to become blueprintable;
until then, keep the three calls next to the blueprint in version control.</p>
<h2 id="publishing-one-version-at-a-time">Publishing: one version at a time</h2>
<p>Blueprint → <code>BlueprintVersion</code> → release. Validation happens at <em>version</em>
time, not create time, and the result lives in <code>status.validationMessages</code>
rather than the HTTP code — a 200 with <code>ContentValid: False</code> is a thing.
And only <strong>one</strong> version can be published: unrelease 1.0.0 before releasing
1.1.0, or you get a 409. (The full list of sharp edges is
<a href="/series/the-vpc-pod-papers/">its own post</a>.)</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>This is where platform engineering turns into a service. The difference
between &ldquo;we can build you an environment&rdquo; and &ldquo;request one from the
catalog&rdquo; is the difference between days and minutes — and between a
bespoke build and one that is consistent, quota-controlled and recorded
every time. For an organisation that means:</p>
<ul>
<li><strong>Time-to-environment</strong> measured in minutes, requested by the people who
need it, without a queue.</li>
<li><strong>Consistency by construction</strong> — every environment comes from the same
definition, so support, training material and runbooks all match.</li>
<li><strong>Governance built in</strong> — quotas, ownership, history and clean teardown
are properties of the deployment record, not a spreadsheet.</li>
</ul>
<p>The nested-ESXi pod is one catalog item. The same approach delivers any
environment shape: application stacks for developers, sandboxes for a
proof of concept, demo kits for a sales team, isolated builds for a
partner.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>All Apps blueprints are <strong>compositions of manifests</strong>: <code>CCI.Supervisor.Namespace</code>
plus <code>CCI.Supervisor.Resource</code> per object. If it works with <code>kubectl</code>, it
works in a blueprint.</li>
<li><code>generateName</code>, not <code>name</code>, for the namespace; <code>contentSources</code> to attach
libraries at creation; <code>zones</code>/<code>storageClasses</code> flat, not wrapped.</li>
<li><code>dependsOn</code> is how you get <strong>deterministic CIDRs</strong> — order the subnets.</li>
<li><code>wait.fields</code> turns &ldquo;request complete&rdquo; into &ldquo;host is powered on&rdquo;.</li>
<li>VPC / VPCAttachment / LoadBalancer are <strong>prerequisites outside the
blueprint</strong>, in that order, before every request.</li>
<li>One published version per blueprint; validation in <code>status</code>, not the
HTTP response.</li>
</ul>
<p><em>Previously: <a href="/posts/shared-services-for-isolated-tenants/">shared services for isolated tenants</a>.
This closes the Pod Papers&rsquo; core arc — the companion posts on
<a href="/series/the-vpc-pod-papers/">dual-NIC</a>, <a href="/series/the-vpc-pod-papers/">no-DHCP bootstrap</a>
and <a href="/series/the-vpc-pod-papers/">blueprint gotchas</a> fill in the details.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Blueprint <code>nested-esxi-pod</code> 1.1.0 is
live in the lab catalog; YAML above trimmed for length.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Blueprinting the supervisor: seven CCI blueprint gotchas</title>
      <link>https://thenestedlab.com/posts/cci-blueprint-gotchas/</link>
      <pubDate>Wed, 16 Sep 2026 07:10:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/cci-blueprint-gotchas/</guid>
      <description>Everything that made the nested-esxi-pod blueprint fail validation before it worked: ${input} inside flow mappings, name vs generateName, flat zones, contentSources, one-published-version, validation-in-status, and the image-sync race. Short, specific, and each one cost me a cycle.</description>
      <content:encoded><![CDATA[<p>The <a href="/posts/nested-esxi-via-vcfa-all-apps/">nested-esxi-pod blueprint</a>
works. Getting there took seven distinct &ldquo;ContentValid: False&rdquo; (or worse:
a 200 that quietly did nothing). None of them are in the docs I could
find; all of them are five-minute fixes once you know. Here they are, in
the order they bit.</p>
<h2 id="1-inputx-is-illegal-inside-a-flow-mapping">1. <code>${input.x}</code> is illegal inside a flow mapping</h2>
<p>This looks like valid YAML and valid blueprint syntax:</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">key</span><span class="p">:</span><span class="w"> </span><span class="nt">guestinfo.hostname, value</span><span class="p">:</span><span class="w"> </span>{<span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;esx01.${input.podName}.res.lab&#34;</span>}}<span class="w">
</span></span></span></code></pre></div><p>It fails content validation. The expression parser doesn&rsquo;t reach into
flow-style (<code>{...}</code>) mappings. Block style is fine:</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">key</span><span class="p">:</span><span class="w"> </span><span class="l">guestinfo.hostname</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">value</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">esx01.${input.podName}.res.lab</span><span class="w">
</span></span></span></code></pre></div><p>Mixed style in the same list is fine too — only the entries that carry an
expression need to be block-style. (This is why the blueprint&rsquo;s
<code>vAppConfig</code> list looks inconsistent; it&rsquo;s deliberate.)</p>
<h2 id="2-name-vs-generatename-for-a-new-namespace">2. <code>name</code> vs <code>generateName</code> for a new namespace</h2>
<p>A <code>CCI.Supervisor.Namespace</code> you&rsquo;re <em>creating</em> must use <code>generateName</code>.
<code>metadata.name</code> is rejected by the CCI API — the platform appends a random
suffix, so <code>pod-a-</code> becomes <code>pod-a-dgf5p</code>. Everything downstream should
reference <code>${resource.namespace.id}</code>, never a literal name.</p>
<h2 id="3-zones-and-storage-classes-are-flat">3. Zones and storage classes are flat</h2>
<p>Early attempts wrapped them the way the raw CCI API does:</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">initialClassConfigOverrides</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">zones</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span></code></pre></div><p>In a blueprint they&rsquo;re top-level properties of the namespace resource:</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">zones</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">domain-c9</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cpuLimit</span><span class="p">:</span><span class="w"> </span><span class="l">40000M</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">memoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l">64000Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">storageClasses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vSAN Default Storage Policy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">limit</span><span class="p">:</span><span class="w"> </span><span class="l">400000Mi</span><span class="w">
</span></span></span></code></pre></div><p>And zones are <strong>required</strong> — omit them and the API says &ldquo;Zone should be
specified&rdquo;, which at least is a clear message.</p>
<h2 id="4-a-new-namespace-has-no-content-library">4. A new namespace has no content library</h2>
<p>Deploy the namespace, deploy a VM, and get: no <code>VirtualMachineImage</code>
found. A VCFA-created namespace attaches <strong>no</strong> content libraries by
default. The fix is one block:</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">contentSources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- {<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">ISO, 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">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></code></pre></div><p>Without it you&rsquo;re in the vSphere Client attaching libraries to a namespace
by hand, which rather defeats the catalog.</p>
<h2 id="5-images-sync-after-attach--wait-for-statusdisks">5. Images sync <em>after</em> attach — wait for <code>status.disks</code></h2>
<p>Even with libraries attached at creation, the first VM create in a fresh
namespace can be rejected:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">no disks found in image ... status.disks
</span></span></code></pre></div><p>The image objects appear immediately; their disk metadata syncs over the
next 1–3 minutes. The quota webhook checks <code>status.disks</code> and refuses
until it&rsquo;s populated. In a blueprint, put the hosts <code>dependsOn</code> something
that takes a couple of minutes (the binding maps did the job here), or add
an explicit wait. In a script, poll:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">kubectl get virtualmachineimage -n &lt;ns&gt; &lt;vmi&gt; -o jsonpath=&#39;{.status.disks}&#39;
</span></span></code></pre></div><h2 id="6-validation-lives-in-status-not-the-http-code">6. Validation lives in <code>status</code>, not the HTTP code</h2>
<p>Creating a <code>BlueprintVersion</code> returns 200 whether or not the content is
valid. Read the object back:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">status:
</span></span><span class="line"><span class="cl">  contentValid: false
</span></span><span class="line"><span class="cl">  validationMessages:
</span></span><span class="line"><span class="cl">    - &#34;... unexpected token ...&#34;
</span></span></code></pre></div><p>If your pipeline checks the response code, it will happily publish a
broken blueprint. Check <code>status.contentValid</code> and print the messages.</p>
<h2 id="7-only-one-published-version--409-on-the-second">7. Only one published version — 409 on the second</h2>
<p>Release 1.1.0 while 1.0.0 is released and you get a 409. It isn&rsquo;t a
transient conflict; it&rsquo;s the rule. <strong>Unrelease</strong> the current version, then
release the new one. Practically that means a publish step is
<code>unrelease old → release new</code>, and there&rsquo;s a short window where the
catalog item has no released version. Do it when nobody&rsquo;s requesting.</p>
<h2 id="bonus-the-things-that-arent-blueprint-problems">Bonus: the things that aren&rsquo;t blueprint problems</h2>
<p>Three prerequisites have <strong>no blueprint resource type</strong> and have to exist
before the request — VPC, VPCAttachment, LoadBalancer, <a href="/posts/the-lb-that-must-exist-first/">in that
order</a>. The blueprint&rsquo;s <code>vpcName</code>
input says &ldquo;must exist and be Realized&rdquo;, and it means it. Nothing in the
blueprint fails if they&rsquo;re missing; the deployment just never gets a VIP.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>VCF Automation&rsquo;s All Apps model is new, and new platforms have edges. None
of these seven are documented; all of them stall a first project by days if
you meet them cold. The value of a delivery partner who has already built
on the platform isn&rsquo;t the YAML — it&rsquo;s that a customer&rsquo;s first blueprint
publishes on day one instead of week two, and that the sharp edges are
encoded into templates and provisioning scripts where users never meet
them.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Expressions need <strong>block-style YAML</strong>; flow mappings don&rsquo;t get parsed.</li>
<li><code>generateName</code>, and reference the namespace by <code>${resource.x.id}</code>.</li>
<li><code>zones</code> and <code>storageClasses</code> are <strong>flat</strong> and zones are required.</li>
<li><code>contentSources</code> on the namespace, or nothing can be deployed.</li>
<li>Wait for image <code>status.disks</code> before the first VM (1–3 min).</li>
<li>Check <code>status.contentValid</code> — the HTTP code lies by omission.</li>
<li>One released version per blueprint: unrelease, then release.</li>
</ul>
<p><em>Companion to <a href="/posts/nested-esxi-via-vcfa-all-apps/">a datacenter in a catalog tile</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Error text captured live.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>VM Apps vs All Apps: a field comparison of VCF Automation&#39;s two provisioning models</title>
      <link>https://thenestedlab.com/posts/vm-apps-vs-all-apps/</link>
      <pubDate>Wed, 16 Sep 2026 07:00:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/vm-apps-vs-all-apps/</guid>
      <description>VCF Automation 9.1 ships two provisioning architectures in one org. I ran the same use cases through both — Linux and Windows VMs, ISO attach, multi-NIC, isolated pods, nested ESXi, shared services, a catalog item. The honest scorecard, and how each use case is actually achieved.</description>
      <content:encoded><![CDATA[<p>VCF Automation 9.1 has two ways to build things, side by side, in the same
organisation. If you&rsquo;ve come from Aria Automation you&rsquo;ll recognise one of
them immediately. The other looks like Kubernetes because it <em>is</em>
Kubernetes. Choosing between them isn&rsquo;t a matter of taste — they have
genuinely different shapes, and some use cases are natural in one and
awkward in the other.</p>
<p>I&rsquo;ve now pushed the same list of requirements through both on a live VCF
9.1 lab. This is the comparison I wish I&rsquo;d had at the start.</p>
<h2 id="the-two-shapes">The two shapes</h2>
<p><strong>VM Apps</strong> — the classic Aria Automation model:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Org ─ Project ─ Cloud Zone(s)
</span></span><span class="line"><span class="cl">                   │
</span></span><span class="line"><span class="cl">        Cloud Account (vCenter / NSX)
</span></span><span class="line"><span class="cl">                   │
</span></span><span class="line"><span class="cl">   flavor · image · network · storage profiles
</span></span><span class="line"><span class="cl">                   │
</span></span><span class="line"><span class="cl">   Cloud Template (formatVersion 1) ──▶ IaaS engine ──▶ vCenter API
</span></span><span class="line"><span class="cl">        Cloud.vSphere.Machine, Cloud.NSX.Network, customization spec
</span></span></code></pre></div><p>Provisioning is <em>imperative through a broker</em>. The IaaS engine holds the
vCenter session; the tenant references abstractions (flavors, image
mappings) that resolve at request time. State lives in the IaaS database.</p>
<p><strong>All Apps</strong> — supervisor-native, via the Cloud Consumption Interface:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Org ─ CCI Project ─ Region
</span></span><span class="line"><span class="cl">          │
</span></span><span class="line"><span class="cl">   VPC (tenant-created; overlapping privateIPs allowed)
</span></span><span class="line"><span class="cl">          │   + VPCAttachment   + LoadBalancer (before the namespace!)
</span></span><span class="line"><span class="cl">   Supervisor Namespace (spec.vpcName pins it)
</span></span><span class="line"><span class="cl">          │
</span></span><span class="line"><span class="cl">   Kubernetes objects reconciled by the Supervisor:
</span></span><span class="line"><span class="cl">     VirtualMachine / VirtualMachineService / Subnet / BindingMap / VKS Cluster
</span></span><span class="line"><span class="cl">          │
</span></span><span class="line"><span class="cl">   Blueprint (formatVersion 2, CCI.Supervisor.*) → BlueprintVersion → Catalog
</span></span></code></pre></div><p>Provisioning is <em>declarative</em>. The blueprint states desired objects; the
supervisor&rsquo;s controllers converge reality onto them and keep it there.
State lives in etcd. Networking is NSX VPC-native.</p>
<h2 id="scorecard-by-use-case">Scorecard by use case</h2>
<p>Every row below was actually built, not read about.</p>
<table>
	<thead>
			<tr>
					<th>Use case</th>
					<th>VM Apps</th>
					<th>All Apps</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Linux VM + software</td>
					<td><code>Cloud.vSphere.Machine</code> + cloudConfig</td>
					<td>VM Service VM + cloud-init (typed field)</td>
			</tr>
			<tr>
					<td>Windows VM</td>
					<td>customization spec + software components</td>
					<td><code>bootstrap.sysprep</code>; ISO attachable declaratively</td>
			</tr>
			<tr>
					<td>ISO / CD-ROM attach</td>
					<td>day-2 edit in vCenter</td>
					<td><code>hardware.cdrom</code> in the manifest</td>
			</tr>
			<tr>
					<td>Multi-NIC</td>
					<td>NICs across network profiles</td>
					<td>multiple <code>interfaces</code> on VPC subnets; failover verified</td>
			</tr>
			<tr>
					<td>Self-service catalog</td>
					<td>template released to catalog</td>
					<td>blueprint → version → publish (one live version)</td>
			</tr>
			<tr>
					<td>Load-balanced access</td>
					<td>NSX LB via network profile</td>
					<td><code>VirtualMachineService</code> → VPC LB VIP</td>
			</tr>
			<tr>
					<td>Kubernetes clusters</td>
					<td>separate TKG integration</td>
					<td>native <code>Cluster</code> object in the namespace</td>
			</tr>
			<tr>
					<td><strong>Identical isolated environments</strong></td>
					<td>hard: on-demand NAT nets, unique addressing usually forced</td>
					<td><strong>natural</strong>: VPC per pod, overlapping CIDRs, zero route between</td>
			</tr>
			<tr>
					<td><strong>Nested ESXi / VLAN networks</strong></td>
					<td>trunk portgroups on the physical vDS — a fabric change</td>
					<td><strong>trunk subnet + binding maps</strong> — no fabric change</td>
			</tr>
			<tr>
					<td>Shared infra (WSUS, repos)</td>
					<td>shared segment routed everywhere</td>
					<td>one <code>PrivateTGW</code> subnet, one-way reachability</td>
			</tr>
			<tr>
					<td>Extensibility</td>
					<td>vRO, ABX, event broker (rich)</td>
					<td>controllers, GitOps, kubectl (thinner day-2 today)</td>
			</tr>
			<tr>
					<td>Deep per-device vSphere tuning</td>
					<td>anything vCenter can do</td>
					<td>what the VM Service API models</td>
			</tr>
	</tbody>
</table>
<p>The two bold rows are the ones that decided it for me. Both are
<a href="/posts/three-datacenters-one-ip-plan/">documented</a> <a href="/posts/nested-esxi-nsx-vpc/">in this
series</a>; both are genuinely hard in VM Apps
and simply the default in All Apps.</p>
<h2 id="where-vm-apps-still-wins">Where VM Apps still wins</h2>
<p>Be fair to the incumbent:</p>
<ul>
<li><strong>Years of content.</strong> vRO workflows, ABX actions, template libraries and
a mature day-2 action framework carry over unchanged. If you have an
estate of them, that&rsquo;s real value you&rsquo;d be throwing away.</li>
<li><strong>Deep vSphere reach.</strong> Anything vCenter can do to a VM — RDMs, per-device
tuning, exotic customization — the IaaS engine can do, because it drives
vCenter directly.</li>
<li><strong>Familiar network model.</strong> Segments, portgroups, on-demand routed/NAT
networks from a network profile. No new mental model.</li>
<li><strong>Multi-cloud lineage.</strong> The same template idiom stretched to other
endpoints.</li>
</ul>
<p>The <a href="/series/the-windows-build-pipeline/">Windows Server 2025 pipeline</a>
elsewhere on this blog is a VM Apps build, and it&rsquo;s a good one: Event Broker
hooks for hostname allocation and placement metadata, cloudbase-init
staging, a reboot-safe state machine in the guest. Nothing about it needs
rewriting.</p>
<h2 id="where-all-apps-wins-and-why-its-structural">Where All Apps wins, and why it&rsquo;s structural</h2>
<ul>
<li><strong>Declarative and self-healing.</strong> Desired state in etcd, controllers
reconcile. There&rsquo;s no second database to drift from vCenter.</li>
<li><strong>Structural multi-tenancy.</strong> A VPC per tenant is a hard NSX boundary,
not an administrative one. Overlapping CIDRs are <em>allowed</em>, so
cookie-cutter environments deploy side by side.</li>
<li><strong>VMs and Kubernetes are one model.</strong> The same blueprint composes a VKS
cluster, VMs, secrets and networking. GitOps-able with ordinary tools.</li>
<li><strong>VPC networking is first-class.</strong> Trunk subnets, binding maps,
<code>PrivateTGW</code>, per-VPC gateway firewall — none of it has a VM Apps
equivalent.</li>
<li><strong>Modern bootstrap.</strong> cloud-init and sysprep as typed API fields.</li>
</ul>
<h2 id="where-all-apps-hurts-today-all-observed-live">Where All Apps hurts today (all observed live)</h2>
<ul>
<li><strong>Ordering matters and the errors are opaque.</strong> <a href="/posts/the-lb-that-must-exist-first/">The LB must exist
before the namespace</a>; new
namespaces reject VMs until images sync; blueprint validation has
<a href="/posts/cci-blueprint-gotchas/">seven sharp edges</a>.</li>
<li><strong>The tenancy layer isn&rsquo;t blueprintable.</strong> VPC, VPCAttachment and
LoadBalancer are cluster-scoped CCI objects with no blueprint resource
type — scripted, not catalogued.</li>
<li><strong>Two endpoints.</strong> The CCI proxy serves tenancy objects; workload
manifests go to the supervisor. You&rsquo;ll hold two kubeconfigs.</li>
<li><strong>Day-2 is thinner.</strong> No event broker; extensibility means controllers
and GitOps, which is fine if that&rsquo;s your team and a gap if it isn&rsquo;t.</li>
</ul>
<h2 id="recommendation">Recommendation</h2>
<p>Default to <strong>All Apps</strong> for new build-outs. Every use case on the list —
including the two that are genuinely hard in VM Apps — is natural in the
VPC model, and the whole estate is version-controlled YAML. Keep <strong>VM
Apps</strong> as the compatibility surface for existing vRA content and for the
rare thing that needs direct vCenter device manipulation. They coexist per
org, so migration is incremental and nobody has to rewrite a working
pipeline on a deadline.</p>
<p>And whichever you pick: <strong>codify the ordering rules</strong> into the scripts
that provision tenancy, so the sharp edges stay encapsulated and the people
requesting catalog items never meet them.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>Most customers arriving at VCF 9 have an estate of Aria Automation content
and a question: rewrite, coexist, or migrate? The answer above is the one
we take into design workshops. In practice it plays out as an assessment —
which existing templates and workflows still earn their keep, which use
cases are genuinely better served by the VPC model, and where the
boundaries sit — followed by a coexistence plan that moves workloads across
opportunistically instead of on a deadline. The two architectures sharing
one organisation is what makes that low-risk.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Two shapes, not two skins: imperative-through-a-broker vs
declarative-reconciled. Pick per use case, not per org.</li>
<li>All Apps is structurally better at <strong>isolation with identical
addressing</strong> and <strong>nested/VLAN networks without fabric changes</strong>.</li>
<li>VM Apps is still the home for <strong>existing vRO/ABX content</strong> and
<strong>deep vCenter device work</strong>.</li>
<li>All Apps&rsquo; pain is <em>ordering</em>: VPC → attachment → LB → namespace →
subnets → image sync → workloads. Script it once.</li>
<li>They coexist; migrate opportunistically.</li>
</ul>
<hr>
<p><em>Lab environment; opinions my own. Grounded in a VCF 9.1 / vSphere
Supervisor with NSX VPC networking build-out; every row was deployed.</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>
