Skip to content

AWS CloudFormation

Infraestructura como Código (IaC) para AWS

AWS CloudFormation consiste en una plantilla en formato YAML, o JSON, que permite definir nuestra Infraestructura, orientada a lo que se denomina Infraestructura como Código (IaC), lo cual aporta características para la automatización de la integración y el despliegue continuo (CI/CD).

En otras palabras, en lugar de operar la Consola de AWS, con la plantilla de CloudFormation es posible establecer la infraestructura programaticamente, reduciendo acciones repetitivas y aportando en la definición de nuestra arquitectura en AWS.

Un trozo de plantilla tomado del sitio oficial de AWS, a modo de ejemplo, nos puede ilustrar la especificación para crear una instancia de computo de la siguiente manera:

yml
AWSTemplateFormatVersion: "2010-09-09"
Description: An EC2 sample template for Free Tier usage
Resources:
  LabEC2Instance:
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: "ami-00a929b66ed6e0de6"
      InstanceType: t2.micro
      KeyName: testkey
      BlockDeviceMappings:
        -
          DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: 15
            DeleteOnTermination: true

En este ejemplo de AWS podemos observar que en la sección de Resources se define la instancia de computo LabEC2Instance bajo la categoría o tipo AWS::EC2::Instance.
Luego se indican unas propiedades que incluyen el identificador de una imagen, el tipo de instancia, el nombre para las llaves y un dispositivo de almacenamiento en bloque. Además, sería conveniente incluir SecurityGroupIds

Las plantillas pueden tener las siguientes secciones o elementos:

  • AWSTemplateFormatVersion (Format Version)
  • Description
  • Metadata
  • Parameters
  • Rules
  • Mappings
  • Conditions
  • Transform
  • Resources
  • Outputs

Sólo Resources es requerida, pero es común incluir AWSTemplateFormatVersion y Description

Adicionalmente, para brindar cierta interacción, validación y funcionalidad, encontramos en plantillas Cloudformation las siguientes funciones intrínsecas:

  • Ref
  • Condition - Fn::If (Fn::And, Fn::Equals, Fn::Not, Fn::Or)
  • Fn::Base64
  • Fn::Cidr
  • Fn::FindInMap
  • Fn::GetAtt
  • Fn::GetAZs
  • Fn::ImportValue
  • Fn::Join
  • Fn::Length
  • Fn::Select
  • Fn::Split
  • Fn::Sub
  • Fn::ToJsonString
  • Fn::Transform

Encontraremos en nuestro recorrido que, para desarrollar aplicaciones Serverless, existen servicios como AWS SAM y AWS Amplify que simplifican o abstraen el uso de Cloudformation. Además, AWS CDK (Cloud Development Kit) que permite usar lenguajes de programación para definir la infraestructura en lugar de Cloudformation (aunque finalmente genera Cloudformation)

En esta guía esencial, se buscará abordar CloudFormation con configuraciones o bloques sencillos para introducir cierto aprendizaje o noción con ejemplos.

Ejemplo de VPC con CloudFormation

El siguiente contenido nos puede servir para preparar la configuración de Networking (red) en la nube de AWS que se organiza a partir de una VPC (Virtual Private Cloud).

Abrimos un nuevo archivo, por ejemplo cf-vpc-net.yaml, y copiamos dentro cada trozo o bloque a continuación. Comenzaremos por definir el encabezado con los atributos AWSTemplateFormatVersion y Description, así:

yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Network Stack for AWS Services'

A continuación especificamos parámetros usando el atributo Parameters de la siguiente manera:

yaml
Parameters:
  EnvironmentName:
    Description: An environment name that is used in resource names
    Type: String
    Default: Lab

  VpcCIDR:
    Description: The IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.0.0.0/16

  PublicSubnet1CIDR:
    Description: The IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.0.1.0/24

  PublicSubnet2CIDR:
    Description: The IP range (CIDR notation) for the public subnet in the second Availability Zone
    Type: String
    Default: 10.0.2.0/24

  PrivateSubnet1CIDR:
    Description: The IP range (CIDR notation) for the private subnet in the first Availability Zone
    Type: String
    Default: 10.0.3.0/24

  PrivateSubnet2CIDR:
    Description: The IP range (CIDR notation) for the private subnet in the second Availability Zone
    Type: String
    Default: 10.0.4.0/24

