Skip to content
English
On this page

Ejercicio: Laravel con AWS CodeArtifact y Pipeline CI/CD

Parte 1: Estructura Base y Configuración

Escenario

Implementaremos una aplicación Laravel con:

  • Gestión de dependencias con CodeArtifact
  • CI/CD con CodeBuild
  • Secretos con Parameter Store
  • Base de datos MySQL con RDS
  • Múltiples entornos (DEV, STG, PROD)

Estructura del Proyecto

laravel-aws-app/
├── .aws/
│   ├── buildspec/
│   │   ├── buildspec-dev.yml
│   │   ├── buildspec-stg.yml
│   │   └── buildspec-prod.yml
│   │
│   ├── scripts/
│   │   ├── setup-codeartifact.sh
│   │   └── configure-environments.sh
│   │
│   └── terraform/
│       ├── modules/
│       │   ├── codeartifact/
│       │   ├── codebuild/
│       │   └── rds/
│       └── environments/
│           ├── dev/
│           ├── stg/
│           └── prod/

├── src/
│   ├── app/
│   ├── bootstrap/
│   ├── config/
│   ├── database/
│   ├── resources/
│   ├── routes/
│   ├── storage/
│   ├── tests/
│   ├── composer.json
│   └── .env.example

├── docker/
│   ├── php/
│   │   └── Dockerfile
│   └── nginx/
│       └── default.conf

└── scripts/
    ├── deploy.sh
    └── setup-env.sh

1. Configuración de CodeArtifact

1.1 Terraform CodeArtifact

hcl
# .aws/terraform/modules/codeartifact/main.tf
resource "aws_codeartifact_domain" "php_packages" {
  domain = "laravel-packages"
  
  encryption_key = aws_kms_key.artifact_key.arn
  
  tags = {
    Environment = var.environment
    Project     = "Laravel-AWS-App"
  }
}

resource "aws_codeartifact_repository" "composer" {
  repository = "composer-${var.environment}"
  domain     = aws_codeartifact_domain.php_packages.domain
  
  external_connections {
    external_connection_name = "public:packagist"
  }
  
  upstream {
    repository_name = "packagist-store"
  }
  
  tags = {
    Environment = var.environment
  }
}

resource "aws_kms_key" "artifact_key" {
  description = "KMS key for CodeArtifact encryption"
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "*"
        }
        Action   = "kms:*"
        Resource = "*"
      }
    ]
  })
  
  tags = {
    Environment = var.environment
  }
}

1.2 Script de Configuración

bash
#!/bin/bash
# .aws/scripts/setup-codeartifact.sh

# Variables
DOMAIN="laravel-packages"
REPO="composer-${ENVIRONMENT}"
REGION="us-east-1"

# Obtener token de autorización
TOKEN=$(aws codeartifact get-authorization-token \
    --domain $DOMAIN \
    --domain-owner $AWS_ACCOUNT_ID \
    --region $REGION \
    --query authorizationToken \
    --output text)

# Configurar Composer
composer config --global repos.packagist.org false
composer config --global repositories.codeartifact composer https://$DOMAIN-$AWS_ACCOUNT_ID.d.codeartifact.$REGION.amazonaws.com/composer/$REPO/

# Exportar token
export COMPOSER_AUTH="{\"bearer\": {\"$DOMAIN\": \"$TOKEN\"}}"

2. Configuración de Entornos

2.1 Parameter Store Setup

python
# scripts/setup-env.py
import boto3
import json

class EnvironmentManager:
    def __init__(self, environment):
        self.ssm = boto3.client('ssm')
        self.environment = environment
        self.app_name = "laravel-aws-app"
        
    def set_parameters(self, config):
        """Guarda parámetros en Parameter Store"""
        for key, value in config.items():
            parameter_name = f"/{self.app_name}/{self.environment}/{key}"
            self.ssm.put_parameter(
                Name=parameter_name,
                Value=value,
                Type='SecureString',
                Overwrite=True,
                Tags=[
                    {
                        'Key': 'Environment',
                        'Value': self.environment
                    },
                    {
                        'Key': 'Application',
                        'Value': self.app_name
                    }
                ]
            )
            
    def get_parameters(self):
        """Obtiene todos los parámetros para el entorno"""
        prefix = f"/{self.app_name}/{self.environment}/"
        params = {}
        
        paginator = self.ssm.get_paginator('get_parameters_by_path')
        for page in paginator.paginate(
            Path=prefix,
            Recursive=True,
            WithDecryption=True
        ):
            for param in page['Parameters']:
                name = param['Name'].replace(prefix, '')
                params[name] = param['Value']
                
        return params
        
    def load_env_file(self, env_file='.env.example'):
        """Carga variables desde archivo .env"""
        config = {}
        with open(env_file, 'r') as f:
            for line in f:
                if '=' in line and not line.startswith('#'):
                    key, value = line.strip().split('=', 1)
                    config[key] = value
        return config

