Aprovechando la pregunta de un compañero acerca de como podríamos crear un blueprint, Cloud template o simplemente Temple, como le quieras llamar, que permita a usuario agregar VMs por filas desde el formulario; recordé que hace algun tiempo me habia planteado el mismo problema pero por temas de tiempo, nunca le dedique que el tiempo a revisarlo. Así que hemos sacado algunos minuto para hacerlo y testearlo en el ambiente de laboratorio.
La idea es simple — en lugar que el usuario tenga que llenar diferentes solicitudes de despliegue o llenar formularios separados por cada VM, le das un solo formulario donde agrega todas las máquinas que necesita de una vez, con sus propios recursos y su propia red. En este artículo te explico cómo construir ese template desde cero, qué limitantes encontramos en el camino y cómo las resolvimos.
Sin más preámbulo, ¡Vamos al grano!.
CONTENIDO
- ¿Qué vamos a construir?
- Prerequisitos
- Estructura del Cloud Template
- Los Resources — VMs, Discos y Redes
- El truco del count.index
- El mapeo de redes — la parte más interesante
- Limitante encontrada con Cloud.vSphere.Network
- El template completo
- Conclusión
¿Qué vamos a construir?
Un Cloud Template que permite desplegar hasta 8 VMs en vSphere desde un único formulario de VCF Automation 8.x (Antes Aria Automation 8.x). El usuario agrega una fila por cada VM que necesita y define de forma independiente:
- vCPUs: Numero de vCPUs
- Memoria: Cantidad de vRAM en MB
- Disco: Tamaño de un disco adicion en GB
- Sistema Operativo: Seleccion del Sistema Operativo invitado, para este caso Ubuntu22 o Windows2022.
- Red: Selecciona la red a la cual estarár conectada cada una de las VMs, en este caso
ProdoDev.
Nada de tickets separados. Nada de formularios por VM. Todo en una sola solicitud.


