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

Article available only in Spanish

1. Introducción

En este artículo se muestra cómo crear una máquina virtual Windows con Terraform en Azure sin usar módulos externos. Si ya estás acostumbrado a crear máquinas virtuales usando el portal de Azure, Azure CLI o Powershell verás que es realmente sencillo. 

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

  • Nuestro proyecto de Terraform usará un service principal como forma de autenticación en Azure (sección provider en main.tf). Por tanto, una vez creado necesitas referenciar tus propios datos de subscription_id, tenant_id, client_id en el fichero terraform.tfvars. Además deberas crear una variable de entorno llamada ARM_CLIENT_SECRET con el valor de la contraseña del service principal. Más información en Terraform: Estructura de Ficheros, Estado Remoto y Autenticación en Azure.
  • La información de estado será almacenada de forma remota en un container de Azure (sección terraform en main.tf). Por tanto, una vez creada la storage account y el container necesitas referenciar tus propios nombres. Además deberás crear una variable de entorno llamada ARM_ACCESS_KEY con el valor de una de las dos claves de acceso a la storage account. Más información en Terraform: Estructura de Ficheros, Estado Remoto y Autenticación en Azure.
  • La máquina virtual contendrá la última versión del sistema operativo Windows Data Center 2019, su size será de tamaño pequeño Standard_B2s, permitirá conexiones entrantes en los puertos 80 (HTTP) y 3389 (RDP), tendrá instalado un servidor IIS con el contenido por defecto y dispondrá de una dirección ip pública asociada a su tarjeta de red (NIC).
  • Una vez creada la máquina virtual seremos capaces de conectarnos a ella vía RDP con las credenciales del administrador y conectarnos con el IIS instalado mediante el uso de cualquier browser para introducir la dirección ip pública asignada.

3. Configuración de Terraform

Esta es la estructura de ficheros del proyecto de Terraform para crear nuestra máquina virtual:

A continuación se detalla el contenido del fichero variables.tf  junto con las explicaciones de cada bloque de código:

# Variable que contiene los datos asociados al service principal sobre el que se autentica Terraform en Azure (subscription_id, tenant_id y client_id)
variable "azure_terraform_sp" {
  type        = map(string)
  default     = {}
  description = "Contains service principal over which security permissions are set"
}
 
# Variable que contiene el nombre y la ubicación del grupo de recursos en Azure donde se creará la máquina virtual y resto de componentes relacionados
variable "azure_new_resource_group" {
  type        = map(string)
  default     = {}
  description = "Contains main properties about new resource group holding all resources"
}
 
# Variable que contiene etiquetas asociadas al grupo de recursos anterior
variable "azure_new_resource_group_tags" {
  type        = map(string)
  default     = {}
  description = "Tags to categorize new resource group and other resources within it"
}
 
# Variable que sirve para asignar un prefijo al nombre de los componentes a crear en nuestro proyecto.
variable "prefix" {
  type        = string
  description = "Prefix to include in resource names for making them more consistent"
}
 
# Variable que contiene el tamaño o tipo (size) de nuestra máquina virtual
variable "vm_size" {
  type        = string
  default     = "Standard_B2s"
  description = "Size for Virtual Machine"
}

 

# Variable que contiene el nombre del Azure Key Vault usado para almacenar el nombre de usuario y contraseña del usuario administrador de la máquina virtual
variable "azure_key_vault_name"{
  type        = string
  description = "Azure Key Vault name holding secrets for admin username and password"
}
 
# Variable que contiene el grupo de recursos donde se encuentra el Azure Key Vault
variable "azure_key_vault_resource_group_name"{
  type        = string
  description = "Resource group name holding Azure Key Vault"
}

 

Este es el contenido del fichero terraform.tfvars para dar valores a las variables anteriores:

# Asigna datos asociados al service principal sobre el que se autentica Terraform en Azure (subscription_id, tenant_id y client_id). El valor de client_secret será asignado a traves de la variable de entorno ARM_CLIENT_SECRET.
azure_terraform_sp = {
  "subscription_id" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  "client_id"       = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  "tenant_id"       = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

# Asigna nombre y ubicación al grupo de recursos en Azure donde se creará la máquina virtual y resto de componentes relacionados
azure_new_resource_group = {
  "name"     = "jm-inn-tf-learn-vms-intro-rg"
  "location" = "westeurope"
}

# Asigna etiquetas al grupo de recursos
azure_new_resource_group_tags = {
  "Category"    = "Terraform"
  "Subcategory" = "Learning"
  "Subject"     = "Virtual Machines"
}
 
# Asigna prefijo para nombres de los componentes
prefix = "vm-intro"
 
# Asigna el nombre del Azure Key Vault usado para almacenar el nombre de usuario y contraseña del usuario administrador de la máquina virtual
azure_key_vault_name = "jm-inn-main-kv"
 
# Asigna el nombre del grupo de recursos donde se encuentra el Azure Key Vault
azure_key_vault_resource_group_name = "jm-inn-core-rg"

 

Por último el contenido del fichero main.tf es el mostrado y explicado a continuación:

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

  # It let us configure certain properties for Azure resources. 
  # 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-intro.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" "vnet1" {
  name                = "${var.prefix}-vnet1"
  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.vnet1.name
  address_prefix       = "10.0.0.0/24"
}

