Created
December 20, 2025 16:25
-
-
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.
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: > | |
| 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