Prerequisitos
Antes de copiar y pegar el YAML, asegúrate de tener configurado lo siguiente en tu ambiente de Aria Automation:
- Image Mappings para
Ubuntu22yWindows2022apuntando a los templates de vSphere correspondientes (Infrastructure > Image Mappings) - Flavor Mappings: Tener presente que el Cloud Template diseñado NO usa Flavor Mappings sino los valore CPU y Memoria RAM. Recordemos que aunque estas dos propiedades son excluyentes, es decir uso Flavor o vCPU y RAM en el YAML, es importante que tengamos en cuenta los valores máximos definidos en los Flavor Mappings, ya que aunque estemos usando los valores de CPU y Memoria RAM, el sistema valida que los valores que ingresemos en el formulario, no superen el máximo valor configurado en los Flavors Mappings de la infraestructura — en este caso el máximo Flavor configurado contiene 2 vCPU / 4096 MB. Sino tiene Flavor Mappings configurados en la infraestructura, ingnorar esto.
- Network Tagging Es inportante que dentro de los Network Profiles realicemos el Tagging de las redes que vamos a disponibilizar en el Cloude Template siguiendo el estandar
key:value, recordemos que a esto se le conoce como los capability tags. Para este caso usaremos dos redesnet:mgmt(para Prod) ynet:vsphere(para Dev), que apuntan a los portgroups de vSphere correctos (Infrastructure > Network Profiles) - Cloud Zone configurada y asociada al proyecto
Si los Image Mappings o los Network Profiles no están configurados correctamente, el TEST del template va a fallar — y no precisamente con un error claro. Así que verifica esto primero antes de seguir.
Estructura del Cloud Template
Ahora si vamos a lo que nos interesa. Todo Cloud Template en Aria Automation 8.18.x parte de esta estructura base:
formatVersion: 1inputs: # Lo que el usuario llena en el formularioresources: # Los objetos que Aria va a crear en vSphere
formatVersion: 1— obligatorio, le dice a Aria la versión del esquema YAMLinputs— las variables del formulario que el usuario ve en Service Brokerresources— las máquinas, discos y redes que se van a desplegar
Los Inputs — el formulario dinámico
Aquí está la clave de todo. En lugar de crear inputs fijos por VM (vm1_cpu, vm2_cpu, vm3_cpu…), usamos un array dinámico de objetos. Esto le presenta al usuario una tabla con botón «+ Add Item» donde agrega una fila por cada VM que necesita.
inputs: vms: type: array title: Máquinas Virtuales minItems: 1 maxItems: 8 default: - cpu: 2 memoriaGB: 4096 discoGB: 100 so: Ubuntu22 red: Prod items: type: object properties: cpu: type: integer title: vCPUs minimum: 1 maximum: 2 default: 2 memoriaGB: type: integer title: Memoria (MB) minimum: 1024 maximum: 4096 default: 4096 discoGB: type: integer title: Disco (GB) default: 100 so: type: string title: S.O. enum: - Ubuntu22 - Windows2022 default: Ubuntu22 red: type: string title: Red oneOf: - title: Prod const: net:mgmt - title: Dev const: net:vsphere default: net:mgmt
Algunos puntos importantes acá:
- El
defaultdel array define la primera fila que aparece precargada cuando el usuario abre el formulario minItems: 1ymaxItems: 8controlan cuántas VMs puede solicitar el usuario. Lo importante aca es que hay una relación con el numero de recursos de red que debe ser precreados (max 8 para este caso) con el fin que cada VM tenga su propio recurso de red disponible.- El campo
redusaoneOfen lugar deenum— esto es intencional. CononeOf, el usuario ve las opciones con nombres amigables (Prod o Dev), pero el valor que viaja al template es elconstdirectamente (net:mgmtonet:vsphere). Sin ternarias, sin conversiones intermedias.
No uses oneOf dentro de objetos anidados en un array si quieres evitar el error "Failed to match exactly one schema (matched 2 of 2)". En este caso funciona porque el oneOf está a nivel de propiedad simple (string), no como discriminador de tipo de objeto.
Los Resources — VMs, Discos y Redes
La VM
resources: VM: type: Cloud.vSphere.Machine allocatePerInstance: true properties: count: '${length(input.vms)}' image: '${input.vms[count.index].so}' cpuCount: '${input.vms[count.index].cpu}' totalMemoryMB: '${input.vms[count.index].memoriaGB}' attachedDisks: '${map_to_object(slice(resource.Disco[*].id, count.index, count.index + 1), "source")}' networks: - deviceIndex: 0 assignment: static network: '${count.index == 0 ? resource.Red_VM_1.id : count.index == 1 ? resource.Red_VM_2.id : count.index == 2 ? resource.Red_VM_3.id : count.index == 3 ? resource.Red_VM_4.id : count.index == 4 ? resource.Red_VM_5.id : count.index == 5 ? resource.Red_VM_6.id : count.index == 6 ? resource.Red_VM_7.id : resource.Red_VM_8.id}'
Nota: cpuCount y totalMemoryMB son mutuamente excluyentes con flavor. No uses los dos al mismo tiempo o el template va a fallar con el error "Cannot find matching flavors for instance". Adicionalmente, recuerda que si existen Flavors Mappings en la infraestructura, el valor máximo de vCPU y Memoria RAM no debe exceder el máximo configurado en los Flavor. Aunque son excluyentes, en la TEST de validación del Assembler te saldrá un error que indica que esta superando lo valores de los Flavors de la infraestructura.
El Disco
En este Cloud Template hemos creado un input para permitir al usuario agregar un disco adicional al del SO. Sin embargo, esto podría ser opcional.
Disco:
type: Cloud.vSphere.Disk
allocatePerInstance: true
properties:
count: '${length(input.vms)}'
capacityGb: '${input.vms[count.index].discoGB}'
Las Redes
Como mencionamos anteriormente, para este caso vamos a disponibilizar dos redes desde una lista desplegable para cada una de las VM. Prod y Dev que apuntan a PortGroups Distribuidos y sus Capability Tags han sido configuradas previamente dentro de los Network Profiles.
Red_VM_1:
type: Cloud.vSphere.Network
properties:
networkType: existing
constraints:
- tag: '${length(input.vms) > 0 ? input.vms[0].red : "net:mgmt"}'
Red_VM_2:
type: Cloud.vSphere.Network
properties:
networkType: existing
constraints:
- tag: '${length(input.vms) > 1 ? input.vms[1].red : "net:mgmt"}'
# ... y así hasta Red_VM_8
El truco del count.index
Este es el concepto más importante de todo el template. Cuando usas allocatePerInstance: true, Aria Automation 8.18.x evalúa las expresiones de forma independiente por cada instancia. count.index es el índice de la instancia actual — empieza en 0 y llega hasta length(input.vms) - 1.
Entonces si el usuario pidió 3 VMs:
Desplegando VM[0] → count.index = 0 → cpu: input.vms[0].cpu, disco: Disco[0], red: Red_VM_1Desplegando VM[1] → count.index = 1 → cpu: input.vms[1].cpu, disco: Disco[1], red: Red_VM_2Desplegando VM[2] → count.index = 2 → cpu: input.vms[2].cpu, disco: Disco[2], red: Red_VM_3
Cada VM evalúa la expresión con su propio count.index — no comparten el valor. Sin allocatePerInstance: true esto no funciona: Aria Automation 8.18.x evaluaría todo una sola vez y todas las VMs terminarían con los mismos recursos.
El patrón para los discos usa map_to_object + slice para asignar un disco por VM:
attachedDisks: '${map_to_object(slice(resource.Disco[*].id, count.index, count.index + 1), "source")}'
resource.Disco[*].id— array con todos los IDs de discoslice(..., count.index, count.index + 1)— extrae solo el disco en posicióncount.indexmap_to_object(..., "source")— lo convierte al formato{source: <id>}que esperaattachedDisks
El mapeo de redes — la parte más interesante
El mapeo entre una VM y su red funciona en dos partes que trabajan juntas:
Parte 1 — La ternaria en la VM asigna Red_VM_X según count.index:
network: '${count.index == 0 ? resource.Red_VM_1.id : count.index == 1 ? resource.Red_VM_2.id : ...}'
VM[0] → Red_VM_1, VM[1] → Red_VM_2, etc. Es un if / else if / else anidado escrito con ? y :.
Parte 2 — Cada Red_VM_X toma el tag del input de su misma posición:
Red_VM_1: constraints: - tag: '${length(input.vms) > 0 ? input.vms[0].red : "net:mgmt"}'Red_VM_2: constraints: - tag: '${length(input.vms) > 1 ? input.vms[1].red : "net:mgmt"}'
El índice del recurso de red coincide con el índice de la VM que lo usa — Red_VM_1 siempre corresponde a input.vms[0], Red_VM_2 a input.vms[1]. No es magia, es una convención de índices.
El fallback "net:mgmt" existe para los recursos de red cuya VM no fue solicitada — evita que Aria Automation intente evaluar input.vms[5].red cuando el array solo tiene 2 elementos. Es como dejarle un recurso de red default desconectado.
Limitante encontrada con Cloud.vSphere.Network
Durante el desarrollo de este template encontramos una limitante importante que vale la pena documentar.
El problema: Inicialmente intentamos hacer las redes dinámicas agregando count: '${length(input.vms) > X ? 1 : 0}' a cada recurso Cloud.vSphere.Network, para que solo se «crearan» las redes correspondientes a las VMs solicitadas y las demás quedaran con count: 0, de manera que no se desplegarían.
Lo que pasó: Cuando un recurso Cloud.vSphere.Network tiene count definido, Aria lo convierte en un array. Cuando la VM intenta referenciar ese recurso con resource.Red_VM_1.id, el campo network: espera un String pero recibe un Array — y el deployment falla con el error:
Cannot deserialize value of type String from Array value(token JsonToken.START_ARRAY) at [Source: UNKNOWN]through reference chain: com.vmware.admiral.compute.content.TemplateNetworkInterfaceDescription["network"]
La solución: Declarar los 8 recursos de red (igual al Max de VMs) sin la propiedad count. Al ser networkType: existing, Aria no crea objetos nuevos en vSphere — solo resuelve qué portgroup usar cuando una VM los referencia. Los recursos de red no referenciados sí se registran en el deployment como objetos, pero no generan cambios en la infraestructura real.
Cloud.vSphere.Network no soporta allocatePerInstance: true de la misma forma que Cloud.vSphere.Machine y Cloud.vSphere.Disk. Intentar usarlo rompe la referencia .id. Esta es la razón por la que los 8 recursos de red deben estar predeclarados de forma estática en el template.
El template completo
Ahora la cereza del pastel, el Template completo para copiar y pegar.
formatVersion: 1inputs: vms: type: array title: Maquinas Virtuales minItems: 1 maxItems: 8 default: - cpu: 2 memoriaGB: 4096 discoGB: 100 so: Ubuntu22 red: net:mgmt items: type: object properties: cpu: type: integer title: vCPUs minimum: 1 maximum: 2 default: 2 memoriaGB: type: integer title: Memoria (MB) minimum: 1024 maximum: 4096 default: 4096 discoGB: type: integer title: Disco (GB) default: 100 so: type: string title: S.O. enum: - Ubuntu22 - Windows2022 default: Ubuntu22 red: type: string title: Red oneOf: - title: Prod const: net:mgmt - title: Dev const: net:vsphere default: net:mgmtresources: VM: type: Cloud.vSphere.Machine allocatePerInstance: true properties: count: '${length(input.vms)}' image: '${input.vms[count.index].so}' cpuCount: '${input.vms[count.index].cpu}' totalMemoryMB: '${input.vms[count.index].memoriaGB}' attachedDisks: '${map_to_object(slice(resource.Disco[*].id, count.index, count.index + 1), "source")}' networks: - deviceIndex: 0 assignment: static network: '${count.index == 0 ? resource.Red_VM_1.id : count.index == 1 ? resource.Red_VM_2.id : count.index == 2 ? resource.Red_VM_3.id : count.index == 3 ? resource.Red_VM_4.id : count.index == 4 ? resource.Red_VM_5.id : count.index == 5 ? resource.Red_VM_6.id : count.index == 6 ? resource.Red_VM_7.id : resource.Red_VM_8.id}' Disco: type: Cloud.vSphere.Disk allocatePerInstance: true properties: count: '${length(input.vms)}' capacityGb: '${input.vms[count.index].discoGB}' Red_VM_1: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 0 ? input.vms[0].red : "net:mgmt"}' Red_VM_2: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 1 ? input.vms[1].red : "net:mgmt"}' Red_VM_3: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 2 ? input.vms[2].red : "net:mgmt"}' Red_VM_4: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 3 ? input.vms[3].red : "net:mgmt"}' Red_VM_5: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 4 ? input.vms[4].red : "net:mgmt"}' Red_VM_6: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 5 ? input.vms[5].red : "net:mgmt"}' Red_VM_7: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 6 ? input.vms[6].red : "net:mgmt"}' Red_VM_8: type: Cloud.vSphere.Network properties: networkType: existing constraints: - tag: '${length(input.vms) > 7 ? input.vms[7].red : "net:mgmt"}'
Y aca una imagen del resultado cuando hemos creados dos VMs con los siguientes inputs.


