Salta el contingut

Pràctica 5: Case Study Infraestructura Cloud Completa

En aquesta secció, veurem un case study complet on una empresa fictícia, "TechStart", migra la seva aplicació des d'un entorn on-premises a AWS utilitzant Terraform.

Context del Projecte

TechStart és una empresa de tecnologia que ha crescut ràpidament i necessita escalar la seva infraestructura. Actualment tenen: - Una aplicació web Python (Django) amb aproximadament 10.000 usuaris actius diaris - Una base de dades PostgreSQL - Un servidor Redis per caching - Diversos serveis de background jobs (Celery workers) - Tot allotjat en servidors físics al seu propi data center

El seu objectiu és migrar a AWS per obtenir: - Escalabilitat automàtica - Alta disponibilitat - Millor rendiment global - Reducció de costos operatius - Agilitat en el desplegament

Arquitectura Objectiu

L'arquitectura que volen implementar inclou: - Multi-AZ per alta disponibilitat - Auto Scaling per gestionar pics de tràfic - CloudFront CDN per contingut estàtic - S3 per emmagatzematge d'assets - RDS Multi-AZ per la base de dades - ElastiCache per Redis - ALB per distribució de càrrega - Route53 per DNS

Fase 1: Configuració Base i Xarxa

El primer pas és establir la infraestructura de xarxa base. TechStart divideix el seu codi en mòduls reutilitzables:

Directory Structure:

techstart-infrastructure/
├── environments/
│   ├── development/
│   ├── staging/
│   └── production/
│       ├── main.tf
│       ├── variables.tf
│       ├── terraform.tfvars
│       └── backend.tf
├── modules/
│   ├── networking/
│   ├── compute/
│   ├── database/
│   ├── cache/
│   └── cdn/
└── shared/
    ├── iam/
    └── monitoring/

modules/networking/main.tf:

# Crear VPC amb subnets privats i públics en 3 AZs

variable "project_name" { type = string }
variable "environment" { type = string }
variable "vpc_cidr" { type = string }
variable "availability_zones" { type = list(string) }

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${local.name_prefix}-vpc"
    Environment = var.environment
    Project     = var.project_name
  }
}

# Subnets públics per ALB
resource "aws_subnet" "public" {
  count = length(var.availability_zones)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 4, count.index)
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.name_prefix}-public-${count.index + 1}"
    Tier = "Public"
  }
}

# Subnets privats per aplicació
resource "aws_subnet" "private_app" {
  count = length(var.availability_zones)

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 4, count.index + 3)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${local.name_prefix}-private-app-${count.index + 1}"
    Tier = "Private-App"
  }
}

# Subnets privats per base de dades
resource "aws_subnet" "private_data" {
  count = length(var.availability_zones)

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 4, count.index + 6)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${local.name_prefix}-private-data-${count.index + 1}"
    Tier = "Private-Data"
  }
}

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

  tags = {
    Name = "${local.name_prefix}-igw"
  }
}

# NAT Gateways (un per AZ)
resource "aws_eip" "nat" {
  count  = length(var.availability_zones)
  domain = "vpc"

  tags = {
    Name = "${local.name_prefix}-nat-eip-${count.index + 1}"
  }

  depends_on = [aws_internet_gateway.main]
}

resource "aws_nat_gateway" "main" {
  count = length(var.availability_zones)

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "${local.name_prefix}-nat-${count.index + 1}"
  }
}

# Route Tables
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 = "${local.name_prefix}-public-rt"
  }
}

resource "aws_route_table" "private" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = {
    Name = "${local.name_prefix}-private-rt-${count.index + 1}"
  }
}

