Introducción

Microsoft Azure es el segundo proveedor de nube más grande del mundo y tiene particularidades propias que lo diferencian de AWS en cuanto a organización y terminología. En este módulo aprenderás a configurar el provider AzureRM para Terraform, a autenticarte de forma segura y a crear los recursos más importantes de Azure.

También verás una tabla comparativa entre AWS y Azure que te ayudará a trasladar tus conocimientos de un proveedor al otro.


Configuración del Provider AzureRM

El provider oficial de Azure para Terraform se llama azurerm y es mantenido por HashiCorp en colaboración con Microsoft.

# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      # Fuente oficial del provider de Azure
      source  = "hashicorp/azurerm"
      # ~> 3.0 permite 3.x pero no 4.x
      version = "~> 3.0"
    }
  }
}

# Bloque de configuración del provider
provider "azurerm" {
  # El bloque features {} es OBLIGATORIO aunque esté vacío
  # Sin él, Terraform dará un error de configuración
  features {
    # Opciones de comportamiento del provider
    # Por ejemplo, para grupos de recursos:
    resource_group {
      # Si es true, permite eliminar un Resource Group aunque contenga recursos
      # que no están en el estado de Terraform
      prevent_deletion_if_contains_resources = false
    }

    # Para máquinas virtuales:
    virtual_machine {
      # Si es true, elimina el disco OS cuando se elimina la VM
      delete_os_disk_on_deletion = true
      # Si es true, apaga la VM de forma segura antes de destruirla
      graceful_shutdown = false
    }

    # Para Key Vaults:
    key_vault {
      # Si es true, recupera un Key Vault eliminado suavemente (soft delete)
      # en lugar de crear uno nuevo
      recover_soft_deleted_key_vaults = true
      # Si es true, purga el Key Vault al destruirlo (saltarse la protección)
      purge_soft_delete_on_destroy = true
    }
  }
}

Configuración con variables

# variables.tf
variable "subscription_id" {
  description = "ID de la suscripción de Azure"
  type        = string
  # No pongas el valor aquí, usa variables de entorno o un archivo .tfvars
}

variable "tenant_id" {
  description = "ID del tenant de Azure Active Directory"
  type        = string
}

variable "client_id" {
  description = "ID de la aplicación del Service Principal"
  type        = string
}

variable "client_secret" {
  description = "Secreto del Service Principal"
  type        = string
  sensitive   = true  # Ocultar en logs y outputs
}

Autenticación: Service Principal, Managed Identity y Azure CLI

Azure ofrece múltiples métodos de autenticación. A continuación los más importantes:

Método 1: Azure CLI (Recomendado para desarrollo local)

# Instalar Azure CLI (Ubuntu/Debian)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Iniciar sesión en Azure
az login
# Se abrirá el navegador para autenticarse

# Seleccionar la suscripción correcta si tienes varias
az account set --subscription "nombre-o-id-de-suscripcion"

# Verificar que estás autenticado correctamente
az account show

Con Azure CLI autenticado, Terraform lo detecta automáticamente sin configuración adicional en el provider:

provider "azurerm" {
  features {}
  # Terraform usa automáticamente las credenciales del az login
}

Método 2: Service Principal con Client Secret (Recomendado para CI/CD)

Un Service Principal es similar a un usuario de servicio de Azure Active Directory.

# Crear un Service Principal con permisos de Contributor en la suscripción
az ad sp create-for-rbac \
  --name "terraform-sp" \
  --role Contributor \
  --scopes /subscriptions/<SUBSCRIPTION_ID>

# Salida:
# {
#   "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",     <- client_id
#   "displayName": "terraform-sp",
#   "password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", <- client_secret
#   "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"      <- tenant_id
# }

Configurar mediante variables de entorno (más seguro que hardcodear):

export ARM_CLIENT_ID="appId-del-service-principal"
export ARM_CLIENT_SECRET="password-del-service-principal"
export ARM_SUBSCRIPTION_ID="id-de-tu-suscripcion"
export ARM_TENANT_ID="id-de-tu-tenant"

O directamente en el provider:

provider "azurerm" {
  features {}

  subscription_id = var.subscription_id
  tenant_id       = var.tenant_id
  client_id       = var.client_id
  client_secret   = var.client_secret
}

Método 3: Managed Identity (Recomendado para Azure VMs, AKS, etc.)

