I’m a practical learner. I want to look at what someone else has made and then try the same thing myself.Some things might not work correctly - I will go away to fix them (or attempt to). Before i realise, i have a pretty good grasp on this new technology.
Let’s have a crack at that same learning pathway for a demo web application on AWS ECS.
Today i’ll be doing everything using AWS CDK.
The goal for this post is to use CDK to create a highly available ECS web application.
That may look like a lot of empty space. And you’re right there is, but for a very good reason. This architecture provides the networking foundation that 99% of web applications will need.
A well built 3 tier architecture, highly available across 3 availability zones.
I’ve covered CDK pipelines in depth in a previous blog post. See Here
So let’s skip over this and get straight into the guts of this template.
Gone are the days of 1000 line long CloudFormation templates just to deploy a VPC. We can deploy a custom 3 tier network with ease. Check it out!
self.vpc = ec2.Vpc(
self,
"VPC",
ip_addresses=ec2.IpAddresses.cidr(self.env.networking.vpc_cidr_range),
max_azs=3,
enable_dns_hostnames=True,
enable_dns_support=True,
subnet_configuration=[
ec2.SubnetConfiguration(
name="Presentation Subnet",
subnet_type=ec2.SubnetType.PUBLIC,
cidr_mask=24
),
ec2.SubnetConfiguration(
name="Application Subnet",
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidr_mask=24
),
ec2.SubnetConfiguration(
name="Data Subnet",
subnet_type=ec2.SubnetType.PRIVATE_ISOLATED,
cidr_mask=24
),
],
nat_gateways=1
)
Let’s have a peek within AWS.
Now this is where the magic happens. This is where we keep our:
Let’s start at the top.
self.alb = elb.ApplicationLoadBalancer(
self,
"ALB",
security_group=self.alb_sg,
vpc=self.vpc,
internet_facing= True,
load_balancer_name=f"alb-{self.env.compute.app_name}-{env_name}",
)
self.alb_target_group = elb.ApplicationTargetGroup(
self,
"TargetGroup",
port = 80,
protocol = elb.ApplicationProtocol.HTTP,
vpc = self.vpc,
target_group_name = f"target-group-{self.env.compute.app_name}-{env_name}",
health_check=elb.HealthCheck(
enabled=True,
path="/"
),
)
self.listener = elb.ApplicationListener(
self,
"Http-Listener",
load_balancer = self.alb,
protocol=elb.ApplicationProtocol.HTTP,
port = 80,
default_target_groups = [self.alb_target_group],
)
Looking good!
self.task_definition = ecs.FargateTaskDefinition(self,
"TaskDefinition",
cpu = 512,
memory_limit_mib = 1024,
execution_role = self.task_role,
task_role = self.task_role,
family = f"{self.env.compute.app_name}-{env_name}",
)
self.task_definition.add_container(
"Nginx Hello World",
image=ecs.ContainerImage.from_registry(self.env.compute.image),
port_mappings = [ecs.PortMapping(container_port=80, host_port=80)],
container_name = f"{self.env.compute.app_name}-{env_name}",
logging=ecs.AwsLogDriver(
stream_prefix=env_name,
mode=ecs.AwsLogDriverMode.NON_BLOCKING,
log_group=self.log_group)
)
So this is a simplistic task definition. Why? It’s a simple web application.
The more complex web applications you bring into ECS, the more complex task definition you need.
self.cluster = ecs.Cluster(
self,
"Cluster",
cluster_name = f"cluster-{self.env.compute.app_name}-{env_name}",
container_insights = True,
enable_fargate_capacity_providers = True,
vpc = self.vpc
)
self.service = ecs.FargateService(
self,
"Service",
task_definition = self.task_definition,
security_groups = [self.cluster_sg],
cluster = self.cluster,
desired_count = 2,
enable_ecs_managed_tags = True,
service_name = f"service-{self.env.compute.app_name}-{env_name}",
)
The primary role of ECS Clusters is a grouping of Tasks & Services.
The primary role of ECS Services is scaling the number of tasks - manually or automatically.
These are quite standard pieces of kit within an ECS web application deployment - most deployments will need little customization here.
self.log_group = logs.LogGroup(
self,
"LogGroup",
retention=logs.RetentionDays.THREE_MONTHS,
log_group_name=f"/aws/ecs/{self.env.compute.app_name}-{env_name}",
)
self.cluster_sg = ec2.SecurityGroup(
self,
"Cluster_SG",
security_group_name=f"cluster-sg-{self.env.compute.app_name}-{env_name}",
vpc=self.vpc,
description="Security Group for ECS Tasks",
allow_all_outbound=True
)
self.alb_sg = ec2.SecurityGroup(
self,
"LoadBalancer_SG",
security_group_name=f"alb-sg-{self.env.compute.app_name}-{env_name}",
vpc=self.vpc,
description="Security Group for ALB",
allow_all_outbound=True
)
self.alb_sg.add_ingress_rule(peer=ec2.Peer.any_ipv4(),
connection=ec2.Port.tcp(80),
description="Open Port from ALB to World")
self.alb_sg.add_ingress_rule(peer=ec2.Peer.any_ipv4(),
connection=ec2.Port.tcp(443),
description="Open Port from ALB to World")
self.task_role = iam.Role(
self,
'Task_Role',
assumed_by=iam.ServicePrincipal(service='ecs-tasks.amazonaws.com'),
role_name=f"task-role-{self.env.compute.app_name}-{env_name}",
managed_policies=[iam.ManagedPolicy.from_aws_managed_policy_name(
managed_policy_name='service-role/AmazonECSTaskExecutionRolePolicy'
)],
inline_policies={
"Task": iam.PolicyDocument(
statements=[
iam.PolicyStatement(
actions=["ecs:StartTelemetrySession", "cloudwatch:*"],
effect=iam.Effect.ALLOW,
resources = ["*"]
)
]
)
}
)
These are very standard components. If you’re familiar with CDK, you will have seen these a thousand times.
Wait, there wasn’t any infrastructure in the Data subnets right?
Correct.
Why?
I felt it was important to deploy the network capable of a data layer. But my example application does not include one.
It’s working!
Not only that, we can see it is highly available across two instances. Each instance is in a different subnet & availability zone.
You can find the complete source code on my GitHub: https://github.com/JeremyRitchie/webapp-ecs-quickstart-cd
I hope you found this informative.
Flick me a message if you have any questions and i’ll update it here!