Terraform: Creación de Cluster de Máquinas Virtuales Windows en Azure

Article available only in Spanish

1. Introducción

En este artículo se muestra cómo crear un cluster de máquinas virtuales Windows con Terraform en Azure sin usar módulos externos.  

Si no estás familiarizado con Terraform, recomiendo eches un vistazo a los artículos previos de este blog listados a continuación, ya que se hace uso de diferentes elementos importantes previamente explicados en ellos:

2. Consideraciones Generales

Este artículo supone una ampliación al artículo anterior Terraform: Creación de Máquinas Virtuales Windows en Azure. En este artículo se añaden los componentes necesarios para crear dos máquinas virtuales en un mismo availability set de Azure, lo cual puede considerarse como un cluster de servidores.

La inclusión de varias máquinas virtuales en cluster aumenta la disponiblidad de nuestro sistema mediante la ubicación de las máquinas en diferentes dominios de actualización (update domain) y dominios de fallo (fault domain). En el primer caso, se asegura que las máquinas no existentes en el dominio que se está actualizando (mantenimiento de hardware, software, etc.) estarán disponibles, nunca se actualizan los dominios al mismo tiempo. En el segundo caso las máquinas en cada dominio están conectadas a racks físicos diferentes, por lo que si falla un fault domain, las máquinas virtuales asociadas a otros dominios no serán afectadas.

En este artículo se muestra como crear dos máquinas virtuales en el mismo availability set, cada una de ellas en un update domain y fault domain diferente. En artículos posteriores se mostrará también cómo crear un load balancer con Terraform capaz de distribuir tráfico TCP sobre el cluster de máquinas virtuales.

3. Configuración de Terraform

La configuración de nuestro proyecto de Terraform es la misma a la ya explicada en el artículo Terraform: Creación de Máquinas Virtuales Windows en Azure en lo que se refiere a la estructura del proyecto, fichero variables.tf y fichero terraform.tfvars por lo que no volveré a incluirlos. Puedes verlos en el enlace anterior.

Repecto al fichero main.tf  hay alguna modificación para incluir las dos máquinas virtuales (en lugar de una sola) junto con las respectivas tarjetas de red (NICs) y direcciones IP públicas, además de la inclusión del availability set:

# Referencia al proveedor de Azure y configuración del service principal. El valor de client_secret será asignado a traves de la variable de entorno ARM_CLIENT_SECRET.

provider "azurerm" {
  # Non-beta version >= 2.5.0 and < 3.0.0
  version = "~>2.5"

  subscription_id = var.azure_terraform_sp["subscription_id"]
  client_id       = var.azure_terraform_sp["client_id"]
  tenant_id       = var.azure_terraform_sp["tenant_id"]

  # Required. Leave it empty if non-used.
  features {}
}

# Referencia al blob (key) del contenedor (container_name) en la storage account de Azure (storage_account_name) para almacenar de forma remota y centralizada el estado de nuestro proyecto de Terraform. Este bloque no permite el uso de variables. El valor del atributo access_key es asignado mediante la variable de entorno ARM_ACCESS_KEY.
terraform {
  backend "azurerm" {
    resource_group_name  = "jm-inn-core-rg"
    storage_account_name = "jminnstgacc"
    container_name       = "terraform"
    key                  = "tf-learn-vms-cluster-hcl.tfstate"
  }
}

# Crea grupo de recursos donde se incluirán la máquina virtual y resto de componentes
resource "azurerm_resource_group" "rg" {
  name     = var.azure_new_resource_group["name"]
  location = var.azure_new_resource_group["location"]
  tags     = var.azure_new_resource_group_tags
}

# Crea una Azure Virtual Network para ubicar la subred que contendrá las máquinas virtuales. 
resource "azurerm_virtual_network" "vnet" {
  name                = "${var.prefix}-vnet"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  tags                = var.azure_new_resource_group_tags
}

# Crea una subnet en la Azure Virtual Network anterior para ubicar las máquinas virtuales. En Azure es obligatorio crear una subred donde ubicar las máquinas virtuales.
resource "azurerm_subnet" "subnet1" {
  name                 = "${var.prefix}-subnet1"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefix       = "10.0.0.0/24"
}

# Crea un par de direcciones IP públicas que serán asignadas a las tarjetas de red de las máquinas virtuales. De este modo podremos conectar vía RDP o HTTP con cada dirección IP pública de las máquinas virtuales.
resource "azurerm_public_ip" "publicip" {
  count               = 2
  name                = "${var.prefix}-publicip-${count.index}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  allocation_method   = "Dynamic"
  tags                = var.azure_new_resource_group_tags
}

# Crea tarjetas de red (NIC) a asociar a cada máquina virtual
resource "azurerm_network_interface" "nic" {
  count               = 2
  name                = "${var.prefix}-nic${count.index}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
 
  ip_configuration {
    name                          = "${var.prefix}-nic-conf"
    subnet_id                     = azurerm_subnet.subnet1.id
    private_ip_address_allocation = "dynamic"
    public_ip_address_id          = azurerm_public_ip.publicip[count.index].id
  }
}