Nótese que cada parámetro se define bajo su propio nombre con propiedades como Description y Type (ejemplo: String).
Es conveniente un valor por defecto (usando Default) de ser posible.

Iniciamos el bloque Resources definiendo el recurso para la VPC (de tipo AWS::EC2::VPC), es decir, asignamos un nombre lógico o etiqueta para una nube privada virtual (que comprenderá componentes de red en AWS)...

yaml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub VPC-${EnvironmentName}

Semejante a Parameters, cada recurso se define bajo su propio nombre y tendrá unas propiedades dependiendo del Type.
La propiedad CidrBlock tendrá el valor referido en el parámetro VpcCIDR y por defecto sería 10.0.0.0/16.

Nótese que encontramos la función intrínseca !Ref que se usa para referenciar recursos o parámetros a partir del nombre especificado. También encontramos la función !Sub que se usa para sustituir expresiones que incluyen variables reflejando su valor, por ejemplo el uso de ${EnvironmentName}

Continuamos con el recurso para Internet Gateway (puerta de enlace a internet), el cual da paso a la comunicación de la VPC hacia Internet...

yaml
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub IGW-${EnvironmentName}

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

No solo se crea el recurso de tipo AWS::EC2::InternetGateway sino que también es necesario asociarlo a la VPC (asignando en VpcId el valor obtenido en el recurso VPC)

Podemos definir inicialmente una tabla de enrutamiento con una ruta por defecto para establecer conectividad de red en la VPC. Esto permitirá que instancias en una subred pública puedan tener acceso a Internet a través de la puerta de enlace a internet (Internet Gateway). Veamos:

yaml
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Routes

  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

Nótese que PublicRouteTable es de tipo AWS::EC2::RouteTable y se estable como ruta DefaultPublicRoute de tipo AWS::EC2::Route cuyo DestinationCidrBlock sería 0.0.0.0/0
DependsOn se usa para indicar una dependencia, en este caso con InternetGatewayAttachment

En una VPC es común tener al menos dos subredes de tipo público y dos subredes de tipo privado, dado que cada una se ubicaría en un AZ (Zona de disponibilidad) de la Region (una región puede brindar dos zonas de disponibilidad). Ilustraremos esto en Cloudformation con el siguiente bloque...

yaml
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ1)

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ2)

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PrivateSubnet1CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Subnet (AZ1)

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      CidrBlock: !Ref PrivateSubnet2CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Subnet (AZ2)

Además de asignar VpcId es clave el atributo AvailabilityZone (que se resuelve obteniendo AZs) y CidrBlock, cuyos valores pueden ser por defecto 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24 y 10.0.4.0/24 repectivamente.

Será necesario incorporar algún NAT (Network Address Translation), el cual actua como traductor entre redes privadas (sin acceso al público) y publicas (con acceso a Internet) tomando la IP privada de una red para trasladarla en una IP pública cuando se tiene una comunicación hacia el mundo (sin exponer la IP privada al público). Se pueden crear 2 NATs para cada subred pública y por tanto 2 IP públicas fijas (EIP) así:

yaml
  NatGateway1EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway2EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway1EIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  NatGateway2:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway2EIP.AllocationId
      SubnetId: !Ref PublicSubnet2

!GetAtt permite recuperar el valor de la propiedad de un recurso, en este caso NatGateway1EIP.AllocationId y NatGateway2EIP.AllocationId para asignarlo a la propieadad AllocationId.
NatGateway1EIP y NatGateway2EIP son de tipo AWS::EC2::EIP y se refieren al servicio EIP (IP v4 fijas). Podría simplificarse a un EIP y NAT si hablamos de entornos No productivos.

Para dirigir el tráfico dentro de una VPC se crean tablas de enrrutamiento que habilitan destinos externos. Es necesario además asociar estas a las subredes a la ruta por defecto, que involucra el Internet Gateway (o un NAT en el caso de subredes privadas).

Veamos el escenario para asociar las dos subredes públicas involucrando la tabla de enrutamiento del Internet Gateway...

yaml
  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