Cuando Terraform se ejecuta desde una VM de Azure o un contenedor en AKS, puede usar la Managed Identity asignada:

provider "azurerm" {
  features {}
  # use_msi = true habilita el uso de Managed Identity automáticamente
  use_msi = true
}
# Variables de entorno para Managed Identity
export ARM_USE_MSI=true
export ARM_SUBSCRIPTION_ID="id-de-tu-suscripcion"
export ARM_TENANT_ID="id-de-tu-tenant"

Método 4: Service Principal con Certificado

Para mayor seguridad que un client secret:

provider "azurerm" {
  features {}

  subscription_id             = var.subscription_id
  tenant_id                   = var.tenant_id
  client_id                   = var.client_id
  client_certificate_path     = "/path/to/certificate.pfx"
  client_certificate_password = var.certificate_password
}

Conceptos Clave de Azure: Resource Groups, Subscriptions y Regions

Antes de crear recursos en Azure, debes entender su modelo de organización:

Jerarquía de Azure

Management Group (raíz)
└── Management Groups (organización)
    └── Subscriptions (facturación y límites)
        └── Resource Groups (contenedores lógicos)
            └── Resources (los recursos reales)

Subscription (Suscripción): Es la unidad de facturación y límites de cuota. Todo recurso debe pertenecer a una suscripción.

Resource Group: Contenedor lógico para recursos relacionados. En Azure, todos los recursos deben pertenecer a un Resource Group. Esto es diferente a AWS donde los recursos se agrupan por VPC o etiquetas.

Region: La ubicación geográfica donde se despliegan los recursos. Ejemplos: East US, West Europe, Brazil South.

Tabla de regiones comunes de Azure

Región Código para Terraform Ubicación
Este de EE.UU. East US Virginia, EE.UU.
Oeste de EE.UU. West US 2 Washington, EE.UU.
Europa Occidental West Europe Países Bajos
Norte de Europa North Europe Irlanda
Brasil Sur Brazil South São Paulo, Brasil
Japón Este Japan East Tokio, Japón
Australia Este Australia East Nueva Gales del Sur
India Central Central India Pune, India

Tabla de Recursos Azure más Usados con sus Tipos en Terraform

Servicio Azure Recurso Tipo en Terraform
Azure Resource Manager Grupo de recursos azurerm_resource_group
Virtual Network Red virtual azurerm_virtual_network
Virtual Network Subred azurerm_subnet
Virtual Network Public IP azurerm_public_ip
Virtual Network Network Interface azurerm_network_interface
Virtual Network Network Security Group azurerm_network_security_group
Compute Máquina Virtual Linux azurerm_linux_virtual_machine
Compute Máquina Virtual Windows azurerm_windows_virtual_machine
Storage Storage Account azurerm_storage_account
Storage Blob Container azurerm_storage_container
Database Azure SQL Server azurerm_sql_server
Database Azure SQL Database azurerm_sql_database
Database PostgreSQL Server azurerm_postgresql_server
App Service App Service Plan azurerm_service_plan
App Service Web App azurerm_linux_web_app
AKS Cluster de Kubernetes azurerm_kubernetes_cluster
Key Vault Almacén de claves azurerm_key_vault
IAM Role Assignment azurerm_role_assignment
Monitor Log Analytics Workspace azurerm_log_analytics_workspace

Tabla Comparativa: AWS vs Azure en Terraform

Concepto AWS Azure
Red privada virtual aws_vpc azurerm_virtual_network
Subred aws_subnet azurerm_subnet
Firewall de red aws_security_group azurerm_network_security_group
Máquina virtual aws_instance azurerm_linux_virtual_machine
Almacenamiento de objetos aws_s3_bucket azurerm_storage_account + azurerm_storage_container
Base de datos relacional aws_db_instance azurerm_sql_server + azurerm_sql_database
DNS aws_route53_zone azurerm_dns_zone
CDN aws_cloudfront_distribution azurerm_cdn_profile + azurerm_cdn_endpoint
Kubernetes aws_eks_cluster azurerm_kubernetes_cluster
Serverless aws_lambda_function azurerm_function_app
Almacén de secretos aws_secretsmanager_secret azurerm_key_vault
IAM aws_iam_role azurerm_role_assignment
Agrupación lógica Tags + VPC azurerm_resource_group
Orquestador de contenedores ECS azurerm_container_group

Recursos Azure más Comunes: Código Completo

Resource Groups

# resource_group.tf

variable "environment" {
  description = "Entorno de despliegue"
  type        = string
  default     = "dev"
}

variable "location" {
  description = "Región de Azure para desplegar los recursos"
  type        = string
  default     = "West Europe"
}

variable "project_name" {
  description = "Nombre del proyecto"
  type        = string
  default     = "miapp"
}

# Resource Group: contenedor para todos nuestros recursos
# TODOS los recursos de Azure deben pertenecer a un Resource Group
resource "azurerm_resource_group" "principal" {
  # Nombre del grupo de recursos (debe ser único en la suscripción)
  name = "rg-${var.project_name}-${var.environment}"

  # Ubicación geográfica del grupo de recursos
  location = var.location

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
    Project     = var.project_name
  }
}

Virtual Networks y Subnets

# network.tf

# Virtual Network (equivalente a VPC en AWS)
resource "azurerm_virtual_network" "principal" {
  name = "vnet-${var.project_name}-${var.environment}"

  # Debe estar en el mismo grupo de recursos
  resource_group_name = azurerm_resource_group.principal.name

  # Debe estar en la misma ubicación que el Resource Group
  location = azurerm_resource_group.principal.location

  # El espacio de direcciones IP de toda la VNet
  # En Azure se llama "address_space" (en AWS "cidr_block")
  address_space = ["10.0.0.0/16"]

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# Subred pública para recursos accesibles desde internet
resource "azurerm_subnet" "publica" {
  name = "snet-public-${var.environment}"

  # Las subredes se definen dentro de un Resource Group y Virtual Network
  resource_group_name  = azurerm_resource_group.principal.name
  virtual_network_name = azurerm_virtual_network.principal.name

  # Rango de IPs de esta subred (subconjunto del address_space de la VNet)
  # En Azure se llama "address_prefixes" y es una lista
  address_prefixes = ["10.0.1.0/24"]
}

# Subred privada para recursos internos (bases de datos, etc.)
resource "azurerm_subnet" "privada" {
  name                 = "snet-private-${var.environment}"
  resource_group_name  = azurerm_resource_group.principal.name
  virtual_network_name = azurerm_virtual_network.principal.name
  address_prefixes     = ["10.0.2.0/24"]

  # Delegaciones de servicio: permiten que servicios de Azure gestionen
  # esta subred (ej: Azure SQL Managed Instance, App Service Integration)
  # En este caso no necesitamos delegaciones
}

# Network Security Group (equivalente a Security Group en AWS)
resource "azurerm_network_security_group" "web" {
  name                = "nsg-web-${var.environment}"
  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Regla para permitir HTTP
  security_rule {
    name                       = "allow-http"
    priority                   = 100          # Prioridad: menor número = mayor precedencia
    direction                  = "Inbound"    # Dirección: Inbound o Outbound
    access                     = "Allow"      # Allow o Deny
    protocol                   = "Tcp"
    source_port_range          = "*"           # Puerto de origen (* = cualquiera)
    destination_port_range     = "80"          # Puerto de destino
    source_address_prefix      = "*"           # IP origen (* = cualquiera)
    destination_address_prefix = "*"           # IP destino (* = cualquiera)
  }

  # Regla para permitir HTTPS
  security_rule {
    name                       = "allow-https"
    priority                   = 110
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  # Regla para permitir SSH
  security_rule {
    name                       = "allow-ssh"
    priority                   = 120
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "203.0.113.0/24" # IP corporativa
    destination_address_prefix = "*"
  }

  tags = {
    Environment = var.environment
  }
}

# Asociar el NSG con la subred pública
resource "azurerm_subnet_network_security_group_association" "publica" {
  subnet_id                 = azurerm_subnet.publica.id
  network_security_group_id = azurerm_network_security_group.web.id
}

Virtual Machines

# vm.tf

# IP pública para la máquina virtual
resource "azurerm_public_ip" "web" {
  name                = "pip-web-${var.environment}"
  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Static: la IP no cambia aunque la VM se detenga
  # Dynamic: la IP puede cambiar cuando la VM se reinicia
  allocation_method = "Static"

  # SKU Standard es necesario para zonas de disponibilidad
  sku = "Standard"

  tags = {
    Environment = var.environment
  }
}

# Network Interface: el adaptador de red de la VM
# En Azure, la NIC es un recurso independiente que se asocia a la VM
resource "azurerm_network_interface" "web" {
  name                = "nic-web-${var.environment}"
  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Configuración de IP de la NIC
  ip_configuration {
    name = "internal"

    # La NIC se conecta a la subred pública
    subnet_id = azurerm_subnet.publica.id

    # Asignación privada dinámica (Azure asigna la IP privada)
    private_ip_address_allocation = "Dynamic"

    # Asociar la IP pública a esta NIC
    public_ip_address_id = azurerm_public_ip.web.id
  }
}

# Máquina Virtual Linux
resource "azurerm_linux_virtual_machine" "web" {
  name                = "vm-web-${var.environment}"
  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Tamaño de la VM: determina CPU, RAM y capacidad de red
  # Standard_B1s: 1 vCPU, 1 GB RAM (económico para pruebas)
  size = "Standard_B1s"

  # Nombre de usuario administrador
  admin_username = "azureuser"

  # Lista de IDs de Network Interfaces a asociar a la VM
  network_interface_ids = [
    azurerm_network_interface.web.id,
  ]

  # Configuración de autenticación SSH (recomendado sobre contraseña)
  admin_ssh_key {
    username   = "azureuser"
    # Leer la clave pública SSH desde un archivo local
    public_key = file("~/.ssh/id_rsa.pub")
  }

  # Disco del sistema operativo
  os_disk {
    # Tipo de almacenamiento: Standard_LRS, Premium_LRS, StandardSSD_LRS
    storage_account_type = "Standard_LRS"
    caching              = "ReadWrite"
  }

  # Imagen del sistema operativo a usar
  source_image_reference {
    publisher = "Canonical"       # Fabricante de la imagen
    offer     = "UbuntuServer"    # Producto
    sku       = "18.04-LTS"       # Versión específica
    version   = "latest"          # Usar la versión más reciente del SKU
  }

  # Script de inicialización (equivalente a User Data en AWS)
  # Se ejecuta una sola vez al crear la VM
  custom_data = base64encode(<<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y nginx
    systemctl start nginx
    systemctl enable nginx
    echo "<h1>VM Azure desplegada con Terraform</h1>" > /var/www/html/index.html
  EOF
  )

  tags = {
    Environment = var.environment
    Role        = "webserver"
  }
}

Storage Accounts

# storage.tf

resource "azurerm_storage_account" "principal" {
  # El nombre debe ser único globalmente en Azure y solo minúsculas/números
  # Máximo 24 caracteres
  name = "st${var.project_name}${var.environment}001"

  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Tipo de redundancia:
  # LRS: Local Redundant Storage (3 copias en el mismo datacenter)
  # GRS: Geo-Redundant Storage (réplica en otra región)
  # ZRS: Zone Redundant Storage (réplica en 3 zonas de disponibilidad)
  account_replication_type = "LRS"

  # Nivel de rendimiento:
  # Standard: HDDs (más económico, para uso general)
  # Premium: SSDs (más rápido, para bases de datos o archivos frecuentes)
  account_tier = "Standard"

  # Versión de TLS mínima para conexiones seguras
  min_tls_version = "TLS1_2"

  # Deshabilitar acceso HTTP (solo HTTPS)
  enable_https_traffic_only = true

  # Prevenir eliminación accidental
  allow_nested_items_to_be_public = false

  tags = {
    Environment = var.environment
    Purpose     = "application-storage"
  }
}

# Contenedor de blobs dentro del Storage Account
# Equivalente a un "bucket" dentro de la cuenta de almacenamiento
resource "azurerm_storage_container" "archivos" {
  name                 = "archivos-app"
  storage_account_name = azurerm_storage_account.principal.name

  # Nivel de acceso:
  # private: solo acceso autenticado
  # blob: acceso público anónimo a los blobs
  # container: acceso público anónimo a blobs y listado del contenedor
  container_access_type = "private"
}

Azure SQL Database

# database.tf

variable "sql_admin_password" {
  description = "Contraseña del administrador de SQL Server"
  type        = string
  sensitive   = true
}

# Azure SQL Server: el servidor que contiene las bases de datos
resource "azurerm_mssql_server" "principal" {
  name                = "sql-${var.project_name}-${var.environment}"
  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Versión del motor SQL Server
  version = "12.0"

  # Credenciales del administrador
  administrator_login          = "sqladmin"
  administrator_login_password = var.sql_admin_password

  # Configuración de Azure AD para autenticación moderna
  azuread_administrator {
    login_username = "[email protected]"
    object_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # ID del usuario/grupo en AAD
  }

  tags = {
    Environment = var.environment
  }
}

# Base de datos dentro del SQL Server
resource "azurerm_mssql_database" "app" {
  name      = "db-${var.project_name}-${var.environment}"
  server_id = azurerm_mssql_server.principal.id

  # SKU de la base de datos (rendimiento y costo)
  # S0, S1, S2... S12: Standard (DTUs)
  # P1, P2... P15: Premium (DTUs)
  # GP_S_Gen5_2: General Purpose Serverless (vCores)
  sku_name = "S1"

  # Retención de backups en días
  short_term_retention_policy {
    retention_days = 7
  }

  tags = {
    Environment = var.environment
  }
}

# Regla de firewall para permitir acceso desde Azure services
resource "azurerm_mssql_firewall_rule" "azure_services" {
  name      = "allow-azure-services"
  server_id = azurerm_mssql_server.principal.id

  # IP 0.0.0.0 a 0.0.0.0 es un valor especial que significa
  # "permitir todos los servicios de Azure"
  start_ip_address = "0.0.0.0"
  end_ip_address   = "0.0.0.0"
}

App Service

# app_service.tf

# App Service Plan: define el hardware donde corren las aplicaciones
resource "azurerm_service_plan" "principal" {
  name                = "asp-${var.project_name}-${var.environment}"
  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Sistema operativo del plan
  os_type = "Linux"

  # SKU que determina el rendimiento y precio:
  # F1: Free (gratis, recursos muy limitados)
  # B1, B2, B3: Basic (sin autoescalado, sin SSL personalizado)
  # S1, S2, S3: Standard (con autoescalado y SSL)
  # P1v3, P2v3, P3v3: Premium v3 (mejor rendimiento)
  sku_name = "B1"

  tags = {
    Environment = var.environment
  }
}

# Web App: la aplicación web en sí
resource "azurerm_linux_web_app" "api" {
  name                = "app-${var.project_name}-${var.environment}"
  resource_group_name = azurerm_resource_group.principal.name
  location            = azurerm_resource_group.principal.location

  # Asociar con el App Service Plan
  service_plan_id = azurerm_service_plan.principal.id

  site_config {
    # Runtime de la aplicación
    application_stack {
      # Puede ser: python, node, java, dotnet, php, ruby
      python_version = "3.11"
    }

    # La app siempre está encendida (evita cold starts en el plan B1)
    always_on = false  # No disponible en Free y Shared tiers
  }

  # Variables de entorno de la aplicación
  app_settings = {
    "DATABASE_URL" = "Server=${azurerm_mssql_server.principal.fully_qualified_domain_name};Database=miapp;"
    "ENVIRONMENT"  = var.environment
    "DEBUG"        = var.environment == "dev" ? "true" : "false"
  }

  # Cadena de conexión a la base de datos
  connection_string {
    name  = "DefaultConnection"
    type  = "SQLServer"
    value = "Server=${azurerm_mssql_server.principal.fully_qualified_domain_name};Database=miapp;User Id=sqladmin;Password=${var.sql_admin_password};"
  }

  tags = {
    Environment = var.environment
  }
}

Data Sources de Azure

# data_sources.tf

# Obtener información de la suscripción actual
data "azurerm_subscription" "actual" {}

# Obtener un Resource Group existente (no creado por este código)
data "azurerm_resource_group" "existente" {
  name = "rg-recurso-compartido"
}

# Obtener una imagen personalizada existente
data "azurerm_image" "personalizada" {
  name                = "mi-imagen-personalizada"
  resource_group_name = data.azurerm_resource_group.existente.name
}

# Obtener el cliente (Service Principal) actual
data "azurerm_client_config" "actual" {}

# Uso de los data sources
locals {
  subscription_id = data.azurerm_subscription.actual.subscription_id
  tenant_id       = data.azurerm_client_config.actual.tenant_id

  # Construir un Key Vault URI
  keyvault_uri = "https://kv-${var.project_name}-${var.environment}.vault.azure.net/"
}

Ejercicio Práctico: Crear un Grupo de Recursos con una VM Linux

Descripción del ejercicio

Crea la siguiente infraestructura en Azure:

  1. Un Resource Group en la región "West Europe"
  2. Una Virtual Network con CIDR 10.0.0.0/16
  3. Una subred pública con CIDR 10.0.1.0/24
  4. Un Network Security Group con reglas para SSH (puerto 22) y HTTP (puerto 80)
  5. Una IP pública estática
  6. Una Network Interface asociada a la subred y la IP pública
  7. Una VM Linux (Ubuntu 18.04) de tamaño Standard_B1s con Nginx instalado

Solución Completa

# main.tf - Solución completa del ejercicio

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = false
    }
  }
}

