Skip to content

Instantly share code, notes, and snippets.

@s1r-J
Created December 20, 2025 16:25
Show Gist options
  • Select an option

  • Save s1r-J/717fccf7313bc3ff3c1d800699255f57 to your computer and use it in GitHub Desktop.

Select an option

Save s1r-J/717fccf7313bc3ff3c1d800699255f57 to your computer and use it in GitHub Desktop.
CloudFormation template to create a VPC with 4 subnets (2 AZs, each with public and private subnets), 2 EC2 instances (bastion and NAT), a security group for EC2 instance for App server and a security group for RDS.
AWSTemplateFormatVersion: '2010-09-09'
Description: >
CloudFormation template to create a VPC with 4 subnets (2 AZs, each with public and private subnets), 2 EC2 instances (bastion and NAT), a security
group for EC2 instance for App server and a security group for RDS.
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: General
Parameters:
- ResourcePrefixName
- Label:
default: VPC and Subnets
Parameters:
- VPCCidrBlock
- PublicSubnet1CidrBlock
- PrivateSubnet1CidrBlock
- PublicSubnet2CidrBlock
- PrivateSubnet2CidrBlock
- Label:
default: EC2 Instances
Parameters:
- AmazonLinux2023AmiId
- Ec2KeyPair
- BastionSshPort
- BastionAllowedIPAddress
ParameterLabels:
ResourcePrefixName:
default: Prefix name for resources
VPCCidrBlock:
default: VPC CIDR
PublicSubnet1CidrBlock:
default: Public subnet 1 CIDR
PrivateSubnet1CidrBlock:
default: Private subnet 1 CIDR
PublicSubnet2CidrBlock:
default: Public subnet 2 CIDR
PrivateSubnet2CidrBlock:
default: Private subnet 2 CIDR
AmazonLinux2023AmiId:
default: Amazon Linux 2023 AMI
Ec2KeyPair:
default: EC2 key pair name
BastionSshPort:
default: Bastion SSH port
BastionAllowedIPAddress:
default: Bastion allowed IP address
Parameters:
ResourcePrefixName:
Type: String
Description: Prefix name to use for resources
VPCCidrBlock:
Type: String
Default: 10.0.0.0/16
Description: CIDR block for the VPC
PublicSubnet1CidrBlock:
Type: String
Default: 10.0.1.0/24
Description: CIDR block for the first public subnet
PrivateSubnet1CidrBlock:
Type: String
Default: 10.0.2.0/24
Description: CIDR block for the first private subnet
PublicSubnet2CidrBlock:
Type: String
Default: 10.0.3.0/24
Description: CIDR block for the second public subnet
PrivateSubnet2CidrBlock:
Type: String
Default: 10.0.4.0/24
Description: CIDR block for the second private subnet
AmazonLinux2023AmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64'
Description: Latest Amazon Linux 2023 ARM64 AMI published in SSM
BastionSshPort:
Type: Number
Default: 22
MinValue: 1
MaxValue: 65535
Description: SSH port to expose on the Bastion host
BastionAllowedIPAddress:
Type: String
Description: IP address allowed to reach the Bastion host. Leave empty to allow all addresses.
Ec2KeyPair:
Type: AWS::EC2::KeyPair::KeyName
Description: Name of an existing EC2 key pair to attach to the Bastion and NAT instances
Conditions:
BastionAllowAll: !Equals [!Ref BastionAllowedIPAddress, '']
Resources:
NetworkVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCidrBlock
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-VPC'
PublicSubnet1:
Metadata:
Description: Public subnet in the first Availability Zone
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref NetworkVPC
CidrBlock: !Ref PublicSubnet1CidrBlock
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-PublicSubnet-1'
PrivateSubnet1:
Metadata:
Description: Private subnet in the first Availability Zone
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref NetworkVPC
CidrBlock: !Ref PrivateSubnet1CidrBlock
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-PrivateSubnet-1'
PublicSubnet2:
Metadata:
Description: Public subnet in the second Availability Zone
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref NetworkVPC
CidrBlock: !Ref PublicSubnet2CidrBlock
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-PublicSubnet-2'
PrivateSubnet2:
Metadata:
Description: Private subnet in the second Availability Zone
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref NetworkVPC
CidrBlock: !Ref PrivateSubnet2CidrBlock
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-PrivateSubnet-2'
NetworkInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-IGW'
AttachInternetGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref NetworkVPC
InternetGatewayId: !Ref NetworkInternetGateway
PublicRouteTable:
Metadata:
Description: Route table for public subnets with internet access
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref NetworkVPC
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-PublicRT'
PublicRoute:
Metadata:
Description: Default route sending traffic to the internet gateway
Type: AWS::EC2::Route
DependsOn: AttachInternetGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref NetworkInternetGateway
PublicSubnet1RouteTableAssociation:
Metadata:
Description: Associates PublicSubnet1 with the public route table
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Metadata:
Description: Associates PublicSubnet2 with the public route table
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
PrivateRouteTable:
Metadata:
Description: Route table for private subnets without direct internet access
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref NetworkVPC
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-PrivateRT'
PrivateSubnet1RouteTableAssociation:
Metadata:
Description: Associates PrivateSubnet1 with the private route table
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Metadata:
Description: Associates PrivateSubnet2 with the private route table
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
BastionSecurityGroup:
Metadata:
Description: Security group for the Bastion host, exposing the custom SSH port
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Bastion host (SSH access)
VpcId: !Ref NetworkVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref BastionSshPort
ToPort: !Ref BastionSshPort
CidrIp: !If [BastionAllowAll, '0.0.0.0/0', !Ref BastionAllowedIPAddress]
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-BastionSG'
AppServerSecurityGroup:
Metadata:
Description: Security group for the application servers, allowing SSH from the Bastion security group
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for App Server, allows SSH from Bastion SG
VpcId: !Ref NetworkVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref BastionSecurityGroup
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-AppServerSG'
RdsSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for RDS instances, allows access from App Server SG
VpcId: !Ref NetworkVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Description: Allow MySQL/Aurora MySQL
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Description: Allow PostgreSQL/Aurora PostgreSQL
- IpProtocol: tcp
FromPort: 1433
ToPort: 1433
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Description: Allow SQL Server
- IpProtocol: tcp
FromPort: 1521
ToPort: 1521
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Description: Allow Oracle
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-RdsSG'
NATSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for NAT instance
VpcId: !Ref NetworkVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Description: Allow HTTP from App Server SG
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Description: Allow HTTPS from App Server SG
- IpProtocol: icmp
FromPort: -1
ToPort: -1
SourceSecurityGroupId: !Ref AppServerSecurityGroup
Description: Allow ICMP from App Server SG
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref BastionSecurityGroup
Description: Allow SSH from Bastion SG
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: Allow outbound HTTP
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: Allow outbound HTTPS
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: 0.0.0.0/0
Description: Allow outbound ICMP
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-NATSG'
BastionEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
BastionInstance:
Metadata:
Description: Bastion instance using a dedicated ENI, static EIP, and an 8 GiB standard volume
Type: AWS::EC2::Instance
Properties:
InstanceType: t4g.nano
ImageId: !Ref AmazonLinux2023AmiId
KeyName: !Ref Ec2KeyPair
UserData:
Fn::Base64: !Sub |
#!/bin/bash
sudo sed -i "s/^#Port 22/Port ${BastionSshPort}/" /etc/ssh/sshd_config
sudo sed -i "s/^Port 22/Port ${BastionSshPort}/" /etc/ssh/sshd_config
sudo systemctl restart sshd
NetworkInterfaces:
- DeviceIndex: 0
NetworkInterfaceId: !Ref BastionENI
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeType: standard
VolumeSize: 8
DeleteOnTermination: true
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-Bastion'
BastionENI:
Metadata:
Description: Elastic network interface for the Bastion host to bind the EIP
Type: AWS::EC2::NetworkInterface
Properties:
SubnetId: !Ref PublicSubnet1
GroupSet:
- !Ref BastionSecurityGroup
Description: ENI for Bastion host
BastionEIPAssociation:
Metadata:
Description: Associates the Bastion EIP with the dedicated ENI
Type: AWS::EC2::EIPAssociation
Properties:
AllocationId: !GetAtt BastionEIP.AllocationId
NetworkInterfaceId: !Ref BastionENI
NATInstance:
Metadata:
Description: NAT instance providing outbound internet access for private subnets
Type: AWS::EC2::Instance
Properties:
InstanceType: t4g.nano
ImageId: !Ref AmazonLinux2023AmiId
KeyName: !Ref Ec2KeyPair
UserData:
Fn::Base64: !Sub |
#!/bin/bash
sudo yum install iptables-services -y
sudo systemctl enable iptables
sudo systemctl start iptables
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.d/custom-ip-forwarding.conf
sudo sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf
sudo /sbin/iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE
sudo /sbin/iptables -F FORWARD
sudo service iptables save
SourceDestCheck: false
NetworkInterfaces:
- AssociatePublicIpAddress: true
DeviceIndex: 0
SubnetId: !Ref PublicSubnet2
GroupSet:
- !Ref NATSecurityGroup
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeType: standard
VolumeSize: 8
DeleteOnTermination: true
Tags:
- Key: Name
Value: !Sub '${ResourcePrefixName}-NAT'
PrivateRouteViaNAT:
Metadata:
Description: Default route for private subnets that targets the NAT instance
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
InstanceId: !Ref NATInstance
Outputs:
RdsSecurityGroupId:
Description: Security Group ID for RDS allowing access from App Server SG
Value: !Ref RdsSecurityGroup
Export:
Name: !Sub '${ResourcePrefixName}-RdsSecurityGroupId'
AppServerSecurityGroupId:
Description: Security Group ID for App Server allowing SSH from Bastion
Value: !Ref AppServerSecurityGroup
Export:
Name: !Sub '${ResourcePrefixName}-AppServerSecurityGroupId'
BastionEIP:
Description: Elastic IP address associated with the Bastion host
Value: !Ref BastionEIP
Export:
Name: !Sub '${ResourcePrefixName}-BastionEIP'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment