Published
- 7 min read
Setting up your VPC
This is not going to be a post about what a VPC is or does as you can read up on that elsewhere. I wanted to write up the setup of a VPC that you can refer back to in some of the other posts.
Note: you could set this up using the Crosswalk package instead but since this a learning experience we’ll do it the slightly harder way.
I’m going to set up my VPC in the eu-west-2 (London) region for convenience as it’s the closest to me. As that region has three Availability Zones, I’m going to set up a public and a private subnet in each. We’ll use a single NAT gateway to give the private subnets through the Internet Gateway (that we’ll also create).
Next we’ll set up the required route tables to wire everything up. Finally we’ll add in some VPC endpoints so that anything in the private subnets can access ECR. This is because we’ll put most of the workloads in the private subnets and they’ll need to download any container images and it’ll limit the amount of network traffic we put through the NAT gateway.
Note 2: I’ll add the entire Pulumi code at the bottom of this post. If you want to just skip there, here’s the link: Show me the code
I’m going to set this up in a new Pulumi project for my convenience: pulumi new aws-typescript
(filling out the settings that you want)
First things first, we’re going to set up the code so the entrypoint is a top-level async function. We’re going to use an asynchronous function in the code and this will make our lives a little bit easier:
import * as aws from "@pulumi/aws";
export = async () => {
// Resources go here
return {} // this will be where any stack outputs go
}
Let’s start with the VPC and the Internet Gateway resources:
const vpc = new aws.ec2.Vpc("vpc", {
cidrBlock: "10.0.0.0/16",
tags: {
Name: "my-vpc",
},
});
const inetgw = new aws.ec2.InternetGateway("inetgw", {
vpcId: vpc.id
});
That IP range will give you 65534 IP addresses which should be more than enough here.
Next we’ll do the subnets. Don’t forget that we want this setup to allow us to go across enough availability zones so that we can keep things up and running if one drops out of service. In this case we’ll have three public subnets (where resources can be accessed by and can access the internet via the Internet Gateway) and three private subnets (where there is no direct access to resources in the subnet from the internet and outgoing access is via the NAT gateway). We’ll tag them to make it easier to see which subnets are public and which are private.
First we’re going to grab the availability zones that have a state of available (no point in getting any others) - this is the async function that I mentioned at the beginning.
We’re then going to get the number of zones available. For this project we don’t need more than three, but if there aren’t three in the region you’ve selected then it’ll grab all of them.
const availablityZones = await aws.getAvailabilityZones({
state: "available",
});
const numberOfAvailabilityZones =
availablityZones.names.length > 3 ? 3 : availablityZones.names.length;
Rather than calculate the subnet IP ranges, I used the VPC Designer web app to do the work for me.
Rather than define each subnet (be it private or public) separately we’ll use the native map() function to loop over the availability zones and create two subnets in each. We’re also adding them to a couple of arrays so we can use some of the outputs later on.
let subnetIndex: number = 1;
let ipIndex: number = 0;
let publicSubnets: aws.ec2.Subnet[] = [];
let privateSubnets: aws.ec2.Subnet[] = [];
availablityZones.names.map((azName) => {
const publicCidrBlock = `10.0.${ipIndex * 32}.0/19`;
const publicSubnet = new aws.ec2.Subnet(`public-subnet-${subnetIndex}`, {
availabilityZone: azName,
cidrBlock: publicCidrBlock,
mapPublicIpOnLaunch: true,
vpcId: vpc.id,
tags: {
SubnetType: "Public",
},
});
publicSubnets.push(publicSubnet);
ipIndex++;
const privateCiderBlock = `10.0.${ipIndex * 32}.0/19`;
const privateSubnet = new aws.ec2.Subnet(`private-subnet-${subnetIndex}`, {
availabilityZone: azName,
cidrBlock: privateCiderBlock,
tags: {
SubnetType: "Private",
},
vpcId: vpc.id,
});
privateSubnets.push(privateSubnet);
subnetIndex++;
ipIndex++;
});
Then it’s the route table (only route is out the internet gateway and here’s where we can use the publicSubnets array to set up the route associations:
const publicRouteTable = new aws.ec2.RouteTable("publicRouteTable", {
routes: [{
cidrBlock: "0.0.0.0/0",
gatewayId: inetgw.id,
}],
vpcId: vpc.id,
});
let publicSubnetIndex: number = 1;
publicSubnets.map(subnet => {
new aws.ec2.RouteTableAssociation(`public-rt-assoc-${publicSubnetIndex}`, {
subnetId: subnet.id,
routeTableId: publicRouteTable.id,
});
publicSubnetIndex++;
});
Creating the Elastic IP for the NAT Gateway and the NAT Gateway itself comes next (I’ll update this with details on creating and using a NAT instance instead if you don’t want to pay the NAT Gateway tax):
const eip = new aws.ec2.Eip("eip", {
domain: "vpc",
tags: {
Name: "eks-nat-eip",
},
});
const natgw = new aws.ec2.NatGateway("natgw", {
allocationId: eip.allocationId,
connectivityType: "public",
subnetId: publicSubnets[0].id,
});
Now that we have the NAT Gateway set up, we can create the route table to allow the private subnets to talk to the internet using it. Same as with the public subnets, we’ll use the array to set up the route table associations:
const privateRouteTable = new aws.ec2.RouteTable("rt-private-1", {
routes: [{
cidrBlock: "0.0.0.0/0",
natGatewayId: natgw.id,
}],
tags: {
Name: "vpc-private-1",
SubnetType: "Private",
},
vpcId: vpc.id,
});
let privateSubnetIndex: number = 1;
privateSubnets.map((subnet) => {
new aws.ec2.RouteTableAssociation(`rta-private-${privateSubnetIndex}`, {
routeTableId: privateRouteTable.id,
subnetId: subnet.id,
});
privateSubnetIndex++;
});
Finally, we’re going to return certain outputs from this async function, which Pulumi will then see as stack outputs:
return {
vpcId: vpc.id,
privateSubnetIds: privateSubnets.map((subnet) => subnet.id),
publicSubnetIds: publicSubnets.map((subnet) => subnet.id),
};
These are the usual suspects when it comes to setting up other Pulumi projects that will require to either be situated within the VPC, or need to know about the subnets.
Other than the loops to make it a little easier to define the subnets and the route table associations, I don’t think there’s anything particularly complicated about this. It works as a good foundation to set up and of course your requirements may be slightly different.
There are also no VPC Endpoints for accessing any services from the subnets, so all the data is going to flow out of the private subnets via the NAT Gateway which could be pricey. However, since different use cases require access to different services (and none of the resources I’m creating here require this kind of access) I didn’t see the point. In future posts I’ll add these in there.
As promised here’s the entire code file:import * as aws from "@pulumi/aws";
export = async () => {
const vpc = new aws.ec2.Vpc("vpc", {
cidrBlock: "10.0.0.0/16",
tags: {
Name: "my-vpc",
},
});
const inetgw = new aws.ec2.InternetGateway("inetgw", {
vpcId: vpc.id,
});
const availablityZones = await aws.getAvailabilityZones({
state: "available",
});
const numberOfAvailabilityZones =
availablityZones.names.length > 3 ? 3 : availablityZones.names.length;
let subnetIndex: number = 1;
let ipIndex: number = 0;
let publicSubnets: aws.ec2.Subnet[] = [];
let privateSubnets: aws.ec2.Subnet[] = [];
availablityZones.names.map((azName) => {
const publicCidrBlock = `10.0.${ipIndex * 32}.0/19`;
const publicSubnet = new aws.ec2.Subnet(`public-subnet-${subnetIndex}`, {
availabilityZone: azName,
cidrBlock: publicCidrBlock,
mapPublicIpOnLaunch: true,
vpcId: vpc.id,
tags: {
SubnetType: "Public",
},
});
publicSubnets.push(publicSubnet);
ipIndex++;
const privateCiderBlock = `10.0.${ipIndex * 32}.0/19`;
const privateSubnet = new aws.ec2.Subnet(`private-subnet-${subnetIndex}`, {
availabilityZone: azName,
cidrBlock: privateCiderBlock,
tags: {
SubnetType: "Private",
},
vpcId: vpc.id,
});
privateSubnets.push(privateSubnet);
subnetIndex++;
ipIndex++;
});
const publicRouteTable = new aws.ec2.RouteTable("publicRouteTable", {
routes: [
{
cidrBlock: "0.0.0.0/0",
gatewayId: inetgw.id,
},
],
vpcId: vpc.id,
});
let publicSubnetIndex: number = 1;
publicSubnets.map((subnet) => {
new aws.ec2.RouteTableAssociation(`public-rt-ass-${publicSubnetIndex}`, {
subnetId: subnet.id,
routeTableId: publicRouteTable.id,
});
publicSubnetIndex++;
});
const eip = new aws.ec2.Eip("eip", {
domain: "vpc",
});
const natgw = new aws.ec2.NatGateway("natgw", {
allocationId: eip.allocationId,
connectivityType: "public",
subnetId: publicSubnets[0].id,
});
const privateRouteTable = new aws.ec2.RouteTable("rt-private-1", {
routes: [
{
cidrBlock: "0.0.0.0/0",
natGatewayId: natgw.id,
},
],
vpcId: vpc.id,
});
let privateSubnetIndex: number = 1;
privateSubnets.map((subnet) => {
new aws.ec2.RouteTableAssociation(`rta-private-${privateSubnetIndex}`, {
routeTableId: privateRouteTable.id,
subnetId: subnet.id,
});
privateSubnetIndex++;
});
return {
vpcId: vpc.id,
privateSubnetIds: privateSubnets.map((subnet) => subnet.id),
publicSubnetIds: publicSubnets.map((subnet) => subnet.id),
};
};