Creación de imágenes generalizadas sin un agente de aprovisionamiento

Se aplica a: ✔️ máquinas virtuales Linux ✔️ conjuntos de escalado flexibles

Microsoft Azure proporciona agentes de aprovisionamiento para máquinas virtuales Linux en forma de walinuxagent o cloud-init (recomendado). Sin embargo, podría haber un escenario en el que no quiera usar ninguna de estas aplicaciones para el agente de aprovisionamiento, como:

  • Su distribución o versión de Linux no admite el Agente Linux/cloud-init.
  • Necesita que se establezcan propiedades de máquina virtual específicas, como el nombre de host.

Nota

Si no necesita que se establezca ninguna propiedad ni que se produzca ningún tipo de aprovisionamiento, debería considerar la posibilidad de crear una imagen especializada.

En este artículo se muestra cómo puede configurar la imagen de la máquina virtual para cumplir los requisitos de la plataforma de Azure y establecer el nombre de host, sin instalar un agente de aprovisionamiento.

Redes e informes listos

Para que la máquina virtual Linux se comunique con los componentes de Azure, se requiere un cliente DHCP. El cliente se usa para recuperar una dirección IP de host, la resolución DNS y la administración de rutas de la red virtual. La mayoría de las distribuciones se envían con estas utilidades integradas. Entre las herramientas que han probado los proveedores de distribución de Linux en Azure se incluyen dhclient, network-manager, systemd-networkd y otras.

Nota

Actualmente, la creación de imágenes generalizadas sin un agente de aprovisionamiento solo admite máquinas virtuales habilitadas para DHCP.

Una vez que se hayan configurado las redes, seleccione "informar que está listo". Esto indica a Azure que la máquina virtual se ha aprovisionó correctamente.

Importante

Si no informa a Azure de que está listo, se reiniciará la máquina virtual.

Demostración/ejemplo

Una imagen de Marketplace existente (en este caso, una máquina virtual de Debian Buster) con el agente Linux (walinuxagent) quitado y un script de Python personalizado agregado es la manera más fácil de indicar a Azure que la máquina virtual está "lista".

Cree el grupo de recursos y la máquina virtual base:

$ az group create --location eastus --name demo1

Cree la máquina virtual base:

$ az vm create \
    --resource-group demo1 \
    --name demo1 \
    --location eastus \
    --ssh-key-value <ssh_pub_key_path> \
    --public-ip-address-dns-name demo1 \
    --image "debian:debian-10:10:latest"

Eliminación del agente de aprovisionamiento de imágenes

Una vez que se aprovisiona la máquina virtual, puede conectarla mediante SSH en ella y quitar el Agente Linux:

$ sudo apt purge -y waagent
$ sudo rm -rf /var/lib/waagent /etc/waagent.conf /var/log/waagent.log

Incorporación del código necesario a la máquina virtual

También dentro de la máquina virtual, dado que hemos quitado el Agente Linux de Azure, debemos proporcionar un mecanismo para informar de que estamos listos.

Script de Python

import http.client
import sys
from xml.etree import ElementTree

wireserver_ip = '168.63.129.16'
wireserver_conn = http.client.HTTPConnection(wireserver_ip)

print('Retrieving goal state from the Wireserver')
wireserver_conn.request(
    'GET',
    '/machine?comp=goalstate',
    headers={'x-ms-version': '2012-11-30'}
)

resp = wireserver_conn.getresponse()

if resp.status != 200:
    print('Unable to connect with wireserver')
    sys.exit(1)

wireserver_goalstate = resp.read().decode('utf-8')

xml_el = ElementTree.fromstring(wireserver_goalstate)

container_id = xml_el.findtext('Container/ContainerId')
instance_id = xml_el.findtext('Container/RoleInstanceList/RoleInstance/InstanceId')
incarnation = xml_el.findtext('Incarnation')
print(f'ContainerId: {container_id}')
print(f'InstanceId: {instance_id}')
print(f'Incarnation: {incarnation}')

# Construct the XML response we need to send to Wireserver to report ready.
health = ElementTree.Element('Health')
goalstate_incarnation = ElementTree.SubElement(health, 'GoalStateIncarnation')
goalstate_incarnation.text = incarnation
container = ElementTree.SubElement(health, 'Container')
container_id_el = ElementTree.SubElement(container, 'ContainerId')
container_id_el.text = container_id
role_instance_list = ElementTree.SubElement(container, 'RoleInstanceList')
role = ElementTree.SubElement(role_instance_list, 'Role')
instance_id_el = ElementTree.SubElement(role, 'InstanceId')
instance_id_el.text = instance_id
health_second = ElementTree.SubElement(role, 'Health')
state = ElementTree.SubElement(health_second, 'State')
state.text = 'Ready'

