Desplegando Multiples VMs desde un único formulario dinámico con VCF Automation 8.18.x

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

  1. ¿Qué vamos a construir?
  2. Prerequisitos
  3. Estructura del Cloud Template
  4. Los Resources — VMs, Discos y Redes
  5. El truco del count.index
  6. El mapeo de redes — la parte más interesante
  7. Limitante encontrada con Cloud.vSphere.Network
  8. El template completo
  9. 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 Prod o Dev.

Nada de tickets separados. Nada de formularios por VM. Todo en una sola solicitud.

Pasted image 20260622142706.png
Pasted image 20260622142803.png

Prerequisitos

Antes de copiar y pegar el YAML, asegúrate de tener configurado lo siguiente en tu ambiente de Aria Automation:

  • Image Mappings para Ubuntu22 y Windows2022 apuntando 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 redes net:mgmt (para Prod) y net:vsphere (para Dev), que apuntan a los portgroups de vSphere correctos (Infrastructure > Network Profiles)
  • Cloud Zone configurada y asociada al proyecto
⚠️ ¡OJO!

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: 1
inputs:
# Lo que el usuario llena en el formulario
resources:
# Los objetos que Aria va a crear en vSphere
  • formatVersion: 1 — obligatorio, le dice a Aria la versión del esquema YAML
  • inputs — las variables del formulario que el usuario ve en Service Broker
  • resources — 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 default del array define la primera fila que aparece precargada cuando el usuario abre el formulario
  • minItems: 1 y maxItems: 8 controlan 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 red usa oneOf en lugar de enum — esto es intencional. Con oneOf, el usuario ve las opciones con nombres amigables (Prod o Dev), pero el valor que viaja al template es el const directamente (net:mgmt o net:vsphere). Sin ternarias, sin conversiones intermedias.
⚠️ ¡OJO!

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_1
Desplegando VM[1] → count.index = 1 → cpu: input.vms[1].cpu, disco: Disco[1], red: Red_VM_2
Desplegando 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 disco
  • slice(..., count.index, count.index + 1) — extrae solo el disco en posición count.index
  • map_to_object(..., "source") — lo convierte al formato {source: <id>} que espera attachedDisks

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.

⚠️ ¡OJO!

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: 1
inputs:
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:mgmt
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}'
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.

Pasted image 20260622142706.png
Pasted image 20260622142803.png

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.

Pasted image 20260622144100.png

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 instancia
  • count.index — el índice de cada instancia, que se usa para leer la fila correcta del array
  • map_to_object + slice — el patrón oficial para asignar un disco por VM en deployments cluster
  • oneOf con const — para pasar el tag de red directamente desde el input sin conversiones intermedias
  • Recursos de red predeclarados — workaround a la limitante de Cloud.vSphere.Network que no soporta count sin romper la referencia .id
⚠️ ¡IMPORTANTE!

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.

Deja un comentario