Appearance
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: trueEn este ejemplo de AWS podemos observar que en la sección de
Resourcesse define la instancia de computoLabEC2Instancebajo la categoría o tipoAWS::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 incluirSecurityGroupIds
Las plantillas pueden tener las siguientes secciones o elementos:
- AWSTemplateFormatVersion (Format Version)
- Description
- Metadata
- Parameters
- Rules
- Mappings
- Conditions
- Transform
- Resources
- Outputs
Sólo
Resourceses requerida, pero es común incluirAWSTemplateFormatVersionyDescription
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/24Nótese que cada parámetro se define bajo su propio nombre con propiedades como
DescriptionyType(ejemplo:String).
Es conveniente un valor por defecto (usandoDefault) 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 delType.
La propiedadCidrBlocktendrá el valor referido en el parámetroVpcCIDRy por defecto sería10.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 VPCNo solo se crea el recurso de tipo
AWS::EC2::InternetGatewaysino que también es necesario asociarlo a la VPC (asignando enVpcIdel valor obtenido en el recursoVPC)
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 InternetGatewayNótese que
PublicRouteTablees de tipoAWS::EC2::RouteTabley se estable como rutaDefaultPublicRoutede tipoAWS::EC2::RoutecuyoDestinationCidrBlocksería0.0.0.0/0DependsOnse usa para indicar una dependencia, en este caso conInternetGatewayAttachment
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
VpcIdes clave el atributoAvailabilityZone(que se resuelve obteniendo AZs) yCidrBlock, cuyos valores pueden ser por defecto10.0.1.0/24,10.0.2.0/24,10.0.3.0/24y10.0.4.0/24repectivamente.
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
!GetAttpermite recuperar el valor de la propiedad de un recurso, en este casoNatGateway1EIP.AllocationIdyNatGateway2EIP.AllocationIdpara asignarlo a la propieadadAllocationId.NatGateway1EIPyNatGateway2EIPson de tipoAWS::EC2::EIPy 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 PublicSubnet2Este bloque de código se asocia con uno anterior en el que definimos previamente el
PublicRouteTableparaInternetGateway(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 PrivateSubnet2En este caso,
PrivateRouteTable1yPrivateRouteTable2tendrían unDefaultPrivateRoute1yDefaultPrivateRoute2(respectivamente) en donde se referencia un NAT enNatGatewayIdy cuyoDestinationCidrBlocksería0.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}
!Joines 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-netse refiere al nombre asignado al Stack (Pila) ycf-vpc-net.yamlse refire el archivo elaborado.us-east-1serí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-1Ejemplo 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 NatGatewayTanto 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: LabEn 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: EnabledNótese que se usa el tipo
AWS::S3::Buckety propiedades tales comoBucketName,PublicAccessBlockConfiguration(referente a bloque público),OwnershipControls(sobre el propietario de los objetos) yWebsiteConfiguration(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 S3Ejemplo 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: EC2SecurityGroupIdEn este caso, se ha usado
!ImportValue PUBLIC-SUBNET-AZ1-Laby!ImportValue VPCID-Labpara 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.comCon 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: RDSInstanceEndpointRecordar 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-LabNó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: LabSe 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 ClusterNótese que se usa el tipo
AWS::ECS::Clustery unas propiedades sencillas (ClusterNameyTags)
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: 80Nótese que se usa el tipo
AWS::ECS::TaskDefinitiony propiedades tales como CPU y memoria, además un rol de ejecución (ExecutionRoleArn) y la imagen del contenedor (Image).
EnImagedebe 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 ECSSecurityGroupAdemá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/AmazonECSTaskExecutionRolePolicyLuego 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/0Al 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