# Associacions
resource "aws_route_table_association" "public" {
  count = length(var.availability_zones)

  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private_app" {
  count = length(var.availability_zones)

  subnet_id      = aws_subnet.private_app[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

resource "aws_route_table_association" "private_data" {
  count = length(var.availability_zones)

  subnet_id      = aws_subnet.private_data[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

# VPC Endpoints per S3 i DynamoDB (estalviar costos de NAT)
resource "aws_vpc_endpoint" "s3" {
  vpc_id       = aws_vpc.main.id
  service_name = "com.amazonaws.${data.aws_region.current.name}.s3"

  route_table_ids = concat(
    [aws_route_table.public.id],
    aws_route_table.private[*].id
  )

  tags = {
    Name = "${local.name_prefix}-s3-endpoint"
  }
}

data "aws_region" "current" {}

# Outputs
output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "private_app_subnet_ids" {
  value = aws_subnet.private_app[*].id
}

output "private_data_subnet_ids" {
  value = aws_subnet.private_data[*].id
}

Fase 2: Base de Dades i Cache

modules/database/main.tf:

variable "project_name" { type = string }
variable "environment" { type = string }
variable "vpc_id" { type = string }
variable "subnet_ids" { type = list(string) }
variable "allowed_security_groups" { type = list(string) }
variable "db_name" { type = string }
variable "db_username" { type = string }
variable "db_password" { type = string; sensitive = true }
variable "instance_class" { type = string; default = "db.t3.medium" }
variable "allocated_storage" { type = number; default = 100 }
variable "multi_az" { type = bool; default = true }
variable "backup_retention_period" { type = number; default = 7 }

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

# Security Group per RDS
resource "aws_security_group" "rds" {
  name        = "${local.name_prefix}-rds-sg"
  description = "Security group for RDS PostgreSQL"
  vpc_id      = var.vpc_id

  ingress {
    description     = "PostgreSQL from application"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = var.allowed_security_groups
  }

  tags = {
    Name = "${local.name_prefix}-rds-sg"
  }
}

# Subnet Group per RDS
resource "aws_db_subnet_group" "main" {
  name       = "${local.name_prefix}-db-subnet-group"
  subnet_ids = var.subnet_ids

  tags = {
    Name = "${local.name_prefix}-db-subnet-group"
  }
}

# Parameter Group personalitzat
resource "aws_db_parameter_group" "postgres" {
  name   = "${local.name_prefix}-postgres15"
  family = "postgres15"

  parameter {
    name  = "log_connections"
    value = "1"
  }

  parameter {
    name  = "log_disconnections"
    value = "1"
  }

  parameter {
    name  = "log_duration"
    value = "1"
  }

  parameter {
    name  = "shared_preload_libraries"
    value = "pg_stat_statements"
  }

  tags = {
    Name = "${local.name_prefix}-postgres-params"
  }
}

# RDS Instance
resource "aws_db_instance" "main" {
  identifier = "${local.name_prefix}-postgres"

  engine               = "postgres"
  engine_version       = "15.3"
  instance_class       = var.instance_class
  allocated_storage    = var.allocated_storage
  max_allocated_storage = var.allocated_storage * 2
  storage_type         = "gp3"
  storage_encrypted    = true

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  multi_az = var.multi_az

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  parameter_group_name   = aws_db_parameter_group.postgres.name
  publicly_accessible    = false

  backup_retention_period = var.backup_retention_period
  backup_window          = "03:00-04:00"
  maintenance_window     = "mon:04:00-mon:05:00"

  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]

  deletion_protection       = var.environment == "production"
  skip_final_snapshot      = var.environment != "production"
  final_snapshot_identifier = var.environment == "production" ? "${local.name_prefix}-final-snapshot-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" : null

  performance_insights_enabled    = true
  performance_insights_retention_period = 7

  tags = {
    Name        = "${local.name_prefix}-postgres"
    Environment = var.environment
  }
}

# CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "database_cpu" {
  alarm_name          = "${local.name_prefix}-db-cpu-utilization"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors RDS CPU utilization"

  dimensions = {
    DBInstanceIdentifier = aws_db_instance.main.id
  }
}

resource "aws_cloudwatch_metric_alarm" "database_connections" {
  alarm_name          = "${local.name_prefix}-db-connections"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "DatabaseConnections"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors RDS database connections"

  dimensions = {
    DBInstanceIdentifier = aws_db_instance.main.id
  }
}

output "endpoint" {
  value     = aws_db_instance.main.endpoint
  sensitive = true
}

output "address" {
  value = aws_db_instance.main.address
}

output "security_group_id" {
  value = aws_security_group.rds.id
}

modules/cache/main.tf:

variable "project_name" { type = string }
variable "environment" { type = string }
variable "vpc_id" { type = string }
variable "subnet_ids" { type = list(string) }
variable "allowed_security_groups" { type = list(string) }
variable "node_type" { type = string; default = "cache.t3.micro" }
variable "num_cache_nodes" { type = number; default = 2 }

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

# Security Group per ElastiCache
resource "aws_security_group" "redis" {
  name        = "${local.name_prefix}-redis-sg"
  description = "Security group for ElastiCache Redis"
  vpc_id      = var.vpc_id

  ingress {
    description     = "Redis from application"
    from_port       = 6379
    to_port         = 6379
    protocol        = "tcp"
    security_groups = var.allowed_security_groups
  }

  tags = {
    Name = "${local.name_prefix}-redis-sg"
  }
}

# Subnet Group per ElastiCache
resource "aws_elasticache_subnet_group" "main" {
  name       = "${local.name_prefix}-redis-subnet-group"
  subnet_ids = var.subnet_ids

  tags = {
    Name = "${local.name_prefix}-redis-subnet-group"
  }
}

# Parameter Group
resource "aws_elasticache_parameter_group" "redis" {
  name   = "${local.name_prefix}-redis7"
  family = "redis7"

  parameter {
    name  = "maxmemory-policy"
    value = "allkeys-lru"
  }

  tags = {
    Name = "${local.name_prefix}-redis-params"
  }
}

# Replication Group (Redis Cluster)
resource "aws_elasticache_replication_group" "redis" {
  replication_group_id       = "${local.name_prefix}-redis"
  replication_group_description = "Redis cluster for ${local.name_prefix}"

  engine               = "redis"
  engine_version       = "7.0"
  node_type            = var.node_type
  num_cache_clusters   = var.num_cache_nodes
  parameter_group_name = aws_elasticache_parameter_group.redis.name
  port                 = 6379

  subnet_group_name  = aws_elasticache_subnet_group.main.name
  security_group_ids = [aws_security_group.redis.id]

  automatic_failover_enabled = var.num_cache_nodes > 1
  multi_az_enabled          = var.num_cache_nodes > 1

  at_rest_encryption_enabled = true
  transit_encryption_enabled = true
  auth_token_enabled        = true
  auth_token                = random_password.redis_auth.result

  snapshot_retention_limit = 5
  snapshot_window         = "03:00-05:00"
  maintenance_window      = "sun:05:00-sun:07:00"

  notification_topic_arn = aws_sns_topic.cache_notifications.arn

  log_delivery_configuration {
    destination      = aws_cloudwatch_log_group.redis_slow_log.name
    destination_type = "cloudwatch-logs"
    log_format       = "json"
    log_type         = "slow-log"
  }

  tags = {
    Name        = "${local.name_prefix}-redis"
    Environment = var.environment
  }
}

# Auth token per Redis
resource "random_password" "redis_auth" {
  length  = 32
  special = false
}

# Guardar auth token a Secrets Manager
resource "aws_secretsmanager_secret" "redis_auth" {
  name = "${local.name_prefix}/redis/auth-token"

  tags = {
    Name = "${local.name_prefix}-redis-auth"
  }
}

resource "aws_secretsmanager_secret_version" "redis_auth" {
  secret_id     = aws_secretsmanager_secret.redis_auth.id
  secret_string = random_password.redis_auth.result
}

# SNS Topic per notificacions
resource "aws_sns_topic" "cache_notifications" {
  name = "${local.name_prefix}-cache-notifications"
}

# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "redis_slow_log" {
  name              = "/aws/elasticache/${local.name_prefix}-redis/slow-log"
  retention_in_days = 7
}

output "primary_endpoint" {
  value = aws_elasticache_replication_group.redis.primary_endpoint_address
}

output "reader_endpoint" {
  value = aws_elasticache_replication_group.redis.reader_endpoint_address
}

output "auth_token_secret_arn" {
  value = aws_secretsmanager_secret.redis_auth.arn
}

output "security_group_id" {
  value = aws_security_group.redis.id
}

Fase 3: Compute i Auto Scaling

modules/compute/main.tf:

variable "project_name" { type = string }
variable "environment" { type = string }
variable "vpc_id" { type = string }
variable "subnet_ids" { type = list(string) }
variable "target_group_arns" { type = list(string) }
variable "instance_type" { type = string; default = "t3.medium" }
variable "min_size" { type = number; default = 2 }
variable "max_size" { type = number; default = 10 }
variable "desired_capacity" { type = number; default = 3 }
variable "db_endpoint" { type = string }
variable "redis_endpoint" { type = string }
variable "app_secrets_arn" { type = string }

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

# Security Group per instàncies de l'aplicació
resource "aws_security_group" "app" {
  name        = "${local.name_prefix}-app-sg"
  description = "Security group for application instances"
  vpc_id      = var.vpc_id

  # Permetre tràfic des del ALB
  ingress {
    description     = "HTTP from ALB"
    from_port       = 8000
    to_port         = 8000
    protocol        = "tcp"
    security_groups = [var.alb_security_group_id]
  }

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

  tags = {
    Name = "${local.name_prefix}-app-sg"
  }
}

# IAM Role per les instàncies
resource "aws_iam_role" "app" {
  name = "${local.name_prefix}-app-role"

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

  tags = {
    Name = "${local.name_prefix}-app-role"
  }
}

# Política per accedir a Secrets Manager
resource "aws_iam_role_policy" "secrets_access" {
  name = "${local.name_prefix}-secrets-access"
  role = aws_iam_role.app.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ]
      Resource = var.app_secrets_arn
    }]
  })
}