# Crea un Network Security Group para controlar el tráfico permitido a nivel de subred. Se configura para aceptar tráfico TCP entrante en los puertos 3389 (RDP) y 80 (HTTP).
resource "azurerm_network_security_group" "nsg1" {
  name                = "${var.prefix}-nsg1"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  security_rule {
    name                       = "RDP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "3389"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "TCP_80"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  tags = var.azure_new_resource_group_tags
}

# Se asocian la Network Security Group (NSG) y la subred creada anteriormente. Así, las reglas contenidas en la NSG para filtrar tráfico aplican a toda la subred.
resource "azurerm_subnet_network_security_group_association" "association" {
  subnet_id                 = azurerm_subnet.subnet1.id
  network_security_group_id = azurerm_network_security_group.nsg1.id
}

# Crea Availability Set para incluir las máquinas virtuales con dos update domain y dos fault domain
resource "azurerm_availability_set" "avset1" {
  name                         = "${var.prefix}-avset1"
  location                     = azurerm_resource_group.rg.location
  resource_group_name          = azurerm_resource_group.rg.name
  platform_fault_domain_count  = 2
  platform_update_domain_count = 2
  managed                      = true
}

# Data source para referenciar Azure Key Vault con secretos de usuario y contraseña
data "azurerm_key_vault" "kv" {
  name                = var.azure_key_vault_name
  resource_group_name = var.azure_key_vault_resource_group_name
}

# Data Source para obtener nombre de usuario administrador guardado en Azure Key Vault
data "azurerm_key_vault_secret" "vm_username" {
  name         = "vm-username-default"
  key_vault_id = data.azurerm_key_vault.kv.id
}

# Data Source para obtener password de usuario administrador guardado en Azure Key Vault
data "azurerm_key_vault_secret" "vm_password" {
  name         = "vm-password-default"
  key_vault_id = data.azurerm_key_vault.kv.id
}

# Crea el cluster de máquinas virtuales
resource "azurerm_windows_virtual_machine" "vms" {
  count                 = 2
  name                  = "${var.prefix}-${count.index}"
  resource_group_name   = azurerm_resource_group.rg.name
  location              = azurerm_resource_group.rg.location
  availability_set_id   = azurerm_availability_set.avset1.id
  admin_username        = data.azurerm_key_vault_secret.vm_username.value
  admin_password        = data.azurerm_key_vault_secret.vm_password.value
  size                  = var.vm_size
  network_interface_ids = [element(azurerm_network_interface.nic.*.id, count.index)]

  os_disk {
    name                 = "${var.prefix}_${count.index}"
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2019-Datacenter"
    version   = "latest"
  }

  tags = var.azure_new_resource_group_tags
}

# Instala IIS mediante una extensión de máquina virtual.
resource "azurerm_virtual_machine_extension" "iis-windows-vm-extension" {
  count                = 2
  name                 = "${var.prefix}-vm-${count.index}-extension"
  virtual_machine_id   = azurerm_windows_virtual_machine.vms[count.index].id
  publisher            = "Microsoft.Compute"
  type                 = "CustomScriptExtension"
  type_handler_version = "1.9"
  settings             = <<SETTINGS
    { 
      "commandToExecute": "powershell Install-WindowsFeature -name Web-Server -IncludeManagementTools;"
    } 
  SETTINGS
  tags                 = var.azure_new_resource_group_tags
}

Como aspectos destacados, fíjate en los siguientes puntos:

  • Uso del atributo count (con valor 2) cuando hemos creado los recursos de tipo azurerm_public_ip (IPs públicas), azurerm_network_interface (tarjetas de red), azurerm_windows_virtual_machine (máquinas virtuales) y azurerm_virtual_machine_extension (extensiones de máquinas virtuales). 
  • Creación del nuevo recurso azurerm_availability_set para crear el availability set con dos update domain y dos fault domain.
  • Asignación de las máquinas virtuales al availability set anterior mediante la asignación availability_set_id   = azurerm_availability_set.avset1.id. Azure automáticamente coloca cada máquina virtual en los dominios adecuados. En este caso cada máquina virtual iría ubicada en un update domain y fault domain diferente.

4. Ejecución de Terraform y Verificación en Azure

La ejecución de nuestro proyecto de Terraform sigue el mismo proceso que el ya explicado en el artículo Terraform: Creación de Máquinas Virtuales Windows en Azure por lo que iré directamente a verificar el resultado de la creación de infraestructura en Azure:

En la imagen superior vemos cómo se han creado las dos máquinas virtuales vm-cl-hcl-0vm-cl-hcl-1 junto con los componentes relacionados correspondientes. También vemos la existencia del availability set vm-cl-hcl-avset1. Si accedemos al availability set, podemos comprobar la existencia de dos fault domains y dos update domains, con cada máquina virtual en fault y update domains diferentes:

Referencias

Add comment