Salta el contingut

Mòduls: Reutilitzant i Organitzant Codi

6. Mòduls: Reutilitzant i Organitzant Codi

A mesura que les teves configuracions de Terraform creixen, necessites una manera d'organitzar i reutilitzar codi. Aquí és on entren els mòduls.

Què És un Mòdul?

Un mòdul és simplement un conjunt de fitxers de configuració de Terraform en un directori. Cada configuració de Terraform que has escrit fins ara és tècnicament un mòdul (anomenat el "root module"). Però la veritable potència ve quan crees mòduls reutilitzables que encapsulen funcionalitat.

Imagina que necessites crear servidors web en diversos projectes. En lloc de copiar i enganxar el mateix codi una i altra vegada, pots crear un mòdul "webserver" que encapsuli tota la lògica per crear un servidor web amb les millors pràctiques, i després reutilitzar aquest mòdul arreu.

Estructura d'un Mòdul

Un mòdul típic té aquesta estructura:

modules/webserver/
├── main.tf          # Recursos principals
├── variables.tf     # Variables d'entrada
├── outputs.tf       # Outputs del mòdul
├── README.md        # Documentació
└── versions.tf      # Requeriments de versions

Vegem un exemple de mòdul complet per crear un servidor web:

modules/webserver/variables.tf:

variable "name" {
  description = "Nom del servidor web"
  type        = string
}

variable "environment" {
  description = "Entorn (dev, staging, production)"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "L'entorn ha de ser dev, staging o production."
  }
}

variable "instance_type" {
  description = "Tipus d'instància EC2"
  type        = string
  default     = "t3.micro"
}

variable "vpc_id" {
  description = "ID de la VPC on crear el servidor"
  type        = string
}

variable "subnet_id" {
  description = "ID del subnet on crear el servidor"
  type        = string
}

variable "allowed_cidr_blocks" {
  description = "Blocs CIDR permesos per accedir al servidor"
  type        = list(string)
  default     = []
}

variable "enable_monitoring" {
  description = "Si s'ha d'activar monitorització detallada"
  type        = bool
  default     = false
}

variable "tags" {
  description = "Tags addicionals per al servidor"
  type        = map(string)
  default     = {}
}

modules/webserver/main.tf:

# Obtenir l'AMI més recent d'Ubuntu
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

# Security Group per al servidor web
resource "aws_security_group" "webserver" {
  name        = "${var.name}-sg"
  description = "Security group for ${var.name}"
  vpc_id      = var.vpc_id

  # HTTP
  ingress {
    description = "HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = length(var.allowed_cidr_blocks) > 0 ? var.allowed_cidr_blocks : ["0.0.0.0/0"]
  }

  # HTTPS
  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = length(var.allowed_cidr_blocks) > 0 ? var.allowed_cidr_blocks : ["0.0.0.0/0"]
  }

  # SSH (només des de les IPs especificades)
  dynamic "ingress" {
    for_each = length(var.allowed_cidr_blocks) > 0 ? [1] : []
    content {
      description = "SSH"
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = var.allowed_cidr_blocks
    }
  }

  # Tot el tràfic sortint
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(var.tags, {
    Name        = "${var.name}-sg"
    Environment = var.environment
  })
}

# IAM Role per al servidor (per utilitzar SSM Session Manager)
resource "aws_iam_role" "webserver" {
  name = "${var.name}-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })

  tags = merge(var.tags, {
    Name        = "${var.name}-role"
    Environment = var.environment
  })
}

# Attach managed policy per SSM
resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.webserver.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

# Instance Profile
resource "aws_iam_instance_profile" "webserver" {
  name = "${var.name}-profile"
  role = aws_iam_role.webserver.name
}

# User data script
locals {
  user_data = <<-EOF
    #!/bin/bash
    set -e

    # Actualitzar sistema
    apt-get update
    apt-get upgrade -y

    # Instal·lar nginx
    apt-get install -y nginx

    # Configurar nginx
    cat > /var/www/html/index.html <<'HTML'
    <!DOCTYPE html>
    <html>
    <head>
        <title>Welcome to ${var.name}</title>
    </head>
    <body>
        <h1>${var.name}</h1>
        <p>Environment: ${var.environment}</p>
        <p>Managed by Terraform</p>
    </body>
    </html>
    HTML

    # Assegurar que nginx està executant-se
    systemctl enable nginx
    systemctl start nginx

    # Instal·lar CloudWatch agent si la monitorització està activada
    %{ if var.enable_monitoring }
    wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
    dpkg -i amazon-cloudwatch-agent.deb
    %{ endif }
  EOF
}

