Last active
October 3, 2022 21:17
-
-
Save laurelmay/b44b317da750ea60d19adb74db58f84a to your computer and use it in GitHub Desktop.
A basic Elastic Load Balancing demo - https://calculator.aws/#/estimate?id=01f237b2db0f66b15326ab19b56a5239d3c1422a
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
AWSTemplateFormatVersion: "2010-09-09" | |
Description: >- | |
Creates a basic setup with an application load balancer and several EC2 instances | |
spread across various subnets. The application is deployed in a two-tier architecture. | |
A VPC is created. Each AZ has two subnets (public and private). Instances live in the | |
private subnet and the load balancer, gateways, and other supporting resources live in | |
the public subnet. | |
This demonstrates a basic deployment of a load balancer meant to highlight the benefits | |
to both availability and security that you get from this setup. | |
A cost estimate is available at: | |
https://calculator.aws/#/estimate?id=01f237b2db0f66b15326ab19b56a5239d3c1422a | |
This template does not work in AWS GovCloud (US) due to the dependence on the Ubuntu AMI | |
SSM parameters. | |
Parameters: | |
CidrBlock: | |
Type: String | |
Description: The IPv4 CIDR block to use for the VPC. This should be a /24. | |
Default: "10.134.126.0/24" | |
Resources: | |
# To get started with any higher level abstractions, we'll need to create a VPC. | |
# Theoretically, there is probably a default VPC within the account that can be used; | |
# however, it's hard to inspect that and have a CloudFormation template that "just works" | |
# across multiple AZs/subnets without creating our own VPC. In general, we want to get | |
# in the habit of not using the Default VPC anyway. | |
# This section has quite a bit of copy-and-paste bits, especially in the subnet creation. | |
# The three public subnets are nearly identical to one another (besided names, CIDR allocations, | |
# and chosen AZ) and the three private subnets are also nearly identical. Unfortunately, we've | |
# got no loops so it just has to be rewritten. | |
# IP addresses are allocated as /27s to each subnet; they are allocated "horizontally", so | |
# the first three /27s in the given VPC CIDR are for the public subnets and the next set of | |
# three are the private subnets (an alternate allocation strategy might be to assign a /26 to | |
# each availability zone but generally that is less useful when writing NACLs and SG rules). | |
Vpc: | |
Type: AWS::EC2::VPC | |
Properties: | |
CidrBlock: !Ref CidrBlock | |
EnableDnsHostnames: true | |
EnableDnsSupport: true | |
InstanceTenancy: default | |
InternetGateway: | |
Type: AWS::EC2::InternetGateway | |
InternetGatewayAttachment: | |
Type: AWS::EC2::VPCGatewayAttachment | |
Properties: | |
VpcId: !Ref Vpc | |
InternetGatewayId: !Ref InternetGateway | |
# Because the public subnets all share the same routing configuration (dumping all traffic | |
# to the Internet Gateway) we can create a single route table and route which can later | |
# be associated with the individual subnets to allow Internet access. The presence of the | |
# Internet Gateway and the 0.0.0.0/0 route pointing to it is the primary thing that makes | |
# these subnets "public". | |
PublicSubnetRouteTable: | |
Type: AWS::EC2::RouteTable | |
Properties: | |
VpcId: !Ref Vpc | |
PublicSubnetRouteTableInternetGatewayRoute: | |
Type: AWS::EC2::Route | |
Properties: | |
RouteTableId: !Ref PublicSubnetRouteTable | |
DestinationCidrBlock: 0.0.0.0/0 | |
GatewayId: !Ref InternetGateway | |
DependsOn: | |
- InternetGatewayAttachment | |
# Public Subnet (AZ A) | |
PublicSubnetA: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: !Ref Vpc | |
AvailabilityZone: | |
Fn::Select: | |
- 0 | |
- Fn::GetAZs: "" | |
CidrBlock: | |
Fn::Select: | |
- 0 | |
- Fn::Cidr: | |
- !Ref CidrBlock | |
- 6 | |
- 5 | |
Tags: | |
- Key: Name | |
Value: Public AZ A | |
PublicSubnetARouteTableAssoc: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
RouteTableId: !Ref PublicSubnetRouteTable | |
SubnetId: !Ref PublicSubnetA | |
PublicSubnetANatGatewayEip: | |
Type: AWS::EC2::EIP | |
PublicSubnetANatGateway: | |
Type: AWS::EC2::NatGateway | |
Properties: | |
SubnetId: !Ref PublicSubnetA | |
AllocationId: !GetAtt PublicSubnetANatGatewayEip.AllocationId | |
ConnectivityType: public | |
Tags: | |
- Key: Name | |
Value: AZ A | |
DependsOn: | |
- PublicSubnetARouteTableAssoc | |
# Public Subnet (AZ B) | |
PublicSubnetB: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: !Ref Vpc | |
AvailabilityZone: | |
Fn::Select: | |
- 1 | |
- Fn::GetAZs: "" | |
CidrBlock: | |
Fn::Select: | |
- 1 | |
- Fn::Cidr: | |
- !Ref CidrBlock | |
- 6 | |
- 5 | |
Tags: | |
- Key: Name | |
Value: Public AZ B | |
PublicSubnetBRouteTableAssoc: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
RouteTableId: !Ref PublicSubnetRouteTable | |
SubnetId: !Ref PublicSubnetB | |
PublicSubnetBNatGatewayEip: | |
Type: AWS::EC2::EIP | |
PublicSubnetBNatGateway: | |
Type: AWS::EC2::NatGateway | |
Properties: | |
SubnetId: !Ref PublicSubnetB | |
AllocationId: !GetAtt PublicSubnetBNatGatewayEip.AllocationId | |
ConnectivityType: public | |
Tags: | |
- Key: Name | |
Value: AZ B | |
DependsOn: | |
- PublicSubnetBRouteTableAssoc | |
# Public Subnet (AZ C) | |
PublicSubnetC: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: !Ref Vpc | |
AvailabilityZone: | |
Fn::Select: | |
- 2 | |
- Fn::GetAZs: "" | |
CidrBlock: | |
Fn::Select: | |
- 2 | |
- Fn::Cidr: | |
- !Ref CidrBlock | |
- 6 | |
- 5 | |
Tags: | |
- Key: Name | |
Value: Public AZ C | |
PublicSubnetCRouteTableAssoc: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
RouteTableId: !Ref PublicSubnetRouteTable | |
SubnetId: !Ref PublicSubnetC | |
PublicSubnetCNatGatewayEip: | |
Type: AWS::EC2::EIP | |
PublicSubnetCNatGateway: | |
Type: AWS::EC2::NatGateway | |
Properties: | |
SubnetId: !Ref PublicSubnetC | |
AllocationId: !GetAtt PublicSubnetCNatGatewayEip.AllocationId | |
ConnectivityType: public | |
Tags: | |
- Key: Name | |
Value: AZ C | |
DependsOn: | |
- PublicSubnetCRouteTableAssoc | |
# Private Subnet (AZ A) | |
PrivateSubnetA: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: !Ref Vpc | |
AvailabilityZone: | |
Fn::Select: | |
- 0 | |
- Fn::GetAZs: "" | |
CidrBlock: | |
Fn::Select: | |
- 3 | |
- Fn::Cidr: | |
- !Ref CidrBlock | |
- 6 | |
- 5 | |
Tags: | |
- Key: Name | |
Value: Private AZ A | |
PrivateSubnetARouteTable: | |
Type: AWS::EC2::RouteTable | |
Properties: | |
VpcId: !Ref Vpc | |
PrivateSubnetARouteToNatGateway: | |
Type: AWS::EC2::Route | |
Properties: | |
RouteTableId: !Ref PrivateSubnetARouteTable | |
DestinationCidrBlock: 0.0.0.0/0 | |
NatGatewayId: !Ref PublicSubnetANatGateway | |
PrivateSubnetARouteTableAssoc: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
RouteTableId: !Ref PrivateSubnetARouteTable | |
SubnetId: !Ref PrivateSubnetA | |
# Private Subnet (AZ B) | |
PrivateSubnetB: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: !Ref Vpc | |
AvailabilityZone: | |
Fn::Select: | |
- 1 | |
- Fn::GetAZs: "" | |
CidrBlock: | |
Fn::Select: | |
- 4 | |
- Fn::Cidr: | |
- !Ref CidrBlock | |
- 6 | |
- 5 | |
Tags: | |
- Key: Name | |
Value: Private AZ B | |
PrivateSubnetBRouteTable: | |
Type: AWS::EC2::RouteTable | |
Properties: | |
VpcId: !Ref Vpc | |
PrivateSubnetBRouteToNatGateway: | |
Type: AWS::EC2::Route | |
Properties: | |
RouteTableId: !Ref PrivateSubnetBRouteTable | |
DestinationCidrBlock: 0.0.0.0/0 | |
NatGatewayId: !Ref PublicSubnetBNatGateway | |
PrivateSubnetBRouteTableAssoc: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
RouteTableId: !Ref PrivateSubnetBRouteTable | |
SubnetId: !Ref PrivateSubnetB | |
# Private Subnet (AZ C) | |
PrivateSubnetC: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: !Ref Vpc | |
AvailabilityZone: | |
Fn::Select: | |
- 2 | |
- Fn::GetAZs: "" | |
CidrBlock: | |
Fn::Select: | |
- 5 | |
- Fn::Cidr: | |
- !Ref CidrBlock | |
- 6 | |
- 5 | |
Tags: | |
- Key: Name | |
Value: Private AZ C | |
PrivateSubnetCRouteTable: | |
Type: AWS::EC2::RouteTable | |
Properties: | |
VpcId: !Ref Vpc | |
PrivateSubnetCRouteToNatGateway: | |
Type: AWS::EC2::Route | |
Properties: | |
RouteTableId: !Ref PrivateSubnetCRouteTable | |
DestinationCidrBlock: 0.0.0.0/0 | |
NatGatewayId: !Ref PublicSubnetCNatGateway | |
PrivateSubnetCRouteTableAssoc: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
RouteTableId: !Ref PrivateSubnetCRouteTable | |
SubnetId: !Ref PrivateSubnetC | |
# Okay!! Now that the base networking is out of the way, we can actually start to build | |
# the resources that this demo is focused on! There is going to be quite a bit of copy-and-paste | |
# here when defining the EC2 instances (Challenge: improve this by using an Autoscaling Group). | |
# The EC2 instances will go into the _private_ subnets, one within each. The instances | |
# are configured to use a basic web server which will show their AZ and the private IP address | |
# of the host. | |
WebServerSecurityGroup: | |
Type: AWS::EC2::SecurityGroup | |
Properties: | |
GroupDescription: Allows traffic to the private web server instances | |
VpcId: !Ref Vpc | |
WebServerInstanceRole: | |
Type: AWS::IAM::Role | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: "2012-10-17" | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: ec2.amazonaws.com | |
Action: sts:AssumeRole | |
ManagedPolicyArns: | |
- !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore | |
WebServerInstanceProfile: | |
Type: AWS::IAM::InstanceProfile | |
Properties: | |
Roles: | |
- !Ref WebServerInstanceRole | |
WebServerLaunchTemplate: | |
Type: AWS::EC2::LaunchTemplate | |
Properties: | |
LaunchTemplateData: | |
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/arm64/hvm/ebs-gp2/ami-id}}' | |
InstanceType: t4g.micro | |
SecurityGroupIds: | |
- !GetAtt WebServerSecurityGroup.GroupId | |
IamInstanceProfile: | |
Arn: !GetAtt WebServerInstanceProfile.Arn | |
MaintenanceOptions: | |
AutoRecovery: default | |
MetadataOptions: | |
HttpEndpoint: enabled | |
HttpPutResponseHopLimit: 1 | |
HttpTokens: required | |
BlockDeviceMappings: | |
- DeviceName: /dev/sda1 | |
Ebs: | |
DeleteOnTermination: true | |
VolumeType: gp3 | |
VolumeSize: 10 | |
UserData: | |
Fn::Base64: | | |
#!/usr/bin/env bash | |
# Update packages and install dependencies | |
sudo apt update -y | |
sudo apt upgrade -y | |
sudo apt install -y nginx | |
# Get a token for the IMDSv2 | |
TOKEN="$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" | |
# Retrieve information from the IMDS to put on the web page | |
imds-request() { | |
local token="$1" | |
local path="$2" | |
curl -H "X-aws-ec2-metadata-token: $1" -v "http://169.254.169.254/latest/meta-data/$path" | |
} | |
INSTANCE_ID="$(imds-request "$TOKEN" "instance-id")" | |
AZ_NAME="$(imds-request "$TOKEN" "placement/availability-zone")" | |
IP_ADDRESS="$(imds-request "$TOKEN" "local-ipv4")" | |
# Create a basic web page with information to show when load balancing | |
cat << EOF > /var/www/html/index.html | |
<!doctype html> | |
<html> | |
<head> | |
<title>Hello from $INSTANCE_ID</title> | |
</head> | |
<body> | |
<h1>Hello from $INSTANCE_ID</h1> | |
<p>This page is served by the instance in $AZ_NAME with the IP $IP_ADDRESS.</p> | |
</body> | |
</html> | |
EOF | |
WebServerA: | |
Type: AWS::EC2::Instance | |
Properties: | |
LaunchTemplate: | |
LaunchTemplateId: !Ref WebServerLaunchTemplate | |
Version: !GetAtt WebServerLaunchTemplate.LatestVersionNumber | |
SubnetId: !Ref PrivateSubnetA | |
PropagateTagsToVolumeOnCreation: true | |
Tags: | |
- Key: Name | |
Value: Web Server A | |
DependsOn: | |
- PrivateSubnetARouteTableAssoc | |
WebServerB: | |
Type: AWS::EC2::Instance | |
Properties: | |
LaunchTemplate: | |
LaunchTemplateId: !Ref WebServerLaunchTemplate | |
Version: !GetAtt WebServerLaunchTemplate.LatestVersionNumber | |
SubnetId: !Ref PrivateSubnetB | |
PropagateTagsToVolumeOnCreation: true | |
Tags: | |
- Key: Name | |
Value: Web Server B | |
DependsOn: | |
- PrivateSubnetBRouteTableAssoc | |
WebServerC: | |
Type: AWS::EC2::Instance | |
Properties: | |
LaunchTemplate: | |
LaunchTemplateId: !Ref WebServerLaunchTemplate | |
Version: !GetAtt WebServerLaunchTemplate.LatestVersionNumber | |
SubnetId: !Ref PrivateSubnetC | |
PropagateTagsToVolumeOnCreation: true | |
Tags: | |
- Key: Name | |
Value: Web Server C | |
DependsOn: | |
- PrivateSubnetCRouteTableAssoc | |
# And now we get to actually build the load balancer!! | |
WebLoadBalancerSecurityGroup: | |
Type: AWS::EC2::SecurityGroup | |
Properties: | |
GroupDescription: Allow internet traffic to the load balancer | |
VpcId: !Ref Vpc | |
SecurityGroupIngress: | |
- IpProtocol: tcp | |
FromPort: 80 | |
ToPort: 80 | |
CidrIp: 0.0.0.0/0 | |
WebLoadBalancer: | |
Type: AWS::ElasticLoadBalancingV2::LoadBalancer | |
Properties: | |
IpAddressType: ipv4 | |
Subnets: | |
- !Ref PublicSubnetA | |
- !Ref PublicSubnetB | |
- !Ref PublicSubnetC | |
Scheme: internet-facing | |
SecurityGroups: | |
- !GetAtt WebLoadBalancerSecurityGroup.GroupId | |
WebServerTargetGroup: | |
Type: AWS::ElasticLoadBalancingV2::TargetGroup | |
Properties: | |
Protocol: HTTP | |
Port: 80 | |
TargetType: instance | |
Targets: | |
- Id: !Ref WebServerA | |
- Id: !Ref WebServerB | |
- Id: !Ref WebServerC | |
VpcId: !Ref Vpc | |
LoadBalancerListener: | |
Type: AWS::ElasticLoadBalancingV2::Listener | |
Properties: | |
LoadBalancerArn: !Ref WebLoadBalancer | |
Port: 80 | |
Protocol: HTTP | |
DefaultActions: | |
- Type: forward | |
TargetGroupArn: !Ref WebServerTargetGroup | |
# And as a final bit of cleanup, let the load balancer reach the instances | |
WebLoadBalancerToWebServerIngressRule: | |
Type: AWS::EC2::SecurityGroupIngress | |
Properties: | |
GroupId: !GetAtt WebServerSecurityGroup.GroupId | |
IpProtocol: tcp | |
FromPort: 80 | |
ToPort: 80 | |
SourceSecurityGroupId: !GetAtt WebLoadBalancerSecurityGroup.GroupId | |
Outputs: | |
LoadBalancerDomainName: | |
Value: !GetAtt WebLoadBalancer.DNSName | |
LoadBalancerUrl: | |
Value: !Sub "http://${WebLoadBalancer.DNSName}/" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment