Lab 0: Prerrequisitos

Lab 0: Prerrequisitos


¿Dónde correr este workshop?

Este workshop es una experiencia volátil diseñada para empezar y concluir en el día. Por este motivo, el estado de Terraform se guarda localmente en archivos terraform.tfstate en el directorio de cada módulo, sin requerir buckets S3 ni almacenamiento remoto.

Aviso

¡Cuidado con los archivos terraform.tfstate y AWS CloudShell!
Si trabajás en AWS CloudShell, tené en cuenta que el almacenamiento es temporal y la sesión puede resetearse. Si la sesión expira o se reinicia, los archivos terraform.tfstate se perderán. Sin ellos, Terraform ya no sabrá qué recursos existen en AWS y no podrás hacer plan, apply ni destroy automáticamente, teniendo que borrarlos manualmente desde la consola.
Nunca borres los archivos terraform.tfstate ni las carpetas del workshop mientras tengas recursos desplegados.


Paso 0: Crear un usuario IAM para el workshop

Consejo

Recomendación de seguridad: No uses las credenciales de tu usuario root ni de un usuario con AdministratorAccess para el workshop. Creá un usuario IAM dedicado con solo los permisos necesarios. Así limitás el radio de impacto si algo sale mal.

¿Por qué un usuario IAM dedicado?

Este workshop crea y destruye recursos reales en AWS (EC2, ALB, CloudFront, Lambda, Route 53, etc.). Usar un usuario con permisos mínimos es una buena práctica que también aplica a entornos de producción.

Crear el usuario IAM desde la consola

  1. Abrí la consola de AWS con tu usuario administrador o root.
  2. Ir a IAM → Users → Create user.
  3. Nombre de usuario: workshop-route53-arc (o el que prefieras).
  4. No habilitar acceso a la consola de administración (solo programático es suficiente para CLI/Terraform).
    • Si vas a usar CloudShell, no necesitás access keys — CloudShell usa las credenciales de la sesión de consola activa. En ese caso sí habilitá acceso a la consola.
  5. En permisos, elegí “Attach policies directly” y creá una política inline o gestionada con el JSON de más abajo.
  6. Finalizá la creación y anotá el Access Key ID y Secret Access Key (solo se muestran una vez).
    💾 La consola ofrece descargar un archivo .csv — hacé clic en “Download .csv” antes de cerrar la ventana. Si cerrás sin guardar, tenés que generar un par nuevo.

Política IAM mínima para el workshop

