Lab práctico · Semana 10: Resiliencia, operaciones y costes: Well-Architected, DR, observabilidad, IaC y FinOps

Infraestructura como código con CloudFormation: stacks, change sets, drift y rollback

⏱ 60-90 minDificultad: mediaTask statements: 2.24.1

Qué vas a construir

Un stack de CloudFormation con varios recursos gobernados por una sola plantilla, y vas a practicar las operaciones que pregunta el examen:

  1. Una plantilla con Parameters, Mappings, Conditions, Resources y Outputs.
  2. Crear el stack para el entorno dev.
  3. Previsualizar el paso a prod con un change set y detectar qué recursos se reemplazan.
  4. Provocar un cambio manual y encontrarlo con drift detection.
  5. Activar la protección contra borrado (termination protection).
  6. Ver un rollback automático cuando una creación falla.
flowchart LR
  T["plantilla.yaml"] --> V["validate-template"]
  V --> C["create-stack (dev)"]
  C --> CS["change set a prod"]
  CS -->|"revisar y ejecutar"| U["stack actualizado"]
  U --> M["cambio manual fuera de CFN"]
  M --> D["detect-stack-drift"]
  D --> L["Limpieza: delete-stack"]

Antes de empezar

  • Usuario de IAM Identity Center o de IAM con MFA (no el raíz) con permisos sobre CloudFormation, S3, SNS, SSM, CloudWatch y CloudWatch Logs.
  • AWS CloudShell en la región eu-south-2 (España).
export AWS_REGION=eu-south-2
export PILA=lab19
mkdir -p ~/lab19 && cd ~/lab19

Paso 1: escribir la plantilla

Qué y por qué: una plantilla parametrizada permite desplegar entornos idénticos (desarrollo, producción, región de DR) sin copiar y pegar. Fíjate en cada sección:

  • Parameters: el entorno se elige al desplegar.
  • Mappings: tabla con la retención de logs por entorno.
  • Conditions: la alarma solo se crea en prod.
  • Resources: bucket cifrado y versionado, tema SNS, parámetro de SSM, grupo de logs y alarma.
  • Outputs: valores útiles; el nombre del bucket se exporta para que otros stacks lo importen con Fn::ImportValue.
cat > plantilla.yaml <<'EOF'
AWSTemplateFormatVersion: "2010-09-09"
Description: Lab 19 - entorno base gobernado por CloudFormation

Parameters:
  Entorno:
    Type: String
    AllowedValues: [dev, prod]
    Default: dev
    Description: Entorno que se despliega

Mappings:
  Retencion:
    dev:
      Dias: 7
    prod:
      Dias: 30

Conditions:
  EsProd: !Equals [!Ref Entorno, prod]

Resources:
  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Proyecto
          Value: lab19
        - Key: Entorno
          Value: !Ref Entorno

  Tema:
    Type: AWS::SNS::Topic
    Properties:
      Tags:
        - Key: Proyecto
          Value: lab19

  Parametro:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub /lab19/${Entorno}/bucket
      Type: String
      Value: !Ref Bucket
      Description: Nombre del bucket del entorno

  Logs:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /lab19/${Entorno}/app
      RetentionInDays: !FindInMap [Retencion, !Ref Entorno, Dias]

  AlarmaFallosSNS:
    Type: AWS::CloudWatch::Alarm
    Condition: EsProd
    Properties:
      AlarmDescription: Hay notificaciones SNS fallidas
      Namespace: AWS/SNS
      MetricName: NumberOfNotificationsFailed
      Dimensions:
        - Name: TopicName
          Value: !GetAtt Tema.TopicName
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 0
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: notBreaching
      AlarmActions:
        - !Ref Tema

Outputs:
  NombreBucket:
    Description: Bucket del entorno
    Value: !Ref Bucket
    Export:
      Name: !Sub ${AWS::StackName}-bucket
  ArnTema:
    Description: Tema de alertas
    Value: !Ref Tema
EOF

aws cloudformation validate-template --template-body file://plantilla.yaml

validate-template comprueba la sintaxis y devuelve los parámetros detectados. No valida que los valores sean correctos para cada servicio: eso se descubre al desplegar.

Paso 2: crear el stack de desarrollo

aws cloudformation create-stack --stack-name $PILA \
  --template-body file://plantilla.yaml \
  --parameters ParameterKey=Entorno,ParameterValue=dev \
  --tags Key=Proyecto,Value=lab19

aws cloudformation wait stack-create-complete --stack-name $PILA

aws cloudformation describe-stacks --stack-name $PILA \
  --query 'Stacks[0].[StackStatus,Outputs]'

aws cloudformation list-stack-resources --stack-name $PILA \
  --query 'StackResourceSummaries[*].[LogicalResourceId,ResourceType,ResourceStatus]' --output table

