Introducción al Proyecto

El despliegue multi-cloud es una estrategia que permite a las organizaciones utilizar servicios de múltiples proveedores de nube simultáneamente. En este proyecto aprenderemos a gestionar infraestructura en AWS y Azure al mismo tiempo usando Terraform, aprovechando su capacidad única de hablar con múltiples providers en el mismo proyecto.

Terraform es ideal para multi-cloud porque su lenguaje HCL es neutral al proveedor. El mismo terraform apply puede crear recursos en AWS, Azure, GCP, Kubernetes y otros simultáneamente.


Casos de Uso Reales de Multi-Cloud

Caso de Uso Descripción Beneficio
Redundancia geográfica Misma app en AWS (EEUU) y Azure (Europa) Latencia reducida por región
Vendor lock-in avoidance No depender de un solo proveedor Negociación de precios, continuidad de negocio
Best-of-breed AWS para ML/AI, Azure para servicios Microsoft Aprovechar las fortalezas de cada cloud
Cumplimiento regulatorio Datos en Azure (Europa, GDPR) + AWS (global) Cumplir normativas por región
Migración gradual Mover cargas progresivamente entre clouds Migración sin downtime
Disaster Recovery AWS como producción, Azure como DR RPO/RTO mejorados

Arquitectura del Proyecto

Componentes

En AWS:

  • EC2 instance con aplicación web
  • S3 bucket para almacenamiento de activos
  • Elastic IP para dirección fija

En Azure:

  • Virtual Machine equivalente
  • Storage Account para almacenamiento
  • Public IP para dirección fija

Gestión de tráfico:

  • DNS con pesos para distribución de carga entre clouds
  • Health checks para failover automático

Diagrama de Arquitectura Multi-Cloud

                     USUARIOS
                        |
              [DNS (Route 53 / Azure DNS)]
              /                          \
     50% tráfico                    50% tráfico
            /                                \
    +-------+--------+            +----------+----------+
    |     AWS        |            |        AZURE        |
    |  us-east-1     |            |  westeurope         |
    |                |            |                     |
    | [Elastic IP]   |            | [Public IP]         |
    |      |         |            |      |              |
    | [EC2 t3.micro] |            | [VM Standard_B1s]  |
    |      |         |            |      |              |
    | [S3 Bucket]    |            | [Storage Account]   |
    |                |            |                     |
    +----------------+            +---------------------+
            |                                |
            +-------[Shared Module]-----------+
                    (mismo código de app)

Tabla Comparativa AWS vs Azure en el Proyecto

Concepto AWS Azure Terraform Resource
Máquina virtual EC2 Instance Virtual Machine aws_instance / azurerm_linux_virtual_machine
Almacenamiento de objetos S3 Bucket Storage Account + Container aws_s3_bucket / azurerm_storage_account
IP pública estática Elastic IP Public IP Address aws_eip / azurerm_public_ip
Firewall/SG Security Group Network Security Group aws_security_group / azurerm_network_security_group
Red virtual VPC Virtual Network aws_vpc / azurerm_virtual_network
Subred Subnet Subnet aws_subnet / azurerm_subnet
Interfaz de red ENI Network Interface implícito / azurerm_network_interface
Grupo de recursos N/A (por defecto ARN) Resource Group N/A / azurerm_resource_group
Región Region Location us-east-1 / westeurope

Estructura del Repositorio

terraform-multi-cloud/
├── main.tf                    # Configuración de providers y backends
├── variables.tf               # Variables comunes y por cloud
├── outputs.tf                 # Outputs de ambos deployments
├── terraform.tfvars           # Valores concretos
│
├── modules/
│   └── application/           # Módulo abstracto de la aplicación
│       ├── main.tf            # Recursos del módulo
│       ├── variables.tf       # Inputs del módulo
│       └── outputs.tf         # Outputs del módulo
│
├── aws/                       # Recursos específicos de AWS
│   ├── vpc.tf                 # VPC y red AWS
│   ├── compute.tf             # EC2 en AWS
│   └── storage.tf             # S3 en AWS
│
└── azure/                     # Recursos específicos de Azure
    ├── network.tf             # VNet y red Azure
    ├── compute.tf             # VM en Azure
    └── storage.tf             # Storage Account en Azure

