<?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-Operations on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/vcf-operations/</link>
    <description>Recent content in Vcf-Operations on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 16 Sep 2026 06:50:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/vcf-operations/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>One VKS cluster, two ways: kubectl vs VCF Automation All Apps</title>
      <link>https://thenestedlab.com/posts/vks-kubectl-vs-vcfa-all-apps/</link>
      <pubDate>Wed, 16 Sep 2026 06:50:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/vks-kubectl-vs-vcfa-all-apps/</guid>
      <description>I built the same VKS cluster twice on the same supervisor: once with kubectl apply, once as a VCF Automation All Apps request. The Cluster object is identical. Everything around it isn&amp;rsquo;t — and the payoff table is what you get for free the second way: catalog, quota class, org RBAC, VCF Operations visibility.</description>
      <content:encoded><![CDATA[<p>The <code>Cluster</code> manifest is the same. That&rsquo;s the point of this post, and
also the punchline: <strong>VKS is VKS</strong> whichever door you walk through. What
differs is everything wrapped around the cluster — who can ask for it,
what limits it, who can see it, and how it shows up in operations tooling.</p>
<p>So: two clusters, one supervisor, one ClusterClass, two paths.</p>
<h2 id="path-a-kubectl-the-way-weve-always-done-it">Path A: kubectl, the way we&rsquo;ve always done it</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">kubectl vsphere login --server &lt;supervisor&gt; --tanzu-kubernetes-cluster-namespace demo
</span></span><span class="line"><span class="cl">kubectl apply -f cluster.yaml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cluster.x-k8s.io/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Cluster</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">clusterNetwork</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="l">pods, services, serviceDomain }  </span><span class="w"> </span><span class="c"># set all three — see below</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">topology</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">builtin-generic-v3.6.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">classNamespace</span><span class="p">:</span><span class="w"> </span><span class="l">vmware-system-vks-public         </span><span class="w"> </span><span class="c"># the gotcha</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="l">v1.35.5+vmware.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">controlPlane</span><span class="p">:</span><span class="w"> </span>{<span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">workers</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="l">one node pool, 2 replicas }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">variables</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">vmClass, storageClass ]</span><span class="w">
</span></span></span></code></pre></div><p>Fifteen minutes later: a cluster. Requires a vSphere namespace that
somebody (an admin) created, with a content library attached, a VM class
assigned and quota set — all in the vSphere Client, by hand.</p>
<h2 id="path-b-the-same-manifest-as-a-catalog-request">Path B: the same manifest, as a catalog request</h2>
<p>In All Apps the cluster is a <code>CCI.Supervisor.Resource</code> inside a blueprint,
sitting next to a <code>CCI.Supervisor.Namespace</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">CCI.Supervisor.Namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">generateName</span><span class="p">:</span><span class="w"> </span><span class="l">${input.name}-</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="l">large                     </span><span class="w"> </span><span class="c"># quota comes from the class</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">regionName</span><span class="p">:</span><span class="w"> </span><span class="l">f06</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">vpcName</span><span class="p">:</span><span class="w"> </span><span class="l">${input.vpc}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contentSources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">f06-vks-lib01, type</span><span class="p">:</span><span class="w"> </span><span class="l">ContentLibrary}]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cluster</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">CCI.Supervisor.Resource</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">${resource.namespace.id}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;the SAME Cluster object as above&gt;</span><span class="w">
</span></span></span></code></pre></div><p>Publish it, and a tenant user requests it from a tile:</p>
<p><img alt="VCFA: VKS cluster list as the tenant sees it" loading="lazy" src="/images/ui/u6a-vks-cluster-list.jpg">
<img alt="VCFA: cluster detail" loading="lazy" src="/images/ui/u6-vks-cluster-detail.jpg"></p>
<p>Fifteen minutes later: a cluster. Byte-identical <code>Cluster</code> object.</p>
<h2 id="what-path-b-adds-for-free">What path B adds for free</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>kubectl</th>
					<th>All Apps request</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Who can create</td>
					<td>anyone with a kubeconfig to that namespace</td>
					<td>org users with catalog entitlement (org RBAC)</td>
			</tr>
			<tr>
					<td>Namespace</td>
					<td>pre-created by an admin, by hand</td>
					<td>created by the request, from a <strong>class</strong></td>
			</tr>
			<tr>
					<td>Quota</td>
					<td>set per namespace in the vSphere Client</td>
					<td>inherited from the namespace class (<code>small</code>/<code>medium</code>/<code>large</code>)</td>
			</tr>
			<tr>
					<td>Content library</td>
					<td>admin attaches manually</td>
					<td><code>contentSources</code> on the blueprint</td>
			</tr>
			<tr>
					<td>Networking</td>
					<td>whatever the namespace has</td>
					<td>pinned to a tenant VPC by <code>vpcName</code></td>
			</tr>
			<tr>
					<td>Sizing choices</td>
					<td>edit YAML</td>
					<td>form inputs with enums (worker count, VM class)</td>
			</tr>
			<tr>
					<td>Record</td>
					<td><code>kubectl get cluster</code></td>
					<td>a <strong>deployment</strong> with inputs, owner, history, day-2 actions</td>
			</tr>
			<tr>
					<td>Visibility</td>
					<td>supervisor only</td>
					<td>VCFA inventory <strong>and</strong> VCF Operations</td>
			</tr>
	</tbody>
</table>
<p>That last row is the one operations teams care about:</p>
<p><img alt="VCF Operations: the VKS cluster object with gauges and time series" loading="lazy" src="/images/ui/u8-ops-vks-summary.jpg">
<img alt="VCF Operations: topology view — the cluster and the apps on it, by name" loading="lazy" src="/images/ui/u9-ops-vks-topology.jpg"></p>
<p>The VCFA-deployed cluster appears in Ops&rsquo; object model — supervisor →
namespace → cluster → nodes → the workloads running on it — and its demo
apps show up by name in the topology tab. The kubectl-built cluster is
<em>also</em> visible to Ops (it&rsquo;s the same supervisor, after all), but it has no
deployment, no owner, no request history and no quota lineage. It&rsquo;s a
thing that exists, not a thing that was <em>provided</em>.</p>
<h2 id="the-four-gotchas-in-order-of-how-much-time-they-cost">The four gotchas, in order of how much time they cost</h2>
<p>Both paths share the same four traps on VKS 1.35 / VCF 9.1:</p>
<ol>
<li><strong>A VCFA-created namespace has no content library.</strong> Zero
<code>VirtualMachineImage</code>s → no cluster possible. <code>contentSources</code> in the
blueprint, or attach by hand.</li>
<li><strong><code>ClusterClass</code> lives in <code>vmware-system-vks-public</code>.</strong> It 404s from
the workload namespace unless the spec sets
<code>topology.classNamespace</code>.</li>
<li><strong>The default Quick Start namespace is 1000M CPU / 1000Mi.</strong> Unusable
for a cluster. Use a real class: <code>small</code> 10000M/10000Mi, <code>medium</code>
20000M, <code>large</code> 40000M.</li>
<li><strong>VKS 1.35 enforces PodSecurity <code>restricted</code> by default.</strong> Demo apps
that run as root get a ReplicaSet and <em>no pods</em>; the events say
<code>FailedCreate</code>. Label the app namespace
<code>pod-security.kubernetes.io/enforce=privileged</code> (or fix the apps).</li>
</ol>
<p>Plus one that only shows up later: set <code>clusterNetwork.serviceDomain</code>
explicitly. It&rsquo;s immutable after create, and a cluster without it produces
a service DNS name that some add-ons build wrongly (<code>....svc.</code> with no
domain). And pick a pod CIDR that doesn&rsquo;t shadow your VPC&rsquo;s external range
— the stock <code>192.168.0.0/16</code> hid the org&rsquo;s <code>192.168.144.0/21</code> from inside
the cluster.</p>
<h2 id="so-which-one">So which one?</h2>
<p>Use <strong>kubectl</strong> when you&rsquo;re the platform team proving something on a
supervisor, or debugging. Use <strong>All Apps</strong> the moment a second person needs
a cluster: the request form is the interface, the namespace class is the
guardrail, the deployment is the audit trail, and Ops sees it as a
provided service rather than a stray object.</p>
<p>The cluster&rsquo;s the same either way. The <em>service</em> isn&rsquo;t.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>The business case for the second path is governance without friction.
Development teams get Kubernetes clusters on request; the platform team
gets quotas, ownership, RBAC and a monitoring view of every cluster for
free. That&rsquo;s the difference between a managed Kubernetes <em>service</em> and a
collection of clusters nobody can account for — and it&rsquo;s typically the
gap that stops organisations offering Kubernetes broadly at all. Everything
the developers touch stays standard Kubernetes; the control lands around
it, not on it.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>The <code>Cluster</code> object is identical across paths — All Apps wraps it, it
doesn&rsquo;t change it.</li>
<li>What All Apps adds: catalog RBAC, class-based quota, library attach,
VPC pinning, deployment history, and a place in the Ops object model.</li>
<li>Four traps on 1.35: no default library, <code>classNamespace</code>, tiny default
quota, PodSecurity <code>restricted</code>.</li>
<li>Set <code>serviceDomain</code> and a non-shadowing pod CIDR at create; both are
immutable.</li>
</ul>
<p><em>Next in All Apps in Practice: <a href="/series/all-apps-in-practice/">the demo apps that live on this
cluster</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
