Terraform: Creación de Load Balancer sobre Cluster de Máquinas Virtuales en Azure

Article available only in Spanish

1. Introducción

En este artículo se muestra cómo crear un Load Balancer sobre un cluster de Máquinas Virtuales Windows con Terraform en Azure.  

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

En artículos anteriores vimos como crear clusters de máquinas virtuales Windows usando simplemente recursos del proveedor Azure para Terraform (azurerm) o mediante el uso de módulos de Terraform Registry. En cualquiera de los dos casos el resultado final era la creación de un availability set con dos update domains y dos fault domains sobre los cuales se ubicaban las máquinas virtuales como cluster de alta disponibilidad ante actualizaciones o fallos de hardware:

Esto está bien, pero nos falta algo más. En concreto, echo de menos algún componente que nos permita distribuir tráfico sobre ambas máquinas virtuales de forma balanceada y teniendo en cuenta el estado de salud de cada máquina. En otras palabras, si las dos máquinas tienen buen estado de salud, el tráfico debería ser distribuido de forma equitativa según algún algoritmo predefinido y si alguna de ellas está inactiva o saturada el tráfico debería ser redirigido automáticamente a la máquina con buen estado de salud. Esto es precisamente lo que se pretende con la inclusión de un Load Balancer. En concreto, la distribución balanceada de tráfico a nivel TCP sobre un backend de servidores compuesto por nuestras máquinas virtuales con supervisión previa del estado de salud de cada servidor para ajustar la distribución del tráfico.

Por tanto, el objetivo de este artículo será mostrar como ampliar la infraestructura previamente creada en otros artículos anteriores para incluir un Load Balancer. Además veremos como usar este componente para acceder vía RDP a las máquinas virtuales mediante la definición de reglas NAT (Network Address Translation). Este procedimiento nos servirá para evitar la asignación directa de direcciones IP públicas a cada máquina virtual.

3. Configuración de Terraform

Como en los artículos anteriores de Terraform sobre creación de clusters de máquinas virtuales, la configuración de nuestro proyecto actual es la misma a la ya explicada en dichos artículos en lo que se refiere a la estructura del proyecto, fichero variables.tf y fichero terraform.tfvars por lo que no se volverá a incluir aquí. Puedes verlos en los enlaces referenciados en el capítulo 1.

Este es el contenido de nuestro fichero main.tf  con las explicaciones de cada bloque:

# 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"

   # Configure service principal
   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á la máquina virtual.
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 la máquina virtual. En Azure es obligatorio crear una subred donde ubicar la máquina virtual.
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 una dirección IP pública para el Load balancer que distribuye tráfico sobre las máquinas virtuales
resource "azurerm_public_ip" "lb_public_ip1" {
   name = "${var.prefix}-lb-public-ip1"
   location = azurerm_resource_group.rg.location
   resource_group_name = azurerm_resource_group.rg.name
   allocation_method = "Static"
   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"
   }
}

# 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 asocia 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 como cluster de alta disponibilidad
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 el Azure Key Vault que contiene nuestros 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 el 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 el 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
}

# Crea Load Balancer para distribuir tráfico sobre las máquinas virtuales
resource "azurerm_lb" "lb1" {
   name = "${var.prefix}-lb1"
   location = azurerm_resource_group.rg.location
   resource_group_name = azurerm_resource_group.rg.name

# Configura el Frontend del Load balancer con su dirección IP pública
frontend_ip_configuration {
   name = "${var.prefix}-lb-frontend-conf1"
   public_ip_address_id = azurerm_public_ip.lb_public_ip1.id
   }
}

# Crea backend de soporte al Load Balancer y que contendrá las máquinas virtuales
resource "azurerm_lb_backend_address_pool" "backend1" {
   resource_group_name = azurerm_resource_group.rg.name
   loadbalancer_id = azurerm_lb.lb1.id
   name = "${var.prefix}-backend1"
}

# Configura el backend para incluir las máquinas virtuales vía NICs
resource "azurerm_network_interface_backend_address_pool_association" "backend_association" {
   count = 2
   ip_configuration_name = "${var.prefix}-nic-conf"
   network_interface_id = azurerm_network_interface.nic[count.index].id
   backend_address_pool_id = azurerm_lb_backend_address_pool.backend1.id
}

# Crea regla en el Load Balancer para dirigir tráfico HTTP al backend
resource "azurerm_lb_rule" "lb_http_rule" {
   resource_group_name = azurerm_resource_group.rg.name
   loadbalancer_id = azurerm_lb.lb1.id
   name = "HTTPRule"
   protocol = "Tcp"
   frontend_port = 80
   backend_port = 80
   frontend_ip_configuration_name = "${var.prefix}-lb-frontend-conf1"
   backend_address_pool_id = azurerm_lb_backend_address_pool.backend1.id
   probe_id = azurerm_lb_probe.probe1.id
   depends_on = [azurerm_lb_probe.probe1]
}

# Check en el Load Balancer para comprobar disponiblidad y estado de salud de cada máquina virtual (health probe)
resource "azurerm_lb_probe" "probe1" {
   resource_group_name = azurerm_resource_group.rg.name
   loadbalancer_id = azurerm_lb.lb1.id
   name = "HTTP"
   port = 80
}