Gestión del Estado: Backends Separados por Cloud

En proyectos multi-cloud, es una buena práctica mantener el estado separado por proveedor. Sin embargo, en este proyecto usaremos un único estado local para simplificar.

# Para producción: backends separados con workspaces

# Backend en AWS S3 (para el estado del deployment AWS)
# terraform {
#   backend "s3" {
#     bucket = "mi-terraform-state"
#     key    = "multi-cloud/aws/terraform.tfstate"
#     region = "us-east-1"
#   }
# }

# Backend en Azure Blob Storage (para el estado del deployment Azure)
# terraform {
#   backend "azurerm" {
#     resource_group_name  = "terraform-state-rg"
#     storage_account_name = "miterraformstate"
#     container_name       = "tfstate"
#     key                  = "multi-cloud/azure/terraform.tfstate"
#   }
# }

Código Terraform Completo

main.tf - Configuración de Providers

# main.tf
# Configuración de múltiples providers en el mismo proyecto
# Este es el poder único de Terraform: un solo archivo puede gestionar
# recursos en múltiples nubes simultáneamente

terraform {
  required_version = ">= 1.0"

  required_providers {
    # Provider de AWS
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }

    # Provider de Azure (AzureRM)
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }

    # Provider de DNS para gestión de registros (opcional)
    # cloudflare = {
    #   source  = "cloudflare/cloudflare"
    #   version = "~> 4.0"
    # }
  }
}

# Configurar el provider de AWS
# Las credenciales se obtienen de:
# 1. Variables de entorno AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY
# 2. Perfil en ~/.aws/credentials
# 3. IAM Role (si se ejecuta en EC2/ECS)
provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = var.project_name
      ManagedBy   = "Terraform"
      MultiCloud  = "true"
      CloudProvider = "AWS"
    }
  }
}

# Configurar el provider de Azure
# Las credenciales se obtienen de:
# 1. Variables de entorno ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID
# 2. Azure CLI (az login)
# 3. Managed Identity
provider "azurerm" {
  features {
    # Comportamiento al eliminar Resource Groups
    resource_group {
      prevent_deletion_if_contains_resources = false  # En dev: borrar aunque tenga recursos
    }

    # Comportamiento al eliminar VMs
    virtual_machine {
      delete_os_disk_on_deletion     = true   # Borrar disco al eliminar VM
      graceful_shutdown              = false  # No esperar apagado graceful
      skip_shutdown_and_force_delete = false  # No forzar borrado
    }
  }
}

# Data sources para información de la cuenta/suscripción actual
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

# En Azure, el equivalente es el cliente actual
data "azurerm_client_config" "current" {}

variables.tf - Variables del Proyecto

# variables.tf
# Variables comunes para ambos clouds y específicas por proveedor

# ========================================
# Variables Comunes (ambos clouds)
# ========================================

variable "project_name" {
  description = "Nombre del proyecto (se usa en todos los recursos)"
  type        = string
  default     = "multi-cloud-app"
}

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

variable "app_port" {
  description = "Puerto en el que corre la aplicación"
  type        = number
  default     = 80
}

# ========================================
# Variables de AWS
# ========================================

variable "aws_region" {
  description = "Región de AWS"
  type        = string
  default     = "us-east-1"
}

variable "aws_instance_type" {
  description = "Tipo de instancia EC2 en AWS"
  type        = string
  default     = "t3.micro"
}

variable "aws_vpc_cidr" {
  description = "CIDR de la VPC en AWS"
  type        = string
  default     = "10.0.0.0/16"
}

