# Following example shows how to create Spoke VPC and integrate with # AWS Gateway Load Balancer (GWLB) 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/ AWSTemplateFormatVersion: "2010-09-09" Description: >- AWS CloudFormation sample template for Spoke VPC for Gateway Load Balancer (GWLB) in centralize architecture. Template is deployed across 2 Availability Zones (AZ) and is created in same account as Appliance VPC and Transit Gateay. This template creates: - 1 VPC - 1 IGW - 4 private subnets, one in each AZ for application instances and TGW attachments - 2 public subnet, one in each AZ - 1 private route table and 1 public route table - 2 Security group: Application and Bastion - 2 Amazon Linux 2 instance acting as applications, one in each AZ - 1 Amazon Linux 2 instance acting as bastion host to access Application instances. **WARNING** This template creates one or more Amazon EC2 instances. 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 - ApplicationSubnet1Cidr - TgwAttachSubnet1Cidr - BastionSubnet1Cidr - AvailabilityZone2 - ApplicationSubnet2Cidr - TgwAttachSubnet2Cidr - BastionSubnet2Cidr - Label: default: Application Configuration Parameters: - ApplicationInstanceType - ApplicationInstanceAmiId - ApplicationInstanceDiskSize - KeyPairName - AccessLocation ParameterLabels: VpcCidr: default: Spoke VPC - VPC CIDR AvailabilityZone1: default: Spoke VPC - Availability Zone 1 ApplicationSubnet1Cidr: default: Spoke VPC - Application Subnet 1 CIDR TgwAttachSubnet1Cidr: default: Spoke VPC - TGW Attachment Subnet 1 CIDR BastionSubnet1Cidr: default: Spoke VPC - Bastion Subnet 1 CIDR AvailabilityZone2: default: Spoke VPC - Availability Zone 2 ApplicationSubnet2Cidr: default: Spoke VPC - Application Subnet 2 CIDR TgwAttachSubnet2Cidr: default: Spoke VPC - TGW Attachment Subnet 2 CIDR BastionSubnet2Cidr: default: Spoke VPC - Bastion Subnet 2 CIDR ApplicationInstanceType: default: Application Instance Type ApplicationInstanceAmiId: default: Latest AMI ID for application (ec2 instance) ApplicationInstanceDiskSize: default: Application Instance Size in GB KeyPairName: default: KeyPair required for accessing application instance AccessLocation: default: Network CIDR to access application instance 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: 10.0.0.0/24 Description: Spoke VPC - CIDR block for the VPC Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 AvailabilityZone1: Description: Spoke VPC - Availability Zone 1 Type: AWS::EC2::AvailabilityZone::Name ConstraintDescription: Valid Availability Zone Id ApplicationSubnet1Cidr: 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: 10.0.0.0/28 Description: Spoke VPC - Application Subnet 1 CIDR in Availability Zone 1 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 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: 10.0.0.16/28 Description: Spoke VPC - TGW Attachment Subnet 1 CIDR in Availability Zone 1 Type: String ConstraintDescription: Subnet CIDR parameter must be in the form x.x.x.x/16-28 BastionSubnet1Cidr: 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: 10.0.0.64/28 Description: Spoke VPC - Bastion Subnet 1 CIDR in Availability Zone 1 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 AvailabilityZone2: Description: Availability Zone to use for the Public Subnet 2 in the VPC Type: AWS::EC2::AvailabilityZone::Name ConstraintDescription: Valid Availability Zone Id ApplicationSubnet2Cidr: 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: 10.0.0.32/28 Description: Spoke VPC - Application Subnet 2 CIDR in Availability Zone 2 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 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: 10.0.0.48/28 Description: Spoke VPC - TGW Attachment Subnet 2 CIDR in Availability Zone 1 Type: String ConstraintDescription: Subnet CIDR parameter must be in the form x.x.x.x/16-28 BastionSubnet2Cidr: 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: 10.0.0.80/28 Description: Spoke VPC - Bastion Subnet 2 CIDR in Availability Zone 2 Type: String ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 ApplicationInstanceType: Description: Select EC2 instance type for Application instance. Default is set to t2.micro Default: t2.micro AllowedValues: - t2.micro Type: String ApplicationInstanceAmiId: Description: EC2 Instance AMI ID retrieved using SSM Type: String ApplicationInstanceDiskSize: Description: Application 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 access Bastion Host. Default is set to access from anywhere (0.0.0.0/0) 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 Resources: # Create 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" # Create IGW and attach to the VPC: InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub "${AWS::StackName}-igw" AttachInternetGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref Vpc InternetGatewayId: !Ref InternetGateway # Create Subnets: # AZ1: BastionSubnet1: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone1 CidrBlock: !Ref BastionSubnet1Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-bastion-subnet-1" ApplicationSubnet1: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone1 CidrBlock: !Ref ApplicationSubnet1Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-application-subnet-1" 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" # AZ2: BastionSubnet2: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone2 CidrBlock: !Ref BastionSubnet2Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-bastion-subnet-2" ApplicationSubnet2: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Ref AvailabilityZone2 CidrBlock: !Ref ApplicationSubnet2Cidr VpcId: !Ref Vpc MapPublicIpOnLaunch: "true" Tags: - Key: Name Value: !Sub "${AWS::StackName}-application-subnet-2" 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" # Create Route Tables: ApplicationRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-applicaiton-rtb" BastionRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${AWS::StackName}-bastion-rtb" # Associate Subnets with Route Tables: # AZ1: ApplicationSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref ApplicationSubnet1 RouteTableId: !Ref ApplicationRouteTable TgwAttachSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref TgwAttachSubnet1 RouteTableId: !Ref ApplicationRouteTable BastionSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref BastionSubnet1 RouteTableId: !Ref BastionRouteTable # AZ2: ApplicationSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref ApplicationSubnet2 RouteTableId: !Ref ApplicationRouteTable TgwAttachSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref TgwAttachSubnet2 RouteTableId: !Ref ApplicationRouteTable BastionSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref BastionSubnet2 RouteTableId: !Ref BastionRouteTable # Create Routes. Routes with TGW as the target are created through TGW template: BastionRoute: Type: AWS::EC2::Route DependsOn: AttachInternetGateway Properties: DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway RouteTableId: !Ref BastionRouteTable # Create Security Group: ApplicationSg: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref Vpc GroupName: !Sub "${AWS::StackName}-application-sg" GroupDescription: >- Access to application instance: allow TCP, UDP and ICMP from appropriate location. Allow all traffic from VPC CIDR. SecurityGroupIngress: - CidrIp: !Ref AccessLocation IpProtocol: tcp FromPort: 0 ToPort: 65535 - CidrIp: !Ref AccessLocation IpProtocol: ICMP FromPort: -1 ToPort: -1 - CidrIp: !Ref AccessLocation IpProtocol: udp FromPort: 0 ToPort: 65535 - 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}-application-sg" # Create Application Instances: Application1: Type: AWS::EC2::Instance Properties: ImageId: !Ref ApplicationInstanceAmiId KeyName: !Ref KeyPairName InstanceType: !Ref ApplicationInstanceType SecurityGroupIds: - !Ref ApplicationSg SubnetId: !Ref ApplicationSubnet1 BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeSize: !Ref ApplicationInstanceDiskSize Tags: - Key: Name Value: !Sub "${AWS::StackName}-application-instance-1" UserData: Fn::Base64: !Sub | #!/bin/bash -ex # Configure hostname: hostnamectl set-hostname ${AWS::StackName}-application1; # 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; Application2: Type: AWS::EC2::Instance Properties: ImageId: !Ref ApplicationInstanceAmiId KeyName: !Ref KeyPairName InstanceType: !Ref ApplicationInstanceType SecurityGroupIds: - !Ref ApplicationSg SubnetId: !Ref ApplicationSubnet2 BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeSize: !Ref ApplicationInstanceDiskSize Tags: - Key: Name Value: !Sub "${AWS::StackName}-application-instance-2" UserData: Fn::Base64: !Sub | #!/bin/bash -ex # Configure hostname: hostnamectl set-hostname ${AWS::StackName}-application2; # 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; # Create security group for bastion host: 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" # Create Bastion Host (creates only one bastion host in one AZ): BastionHost: Type: AWS::EC2::Instance Properties: ImageId: !Ref ApplicationInstanceAmiId KeyName: !Ref KeyPairName InstanceType: !Ref ApplicationInstanceType SecurityGroupIds: - !Ref BastionSg SubnetId: !Ref BastionSubnet1 BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeSize: !Ref ApplicationInstanceDiskSize Tags: - Key: Name Value: !Sub "${AWS::StackName}-bastion-host-1" UserData: Fn::Base64: !Sub | #!/bin/bash -ex # Install packages: yum update -y; yum install htop -y; # Configure hostname: hostnamectl set-hostname ${AWS::StackName}-bastion-host1; # 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; # Edit applicaiton security group to allow access from bastion host: ApplicationSgIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref ApplicationSg IpProtocol: tcp FromPort: 22 ToPort: 22 SourceSecurityGroupId: !GetAtt BastionSg.GroupId Outputs: SpokeVpcCidr: Description: Spoke VPC CIDR Value: !Ref VpcCidr SpokeVpcId: Description: Spoke VPC ID Value: !Ref Vpc SpokeApplication1PrivateIp: Description: Spoke VPC Application Instance Private IP Value: !GetAtt Application1.PrivateIp SpokeApplication2PrivateIp: Description: Spoke VPC Application Instance Private IP Value: !GetAtt Application2.PrivateIp SpokeApplication1PublicIp: Description: Spoke VPC Application Instance Public IP Value: !GetAtt Application1.PublicIp SpokeApplication2PublicIp: Description: Spoke VPC Application Instance Public IP Value: !GetAtt Application2.PublicIp SpokeBastionHostPublicIp: Description: Spoke VPC Bastion Instance Public IP Value: !GetAtt BastionHost.PublicIp SpokeTgwAttachSubnet1Id: Description: Spoke VPC TgwAttachSubnet1 ID Value: !Ref TgwAttachSubnet1 SpokeTgwAttachSubnet2Id: Description: Spoke VPC TgwAttachSubnet2 ID Value: !Ref TgwAttachSubnet2 SpokeApplicationRouteTableId: Description: Application Route Table ID Value: !Ref ApplicationRouteTable