Condividi tramite


Las máquinas virtuales también escalan

El escalado automático de soluciones IaaS suele ser complicado, pero gracias a los Virtual Machine Scale Sets vamos a ver cómo podemos crear un conjunto de máquinas virtuales que escalen automáticamente cuando necesitemos añadir potencia a nuestra aplicación. Los VMSS se pueden crear basados en un listado de imágenes de sistema que tenemos en la plataforma o bien sobre una imagen de máquina virtual personalizada. Este último tiene la ventaja de que nuestro script de arranque será mucho más sencillo y rápido, pues el trabajo de instalación y actualización ya lo tendremos hecho. En el artículo sobre Node.js en IaaS, creamos un Ubuntu 16 que incluía un servidor Node.js actualizado a la última versión y que se ejecutaba como servicio sobre una carpeta donde teníamos la aplicación. Ahora vamos a crear el conjunto de escalado sobre esta imagen y usaremos un script personalizado para descargar e instalar una versión actualizada de nuestra aplicación, de forma que no tengamos que actualizar la imagen básica tan a menudo.

IaaS y el escalado

Cuando usamos máquinas virtuales, tanto en cloud como en nuestro propio datacneter, y nuestra aplicación necesita escalar, necesitamos:

  • Una imagen del sistema operativo con todo lo básico para nuestra aplicación ya instalado. Dicha imagen tiene que estar preparada para arrancar como una nueva máquina, es decir, no debe que contener valores que tienen que ser únicos, como el nombre de la máquina, opciones de usuario, etc. Esto en Windows se consigue con el comando sysprep.
  • Scripts que desplieguen de forma automatizada los paquetes y aplicaciones que no vayan en la imagen base
  • Un conjunto de máquinas físicas y/o virtuales donde desplegar esas imágenes
  • Un balanceador de carga, que establezca qué máquinas están disponibles y dirija el tráfico en función de la carga de cada una de las VM
  • Un orquestador, que gestione el despliegue de las imágenes y la actualización de las mismas
  • Un conjunto de scripts que arranquen y paren las máquinas en función de las necesidades y de las métricas de uso establecidas (uso de CPU, memoria, cola de llamadas, etc.), si queremos automatizar el escalado

Todo esto puede llegar a ser bastante complicado, por suerte, hay muchos productos que nos permiten gestionar un despliegue en alta disponibilidad.

En Azure, disponemos de los conjuntos de escalado (Azure Scale Sets) que ya cuentan con muchas de las funcionalidades que necesitamos para tener nuestra aplicación en alta disponibilidad, con un escalado flexible con el que podemos añadir y quitar máquinas para ajustar el coste de nuestra solución a las necesidades reales de nuestros usuarios.

En este artículo vamos a crear un conjunto de escalado basado en una imagen personalizada de un sistema operativo, junto con un script que nos permita desplegar nuestra aplicación web cuando la actualicemos, sin necesidad de volver a generar la imagen base del sistema operativo.

Nota: si quieres hacer el mismo ejercicio que presento en este artículo necesitarás:

Imagen personalizada para el conjunto de escalado

Al usar conjuntos de escalado tenemos dos opciones:

  • Usar una imagen proporcionada por la plataforma: esto nos facilitará la creación del conjunto, pero tendremos que añadir un script que instalará los elementos necesarios para que se ejecute nuestra aplicación. Esto puede ralentizar el proceso de escalado dependiendo de las necesidades de nuestra aplicación.
  • Usar una imagen personalizada: podemos crear una imagen con las actualizaciones, configuraciones e instalación necesarias para ejecutar nuestra aplicación. Es lo que hicimos en el artículo anterior y vamos a utilizar como base para crear la plantilla.

Escalado horizontal automático

