To run a production-ready application on EC2 gives you maximum freedom but also maximum responsibilities. By production-ready, I mean:

Highly available: no single point of failure

Scalable: increase or decrease the number of instances based on load

Frictionless deployment: deliver new versions of your application automatically without downtime

Secure: patching operating systems and libraries frequently, follow the least privilege principle in all areas

Operations: provide tools like logging, monitoring and alerting to recognize and debug problems

The overall architecture will consist of a load balancer, forwarding requests to multiple EC2 instances, distributed among different availability zones (data centers).

The diagram was created with Cloudcraft - Visualize your cloud architecture like a pro.

AWS Velocity Series Most of our clients use AWS to reduce time-to-market following an agile approach. But AWS is only one part of the solution. In this article series, I show you how we help our clients to improve velocity: the time from idea to production. Discover all posts!

Let’s start simple and tackle all the challenges along the way.

A single EC2 instance is a single point of failure

A single EC2 instance is a single point of failure. When you want to run a production-ready app on EC2, you need more than one EC2 instance. Luckily, AWS provides a way to manage multiple EC2 instances: the Auto Scaling Group. But if you run multiple EC2 instances to serve your application, you also need a load balancer to distribute the requests to one of the EC2 instances.

In the Local development environment part of this series, you created an infrastructure folder which is empty by now. It’s time to change this. You will now create a CloudFormation template that describes the infrastructure that is needed to run the app on EC2 instances.

Load balancer

You can follow step by step or get the full source code here: https://github.com/widdix/aws-velocity

Create a file infrastructure/ec2.yml . The first part of the file contains the load balancer. To fully describe an Application Load Balancer, you need:

A Security Group that allows traffic on port 80

The lApplication Load Balancer itself

A Target Group, which is a fleet of EC2 instances that can receive traffic from the load balancer

A Listener, which wires the load balancer together with the target group and defines the listening port

Watch out for comments with more detailed information in the code.

infrastructure/ec2.yml GitHub

AWSTemplateFormatVersion: '2010-09-09'

Description: 'EC2'

Parameters:



ParentVPCStack:

Description: 'Stack name of parent VPC stack based on vpc/vpc-*azs.yaml template.'

Type: String

Conditions:

Resources:



LoadBalancerSecurityGroup:

Type: 'AWS::EC2::SecurityGroup'

Properties:

GroupDescription: 'load-balancer-sg'

VpcId:

'Fn::ImportValue' : !Sub '${ParentVPCStack}-VPC'

SecurityGroupIngress:

- CidrIp: '0.0.0.0/0'

FromPort: 80

ToPort: 80

IpProtocol: tcp



LoadBalancer:

Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'

Properties:

Scheme: 'internet-facing'

SecurityGroups:

- !Ref LoadBalancerSecurityGroup

Subnets:

- 'Fn::ImportValue' : !Sub '${ParentVPCStack}-SubnetAPublic'

- 'Fn::ImportValue' : !Sub '${ParentVPCStack}-SubnetBPublic'

Tags:

- Key: Name

Value: 'load-balancer'



TargetGroup:

Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'

Properties:

HealthCheckIntervalSeconds: 15

HealthCheckPath: '/5'

HealthCheckPort: 3000

HealthCheckProtocol: HTTP

HealthCheckTimeoutSeconds: 10

HealthyThresholdCount: 2

UnhealthyThresholdCount: 8

Matcher:

HttpCode: 200

Port: 3000

Protocol: HTTP

Tags:

- Key: Name

Value: 'target-group'

VpcId:

'Fn::ImportValue' : !Sub '${ParentVPCStack}-VPC'



Listener:

Type: 'AWS::ElasticLoadBalancingV2::Listener'

Properties:

DefaultActions:

- TargetGroupArn: !Ref TargetGroup

Type: forward

LoadBalancerArn: !Ref LoadBalancer

Port: 80

Protocol: HTTP



Outputs:

DNSName:

Description: 'The DNS name for the load balancer.'

Value: !GetAtt 'LoadBalancer.DNSName'

Export:

Name: !Sub '${AWS::StackName}-DNSName'



URL:

Description: 'URL to the load balancer.'

Value: !Sub 'http://${LoadBalancer.DNSName}'

Export:

Name: !Sub '${AWS::StackName}-URL'



But how do you get notified if something goes wrong? Let’s add a parameter to the Parameters section to make the receiver configurable:

infrastructure/ec2.yml GitHub AdminEmail:

Description: 'The email address of the admin who receives alerts.'

Type: String



Alerts are triggered by a CloudWatch Alarm which can send an alert to an SNS topic. You can subscribe to this topic via an email address to receive the alerts. Let’s create a SNS topic and two alarms in the Resources section:

infrastructure/ec2.yml GitHub

Alerts:

Type: 'AWS::SNS::Topic'

Properties:

Subscription:

- Endpoint: !Ref AdminEmail

Protocol: email



LoadBalancer5XXAlarm:

Type: 'AWS::CloudWatch::Alarm'

Properties:

EvaluationPeriods: 1

Statistic: Sum

Threshold: 0

AlarmDescription: 'Load balancer responds with 5XX status codes.'

Period: 60

AlarmActions:

- !Ref Alerts

Namespace: 'AWS/ApplicationELB'

Dimensions:

- Name: LoadBalancer

Value: !GetAtt 'LoadBalancer.LoadBalancerFullName'

ComparisonOperator: GreaterThanThreshold

MetricName: HTTPCode_ELB_5XX_Count



LoadBalancerTargetGroup5XXAlarm:

Type: 'AWS::CloudWatch::Alarm'

Properties:

EvaluationPeriods: 1

Statistic: Sum

Threshold: 0

AlarmDescription: 'Load balancer target responds with 5XX status codes.'

Period: 60

AlarmActions:

- !Ref Alerts

Namespace: 'AWS/ApplicationELB'

Dimensions:

- Name: LoadBalancer

Value: !GetAtt 'LoadBalancer.LoadBalancerFullName'

ComparisonOperator: GreaterThanThreshold

MetricName: HTTPCode_Target_5XX_Count



Let’s recap what you implemented: A load balancer with a firewall rule that allows traffic on port 80. In the case of 5XX status codes you will receive an Email. But the load balancer alone is not enough. Now it’s time to add the EC2 instances.