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 AzureGestió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ñaaws/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-identityPaso 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 showPaso 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 applyPaso 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/nullPaso 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_deploymentDesafí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:
- Provider de GCP en
main.tf - Archivo
gcp/compute.tfcon una instancia Compute Engine - Archivo
gcp/storage.tfcon un bucket GCS - 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 listErrores 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_RGError: "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:
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.tfvarscomú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.
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
