Introducción¶
Terraform es una herramienta de Infraestructura como Código (IaC) que permite definir, gestionar y versionar infraestructura en múltiples proveedores de nube mediante un enfoque declarativo. Este taller aborda los fundamentos para implementar recursos básicos en AWS y Azure.
Ejemplo de infraestructura como código¶
graph TD
subgraph AWS_VPC["AWS VPC (10.0.0.0/16)"]
direction LR
subgraph Public_Subnet["Subred Pública (10.0.1.0/24)"]
direction LR
ec2[EC2 Instance: App]
end
subgraph Private_Subnet["Subred Privada (10.0.2.0/24)"]
direction LR
s3_bucket[S3 Bucket]
ec2_db[EC2 Instance: Postgres]
end
end
ec2 --> s3_bucket
ec2 --> ec2_db
Parte 1: Implementación en AWS¶
Configuración Inicial¶
Instalación de Terraform: https://developer.hashicorp.com/terraform/install
Instalar AWS CLI https://aws.amazon.com/cli/
Estructura¶
aws/
├── main.tf
├── variables.tf
├── outputs.tf
├── api_user_data.sh
└── postgres_user_data.sh
Creación de Recursos¶
Archivo main.tf:
La infraestructura del despliegue
provider "aws" {
region = var.aws_region
ignore_tags {
key_prefixes = ["aws:"]
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
}
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "ec2_sg" {
name = "ec2_sg"
description = "Allow SSH, HTTP and PostgreSQL"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP (API)"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "PostgreSQL access from app"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "db_server" {
ami = var.ec2_ami_id
instance_type = var.ec2_instance_type
subnet_id = aws_subnet.private.id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
key_name = var.key_name
associate_public_ip_address = false
user_data = file("postgres_user_data.sh")
tags = {
Name = "PostgreSQL Server"
}
}
resource "aws_instance" "app_server" {
ami = var.ec2_ami_id
instance_type = var.ec2_instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
associate_public_ip_address = true
key_name = var.key_name
user_data = file("api_user_data.sh")
tags = {
Name = "API Server"
}
}
resource "aws_s3_bucket" "private_data" {
bucket = var.s3_bucket_name
force_destroy = true
lifecycle {
ignore_changes = [object_lock_configuration]
}
}
Variables¶
Archivo variables.tf:
Este archivo permite almacenar variables que vamos utilizar en el archivo main.tf
variable "aws_region" {
description = "Región de AWS donde se desplegará la infraestructura"
type = string
default = "us-east-1"
}
variable "vpc_cidr" {
description = "CIDR de la VPC principal"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidr" {
description = "CIDR de la subred pública"
type = string
default = "10.0.1.0/24"
}
variable "private_subnet_cidr" {
description = "CIDR de la subred privada"
type = string
default = "10.0.2.0/24"
}
variable "private_subnet_b_cidr" {
description = "CIDR de la segunda subred privada"
type = string
default = "10.0.3.0/24"
}
variable "ec2_instance_type" {
description = "Tipo de instancia EC2"
type = string
default = "t2.micro"
}
variable "ec2_ami_id" {
description = "AMI ID para las instancias EC2 (Amazon Linux 2)"
type = string
default = "ami-0cbbe2c6a1bb2ad63"
}
variable "key_name" {
description = "Nombre de la clave SSH existente para acceder a las instancias EC2"
type = string
default = "vockey"
}
variable "s3_bucket_name" {
description = "Nombre del bucket S3 privado"
type = string
default = "mi-bucket-privado-terraform"
}
Archivos de debugging¶
Archivo outputs.tf:
Son salidas que se muestran en consola para el control del proceso
output "vpc_id" {
description = "ID de la VPC creada"
value = aws_vpc.main.id
}
output "public_subnet_id" {
description = "ID de la subred pública"
value = aws_subnet.public.id
}
output "private_subnet_id" {
description = "ID de la subred privada"
value = aws_subnet.private.id
}
output "ec2_public_ip" {
description = "IP pública de la instancia EC2 (API REST)"
value = aws_instance.app_server.public_ip
}
output "ec2_private_ip_db" {
description = "IP privada de la instancia EC2 (PostgreSQL)"
value = aws_instance.db_server.private_ip
}
output "s3_bucket_name" {
description = "Nombre del bucket S3 privado"
value = aws_s3_bucket.private_data.bucket
}
Archivos de configuración¶
Archivo postgres_user_data.sh
#!/bin/bash
yum update -y
yum install -y postgresql17-server postgresql17
# Inicializar y arrancar PostgreSQL
/usr/bin/postgresql-setup initdb
systemctl enable postgresql
systemctl start postgresql
# Crear base y tablas
sudo -u postgres psql <<EOF
CREATE DATABASE empresa;
\c empresa
CREATE TABLE cliente (id SERIAL PRIMARY KEY, nombre VARCHAR(50));
CREATE TABLE vendedor (id SERIAL PRIMARY KEY, nombre VARCHAR(50));
CREATE TABLE factura (
id SERIAL PRIMARY KEY,
cliente_id INT REFERENCES cliente(id),
vendedor_id INT REFERENCES vendedor(id),
total NUMERIC
);
INSERT INTO cliente (nombre) VALUES ('Carlos'), ('Ana');
INSERT INTO vendedor (nombre) VALUES ('Luis'), ('Laura');
INSERT INTO factura (cliente_id, vendedor_id, total) VALUES (1, 1, 100.00), (2, 2, 200.00);
EOF
#!/bin/bash
yum update -y
yum install -y python3 git python3-pip
pip3 install flask psycopg2-binary
DB_HOST='${aws_instance.db_server.private_ip}'
# Crear API sencilla
cat <<EOF > /home/ec2-user/api.py
from flask import Flask, jsonify
import psycopg2
app = Flask(__name__)
conn = psycopg2.connect(dbname='empresa', user='postgres', host='$DB_HOST', password='')
@app.route("/clientes")
def clientes():
cur = conn.cursor()
cur.execute("SELECT * FROM cliente")
rows = cur.fetchall()
cur.close()
return jsonify(rows)
@app.route("/facturas")
def facturas():
cur = conn.cursor()
cur.execute("SELECT * FROM factura")
rows = cur.fetchall()
cur.close()
return jsonify(rows)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
EOF
chmod +x /home/ec2-user/api.py
nohup python3 /home/ec2-user/api.py &
Ejecución de Comandos¶
terraform init
terraform plan
terraform apply # Confirmar con 'yes'
Limpieza de Recursos¶
terraform destroy # Confirmar con 'yes'
Parte 2: Implementación en Azure¶
Recursos¶
- Instalación Azure CLI https://learn.microsoft.com/en-us/cli/azure/?view=azure-cli-latest
- Loguearse con az login
Estructura¶
azure/
├── main.tf
├── variables.tf
├── outputs.tf
└── vm_user_data.sh
graph TD
subgraph Azure_VNet["Azure VNet (10.0.0.0/16)"]
direction LR
subgraph Public_Subnet["Subred Pública (10.0.1.0/24)"]
direction TB
vm["VM Linux (App)"]
end
subgraph Private_Subnet["Subred Privada (10.0.2.0/24)"]
direction LR
blob_storage["Blob Storage"]
cosmos_db["Cosmos DB (Gremlin)"]
end
end
vm --> blob_storage
vm --> cosmos_db
main.tf¶
provider "azurerm" {
subscription_id = var.subscription_id
features {}
}
resource "azurerm_resource_group" "main" {
name = "rg-gremlin-app"
location = var.location
}
resource "azurerm_virtual_network" "vnet" {
name = "vnet-app"
address_space = [var.vnet_cidr]
location = var.location
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_subnet" "public" {
name = "public-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.public_subnet_cidr]
}
resource "azurerm_subnet" "private" {
name = "private-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.private_subnet_cidr]
}
resource "azurerm_network_interface" "vm_nic" {
name = "vm-nic"
location = var.location
resource_group_name = azurerm_resource_group.main.name
ip_configuration {
name = "vm-ipconfig"
subnet_id = azurerm_subnet.public.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.vm_public_ip.id
}
}
resource "azurerm_public_ip" "vm_public_ip" {
name = "vm-public-ip"
location = var.location
resource_group_name = azurerm_resource_group.main.name
allocation_method = "Dynamic"
}
resource "azurerm_linux_virtual_machine" "vm" {
name = "app-vm"
resource_group_name = azurerm_resource_group.main.name
location = var.location
size = var.vm_size
admin_username = var.admin_username
network_interface_ids = [
azurerm_network_interface.vm_nic.id
]
admin_ssh_key {
username = var.admin_username
public_key = file(var.public_key_path)
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "20_04-lts-gen2"
version = "latest"
}
custom_data = filebase64("vm_user_data.sh")
}
resource "azurerm_storage_account" "private_storage" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.main.name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_container" "private_container" {
name = "private-container"
storage_account_name = azurerm_storage_account.private_storage.name
container_access_type = "private"
}
resource "azurerm_cosmosdb_account" "gremlin" {
name = var.cosmosdb_account_name
location = var.location
resource_group_name = azurerm_resource_group.main.name
offer_type = "Standard"
kind = "GlobalDocumentDB"
consistency_policy {
consistency_level = "Session"
}
geo_location {
location = var.location
failover_priority = 0
}
capabilities {
name = "EnableGremlin"
}
}
Variables.tf¶
variable "location" {
default = "East US"
}
variable "vnet_cidr" {
default = "10.0.0.0/16"
}
variable "public_subnet_cidr" {
default = "10.0.1.0/24"
}
variable "private_subnet_cidr" {
default = "10.0.2.0/24"
}
variable "vm_size" {
default = "Standard_B1s"
}
variable "admin_username" {
default = "azureuser"
}
variable "public_key_path" {
description = "Path to your public SSH key"
default = "~/.ssh/id_rsa.pub"
}
#ruta C:/Users/pepito/.ssh/id_rsa.pub
#ssh-keygen -t rsa
variable "storage_account_name" {
default = "privatestorageacc123"
}
variable "cosmosdb_account_name" {
default = "gremlinaccount123"
}
variable "subscription_id" {
description = "Azure Subscription ID"
default = "Resultado comando <az account show --query id>"
}
Outputs.tf¶
output "vm_public_ip" {
value = azurerm_public_ip.vm_public_ip.ip_address
}
output "storage_account_name" {
value = azurerm_storage_account.private_storage.name
}
output "cosmosdb_endpoint" {
value = azurerm_cosmosdb_account.gremlin.endpoint
}
vm_user_data.sh¶
#!/bin/bash
sudo apt-get update -y
sudo apt-get install -y python3-pip
pip3 install azure-storage-blob gremlinpython
# Simula una consulta y escribe a blob
echo "Consulta de prueba a Cosmos DB" > result.txt