Observa que no existe AlarmaFallosSNS: la condición EsProd es falsa. Abre también la consola de CloudFormation y revisa la pestaña Events: verás el orden en el que se crearon los recursos según sus dependencias (el parámetro de SSM espera al bucket porque usa !Ref Bucket).

Paso 3: previsualizar el cambio a producción con un change set

Qué y por qué: en producción nunca actualices a ciegas. Un change set muestra qué va a pasar con cada recurso antes de ejecutarlo, incluido si se reemplaza (se borra y se crea otro: en una base de datos implicaría perder datos si no tienes DeletionPolicy o snapshot).

aws cloudformation create-change-set --stack-name $PILA \
  --change-set-name a-prod \
  --use-previous-template \
  --parameters ParameterKey=Entorno,ParameterValue=prod

aws cloudformation wait change-set-create-complete --stack-name $PILA --change-set-name a-prod

aws cloudformation describe-change-set --stack-name $PILA --change-set-name a-prod \
  --query 'Changes[*].ResourceChange.[Action,LogicalResourceId,ResourceType,Replacement]' \
  --output table

Interpreta la tabla:

  • Add para AlarmaFallosSNS (ahora la condición es verdadera).
  • Modify con Replacement = True para Parametro y Logs: su nombre cambia (/lab19/prod/...) y ese atributo no se puede modificar en caliente, así que CloudFormation creará recursos nuevos y borrará los viejos.
  • Modify sin reemplazo para el bucket (cambia una etiqueta).

Si te convence, ejecútalo:

aws cloudformation execute-change-set --stack-name $PILA --change-set-name a-prod
aws cloudformation wait stack-update-complete --stack-name $PILA

aws ssm get-parameter --name /lab19/prod/bucket --query Parameter.Value --output text
aws cloudwatch describe-alarms --alarm-name-prefix $PILA --query 'MetricAlarms[*].[AlarmName,StateValue]'

Paso 4: provocar deriva y detectarla (drift detection)

Qué y por qué: alguien «arregla algo rápido» en la consola y el recurso deja de coincidir con la plantilla. Drift detection compara la configuración real con la esperada. Es una estrategia de automatización para garantizar la integridad de la infraestructura (infrastructure integrity).

Suspende el versionado del bucket fuera de CloudFormation:

export BUCKET=$(aws cloudformation describe-stacks --stack-name $PILA \
  --query "Stacks[0].Outputs[?OutputKey=='NombreBucket'].OutputValue" --output text)

aws s3api put-bucket-versioning --bucket $BUCKET \
  --versioning-configuration Status=Suspended

Lanza la detección y espera el resultado:

DETECCION=$(aws cloudformation detect-stack-drift --stack-name $PILA \
  --query StackDriftDetectionId --output text)

until [ "$(aws cloudformation describe-stack-drift-detection-status \
  --stack-drift-detection-id $DETECCION --query DetectionStatus --output text)" != "DETECTION_IN_PROGRESS" ]; do
  sleep 5
done

aws cloudformation describe-stack-drift-detection-status \
  --stack-drift-detection-id $DETECCION --query '[StackDriftStatus,DriftedStackResourceCount]'

aws cloudformation describe-stack-resource-drifts --stack-name $PILA \
  --stack-resource-drift-status-filters MODIFIED \
  --query 'StackResourceDrifts[*].[LogicalResourceId,PropertyDifferences[*].[PropertyPath,ExpectedValue,ActualValue]]'

Verás DRIFTED y la diferencia en /VersioningConfiguration/Status (esperado Enabled, real Suspended). Corrige la deriva devolviendo el recurso a lo que dice la plantilla:

aws s3api put-bucket-versioning --bucket $BUCKET \
  --versioning-configuration Status=Enabled

Ojo: una actualización del stack no corrige la deriva por sí sola si la plantilla no cambia esa propiedad. Para detectar cambios de configuración de forma continua y en toda la cuenta, usarías AWS Config (módulo 07).

Paso 5: protección contra borrado

aws cloudformation update-termination-protection --stack-name $PILA \
  --enable-termination-protection

aws cloudformation delete-stack --stack-name $PILA

El borrado falla con un error que menciona la protección. Es la red de seguridad para stacks de producción. Desactívala para poder limpiar al final:

aws cloudformation update-termination-protection --stack-name $PILA \
  --no-enable-termination-protection

Paso 6: rollback automático ante un error

Qué y por qué: si un recurso no se puede crear, CloudFormation deshace todo lo creado en esa operación. Crea un stack con un nombre de bucket inválido (mayúsculas y guiones bajos no están permitidos):

cat > fallo.yaml <<'EOF'
Resources:
  TemaBueno:
    Type: AWS::SNS::Topic
  BucketMalo:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: Nombre_No_Valido
EOF

aws cloudformation create-stack --stack-name lab19-fallo --template-body file://fallo.yaml