# Attached managed policies
resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.app.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role_policy_attachment" "cloudwatch" {
  role       = aws_iam_role.app.name
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}

# Instance Profile
resource "aws_iam_instance_profile" "app" {
  name = "${local.name_prefix}-app-profile"
  role = aws_iam_role.app.name
}

# Obtenir l'AMI més recent d'Amazon Linux 2023
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

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

# User data script
locals {
  user_data = templatefile("${path.module}/user_data.sh", {
    environment     = var.environment
    db_endpoint     = var.db_endpoint
    redis_endpoint  = var.redis_endpoint
    secrets_arn     = var.app_secrets_arn
    region          = data.aws_region.current.name
  })
}

data "aws_region" "current" {}

# Launch Template
resource "aws_launch_template" "app" {
  name_prefix   = "${local.name_prefix}-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.app.id]
  iam_instance_profile {
    arn = aws_iam_instance_profile.app.arn
  }

  user_data = base64encode(local.user_data)

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

  monitoring {
    enabled = true
  }

  block_device_mappings {
    device_name = "/dev/xvda"

    ebs {
      volume_size           = 30
      volume_type           = "gp3"
      encrypted             = true
      delete_on_termination = true
    }
  }

  tag_specifications {
    resource_type = "instance"

    tags = {
      Name        = "${local.name_prefix}-app"
      Environment = var.environment
      Project     = var.project_name
    }
  }

  tag_specifications {
    resource_type = "volume"

    tags = {
      Name        = "${local.name_prefix}-app-volume"
      Environment = var.environment
    }
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "app" {
  name                = "${local.name_prefix}-asg"
  min_size            = var.min_size
  max_size            = var.max_size
  desired_capacity    = var.desired_capacity
  health_check_type   = "ELB"
  health_check_grace_period = 300
  vpc_zone_identifier = var.subnet_ids
  target_group_arns   = var.target_group_arns

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  enabled_metrics = [
    "GroupDesiredCapacity",
    "GroupInServiceInstances",
    "GroupMaxSize",
    "GroupMinSize",
    "GroupPendingInstances",
    "GroupStandbyInstances",
    "GroupTerminatingInstances",
    "GroupTotalInstances"
  ]

  tag {
    key                 = "Name"
    value               = "${local.name_prefix}-app"
    propagate_at_launch = true
  }

  tag {
    key                 = "Environment"
    value               = var.environment
    propagate_at_launch = true
  }

  lifecycle {
    create_before_destroy = true
  }
}

# Target Tracking Scaling Policy - CPU
resource "aws_autoscaling_policy" "cpu" {
  name                   = "${local.name_prefix}-cpu-scaling"
  autoscaling_group_name = aws_autoscaling_group.app.name
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ASGAverageCPUUtilization"
    }
    target_value = 70.0
  }
}

