Skip to content

Instantly share code, notes, and snippets.

@laurelmay
Last active October 3, 2022 21:17
Show Gist options
  • Save laurelmay/b44b317da750ea60d19adb74db58f84a to your computer and use it in GitHub Desktop.
Save laurelmay/b44b317da750ea60d19adb74db58f84a to your computer and use it in GitHub Desktop.
---
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