Como podemos apreciar, el despliegue conecta unicamente dos recursos de red, uno para cada instancia de VM desplegada, y los demás recursos de red aunque estan deplesplegados no están conectados.

Conclusión
Con este Cloud Template logramos que el usuario pueda solicitar hasta 8 VMs desde un único formulario dinámico, cada una con su propia configuración de cómputo, disco y red — sin duplicar inputs, sin templates separados por rol, y sin intervención manual del administrador.
Los conceptos clave que hacen que esto funcione son:
allocatePerInstance: true— le dice a Aria Automation que evalúe los inputs de forma independiente por instanciacount.index— el índice de cada instancia, que se usa para leer la fila correcta del arraymap_to_object + slice— el patrón oficial para asignar un disco por VM en deployments clusteroneOfconconst— para pasar el tag de red directamente desde el input sin conversiones intermedias- Recursos de red predeclarados — workaround a la limitante de
Cloud.vSphere.Networkque no soportacountsin romper la referencia.id
He migrado el blog del dominio nachoaprendevirtualizacion.com a nachoaprendeit.com. Si te ha servido este artículo, deja tu buen 👍 Like y compártelo con tus colegas. Estas acciones me ayudarán a optimizar los motores de búsqueda para llegar a más personas y a motivarme a seguir compartiendo este tipo de artículos.

TODOS LOS NOMBRES DE VMS USADOS EN ESTE BLOG SON INVENTADOS Y OBEDECEN A UN AMBIENTE DE LABORATORIO PROPIO, UTILIZADO PARA FINES DE ESTUDIO.