# Reglas NAT (Network Address Translation) en el Load Balancer para permitir acceso RDP (puerto 3389) mediante uso externo de otros puertos (50001, 50002)
resource "azurerm_lb_nat_rule" "nat_rule" {
   count = 2
   resource_group_name = azurerm_resource_group.rg.name
   loadbalancer_id = azurerm_lb.lb1.id
   name = "RDPAccess-${count.index}"
   protocol = "Tcp"
   frontend_port = 50001 + count.index
   backend_port = 3389
   frontend_ip_configuration_name = "${var.prefix}-lb-frontend-conf1"
}

# Configura asociación entre NICs (VMs) y reglas NAT
resource "azurerm_network_interface_nat_rule_association" "nat_rule_association" {
   count = 2
   ip_configuration_name = "${var.prefix}-nic-conf"
   network_interface_id = azurerm_network_interface.nic[count.index].id
   nat_rule_id = azurerm_lb_nat_rule.nat_rule[count.index].id
}

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 Cluster de Máquinas Virtuales Windows por lo que iré directamente a verificar el resultado de la creación de infraestructura en Azure tras la ejecución de los comandos terraform init, terraform plan y terraform apply.

Esta es la lista de recursos más importantes en relación al tema que nos ocupa creados en Azure:

Como se ve se ha creado un Load Balancer llamado vm-cl-lb-lb1 de forma adicional al cluster de máquinas virtuales formado por el availability set vm-cl-lb-avset1 y las máquinas virtuales vm-cl-lb-0 y vm-cl-lb-1. No aparece en la lista pero también se crea una Azure Virtual Network para conectar las tarjetas de red (NIC) de cada máquina virtual. En la siguiente imagen se puede ver de forma más clara el esquema de componentes de la red creada:

Esta es la información principal de nuestro Load Balancer. Se referencia el backend pool, health probe para evaluar el estado de salud de cada máquina virtual en el backend pool, regla de balanceo de carga para distribuir tráfico TCP en el puerto 80 desde el frontend del Load Balancer al backend pool, reglas NAT para proporcionar servicios de port-forwarding y la dirección IP pública del Load Balancer sobre la cual estableceremos todas las comunicaciones (HTTP, RDP):

El Backend Pool asociado al Load Balancer está formado por el cluster de máquinas virtuales vm-cl-lb-0 y vm-cl-lb-1 como se puede apreciar en la siguiente imagen. Las máquinas virtuales no tienen ninguna dirección IP pública asignada, solo direcciones IP privadas:

Las reglas del Load Balancer para distribuir tráfico entrante son las siguientes:

Unicamente existe una regla denominada HTTPRule cuyas funciones son:

  • Permanecer a la escucha de tráfico TCP en el puerto 80 en la dirección IP pública asociada al Load Balancer.
  • Reenviar el tráfico entrante en el Load Balancer según el punto anterior al backend de servidores de forma balanceada. El tráfico se reenvía mediante una nueva comunicación TCP en el puerto 80 de modo que los IIS alojados en las máquinas virtuales puedan responder a estas peticiones. El tráfico que llega a las máquinas virtuales lo hace por medio del Load Balancer, de hecho no hemos creado direcciones IP públicas para ellas.
  • Consultar la regla de chequeo (health probe) de evaluación de estado de salud de las máquinas virtuales, para descartar el envío a aquellas en estado no disponible debido a fallos o sobrecarga.

Al acceder vía browser a la dirección IP pública del Load Balancer, éste distribuirá el tráfico de forma balanceada según un algoritmo de tipo hash basado en los siguientes parámetros de la comunicación:

  • IP Origen
  • Puerto Origen
  • IP Destino
  • Puerto Destino
  • Protocolo para mapear flujos TCP entrantes a backend de servidores

En nuestro caso la dirección IP destino (única IP pública del Load Balancer), el puerto destino (80) y el protocolo (TCP) serán siempre los mismos pero la dirección IP Origen y el puerto origen  será distinto según el cliente y la conexión. Por ello, el tráfico será distribuido a diferentes máquinas en el backend pool. Tienes explicaciones mucho más detalladas en la documentación del modo de distribución de Azure Load Balancer por lo que no me voy a extender más si bien te recomiendo le eches un vistazo al concepto de session affinity que te permitirá distribuir tráfico entrante de una IP origen siempre a la misma IP destino a través del Load Balancer. Esto puede ser necesario en algún escenario concreto.

Las reglas NAT (Network Address Translation) asociadas al Load Balancer son las siguientes:

En este caso las reglas NAT nos permiten "redireccionar" o "reenviar" tráfico IP de modo que el Load Balancer sea capaz de escuchar tráfico TCP en una dirección IP y puerto para crear una nueva conexión TCP en otra dirección IP y puerto. Esta funcionalidad nos permite acceder a las máquinas virtuales internas por medio del Load Balancer mediante su dirección IP pública y la configuración de varios puertos:

IP Pública + Puerto 50001 -> IP Privada 10.0.0.4 (vm-cl-lb-0) + Puerto 3389 (RDP)

IP Pública + Puerto 50002 -> IP Privada 10.0.0.5 (vm-cl-lb-1) + Puerto 3389 (RDP)

Para verificar el correcto funcionamiento de las regals NAT he creado dos ficheros con las conexiones RDP a la dirección IP pública del Load Balancer en los puertos 50001 y 50002:

Contenido del fichero vm-cl-lb-50001.rdp:

full address:s:52.233.157.69:50001
prompt for credentials:i:1
administrative session:i:1

Contenido del fichero vm-cl-lb-50002.rdp:

full address:s:52.233.157.69:50002
prompt for credentials:i:1
administrative session:i:1

Al acceder con cada uno de ellos ambos funcionan correctamente :)

 

Add comment