Este bloque de código se asocia con uno anterior en el que definimos previamente el PublicRouteTable para InternetGateway (podríamos organizar este código dejando estas definiciones conjuntamente con la asociada anteriormente)

Ahora veamos el escenario para las subredes privadas involucrando un NAT...

yaml
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ1)

  DefaultPrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1

  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ2)

  DefaultPrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      SubnetId: !Ref PrivateSubnet2

En este caso, PrivateRouteTable1 y PrivateRouteTable2 tendrían un DefaultPrivateRoute1 y DefaultPrivateRoute2 (respectivamente) en donde se referencia un NAT en NatGatewayId y cuyo DestinationCidrBlock sería 0.0.0.0/0

Para retornar ciertos valores de recursos que intervienen en el Stack (Pila), en el bloque final del archivo usaremos bajo el atributo Outputs un contenido como el siguiente...

yaml
Outputs:
  VPC:
    Description: A reference to the created VPC
    Value: !Ref VPC
    Export:
      Name: !Sub VPCID-${EnvironmentName}

  PublicSubnets:
    Description: A list of the public subnets
    Value: !Join [ ",", [ !Ref PublicSubnet1, !Ref PublicSubnet2 ]]
    Export:
      Name: !Sub PUBLIC-SUBNETS-${EnvironmentName}

  PrivateSubnets:
    Description: A list of the private subnets
    Value: !Join [ ",", [ !Ref PrivateSubnet1, !Ref PrivateSubnet2 ]]
    Export:
      Name: !Sub PRIVATE-SUBNETS-${EnvironmentName}

  PublicSubnet1:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub PUBLIC-SUBNET-AZ1-${EnvironmentName}

  PublicSubnet2:
    Description: A reference to the public subnet in the 2nd Availability Zone
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub PUBLIC-SUBNET-AZ2-${EnvironmentName}

  PrivateSubnet1:
    Description: A reference to the private subnet in the 1st Availability Zone
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub PRIVATE-SUBNET-AZ1-${EnvironmentName}

  PrivateSubnet2:
    Description: A reference to the private subnet in the 2nd Availability Zone
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub PRIVATE-SUBNET-AZ2-${EnvironmentName}

!Join es usado para unir valores de una lista y expresarlos en una cadena usando un separador (,)

Finalmente, podemos crear un Stack (Pila) ejecutando un comando como el siguiente:

bash
aws cloudformation create-stack \
--stack-name my-vpc-net \
--template-body file://cf-vpc-net.yaml \
--region us-east-1

my-vpc-net se refiere al nombre asignado al Stack (Pila) y cf-vpc-net.yaml se refire el archivo elaborado. us-east-1 sería la región respectiva.
Se podrían incorporar parámetros indicando por ejemplo: --parameters ParameterKey=EnvironmentName,ParameterValue=Test

Si se requiere deshacer el Stack (Pila), se puede ejecutar lo siguiente:

bash
aws cloudformation delete-stack  --stack-name my-vpc-net --region us-east-1

Ejemplo ajustado para VPC de pruebas

Para reducir costos, por usar doble EIP y NAT, cuando se trata de un entorno transitorio o de prueba se puede considerar la siguiente configuración (reemplazando los atributos respectivos)...

yaml
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ1)

  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ2)

  DefaultPrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  DefaultPrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

Tanto EIP como NatGateway aún pueden generar algún costo. Para evitar costos en un entorno de pruebas o laboratorio, se puede evitar el uso de EIP y remover el NatGateway siempre y cuando No se necesiten que en las redes privadas se tenga acceso a Internet. Y es que lo común es que se tenga acceso a Internet en los recursos de una subred pública y No en una subred privada (salvo excepciones como bases de datos). Siendo así el bloque de configuración anterior quedaría simplificado de la siguiente manera:

yaml
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ1)

  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ2)

En este último escenario las subredes privadas No tendrían acceso a Internet por lo que no aplican los recursos: NatGatewayEIP, NatGateway, DefaultPrivateRoute1, DefaultPrivateRoute1

Ejemplo de S3 con CloudFormation

En el siguiente ejercicio podemos ilustrar como configurar los recursos para una aplicación web que usa archivos estátcos (HTML5, CSS3, JS) con S3.