out_xml = ElementTree.tostring(
    health,
    encoding='unicode',
    method='xml'
)
print('Sending the following data to Wireserver:')
print(out_xml)

wireserver_conn.request(
    'POST',
    '/machine?comp=health',
    headers={
        'x-ms-version': '2012-11-30',
        'Content-Type': 'text/xml;charset=utf-8',
        'x-ms-agent-name': 'custom-provisioning'
    },
    body=out_xml
)

resp = wireserver_conn.getresponse()
print(f'Response: {resp.status} {resp.reason}')

wireserver_conn.close()

Script de Bash

#!/bin/bash

attempts=1
until [ "$attempts" -gt 5 ]
do
    echo "obtaining goal state - attempt $attempts"
    goalstate=$(curl --fail -v -X 'GET' -H "x-ms-agent-name: azure-vm-register" \
                                        -H "Content-Type: text/xml;charset=utf-8" \
                                        -H "x-ms-version: 2012-11-30" \
                                           "http://168.63.129.16/machine/?comp=goalstate")
    if [ $? -eq 0 ]
    then
       echo "successfully retrieved goal state"
       retrieved_goal_state=true
       break
    fi
    sleep 5
    attempts=$((attempts+1))
done

if [ "$retrieved_goal_state" != "true" ]
then
    echo "failed to obtain goal state - cannot register this VM"
    exit 1
fi

container_id=$(grep ContainerId <<< "$goalstate" | sed 's/\s*<\/*ContainerId>//g' | sed 's/\r$//')
instance_id=$(grep InstanceId <<< "$goalstate" | sed 's/\s*<\/*InstanceId>//g' | sed 's/\r$//')

ready_doc=$(cat << EOF
<?xml version="1.0" encoding="utf-8"?>
<Health xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <GoalStateIncarnation>1</GoalStateIncarnation>
  <Container>
    <ContainerId>$container_id</ContainerId>
    <RoleInstanceList>
      <Role>
        <InstanceId>$instance_id</InstanceId>
        <Health>
          <State>Ready</State>
        </Health>
      </Role>
    </RoleInstanceList>
  </Container>
</Health>
EOF
)

attempts=1
until [ "$attempts" -gt 5 ]
do
    echo "registering with Azure - attempt $attempts"
    curl --fail -v -X 'POST' -H "x-ms-agent-name: azure-vm-register" \
                             -H "Content-Type: text/xml;charset=utf-8" \
                             -H "x-ms-version: 2012-11-30" \
                             -d "$ready_doc" \
                             "http://168.63.129.16/machine?comp=health"
    if [ $? -eq 0 ]
    then
       echo "successfully register with Azure"
       break
    fi
    sleep 5 # sleep to prevent throttling from wire server
done

Pasos genéricos (si no se usa Python o Bash)

Si la máquina virtual no tiene Python instalado ni tampoco disponible, puede reproducir esta lógica de script anterior mediante programación con los pasos siguientes:

  1. Recupere los valores ContainerId, InstanceId y Incarnation mediante el análisis de la respuesta de WireServer: curl -X GET -H 'x-ms-version: 2012-11-30' http://168.63.129.16/machine?comp=goalstate.

  2. Inserte los valores ContainerId, InstanceId y Incarnation analizados en el paso anterior y construya los siguientes datos XML:

    <Health>
      <GoalStateIncarnation>INCARNATION</GoalStateIncarnation>
      <Container>
        <ContainerId>CONTAINER_ID</ContainerId>
        <RoleInstanceList>
          <Role>
            <InstanceId>INSTANCE_ID</InstanceId>
            <Health>
              <State>Ready</State>
            </Health>
          </Role>
        </RoleInstanceList>
      </Container>
    </Health>
    
  3. Publique estos datos en WireServer: curl -X POST -H 'x-ms-version: 2012-11-30' -H "x-ms-agent-name: WALinuxAgent" -H "Content-Type: text/xml;charset=utf-8" -d "$REPORT_READY_XML" http://168.63.129.16/machine?comp=health

Automatización de la ejecución del código en el primer arranque

En esta demostración se usa systemd, que es el sistema de inicialización más habitual en las distribuciones de Linux modernas. Así pues, la forma más sencilla y nativa de garantizar que este mecanismo para informar de que se está listo se ejecute en el momento adecuado es crear una unidad de servicio de systemd. Puede agregar el siguiente archivo de unidad a /etc/systemd/system (en este ejemplo, el archivo de unidad se denomina azure-provisioning.service):

[Unit]
Description=Azure Provisioning

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /usr/local/azure-provisioning.py
ExecStart=/bin/bash -c "hostnamectl set-hostname $(curl \
    -H 'metadata: true' \
    'http://169.254.169.254/metadata/instance/compute/name?api-version=2019-06-01&format=text')"
