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 showCon Azure CLI autenticado, Terraform lo detecta automáticamente sin configuración adicional en el provider:
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:
- Un Resource Group en la región "West Europe"
- Una Virtual Network con CIDR
10.0.0.0/16 - Una subred pública con CIDR
10.0.1.0/24 - Un Network Security Group con reglas para SSH (puerto 22) y HTTP (puerto 80)
- Una IP pública estática
- Una Network Interface asociada a la subred y la IP pública
- Una VM Linux (Ubuntu 18.04) de tamaño
Standard_B1scon 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-approveErrores 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
Solución: No todos los tamaños de VM están disponibles en todas las regiones. Consulta los disponibles:
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:
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 destroycuando termines de practicar. Azure ofrece 30 días de prueba gratuita para nuevas cuentas.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
