Infrastructure as Code
This section is a BIG one! In this section your coding skill are put on a test when you face the wrath of AWS CDK + ECS + Fargate + Cloudformation!
You will use CDK to code the following: * Create ECS Cluster * Create ECS Policies & Roles * Create Security Groups * Create ECS Task Definitions * Create Log groups * Create container definitions * Create ECS Fargate * Create External ALB
Preparation
Configuring AWS Account
For CDK to work properly, you need to configure credentials to access AWS account.
Follow these steps:
- If you don't have aws cli v2, follow instructions how to install it: https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html
- Run
aws configure
- Enter your AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY provided by your instructor
- Verify your setup by running
aws sts get-caller-identity
This should show something like this:{ "UserId": "AIDAR3C4LGTKR5WUAWEFG", "Account": "126895201313", "Arn": "arn:aws:iam::126895201313:user/dude.perfect@fakemail.com" }
Installing CDK
Allthough we will be using CI/CD pipeline to run IaC into AWS, we can use CDK package to initialize the IaC folder structure and help us to debug our code.
- Install CDK client:
npm install -g aws-cdk
- Create directory
iac
to your repository - Run
cdk init --language python --app iac
. If you want to use other language like TypeScript, JavaScript, Java, C#, you can but Answers/Examples are done \ with Python to make things more readable - Create python3 virtual environment, if you like,
python3 -m venv .env
- Activate Python Virtual environment
source .env/bin/activate
- Install Python requirements
pip install -r requirements.txt
- Install additional package
pip install aws_cdk.aws_ec2 aws_cdk.aws_ecs aws_cdk.aws_ecs_patterns
- Your
iac/iac/iac_stack.py
files imports should look like:
from aws_cdk import (
core,
aws_ec2 as ec2,
aws_ecs as ecs,
aws_iam as iam,
aws_ecr as ecr,
aws_elasticloadbalancingv2 as elbv2,
aws_logs as logs,
)
IaC Code
Stack Configuration
- Create environment configuration
core.Environment
:- Region must be
eu-west-1
- Account must be
XXXXXXXX
, the one your instructor provided
- Region must be
- Pass it to the Iac class
- Change the IacStack ID
iac
to<username>
Help
File: iac/app.py
#!/usr/bin/env python3
from aws_cdk import core
from iac.iac.iac_stack import IacStack
app = core.App()
myEnv = core.Environment(region="eu-west-1", account="<AWS Account ID")
IacStack(app, "<username>", env=myEnv)
app.synth()
VPC & Cluster Configuration
- VPC configuration needs to be retrieved (
ec2.Vpc.from_lookup()
), save it tovpc
variable- VPC ID can be obtained from your instructor
- Create new cluster
ecs.Cluster()
- Name it according to your username, i.e.
apprentice-0
- Assign correct VPC object
- Name it according to your username, i.e.
- Create new namespace
<cluster object>.add_default_cloud_map_namespace(name="<username>")
Help
# VPC and Cluster Configuration
vpc = ec2.Vpc.from_lookup(self, id="VPC", vpc_id="<VPC ID>")
cluster = ecs.Cluster(self, "<username>", vpc=vpc)
cluster.add_default_cloud_map_namespace(name="<username>")
ECS Policies and Roles
In order for ECS Fargate to be able to retrieve ECR images and log itself to CloudWatch
- Create ServicePrincipal
iam.ServicePrincipal('ecs-tasks.amazonaws.com)
This will be used as a service account mapping for the ECS Service - Create Task Role
iam.Role(self, 'ecsTaskExecutionRole-<username>, assumed_by=<service_principal_object>
- Get AWS Managed IAM Policy object
iam.ManagedPolicy.from_aws_managed_policy_name('service-role/AmazonECSTaskExecutionRolePolicy')
This is the role ECS will use to Retrieve ECR images and write to CloudWatch - Add managed policy role to your task role
<role_object>.add_managed_policy(<iam_managed_policy_object>)
Help
service_principal = iam.ServicePrincipal('ecs-tasks.amazonaws.com')
task_role = iam.Role(self, 'ecsTaskExecutionRole-<username>', assumed_by=service_principal)
ecs_managed_policy = iam.ManagedPolicy.from_aws_managed_policy_name(
'service-role/AmazonECSTaskExecutionRolePolicy'
)
task_role.add_managed_policy(ecs_managed_policy)
Create ECR Repository objects
ECR objects are needed to configure images for the containers
1. Create fronend ECR object
ecr.Repository.from_repository_name(self, id="frontend-<username>", repository_name="<username>-frontend")
1. Create backend ECR object
backend_ecr_repo = ecr.Repository.from_repository_name(self, id="backend-<username>", repository_name="<username>-backend")
Help
frontend_ecr_repo = ecr.Repository.from_repository_name(
self,
id="frontend-<username>",
repository_name="<username>-frontend"
)
backend_ecr_repo = ecr.Repository.from_repository_name(
self,
id="backend-<username>",
repository_name="<username>-backend",
)
Create Security Groups
Security groups are needed to gain access to the containers
- Create three security groups for
frontend
|backend
|database
withec2.SecurityGroup
- ID =
<fronend|backend|database>-sg-<username>
- allow_all_outbound =
True
- security_group_name =
<FrontendSG|BackendSG|DatabaseSG>-<username>
- vpc =
vpc
- ID =
- Allow Incoming traffic with
<security_group_object>.connections.allow_from_any_ipv4()
- Frontend:
ec2.Port.tcp(5000)
- Backend:
ec2.Port.tcp(8080)
- Database:
ec2.Port.tcp(6379)
- Frontend:
Help
frontend_security_group = ec2.SecurityGroup(
self,
'frontend-sg-<username>',
allow_all_outbound=True,
security_group_name="FrontendSG-<username>",
vpc=vpc
)
backend_security_group = ec2.SecurityGroup(
self,
'backend-sg-<username>',
allow_all_outbound=True,
security_group_name="BackendSG-<username>",
vpc=vpc
)
database_security_group = ec2.SecurityGroup(
self,
'database-sg-<username>',
allow_all_outbound=True,
security_group_name="DatabaseSG-<username>",
vpc=vpc
)
frontend_security_group.connections.allow_from_any_ipv4(
ec2.Port.tcp(5000),
)
backend_security_group.connections.allow_from_any_ipv4(
ec2.Port.tcp(8080),
)
database_security_group.connections.allow_from_any_ipv4(
ec2.Port.tcp(6379)
)
Create Fargate TaskDefinitions
Task Definitions you set the CPU & Memory limits the Fargate task is allowed to consume. \ You will also configure what Service Role ECS Fargate uses.
- Create Frontend Task Definition
ecs.FargateTaskDefinition
- ID =
frontendTaskDefinition-<username>
- CPU =
256
- Memory limit =
512
- Task Role =
<task_role_object>
- ID =
- Create Backend Task Definition
ecs.FargateTaskDefinition
- ID =
backendTaskDefinition-<username>
- CPU =
256
- Memory limit =
512
- Task Role =
<task_role_object>
- ID =
- Create Database Task Definition
ecs.FargateTaskDefinition
- ID =
databaseTaskDefinition-<username>
- CPU =
256
- Memory limit =
512
- Task Role =
task_role_object
- ID =
Help
# Frontend TaskDefinition
frontend_task_definition = ecs.FargateTaskDefinition(
self,
'frontendTaskDefinition-<username>',
cpu=256,
memory_limit_mib=512,
task_role=task_role,
)
# Backend TaskDefinition
backend_task_definition = ecs.FargateTaskDefinition(
self,
'backendTaskDefinition-<username>',
cpu=256,
memory_limit_mib=512,
task_role=task_role
)
# Database TaskDefinition
database_task_definition = ecs.FargateTaskDefinition(
self,
'databaseTaskDefinition-<username>',
cpu=256,
memory_limit_mib=1024,
task_role=task_role
)
Logging
Log groups are needed for ECS to log into correct logging groups
You will need three log groups for frontend
| backend
| database
- Create each log group with
logs.LogGroup()
- ID =
<fronendLogGroup|backendLogGroup|databaseLogGroup>-<username>
- log_group_name =
/ecs/<fronend|backend|database>LogGroup-<username>
- removal_policy =
core.RemovalPolicy.DESTROY
- ID =
- Create Log Driver for each
fronend|backend|database
withecs.AwsLogDriver()
, this will be used by the container for logging.- log_group =
<log group object>
- stream_prefix =
<fronend|backend|database>
- log_group =
Help
# Logging
frontend_log_group = logs.LogGroup(
self,
"frontendLogGroup-<username>",
log_group_name="/ecs/frontendLogGroup-<username>",
removal_policy=core.RemovalPolicy.DESTROY
)
backend_log_group = logs.LogGroup(
self,
"backendLogGroup-<username>",
log_group_name="/ecs/backendLogGroup-<username>",
removal_policy=core.RemovalPolicy.DESTROY
)
database_log_group = logs.LogGroup(
self,
"databaseLogGroup-<username>",
log_group_name="/ecs/databaseLogGroup-<username>",
removal_policy=core.RemovalPolicy.DESTROY
)
frontend_log_driver = ecs.AwsLogDriver(
log_group=frontend_log_group,
stream_prefix="frontend"
)
backend_log_driver = ecs.AwsLogDriver(
log_group=backend_log_group,
stream_prefix="backend",
)
database_log_driver = ecs.AwsLogDriver(
log_group=database_log_group,
stream_prefix="database",
)
Container Definitions
Container definitions define image that the container uses, you can also assign Environment variables, hostname etc
- Create frontend container definition
<frontend_task_definition_object.add_container()
- ID =
frontend-<username>
- image =
ecs.ContainerImage.from_ecr_repository(<frontend_ecr_repo_object>, tag=<tag_from_CI>)
- Hint! Get the correct tag with
os.environ["CI_COMMIT_SHORT_SHA"]
- Hint! Get the correct tag with
- Environment:
- "COLORS_BACKEND_ADDRESS": "backend.<username>:8080",
- logging:
<frontend log driver object>
- ID =
-
Add port mapping to the frontend container
<frontend_container_object>.add_port_mappings(ecs.PortMapping(container_port=<frontend_containerPort>))
-
Create backend container definition
<backend_task_definition_object>.add_container()
- ID =
backend-<username>
- image =
ecs.ContainerImage.from_ecr_repository(<backend_ecr_repo_object>)
- Environment:
- "COLORS_DATABASE_ADDRESS": "database.<username>:6379",
- logging:
<backend log driver object>
- ID =
-
Add port mapping to the backend container
<backend_container_object>.add_port_mappings(ecs.PortMapping(container_port=<backend_containerPort>))
-
Create database container definition
<database_task_definition_object>.add_container()
- ID =
database-<username>
- image =
ecs.ContainerImage.from_registry("redis:6.0-alpine")
- logging:
<database log driver object>
- ID =
Help
# Frontend container
frontend_container = frontend_task_definition.add_container(
'frontend-<username>',
image=ecs.ContainerImage.from_ecr_repository(frontend_ecr_repo, tag=os.environ["CI_COMMIT_SHORT_SHA"]),
logging=frontend_log_driver,
environment={
"COLORS_BACKEND_ADDRESS": "backend.<username>:8080",
},
memory_limit_mib=512,
)
frontend_container.add_port_mappings(ecs.PortMapping(container_port=5000))
# Backend container
backend_container = backend_task_definition.add_container(
'backend-<username>',
image=ecs.ContainerImage.from_ecr_repository(backend_ecr_repo, tag=os.environ["CI_COMMIT_SHORT_SHA"]),
logging=backend_log_driver,
environment={
"COLORS_DATABASE_ADDRESS": "database.<username>:6379",
},
memory_limit_mib=512,
)
backend_container.add_port_mappings(ecs.PortMapping(container_port=8080))
# Database container
database_container = database_task_definition.add_container(
'database-<username>',
image=ecs.ContainerImage.from_registry("redis:6.0-alpine"),
logging=database_log_driver,
)
database_container.add_port_mappings(ecs.PortMapping(container_port=6379))
ECS Fargate Definitions
- Create Frontend ECS Fargate Definition
ecs.FargateService()
- ID =
FrontendFargateService-<username>
- cluster =
<cluster_object>
- task_definition =
<frontend_task_definition_object>
- security_group =
<frontend_security_group_object>
- desired_count = 1
- cloud_map_options =
ecs.CloudMapOptions(name="frontend")
- service_name =
frontend
- ID =
- Create Backend ECS Fargate Definition
ecs.FargateService()
- ID =
BackendFargateService-<username>
- cluster =
<cluster_object>
- task_definition =
<backend_task_definition_object>
- security_group =
<backend_security_group_object>
- desired_count = 1
- cloud_map_options =
ecs.CloudMapOptions(name="backend")
- service_name =
backend
- ID =
- Create Database ECS Fargate Definition
ecs.FargateService()
- ID =
DatabaseFargateService-<username>
- cluster =
<cluster_object>
- task_definition =
<database_task_definition_object>
- security_group =
<database_security_group_object>
- desired_count = 1
- cloud_map_options =
ecs.CloudMapOptions(name="database")
- service_name =
database
- ID =
Help
frontend_service = ecs.FargateService(
self,
"Frontend-<username>",
cluster=cluster, # Required
task_definition=frontend_task_definition,
security_group=frontend_security_group,
desired_count=1,
assign_public_ip=True,
cloud_map_options=ecs.CloudMapOptions(name="frontend"),
service_name="frontend"
)
ecs.FargateService(
self,
"Backend-<username>",
cluster=cluster, # Required
task_definition=backend_task_definition,
security_group=backend_security_group,
assign_public_ip=True,
desired_count=1,
cloud_map_options=ecs.CloudMapOptions(name="backend"),
service_name="backend"
)
ecs.FargateService(
self,
"Database-<username>",
cluster=cluster, # Required
task_definition=database_task_definition,
security_group=database_security_group,
desired_count=1,
cloud_map_options=ecs.CloudMapOptions(name="database"),
service_name="database"
)
Create ALB
ALB (Application Load Balancer) will serve the frontend application to the world
- Create ALB
elbv2.ApplicationLoadBalancer
- ID =
external-<username>
- vpc =
<vpc_objec>
- internet_facing =
True
- ID =
- Add listener
<ALB-object>.add_listener()
- ID =
<alb-listener-<username>
- port = 80
- ID =
- Create Health check
elbv2.HealthCheck(path='/')
- Add Target to the listener
<listener-object>.add_targets()
- ID =
tg-<username>
- port = 80
- health_check =
<health-check-object>
- targets =
[<frontend-Fargete-service-object]
- ID =
- Print out the ALB Address
core.CfnOutput(self, 'ALB DNS: ', value=<alb-object>.load_balancer_dns_name)
Help
alb = elbv2.ApplicationLoadBalancer(
self,
"external-<username>",
vpc=vpc,
internet_facing=True
)
alb_listener = alb.add_listener(
'alb-listener-<username>',
port=80
)
alb_health_check = elbv2.HealthCheck(path='/')
alb_listener.add_targets(
'tg-<username>',
port=80,
health_check=alb_health_check,
targets=[frontend_service]
)
core.CfnOutput(self, 'ALB DNS: ', value=alb.load_balancer_dns_name)
Check that the configuration is correct
Now that the code is done we need to check that there isn't any mistakes.
You can dry run the CDK in the command line cdk synth
. \
This will print out the YAML of the build.
Lastly save requirements: pip freeze > requirements.txt
CI/CD
IaC should be also part of CI/CD pipeline.
We first add IaC to the Test part of our pipeline then execute CDK deploy
, when images have been created.
Template:
image: <image>
variables:
AWS_DEFAULT_REGION: eu-west-1
stages:
- <stage>
<stage>:<name>:
stage: <stage>
before_script:
- <items>
script:
- <items>
Test
- Create
.gitlab-ci-iac-test.yml
toiac
folder and include it in the main.gitlab-ci.yml
file - Use image
node:14.4.0-alpine
- Name the stage as
test
- Before script
- Install
awslint
as Global NPM package - Change working directory to
iac
- Install
- Script
- Run
awslint
- Run
Help
image: node:14.4.0-alpine
variables:
AWS_DEFAULT_REGION: eu-west-1
stages:
- test
test:iac:
stage: test
before_script:
- npm install -g awslint
- cd iac
script:
- awslint
Deploy
- Create
.gitlab-ci-iac.yml
toiac
folder and include it in the main.gitlab-ci.yml
file - Use image
node:14.4.0-alpine
- Name the stage as
deploy
- Before script
- Install
aws-cdk
as a Global NPM package - Install Python3
- Change working directory to
iac
- Install requirements
pip3 install -r requirements.txt
- Install
- Script
- Run
cdk deploy --require-approval never
- Run
Help
image: node:14.4.0-alpine
variables:
AWS_DEFAULT_REGION: eu-west-1
stages:
- deploy
deploy:iac:
stage: deploy
before_script:
- npm install -g aws-cdk
- apk add python3
- cd iac
- pip3 install -r requirements.txt
script:
- cdk deploy --require-approval never
Now commit everything and see how it goes!! Good luck!
Here is the whole gitlab-ci.yml file
stages:
- test
- build
- deploy
test:frontend:
stage: test
trigger:
include:
- local: /frontend/.gitlab-ci-frontend-test.yml
strategy: depend
test:backend:
stage: test
trigger:
include:
- local: /backend/.gitlab-ci-backend-test.yml
strategy: depend
test:iac:
stage: test
trigger:
include:
- local: /iac/.gitlab-ci-iac-test.yml
strategy: depend
build:frontend:
stage: build
trigger:
include:
- local: /frontend/.gitlab-ci-frontend-build.yml
strategy: depend
build:backend:
stage: build
trigger:
include:
- local: /backend/.gitlab-ci-backend-build.yml
strategy: depend
deploy:iac:
stage: deploy
trigger:
include:
- local: /iac/.gitlab-ci-iac.yml
strategy: depend