# Uso
if __name__ == "__main__":
    environments = ['dev', 'stg', 'prod']
    
    for env in environments:
        manager = EnvironmentManager(env)
        
        # Cargar configuración base
        base_config = manager.load_env_file('src/.env.example')
        
        # Personalizar por entorno
        if env == 'prod':
            base_config.update({
                'APP_DEBUG': 'false',
                'APP_ENV': 'production',
                'LOG_LEVEL': 'error'
            })
        elif env == 'stg':
            base_config.update({
                'APP_DEBUG': 'false',
                'APP_ENV': 'staging',
                'LOG_LEVEL': 'debug'
            })
        else:  # dev
            base_config.update({
                'APP_DEBUG': 'true',
                'APP_ENV': 'development',
                'LOG_LEVEL': 'debug'
            })
            
        # Guardar en Parameter Store
        manager.set_parameters(base_config)

2.2 Script de Despliegue

bash
#!/bin/bash
# scripts/deploy.sh

# Variables
ENVIRONMENT=$1
APP_NAME="laravel-aws-app"

# Validar ambiente
if [[ ! "$ENVIRONMENT" =~ ^(dev|stg|prod)$ ]]; then
    echo "Ambiente inválido. Usar: dev, stg, o prod"
    exit 1
fi

# Obtener variables de entorno
echo "Obteniendo configuración de Parameter Store..."
aws ssm get-parameters-by-path \
    --path "/${APP_NAME}/${ENVIRONMENT}/" \
    --recursive \
    --with-decryption \
    --query "Parameters[*].{Name:Name,Value:Value}" \
    --output json > env.json

# Crear archivo .env
echo "Generando archivo .env..."
jq -r 'to_entries | .[] | .key + "=" + .value' env.json > src/.env

# Instalar dependencias
echo "Instalando dependencias..."
composer install --no-dev --optimize-autoloader

# Limpiar caché
php artisan config:clear
php artisan cache:clear
php artisan route:clear
php artisan view:clear

# Migraciones
if [ "$ENVIRONMENT" != "prod" ]; then
    echo "Ejecutando migraciones..."
    php artisan migrate --force
fi

3. Laravel Base

3.1 Dockerfile

dockerfile
# docker/php/Dockerfile
FROM php:8.2-fpm

# Instalar dependencias
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

# Instalar extensiones PHP
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd

# Instalar Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Directorio de trabajo
WORKDIR /var/www

# Copiar aplicación
COPY src/ .

# Permisos
RUN chown -R www-data:www-data \
    /var/www/storage \
    /var/www/bootstrap/cache

# Instalar dependencias
RUN composer install --no-interaction --optimize-autoloader --no-dev

# Optimizar
RUN php artisan optimize

3.2 Nginx Config

nginx
# docker/nginx/default.conf
server {
    listen 80;
    server_name _;
    root /var/www/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Verificación Parte 1

1. Verificar CodeArtifact

  • [ ] Dominio creado
  • [ ] Repositorio configurado
  • [ ] Composer conectado
  • [ ] KMS configurado

2. Verificar Parameter Store

  • [ ] Parámetros guardados
  • [ ] Encriptación activa
  • [ ] Variables por entorno
  • [ ] Acceso configurado

3. Verificar Docker

  • [ ] Imagen construida
  • [ ] Nginx configurado
  • [ ] PHP configurado
  • [ ] Permisos correctos

Troubleshooting Común

Errores de CodeArtifact

  1. Verificar token
  2. Revisar conexión
  3. Verificar permisos

Errores de Parámetros

  1. Verificar encriptación
  2. Revisar nombres
  3. Verificar permisos

Errores de Docker

  1. Verificar dependencias
  2. Revisar permisos
  3. Verificar red

Parte 2: CodeBuild y Pipeline CI/CD

1. Configuración de CodeBuild

1.1 Terraform CodeBuild

hcl
# .aws/terraform/modules/codebuild/main.tf
resource "aws_codebuild_project" "laravel_build" {
  name           = "laravel-build-${var.environment}"
  description    = "Laravel build project for ${var.environment}"
  build_timeout  = "30"
  service_role   = aws_iam_role.codebuild_role.arn

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                      = "aws/codebuild/amazonlinux2-x86_64-standard:4.0"
    type                       = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = true

    environment_variable {
      name  = "ENVIRONMENT"
      value = var.environment
    }

    environment_variable {
      name  = "APP_NAME"
      value = "laravel-aws-app"
    }
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec-${var.environment}.yml"
  }

  vpc_config {
    vpc_id             = var.vpc_id
    subnets           = var.private_subnet_ids
    security_group_ids = [aws_security_group.codebuild_sg.id]
  }

  logs_config {
    cloudwatch_logs {
      group_name  = "/aws/codebuild/laravel-${var.environment}"
      stream_name = "build-logs"
    }
  }

  tags = {
    Environment = var.environment
  }
}

resource "aws_security_group" "codebuild_sg" {
  name        = "codebuild-sg-${var.environment}"
  description = "Security group for CodeBuild"
  vpc_id      = var.vpc_id

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

  tags = {
    Environment = var.environment
  }
}