# Variables
variable "location" {
  default = "West Europe"
}

variable "environment" {
  default = "ejercicio"
}

variable "admin_username" {
  default = "azureuser"
}

# Resource Group
resource "azurerm_resource_group" "ejercicio" {
  name     = "rg-terraform-ejercicio"
  location = var.location

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# Virtual Network
resource "azurerm_virtual_network" "ejercicio" {
  name                = "vnet-ejercicio"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.ejercicio.location
  resource_group_name = azurerm_resource_group.ejercicio.name

  tags = {
    Environment = var.environment
  }
}

# Subred
resource "azurerm_subnet" "publica" {
  name                 = "snet-publica"
  resource_group_name  = azurerm_resource_group.ejercicio.name
  virtual_network_name = azurerm_virtual_network.ejercicio.name
  address_prefixes     = ["10.0.1.0/24"]
}

# Network Security Group
resource "azurerm_network_security_group" "ejercicio" {
  name                = "nsg-ejercicio"
  location            = azurerm_resource_group.ejercicio.location
  resource_group_name = azurerm_resource_group.ejercicio.name

  security_rule {
    name                       = "allow-ssh"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "allow-http"
    priority                   = 110
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  tags = {
    Environment = var.environment
  }
}

# Asociar NSG con la subred
resource "azurerm_subnet_network_security_group_association" "publica" {
  subnet_id                 = azurerm_subnet.publica.id
  network_security_group_id = azurerm_network_security_group.ejercicio.id
}

# IP Pública
resource "azurerm_public_ip" "ejercicio" {
  name                = "pip-ejercicio"
  resource_group_name = azurerm_resource_group.ejercicio.name
  location            = azurerm_resource_group.ejercicio.location
  allocation_method   = "Static"
  sku                 = "Standard"

  tags = {
    Environment = var.environment
  }
}

# Network Interface
resource "azurerm_network_interface" "ejercicio" {
  name                = "nic-ejercicio"
  location            = azurerm_resource_group.ejercicio.location
  resource_group_name = azurerm_resource_group.ejercicio.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.publica.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.ejercicio.id
  }
}

# Máquina Virtual Linux
resource "azurerm_linux_virtual_machine" "ejercicio" {
  name                = "vm-ejercicio"
  resource_group_name = azurerm_resource_group.ejercicio.name
  location            = azurerm_resource_group.ejercicio.location
  size                = "Standard_B1s"
  admin_username      = var.admin_username

  network_interface_ids = [
    azurerm_network_interface.ejercicio.id,
  ]

  # Autenticación por contraseña (solo para ejercicio, en producción usa SSH keys)
  admin_password                  = "P@ssw0rd1234!" # Solo para pruebas
  disable_password_authentication = false

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  # Instalar Nginx al arrancar
  custom_data = base64encode(<<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y nginx
    systemctl start nginx
    systemctl enable nginx
    echo "<h1>Azure VM - Ejercicio Terraform Completado</h1>" > /var/www/html/index.html
  EOF
  )

  tags = {
    Environment = var.environment
    Role        = "webserver"
  }
}

# Outputs
output "vm_ip_publica" {
  value       = azurerm_public_ip.ejercicio.ip_address
  description = "IP pública de la VM"
}

output "url_web" {
  value       = "http://${azurerm_public_ip.ejercicio.ip_address}"
  description = "URL para acceder al servidor web"
}

output "comando_ssh" {
  value       = "ssh ${var.admin_username}@${azurerm_public_ip.ejercicio.ip_address}"
  description = "Comando para conectarse por SSH"
}
# Comandos para ejecutar el ejercicio
terraform init
terraform plan
terraform apply -auto-approve

# Esperar unos segundos para que Nginx termine de instalarse
sleep 30

# Verificar que Nginx responde
curl http://$(terraform output -raw vm_ip_publica)

# Limpiar todos los recursos al terminar
terraform destroy -auto-approve

Errores Comunes con Azure

Error 1: Provider no registrado

Error: compute.VirtualMachinesClient#CreateOrUpdate: Failure sending request:
StatusCode=409 -- Original Error: autorest/azure: Service returned an error.
Status=409 Code="MissingSubscriptionRegistration"

Causa: El provider de Azure (servicio de Azure) no está registrado en la suscripción.

Solución:

# Registrar el provider necesario
az provider register --namespace Microsoft.Compute
az provider register --namespace Microsoft.Network
az provider register --namespace Microsoft.Storage

# Verificar el estado del registro
az provider show --namespace Microsoft.Compute --query "registrationState"

Error 2: Nombre de Storage Account inválido

Error: creating Azure Storage Account: storageaccounts.Client#Create:
Failure: Response Status Code=400 Message="The storage account name is invalid"

Causa: Los nombres de Storage Account en Azure deben ser únicos globalmente, solo minúsculas y números, y máximo 24 caracteres.

Solución: Generar un nombre válido:

resource "random_string" "sufijo" {
  length  = 6
  special = false
  upper   = false
}

resource "azurerm_storage_account" "principal" {
  # Nombre válido: solo minúsculas, máx 24 caracteres
  name = "stmiapp${var.environment}${random_string.sufijo.result}"
}

Error 3: Permisos insuficientes del Service Principal

Error: authorization.RoleAssignmentsClient#Create:
Failure: Response Status Code=403 -- Error: "The client does not have
authorization to perform action 'Microsoft.Authorization/roleAssignments/write'"

Solución: El Service Principal necesita el rol "User Access Administrator" además de "Contributor" para poder asignar roles.

az role assignment create \
  --assignee "<service-principal-id>" \
  --role "User Access Administrator" \
  --scope "/subscriptions/<subscription-id>"

Error 4: Región no disponible para el SKU

Error: The requested size is not available in the requested location

Solución: No todos los tamaños de VM están disponibles en todas las regiones. Consulta los disponibles:

az vm list-skus --location "West Europe" --size "Standard_B" --output table

Error 5: Key Vault con soft delete

Error: A resource with the ID "/subscriptions/.../vaults/kv-miapp-dev"
already exists - to be managed via Terraform this resource needs to be imported

Causa: Azure Key Vault tiene "soft delete" habilitado por defecto. Un Key Vault eliminado permanece 90 días en estado "eliminado suavemente" y no se puede crear otro con el mismo nombre.

Solución: Purgar el Key Vault eliminado antes de recrearlo:

az keyvault purge --name "kv-miapp-dev" --location "West Europe"

Resumen

En este módulo has aprendido a trabajar con Azure y Terraform:

  • Configurar el provider AzureRM con el bloque features {} obligatorio
  • Los cuatro métodos principales de autenticación: Azure CLI, Service Principal, Managed Identity y certificados
  • La jerarquía de Azure: Management Groups → Subscriptions → Resource Groups → Resources
  • La diferencia fundamental: en Azure todo recurso necesita un Resource Group
  • Crear los recursos más comunes: Virtual Networks, NSGs, VMs, Storage Accounts, SQL Database y App Service
  • Las diferencias clave entre AWS y Azure en cuanto a terminología y organización

El ejercicio te llevó a crear una arquitectura completa con red, seguridad y una VM funcional con Nginx, demostrando que Terraform abstrae muy bien las diferencias entre proveedores.

En el siguiente módulo aprenderás a aprovisionar recursos en Google Cloud Platform (GCP), el tercer gran proveedor de nube. Al finalizar el módulo 5, tendrás una visión completa de cómo Terraform se comporta con los tres grandes proveedores cloud.

Nota de costos: Los recursos de Azure del ejercicio (VM Standard_B1s, IP pública) generan costos. Asegúrate de ejecutar terraform destroy cuando termines de practicar. Azure ofrece 30 días de prueba gratuita para nuevas cuentas.

© Copyright 2026. Todos los derechos reservados