Para este ejercicio abrimos un nuevo archivo, por ejemplo cf-s3-web.yaml, y copiamos dentro cada trozo o bloque a continuación. Comenzaremos por definir el encabezado con los atributos y parámetros, así:

yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Stack for Web App'

Parameters:
  EnvironmentName:
    Description: An environment name that is used in resource names
    Type: String
    Default: Lab

En los recursos tendríamos lo siguiente...

yaml
Resources:
  MyS3:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub bucket-2024-${EnvironmentName}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: index.html
      VersioningConfiguration:
        Status: Enabled

Nótese que se usa el tipo AWS::S3::Bucket y propiedades tales como BucketName, PublicAccessBlockConfiguration (referente a bloque público), OwnershipControls (sobre el propietario de los objetos) y WebsiteConfiguration (para configurar S3 como sitio web).

Y podemos acompañar lo anterior con la siguiente política:

yaml
  MyS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref MyS3
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: PublicReadGetObject
            Effect: Allow
            Principal: '*'
            Action: 's3:GetObject'
            Resource: !Join ['', ['arn:aws:s3:::', !Ref MyS3, '/*']]

Al finalizar el archivo incluimos una salida que nos reporte la URL de nuestra web, aplicando el siguiente código:

yaml
Outputs:
  WebsiteURL:
    Value: !GetAtt MyS3.WebsiteURL
    Description: URL for website hosted on S3

Ejemplo de EC2 con CloudFormation

Aunque vimos un ejemplo inicialmente, ahora podemos agregar parámetros y un grupo de seguridad. Veamos una plantilla de ejemplo para usar la capa gratuita:

yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: An EC2 sample template for Free Tier usage

Parameters:
  KeyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instance
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: must be the name of an existing EC2 KeyPair.
    MinLength: 1

Resources:
  LabEC2Instance:
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: "ami-00a929b66ed6e0de6"
      InstanceType: t2.micro
      KeyName: !Ref KeyName
      SecurityGroupIds:
        - !Ref LabEC2SecurityGroup
      BlockDeviceMappings:
        -
          DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: 15
            DeleteOnTermination: true

  LabEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable SSH access via port 22
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0  # My IP (/32)

Para este ejemplo debemos haber creado un par de llaves (AWS::EC2::KeyPair::KeyName)
En la última línea se puede espcificar la IP publica de nuestra máquina, la cual puedes obtener usando por ejemplo: curl ifconfig.me

Para contectarse a la instancia podría usarse ssh con el archivo de llaves que se haya creado y los privilegios apropiados (chmod 400). Por ejemplo, se podría ejecutar un comando como el siguiente:

bash
ssh -i "MyKeyPairs.pem" [email protected]

Para un mejor escenario se puede probar combinar la VPC que creamos anteriormente, es decir, aplicando SubnetId y VpcId, por ejemplo así:

yaml
Resources:
  LabEC2Instance:
    Type: "AWS::EC2::Instance"
    Properties: 
      SubnetId: !ImportValue PUBLIC-SUBNET-AZ1-Lab
      ImageId: "ami-00a929b66ed6e0de6"
      InstanceType: t2.micro
      KeyName: !Ref KeyName
      SecurityGroupIds:
        - !Ref LabEC2SecurityGroup
      BlockDeviceMappings:
        -
          DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: 15
            DeleteOnTermination: true

  LabEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable SSH access via port 22
      VpcId: !ImportValue VPCID-Lab
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0  # My IP (/32)
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0

Outputs:
  EC2InstanceId:
    Description: ID of the created EC2 instance
    Value: !Ref LabEC2Instance
    Export:
      Name: EC2InstanceId

  EC2SecurityGroupId:
    Description: Security Group ID for the EC2 instance
    Value: !Ref LabEC2SecurityGroup
    Export:
      Name: EC2SecurityGroupId

En este caso, se ha usado !ImportValue PUBLIC-SUBNET-AZ1-Lab y !ImportValue VPCID-Lab para referirnos a recursos ya establecidos con una pila anterior de CloudFormation (según se hayan exportado)

Ejemplo de AWS Lambda (+EventBridge) con CloudFormation