1.2 Buildspec Files

yaml
# .aws/buildspec/buildspec-dev.yml
version: 0.2

env:
  variables:
    ENVIRONMENT: dev
  parameter-store:
    CODEARTIFACT_AUTH_TOKEN: /codeartifact/auth-token
    
phases:
  install:
    runtime-versions:
      php: 8.2
      nodejs: 16
    commands:
      - composer config --global repositories.packagist.org false
      - composer config --global repositories.codeartifact composer "https://${CODEARTIFACT_DOMAIN}.d.codeartifact.${AWS_REGION}.amazonaws.com/composer/${ENVIRONMENT}/"
      - export COMPOSER_AUTH="{\"bearer\":{\"${CODEARTIFACT_DOMAIN}\":\"${CODEARTIFACT_AUTH_TOKEN}\"}}"
      
  pre_build:
    commands:
      - echo "Fetching environment variables from Parameter Store..."
      - aws ssm get-parameters-by-path --path "/${APP_NAME}/${ENVIRONMENT}/" --recursive --with-decryption --query "Parameters[*].{Name:Name,Value:Value}" --output json > env.json
      - jq -r 'to_entries | .[] | .key + "=" + .value' env.json > .env
      
  build:
    commands:
      - echo "Installing dependencies..."
      - composer install --no-interaction --optimize-autoloader
      - npm ci
      - npm run build
      - php artisan config:cache
      - php artisan route:cache
      - php artisan view:cache
      
  post_build:
    commands:
      - echo "Running tests..."
      - ./vendor/bin/phpunit
      - echo "Running Laravel Pint..."
      - ./vendor/bin/pint --test
      
artifacts:
  files:
    - '**/*'
  base-directory: '.'
  
cache:
  paths:
    - '/root/.composer/cache/**/*'
    - 'node_modules/**/*'
yaml
# .aws/buildspec/buildspec-prod.yml
version: 0.2

env:
  variables:
    ENVIRONMENT: prod
  parameter-store:
    CODEARTIFACT_AUTH_TOKEN: /codeartifact/auth-token
    
phases:
  install:
    runtime-versions:
      php: 8.2
      nodejs: 16
    commands:
      - composer config --global repositories.packagist.org false
      - composer config --global repositories.codeartifact composer "https://${CODEARTIFACT_DOMAIN}.d.codeartifact.${AWS_REGION}.amazonaws.com/composer/${ENVIRONMENT}/"
      - export COMPOSER_AUTH="{\"bearer\":{\"${CODEARTIFACT_DOMAIN}\":\"${CODEARTIFACT_AUTH_TOKEN}\"}}"
      
  pre_build:
    commands:
      - echo "Fetching production environment variables..."
      - aws ssm get-parameters-by-path --path "/${APP_NAME}/${ENVIRONMENT}/" --recursive --with-decryption --query "Parameters[*].{Name:Name,Value:Value}" --output json > env.json
      - jq -r 'to_entries | .[] | .key + "=" + .value' env.json > .env
      
  build:
    commands:
      - echo "Installing production dependencies..."
      - composer install --no-dev --no-interaction --optimize-autoloader
      - npm ci --production
      - npm run build
      - php artisan config:cache
      - php artisan route:cache
      - php artisan view:cache
      
  post_build:
    commands:
      - echo "Running security audit..."
      - composer audit
      - npm audit
      - echo "Creating deployment package..."
      - zip -r deployment.zip . -x "tests/*" "*.git*" "node_modules/*"
      
artifacts:
  files:
    - deployment.zip
  
cache:
  paths:
    - '/root/.composer/cache/**/*'

2. Configuración de Pipeline

2.1 IAM Roles

hcl
# .aws/terraform/modules/codebuild/iam.tf
resource "aws_iam_role" "codebuild_role" {
  name = "codebuild-role-${var.environment}"

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

resource "aws_iam_role_policy" "codebuild_policy" {
  role = aws_iam_role.codebuild_role.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ssm:GetParameters",
          "ssm:GetParameter",
          "ssm:GetParametersByPath"
        ]
        Resource = [
          "arn:aws:ssm:${var.region}:${var.account_id}:parameter/${var.app_name}/${var.environment}/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "codeartifact:GetAuthorizationToken",
          "codeartifact:GetRepositoryEndpoint",
          "codeartifact:ReadFromRepository"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "s3:PutObject",
          "s3:GetObject",
          "s3:GetObjectVersion",
          "s3:GetBucketAcl",
          "s3:GetBucketLocation"
        ]
        Resource = [
          "${var.artifact_bucket_arn}",
          "${var.artifact_bucket_arn}/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "ec2:CreateNetworkInterface",
          "ec2:DescribeNetworkInterfaces",
          "ec2:DeleteNetworkInterface",
          "ec2:DescribeSubnets",
          "ec2:DescribeSecurityGroups",
          "ec2:DescribeDhcpOptions",
          "ec2:DescribeVpcs"
        ]
        Resource = "*"
      }
    ]
  })
}