Si queremos que el sistema escale de forma automática, según unas métricas que vamos a proporcionar, tendremos que indicar esto en la plantilla de despliegue, pues por ahora no tenemos disponible esa configuración en el portal. Las plantillas de Azure Resource Manager nos ayudan a escribir las especificaciones de infraestructura como código, de forma que podamos repetir de forma automatizada el despliegue o actualizar uno existente. La plantilla no es más que un archivo .json que contiene esa especificación. Este archivo puede estar acompañado de un archivo de parámetros y archivos adicionales que necesiten ser desplegados con nuestra plantilla, como pueden ser scripts, imágenes u otras plantillas que hayamos anidado en nuestra plantilla principal.

Tenemos algunos ejemplos en GitHub de las plantillas más comunes y encontrarás unas cuantas sobre conjuntos de escalado, como un Ubuntu con escalado automático, o un despliegue de Windows sobre una imagen personalizada. Para editar la plantilla podemos usar cualquier editor de texto. En Visual Studio Code podemos descargar plugin que nos ayudará con la edición de las plantillas:

image

Y para una experiencia mucho más completa podemos usar Visual Studio, que nos permitirá crear recursos desde multitud de plantillas. En el apartado Cloud de Visual C# o Visual Basic (aunque usemos cualquier otro lenguaje), tenemos la plantilla de Azure Resource Group:

image

Además, Visual Studio nos proporciona un selector para añadir el esqueleto de los elementos más comunes, de forma que no tengamos que escribirlo todo desde cero. Podremos elegir entre las plantillas que ya tenemos instaladas, o las que tenemos disponibles en la cuenta oficial de Github:

image

Para este ejercicio, yo utilicé la plantilla Linux Virtual Machine Sale Set y luego fui extendiendo la funcionalidad.

Estructura de las plantillas

Los archivos de plantilla de ARM suelen tener una estructura similar a la siguiente, con 4 secciones principales:

image

Podemos ver una sección de parámetros, otra de variables, una de recursos y otra opcional con los outputs.

La sección de salida es útil cuando queremos devolver algo a un script o queremos encadenar diferentes plantillas. En nuestro caso nos devolverá el nombre DNS asignado a la IP pública del recurso, de forma que podamos lanzar un navegador sobre la página principal al acabar el despliegue:

image

Parámetros de la plantilla