until aws cloudformation describe-stacks --stack-name lab19-fallo \
  --query 'Stacks[0].StackStatus' --output text | grep -qE 'COMPLETE|FAILED'; do
  sleep 5
done
aws cloudformation describe-stacks --stack-name lab19-fallo --query 'Stacks[0].StackStatus'

aws cloudformation describe-stack-events --stack-name lab19-fallo \
  --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[LogicalResourceId,ResourceStatusReason]"

El estado final es ROLLBACK_COMPLETE: el tema SNS que sí se había creado se ha borrado. Un stack en ese estado no se puede actualizar; solo borrar:

aws cloudformation delete-stack --stack-name lab19-fallo
aws cloudformation wait stack-delete-complete --stack-name lab19-fallo

Para saber más: StackSets (sin ejecutar)

Con StackSets desplegarías esta misma plantilla en varias cuentas y regiones con una sola operación: por ejemplo, el mismo entorno base en eu-south-2 y en eu-west-1 como región de DR, o un rol de auditoría en todas las cuentas de una OU. Con permisos service-managed (integrados con AWS Organizations) los stacks se crean automáticamente en las cuentas nuevas de la OU. No lo ejecutamos porque requiere Organizations o roles de administración y ejecución en cada cuenta.

Comprueba que funciona

  • Tras el paso 3, aws ssm get-parameter --name /lab19/prod/bucket devuelve el nombre del bucket y ya no existe /lab19/dev/bucket.
  • Tras el paso 4, la detección de deriva mostró DRIFTED y la propiedad VersioningConfiguration/Status.
  • Tras el paso 6, lab19-fallo terminó en ROLLBACK_COMPLETE sin dejar recursos.

Limpieza

El bucket está vacío, así que CloudFormation puede borrarlo. Borrar el stack elimina todos sus recursos en el orden inverso de dependencias.

aws cloudformation delete-stack --stack-name $PILA
aws cloudformation wait stack-delete-complete --stack-name $PILA

aws cloudformation list-stacks --stack-status-filter DELETE_COMPLETE \
  --query "StackSummaries[?starts_with(StackName,'lab19')].[StackName,StackStatus]"
aws ssm get-parameters-by-path --path /lab19 --recursive --query 'Parameters[*].Name'
aws logs describe-log-groups --log-group-name-prefix /lab19 --query 'logGroups[*].logGroupName'

cd ~ && rm -rf ~/lab19

Las dos últimas consultas deben devolver listas vacías. Si el borrado del stack falla porque el bucket tiene objetos (por ejemplo, si subiste alguno), vacíalo antes, incluidas las versiones, o bórralo desde la consola de S3 con Empty.

Preguntas para pensar como arquitecto

  1. Tu empresa quiere recuperar en eu-west-1 toda la infraestructura de eu-south-2 en menos de 4 horas si cae la región, con el mínimo coste permanente. ¿Qué papel juega CloudFormation?
Respuesta

Es la base de una estrategia backup and restore (o pilot light): la infraestructura está descrita como código y se despliega en la región de DR solo cuando hace falta (o reducida con Conditions y parámetros), mientras que los datos llegan allí con copias entre regiones (AWS Backup, snapshots, replicación de S3). Sin IaC, reconstruir a mano haría imposible cumplir el RTO. Conviene tener las AMI copiadas y las cuotas ampliadas en la región de DR.

  1. Un operador modificó un security group a mano y la plantilla dice otra cosa. ¿Cómo lo detectas y cómo evitas que vuelva a pasar?
Respuesta

Lo detectas con drift detection de CloudFormation (bajo demanda) o de forma continua con reglas de AWS Config, que además pueden remediar con Systems Manager Automation. Para prevenirlo: permisos de IAM que solo permitan cambios a través de CloudFormation (el rol de servicio del stack), SCP, y un proceso de cambios con change sets revisados.

  1. Vas a actualizar el stack de una base de datos RDS de producción y el change set indica Replacement: True. ¿Qué haces?
Respuesta

No ejecutarlo sin más: el reemplazo crea una instancia nueva y borra la antigua. Revisa qué propiedad lo provoca y busca una alternativa sin reemplazo. Como protección, la plantilla debe llevar DeletionPolicy: Snapshot (o Retain) y UpdateReplacePolicy: Snapshot en la base de datos, y el stack, termination protection y una stack policy que impida reemplazarla.

  1. Los equipos de desarrollo quieren crear sus propios entornos con esta plantilla, pero sin tener permisos amplios de IAM. ¿Qué servicio usarías?
Respuesta

AWS Service Catalog: publicas la plantilla como producto en un portfolio compartido con esos equipos y defines una launch constraint con el rol de IAM que CloudFormation usará para crear los recursos. Los desarrolladores solo necesitan permiso para lanzar productos del catálogo.


Volver al módulo