Creá una política nueva llamada WorkshopRoute53ARC con este JSON:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EC2",
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "ec2:CreateVpc", "ec2:DeleteVpc",
        "ec2:CreateSubnet", "ec2:DeleteSubnet",
        "ec2:CreateInternetGateway", "ec2:DeleteInternetGateway",
        "ec2:AttachInternetGateway", "ec2:DetachInternetGateway",
        "ec2:CreateRouteTable", "ec2:DeleteRouteTable",
        "ec2:CreateRoute", "ec2:DeleteRoute",
        "ec2:AssociateRouteTable", "ec2:DisassociateRouteTable",
        "ec2:CreateSecurityGroup", "ec2:DeleteSecurityGroup",
        "ec2:AuthorizeSecurityGroupIngress", "ec2:AuthorizeSecurityGroupEgress",
        "ec2:RevokeSecurityGroupIngress", "ec2:RevokeSecurityGroupEgress",
        "ec2:RunInstances", "ec2:TerminateInstances",
        "ec2:StartInstances", "ec2:StopInstances",
        "ec2:ModifyInstanceAttribute",
        "ec2:CreateTags", "ec2:DeleteTags",
        "ec2:ModifyVpcAttribute",
        "ec2:ModifyVpcAttribute",
        "ec2:ModifySubnetAttribute",
        "ec2:Get*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ELB",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "S3",
      "Effect": "Allow",
      "Action": [
        "s3:Get*",
        "s3:List*",
        "s3:CreateBucket", "s3:DeleteBucket",
        "s3:PutBucketTagging", "s3:DeleteBucketTagging",
        "s3:PutBucketPolicy", "s3:DeleteBucketPolicy",
        "s3:PutBucketPublicAccessBlock",
        "s3:PutBucketOwnershipControls",
        "s3:PutBucketVersioning",
        "s3:PutObject", "s3:DeleteObject", "s3:DeleteObjects",
        "s3:PutObjectTagging"
      ],
      "Resource": "*"
    },

    {
      "Sid": "CloudFront",
      "Effect": "Allow",
      "Action": [
        "cloudfront:CreateDistribution",
        "cloudfront:UpdateDistribution",
        "cloudfront:DeleteDistribution",
        "cloudfront:GetDistribution",
        "cloudfront:GetDistributionConfig",
        "cloudfront:ListDistributions",
        "cloudfront:CreateOriginAccessControl",
        "cloudfront:DeleteOriginAccessControl",
        "cloudfront:GetOriginAccessControl",
        "cloudfront:ListOriginAccessControls",
        "cloudfront:TagResource",
        "cloudfront:UntagResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Route53",
      "Effect": "Allow",
      "Action": [
        "route53:CreateHostedZone", "route53:DeleteHostedZone",
        "route53:GetHostedZone", "route53:ListHostedZones",
        "route53:ChangeResourceRecordSets",
        "route53:ListResourceRecordSets",
        "route53:CreateHealthCheck", "route53:DeleteHealthCheck",
        "route53:GetHealthCheck", "route53:ListHealthChecks",
        "route53:UpdateHealthCheck",
        "route53:ChangeTagsForResource",
        "route53:ListTagsForResource",
        "route53:GetChange"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Route53ARC",
      "Effect": "Allow",
      "Action": [
        "route53-recovery-control-config:*",
        "route53-recovery-cluster:*",
        "route53-recovery-readiness:*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "APIGateway",
      "Effect": "Allow",
      "Action": [
        "apigateway:*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CloudWatch",
      "Effect": "Allow",
      "Action": [
        "cloudwatch:PutMetricAlarm",
        "cloudwatch:DeleteAlarms",
        "cloudwatch:DescribeAlarms",
        "cloudwatch:SetAlarmState",
        "cloudwatch:GetMetricData",
        "cloudwatch:ListTagsForResource",
        "cloudwatch:TagResource",
        "logs:CreateLogGroup", "logs:DeleteLogGroup",
        "logs:CreateLogStream", "logs:PutLogEvents",
        "logs:DescribeLogGroups", "logs:DescribeLogStreams",
        "logs:GetLogEvents", "logs:FilterLogEvents"
      ],
      "Resource": "*"
    },
    {
      "Sid": "SNS",
      "Effect": "Allow",
      "Action": [
        "sns:CreateTopic", "sns:DeleteTopic",
        "sns:GetTopicAttributes", "sns:SetTopicAttributes",
        "sns:ListTopics",
        "sns:Subscribe", "sns:Unsubscribe",
        "sns:GetSubscriptionAttributes",
        "sns:ListSubscriptionsByTopic",
        "sns:Publish",
        "sns:TagResource", "sns:ListTagsForResource", "sns:UntagResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Lambda",
      "Effect": "Allow",
      "Action": [
        "lambda:CreateFunction", "lambda:DeleteFunction",
        "lambda:GetFunction", "lambda:GetFunctionCodeSigningConfig",
        "lambda:GetPolicy", "lambda:ListVersionsByFunction",
        "lambda:ListTagsForResource", "lambda:TagResource", "lambda:UntagResource",
        "lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration",
        "lambda:InvokeFunction", "lambda:AddPermission", "lambda:RemovePermission",
        "lambda:CreateFunctionUrlConfig", "lambda:GetFunctionUrlConfig",
        "lambda:DeleteFunctionUrlConfig", "lambda:UpdateFunctionUrlConfig"
      ],
      "Resource": "*"
    },
    {
      "Sid": "IAM",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole", "iam:DeleteRole",
        "iam:GetRole", "iam:ListRoles",
        "iam:PassRole",
        "iam:AttachRolePolicy", "iam:DetachRolePolicy",
        "iam:PutRolePolicy", "iam:DeleteRolePolicy",
        "iam:GetRolePolicy",
        "iam:CreateInstanceProfile", "iam:DeleteInstanceProfile",
        "iam:AddRoleToInstanceProfile", "iam:RemoveRoleFromInstanceProfile",
        "iam:GetInstanceProfile", "iam:ListInstanceProfilesForRole",
        "iam:TagRole", "iam:UntagRole",
        "iam:TagInstanceProfile", "iam:UntagInstanceProfile",
        "iam:ListAttachedRolePolicies",
        "iam:ListRolePolicies",
        "iam:CreateServiceLinkedRole"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ACM",
      "Effect": "Allow",
      "Action": [
        "acm:RequestCertificate",
        "acm:DescribeCertificate",
        "acm:DeleteCertificate",
        "acm:ListCertificates",
        "acm:ListTagsForCertificate",
        "acm:AddTagsToCertificate",
        "acm:RemoveTagsFromCertificate"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Amplify",
      "Effect": "Allow",
      "Action": [
        "amplify:CreateApp", "amplify:DeleteApp", "amplify:GetApp", "amplify:ListApps", "amplify:UpdateApp",
        "amplify:CreateBranch", "amplify:DeleteBranch", "amplify:GetBranch", "amplify:ListBranches", "amplify:UpdateBranch",
        "amplify:CreateDeployment", "amplify:StartDeployment", "amplify:StopJob", "amplify:GetJob",
        "amplify:CreateDomainAssociation", "amplify:DeleteDomainAssociation", "amplify:GetDomainAssociation", "amplify:UpdateDomainAssociation", "amplify:ListDomainAssociations",
        "amplify:TagResource", "amplify:UntagResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "SSM",
      "Effect": "Allow",
      "Action": [
        "ssm:SendCommand",
        "ssm:GetCommandInvocation",
        "ssm:DescribeInstanceInformation",
        "ssm:CreateDocument", "ssm:DeleteDocument",
        "ssm:DescribeDocument", "ssm:GetDocument",
        "ssm:DescribeDocumentPermission", "ssm:ModifyDocumentPermission",
        "ssm:StartAutomationExecution", "ssm:GetAutomationExecution",
        "ssm:AddTagsToResource", "ssm:RemoveTagsFromResource", "ssm:ListTagsForResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Synthetics",
      "Effect": "Allow",
      "Action": [
        "synthetics:CreateCanary", "synthetics:DeleteCanary",
        "synthetics:GetCanary", "synthetics:GetCanaryRuns",
        "synthetics:StartCanary", "synthetics:StopCanary",
        "synthetics:UpdateCanary", "synthetics:TagResource", "synthetics:UntagResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "STSReadOnly",
      "Effect": "Allow",
      "Action": [
        "sts:GetCallerIdentity"
      ],
      "Resource": "*"
    }
  ]
}
Información

Terraform también necesita describir recursos existentes (Describe*) para gestionar el state. Los permisos de lectura incluidos en cada sección cubren esto.

Cómo crear la política en la consola

  1. IAM → Policies → Create policy.
  2. Pestaña JSON → pegá el bloque de arriba.
  3. Nombre: WorkshopRoute53ARC → Create policy.
  4. Volvé al usuario workshop-route53-arc → Add permissions → Attach policies directly → buscá WorkshopRoute53ARC → Add permissions.

Opción alternativa: crear usuario y política con AWS CLI

Si ya tenés un usuario administrador configurado en CLI:

# 1. Crear usuario
aws iam create-user --user-name workshop-route53-arc

# 2. Crear la política (guardá el JSON en un archivo primero)
cat > /tmp/workshop-policy.json << 'EOF'
<PEGAR_EL_JSON_DE_ARRIBA>
EOF

POLICY_ARN=$(aws iam create-policy \
  --policy-name WorkshopRoute53ARC \
  --policy-document file:///tmp/workshop-policy.json \
  --query 'Policy.Arn' --output text)

# 3. Asignar al usuario
aws iam attach-user-policy \
  --user-name workshop-route53-arc \
  --policy-arn $POLICY_ARN

# 4. Crear access keys
aws iam create-access-key \
  --user-name workshop-route53-arc \
  --query 'AccessKey.{ID:AccessKeyId,Secret:SecretAccessKey}' \
  --output table
Aviso

Guardá las credenciales en el momento. El Secret Access Key solo se muestra una vez. Si lo perdés, tenés que crear un nuevo par de keys.


Instalación de Terraform

Utilizá las instrucciones oficiales según tu sistema operativo o entorno:

CloudShell: Amazon Linux 2023

Aviso

Peligro de pérdida de datos: Al utilizar CloudShell, recordá respaldar o mantener activa la sesión para evitar perder los archivos terraform.tfstate al resetearse el entorno.

sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo dnf -y install terraform

macOS

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

Ubuntu

sudo apt-get update && sudo apt-get install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | \
gpg --dearmor | \
sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
gpg --no-default-keyring \
--keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \
--fingerprint
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt-get install terraform

Verificar versiones

terraform version
# Terraform v1.x.x

aws --version
# aws-cli/2.x.x

python3 --version
# Python 3.x.x

Paso 1: Configurar AWS CLI con el usuario del workshop

Usá las credenciales del usuario IAM workshop-route53-arc que creaste en el paso anterior.

Opción A: AWS CLI local (macOS / Ubuntu / CloudShell con keys)

aws configure --profile workshop
# AWS Access Key ID:     AKIA...  ← el del usuario workshop-route53-arc
# AWS Secret Access Key: ****
# Default region name:   us-east-1
# Default output format: json

Exportá el perfil para que todos los comandos del workshop lo usen por defecto:

export AWS_PROFILE=workshop

Verificar que las credenciales funcionan y que apuntan al usuario correcto:

aws sts get-caller-identity
# Deberías ver el ARN del usuario workshop-route53-arc

Salida esperada:

{
    "UserId": "AIDA...",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/workshop-route53-arc"
}

Opción B: AWS CloudShell (sin access keys)

Si vas a usar CloudShell, no necesitás configurar credenciales. CloudShell usa automáticamente las credenciales de la sesión de consola activa.

  1. Iniciá sesión en la consola AWS con el usuario workshop-route53-arc.
  2. Abrí CloudShell (ícono de terminal en la barra superior de la consola).
  3. Verificá la identidad:
aws sts get-caller-identity
Aviso

CloudShell + estado local: Aunque no necesitás keys, recordá que los archivos terraform.tfstate se pueden perder si la sesión de CloudShell se resetea. Leé las advertencias al inicio de este lab.


Paso 2: Directorio de trabajo

Todos los módulos Terraform viven en una carpeta de trabajo. Esto es lo que no debes perder:

mkdir -p ~/route53-arc-tf/{01_primary,02_secondary,03_arc,04_automation}
cd ~/route53-arc-tf
echo "✅ Workspace listo"

Estructura que tendrás al final del workshop:

~/route53-arc-tf/
├── global.env           Variables compartidas
├── 01_primary/
│   ├── main.tf          Configuración infraestructura sitio primario (EC2 + ALB + VPC)
│   ├── variables.tf     Definición de variables del módulo
│   ├── outputs.tf       Valores exportados (DNS ALB, Instance ID)
│   ├── userdata.sh      Script de bootstrap de Nginx y metadata
│   └── terraform.tfstate   ← NO borrar (Estado local)
├── 02_secondary/
│   ├── main.tf          Configuración sitio secundario (S3 + CloudFront)
│   ├── variables.tf     Definición de variables del módulo
│   ├── outputs.tf       Valores exportados (Dominio CloudFront, Bucket)
│   └── terraform.tfstate   ← NO borrar (Estado local)
├── 03_arc/
│   ├── main.tf          Configuración ARC Cluster, Routing Controls, Health Checks y DNS
│   ├── variables.tf     Definición de variables del módulo
│   ├── outputs.tf       Valores exportados (Endpoints ARC, ARNs Controls)
│   └── terraform.tfstate   ← NO borrar (Estado local)
└── 04_automation/
    ├── main.tf          Configuración SNS + Lambda Failover + Alarma CloudWatch
    ├── variables.tf     Definición de variables del módulo
    ├── outputs.tf       Valores exportados (ARN SNS, Nombre Lambda)
    ├── failover.zip     Paquete de código de la función Lambda
    └── terraform.tfstate   ← NO borrar (Estado local)

Paso 3: Verificar VPC Default y Crear Archivo de Variables Globales

¿Por qué creamos global.env?

Este archivo sirve para centralizar las variables de entorno compartidas por los 4 laboratorios (como la región de AWS, el ID de tu cuenta, el modo de VPC y la configuración de dominio). Al cargarlo con source global.env al inicio de cada lab, evitamos la necesidad de ingresar o modificar manualmente estos parámetros en cada paso.

Verificación de VPC Default

En AWS, cada región suele tener una VPC Default creada por defecto. Sin embargo, en algunas cuentas de producción o sandbox esta VPC puede haber sido eliminada. Para verificar si tu región actual dispone de una VPC Default antes de configurar tu entorno, ejecutá:

aws ec2 describe-vpcs --filters "Name=is-default,Values=true" --query "Vpcs[0].VpcId" --output text
  • Si devuelve un ID (ej: vpc-0a1b2c3d): Tenés una VPC Default funcional. Podés usar VPC_MODE="default".
  • Si devuelve None o nada: No tenés VPC Default. Debes usar VPC_MODE="new" para que Terraform cree automáticamente una VPC nueva con sus subredes e Internet Gateway.
Modo VPC ¿Qué usa? ¿Qué crea Terraform? ¿Cuándo usarlo?
default VPC Default existente con sus subredes Nada de red La mayoría de cuentas AWS la tienen
new — VPC /16 + 2 subredes públicas + Internet Gateway + Route Table Cuentas sandbox sin VPC Default

Crear global.env

El siguiente bloque detecta automáticamente si tenés VPC Default y genera el archivo de variables. Copiá y pegá el bloque completo en tu terminal:

DEFAULT_VPC_CHECK=$(aws ec2 describe-vpcs \
  --filters "Name=is-default,Values=true" \
  --query "Vpcs[0].VpcId" --output text 2>/dev/null)

if [ "$DEFAULT_VPC_CHECK" != "None" ] && [ -n "$DEFAULT_VPC_CHECK" ]; then
  AUTO_VPC_MODE="default"
  echo "VPC Default detectada: $DEFAULT_VPC_CHECK"
else
  AUTO_VPC_MODE="new"
  echo "No se encontró VPC Default. Se creará VPC nueva automáticamente."
fi

# Detectar si estamos en CloudShell para no sobreescribir las credenciales de sesión
if [ -n "$AWS_EXECUTION_ENV" ] && echo "$AWS_EXECUTION_ENV" | grep -q "CloudShell"; then
  IS_CLOUDSHELL=true
  echo "☁️  Entorno CloudShell detectado — se omite AWS_PROFILE"
else
  IS_CLOUDSHELL=false
fi

cat > ~/route53-arc-tf/global.env << EOF
export AWS_REGION="us-east-1"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"

# Perfil AWS CLI configurado en el Paso 1
# Terraform lo usa automáticamente para autenticarse
# (se omite automáticamente en CloudShell)
$([ "$IS_CLOUDSHELL" = "false" ] && echo 'export AWS_PROFILE="workshop"' || echo '# AWS_PROFILE omitido (CloudShell)')

# VPC: "default" = reutilizar VPC default existente | "new" = crear VPC nueva
export VPC_MODE="${AUTO_VPC_MODE}"

# Dominio (opcional)
# "" = modo demo sin DNS público
# "tudominio.com" = dominio real con Public Hosted Zone en Route 53
export DOMAIN_NAME=""
export APP_SUBDOMAIN="app"
export HOSTED_ZONE_ID=""

# Email para alertas SNS (reemplazá con tu email real antes del Lab 4)
export NOTIFICATION_EMAIL="tu-email@ejemplo.com"

# Tipo de instancia EC2 (t4g.micro = ARM, t3.micro = x86)
export INSTANCE_TYPE="t4g.micro"
EOF

source ~/route53-arc-tf/global.env
echo "Account: $AWS_ACCOUNT_ID | Region: $AWS_REGION | VPC_MODE: $VPC_MODE"
Información

¿Por qué AWS_PROFILE en global.env?
Terraform no usa automáticamente el perfil configurado en la sesión de shell. Al incluir AWS_PROFILE="workshop" en global.env y ejecutar source global.env al inicio de cada lab, Terraform toma las credenciales del perfil correcto sin ninguna configuración adicional.
Si usás CloudShell: El script detecta automáticamente que estás en CloudShell y omite AWS_PROFILE — CloudShell usa las credenciales de sesión activa y no necesita perfil.

Agregá este source a tu .zshrc / .bashrc si querés que cargue automáticamente en cada sesión:

echo 'source ~/route53-arc-tf/global.env 2>/dev/null' >> ~/.zshrc

✅ Verificación del Lab 0

echo "=== Verificación Lab 0 ==="

terraform version | head -1 \
  && echo "✅ Terraform OK" || echo "❌ Terraform no instalado"

aws sts get-caller-identity --query Account --output text > /dev/null \
  && echo "✅ AWS CLI autenticado" || echo "❌ AWS CLI sin credenciales"

# Verificar que estamos usando el usuario correcto (no root)
CALLER_ARN=$(aws sts get-caller-identity --query Arn --output text)
echo "🔑 Identidad activa: $CALLER_ARN"
echo "$CALLER_ARN" | grep -q "root" \
  && echo "⚠️  Estás usando root — se recomienda usar el usuario workshop-route53-arc" \
  || echo "✅ No es root"

echo "Account: $AWS_ACCOUNT_ID | Region: $AWS_REGION | VPC_MODE: $VPC_MODE"

Siguiente paso → Lab 1: Sitio Primario