variable "aws_subnet_cidr" {
  description = "CIDR de la subred pública en AWS"
  type        = string
  default     = "10.0.1.0/24"
}

# ========================================
# Variables de Azure
# ========================================

variable "azure_location" {
  description = "Región de Azure (location)"
  type        = string
  default     = "westeurope"
}

variable "azure_vm_size" {
  description = "Tamaño de la VM en Azure"
  type        = string
  default     = "Standard_B1s"
}

variable "azure_vnet_cidr" {
  description = "CIDR de la Virtual Network en Azure"
  type        = string
  default     = "10.1.0.0/16"
}

variable "azure_subnet_cidr" {
  description = "CIDR de la subred en Azure"
  type        = string
  default     = "10.1.1.0/24"
}

# ========================================
# Variables de Credenciales (solo para VMs)
# ========================================

variable "admin_username" {
  description = "Usuario administrador para las VMs"
  type        = string
  default     = "adminuser"
}

variable "admin_password" {
  description = "Contraseña para las VMs (mínimo 12 caracteres, letras, números y símbolos)"
  type        = string
  sensitive   = true
}

variable "ssh_public_key" {
  description = "Clave pública SSH para acceso a las VMs"
  type        = string
  default     = ""
}

terraform.tfvars - Valores

# terraform.tfvars

project_name = "multi-cloud-app"
environment  = "dev"
app_port     = 80

# AWS
aws_region         = "us-east-1"
aws_instance_type  = "t3.micro"
aws_vpc_cidr       = "10.0.0.0/16"
aws_subnet_cidr    = "10.0.1.0/24"

# Azure
azure_location    = "westeurope"
azure_vm_size     = "Standard_B1s"
azure_vnet_cidr   = "10.1.0.0/16"
azure_subnet_cidr = "10.1.1.0/24"

# Credenciales (en producción usar variables de entorno)
admin_username = "adminuser"
admin_password = "CambiaEsto123!@#"
ssh_public_key = ""  # Dejar vacío para usar contraseña

aws/vpc.tf - Red en AWS

# aws/vpc.tf
# Infraestructura de red en AWS

# VPC en AWS
resource "aws_vpc" "main" {
  cidr_block           = var.aws_vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name  = "${var.project_name}-aws-vpc"
    Cloud = "AWS"
  }
}

# Internet Gateway para la VPC
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name  = "${var.project_name}-aws-igw"
    Cloud = "AWS"
  }
}

# Subred pública en AWS
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.aws_subnet_cidr
  map_public_ip_on_launch = true
  availability_zone       = "${var.aws_region}a"

  tags = {
    Name  = "${var.project_name}-aws-subnet"
    Cloud = "AWS"
  }
}

# Tabla de rutas: enviar tráfico a Internet via IGW
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "${var.project_name}-aws-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# Security Group para la instancia EC2
resource "aws_security_group" "app" {
  name        = "${var.project_name}-aws-sg"
  description = "Security group para la aplicacion en AWS"
  vpc_id      = aws_vpc.main.id

  # Permitir HTTP
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTP publico"
  }

  # Permitir SSH (solo para desarrollo)
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]  # En prod: limitar a tu IP
    description = "SSH para administracion"
  }

  # Salida sin restricciones
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name  = "${var.project_name}-aws-sg"
    Cloud = "AWS"
  }
}

aws/compute.tf - EC2 en AWS

# aws/compute.tf
# Instancia EC2 en AWS

# AMI de Amazon Linux 2023 más reciente
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Elastic IP: dirección IP pública estática
# Sin esto, la IP cambiaría cada vez que se reinicia la instancia
resource "aws_eip" "app" {
  domain = "vpc"

  tags = {
    Name  = "${var.project_name}-aws-eip"
    Cloud = "AWS"
  }
}

