<?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>Vcf-Automation on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/vcf-automation/</link>
    <description>Recent content in Vcf-Automation 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/vcf-automation/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>
    <item>
      <title>One catalog item, one VCF instance: building a lab factory</title>
      <link>https://thenestedlab.com/posts/one-catalog-item-one-vcf-instance/</link>
      <pubDate>Wed, 16 Sep 2026 06:40:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/one-catalog-item-one-vcf-instance/</guid>
      <description>How an interactive PowerShell script grew into a catalog-driven factory that stands up complete nested VCF 9.1 instances — hosts, bringup, supervisor, fleet components — from a single request form. The design rules that made it survivable, and the traps that shaped them.</description>
      <content:encoded><![CDATA[<p>Every nested VCF lab starts the same way: a heroic PowerShell script.
Ours was <code>esxihostdeploy.ps1</code> — ovftool plus PowerCLI, an interactive menu
asking which environment, which ESX version, which role, how many hosts.
It worked. It also lived on one person&rsquo;s machine, prompted for credentials,
and knew nothing about everything that comes <em>after</em> the hosts exist.</p>
<p>This post is about what it became: a set of VCF Automation catalog items
where requesting <strong>one form</strong> produces a complete nested VCF 9.1 instance —
ESXi hosts, bringup (vCenter, NSX, SDDC Manager), a vSphere Supervisor,
VCF Automation, Operations, identity — with the environment number as
practically the only real input.</p>
<h2 id="the-shape-of-the-factory">The shape of the factory</h2>
<p>Three stages, each a catalog item, plus a wrapper that chains them:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Stage 1  Nested ESX Hosts        VM Apps template + vRO actions
</span></span><span class="line"><span class="cl">         (the old script, reborn declaratively)
</span></span><span class="line"><span class="cl">Stage 2  Deploy VCF 9.1 Instance vRO workflow driving the VCF Installer API
</span></span><span class="line"><span class="cl">         (spec generated, validated, bringup started)
</span></span><span class="line"><span class="cl">Day-N    Supervisor · NSX Edge · VCF Automation · Ops Logs/Networks/RTM ·
</span></span><span class="line"><span class="cl">         Identity (AD)           one catalog item each
</span></span><span class="line"><span class="cl">Wrapper  &#34;Deploy VCF Stack&#34;      one form, checkbox per component
</span></span></code></pre></div><p><img alt="The factory catalog: hosts, bringup, every day-N component, and the wrapper — ten tiles" loading="lazy" src="/images/ui/f1-f00-factory-catalog.jpg"></p>
<p>The wrapper&rsquo;s form has a checkbox per component; ticking one reveals that
component&rsquo;s tab with every field pre-populated. One lab password feeds every
credential. Tick everything, click request, and go get coffee.</p>
<h2 id="rule-1-derive-everything-from-one-number">Rule 1: derive everything from one number</h2>
<p>Each lab environment is <code>f0X</code>, and <em>everything</em> scales from X by formula:</p>
<table>
	<thead>
			<tr>
					<th>Element</th>
					<th>Pattern</th>
					<th>f03 example</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Names</td>
					<td><code>f0X-m01-*</code></td>
					<td><code>f03-m01-vc01.res.lab</code></td>
			</tr>
			<tr>
					<td>Subnets</td>
					<td><code>10.(20+X).&lt;sub&gt;.0/24</code></td>
					<td><code>10.23.1.0/24</code> (mgmt)</td>
			</tr>
			<tr>
					<td>VLANs</td>
					<td><code>2X0n</code></td>
					<td>2307 (edge TEP)</td>
			</tr>
	</tbody>
</table>
<p>The bringup spec — hundreds of lines of JSON the VCF Installer wants — is
generated by a vRO action from a known-good reference spec plus X. Nobody
edits a deployment spec by hand, which means nobody typo-breaks a bringup at
2am. When the old script did this, the formulas lived in string
concatenation; now they live in one action with the reference spec beside it.</p>
<p>The same philosophy carried into stage 1: the script&rsquo;s &ldquo;next free esxNN
index&rdquo; scan became a vRO action bound to the request form, its VLAN/IP
arithmetic became template expressions, its <code>--prop:guestinfo.*</code> flags
became <code>ovfProperties</code> in the template. Porting a script isn&rsquo;t rewriting
it — it&rsquo;s finding the declarative home for each behaviour.</p>
<h2 id="rule-2-never-wait-for-anything-you-can-watch-instead">Rule 2: never wait for anything you can watch instead</h2>
<p>The hard constraint that shaped the whole design: a VCF Automation request
gets about <strong>two hours</strong> before the platform gives up on it. A full VCF
bringup takes longer than that. So the wrapper <em>never waits</em>:</p>
<ul>
<li>Bringup is <strong>fire-and-forget</strong> — the workflow authenticates to the
installer, validates the spec, starts the task, and hands back a
<code>watchTaskId</code>. Re-attach any time to check on it.</li>
<li>Fleet deployments (VCF Automation, Ops for Logs/Networks, metrics) are
server-side tasks; the items submit with <code>waitForCompletion=false</code>.</li>
<li>The supervisor item submits enablement and returns; vCenter carries on.</li>
<li>Only fast, deterministic steps (identity configuration, minutes) run to
completion inside the request.</li>
</ul>
<p><img alt="F06-Mgmt-VCF: the wrapper deployment, Create Successful, 13:12 → 14:05" loading="lazy" src="/images/ui/f3-f00-stack-deployment-success.jpg">
<em>A whole VCF instance as one deployment record — the request finished in under an hour while the build ran on for twelve.</em></p>
<p>Result: a full-stack kick-off <em>completes</em> as a request in well under an hour (53 minutes on the run pictured),
while the actual multi-hour build continues as watchable server-side tasks.
The request&rsquo;s job isn&rsquo;t to do the work — it&rsquo;s to <strong>start the work
correctly</strong> and tell you where to watch it.</p>
<p>The corollary: a failed component is recorded and the remaining components
still run. You fix one thing and re-run one item, not the world.</p>
<h2 id="rule-3-plan-mode-for-infrastructure">Rule 3: plan mode for infrastructure</h2>
<p>Every item in the chain supports <code>validateOnly</code> — and the wrapper cascades
it. Tick everything, set validateOnly, and the entire stack is <em>planned</em>
against the live environment with zero changes: specs generated,
prerequisites checked, name/IP collisions caught. A smoke runner exercises
exactly this on every change to the automation itself.</p>
<p>If you build nothing else into your lab automation, build this. The number
of 2am bringups saved by a five-minute dry run is not small.</p>
<h2 id="the-traps-that-shaped-the-rules">The traps that shaped the rules</h2>
<p>Some of the design above exists because of scars:</p>
<ul>
<li><strong>Hardware validation hates virtual NVMe.</strong> Bringup&rsquo;s HCL check will
block nested hosts; the spec generator has to account for it, or you
discover it two hours in — twice, if you&rsquo;re us.</li>
<li><strong>Small disks, surprising layouts.</strong> Nested hosts with 64 GB disks ship
ESX-OSDATA at essentially the whole disk; a post-provision step relocates
scratch or stage-2 fills the disk with logs.</li>
<li><strong>DNS is a prerequisite, not a step.</strong> The installer&rsquo;s pre-flight wants
every record resolvable before it starts; a one-shot script creates the
per-environment records ahead of the request.</li>
<li><strong>vRO&rsquo;s content-source lag.</strong> A new or changed workflow takes 15–20
minutes of data-collection before the catalog sees it. Publish, wait,
<em>then</em> test — or you&rsquo;ll debug a ghost.</li>
<li><strong>Wrapper inputs are duplicated by necessity.</strong> vRO requires every
sub-workflow input to be passed explicitly, so adding an input to a
component means updating the wrapper&rsquo;s call too. Null-guards in each
component turn a forgotten field into a loud failure instead of a silent
default.</li>
</ul>
<h2 id="why-bother">Why bother?</h2>
<p>Because the payoff compounds. Once a full VCF instance is a catalog request,
everything downstream changes character: upgrade rehearsals happen on
freshly-built instances instead of precious pets; a broken environment is
redeployed, not repaired; and the lab stops being a snowflake collection and
becomes a <em>product</em> — versioned, validated, reproducible.</p>
<p><img alt="Three pods, identical IP plans, no route between them" loading="lazy" src="/images/product-01-hook.jpg">
<em>Where this is heading: the factory&rsquo;s output feeding per-student pods with identical addressing.</em></p>
<p>The factory&rsquo;s next customers, funnily enough, are the isolated VPC pods from
<a href="/series/the-vpc-pod-papers/">the other series on this blog</a> — same
philosophy, one layer further down.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>A complete VCF instance from one form changes what an environment costs
to have. Environments that used to be precious — because building one took
a week — become disposable, and a lot follows from that:</p>
<ul>
<li><strong>Proofs of concept</strong> run on an environment built for the customer&rsquo;s
scenario, not on whatever happens to be free.</li>
<li><strong>Upgrade and migration rehearsals</strong> happen on a fresh instance of the
right version, then it&rsquo;s deleted.</li>
<li><strong>Training and enablement</strong> get a real VCF per person or per team.</li>
<li><strong>Reference builds</strong> exist for every supported release, on demand.</li>
</ul>
<p>This is how Comms-care provides a dedicated instance to every consultant.
The same factory, pointed at a customer&rsquo;s requirements, is a repeatable
way to deliver environments rather than a one-off project each time.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Derive names, subnets and VLANs from a single environment number; generate
specs, never hand-edit them.</li>
<li>Respect the request-duration ceiling: start long work, return a task
handle, re-attach to watch. Never block a wrapper on an hours-long task.</li>
<li><code>validateOnly</code> on every item, cascaded by the wrapper — dry-run the whole
stack before touching anything.</li>
<li>Componentise failure: one broken step re-runs alone.</li>
<li>Pre-create DNS; expect HCL friction on virtual hardware; budget for vRO&rsquo;s
content-source lag.</li>
</ul>
<hr>
<p><em>Lab environment; opinions my own. The automation described builds nested
VCF 9.1 instances for lab and rehearsal use — patterns transfer, specifics
are ours.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>validateOnly everywhere: plan mode for infrastructure</title>
      <link>https://thenestedlab.com/posts/validateonly-everywhere/</link>
      <pubDate>Wed, 16 Sep 2026 06:30:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/validateonly-everywhere/</guid>
      <description>Terraform has plan. Kubernetes has &amp;ndash;dry-run. Your vRO workflows have nothing — unless you give them a validateOnly input and make the wrapper cascade it. A short argument for the single most valuable checkbox in the lab factory, with the failures it caught.</description>
      <content:encoded><![CDATA[<p>Terraform has <code>plan</code>. Kubernetes has <code>--dry-run=server</code>. Ansible has
<code>--check</code>. Every mature infrastructure tool grew a way to say &ldquo;tell me what
you&rsquo;d do, then don&rsquo;t&rdquo; — because the alternative is finding out at 2am, two
hours into a bringup, that a hostname doesn&rsquo;t resolve.</p>
<p>vRO workflows don&rsquo;t come with one. This is the case for adding it to every
single one you write, and cascading it through every wrapper.</p>
<h2 id="the-shape">The shape</h2>
<p>Every catalog item in <a href="/posts/one-catalog-item-one-vcf-instance/">the lab factory</a>
has a boolean input, <code>validateOnly</code>, default false. When true the workflow
does <em>everything it can without changing anything</em>:</p>
<ul>
<li>authenticate to every endpoint it would touch</li>
<li>resolve every name it would use, and fail on the ones that don&rsquo;t</li>
<li>generate every spec it would submit, and run the target&rsquo;s own validation
API on it where one exists (the VCF Installer has one; use it)</li>
<li>check for collisions — names, IPs, existing objects</li>
<li>report what it <em>would</em> have created, then return <code>CREATE_SUCCESSFUL</code></li>
</ul>
<p>The wrapper — the one form that chains hosts, bringup, supervisor, fleet
components, identity — has the same checkbox, and <strong>cascades</strong> it to every
component. Tick everything, tick validateOnly, request. Thirty seconds to
a few minutes later you have a full-stack plan against the <em>live</em>
environment, and nothing has moved.</p>
<h2 id="what-it-caught">What it caught</h2>
<p>Not hypothetically. On a built environment, the cascaded dry run of the
whole stack reported:</p>
<ul>
<li>edge cluster: <strong>already exists</strong> — correctly recorded, wrapper carried on</li>
<li>Ops for Logs: <strong>IP_IN_USE</strong> on the planned address — right, it&rsquo;s deployed</li>
<li>Ops for Networks: same</li>
<li>supervisor: the existing one would be reused; the per-service plan
listed which services were already active</li>
<li>identity: bind succeeded, group resolved, no changes needed</li>
</ul>
<p>That&rsquo;s a plan output. On a <em>fresh</em> environment the same run has caught, at
various times: a DNS record missing for one of ~40 required names (the
installer&rsquo;s own pre-flight found it, in seconds, instead of bringup
finding it in hour two); a stale content-library image ID; a form field
arriving <code>null</code> because a custom form hadn&rsquo;t finished re-importing — which
is a <em>publishing</em> bug the dry run surfaced before anyone requested
anything real.</p>
<h2 id="the-argument-against-answered">The argument against, answered</h2>
<p>&ldquo;It doubles the code.&rdquo; It doesn&rsquo;t — it moves the <code>if (!validateOnly)</code> guard
around the mutating call, and the validation logic is code you should have
had anyway. What it <em>does</em> force is separating &ldquo;compute what to do&rdquo; from
&ldquo;do it&rdquo;, which is how the workflows should have been structured in the
first place.</p>
<p>&ldquo;Some things can&rsquo;t be validated without doing them.&rdquo; True. Say so in the
result summary — &ldquo;would deploy X; no pre-validation available&rdquo; — rather
than skipping the item. Partial plans are still plans.</p>
<p>&ldquo;We have a test environment.&rdquo; You have <em>a</em> test environment. A dry run
against the <em>target</em> is what catches the collision with the thing that&rsquo;s
already there.</p>
<h2 id="make-it-the-smoke-test">Make it the smoke test</h2>
<p>The best consequence: a validateOnly request against a known environment
is a <strong>regression test for the automation itself</strong>, runnable on every
change. The factory&rsquo;s smoke runner does exactly this — request every item
with <code>validateOnly: true</code>, assert <code>CREATE_SUCCESSFUL</code>, diff the plan
summary against the last run. It takes minutes and it has caught more
bugs in the workflows than any amount of code review.</p>
<p><img alt="Deploy VCF Stack request form: one checkbox per component, and validateOnly" loading="lazy" src="/images/ui/f2-f00-stack-form-validateonly.jpg">
<em>The same form, real or dry-run. One checkbox decides.</em></p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>For anyone who has sat through a failed change window, the value is
obvious: a full dry run against the <em>real</em> estate before anything moves.
Fewer failed changes, shorter windows, and a plan output that answers the
change board&rsquo;s questions before they&rsquo;re asked. It also gives auditors
something they rarely get from infrastructure automation — evidence of what
was going to happen, produced by the same tooling that then did it.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Add <code>validateOnly</code> to <strong>every</strong> workflow. Default false. Wrappers
cascade it.</li>
<li>Dry-run does everything but mutate: auth, resolve, generate, call the
target&rsquo;s validator, check collisions, report.</li>
<li>Where a step truly can&rsquo;t be pre-validated, <em>say so</em> in the summary.
Never skip it silently.</li>
<li>A dry run against the real target is a plan. A dry run on every change
is a smoke test. Same checkbox.</li>
<li>The refactor it forces — compute, <em>then</em> act — is the one you wanted.</li>
</ul>
<p><em>Part of <a href="/series/the-lab-factory/">The Lab Factory</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Porting a PowerShell deploy script to a catalog item: the mapping table is the post</title>
      <link>https://thenestedlab.com/posts/porting-a-powershell-deploy-script/</link>
      <pubDate>Wed, 16 Sep 2026 06:20:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/porting-a-powershell-deploy-script/</guid>
      <description>esxihostdeploy.ps1 was 400 lines of ovftool and PowerCLI behind a menu. It became a cloud template, two vRO actions and two subscriptions — and the interesting part is deciding where each behaviour belongs. The full mapping, four non-obvious decisions, and the &amp;lsquo;yes&amp;rsquo; that isn&amp;rsquo;t &amp;rsquo;true&amp;rsquo;.</description>
      <content:encoded><![CDATA[<p>Every lab has one: the script that builds the nested hosts. Ours was
<code>esxihostdeploy.ps1</code> — ovftool plus PowerCLI, an interactive menu for
environment, ESX version, role, size, host count, then a loop of
<code>ovftool --prop:guestinfo.*</code>, <code>Set-VM</code>, <code>New-NetworkAdapter</code>, <code>Set-HardDisk</code>.
It worked for years. It also prompted for credentials, lived on one
machine, and knew nothing about the bringup that came after.</p>
<p>Porting it to a VCF Automation catalog item (VM Apps — a cloud template
plus vRO) is not a rewrite. It&rsquo;s a <strong>sorting exercise</strong>: every behaviour in
the script has a natural home in the declarative model, and the skill is
finding it. Here&rsquo;s the whole table, then the four rows that took thought.</p>
<h2 id="the-mapping">The mapping</h2>
<table>
	<thead>
			<tr>
					<th>Script behaviour</th>
					<th>Where it lives now</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Environment menu (f01–f10)</td>
					<td><code>environment</code> input (enum)</td>
			</tr>
			<tr>
					<td>Version menu → OVA path on a share</td>
					<td><code>esxVersion</code> input → <strong>image mapping</strong> → content library item</td>
			</tr>
			<tr>
					<td>Role menu (management / workload / both)</td>
					<td><code>role</code> input; &ldquo;both&rdquo; = two requests</td>
			</tr>
			<tr>
					<td>Size menu / auto-detect from an existing host</td>
					<td><code>size</code> input (auto-detect dropped — see below)</td>
			</tr>
			<tr>
					<td>vCenter + ESXi credential prompts</td>
					<td>vCenter creds gone (cloud account); <code>esxiRootPassword</code> an encrypted input</td>
			</tr>
			<tr>
					<td>Next-free <code>esxNN</code> index scan (gap-filling)</td>
					<td>vRO <strong>action</strong> <code>getNextEsxHostIndexes</code>, bound to the request form</td>
			</tr>
			<tr>
					<td>VLAN / IP / gateway arithmetic</td>
					<td><strong>template expressions</strong> (same formulas)</td>
			</tr>
			<tr>
					<td><code>ovftool --prop:guestinfo.*</code></td>
					<td><code>ovfProperties</code> on <code>Cloud.vSphere.Machine</code></td>
			</tr>
			<tr>
					<td>Folder lookup / <code>New-Folder</code></td>
					<td><strong>allocation-phase subscription</strong> creates the folder if missing</td>
			</tr>
			<tr>
					<td><code>Set-VM</code> cpu / mem</td>
					<td><code>cpuCount</code> / <code>totalMemoryMB</code> in the template</td>
			</tr>
			<tr>
					<td>2× <code>New-NetworkAdapter</code> (Vmxnet3)</td>
					<td><code>networks</code> array, <code>deviceIndex</code> 0/1/2</td>
			</tr>
			<tr>
					<td>3× <code>Set-HardDisk</code> grow</td>
					<td><strong>post-provision subscription</strong></td>
			</tr>
			<tr>
					<td><code>NestedHVEnabled = $true</code></td>
					<td>post-provision subscription</td>
			</tr>
			<tr>
					<td>&ldquo;Power on after?&rdquo; prompt</td>
					<td><code>powerOn</code> input, honoured post-provision</td>
			</tr>
			<tr>
					<td>Summary table printed at the end</td>
					<td>the deployment view in the UI</td>
			</tr>
	</tbody>
</table>
<p>Five homes, in decreasing order of preference: <strong>input</strong>, <strong>template
expression</strong>, <strong>platform abstraction</strong> (image mapping, network profile,
cloud account), <strong>form action</strong> (read-only lookup at request time),
<strong>subscription</strong> (imperative work at a lifecycle stage). Push each
behaviour as far up that list as it will go.</p>
<h2 id="the-four-decisions-that-werent-obvious">The four decisions that weren&rsquo;t obvious</h2>
<h3 id="1-drop-auto-detect-the-platform-already-remembers">1. Drop auto-detect; the platform already remembers</h3>
<p>The script inspected an existing host to infer size. That was a workaround
for having no record. The catalog <em>is</em> the record — deployment history
shows what size every existing host was requested at — so the input
simply asks. Fewer moving parts, and the requester sees the choice.</p>
<h3 id="2-the-index-scan-is-a-form-action-not-a-workflow-step">2. The index scan is a form action, not a workflow step</h3>
<p>&ldquo;Next free <code>esx07</code>&rdquo; has to be known <em>at request time</em> so the requester
sees the names they&rsquo;ll get. That&rsquo;s a <strong>vRO action bound to the custom
form</strong> (<code>getNextEsxHostIndexes(environment, role, count)</code> → array of
strings), not a step inside provisioning. Forms can call actions; use it
for anything that&rsquo;s a lookup.</p>
<h3 id="3-rename-and-folder-go-in-compute-allocation-hardware-goes-in-post-provision">3. Rename and folder go in <em>Compute Allocation</em>, hardware goes in <em>Post Provision</em></h3>
<p>Two blocking subscriptions, filtered by a custom property on the template:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Subscription 1</th>
					<th>Subscription 2</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Topic</td>
					<td>Compute allocation</td>
					<td>Compute post provision</td>
			</tr>
			<tr>
					<td>Runnable</td>
					<td>&ldquo;Set VM Name &amp; Folder&rdquo;</td>
					<td>&ldquo;Finalize Hardware&rdquo;</td>
			</tr>
			<tr>
					<td>Does</td>
					<td>sets <code>resourceNames</code>, creates folder</td>
					<td>grows 3 disks, <code>NestedHVEnabled</code>, power on</td>
			</tr>
			<tr>
					<td>Timeout</td>
					<td>10 min</td>
					<td>30 min</td>
			</tr>
	</tbody>
</table>
<p>The rename <em>must</em> be at allocation — it&rsquo;s the only stage where a workflow
output named <code>resourceNames</code> is applied to the machine. Disk growth and
nested-HV need a VM that exists, so they wait for post-provision. Both
are blocking: the deployment doesn&rsquo;t proceed until they return.</p>
<h3 id="4-nestedesx-yes--not-true">4. <code>nestedEsx: 'yes'</code> — not <code>true</code></h3>
<p>The subscription condition is
<code>event.data.customProperties.nestedEsx == &quot;yes&quot;</code>. Why not <code>&quot;true&quot;</code>?
Because boolean-looking strings can arrive in the event payload as typed
booleans, and <code>true == &quot;true&quot;</code> is false in the condition evaluator <em>and</em>
in the vRO code. It fails silently — the subscription just never fires.
<code>yes</code> can&rsquo;t be coerced. Small thing; two hours.</p>
<h2 id="what-stayed-exactly-the-same">What stayed exactly the same</h2>
<p>The formulas. <code>10.(20+X).&lt;sub&gt;.0/24</code>, VLAN <code>2X0n</code>, gateway <code>.254</code> — they
were string concatenation in PowerShell and they&rsquo;re template expressions
now, character for character. The <code>guestinfo.*</code> property names — identical,
because the OVA didn&rsquo;t change. Porting a script well means most of it
survives; only the <em>plumbing</em> moves.</p>
<h2 id="the-bits-that-still-bite">The bits that still bite</h2>
<ul>
<li><strong>Network profile without IP ranges.</strong> Addressing is injected via
<code>guestinfo</code>, not the platform&rsquo;s IPAM. Tag the trunk portgroup, add no
ranges, or IPAM and guestinfo will disagree.</li>
<li><strong>Two template revisions in the repo.</strong> v1 is what the guide documents;
v2 grew later. Both kept deliberately, both labelled. Check which one
the org has <em>imported</em> before editing either.</li>
<li><strong>Content-source lag.</strong> A new or changed vRO action needs ~15–20 minutes
of data collection before the form sees it. Publish, wait, then test.</li>
<li><strong>Small disks and OSDATA.</strong> Nested hosts with 64 GB disks ship
ESX-OSDATA at essentially the whole disk. Templates now provision 128 GB;
a relocate-scratch script mitigates existing hosts.</li>
</ul>
<p><img alt="The Nested ESX request form: environment, version, role, size, count — and Host Indexes already computed by the form action" loading="lazy" src="/images/ui/f6-f00-nested-esx-form.jpg">
<em>Every menu prompt from the script is now a field; <code>Host Indexes</code> is the form action&rsquo;s answer to &ldquo;next free esxNN&rdquo;.</em></p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>Almost every organisation has these scripts: valuable, trusted, and stuck
on one person&rsquo;s machine. The message of this post for them is that
modernising doesn&rsquo;t mean rewriting. The logic survives; what changes is
where it lives — behind a request form with access control, an audit
trail, consistent inputs and a deployment record. That&rsquo;s how a team turns
tribal knowledge into a service without losing the years of edge cases the
script already handles.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Porting is <strong>sorting</strong>: input → expression → platform abstraction →
form action → subscription. Push each behaviour as far up as it goes.</li>
<li>Lookups the requester needs to <em>see</em> are <strong>form actions</strong>.</li>
<li>Rename at <strong>allocation</strong> (<code>resourceNames</code> output); hardware at
<strong>post-provision</strong>. Both blocking.</li>
<li>Filter subscriptions on a custom property, and make its value a word
that can&rsquo;t be coerced to a boolean.</li>
<li>Drop workarounds for missing state; the catalog is the state.</li>
<li>Keep the formulas. Move the plumbing.</li>
</ul>
<p><em>Part of <a href="/series/the-lab-factory/">The Lab Factory</a>. Next: <a href="/series/the-lab-factory/">driving the
VCF Installer API from vRO</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Driving the VCF Installer API from vRO: generate, validate, start, walk away</title>
      <link>https://thenestedlab.com/posts/driving-the-vcf-installer-api-from-vro/</link>
      <pubDate>Wed, 16 Sep 2026 06:10:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/driving-the-vcf-installer-api-from-vro/</guid>
      <description>Stage 2 of the lab factory: a vRO workflow that turns an environment number into a complete VCF 9.1 deployment spec, runs the installer&amp;rsquo;s own validation, starts bringup and hands back a task id — because the request dies long before the eight-hour build does. Plus how the wrapper slips the two-hour leash.</description>
      <content:encoded><![CDATA[<p>The <a href="/posts/porting-a-powershell-deploy-script/">nested hosts exist</a>. Now
they need to become a VCF instance: vCenter, NSX, SDDC Manager, the fleet
components. The VCF 9.1 Installer appliance does that from a deployment
spec — a few hundred lines of JSON — through an API. This post is the vRO
workflow that drives it, and the three design constraints that shaped it:
nobody edits the spec by hand, the request can&rsquo;t outlive two hours, and
nested hosts fail hardware validation.</p>
<p>Unlike stage 1 this isn&rsquo;t VM provisioning, so it&rsquo;s not a cloud template.
It&rsquo;s a <strong>vRO workflow published directly as a catalog item</strong> through an
Orchestrator content source.</p>
<h2 id="the-spec-is-generated-never-edited">The spec is generated, never edited</h2>
<p>A vRO action, <code>buildVcfDeploymentSpec(environment, hostFqdns, labPassword, …)</code>, returns the whole spec as a string. Its structure was reconciled
against a <em>validated</em> export from a real bringup — the installer UI lets
you export the spec it accepted — and everything variable derives from the
environment number X:</p>
<table>
	<thead>
			<tr>
					<th>Element</th>
					<th>Pattern</th>
					<th>f03</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Names</td>
					<td><code>f0X-m01-*</code></td>
					<td><code>f03-m01-vc01.res.lab</code></td>
			</tr>
			<tr>
					<td>Subnets</td>
					<td><code>10.(20+X).&lt;sub&gt;.0/24</code></td>
					<td><code>10.23.1.0/24</code> (mgmt)</td>
			</tr>
			<tr>
					<td>VLANs</td>
					<td><code>2X0&lt;sub&gt;</code></td>
					<td>2301 mgmt … 2306 TEP</td>
			</tr>
			<tr>
					<td>Gateways</td>
					<td><code>.254</code></td>
					<td><code>10.23.1.254</code></td>
			</tr>
			<tr>
					<td>vMotion / vSAN ranges</td>
					<td><code>.1–.16</code></td>
					<td><code>10.23.3.1-16</code></td>
			</tr>
			<tr>
					<td>NSX TEP pool</td>
					<td><code>.6.1–.6.32</code></td>
					<td><code>10.23.6.1-32</code></td>
			</tr>
			<tr>
					<td>SDDC Manager</td>
					<td><code>f0X-vcf01.res.lab</code></td>
					<td><code>f03-vcf01.res.lab</code></td>
			</tr>
	</tbody>
</table>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">n</span>   <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">environment</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="mi">10</span><span class="p">);</span>   <span class="c1">// &#34;f03&#34; -&gt; 3
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">pfx</span> <span class="o">=</span> <span class="nx">environment</span> <span class="o">+</span> <span class="s2">&#34;-m01&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">net</span> <span class="o">=</span> <span class="s2">&#34;10.&#34;</span> <span class="o">+</span> <span class="p">(</span><span class="mi">20</span> <span class="o">+</span> <span class="nx">n</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">vlan</span><span class="p">(</span><span class="nx">o</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="mi">2000</span> <span class="o">+</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">)</span> <span class="o">+</span> <span class="nx">o</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">gw</span><span class="p">(</span><span class="nx">sub</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">net</span> <span class="o">+</span> <span class="s2">&#34;.&#34;</span> <span class="o">+</span> <span class="nx">sub</span> <span class="o">+</span> <span class="s2">&#34;.254&#34;</span><span class="p">;</span> <span class="p">}</span>
</span></span></code></pre></div><p>Static across environments: DNS, NTP, subdomain, component sizes, vSAN ESA
FTT=1, and the component build versions pinned to the installer binaries.
One lab password feeds every credential field (the UI export scrubs them;
the action puts them back per the API schema).</p>
<p>Two spec-level decisions worth stealing:</p>
<ul>
<li><code>skipEsxThumbprintValidation: true</code> instead of carrying per-host
<code>sslThumbprint</code>. Supported, and the right trade-off for a lab.</li>
<li>Ops and Automation are <strong>checkboxes</strong> that add their blocks to the spec
— and the installer only accepts a <code>licenseServerSpec</code> when Ops is
present, so the action adds them together or not at all.</li>
</ul>
<h2 id="the-workflow-authenticate--validate--start--return">The workflow: authenticate → validate → start → return</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">1. POST /v1/tokens                 installer login
</span></span><span class="line"><span class="cl">2. POST /v1/sddcs/validations      the installer&#39;s OWN pre-flight on the spec
</span></span><span class="line"><span class="cl">   (poll until COMPLETED; fail on any FAILED check)
</span></span><span class="line"><span class="cl">3. if validateOnly -&gt; return the validation report; touch nothing
</span></span><span class="line"><span class="cl">4. POST /v1/sddcs                  start bringup -&gt; sddcTaskId
</span></span><span class="line"><span class="cl">5. return { sddcTaskId, installerUrl }
</span></span></code></pre></div><p>Step 2 is the <a href="/posts/validateonly-everywhere/">validateOnly</a> story made
concrete: the installer will tell you, in seconds, that
<code>f03-m01-nsx01.res.lab</code> doesn&rsquo;t resolve, that an IP is in use, that a host
isn&rsquo;t reachable. Two hours into a bringup is a bad time to learn that.
Every one of the ~40 DNS records the pre-flight wants is created ahead of
time by a one-shot PowerShell script (<code>New-LabEnvDnsRecords.ps1</code>) — DNS is
a prerequisite, not a step.</p>
<h2 id="the-two-hour-leash-and-how-to-slip-it">The two-hour leash, and how to slip it</h2>
<p>A request from the catalog carries a token with a roughly <strong>two-hour</strong>
lifetime, and a bringup takes around <strong>eight</strong>. So by default the workflow
is fire-and-forget: <code>waitForCompletion=false</code>, return the task id, watch
progress in the installer UI. A <code>watchTaskId</code> input lets you re-attach
later and poll an already-running bringup from a new request.</p>
<p>The wrapper that chains <em>everything</em> — hosts, bringup, then the day-N
components that need bringup to be finished — has a neater trick. The
catalog-bound parent deploys the hosts, submits bringup, and then
<strong>re-executes itself as a plain vRO run</strong> (Orchestrator → Run, no catalog
token, no two-hour kill) carrying the hidden <code>bringupWatchTaskId</code>. That
continuation polls the installer task to completion — eight hours, fine —
and then runs certificates, fleet items, edge, supervisor and identity.
Watch it under <em>Orchestrator → Activity → Runs</em>. The catalog request
itself completes in under an hour — 47 minutes on the run pictured below — having
<em>started the work correctly and handed off</em>.</p>
<p><img alt="Orchestrator runs: the catalog-bound parent (13:12→14:00) and the continuation it spawned (14:00 → 01:56 next day)" loading="lazy" src="/images/ui/f5-f00-vro-runs-parent-continuation.jpg">
<em>Two rows, one build. The parent returns inside the catalog&rsquo;s window; the continuation waits out the bringup and does the day-N work.</em></p>
<p><img alt="The deployment&rsquo;s stackSummary output: hosts ready, bringup completed, continuation started — watch it in Orchestrator › Activity › Runs" loading="lazy" src="/images/ui/f4-f00-stack-outputs-continuation.jpg"></p>
<h2 id="nested-host-frictions">Nested-host frictions</h2>
<p>Three things a physical bringup never meets:</p>
<ul>
<li><strong>HCL validation vs virtual NVMe.</strong> The installer&rsquo;s hardware check
blocks the virtual NVMe controller. Fix at the vLCM layer:
<code>enforce_hcl_validation=false</code> on the image policy. The vSAN health test
<code>nvmeonhcl</code> also complains; silenced via the vSAN API, best-effort with
manual fallback.</li>
<li><strong>DVS compatibility appears late.</strong> After bringup, NSX takes 1–2 hours
to settle before the supervisor&rsquo;s zones endpoint stops returning 500.
If the supervisor stage fails &ldquo;No compatible DVS&rdquo; on a fresh instance,
wait and re-run just that item.</li>
<li><strong>TSM-SSH.</strong> Bringup wants SSH on the hosts; the wrapper enables it
host-direct via SOAP before submitting.</li>
</ul>
<h2 id="stale-schema-the-failure-that-looks-like-a-bug-and-isnt">Stale schema: the failure that looks like a bug and isn&rsquo;t</h2>
<p>Add an input to the vRO workflow after the catalog item exists and the
form will show the new field, the request will record its value, and the
workflow will receive <strong>null</strong> — Service Broker keeps the old request
schema until the content source re-imports. The workflow null-guards every
boolean and aborts with &ldquo;inputs not mapped&rdquo; rather than running with
silently-wrong options. Fix: re-import the content source, confirm the
schema, submit a <em>new</em> request (resubmitting an old one reuses the old
payload).</p>
<p>There are actually three async layers between &ldquo;publish&rdquo; and &ldquo;mappable
request&rdquo; — vRO processing the import, the catalog schema after re-import,
and the form service still enforcing the previous custom form for a minute
or two. Same symptom for all three. Check timing before assuming a bug.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>Repeatable, generated VCF deployments matter well beyond a lab: a second
site, a disaster-recovery instance, a new business unit, an environment per
supported release. Generating the specification from a validated reference
removes the class of errors that comes from editing hundreds of lines of
JSON by hand, and running the installer&rsquo;s own validation first turns
&ldquo;find out in hour two&rdquo; into &ldquo;find out in minute one&rdquo;. It&rsquo;s the difference
between a VCF deployment being a project and being a procedure.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li><strong>Generate the spec</strong> from a validated export plus one number. Nobody
hand-edits JSON at 2am.</li>
<li>Run the <strong>installer&rsquo;s own validation</strong> first, and make it a mode you
can request on its own.</li>
<li>Pre-create DNS. Enable SSH. Disable HCL enforcement on virtual NVMe.</li>
<li>Respect the request lifetime: <strong>start, return a task id, re-attach</strong>.
For a long chain, have the workflow re-run itself outside the catalog.</li>
<li>Null-guard every input and fail loud; stale schemas are a fact of life
after adding inputs.</li>
<li>On a fresh instance, give NSX an hour before you expect DVS
compatibility.</li>
</ul>
<p><em>Part of <a href="/series/the-lab-factory/">The Lab Factory</a>. Previously:
<a href="/posts/porting-a-powershell-deploy-script/">porting the host script</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Bringup verified end-to-end on a
rebuilt environment: 305/305 tasks, <code>COMPLETED_WITH_SUCCESS</code>.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