# Crea una dirección IP pública que será asignada a la tarjeta de red de la máquina virtual. De este modo podremos conectar vía RDP o HTTP con esta dirección IP pública a la máquina virtual.
resource "azurerm_public_ip" "publicip1" {
  name                = "${var.prefix}-publicip1"
  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 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 creadas 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 la tarjeta de red o Virtual Network Interface Card (NIC) que se asociará a la máquina virtual. La NIC es asociada a la dirección IP pública creada con anterioridad.
resource "azurerm_network_interface" "nic1" {
  name                = "${var.prefix}-nic1"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  ip_configuration {
    name                          = "nic-config1"
    subnet_id                     = azurerm_subnet.subnet1.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.publicip1.id
  }

  tags = var.azure_new_resource_group_tags
}

# 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 la Máquina Virtual
resource "azurerm_windows_virtual_machine" "vm1" {
  name                = "${var.prefix}-1"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  size                = var.vm_size
  admin_username      = data.azurerm_key_vault_secret.vm_username.value
  admin_password      = data.azurerm_key_vault_secret.vm_password.value
  network_interface_ids = [
    azurerm_network_interface.nic1.id,
  ]

  os_disk {
    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" {
  depends_on           = [azurerm_windows_virtual_machine.vm1]
  name                 = "${var.prefix}-vm1-extension"
  virtual_machine_id   = azurerm_windows_virtual_machine.vm1.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
}

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

Una vez finalizado nuestro código de Terraform, podemos ejecutarlo para crear la infraestructura en Azure. Los pasos son ya conocidos si has leído otros artículos relacionados:

1. Ejecución de terraform init en el directorio donde se encuentra nuestro proyecto para inicializar Terraform. Descarga de plugins con las versiones necesarias (azurerm).

2. Ejecución de terraform plan para obtener una descripción de los recursos que Terraform va a añadir, modificar o eliminar. En este caso todos los recursos son nuevos, así que Terraform nos informa que 9 recursos serán creados, 0 actualizados y 0 eliminados. El plan de ejecución completo es el siguiente:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# azurerm_network_interface.nic1 will be created
+ resource "azurerm_network_interface" "nic1" {
+ applied_dns_servers = (known after apply)
+ dns_servers = (known after apply)
+ enable_accelerated_networking = false
+ enable_ip_forwarding = false
+ id = (known after apply)
+ internal_dns_name_label = (known after apply)
+ location = "westeurope"
+ mac_address = (known after apply)
+ name = "vm-intro-nic1"
+ private_ip_address = (known after apply)
+ private_ip_addresses = (known after apply)
+ resource_group_name = "jm-inn-tf-learn-vms-intro-rg"
+ tags = {
+ "Category" = "Terraform"
+ "Subcategory" = "Learning"
+ "Subject" = "Virtual Machines"
}
+ virtual_machine_id = (known after apply)

+ ip_configuration {
+ name = "nic-config1"
+ primary = (known after apply)
+ private_ip_address = (known after apply)
+ private_ip_address_allocation = "dynamic"
+ private_ip_address_version = "IPv4"
+ public_ip_address_id = (known after apply)
+ subnet_id = (known after apply)
}
}

# azurerm_network_security_group.nsg1 will be created
+ resource "azurerm_network_security_group" "nsg1" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "vm-intro-nsg1"
+ resource_group_name = "jm-inn-tf-learn-vms-intro-rg"
+ security_rule = [
+ {
+ access = "Allow"
+ description = ""
+ destination_address_prefix = "*"
+ destination_address_prefixes = []
+ destination_application_security_group_ids = []
+ destination_port_range = "3389"
+ destination_port_ranges = []
+ direction = "Inbound"
+ name = "RDP"
+ priority = 100
+ protocol = "Tcp"
+ source_address_prefix = "*"
+ source_address_prefixes = []
+ source_application_security_group_ids = []
+ source_port_range = "*"
+ source_port_ranges = []
},
+ {
+ access = "Allow"
+ description = ""
+ destination_address_prefix = "*"
+ destination_address_prefixes = []
+ destination_application_security_group_ids = []
+ destination_port_range = "80"
+ destination_port_ranges = []
+ direction = "Inbound"
+ name = "TCP_80"
+ priority = 200
+ protocol = "Tcp"
+ source_address_prefix = "*"
+ source_address_prefixes = []
+ source_application_security_group_ids = []
+ source_port_range = "*"
+ source_port_ranges = []
},
]
+ tags = {
+ "Category" = "Terraform"
+ "Subcategory" = "Learning"
+ "Subject" = "Virtual Machines"
}
}

# azurerm_public_ip.publicip1 will be created
+ resource "azurerm_public_ip" "publicip1" {
+ allocation_method = "Dynamic"
+ fqdn = (known after apply)
+ id = (known after apply)
+ idle_timeout_in_minutes = 4
+ ip_address = (known after apply)
+ ip_version = "IPv4"
+ location = "westeurope"
+ name = "vm-intro-publicip1"
+ resource_group_name = "jm-inn-tf-learn-vms-intro-rg"
+ sku = "Basic"
+ tags = {
+ "Category" = "Terraform"
+ "Subcategory" = "Learning"
+ "Subject" = "Virtual Machines"
}
}

# azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "jm-inn-tf-learn-vms-intro-rg"
+ tags = {
+ "Category" = "Terraform"
+ "Subcategory" = "Learning"
+ "Subject" = "Virtual Machines"
}
}

# azurerm_subnet.subnet1 will be created
+ resource "azurerm_subnet" "subnet1" {
+ address_prefix = "10.0.0.0/24"
+ enforce_private_link_endpoint_network_policies = false
+ enforce_private_link_service_network_policies = false
+ id = (known after apply)
+ name = "vm-intro-subnet1"
+ resource_group_name = "jm-inn-tf-learn-vms-intro-rg"
+ virtual_network_name = "vm-intro-vnet1"
}

# azurerm_subnet_network_security_group_association.association will be created
+ resource "azurerm_subnet_network_security_group_association" "association" {
+ id = (known after apply)
+ network_security_group_id = (known after apply)
+ subnet_id = (known after apply)
}