# Instancia EC2
resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.aws_instance_type

  # Ubicar en nuestra subred pública
  subnet_id                   = aws_subnet.public.id
  vpc_security_group_ids      = [aws_security_group.app.id]
  associate_public_ip_address = true

  # Script de inicialización
  user_data = base64encode(<<-EOF
    #!/bin/bash
    yum update -y
    yum install -y nginx

    # Crear página de bienvenida con información del cloud
    cat > /usr/share/nginx/html/index.html << 'HTML'
    <!DOCTYPE html>
    <html>
    <head>
      <title>Multi-Cloud App - AWS</title>
      <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        .aws { color: #FF9900; }
      </style>
    </head>
    <body>
      <h1 class="aws">Desplegado en AWS</h1>
      <p>Servidor: $(hostname)</p>
      <p>Región: ${var.aws_region}</p>
      <p>Proyecto: ${var.project_name}</p>
    </body>
    </html>
    HTML

    systemctl start nginx
    systemctl enable nginx
  EOF
  )

  tags = {
    Name  = "${var.project_name}-aws-instance"
    Cloud = "AWS"
  }
}

# Asociar la Elastic IP con la instancia
resource "aws_eip_association" "app" {
  instance_id   = aws_instance.app.id
  allocation_id = aws_eip.app.id
}

aws/storage.tf - S3 en AWS

# aws/storage.tf
# Bucket S3 para almacenamiento en AWS

resource "aws_s3_bucket" "app" {
  bucket = "${var.project_name}-aws-storage-${data.aws_caller_identity.current.account_id}"

  tags = {
    Name  = "${var.project_name}-aws-storage"
    Cloud = "AWS"
  }
}

