Everything in this series so far was built with kubectl and API calls.
That proves the platform. It doesn’t make a product. This post turns the
pod into a catalog item: fill in a name, pick a VPC, click Request, and
a few minutes later there’s a datacenter-in-miniature with two SSH prompts
waiting.

All Apps in one paragraph
VCF Automation 9.1 has two provisioning models side by side. VM Apps is
the classic Aria Automation path — cloud templates through an IaaS engine
that drives vCenter. All Apps is the supervisor-native path: the
blueprint composes Kubernetes objects (a Supervisor Namespace, VM Service
VMs, NSX subnets, VKS clusters) and the vSphere Supervisor’s controllers
reconcile them. A blueprint is formatVersion: 2; its resources are
CCI.Supervisor.Namespace and CCI.Supervisor.Resource — the latter is
literally “here’s a manifest, apply it in that namespace.”
That makes the blueprint a composition of the manifests from the earlier
posts, with two additions: inputs, and dependsOn.
The blueprint, section by section
Inputs — the form
inputs:
podName: {type: string, default: nested-pod, pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'}
vpcName: {type: string, description: Must exist and be Realized before deploying.}
esxOva: {type: string, default: vmi-61bb062ddfc506b79} # Nested ESXi 9.1 appliance
isoImage: {type: string, default: vmi-39f562e2ae9e9c501} # the ISO to attach
vmClass: {type: string, default: best-effort-large, enum: [best-effort-large, best-effort-xlarge, best-effort-2xlarge]}

The namespace — with libraries attached
namespace:
type: CCI.Supervisor.Namespace
properties:
generateName: ${input.podName}- # NOT name — new namespaces get a suffix
className: large
regionName: f06
vpcName: ${input.vpcName} # pins the namespace to the pod's VPC
storageClasses: [{name: vSAN Default Storage Policy, limit: 400000Mi}]
zones: [{name: domain-c9, cpuLimit: 40000M, memoryLimit: 64000Mi, ...}]
contentSources:
- {name: ISO, type: ContentLibrary}
- {name: f06-vks-lib01, type: ContentLibrary}
contentSources is the line that closes the gap a lot of first attempts
hit: a VCFA-created namespace has no content library, so there are no
VirtualMachineImages and nothing can be deployed. Declaring the libraries
here attaches them at creation.
The topology — ordered on purpose
snTrunk: {type: CCI.Supervisor.Resource, properties: {context: ${resource.namespace.id}, manifest: <Subnet sn-trunk>}}
snMgmt: {dependsOn: [snTrunk], ... manifest: <Subnet sn-mgmt>}
snVmotion: {dependsOn: [snMgmt], ... manifest: <Subnet sn-vmotion>}
bmMgmt: {... manifest: <SubnetConnectionBindingMap sn-mgmt -> sn-trunk, vlan 1610>}
bmVmotion: {... manifest: <SubnetConnectionBindingMap sn-vmotion -> sn-trunk, vlan 1611>}
The dependsOn chain is the whole reason every pod has identical
CIDRs: fresh VPCs realize subnets
in creation order, and the blueprint fixes that order.

The hosts — dual-NIC, ISO attached, bootstrapped by OVF
esx01:
type: CCI.Supervisor.Resource
dependsOn: [bmMgmt] # no point booting before VLAN 1610 exists
properties:
manifest:
kind: VirtualMachine # vmoperator.vmware.com/v1alpha5
spec:
hardware:
cdrom: [ ... the ISO, declared, connected ... ]
network:
interfaces: [ eth0 -> sn-trunk, eth1 -> sn-trunk ]
bootstrap:
vAppConfig: [ guestinfo.hostname / ipaddress / vlan / ... ]
wait:
fields: [{path: status.powerState, value: PoweredOn}]
(Abridged — the full resource carries the image references, VM class,
guest ID and the complete guestinfo set.)
Two vNICs, both on the trunk — the nested equivalent of a VCF host’s two
pNICs. The ISO rides along as a declarative
CD-ROM. And the wait block makes the deployment’s completion mean
something: the request doesn’t finish until the host is powered on.
The doors — one VIP per host
A VirtualMachineService of type LoadBalancer per host, selecting it by
label and publishing 22 and 443, and a blueprint output that reads the
VIP back out of the service’s status:
outputs:
esx01Ssh: {value: "ssh root@${resource.esx01Access.object.status.loadBalancer.ingress[0].ip}"}
The outputs surface in the deployment view — the requester gets the SSH command, not a scavenger hunt.

The request flow, end to end.
What the blueprint cannot express (yet)
Three cluster-scoped objects have no blueprint resource type, and they must exist before the request — in this order:
VPC—privateIPs: 172.30.0.0/16, same in every podVPCAttachment— connectivity profile with the service gateway; the LB creation fails loudly without itLoadBalancer— silently, permanently required before the namespace
Today that’s a short script or a runbook step per pod. The honest framing: the blueprint is the pod; the VPC is the tenancy, and tenancy is still created one layer up. I’d expect that layer to become blueprintable; until then, keep the three calls next to the blueprint in version control.
Publishing: one version at a time
Blueprint → BlueprintVersion → release. Validation happens at version
time, not create time, and the result lives in status.validationMessages
rather than the HTTP code — a 200 with ContentValid: False is a thing.
And only one 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
its own post.)
Why this matters outside the lab
This is where platform engineering turns into a service. The difference between “we can build you an environment” and “request one from the catalog” 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:
- Time-to-environment measured in minutes, requested by the people who need it, without a queue.
- Consistency by construction — every environment comes from the same definition, so support, training material and runbooks all match.
- Governance built in — quotas, ownership, history and clean teardown are properties of the deployment record, not a spreadsheet.
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.
Rules learned
- All Apps blueprints are compositions of manifests:
CCI.Supervisor.NamespaceplusCCI.Supervisor.Resourceper object. If it works withkubectl, it works in a blueprint. generateName, notname, for the namespace;contentSourcesto attach libraries at creation;zones/storageClassesflat, not wrapped.dependsOnis how you get deterministic CIDRs — order the subnets.wait.fieldsturns “request complete” into “host is powered on”.- VPC / VPCAttachment / LoadBalancer are prerequisites outside the blueprint, in that order, before every request.
- One published version per blueprint; validation in
status, not the HTTP response.
Previously: shared services for isolated tenants. This closes the Pod Papers’ core arc — the companion posts on dual-NIC, no-DHCP bootstrap and blueprint gotchas fill in the details.
Lab environment; opinions my own. Blueprint nested-esxi-pod 1.1.0 is
live in the lab catalog; YAML above trimmed for length.