Siguiendo el ejemplo anterior, en el que definimos una máquina virtual con EC2, si se trata de una instancia de desarrollo puede ser conveniente usar una ventana de tiempo en el que la instancia se encuentre disponible. Para esto se pueda usar el servicio EventBridge combinado con AWS Lambda y en la plantilla podríamos agregar los siguientes recursos:

yaml
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: "LambdaEC2ControlPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:StartInstances
                  - ec2:StopInstances
                Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/${LabEC2Instance}"
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"

  StartStopLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import boto3
          import os

          ec2 = boto3.client('ec2')
          def handler(event, context):
              instance_id = os.environ['INSTANCE_ID']
              action = event['action']
              if action == 'start':
                  ec2.start_instances(InstanceIds=[instance_id])
              elif action == 'stop':
                  ec2.stop_instances(InstanceIds=[instance_id])
              return {"status": "success", "action": action}

      Runtime: python3.9
      Timeout: 30
      Environment:
        Variables:
          INSTANCE_ID: !Ref LabEC2Instance

  StartInstanceRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: "cron(40 7 ? * 2-7 *)"
      Targets:
        - Arn: !GetAtt StartStopLambdaFunction.Arn
          Id: "StartInstance"
          Input: '{"action": "start"}'

  StopInstanceRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: "cron(40 18 ? * 2-7 *)"
      Targets:
        - Arn: !GetAtt StartStopLambdaFunction.Arn
          Id: "StopInstance"
          Input: '{"action": "stop"}'

  LambdaPermissionForEventBridge:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref StartStopLambdaFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com

Con esto tendríamos la instancia encendida entre las 7:40 am y las 6:40 pm de lunes a sábado, contando con un evento (regla de EventBridge) para encendido de la instancia y otro de apagado

Ejemplo de RDS para Base de Datos relacional con CloudFormation

Con el servicio RDS es posible contar con instancias de base de datos como PostgreSQL y MySQL (u otras variantes). En realidad se trata de una instancia de computo destinada y enfocada en la gestión de este tipo de motores con bases de datos SQL. En este caso complementaremos la plantilla anterior agregando, como ejemplo, los recursos para usar RDS en la capa gratuita:

yaml
  LabRDSInstance:
    Type: "AWS::RDS::DBInstance"
    Properties:
      DBInstanceIdentifier: "myxdb"
      DBInstanceClass: db.t3.micro
      Engine: mysql
      MasterUsername: !Ref DBUsername
      MasterUserPassword: !Ref DBPassword
      AllocatedStorage: 20
      StorageType: gp2
      MultiAZ: false
      PubliclyAccessible: false
      BackupRetentionPeriod: 7
      VPCSecurityGroups:
        - !GetAtt DBSecurityGroup.GroupId
    DependsOn: DBSecurityGroup

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow access to RDS
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !GetAtt LabEC2SecurityGroup.GroupId

Outputs:
  RDSInstanceEndpoint:
    Description: Endpoint of the RDS instance
    Value: !GetAtt LabRDSInstance.Endpoint.Address
    Export:
      Name: RDSInstanceEndpoint

Recordar que este bloque acompaña a la plantilla vista para EC2 y por ello podemos usar la referencia de SourceSecurityGroupId: !Ref LabEC2SecurityGroup

Para que los recursos funcionen se han creado parámetros para el usuario y contraseña, por lo que debemos copiar también el siguiente bloque en Parameters:

yaml
  DBUsername:
    Description: The database admin account username
    Type: String
    MinLength: 1
    MaxLength: 16
    Default: admin

  DBPassword:
    Description: The database admin account password
    Type: String
    NoEcho: true
    MinLength: 8
    MaxLength: 41
    AllowedPattern: "[a-zA-Z0-9]*"
    ConstraintDescription: must contain only alphanumeric characters.

Finalmente, podríamos asignar un esquema de red previamente establecido con una pila de CloudFormation (semejante a lo definido con EC2). Para esto tendríamos que definir nuestros recursos de RDS como en el siguiente ejemplo:

yaml
  LabRDSInstance:
    Type: "AWS::RDS::DBInstance"
    Properties:
      DBInstanceIdentifier: "xdb"
      DBInstanceClass: db.t3.micro
      Engine: mysql
      MasterUsername: !Ref DBUsername
      MasterUserPassword: !Ref DBPassword
      AllocatedStorage: 20
      StorageType: gp2
      MultiAZ: false
      PubliclyAccessible: false
      BackupRetentionPeriod: 7
      VPCSecurityGroups:
        - !Ref DBSecurityGroup
      DBSubnetGroupName: !Ref RDSSubnetGroup
    DependsOn: DBSecurityGroup

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow access to RDS
      VpcId: !ImportValue VPCID-Lab
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !GetAtt LabEC2SecurityGroup.GroupId

  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Subnet group for RDS
      SubnetIds:
        - !ImportValue PRIVATE-SUBNET-AZ1-Lab
        - !ImportValue PRIVATE-SUBNET-AZ2-Lab

Nótese que se ha agregado el recurso RDSSubnetGroup, y se importan la VPC (!ImportValue VPCID-Lab) y las subredes (!ImportValue PRIVATE-SUBNET-AZ1-Lab, !ImportValue PRIVATE-SUBNET-AZ2-Lab)

Ejemplo de ECS con CloudFormation

Con el servico ECS (Elastic Container Service) se pueden gestionar contenedores en AWS de un modo más sencillo que otros mecanismos para la nube.

Para este ejercicio abrimos un nuevo archivo, por ejemplo cf-ecs-app.yaml, y copiamos dentro cada trozo o bloque a continuación. Comenzaremos por definir el encabezado con los atributos y parámetros, así:

yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Stack for App with ECS'

Parameters:
  EnvironmentName:
    Description: An environment name that is used in resource names
    Type: String
    Default: Lab

Se puede decir que para que opere un contenedor en el servicio ECS se debe definir el Cluster, un Task Definition (el cual especifica la configuración para lanzar un contenedor o crear su instancia), un Service (servicio), un Role y un Securiy Group (Grupo de Seguridad.)

Iniciamos los recursos con el Cluster, por ejemplo:

yaml
Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub cluster-${EnvironmentName}
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} ECS Cluster

Nótese que se usa el tipo AWS::ECS::Cluster y unas propiedades sencillas (ClusterName y Tags)

Una vez definido el Cluster procedemos a definir la tarea (Task Definition) así:

yaml
  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub task-${EnvironmentName}
      Cpu: '256'
      Memory: '512'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !Ref ECSTaskExecutionRole
      ContainerDefinitions:
        - Name: !Sub container-${EnvironmentName}
          Image: 'amazon/my-ecr-image'
          PortMappings:
            - ContainerPort: 80

Nótese que se usa el tipo AWS::ECS::TaskDefinition y propiedades tales como CPU y memoria, además un rol de ejecución (ExecutionRoleArn) y la imagen del contenedor (Image).
En Image debe reeplazarse con el valor correspondiente a la imagen subida en algun servicio para ello, por ejemplo, podría usarse ECR

Continuamos con el Servicio, el cual gestiona la tarea y podría tener un contenido como el siguiente:

yaml
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ECSTaskDefinition
    Properties:
      ServiceName: !Sub service-${EnvironmentName}
      Cluster: !Ref ECSCluster
      TaskDefinition: !Ref ECSTaskDefinition
      DesiredCount: 2
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets:
            - !Ref PublicSubnet1
            - !Ref PublicSubnet2
          SecurityGroups:
            - !Ref ECSSecurityGroup

Además agregamos un rol con la política para ejecutar la tarea. Por ejemplo:

yaml
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

Luego creamos un grupo de seguridad que permite abrir el puerto asociado al servicio. Por ejemplo:

yaml
  ECSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ECS tasks
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

Al finalizar el archivo podemos incluir una salida que nos de razón del Cluster y Servicio creados. Por ejemplo:

yaml
Outputs:
  ECSCluster:
    Description: A reference to the ECS cluster
    Value: !Ref ECSCluster
    Export:
      Name: !Sub ${EnvironmentName}-ECSCluster

  ECSService:
    Description: A reference to the ECS service
    Value: !Ref ECSService
    Export:
      Name: !Sub ${EnvironmentName}-ECSService