Quick Start WebApp on ECS using AWS CDK

Quick Start WebApp on ECS using AWS CDK

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.

Table of contents

Goal

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.

Architecture

Architecture

Architecture

Architecture

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.

CDK Pipeline

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.

Networking Stack

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
)

Expansion Opportunity

  • Add more NAT gateways (one per AZ) to make this highly available.

Let’s have a peek within AWS.

AWS Subnets

AWS Subnets

AWS Subnets

Compute Stack

Now this is where the magic happens. This is where we keep our:

  • Application Load Balancer & Target Group
  • ECS Task Definition
  • ECS Service & Cluster
  • Associated IAM Roles, Security Groups & Log Groups

Let’s start at the top.

ALB

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!

Expansion Opportunity

  • Adjust the listener port to whatever port your web application listens on. 8080 is another common one.
  • Add additional listeners for HTTPS or to provide specific functionality for certain requests.
  • Add custom health check relative to your web application.

ECS Task Definition

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.

Expansion Opportunity

  • Switch image to a different web application. Perhaps a .net demo app? https://hub.docker.com/_/microsoft-dotnet-samples
  • Switch task definition to json file import. Necessary for very complex task definitions.
  • Ensure container/host port mapping is correct relative to your web application.

ECS Service & Cluster

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.

Expansion Opportunity

  • Add ECS Task metric-based autoscaling to the service.
  • Switch task definition to json file import. Necessary for very complex task definitions.
  • Use a custom capacity provider for the cluster. Save money on Fargate Spot instances!

Associated IAM Roles, Security Groups & Log Groups

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.

Expansion Opportunity

  • Use principle of least privilege for Task Role. Notice where it could be improved?

Data Stack?

Wait, there wasn’t any infrastructure in the Data subnets right?

Correct.

Why?

  1. It’s expensive. This template is for development and learning. Running a $200 per month PostGreSQL DB won’t be doing anyone any favours.
  2. Do you REALLY need relational? Consider NoSQL such as DynamoDB.
  3. My demo application doesn’t need a database. My static website doesn’t need a database (though i’m adding a NoSQL one soon). Neither does this template.

I felt it was important to deploy the network capable of a data layer. But my example application does not include one.

Testing

Web Application

Web Application

Web Application

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.

Expansion Opportunity

  • Add a custom domain with HTTPS to this application.

Conclusion

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!

comments powered by Disqus