ExecStart=/usr/bin/systemctl disable azure-provisioning.service

[Install]
WantedBy=multi-user.target

Este servicio de systemd realiza tres acciones para el aprovisionamiento básico:

  1. Informa a Azure de que está listo (para indicar que apareció correctamente).
  2. Cambia el nombre de la máquina virtual en función del nombre de la máquina virtual proporcionado por el usuario extrayendo estos datos de Azure Instance Metadata Service (IMDS). Tenga en cuenta que IMDS también proporciona otros metadatos de instancia, como las claves públicas SSH, por lo que puede establecer más de un nombre de host.
  3. Se deshabilita por su cuenta, de modo que solo se ejecute en el primer arranque y no en reinicios posteriores.

Con la unidad en el sistema de archivos, ejecute lo siguiente para su habilitación:

$ sudo systemctl enable azure-provisioning.service

Ahora la máquina virtual está lista para generalizarse y tener una imagen creada a partir de ella.

Finalización de la preparación de la imagen

De nuevo en la máquina de desarrollo, ejecute lo siguiente a fin de prepararse para la creación de imágenes a partir de la máquina virtual base:

$ az vm deallocate --resource-group demo1 --name demo1
$ az vm generalize --resource-group demo1 --name demo1

Asimismo, cree la imagen a partir de esta máquina virtual:

$ az image create \
    --resource-group demo1 \
    --source demo1 \
    --location eastus \
    --name demo1img

Ahora estamos listos para crear una nueva máquina virtual a partir de la imagen. También se puede usar para crear varias máquinas virtuales:

$ IMAGE_ID=$(az image show -g demo1 -n demo1img --query id -o tsv)
$ az vm create \
    --resource-group demo12 \
    --name demo12 \
    --location eastus \
    --ssh-key-value <ssh_pub_key_path> \
    --public-ip-address-dns-name demo12 \
    --image "$IMAGE_ID"
    --enable-agent false

Nota

Es importante establecer --enable-agent en false, ya que walinuxagent no existe en esta máquina virtual que se va a crear a partir de la imagen.

La máquina virtual se debe aprovisionar correctamente. Después de iniciar sesión en la máquina virtual recién aprovisionada, debería poder ver la salida del servicio systemd listo para el informe:

$ sudo journalctl -u azure-provisioning.service
-- Logs begin at Thu 2020-06-11 20:28:45 UTC, end at Thu 2020-06-11 20:31:24 UTC. --
Jun 11 20:28:49 thstringnopa systemd[1]: Starting Azure Provisioning...
Jun 11 20:28:54 thstringnopa python3[320]: Retrieving goal state from the Wireserver
Jun 11 20:28:54 thstringnopa python3[320]: ContainerId: 7b324f53-983a-43bc-b919-1775d6077608
Jun 11 20:28:54 thstringnopa python3[320]: InstanceId: fbb84507-46cd-4f4e-bd78-a2edaa9d059b._thstringnopa2
Jun 11 20:28:54 thstringnopa python3[320]: Sending the following data to Wireserver:
Jun 11 20:28:54 thstringnopa python3[320]: <Health><GoalStateIncarnation>1</GoalStateIncarnation><Container><ContainerId>7b324f53-983a-43bc-b919-1775d6077608</ContainerId><RoleInstanceList><Role><InstanceId>fbb84507-46cd-4f4e-bd78-a2edaa9d059b._thstringnopa2</InstanceId><Health><State>Ready</State></Health></Role></RoleInstanceList></Container></Health>
Jun 11 20:28:54 thstringnopa python3[320]: Response: 200 OK
Jun 11 20:28:56 thstringnopa bash[472]:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
Jun 11 20:28:56 thstringnopa bash[472]:                                  Dload  Upload   Total   Spent    Left  Speed
Jun 11 20:28:56 thstringnopa bash[472]: [158B blob data]
Jun 11 20:28:56 thstringnopa2 systemctl[475]: Removed /etc/systemd/system/multi-user.target.wants/azure-provisioning.service.
Jun 11 20:28:56 thstringnopa2 systemd[1]: azure-provisioning.service: Succeeded.
Jun 11 20:28:56 thstringnopa2 systemd[1]: Started Azure Provisioning.

Soporte técnico

Si implementa su propio código o agente de aprovisionamiento, contará con el soporte técnico de este código. El soporte técnico de Microsoft solo investigará aquellas incidencias relacionadas con las interfaces de aprovisionamiento no disponibles. Continuamente realizamos mejoras y cambios en esta área, por lo que debe supervisar los cambios en el Agente Linux de Azure y cloud-init para aprovisionar los cambios de API.

Pasos siguientes

Para más información, consulte Aprovisionamiento de Linux.