# Target Tracking Scaling Policy - Request Count
resource "aws_autoscaling_policy" "requests" {
  name                   = "${local.name_prefix}-request-scaling"
  autoscaling_group_name = aws_autoscaling_group.app.name
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ALBRequestCountPerTarget"
      resource_label        = "${var.alb_arn_suffix}/${var.target_group_arn_suffix}"
    }
    target_value = 1000.0
  }
}

output "security_group_id" {
  value = aws_security_group.app.id
}

output "autoscaling_group_name" {
  value = aws_autoscaling_group.app.name
}

modules/compute/user_data.sh:

#!/bin/bash
set -e

# Variables
ENVIRONMENT="${environment}"
DB_ENDPOINT="${db_endpoint}"
REDIS_ENDPOINT="${redis_endpoint}"
SECRETS_ARN="${secrets_arn}"
REGION="${region}"

# Actualitzar sistema
dnf update -y

# Instal·lar dependencies
dnf install -y python3 python3-pip git postgresql15

# Instal·lar CloudWatch Agent
wget https://s3.amazonaws.com/amazoncloudwatch-agent/amazon_linux/amd64/latest/amazon-cloudwatch-agent.rpm
rpm -U ./amazon-cloudwatch-agent.rpm

# Configurar CloudWatch Agent
cat > /opt/aws/amazon-cloudwatch-agent/etc/config.json << 'EOF'
{
  "metrics": {
    "namespace": "TechStart/${environment}",
    "metrics_collected": {
      "cpu": {
        "measurement": [{"name": "cpu_usage_idle"}],
        "metrics_collection_interval": 60,
        "totalcpu": false
      },
      "mem": {
        "measurement": [{"name": "mem_used_percent"}],
        "metrics_collection_interval": 60
      }
    }
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/app/application.log",
            "log_group_name": "/aws/ec2/${environment}/application",
            "log_stream_name": "{instance_id}"
          }
        ]
      }
    }
  }
}
EOF

/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -s \
  -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json

# Obtenir secrets
SECRETS=$(aws secretsmanager get-secret-value \
  --secret-id $SECRETS_ARN \
  --region $REGION \
  --query SecretString \
  --output text)

# Crear usuari per l'aplicació
useradd -m -s /bin/bash appuser

# Clonar aplicació (substitueix amb el teu repo real)
cd /home/appuser
git clone https://github.com/techstart/app.git
cd app

# Crear virtual environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Crear fitxer de configuració
cat > /home/appuser/app/.env << EOF
ENVIRONMENT=$ENVIRONMENT
DB_HOST=$(echo $DB_ENDPOINT | cut -d: -f1)
DB_PORT=5432
DB_NAME=techstart
REDIS_HOST=$REDIS_ENDPOINT
REDIS_PORT=6379
$(echo $SECRETS | jq -r 'to_entries | .[] | "\(.key)=\(.value)"')
EOF

# Executar migracions (només en la primera instància)
if aws dynamodb get-item \
  --table-name techstart-migrations-lock \
  --key '{"LockID": {"S": "migration"}}' \
  --region $REGION 2>&1 | grep -q "Item"; then
  echo "Migrations already running or completed"
else
  aws dynamodb put-item \
    --table-name techstart-migrations-lock \
    --item '{"LockID": {"S": "migration"}, "Timestamp": {"N": "'$(date +%s)'"}}' \
    --region $REGION

  python manage.py migrate

  aws dynamodb delete-item \
    --table-name techstart-migrations-lock \
    --key '{"LockID": {"S": "migration"}}' \
    --region $REGION
fi

# Crear systemd service
cat > /etc/systemd/system/techstart-app.service << 'EOF'
[Unit]
Description=TechStart Application
After=network.target

[Service]
Type=notify
User=appuser
WorkingDirectory=/home/appuser/app
Environment="PATH=/home/appuser/app/venv/bin"
EnvironmentFile=/home/appuser/app/.env
ExecStart=/home/appuser/app/venv/bin/gunicorn \
  --workers 4 \
  --bind 0.0.0.0:8000 \
  --access-logfile /var/log/app/access.log \
  --error-logfile /var/log/app/error.log \
  techstart.wsgi:application
Restart=always

[Install]
WantedBy=multi-user.target
EOF

# Crear directori de logs
mkdir -p /var/log/app
chown appuser:appuser /var/log/app

# Iniciar aplicació
systemctl daemon-reload
systemctl enable techstart-app
systemctl start techstart-app

# Health check
sleep 10
curl -f http://localhost:8000/health || exit 1

echo "Application started successfully"

Fase 4: Desplegament en Producció

environments/production/main.tf:

terraform {
  required_version = ">= 1.0"

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

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = "TechStart"
      Environment = "production"
      ManagedBy   = "Terraform"
      Team        = "DevOps"
    }
  }
}

# Variables locals
locals {
  project_name = "techstart"
  environment  = "production"

  availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}

# Networking
module "networking" {
  source = "../../modules/networking"

  project_name       = local.project_name
  environment        = local.environment
  vpc_cidr           = "10.0.0.0/16"
  availability_zones = local.availability_zones
}