# Instància EC2
resource "aws_instance" "webserver" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.webserver.id]
  iam_instance_profile   = aws_iam_instance_profile.webserver.name

  user_data = local.user_data

  monitoring = var.enable_monitoring

  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"
    http_put_response_hop_limit = 1
  }

  root_block_device {
    volume_type           = "gp3"
    volume_size           = 20
    encrypted             = true
    delete_on_termination = true
  }

  tags = merge(var.tags, {
    Name        = var.name
    Environment = var.environment
    ManagedBy   = "Terraform"
  })

  lifecycle {
    create_before_destroy = true
  }
}

# Elastic IP (opcional, però recomanat per tenir IP estàtica)
resource "aws_eip" "webserver" {
  instance = aws_instance.webserver.id
  domain   = "vpc"

  tags = merge(var.tags, {
    Name        = "${var.name}-eip"
    Environment = var.environment
  })
}

modules/webserver/outputs.tf:

output "instance_id" {
  description = "ID de la instància EC2"
  value       = aws_instance.webserver.id
}

output "public_ip" {
  description = "IP pública del servidor"
  value       = aws_eip.webserver.public_ip
}

output "private_ip" {
  description = "IP privada del servidor"
  value       = aws_instance.webserver.private_ip
}

output "security_group_id" {
  description = "ID del security group"
  value       = aws_security_group.webserver.id
}

output "instance_arn" {
  description = "ARN de la instància"
  value       = aws_instance.webserver.arn
}

modules/webserver/versions.tf:

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
    }
  }
}

Utilitzant el Mòdul

Ara que hem creat el mòdul, podem utilitzar-lo fàcilment en diferents projectes:

# main.tf
module "web_server_production" {
  source = "./modules/webserver"

  name                = "production-web-01"
  environment         = "production"
  instance_type       = "t3.medium"
  vpc_id              = aws_vpc.main.id
  subnet_id           = aws_subnet.public[0].id
  allowed_cidr_blocks = ["203.0.113.0/24"]  # Només des de l'oficina
  enable_monitoring   = true

  tags = {
    Project = "MyApp"
    Owner   = "DevOps Team"
  }
}

module "web_server_staging" {
  source = "./modules/webserver"

  name                = "staging-web-01"
  environment         = "staging"
  instance_type       = "t3.small"
  vpc_id              = aws_vpc.main.id
  subnet_id           = aws_subnet.public[1].id
  allowed_cidr_blocks = ["0.0.0.0/0"]  # Accessible públicament per testing
  enable_monitoring   = false

  tags = {
    Project = "MyApp"
    Owner   = "DevOps Team"
  }
}

# Utilitzar els outputs dels mòduls
output "production_web_ip" {
  value = module.web_server_production.public_ip
}

output "staging_web_ip" {
  value = module.web_server_staging.public_ip
}

Mòduls del Terraform Registry

A més de crear els teus propis mòduls, pots utilitzar mòduls de la comunitat des del Terraform Registry (https://registry.terraform.io/):

# Utilitzar el mòdul VPC oficial d'AWS
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = false

  tags = {
    Environment = "production"
  }
}

Els mòduls del Registry estan generalment molt ben testejats i segueixen les millors pràctiques, estalviant-te molt de temps i esforç.

Best Practices per Mòduls

Quan creïs mòduls, segueix aquestes millors pràctiques:

1. Fer els mòduls petits i enfocats: Cada mòdul hauria de fer una cosa i fer-la bé. Un mòdul "webserver" és millor que un mòdul "application" que fa massa coses.

2. Documentar bé: Escriu un README.md complet explicant què fa el mòdul, quines variables requereix, i quins outputs proporciona.

3. Utilitzar validacions: Valida les entrades per prevenir errors:

variable "environment" {
  type = string

  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "L'entorn ha de ser dev, staging o production."
  }
}

4. Proporcionar defaults sensats: Les variables haurien de tenir valors per defecte raonables quan sigui possible:

variable "instance_type" {
  type    = string
  default = "t3.micro"
}

5. Fer outputs útils: Exposa tota la informació que els usuaris del mòdul puguin necessitar:

output "instance_id" {
  value = aws_instance.this.id
}

output "public_ip" {
  value = aws_instance.this.public_ip
}

6. Versionar els mòduls: Utilitza Git tags per versionar els teus mòduls, permetent als usuaris fixar una versió específica.