# azurerm_virtual_machine_extension.iis-windows-vm-extension will be created
+ resource "azurerm_virtual_machine_extension" "iis-windows-vm-extension" {
+ id = (known after apply)
+ name = "vm-intro-vm1-extension"
+ publisher = "Microsoft.Compute"
+ settings = jsonencode(
{
+ commandToExecute = "powershell Install-WindowsFeature -name Web-Server -IncludeManagementTools;"
}
)
+ tags = {
+ "Category" = "Terraform"
+ "Subcategory" = "Learning"
+ "Subject" = "Virtual Machines"
}
+ type = "CustomScriptExtension"
+ type_handler_version = "1.9"
+ virtual_machine_id = (known after apply)
}

# azurerm_virtual_network.vnet1 will be created
+ resource "azurerm_virtual_network" "vnet1" {
+ address_space = [
+ "10.0.0.0/16",
]
+ id = (known after apply)
+ location = "westeurope"
+ name = "vm-intro-vnet1"
+ resource_group_name = "jm-inn-tf-learn-vms-intro-rg"
+ tags = {
+ "Category" = "Terraform"
+ "Subcategory" = "Learning"
+ "Subject" = "Virtual Machines"
}

+ subnet {
+ address_prefix = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ security_group = (known after apply)
}
}

# azurerm_windows_virtual_machine.vm1 will be created
+ resource "azurerm_windows_virtual_machine" "vm1" {
+ admin_password = (sensitive value)
+ admin_username = "jamuro"
+ allow_extension_operations = true
+ computer_name = (known after apply)
+ enable_automatic_updates = true
+ id = (known after apply)
+ location = "westeurope"
+ max_bid_price = -1
+ name = "vm-intro-1"
+ network_interface_ids = (known after apply)
+ priority = "Regular"
+ private_ip_address = (known after apply)
+ private_ip_addresses = (known after apply)
+ provision_vm_agent = true
+ public_ip_address = (known after apply)
+ public_ip_addresses = (known after apply)
+ resource_group_name = "jm-inn-tf-learn-vms-intro-rg"
+ size = "Standard_B2s"
+ tags = {
+ "Category" = "Terraform"
+ "Subcategory" = "Learning"
+ "Subject" = "Virtual Machines"
}
+ virtual_machine_id = (known after apply)

+ os_disk {
+ caching = "ReadWrite"
+ disk_size_gb = (known after apply)
+ name = (known after apply)
+ storage_account_type = "Standard_LRS"
+ write_accelerator_enabled = false
}

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

Plan: 9 to add, 0 to change, 0 to destroy.

3. Ejecución de terraform apply. Una vez verificado el plan de ejecución trazado, ejecutamos este comando para crear la infraestructura. Si todo funcionan de forma correcta, obtenemos un mensaje similar al siguiente:

4. Comprobamos en Azure los recursos creados junto con la máquina virtual vm-intro-1 incluidos en el grupo de recursos que configuramos en Terraform:

5. Accedemos a la dirección IP pública asociada a la máquina virtual:

6. Verificamos la obtención de la página por defecto de IIS al navegar a la dirección IP pública asociada a la máquina virtual. Como se puede ver en la imagen, obtenemos la página por defecto iisstart.htm:

Así, tanto la instalación de la extensión de IIS como las reglas de filtrado de tráfico HTTP hacia nuestra máquina virtual funcionan correctamente :)

7. Si ejecutas este código de ejemplo, verás que el acceso vía RDP también funciona de forma correcta. Tan solo tienes que realizar los siguientes pasos:

- Descargar el acceso directo vía RDP mediante Connect -> RDP -> Download RDP file de la máquina virtual vm-intro-1 creada:

- Ejecutar el fichero descargado e introducir el nombre de usuario y contraseña almacenado en tu Azure Key Vault:

En mi caso, funcionó correctamente.

8. Por último, una vez verificado todo y si no vamos a trabajar más con los nuevos recursos procedemos a eliminarlos mediante la ejecución del comando terraform destroy.

Referencias

Add comment