2.2 Scripts de Pipeline

python
# scripts/pipeline/build_validator.py
import boto3
import time

class BuildValidator:
    def __init__(self, project_name, environment):
        self.codebuild = boto3.client('codebuild')
        self.project_name = project_name
        self.environment = environment
        
    def validate_build(self, build_id):
        """Valida el estado de un build"""
        while True:
            response = self.codebuild.batch_get_builds(ids=[build_id])
            build = response['builds'][0]
            status = build['buildStatus']
            
            if status == 'SUCCEEDED':
                print(f"Build completado exitosamente: {build_id}")
                return True
            elif status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']:
                print(f"Build falló: {build_id}")
                print(f"Fase: {build.get('currentPhase')}")
                print(f"Error: {build.get('failureMessage', 'No error message')}")
                return False
                
            print(f"Build en progreso... Estado: {status}")
            time.sleep(30)
            
    def get_build_logs(self, build_id):
        """Obtiene los logs del build"""
        response = self.codebuild.batch_get_builds(ids=[build_id])
        build = response['builds'][0]
        
        logs = {
            'build_id': build_id,
            'project': self.project_name,
            'environment': self.environment,
            'status': build['buildStatus'],
            'start_time': build['startTime'].isoformat(),
            'end_time': build.get('endTime', '').isoformat() if build.get('endTime') else None,
            'phases': []
        }
        
        for phase in build['phases']:
            phase_info = {
                'phase_name': phase['phaseType'],
                'status': phase.get('phaseStatus', 'IN_PROGRESS'),
                'duration': phase.get('durationInSeconds'),
                'messages': phase.get('contextMessages', [])
            }
            logs['phases'].append(phase_info)
            
        return logs

3. Validación y Pruebas

3.1 Script de Pruebas

bash
#!/bin/bash
# scripts/pipeline/run_tests.sh

# Variables
ENVIRONMENT=$1
PROJECT_NAME="laravel-build-${ENVIRONMENT}"

# Validar ambiente
if [[ ! "$ENVIRONMENT" =~ ^(dev|stg|prod)$ ]]; then
    echo "Ambiente inválido. Usar: dev, stg, o prod"
    exit 1
fi

# Ejecutar build
BUILD_ID=$(aws codebuild start-build \
    --project-name $PROJECT_NAME \
    --environment-variables-override \
        name=ENVIRONMENT,value=$ENVIRONMENT,type=PLAINTEXT \
    --query 'build.id' \
    --output text)

echo "Build iniciado: $BUILD_ID"

# Monitorear build
python3 scripts/pipeline/build_validator.py $BUILD_ID $ENVIRONMENT

3.2 Configuración PHPUnit

xml
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

Verificación Parte 2

1. Verificar CodeBuild

  • [ ] Proyectos creados
  • [ ] IAM roles configurados
  • [ ] Buildspecs validados
  • [ ] Caché configurado

2. Verificar Pipeline

  • [ ] Scripts funcionando
  • [ ] Validaciones activas
  • [ ] Logs disponibles
  • [ ] Notificaciones configuradas

3. Verificar Pruebas

  • [ ] PHPUnit configurado
  • [ ] Tests ejecutándose
  • [ ] Cobertura medida
  • [ ] CI funcionando

Troubleshooting Común

Errores de CodeBuild

  1. Verificar permisos IAM
  2. Revisar buildspec
  3. Verificar logs

Errores de Pipeline

  1. Verificar scripts
  2. Revisar validaciones
  3. Verificar notificaciones

Errores de Pruebas

  1. Verificar configuración
  2. Revisar memoria
  3. Verificar dependencias

Parte 3: RDS MySQL y Laravel Integration

1. Configuración de RDS

1.1 Terraform RDS

hcl
# .aws/terraform/modules/rds/main.tf
resource "aws_db_instance" "laravel_db" {
  identifier           = "laravel-db-${var.environment}"
  engine              = "mysql"
  engine_version      = "8.0"
  instance_class      = var.instance_class
  allocated_storage   = var.storage_size
  storage_type        = "gp3"
  
  db_name             = "laravel_${var.environment}"
  username           = var.db_username
  password           = var.db_password
  
  vpc_security_group_ids = [aws_security_group.rds_sg.id]
  db_subnet_group_name   = aws_db_subnet_group.rds_subnet_group.name
  
  backup_retention_period = var.backup_retention_days
  backup_window          = "03:00-04:00"
  maintenance_window     = "Mon:04:00-Mon:05:00"
  
  multi_az               = var.environment == "prod"
  skip_final_snapshot    = var.environment != "prod"
  
  parameter_group_name   = aws_db_parameter_group.laravel_params.name
  
  enabled_cloudwatch_logs_exports = ["error", "general", "slowquery"]
  
  performance_insights_enabled = var.environment == "prod"
  
  tags = {
    Environment = var.environment
    Project     = "Laravel-AWS-App"
  }
}

resource "aws_db_parameter_group" "laravel_params" {
  family = "mysql8.0"
  name   = "laravel-params-${var.environment}"

  parameter {
    name  = "character_set_server"
    value = "utf8mb4"
  }

  parameter {
    name  = "collation_server"
    value = "utf8mb4_unicode_ci"
  }

  parameter {
    name  = "max_connections"
    value = var.environment == "prod" ? "150" : "50"
  }

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

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

1.2 Security Groups

hcl
# .aws/terraform/modules/rds/security.tf
resource "aws_security_group" "rds_sg" {
  name        = "rds-sg-${var.environment}"
  description = "Security group for RDS instance"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [var.app_security_group_id]
  }

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

  tags = {
    Environment = var.environment
  }
}

2. Configuración de Laravel

2.1 Database Config

php
// config/database.php
return [
    'default' => env('DB_CONNECTION', 'mysql'),
    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE'),
            'username' => env('DB_USERNAME'),
            'password' => env('DB_PASSWORD'),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
                PDO::ATTR_PERSISTENT => env('DB_PERSISTENT', false),
            ]) : [],
            'pool' => [
                'enabled' => env('DB_POOL', false),
                'min' => env('DB_POOL_MIN', 2),
                'max' => env('DB_POOL_MAX', 10),
            ],
        ],
    ],
];

2.2 Migration Manager

php
// app/Services/MigrationManager.php
namespace App\Services;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;

class MigrationManager
{
    protected $environment;

    public function __construct()
    {
        $this->environment = app()->environment();
    }

    public function runMigrations()
    {
        try {
            // Verificar conexión
            DB::connection()->getPdo();

            // Backup en producción
            if ($this->environment === 'production') {
                $this->backupDatabase();
            }

            // Ejecutar migraciones
            $output = shell_exec('php artisan migrate --force');
            Log::info("Migrations executed: " . $output);

            // Seed en desarrollo
            if ($this->environment === 'development') {
                $this->runSeeders();
            }

            return true;
        } catch (Exception $e) {
            Log::error("Migration failed: " . $e->getMessage());
            
            if ($this->environment === 'production') {
                $this->notifyAdmins($e->getMessage());
            }

            return false;
        }
    }

    protected function backupDatabase()
    {
        $filename = 'backup-' . date('Y-m-d-H-i-s') . '.sql';
        $command = sprintf(
            'mysqldump -h%s -u%s -p%s %s > %s',
            config('database.connections.mysql.host'),
            config('database.connections.mysql.username'),
            config('database.connections.mysql.password'),
            config('database.connections.mysql.database'),
            storage_path('backups/' . $filename)
        );
        
        exec($command);
    }

    protected function runSeeders()
    {
        shell_exec('php artisan db:seed --force');
    }

    protected function notifyAdmins($error)
    {
        // Implementar notificación
    }
}

2.3 Comandos de Migración

php
// app/Console/Commands/SafeMigrate.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\MigrationManager;

class SafeMigrate extends Command
{
    protected $signature = 'migrate:safe';
    protected $description = 'Safely run migrations with environment checks';

    protected $migrationManager;

    public function __construct(MigrationManager $migrationManager)
    {
        parent::__construct();
        $this->migrationManager = $migrationManager;
    }

    public function handle()
    {
        $this->info('Starting safe migration process...');

        if ($this->migrationManager->runMigrations()) {
            $this->info('Migrations completed successfully.');
            return 0;
        }

        $this->error('Migration failed. Check logs for details.');
        return 1;
    }
}

3. Scripts de Base de Datos

3.1 Script de Backup

bash
#!/bin/bash
# scripts/database/backup.sh

# Variables
DB_INSTANCE="laravel-db-${ENVIRONMENT}"
TIMESTAMP=$(date +%Y-%m-%d-%H-%M-%S)
BACKUP_BUCKET="laravel-db-backups-${ENVIRONMENT}"
BACKUP_FILE="backup-${TIMESTAMP}.sql"

# Crear snapshot
aws rds create-db-snapshot \
    --db-instance-identifier $DB_INSTANCE \
    --db-snapshot-identifier "snapshot-${TIMESTAMP}"

# Backup lógico
mysqldump \
    -h$(aws ssm get-parameter --name "/${APP_NAME}/${ENVIRONMENT}/DB_HOST" --with-decryption --query 'Parameter.Value' --output text) \
    -u$(aws ssm get-parameter --name "/${APP_NAME}/${ENVIRONMENT}/DB_USERNAME" --with-decryption --query 'Parameter.Value' --output text) \
    -p$(aws ssm get-parameter --name "/${APP_NAME}/${ENVIRONMENT}/DB_PASSWORD" --with-decryption --query 'Parameter.Value' --output text) \
    $(aws ssm get-parameter --name "/${APP_NAME}/${ENVIRONMENT}/DB_DATABASE" --with-decryption --query 'Parameter.Value' --output text) \
    > $BACKUP_FILE

# Comprimir backup
gzip $BACKUP_FILE

# Subir a S3
aws s3 cp "${BACKUP_FILE}.gz" "s3://${BACKUP_BUCKET}/daily/${BACKUP_FILE}.gz"

# Limpiar archivos locales
rm "${BACKUP_FILE}.gz"

# Retener últimos 30 snapshots
aws rds describe-db-snapshots \
    --db-instance-identifier $DB_INSTANCE \
    --query 'reverse(sort_by(DBSnapshots, &SnapshotCreateTime))[30:].DBSnapshotIdentifier' \
    --output text | \
    xargs -n 1 aws rds delete-db-snapshot --db-snapshot-identifier

3.2 Script de Restauración

python
# scripts/database/restore.py
import boto3
import click
import time

class DatabaseRestorer:
    def __init__(self, environment):
        self.rds = boto3.client('rds')
        self.s3 = boto3.client('s3')
        self.environment = environment
        self.app_name = 'laravel-aws-app'

    def restore_from_snapshot(self, snapshot_id, new_instance_id):
        """Restaura desde un snapshot de RDS"""
        try:
            response = self.rds.restore_db_instance_from_db_snapshot(
                DBInstanceIdentifier=new_instance_id,
                DBSnapshotIdentifier=snapshot_id,
                AutoMinorVersionUpgrade=True,
                CopyTagsToSnapshot=True,
                PubliclyAccessible=False
            )
            
            self._wait_for_instance(new_instance_id)
            return True
        except Exception as e:
            print(f"Error restoring from snapshot: {str(e)}")
            return False

    def restore_from_backup(self, backup_file, instance_id):
        """Restaura desde un backup lógico"""
        try:
            # Descargar backup
            self.s3.download_file(
                f'laravel-db-backups-{self.environment}',
                f'daily/{backup_file}',
                backup_file
            )
            
            # Descomprimir
            if backup_file.endswith('.gz'):
                import gzip
                with gzip.open(backup_file, 'rb') as f_in:
                    with open(backup_file[:-3], 'wb') as f_out:
                        f_out.write(f_in.read())
                backup_file = backup_file[:-3]
            
            # Obtener credenciales
            ssm = boto3.client('ssm')
            credentials = {
                'host': self._get_parameter('DB_HOST'),
                'user': self._get_parameter('DB_USERNAME'),
                'password': self._get_parameter('DB_PASSWORD'),
                'database': self._get_parameter('DB_DATABASE')
            }
            
            # Restaurar
            import subprocess
            cmd = f"mysql -h{credentials['host']} -u{credentials['user']} -p{credentials['password']} {credentials['database']} < {backup_file}"
            subprocess.run(cmd, shell=True, check=True)
            
            return True
        except Exception as e:
            print(f"Error restoring from backup: {str(e)}")
            return False

    def _wait_for_instance(self, instance_id):
        """Espera a que la instancia esté disponible"""
        while True:
            response = self.rds.describe_db_instances(
                DBInstanceIdentifier=instance_id
            )
            status = response['DBInstances'][0]['DBInstanceStatus']
            
            if status == 'available':
                break
            
            print(f"Waiting for instance... Status: {status}")
            time.sleep(30)

    def _get_parameter(self, param_name):
        """Obtiene parámetro de SSM"""
        ssm = boto3.client('ssm')
        response = ssm.get_parameter(
            Name=f"/{self.app_name}/{self.environment}/{param_name}",
            WithDecryption=True
        )
        return response['Parameter']['Value']

@click.command()
@click.option('--environment', required=True, type=click.Choice(['dev', 'stg', 'prod']))
@click.option('--snapshot-id', help='Snapshot ID for restoration')
@click.option('--backup-file', help='Backup file name for restoration')
@click.option('--instance-id', required=True, help='Target instance ID')
def restore(environment, snapshot_id, backup_file, instance_id):
    restorer = DatabaseRestorer(environment)
    
    if snapshot_id:
        success = restorer.restore_from_snapshot(snapshot_id, instance_id)
    elif backup_file:
        success = restorer.restore_from_backup(backup_file, instance_id)
    else:
        click.echo("Either snapshot-id or backup-file must be provided")
        return
    
    if success:
        click.echo("Restoration completed successfully")
    else:
        click.echo("Restoration failed")

if __name__ == '__main__':
    restore()

Verificación Parte 3

1. Verificar RDS

  • [ ] Instancia creada
  • [ ] Parámetros configurados
  • [ ] Seguridad establecida
  • [ ] Backups funcionando

2. Verificar Laravel

  • [ ] Conexión establecida
  • [ ] Migraciones funcionando
  • [ ] Seeders ejecutándose
  • [ ] Logs disponibles

3. Verificar Scripts

  • [ ] Backup automático
  • [ ] Restauración probada
  • [ ] Rotación funcionando
  • [ ] Monitoreo activo

Troubleshooting Común

Errores de RDS

  1. Verificar conectividad
  2. Revisar credenciales
  3. Verificar seguridad

Errores de Laravel

  1. Verificar configuración
  2. Revisar migraciones
  3. Verificar logs

