# Following example shows how to create AWS Gateway Load Balancer (GWLB) # Appliance VPC for centralized architecture using AWS CloudFormation. # For architecture details refer to blog: # https://aws.amazon.com/blogs/networking-and-content-delivery/centralized-inspection-architecture-with-aws-gateway-load-balancer-and-aws-transit-gateway/ # Template uses Amazon Linux 2 instances as appliances behind the GWLB. # Configures iptables on instances for hairpin setup. The hairpin setup # allows traffic coming from GWLB on appliance to be sent back to GWLB. # iptables configuration is for sample purpose only. It allows all the traffic! Use it for GWLB POC only** AWSTemplateFormatVersion: "2010-09-09" Description: >- AWS CloudFormation Sample Template For Appliance VPC Setup in Centralized Archite For Gateway Load Balancer (GWLB). It creates a hairpin setup using iptables. Appliance VPC is created in the same account as Spoke VPCs and Transit Gateay. This template creates: - 1 VPC - 1 IGW - 2 NAT gateways - 6 subnets, 3 in each Availability Zone (AZ) - 6 route tables, 3 in each AZ - 1 Security group - port 22 access - port 80 access - All TCP, UDP and ICMP from VPC CIDR - 2 Amazon Linux 2 instances acting as appliance in each AZ. Registered as targets behind a GWLB. Configures iptables on instances for hairpin setup - 1 Amazon Linux 2 acting as bastion host - 1 GWLB - 1 Target group for GWLB - 1 Listner for GWLB - 1 VPC endpoint service - 2 GWLB endpoints, 1 in each AZ **WARNING** This template creates one or more Amazon EC2 instances, GWLB, GWLB endpints and NAT gateways. You will be billed for the AWS resources used if you create a stack from this template. Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Network Configuration Parameters: - VpcCidr - AvailabilityZone1 - TgwAttachSubnet1Cidr - ApplianceSubnet1Cidr - NatgwSubnet1Cidr - AvailabilityZone2 - TgwAttachSubnet2Cidr - ApplianceSubnet2Cidr - NatgwSubnet2Cidr - Label: default: Appliance Configuration Parameters: - ApplianceInstanceType - ApplianceInstanceAmiId - ApplianceInstanceDiskSize - KeyPairName - AccessLocation - Label: default: Gateway Load Balancer Configuration Parameters: - GwlbName - TargetGroupName - HealthPort - HealthProtocol - Label: default: Appliance VPC Route Table Configuration Parameters: - Spoke1VpcCidr - Spoke2VpcCidr ParameterLabels: VpcCidr: default: Appliance VPC - Network CIDR for VPC AvailabilityZone1: default: Appliance VPC - Availability Zone 1 TgwAttachSubnet1Cidr: default: Appliance VPC - TGW Attachment Subnet 1 CIDR in AZ1 ApplianceSubnet1Cidr: default: Appliance VPC - Appliance Subnet 1 CIDR in AZ1 NatgwSubnet1Cidr: default: Appliance VPC - NAT GW Subnet 1 CIDR in AZ1 AvailabilityZone2: default: Availability Zone 2 TgwAttachSubnet2Cidr: default: Appliance VPC - TGW Attachment Subnet 2 CIDR in AZ2 ApplianceSubnet2Cidr: default: Appliance VPC - Appliance Subnet 2 CIDR in AZ2 NatgwSubnet2Cidr: default: Appliance VPC - NAT GW Subnet 2 CIDR in AZ2 ApplianceInstanceType: default: Appliance Instance Type ApplianceInstanceAmiId: default: Latest AMI ID for appliance (ec2 instance) ApplianceInstanceDiskSize: default: Appliance Instance Size in GB KeyPairName: default: KeyPair required for accessing Appliance instance AccessLocation: default: Network CIDR to access Appliance instance Spoke1VpcCidr: default: Spoke1 VPC Network CIDR Spoke2VpcCidr: default: Spoke2 VPC Network CIDR GwlbName: default: Appliance VPC - Gateway Load Balancer Name TargetGroupName: default: Appliance VPC - Target Group Name HealthPort: default: Appliance VPC - Health Check Port HealthProtocol: default: Appliance VPC - Health Check Protocol Parameters: VpcCidr: AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$" Default: 192.168.1.0/24 Description: Appliance VPC - CIDR block for the VPC Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/y AvailabilityZone1: Description: Appliance VPC - Availability Zone 1 Type: AWS::EC2::AvailabilityZone::Name ConstraintDescription: Valid Availability Zone Id TgwAttachSubnet1Cidr: AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$" Default: 192.168.1.0/28 Description: Appliance VPC - TGW Attachment Subnet 1 CIDR in AZ1 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 ApplianceSubnet1Cidr: AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$" Default: 192.168.1.16/28 Description: Appliance VPC - Appliance Subnet 1 CIDR in AZ1 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 NatgwSubnet1Cidr: AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$" Default: 192.168.1.32/28 Description: Appliance VPC - NAT GW Subnet 1 CIDR in AZ1 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 AvailabilityZone2: Description: Appliance VPC - Availability Zone 2 Type: AWS::EC2::AvailabilityZone::Name ConstraintDescription: Valid Availability Zone Id TgwAttachSubnet2Cidr: AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$" Default: 192.168.1.48/28 Description: Appliance VPC - TGW Attachment Subnet 2 CIDR in AZ2 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 ApplianceSubnet2Cidr: AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$" Default: 192.168.1.64/28 Description: Appliance VPC - Appliance Subnet 2 CIDR in AZ2 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 NatgwSubnet2Cidr: AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$" Default: 192.168.1.80/28 Description: Appliance VPC - NAT GW Subnet 2 CIDR in AZ2 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 ApplianceInstanceType: Description: Select EC2 instance type for Appliance instance. Default is set to t2.micro Default: t2.micro AllowedValues: - t2.micro Type: String ApplianceInstanceAmiId: Description: EC2 Instance AMI ID retrieved using SSM Type: String ApplianceInstanceDiskSize: Description: Appliance instance disk size in GB. Default is set to 8GB Default: 8 AllowedValues: [8] Type: Number ConstraintDescription: Should be a valid instance size in GB KeyPairName: Description: EC2 KeyPair required for accessing EC2 instance Type: AWS::EC2::KeyPair::KeyName ConstraintDescription: Must be the name of an existing EC2 KeyPair AccessLocation: Description: >- Enter desired Network CIDR to allow traffic to appliance. Default is set to access from anywhere and it is not recommended AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" MinLength: "9" MaxLength: "18" Default: 0.0.0.0/0 Type: String ConstraintDescription: Must be a valid Network CIDR of the form x.x.x.x/y Spoke1VpcCidr: Description: Enter Spoke1 VPC Network CIDR required for routing desired traffic to Spoke1. AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" Default: 10.0.0.0/24 Type: String ConstraintDescription: Must be a valid Network CIDR of the form x.x.x.x/y Spoke2VpcCidr: Description: Enter Spoke2 VPC Network CIDR required for routing desired traffic to Spoke2. AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" Default: 10.0.1.0/24 Type: String ConstraintDescription: Must be a valid Network CIDR of the form x.x.x.x/y GwlbName: Description: >- Gateway Load Balancer name. This name must be unique within your AWS account and can have a maximum of 32 alphanumeric characters and hyphens. A name cannot begin or end with a hyphen. Type: String Default: gwlb1 ConstraintDescription: Must be a valid GWLB Name TargetGroupName: Description: Target Group Name Type: String Default: gwlb1-tg1 ConstraintDescription: Must be a valid target group name HealthProtocol: Description: >- The protocol GWLB uses when performing health checks on targets. Default is HTTP. Type: String Default: HTTP AllowedValues: ['TCP', 'HTTP', 'HTTPS'] ConstraintDescription: Must be a valid health check protocol HealthPort: Description: >- The port the load balancer uses when performing health checks on targets. Default is 80. Type: String Default: '80' ConstraintDescription: Must be a valid health check port Resources: # Create VPC, IGW and associate IGW with VPC: Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VpcCidr EnableDnsSupport: "true" EnableDnsHostnames: "true" InstanceTenancy: default Tags: - Key: Name Value: !Join - "" - - !Ref AWS::StackName - "-vpc" InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Join - "" - - !Ref AWS::StackName - "-igw" AttachInternetGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref Vpc InternetGatewayId: !Ref InternetGateway # Create Subnets: # AZ1: TgwAttachSubnet1: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone1 CidrBlock: !Ref TgwAttachSubnet1Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-tgw-attach-subnet-1" ApplianceSubnet1: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone1 CidrBlock: !Ref ApplianceSubnet1Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-applinace-subnet-1" NatgwSubnet1: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone1 CidrBlock: !Ref NatgwSubnet1Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-natgw-subnet-1" # AZ2: TgwAttachSubnet2: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone2 CidrBlock: !Ref TgwAttachSubnet2Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-tgw-attach-subnet-2" ApplianceSubnet2: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone2 CidrBlock: !Ref ApplianceSubnet2Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-appliance-subnet-2" NatgwSubnet2: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone2 CidrBlock: !Ref NatgwSubnet2Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-natgw-subnet-2" # Create NAT Gateways: # AZ1: Eip1: Type: AWS::EC2::EIP Properties: Domain: vpc Natgw1: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt - Eip1 - AllocationId SubnetId: !Ref NatgwSubnet1 Tags: - Key: Name Value: !Sub "${AWS::StackName}-natgw-1" # AZ2: Eip2: Type: AWS::EC2::EIP Properties: Domain: vpc Natgw2: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt - Eip2 - AllocationId SubnetId: !Ref NatgwSubnet2 Tags: - Key: Name Value: !Sub "${AWS::StackName}-natgw-2" # Create Route Tables: # AZ1: TgwAttachRouteTable1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-tgw-attach-rtb-1" ApplianceRouteTable1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-appliance-rtb-1" NatGwRouteTable1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-natgw-rtb-1" # AZ2: TgwAttachRouteTable2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-tgw-attach-rtb-2" ApplianceRouteTable2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-appliance-rtb-2" NatGwRouteTable2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-natgw-rtb-2" # Associate Subnets with appropriate Route Tables: # AZ1: TgwAttachSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref TgwAttachSubnet1 RouteTableId: !Ref TgwAttachRouteTable1 ApplianceSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref ApplianceSubnet1 RouteTableId: !Ref ApplianceRouteTable1 NatgwSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref NatgwSubnet1 RouteTableId: !Ref NatGwRouteTable1 # AZ2: TgwAttachSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref TgwAttachSubnet2 RouteTableId: !Ref TgwAttachRouteTable2 ApplianceSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref ApplianceSubnet2 RouteTableId: !Ref ApplianceRouteTable2 NatgwSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref NatgwSubnet2 RouteTableId: !Ref NatGwRouteTable2 # Create Security Group: BastionSg: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref Vpc GroupName: !Sub "${AWS::StackName}-bastion-sg" GroupDescription: >- Access to bastion instance: allow SSH and ICMP access from appropriate location. Allow all traffic from VPC CIDR SecurityGroupIngress: - CidrIp: !Ref AccessLocation IpProtocol: tcp FromPort: 22 ToPort: 22 - CidrIp: !Ref AccessLocation IpProtocol: ICMP FromPort: -1 ToPort: -1 - CidrIp: !Ref VpcCidr IpProtocol: "-1" FromPort: -1 ToPort: -1 SecurityGroupEgress: - CidrIp: 0.0.0.0/0 IpProtocol: "-1" FromPort: -1 ToPort: -1 Tags: - Key: Name Value: !Sub "${AWS::StackName}-bastion-sg" ApplianceSg: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref Vpc GroupName: !Sub "${AWS::StackName}-appliance-sg" GroupDescription: >- Access to Appliance instance: allow SSH and ICMP access from Bastion SG. Allow all traffic from VPC CIDR SecurityGroupIngress: - SourceSecurityGroupId: !GetAtt BastionSg.GroupId IpProtocol: tcp FromPort: 22 ToPort: 22 - SourceSecurityGroupId: !GetAtt BastionSg.GroupId IpProtocol: ICMP FromPort: -1 ToPort: -1 - SourceSecurityGroupId: !GetAtt BastionSg.GroupId IpProtocol: tcp FromPort: 80 ToPort: 80 - CidrIp: !Ref VpcCidr IpProtocol: "-1" FromPort: -1 ToPort: -1 SecurityGroupEgress: - CidrIp: 0.0.0.0/0 IpProtocol: "-1" FromPort: -1 ToPort: -1 Tags: - Key: Name Value: !Sub "${AWS::StackName}-appliance-sg" # Create Appliances and related resources: ApplianceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - 'sts:AssumeRole' Path: / AppliancePolicy: Type: AWS::IAM::Policy Properties: PolicyName: ApplianceInstanceAccess PolicyDocument: Statement: - Effect: Allow Action: - ec2:DescribeNetworkInterfaces Resource: '*' Roles: - !Ref ApplianceRole ApplianceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref ApplianceRole Appliance1: DependsOn: - Gwlb - ApplianceRtb1Natgw1Route Type: AWS::EC2::Instance Properties: ImageId: !Ref ApplianceInstanceAmiId KeyName: !Ref KeyPairName InstanceType: !Ref ApplianceInstanceType IamInstanceProfile: !Ref ApplianceProfile SecurityGroupIds: - !Ref ApplianceSg SubnetId: !Ref ApplianceSubnet1 BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeSize: !Ref ApplianceInstanceDiskSize Tags: - Key: Name Value: !Sub "${AWS::StackName}-appliance-1" UserData: Fn::Base64: !Sub | #!/bin/bash -ex # Install packages: yum update -y; yum install jq -y; yum install httpd -y; yum install htop -y; yum install iptables-services -y; # Enable IP Forwarding and persist across reboot: # sysctl -w net.ipv4.ip_forward=1; echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/00-defaults.conf sysctl -p /etc/sysctl.d/00-defaults.conf # Configure hostname: hostnamectl set-hostname ${AWS::StackName}-appliance1; # Configure SSH client alive interval for ssh session timeout: echo 'ClientAliveInterval 60' | sudo tee --append /etc/ssh/sshd_config; service sshd restart; # Set dark background for vim: touch /home/ec2-user/.vimrc; echo "set background=dark" >> /home/ec2-user/.vimrc; # Define variables: curl --silent http://169.254.169.254/latest/dynamic/instance-identity/document > /home/ec2-user/iid; export instance_interface=$(curl --silent http://169.254.169.254/latest/meta-data/network/interfaces/macs/); export instance_vpcid=$(curl --silent http://169.254.169.254/latest/meta-data/network/interfaces/macs/$instance_interface/vpc-id); export instance_az=$(cat /home/ec2-user/iid |grep 'availability' | awk -F': ' '{print $2}' | awk -F',' '{print $1}'); export instance_ip=$(cat /home/ec2-user/iid |grep 'privateIp' | awk -F': ' '{print $2}' | awk -F',' '{print $1}' | awk -F'"' '{print$2}'); export instance_region=$(cat /home/ec2-user/iid |grep 'region' | awk -F': ' '{print $2}' | awk -F',' '{print $1}' | awk -F'"' '{print$2}'); export gwlb_ip=$(aws --region $instance_region ec2 describe-network-interfaces --filters Name=vpc-id,Values=$instance_vpcid | jq ' .NetworkInterfaces[] | select(.AvailabilityZone=='$instance_az') | select(.InterfaceType=="gateway_load_balancer") |.PrivateIpAddress' -r); # Start http and configure index.html: systemctl start httpd; touch /var/www/html/index.html; echo "" >> /var/www/html/index.html echo "
" >> /var/www/html/index.html echo "