How to Create VPC using CloudFormation
Dear Reader, I hope you are doing well. In this post, I am sharing with you how to create VPC using CloudFormation on AWS.
When it comes to creating Amazon Virtual Private Cloud or VPC, you can use AWS Management Console, AWS CLI, AWS SDK etc. Although using the console’s VPC wizard looks like the easiest option but imagine if you have to do the same in multiple accounts and with the same configuration, I am sure it won’t be fun.
And, that’s when automation comes into the picture to make your life easier. You can make use of automation tools like CloudFormation or Terraform to create your VPC. This way you can reuse your template to create the same configuration in multiple accounts/environment
In this post, we’ll learn how to create VPC using CloudFormation. We will be creating this VPC with public and private subnets both. And we’ll also ensure connectivity is working perfectly after the setup.
In this tutorial, we’ll create:-
- 1 VPC
- 2 Public Subnet and 2 Private Subnet
- 1 Internet Gateway and attach it to VPC
- 1 Public Route table for all the public connectivity – all public traffic goes through the internet gateway
- Associate both public subnets to public route table
- 2 elastic IPs will be used by 2 NAT gateway
- Create two private route tables one for each private subnet
- 2 NAT Gateway to provide internet connectivity to our private subnet
- Route private traffic of each subnet in the route table via the respective NAT gateway
- Associate private subnet to the private route table
- And we will have a full-function VPC with internet connection in private as well as public subnet
This is what it looks like –
Prerequisites:
- An AWS Account
- Basic knowledge of JSON/YAML
- Basic Knowledge of VPC
- Basic Knowledge of CloudFormation
Basic Overview of Amazon VPC or Virtual Private Cloud
Amazon VPC or virtual private network is a locally isolated section of AWS where you can deploy your AWS resources like EC2. Ideally, it’s a virtual network on the cloud that you define and is isolated from other networks.
Every AWS account comes with a default VPC in each region which is used to launch resources when you don’t specify VPC explicitly. However, when you need a custom VPC, you can add various components to create a full function VPC.
Usually, it comprises of public subnets where you can keep your publicly accessible resource such as web servers and private subnets where you can keep private resources like your database.
Our example VPC:
Steps to Create VPC with Public and Private Subnet using CloudFormation
Let’s see the step-by-step instruction to create highly available and fault-tolerant Amazon VPC using CloudFormation.
- Step 1: Provide proper permission
- Step 2: Prepare a template
- Step3: Create a Stack using the prepared template
Step 1: Provide proper permission
If you are not an admin user, you should explicitly provide vpc:* permission for your user/role. Additionally, you will also need cloudformation:* as well to be able to do CloudFormation stack creation, updation etc.
Note: For simplicity, I am using ‘*’ in the permission above. However, it is recommended to use the least privilege for security reasons.
Step 2: Prepare a template
You can use YAML or JSON for your template. I prefer YAML for writing my templates. But don’t worry, If you want it in JSON, I will provide a JSON template as well.
You can also learn to do it yourself using my previous tutorial Convert a CloudFormation Template from YAML to JSON and Vice Versa
To create a simple VPC, all you need is a AWS::EC2::VPC resource like below.
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: String
EnableDnsHostnames: Boolean
EnableDnsSupport: Boolean
InstanceTenancy: String
Ipv4IpamPoolId: String
Ipv4NetmaskLength: Integer
Tags:
- Tag
Few things you should note there –
- CidrBlock is the range of IP addresses that you choose for your private network.
- EnableDnsHostnames determines if public DNS is assigned to the instance with public IP
- EnableDnsSupport must be true if EnableDnsHostnames is true
- InstanceTenancy by default is shared
Ideally, the template to create a simple VPC looks like below.
Resources:
DemoVPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: !Ref VPCCidr
Tags:
-
Key: Name
Value: !Ref VPCName
Subnets
- A subnet is a range of IP addresses in your VPC
- You launch your resources in a subnet
- A subnet can not span multiple availability zones
- You can create the public, private or VPN-only subnet
- You use a public subnet for resources that needs internet connectivity and a private subnet for resources that are private
- Each subnet must be associated with a route table which determines how outgoing traffic will be routed
Here is how to create a subnet using CloudFormation:
PrivateSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneB
CidrBlock: !Ref PrivateSubnetBCidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PrivateSubnetB'
- It is taking vpcId, the availability zone in which it will be, and CidrBlock which is the IP range for it. The tag is just for naming purposes here.
The rest of the resources I am not describing here but a complete template is here below for your reference. Feel free to reach out to me if you face any problems.
Template to Create VPC using CloudFormation: YAML
AWSTemplateFormatVersion: 2010-09-09
Description: Custom VPC Creation With Private and Public Subnet
Parameters:
VPCName:
Description: CIDR range for our VPC
Type: String
Default: DemoCustomVPC
VPCCidr:
Description: CIDR range for our VPC
Type: String
Default: 10.0.0.0/16
PrivateSubnetACidr:
Description: Private Subnet IP Range
Type: String
Default: 10.0.0.0/24
PrivateSubnetBCidr:
Description: Private Subnet IP Range
Type: String
Default: 10.0.1.0/24
PublicSubnetACidr:
Description: Public Subnet IP Range
Type: String
Default: 10.0.2.0/24
PublicSubnetBCidr:
Description: Public Subnet IP Range
Type: String
Default: 10.0.3.0/24
AvailabilityZoneA:
Description: Avaibalbility Zone 1
Type: String
Default: ap-south-1a
AvailabilityZoneB:
Description: Avaibalbility Zone 2
Type: String
Default: ap-south-1b
Resources:
DemoVPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: !Ref VPCCidr
Tags:
-
Key: Name
Value: !Ref VPCName
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneA
CidrBlock: !Ref PrivateSubnetACidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PrivateSubnetA'
PrivateSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneB
CidrBlock: !Ref PrivateSubnetBCidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PrivateSubnetB'
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneA
CidrBlock: !Ref PublicSubnetACidr
MapPublicIpOnLaunch: true
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PublicSubnetA'
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneB
CidrBlock: !Ref PublicSubnetBCidr
MapPublicIpOnLaunch: true
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PublicSubnetB'
InternetGateway:
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
VpcId: !Ref DemoVPC
InternetGatewayId: !Ref InternetGateway
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
PublicSubnetRouteTableAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRouteTable
ElasticIPA:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
ElasticIPB:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NATGatewayA:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIPA.AllocationId
SubnetId: !Ref PublicSubnetA
NATGatewayB:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIPB.AllocationId
SubnetId: !Ref PublicSubnetB
PrivateRouteTableA:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PrivateRouteTableB:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PrivateRouteToInternetA:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableA
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGatewayA
PrivateRouteToInternetB:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableB
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGatewayB
PrivateSubnetRouteTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetA
RouteTableId: !Ref PrivateRouteTableA
PrivateSubnetRouteTableAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetB
RouteTableId: !Ref PrivateRouteTableB
#By this time, we have internet- lets launch an instance
DemoInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-08df646e18b182346
InstanceType: t2.micro
AvailabilityZone: !Ref AvailabilityZoneA
SubnetId: !Ref PublicSubnetA
KeyName: MyDemoEC2eyPair
SecurityGroupIds:
- !Ref DemoSecurityGroup
UserData:
Fn::Base64:
!Sub |
#!/bin/bash
yum update -y
yum install -y httpd.x86_64
systemctl start httpd.service
systemctl enable httpd.service
echo ?Hello World from $(hostname -f)? > /var/www/html/index.html
DemoSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref DemoVPC
GroupDescription: SG to allow SSH and HTTP
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: '22'
ToPort: '22'
CidrIp: '0.0.0.0/0'
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: EC2-SG
Outputs:
VPCId:
Description: vpc id
Value: !Ref DemoVPC
PublicSubnetA:
Description: SubnetId of public subnet A
Value: !Ref PublicSubnetA
PublicSubnetB:
Description: SubnetId of public subnet B
Value: !Ref PublicSubnetB
PrivateSubnetA:
Description: SubnetId of private subnet A
Value: !Ref PrivateSubnetA
PrivateSubnetB:
Description: SubnetId of private subnet B
Value: !Ref PublicSubnetB
DemoInstanceId:
Description: Demo Instance Id
Value: !Ref DemoInstance
Template to Create VPC using CloudFormation: JSON
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Custom VPC Creation With Private and Public Subnet",
"Parameters": {
"VPCName": {
"Description": "CIDR range for our VPC",
"Type": "String",
"Default": "DemoCustomVPC"
},
"VPCCidr": {
"Description": "CIDR range for our VPC",
"Type": "String",
"Default": "10.0.0.0/16"
},
"PrivateSubnetACidr": {
"Description": "Private Subnet IP Range",
"Type": "String",
"Default": "10.0.0.0/24"
},
"PrivateSubnetBCidr": {
"Description": "Private Subnet IP Range",
"Type": "String",
"Default": "10.0.1.0/24"
},
"PublicSubnetACidr": {
"Description": "Public Subnet IP Range",
"Type": "String",
"Default": "10.0.2.0/24"
},
"PublicSubnetBCidr": {
"Description": "Public Subnet IP Range",
"Type": "String",
"Default": "10.0.3.0/24"
},
"AvailabilityZoneA": {
"Description": "Avaibalbility Zone 1",
"Type": "String",
"Default": "ap-south-1a"
},
"AvailabilityZoneB": {
"Description": "Avaibalbility Zone 2",
"Type": "String",
"Default": "ap-south-1b"
}
},
"Resources": {
"DemoVPC": {
"Type": "AWS::EC2::VPC",
"Properties": {
"EnableDnsSupport": true,
"EnableDnsHostnames": true,
"CidrBlock": {
"Ref": "VPCCidr"
},
"Tags": [
{
"Key": "Name",
"Value": {
"Ref": "VPCName"
}
}
]
}
},
"PrivateSubnetA": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
},
"AvailabilityZone": {
"Ref": "AvailabilityZoneA"
},
"CidrBlock": {
"Ref": "PrivateSubnetACidr"
},
"Tags": [
{
"Key": "Name",
"Value": {
"Fn::Sub": "${VPCName}-PrivateSubnetA"
}
}
]
}
},
"PrivateSubnetB": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
},
"AvailabilityZone": {
"Ref": "AvailabilityZoneB"
},
"CidrBlock": {
"Ref": "PrivateSubnetBCidr"
},
"Tags": [
{
"Key": "Name",
"Value": {
"Fn::Sub": "${VPCName}-PrivateSubnetB"
}
}
]
}
},
"PublicSubnetA": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
},
"AvailabilityZone": {
"Ref": "AvailabilityZoneA"
},
"CidrBlock": {
"Ref": "PublicSubnetACidr"
},
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Key": "Name",
"Value": {
"Fn::Sub": "${VPCName}-PublicSubnetA"
}
}
]
}
},
"PublicSubnetB": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
},
"AvailabilityZone": {
"Ref": "AvailabilityZoneB"
},
"CidrBlock": {
"Ref": "PublicSubnetBCidr"
},
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Key": "Name",
"Value": {
"Fn::Sub": "${VPCName}-PublicSubnetB"
}
}
]
}
},
"InternetGateway": {
"Type": "AWS::EC2::InternetGateway"
},
"VPCGatewayAttachment": {
"Type": "AWS::EC2::VPCGatewayAttachment",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
},
"InternetGatewayId": {
"Ref": "InternetGateway"
}
}
},
"PublicRouteTable": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
}
}
},
"PublicRoute": {
"Type": "AWS::EC2::Route",
"DependsOn": "VPCGatewayAttachment",
"Properties": {
"RouteTableId": {
"Ref": "PublicRouteTable"
},
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": {
"Ref": "InternetGateway"
}
}
},
"PublicSubnetRouteTableAssociationA": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"SubnetId": {
"Ref": "PublicSubnetA"
},
"RouteTableId": {
"Ref": "PublicRouteTable"
}
}
},
"PublicSubnetRouteTableAssociationB": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"SubnetId": {
"Ref": "PublicSubnetB"
},
"RouteTableId": {
"Ref": "PublicRouteTable"
}
}
},
"ElasticIPA": {
"Type": "AWS::EC2::EIP",
"Properties": {
"Domain": "vpc"
}
},
"ElasticIPB": {
"Type": "AWS::EC2::EIP",
"Properties": {
"Domain": "vpc"
}
},
"NATGatewayA": {
"Type": "AWS::EC2::NatGateway",
"Properties": {
"AllocationId": {
"Fn::GetAtt": [
"ElasticIPA",
"AllocationId"
]
},
"SubnetId": {
"Ref": "PublicSubnetA"
}
}
},
"NATGatewayB": {
"Type": "AWS::EC2::NatGateway",
"Properties": {
"AllocationId": {
"Fn::GetAtt": [
"ElasticIPB",
"AllocationId"
]
},
"SubnetId": {
"Ref": "PublicSubnetB"
}
}
},
"PrivateRouteTableA": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
}
}
},
"PrivateRouteTableB": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
}
}
},
"PrivateRouteToInternetA": {
"Type": "AWS::EC2::Route",
"Properties": {
"RouteTableId": {
"Ref": "PrivateRouteTableA"
},
"DestinationCidrBlock": "0.0.0.0/0",
"NatGatewayId": {
"Ref": "NATGatewayA"
}
}
},
"PrivateRouteToInternetB": {
"Type": "AWS::EC2::Route",
"Properties": {
"RouteTableId": {
"Ref": "PrivateRouteTableB"
},
"DestinationCidrBlock": "0.0.0.0/0",
"NatGatewayId": {
"Ref": "NATGatewayB"
}
}
},
"PrivateSubnetRouteTableAssociationA": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"SubnetId": {
"Ref": "PrivateSubnetA"
},
"RouteTableId": {
"Ref": "PrivateRouteTableA"
}
}
},
"PrivateSubnetRouteTableAssociationB": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"SubnetId": {
"Ref": "PrivateSubnetB"
},
"RouteTableId": {
"Ref": "PrivateRouteTableB"
}
}
},
"DemoInstance": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": "ami-08df646e18b182346",
"InstanceType": "t2.micro",
"AvailabilityZone": {
"Ref": "AvailabilityZoneA"
},
"SubnetId": {
"Ref": "PublicSubnetA"
},
"KeyName": "MyDemoEC2eyPair",
"SecurityGroupIds": [
{
"Ref": "DemoSecurityGroup"
}
],
"UserData": {
"Fn::Base64": {
"Fn::Sub": "#!/bin/bash\nyum update -y\nyum install -y httpd.x86_64\nsystemctl start httpd.service\nsystemctl enable httpd.service\necho ?Hello World from $(hostname -f)? > /var/www/html/index.html\n"
}
}
}
},
"DemoSecurityGroup": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"VpcId": {
"Ref": "DemoVPC"
},
"GroupDescription": "SG to allow SSH and HTTP",
"SecurityGroupIngress": [
{
"IpProtocol": "tcp",
"FromPort": "22",
"ToPort": "22",
"CidrIp": "0.0.0.0/0"
},
{
"IpProtocol": "tcp",
"FromPort": 80,
"ToPort": 80,
"CidrIp": "0.0.0.0/0"
}
],
"Tags": [
{
"Key": "Name",
"Value": "EC2-SG"
}
]
}
}
},
"Outputs": {
"VPCId": {
"Description": "vpc id",
"Value": {
"Ref": "DemoVPC"
}
},
"PublicSubnetA": {
"Description": "SubnetId of public subnet A",
"Value": {
"Ref": "PublicSubnetA"
}
},
"PublicSubnetB": {
"Description": "SubnetId of public subnet B",
"Value": {
"Ref": "PublicSubnetB"
}
},
"PrivateSubnetA": {
"Description": "SubnetId of private subnet A",
"Value": {
"Ref": "PrivateSubnetA"
}
},
"PrivateSubnetB": {
"Description": "SubnetId of private subnet B",
"Value": {
"Ref": "PublicSubnetB"
}
},
"DemoInstanceId": {
"Description": "Demo Instance Id",
"Value": {
"Ref": "DemoInstance"
}
}
}
}
Step3: Create a Stack using the prepared template
Now, we know the basics and we have the template so let’s go and create the stack.
- Grab the YAML or JSON template from above at your convenience.
- Save the template with .yml or .json as per the choice of template and follow the below steps.
- Login to AWS Management Console, navigate to CloudFormation and click on Create stack
- Click on “Upload a template file”, upload your saved .yml or .json file and click Next
- Enter the stack name and click on Next. In the configuration, keep everything as default and click on Next.
- In the events tab of stack, you can view the status.
- Once stack is successfully created, you can go to VPC service and verify your VPC.
- Also, you can check resource tab of your CloudFormation stack to view all the resources that are created. You can navigate to those resources from here itself.
Clean Up
If you are creating this VPC for learning purpose. Don’t forget to delete your CloudFormation stack so that your VPC and related components are deleted and you don’t bear any cost.
Select your stack and click on the Delete button as shown below to delete your stack.
Note: Please note that, this template contains NAT gateways and EC2 and if left undestroyed, you are gonna bear cost. So please go ahead and delete the stack if you don’t need it.
Happy Learning !!!
Conclusion:
In this post, we learnt how to create VPC using CloudFormation
- We created a fully functional and highly available VPC with public and private subnets
- Used internet gateway and NAT gateway for internet connectivity in public and private subnets respectively
- Created an EC2 instance using created VPC and accessed it over the internet
I hope you found this post helpful.
Enjoyed the content?
Subscribe to our newsletter below to get awesome AWS learning materials delivered straight to your inbox.
Don’t forget to motivate me by-
- Adding a comment below on what you liked and what can be improved.
- Follow us on
- Subscribe to our newsletter to get notified each time we post new content
- Share this post with your friends
One thought on “How to Create VPC using CloudFormation [Public & Private Subnet]”
I am beginner in cloud environment, and this article, already solved many queries.