Errores de Scripts

  1. Verificar permisos
  2. Revisar espacio
  3. Verificar conexión

Parte 4: Monitoreo, Logging y Alertas

1. Configuración de CloudWatch

1.1 Métricas Personalizadas

php
// app/Services/Monitoring/MetricsService.php
namespace App\Services\Monitoring;

use Aws\CloudWatch\CloudWatchClient;
use Illuminate\Support\Facades\Log;

class MetricsService
{
    protected $cloudWatch;
    protected $environment;
    protected $appName;

    public function __construct()
    {
        $this->cloudWatch = new CloudWatchClient([
            'version' => 'latest',
            'region'  => config('services.aws.region')
        ]);
        $this->environment = config('app.env');
        $this->appName = config('app.name');
    }

    public function publishMetric($name, $value, $unit = 'Count', $dimensions = [])
    {
        try {
            $defaultDimensions = [
                [
                    'Name' => 'Environment',
                    'Value' => $this->environment
                ],
                [
                    'Name' => 'Application',
                    'Value' => $this->appName
                ]
            ];

            $this->cloudWatch->putMetricData([
                'Namespace' => 'Laravel/Application',
                'MetricData' => [
                    [
                        'MetricName' => $name,
                        'Value' => $value,
                        'Unit' => $unit,
                        'Dimensions' => array_merge($defaultDimensions, $dimensions),
                        'Timestamp' => time()
                    ]
                ]
            ]);
        } catch (\Exception $e) {
            Log::error("Error publishing metric: {$e->getMessage()}");
        }
    }
}

1.2 Dashboard Principal

yaml
# cloudwatch/dashboards/main_dashboard.yaml
{
    "widgets": [
        {
            "type": "metric",
            "properties": {
                "metrics": [
                    ["Laravel/Application", "RequestCount", "Environment", "${Environment}"],
                    [".", "ResponseTime", ".", "."],
                    [".", "ErrorRate", ".", "."]
                ],
                "period": 300,
                "stat": "Average",
                "region": "${AWS::Region}",
                "title": "Application Metrics"
            }
        },
        {
            "type": "metric",
            "properties": {
                "metrics": [
                    ["AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", "${DBInstance}"],
                    [".", "FreeStorageSpace", ".", "."],
                    [".", "DatabaseConnections", ".", "."]
                ],
                "period": 300,
                "stat": "Average",
                "region": "${AWS::Region}",
                "title": "Database Metrics"
            }
        },
        {
            "type": "metric",
            "properties": {
                "metrics": [
                    ["AWS/CodeBuild", "BuildSuccess", "ProjectName", "${BuildProject}"],
                    [".", "BuildFailure", ".", "."],
                    [".", "BuildDuration", ".", "."]
                ],
                "period": 300,
                "stat": "Sum",
                "region": "${AWS::Region}",
                "title": "CI/CD Metrics"
            }
        }
    ]
}

2. Centralización de Logs

2.1 CloudWatch Logs

php
// config/logging.php
return [
    'channels' => [
        'cloudwatch' => [
            'driver' => 'custom',
            'via' => \App\Logging\CloudWatchLogger::class,
            'level' => env('LOG_LEVEL', 'debug'),
            'group_name' => env('APP_NAME') . '-' . env('APP_ENV'),
            'retention' => 14,
            'batch_size' => 10000,
            'version' => env('APP_VERSION', '1.0.0'),
        ],
        // ...
    ],
];

// app/Logging/CloudWatchLogger.php
namespace App\Logging;

use Aws\CloudWatchLogs\CloudWatchLogsClient;
use Monolog\Handler\CloudWatch\CloudWatchLogsHandler;
use Monolog\Logger;

class CloudWatchLogger
{
    public function __invoke(array $config)
    {
        $client = new CloudWatchLogsClient([
            'version' => 'latest',
            'region'  => config('services.aws.region'),
        ]);

        $handler = new CloudWatchLogsHandler(
            $client,
            $config['group_name'],
            $config['group_name'] . '-stream-' . date('Y-m-d'),
            $config['retention'],
            $config['batch_size'],
            ['environment' => config('app.env'), 'version' => $config['version']],
            Logger::DEBUG
        );

        return new Logger('cloudwatch', [$handler]);
    }
}

2.2 Log Processor

php
// app/Services/Logging/LogProcessor.php
namespace App\Services\Logging;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;

class LogProcessor
{
    public function __invoke(array $record): array
    {
        $record['extra'] = array_merge(
            $record['extra'] ?? [],
            [
                'request_id' => request()->id ?? Str::uuid(),
                'user_id' => Auth::id(),
                'ip' => request()->ip(),
                'user_agent' => request()->userAgent(),
                'session_id' => session()->getId(),
            ]
        );

        return $record;
    }
}

3. Sistema de Alertas

3.1 Configuración de SNS

hcl
# terraform/modules/monitoring/sns.tf
resource "aws_sns_topic" "alerts" {
  name = "application-alerts-${var.environment}"
  
  tags = {
    Environment = var.environment
    Project     = var.project_name
  }
}

resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email
}

3.2 Alarmas

hcl
# terraform/modules/monitoring/alarms.tf
resource "aws_cloudwatch_metric_alarm" "high_error_rate" {
  alarm_name          = "high-error-rate-${var.environment}"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "ErrorRate"
  namespace           = "Laravel/Application"
  period              = "300"
  statistic           = "Average"
  threshold           = "5"
  alarm_description   = "Application error rate is too high"
  alarm_actions       = [aws_sns_topic.alerts.arn]
  
  dimensions = {
    Environment = var.environment
    Application = var.project_name
  }
}

resource "aws_cloudwatch_metric_alarm" "database_connections" {
  alarm_name          = "high-db-connections-${var.environment}"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "3"
  metric_name         = "DatabaseConnections"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = var.max_db_connections
  alarm_description   = "Database connection count is too high"
  alarm_actions       = [aws_sns_topic.alerts.arn]
  
  dimensions = {
    DBInstanceIdentifier = var.db_instance_id
  }
}

4. Healthchecks y Monitoreo de Aplicación

4.1 Healthcheck Controller

php
// app/Http/Controllers/HealthController.php
namespace App\Http\Controllers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class HealthController extends Controller
{
    public function check()
    {
        $status = [
            'status' => 'ok',
            'timestamp' => now()->toIso8601String(),
            'services' => []
        ];

        // Verificar DB
        try {
            DB::connection()->getPdo();
            $status['services']['database'] = 'healthy';
        } catch (\Exception $e) {
            $status['services']['database'] = 'unhealthy';
            $status['status'] = 'error';
            Log::error('Database health check failed: ' . $e->getMessage());
        }

        // Verificar Cache
        try {
            Cache::set('health_check', true, 10);
            $status['services']['cache'] = 'healthy';
        } catch (\Exception $e) {
            $status['services']['cache'] = 'unhealthy';
            $status['status'] = 'error';
            Log::error('Cache health check failed: ' . $e->getMessage());
        }

        // Verificar Storage
        try {
            if (storage_path() && is_writable(storage_path())) {
                $status['services']['storage'] = 'healthy';
            } else {
                throw new \Exception('Storage not writable');
            }
        } catch (\Exception $e) {
            $status['services']['storage'] = 'unhealthy';
            $status['status'] = 'error';
            Log::error('Storage health check failed: ' . $e->getMessage());
        }

        return response()->json($status)
            ->setStatusCode($status['status'] === 'ok' ? 200 : 503);
    }
}

4.2 Middleware de Performance

php
// app/Http/Middleware/PerformanceMonitoring.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use App\Services\Monitoring\MetricsService;

class PerformanceMonitoring
{
    protected $metrics;

    public function __construct(MetricsService $metrics)
    {
        $this->metrics = $metrics;
    }

    public function handle(Request $request, Closure $next)
    {
        $startTime = microtime(true);
        
        $response = $next($request);
        
        $duration = microtime(true) - $startTime;
        
        // Publicar métricas
        $this->metrics->publishMetric('ResponseTime', $duration * 1000, 'Milliseconds');
        $this->metrics->publishMetric('RequestCount', 1);
        
        if ($response->getStatusCode() >= 400) {
            $this->metrics->publishMetric('ErrorCount', 1);
        }
        
        return $response;
    }
}

Verificación Final

1. Verificar Monitoreo

  • [ ] Métricas registradas
  • [ ] Dashboard funcional
  • [ ] Alarmas activas
  • [ ] Notificaciones configuradas

2. Verificar Logs

  • [ ] Logs centralizados
  • [ ] Formato correcto
  • [ ] Retención configurada
  • [ ] Búsqueda funcional

3. Verificar Healthchecks

  • [ ] Endpoints respondiendo
  • [ ] Servicios monitoreados
  • [ ] Métricas capturadas
  • [ ] Alertas funcionando

Troubleshooting Final

Problemas de Monitoreo

  1. Verificar permisos IAM
  2. Revisar métricas
  3. Verificar dashboards

Problemas de Logs

  1. Verificar streaming
  2. Revisar retención
  3. Verificar formato

Problemas de Alertas

  1. Verificar SNS
  2. Revisar umbrales
  3. Verificar suscripciones

Puntos Importantes

  1. Monitoreo proactivo
  2. Logs centralizados
  3. Alertas efectivas
  4. Healthchecks regulares

Este ejercicio completo proporciona:

  1. Monitoreo integral con CloudWatch
  2. Logging centralizado
  3. Sistema de alertas
  4. Healthchecks de aplicación

Puntos clave para recordar:

  • Monitoreo proactivo es esencial
  • Los logs deben estar centralizados
  • Las alertas deben ser relevantes
  • Los healthchecks deben ser completos

El sistema completo permite:

  • Detectar problemas temprano
  • Investigar issues rápidamente
  • Mantener la aplicación saludable
  • Responder a incidentes eficientemente