# Bloquear acceso público
resource "aws_s3_bucket_public_access_block" "app" {
  bucket = aws_s3_bucket.app.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Activar versionado
resource "aws_s3_bucket_versioning" "app" {
  bucket = aws_s3_bucket.app.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Encriptación en reposo
resource "aws_s3_bucket_server_side_encryption_configuration" "app" {
  bucket = aws_s3_bucket.app.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

azure/network.tf - Red en Azure

# azure/network.tf
# Infraestructura de red en Azure
# En Azure, los recursos se organizan en "Resource Groups"

# Resource Group: contenedor lógico de recursos en Azure
# No tiene equivalente directo en AWS (en AWS se organizan por tags o ARNs)
resource "azurerm_resource_group" "main" {
  name     = "${var.project_name}-rg"
  location = var.azure_location

  tags = {
    Project   = var.project_name
    Cloud     = "Azure"
    ManagedBy = "Terraform"
  }
}

# Virtual Network (equivalente a VPC en AWS)
resource "azurerm_virtual_network" "main" {
  name                = "${var.project_name}-vnet"
  address_space       = [var.azure_vnet_cidr]
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  tags = {
    Name  = "${var.project_name}-azure-vnet"
    Cloud = "Azure"
  }
}

# Subred dentro de la VNet
# En Azure, las subredes no necesitan Internet Gateway (acceso por defecto)
resource "azurerm_subnet" "main" {
  name                 = "${var.project_name}-subnet"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [var.azure_subnet_cidr]
}

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

  # Regla para permitir HTTP
  security_rule {
    name                       = "allow-http"
    priority                   = 100      # Prioridad (menor número = mayor prioridad)
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  # Regla para permitir SSH
  security_rule {
    name                       = "allow-ssh"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  tags = {
    Cloud = "Azure"
  }
}

# IP pública para la VM de Azure
resource "azurerm_public_ip" "app" {
  name                = "${var.project_name}-public-ip"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  allocation_method   = "Static"  # IP fija (equivalente a Elastic IP en AWS)
  sku                 = "Standard"

  tags = {
    Cloud = "Azure"
  }
}

# Network Interface (NIC): interfaz de red virtual de la VM
# En Azure, la VM no se conecta directamente a la subred, sino a través de una NIC
# En AWS, esto es transparente (ENI implícita)
resource "azurerm_network_interface" "app" {
  name                = "${var.project_name}-nic"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.main.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.app.id
  }
}

# Asociar el NSG con la NIC
resource "azurerm_network_interface_security_group_association" "app" {
  network_interface_id      = azurerm_network_interface.app.id
  network_security_group_id = azurerm_network_security_group.app.id
}

azure/compute.tf - VM en Azure

# azure/compute.tf
# Máquina Virtual Linux en Azure

resource "azurerm_linux_virtual_machine" "app" {
  name                = "${var.project_name}-vm"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  size                = var.azure_vm_size  # Standard_B1s = 1 vCPU, 1GB RAM

  # Usuario administrador
  admin_username = var.admin_username

  # Autenticación: si se proporciona clave SSH, usarla; si no, contraseña
  # En producción siempre usar clave SSH (más seguro)
  disable_password_authentication = var.ssh_public_key != "" ? true : false

  # Contraseña (solo si no se usa SSH)
  admin_password = var.ssh_public_key == "" ? var.admin_password : null

  # Conectar a la NIC que creamos
  network_interface_ids = [azurerm_network_interface.app.id]

  # Disco del sistema operativo
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"  # HDD estándar (más barato para dev)
    disk_size_gb         = 30
  }

  # Imagen del sistema operativo (Ubuntu 22.04 LTS)
  # Equivalente a la AMI en AWS
  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts"
    version   = "latest"
  }

  # Script de inicialización (equivalente a user_data en AWS)
  # En Azure se llama "custom_data" y debe estar en base64
  custom_data = base64encode(<<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y nginx

    # Crear página de bienvenida con tema Azure
    cat > /var/www/html/index.html << 'HTML'
    <!DOCTYPE html>
    <html>
    <head>
      <title>Multi-Cloud App - Azure</title>
      <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        .azure { color: #0078D4; }
      </style>
    </head>
    <body>
      <h1 class="azure">Desplegado en Azure</h1>
      <p>Servidor: $(hostname)</p>
      <p>Región: ${var.azure_location}</p>
      <p>Proyecto: ${var.project_name}</p>
    </body>
    </html>
    HTML

    systemctl start nginx
    systemctl enable nginx
  EOF
  )

  # Si se proporciona clave SSH, configurarla
  dynamic "admin_ssh_key" {
    for_each = var.ssh_public_key != "" ? [1] : []
    content {
      username   = var.admin_username
      public_key = var.ssh_public_key
    }
  }

  tags = {
    Name  = "${var.project_name}-azure-vm"
    Cloud = "Azure"
  }
}

azure/storage.tf - Storage Account en Azure

# azure/storage.tf
# Azure Storage Account: equivalente a S3 en AWS

# Storage Account: contenedor de servicios de almacenamiento
# Los nombres deben ser únicos globalmente, 3-24 chars, solo alfanuméricos en minúscula
resource "azurerm_storage_account" "app" {
  # Generar nombre único: solo letras y números, max 24 chars
  name                = "${replace(var.project_name, "-", "")}storage"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location

  account_tier             = "Standard"     # Rendimiento estándar (HDD)
  account_replication_type = "LRS"          # Locally Redundant Storage (3 copias en datacenter)
  # En prod usar: "GRS" (Geo-Redundant, copia en otra región)

  # Encriptación en reposo (activado por defecto en Azure)
  min_tls_version = "TLS1_2"

  tags = {
    Cloud = "Azure"
  }
}

# Blob Container: equivalente a "prefijo de carpeta" en S3
resource "azurerm_storage_container" "app" {
  name                  = "app-storage"
  storage_account_name  = azurerm_storage_account.app.name
  container_access_type = "private"  # Solo acceso privado
}

Módulo Compartido: modules/application/

# modules/application/variables.tf
# Variables del módulo de aplicación compartido

variable "cloud_provider" {
  description = "Proveedor de nube (aws o azure)"
  type        = string

  validation {
    condition     = contains(["aws", "azure", "gcp"], var.cloud_provider)
    error_message = "El proveedor debe ser aws, azure o gcp."
  }
}

variable "app_name" {
  description = "Nombre de la aplicación"
  type        = string
}

variable "region" {
  description = "Región/Location donde se despliega"
  type        = string
}

variable "public_ip" {
  description = "IP pública del servidor de aplicación"
  type        = string
}

variable "storage_endpoint" {
  description = "Endpoint del almacenamiento (S3 o Storage Account)"
  type        = string
}
# modules/application/outputs.tf
# Outputs del módulo compartido

output "deployment_info" {
  description = "Información del despliegue en este cloud"
  value = {
    cloud    = var.cloud_provider
    region   = var.region
    endpoint = "http://${var.public_ip}"
    storage  = var.storage_endpoint
  }
}

output "health_check_url" {
  description = "URL para verificar que la aplicación funciona"
  value       = "http://${var.public_ip}/"
}

outputs.tf - Outputs del Proyecto Multi-Cloud

# outputs.tf
# Outputs que conectan los dos deployments

output "aws_deployment" {
  description = "Información del despliegue en AWS"
  value = {
    ip_address   = aws_eip.app.public_ip
    url          = "http://${aws_eip.app.public_ip}"
    region       = var.aws_region
    instance_id  = aws_instance.app.id
    s3_bucket    = aws_s3_bucket.app.id
  }
}

output "azure_deployment" {
  description = "Información del despliegue en Azure"
  value = {
    ip_address      = azurerm_public_ip.app.ip_address
    url             = "http://${azurerm_public_ip.app.ip_address}"
    location        = var.azure_location
    vm_id           = azurerm_linux_virtual_machine.app.id
    storage_account = azurerm_storage_account.app.name
  }
}

output "multi_cloud_summary" {
  description = "Resumen del despliegue multi-cloud"
  value = {
    aws_endpoint   = "http://${aws_eip.app.public_ip}"
    azure_endpoint = "http://${azurerm_public_ip.app.ip_address}"
    status         = "Ambos deployments activos"
  }
}

Instrucciones de Despliegue Paso a Paso

Paso 1: Configurar Credenciales de AWS

# Opción A: Variables de entorno (recomendado para CI/CD)
export AWS_ACCESS_KEY_ID="tu-access-key"
export AWS_SECRET_ACCESS_KEY="tu-secret-key"
export AWS_DEFAULT_REGION="us-east-1"

# Opción B: Perfil de AWS CLI
aws configure --profile multi-cloud
export AWS_PROFILE=multi-cloud

# Verificar
aws sts get-caller-identity

Paso 2: Configurar Credenciales de Azure

# Opción A: Azure CLI (más fácil para desarrollo)
az login
az account set --subscription "tu-subscription-id"

# Opción B: Service Principal (para CI/CD)
export ARM_CLIENT_ID="tu-client-id"
export ARM_CLIENT_SECRET="tu-client-secret"
export ARM_TENANT_ID="tu-tenant-id"
export ARM_SUBSCRIPTION_ID="tu-subscription-id"

# Verificar
az account show

Paso 3: Inicializar y Desplegar

# Crear estructura de directorios
mkdir -p terraform-multi-cloud/{aws,azure,modules/application}
cd terraform-multi-cloud

# Crear todos los archivos según el tutorial

# Inicializar (descarga ambos providers: AWS y Azure)
terraform init

# Planificar: verás recursos de AMBOS clouds en el mismo plan
terraform plan

# Aplicar (crea recursos en paralelo en AWS y Azure)
terraform apply

Paso 4: Verificar el Despliegue en Ambos Clouds

# Obtener las URLs de los outputs
AWS_URL=$(terraform output -json aws_deployment | jq -r '.url')
AZURE_URL=$(terraform output -json azure_deployment | jq -r '.url')

echo "AWS: $AWS_URL"
echo "Azure: $AZURE_URL"

# Verificar ambas aplicaciones
curl $AWS_URL
curl $AZURE_URL

# Comparar latencias desde tu ubicación
time curl -s $AWS_URL > /dev/null
time curl -s $AZURE_URL > /dev/null

Paso 5: Ver Información Completa

# Ver resumen del despliegue multi-cloud
terraform output multi_cloud_summary

# Ver detalles de AWS
terraform output aws_deployment

# Ver detalles de Azure
terraform output azure_deployment

Desafíos del Multi-Cloud y Cómo Terraform los Aborda

Desafío Sin Terraform Con Terraform
Diferentes APIs Aprender CLI de AWS Y Azure CLI Un solo lenguaje HCL
Consistencia de nombres Convención manual, errores humanos Variables compartidas, names coherentes
Estado de la infraestructura Spreadsheets, documentación manual Archivo de estado automático
Reproducibilidad "Funciona en AWS pero no en Azure" Mismo apply, mismos resultados
Destrucción segura Borrar manualmente en dos consolas Un solo terraform destroy
Cambios coordinados Coordinar dos equipos Un solo PR en git
Secretos Variables hardcodeadas en cada cloud Variables sensibles centralizadas

Ejercicio de Extensión: Añadir GCP como Tercer Cloud

Extiende el proyecto para desplegar también en Google Cloud Platform.

Qué añadir:

  1. Provider de GCP en main.tf
  2. Archivo gcp/compute.tf con una instancia Compute Engine
  3. Archivo gcp/storage.tf con un bucket GCS
  4. Outputs para el despliegue en GCP

Solución:

# En main.tf, añadir el provider de GCP
provider "google" {
  project = var.gcp_project_id
  region  = var.gcp_region
}

# En variables.tf, añadir variables de GCP
variable "gcp_project_id" {
  description = "ID del proyecto de Google Cloud"
  type        = string
}

variable "gcp_region" {
  description = "Región de Google Cloud"
  type        = string
  default     = "europe-west1"
}

variable "gcp_machine_type" {
  description = "Tipo de máquina en GCP"
  type        = string
  default     = "e2-micro"  # Elegible para el free tier
}
# gcp/compute.tf
# Instancia de Compute Engine en GCP

# IP pública reservada en GCP
resource "google_compute_address" "app" {
  name   = "${var.project_name}-gcp-ip"
  region = var.gcp_region
}

# Regla de firewall para permitir HTTP y SSH
resource "google_compute_firewall" "app" {
  name    = "${var.project_name}-fw"
  network = "default"  # Usar la red por defecto de GCP

  allow {
    protocol = "tcp"
    ports    = ["80", "22", "443"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["web-server"]
}

# Instancia de Compute Engine (equivalente a EC2 en AWS / VM en Azure)
resource "google_compute_instance" "app" {
  name         = "${var.project_name}-gcp-vm"
  machine_type = var.gcp_machine_type
  zone         = "${var.gcp_region}-b"

  tags = ["web-server"]  # Aplicar la regla de firewall

  # Disco de arranque (equivalente al AMI en AWS)
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"  # Debian 11 (Bullseye)
      size  = 20  # GB
    }
  }

  # Red: usar la red por defecto con IP pública reservada
  network_interface {
    network = "default"

    access_config {
      nat_ip = google_compute_address.app.address  # Asociar la IP reservada
    }
  }

  # Script de inicialización (equivalente a user_data en AWS)
  metadata_startup_script = <<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y nginx

    cat > /var/www/html/index.html << 'HTML'
    <!DOCTYPE html>
    <html>
    <head>
      <title>Multi-Cloud App - GCP</title>
      <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        .gcp { color: #4285F4; }
      </style>
    </head>
    <body>
      <h1 class="gcp">Desplegado en Google Cloud</h1>
      <p>Servidor: $(hostname)</p>
      <p>Proyecto: ${var.project_name}</p>
    </body>
    </html>
    HTML

    systemctl start nginx
    systemctl enable nginx
  EOF

  labels = {
    project = var.project_name
    cloud   = "gcp"
  }
}
# gcp/storage.tf
# Google Cloud Storage: equivalente a S3 en AWS / Storage Account en Azure

resource "google_storage_bucket" "app" {
  name          = "${var.project_name}-gcp-storage-${var.gcp_project_id}"
  location      = var.gcp_region
  force_destroy = true  # En dev: permite borrar aunque tenga objetos

  # Versionado
  versioning {
    enabled = true
  }

  # Encriptación por defecto de Google
  # No se necesita configurar explícitamente

  labels = {
    project = var.project_name
    cloud   = "gcp"
  }
}

# Hacer el bucket privado (bloquear acceso público)
resource "google_storage_bucket_iam_binding" "private" {
  bucket = google_storage_bucket.app.name
  role   = "roles/storage.objectViewer"
  members = []  # Sin miembros = privado
}
# Añadir en outputs.tf
output "gcp_deployment" {
  description = "Información del despliegue en GCP"
  value = {
    ip_address = google_compute_address.app.address
    url        = "http://${google_compute_address.app.address}"
    project    = var.gcp_project_id
    region     = var.gcp_region
    gcs_bucket = google_storage_bucket.app.name
  }
}

Comandos para Configurar Credenciales de GCP

# Instalar Google Cloud CLI
# https://cloud.google.com/sdk/docs/install

# Autenticarse
gcloud auth login
gcloud config set project TU_PROJECT_ID

# Crear credenciales de aplicación
gcloud auth application-default login

# O usando service account
export GOOGLE_APPLICATION_CREDENTIALS="/ruta/al/service-account.json"

# Verificar
gcloud auth list

Errores Comunes en Multi-Cloud

Error: "Provider configuration not present"

Error: Provider configuration not present
To work with azurerm_linux_virtual_machine.app, its original provider configuration
at provider["registry.terraform.io/hashicorp/azurerm"] is required.

Solución: Asegúrate de que el bloque provider "azurerm" está en main.tf con el bloque features {}.

Error: "A resource with the ID already exists"

Causa: El recurso existe en el cloud pero no en el estado de Terraform. Solución: Usar terraform import para importar el recurso existente.

# Ejemplo: importar un Resource Group de Azure existente
terraform import azurerm_resource_group.main /subscriptions/SUB_ID/resourceGroups/NOMBRE_RG

Error: "Error: creating Storage Account: naming rules"

Causa: Los nombres de Storage Accounts en Azure tienen reglas estrictas (solo alfanumérico, 3-24 chars). Solución: Usar la función replace() para eliminar guiones:

name = lower(replace("${var.project_name}storage", "-", ""))

Resumen y Lecciones Aprendidas

En este proyecto hemos demostrado el poder de Terraform como herramienta multi-cloud:

  • Un solo lenguaje para múltiples nubes: HCL es el idioma común que habla con AWS, Azure, GCP y decenas de otros providers simultáneamente.
  • Diferencias conceptuales: Cada cloud tiene su modelo mental (Resource Groups en Azure, IAM por defecto en AWS). Terraform te obliga a entender estas diferencias, no las oculta.
  • Variables compartidas: Usar un terraform.tfvars común para valores que se comparten entre clouds mejora la consistencia.
  • Outputs como conexión: Los outputs permiten que el deployment de un cloud consuma información del otro (ej: el DNS de AWS como destino de un record en Azure DNS).
  • Estado unificado: Un solo archivo de estado gestiona todos los clouds, lo que facilita la vista global de la infraestructura.

En el siguiente proyecto aplicaremos Terraform a la gestión de Kubernetes, un caso de uso cada vez más común en entornos modernos.

© Copyright 2026. Todos los derechos reservados