# Database
module "database" {
  source = "../../modules/database"

  project_name            = local.project_name
  environment             = local.environment
  vpc_id                  = module.networking.vpc_id
  subnet_ids              = module.networking.private_data_subnet_ids
  allowed_security_groups = [module.compute.security_group_id]

  db_name     = "techstart"
  db_username = "admin"
  db_password = var.db_password

  instance_class           = "db.t3.large"
  allocated_storage        = 200
  multi_az                 = true
  backup_retention_period  = 7
}

# Cache
module "cache" {
  source = "../../modules/cache"

  project_name            = local.project_name
  environment             = local.environment
  vpc_id                  = module.networking.vpc_id
  subnet_ids              = module.networking.private_data_subnet_ids
  allowed_security_groups = [module.compute.security_group_id]

  node_type        = "cache.t3.medium"
  num_cache_nodes  = 3
}

# ALB
module "load_balancer" {
  source = "../../modules/load-balancer"

  project_name = local.project_name
  environment  = local.environment
  vpc_id       = module.networking.vpc_id
  subnet_ids   = module.networking.public_subnet_ids

  certificate_arn = var.ssl_certificate_arn
}

# Compute
module "compute" {
  source = "../../modules/compute"

  project_name      = local.project_name
  environment       = local.environment
  vpc_id            = module.networking.vpc_id
  subnet_ids        = module.networking.private_app_subnet_ids
  target_group_arns = [module.load_balancer.target_group_arn]

  instance_type    = "t3.medium"
  min_size         = 3
  max_size         = 15
  desired_capacity = 6

  db_endpoint     = module.database.address
  redis_endpoint  = module.cache.primary_endpoint
  app_secrets_arn = aws_secretsmanager_secret.app_secrets.arn

  alb_security_group_id = module.load_balancer.security_group_id
  alb_arn_suffix        = module.load_balancer.alb_arn_suffix
  target_group_arn_suffix = module.load_balancer.target_group_arn_suffix
}

# Secrets per l'aplicació
resource "aws_secretsmanager_secret" "app_secrets" {
  name = "${local.project_name}/${local.environment}/app-secrets"

  tags = {
    Name = "${local.project_name}-${local.environment}-app-secrets"
  }
}

resource "aws_secretsmanager_secret_version" "app_secrets" {
  secret_id = aws_secretsmanager_secret.app_secrets.id
  secret_string = jsonencode({
    SECRET_KEY         = var.django_secret_key
    DB_PASSWORD        = var.db_password
    REDIS_AUTH_TOKEN   = module.cache.auth_token
    AWS_ACCESS_KEY_ID  = var.app_aws_access_key
    AWS_SECRET_ACCESS_KEY = var.app_aws_secret_key
  })
}

# CloudFront i S3 per assets estàtics
module "cdn" {
  source = "../../modules/cdn"

  project_name = local.project_name
  environment  = local.environment

  origin_domain = module.load_balancer.dns_name
  domain_name   = var.domain_name
  certificate_arn = var.cloudfront_certificate_arn
}

# Outputs
output "alb_dns_name" {
  description = "DNS name del Application Load Balancer"
  value       = module.load_balancer.dns_name
}

output "cloudfront_domain" {
  description = "Domini de CloudFront"
  value       = module.cdn.cloudfront_domain
}

output "database_endpoint" {
  description = "Endpoint de la base de dades"
  value       = module.database.endpoint
  sensitive   = true
}

Resultats del Case Study

Després d'implementar aquesta infraestructura amb Terraform, TechStart va aconseguir:

Velocitat de Desplegament: - Abans: 2-3 setmanes per configurar manualment un nou entorn - Després: 30 minuts amb terraform apply

Consistència: - Els entorns de development, staging i production són idèntics (excepte mida de recursos) - No més "funciona en el meu entorn però no en producció"

Escalabilitat: - Auto Scaling gestiona pics de tràfic automàticament - Van poder gestionar Black Friday (10x tràfic normal) sense problemes

Costs: - Reducció del 40% en costos d'infraestructura - Auto Scaling escala cap avall durant hores vall

Documentació: - Tot el codi està versionat a Git - Cada canvi té un commit message explicant el "per què" - Nous membres de l'equip poden entendre la infraestructura llegint el codi

Disaster Recovery: - Poden recrear tota la infraestructura en una altra regió en 1 hora - Backups automàtics de bases de dades i state