Las plantillas de ARM sirven para que podamos repetir el mismo despliegue tantas veces como necesitemos y se pueden parametrizar de forma que podamos cambiar algunos valores. Por ejemplo, si permitimos cambiar el tamaño de la máquina virtual, podemos establecer un parámetro basado en una lista cerrada de opciones:

  
"parameters": {
  "vmSku": {
    "type": "string",
    "defaultValue": "Standard_D1",
    "metadata": {
      "description": "Size of VMs in the VM Scale Set."
    },
    "allowedValues": [
      "Standard_D1",
      "Standard_DS1",
      "Standard_D2",
      "Standard_DS2",
      "Standard_D3",
      "Standard_DS3",
      "Standard_D4",
      "Standard_DS4",
      "Standard_D11",
      "Standard_DS11",
      "Standard_D12",
      "Standard_DS12",
      "Standard_D13",
      "Standard_DS13",
      "Standard_D14",
      "Standard_DS14"
    ]
  },

También podemos proporcionar un tamaño mínimo o máximo del valor, e incluso si es un password evitar que se muestre mientras lo escriben:

 
"adminPassword": {
  "type": "securestring",
  "metadata": {
    "description": "Admin password on all VMs. It must be at least 12 characters in length."
  },
  "minLength": 12
},

Para utilizar estos parámetros desde dentro de la plantilla llamaremos a la función parameters() con el nombre del parámetro:

 
"sku": {
  "name": "[parameters('vmSku')]",
  "tier": "Standard",
  "capacity": "[parameters('capacity')]"
},

Variables

Además de los parámetros, podemos definir variables, que pueden estar basadas o calculadas a partir de los parámetros, de forma que no haga falta crear parámetros para todos los valores que necesitamos rellenar. Tenemos un conjunto de funciones para realizar estos cálculos.

 
"variables": {
    "vmssuniqueName": "[toLower(substring(concat(substring(parameters('vmssName'),0,6), uniqueString(resourceGroup().id)), 0, 9))]",

Construcción de la plantilla

Tras esta breve explicación de cómo funciona el sistema de plantillas, vamos a crear la plantilla de VMSS que pueda utilizar la imagen del VHD que creamos en el ejercicio anterior. Dentro de la plantilla vamos a crear todos los recursos de infraestructura que necesitará el sistema:

  • Provisionado de discos para las VMs
  • La red virtual donde estarán las máquinas
  • Una IP pública para el balanceador de carga
  • Un balanceador de carga con sus reglas de balanceo y puertos
  • El conjunto de escalado automático(VMSS) que apuntará a la imagen personalizada y a un script personalizado de inicialización
  • Las reglas de escalado automático

Como necesitamos un mínimo de discos ya preparados para el conjunto de escalado automático, las plantillas tienen una forma de utilizar un array de valores para crear múltiples copias, usaremos la variable “vmssStorageAccounts” que ya hemos inicializado con 5 valores diferentes:

 
{
  "type": "Microsoft.Storage/storageAccounts",
  "name": "[concat(variables('vmssStorageAccounts')[copyIndex()], variables('vmssnewAccountSuffix'))]",
  "location": "[resourceGroup().location]",
  "apiVersion": "2015-06-15",
  "copy": {
    "name": "storageLoop",
    "count": "[variables('vmsssaCount')]"
  },
  "properties": {
    "accountType": "[variables('vmssaccountType')]"
  }
},

La red virtual de esta plantilla será muy sencilla, pues sólo tendremos una capa de front-end web, así que tendremos todas las máquinas dentro de la misma subnet:

 
{
  "type": "Microsoft.Network/virtualNetworks",
  "name": "[variables('virtualNetworkName')]",
  "location": "[resourceGroup().location]",
  "apiVersion": "2016-03-30",
  "properties": {
    "addressSpace": {
      "addressPrefixes": [
        "[variables('addressPrefix')]"
      ]
    },
    "subnets": [
      {
        "name": "[variables('subnetName')]",
        "properties": {
          "addressPrefix": "[variables('subnetPrefix')]"
        }
      }
    ]
  }
}, 

Podéis ver que el “type” está definido como Microsoft.Network/virtualNetworks y luego utilizamos algunas variables para los prefijos de red, tanto de la red virtual como de la subred.

En el balanceador de carga, necesitamos crear una sonda (“probe”) para indicarle qué puerto de las máquinas tiene que probar para saber si puede dirigir el tráfico a esa máquina:

 
{
  "name": "[variables('httpProbeName')]",
  "properties": {
    "protocol": "Tcp",
    "port": 80,
    "intervalInSeconds": 5,
    "numberOfProbes": 2
  }
}, 

Luego esas sondas se configuran en la regla:

   
"loadBalancingRules": [
  {
    "name": "HTTPRule",
    "properties": {
      "loadDistribution": "Default",
      "frontendIPConfiguration": {
        "id": "[variables('feIpConfigId')]"
      },
      "backendAddressPool": {
        "id": "[variables('bepoolID')]"
      },
      "protocol": "Tcp",
      "frontendPort": 80,
      "backendPort": 80,
      "enableFloatingIP": false,
      "idleTimeoutInMinutes": 5,
      "probe": {
        "id": "[concat(variables('lbId'), '/probes/', variables('httpProbeName'))]"
      }
    }
  },

Finalmente creamos el conjunto de escalado automático (VMSS), donde usaremos todos los elementos que hemos creado anteriormente: discos, red, balanceador, reglas y además usaremos la imagen del VHD que creamos en el ejercicio anterior. En la sección virtualMachineProfile definimos un perfil de almacenamiento donde usaremos la imagen personalizada:

   
"virtualMachineProfile": {
  "storageProfile": {
    "osDisk": {
      "name": "vmssosdisk",
      "caching": "ReadOnly",
      "createOption": "FromImage",
      "osType": "Linux",
      "image": {
        "uri": "[parameters('sourceImageVhdUri')]"
      }
    }
  }

Y más abajo encontraremos las extensiones, en este caso usaremos una extensión que nos permite ejecutar un script personalizado durante el arranque de la máquina. El script lo subiremos a un blob privado definido en la propiedades _artifactsLocation y _artifactsLocationSasToken:

   
"extensionProfile": {
  "extensions": [
    {
      "name": "updatescriptextension",
      "properties": {
        "publisher": "Microsoft.Azure.Extensions",
        "type": "CustomScript",
        "typeHandlerVersion": "2.0",
        "autoUpgradeMinorVersion": true,
        "settings": {
          "fileUris": [
            "[concat(parameters('_artifactsLocation'), '/scripts/', parameters('customScriptName'), parameters('_artifactsLocationSasToken'))]",
            "[concat(parameters('_artifactsLocation'), '/app/', parameters('appPackage'), parameters('_artifactsLocationSasToken'))]"
          ],
          "commandToExecute": "[concat('bash ',parameters('customScriptName'), ' ', variables('quote'),parameters('appPackage'),variables('quote'),' ',parameters('destinationFolder'),'
',parameters('serviceName'))]"
        }
      }
    }
  ]
}

Podemos usar un script que acompaña a la plantilla que ya sube esos ficheros por nosotros, o podremos indicar en la propiedad _artifactsLocation un sitio público como una carpeta de un proyecto en GitHub.

El script puede recibir parámetros al crear la línea de comando de llamada dentro del parámetro "commandToExecute", de forma que podemos pasar valores provenientes de la plantilla, tanto de los parámetros definidos como de otros elementos usando las funciones que tenemos para ello. En este caso el script recoge algunos de los parámetros definidos y luego se usarán dentro del updateapp.sh:

 
# script start
echo "Welcome to updateapp.sh"
echo "Number of parameters was: " $#
if [ $# -ne 3 ]; then
    echo usage: $0 {sasuri} {destination} {serviceName}
        exit 1
fi
echo "downloading: " $1 "into " $2
update_app $1 $2
echo "restarting service " $3
restart_service $3

Despliegue de la plantilla

La plantilla se puede desplegar ejecutando desde la línea de comando o bien podemos configurarla directamente en el portal, basta abrir este enlace que desplegará en nuestro portal de Azure la plantilla que ya tenemos alojada en GitHub. En el portal se nos solicitarán algunos parámetros como el nombre del conjunto de disponibilidad. También tendremos que indicar dónde está alojada la imagen de la máquina virtual que generalizamos en el artículo anterior. Será la URL que nos da directamente el portal, aunque esté en un blob privado, pues el archivo .vhd tiene que estar en nuestra cuenta y en la misma región donde vayamos a desplegar.

image

Una vez desplegado, podremos abrir el navegador en la IP pública que se ha generado para comprobar que ya funciona. En la plantilla hemos creado también unas reglas de NAT en el balanceador, para poder acceder directamente a las máquinas por SSH. Para las dos primeras podremos acceder por los puertos 10022 y 10023. Una vez allí podremos ver qué ha pasado durante la instalación del script en la máquina y los logs de salida del mismo.

  • En /var/log/azure/custom-script encontraremos un handler.log donde veremos qué ha pasado durante la descarga e instalación del script
  • En /var/lib/waagent/custom-script/download/0 veremos qué se ha descargado y, si se ha ejecutado, los archivos stderr y stdout contendrán la salida del script.
  • Y en /var/log encontraremos el waagent.log, donde también encontraremos eventos relacionados con el script:

image

Recursos útiles

Que la IaaS os acompañe!

@jmservera
Senior Technical Evangelist
Developer eXperience