Introduction

When working in the Cloud, the first thing to know is that automation is key, and I’ve found no better tool than Ansible to help me with that. It is flexible, and yet very easy to use and learn. I love it.

I’ve been using AWS for a long time now and, as many other people using EC2 instances, I’ve set up a lot of Virtual Private Clouds (or VPCs).

For those who are not familiar with VPCs, here is a small description from AWS :

Amazon Virtual Private Cloud (Amazon VPC) lets you provision a logically isolated section of the Amazon Web Services (AWS) cloud where you can launch AWS resources in a virtual network that you define. You have complete control over your virtual networking environment, including selection of your own IP address range, creation of subnets, and configuration of route tables and network gateways.

You can easily customize the network configuration for your Amazon Virtual Private Cloud. For example, you can create a public-facing subnet for your webservers that has access to the Internet, and place your backend systems such as databases or application servers in a private-facing subnet with no Internet access. You can leverage multiple layers of security, including security groups and network access control lists, to help control access to Amazon EC2 instances in each subnet.

Using a VPC when working with EC2 instances is vital. It is so important that Amazon Cloud Services actually made it mandatory for any new EC2 instance. You can either use the default one provided by AWS with every account, or create your owns to suit your needs.

In this post we will focus on using Ansible to create a VPC from scratch. I will present 3 different topologies :

  1. VPC with Public subnet only
  2. VPC with Public/Private subnets
  3. VPC with Public/Private subnets and Multi Availability Zones

(Hint : the last topology is the best.)

All of the Ansible playbooks I will use are available on my Github page. If you are not familiar with the way Ansible works, you can have a look at its documentation.

Prerequisites

  • Ansible : sudo pip install ansible
  • Boto : sudo pip install boto
  • AWS CLI : sudo pip install awscli

Let’s get started

First, let’s have a look at what the structure of the playbook will look like :

├── playbook.yml
├── inventory
├── vars.yml
├── roles/
│   ├── vpc/
│   │   ├── tasks/
│   │   │   ├── main.yml

This folder structure will look very familiar to Ansible users. it will be the same for all of the VPC topologies I present here. Only the content of roles/vpc/tasks/main.yml will change depending on the VPC definition.

The main file of the playbook, playbook.yml, is very easy to set up : we just define a list of roles we want to apply to a group of hosts. Here we will only have one role : vpc.

---

- hosts: local
  roles:
    - vpc

The group of hosts which will run the playbook is defined in the inventory file. In this case, we run the playbook locally, therefore the file looks like this :

[local]
localhost ansible_connection=local

Finally, all the variables we use in the playbook are listed in the vars.yml file :


---

############################
# Used in parts 1, 2 and 3 #
############################

# AWS Credentials
aws_access_key: "THISISMYAWSACCESSKEY"
aws_secret_key: "ThisIsMyAwSSecretKey"
aws_region:     "eu-west-1"

# VPC Information
vpc_name:       "My VPC"
vpc_cidr_block: "10.0.0.0/16"

# For Security Group Rule
my_ip:          "X.X.X.X"

# Subnets
public_subnet_1_cidr:  "10.0.0.0/24"

############################
# Used in parts 2 and 3    #
############################

# Subnets
private_subnet_1_cidr: "10.0.1.0/24"

############################
# Used in part 3 only      #
############################

# Subnets
public_subnet_2_cidr:  "10.0.10.0/24"
private_subnet_2_cidr: "10.0.11.0/24"

You will need to replace the AWS credential variables. Feel free to change the AWS Region.

Note : The my_ip variable is used to configure the Security Group (or SG) that the playbook will add to the VPC. That SG will allow incoming SSH access from this IP only to any EC2 instance attached with this group. Security Groups are very flexible and can therefore be modified later.

Now, we will focus on the heart of the playbook : the tasks. As I mentioned before, in this example all the tasks are located in roles/vpc/tasks/main.yml. Depending on the VPC topology, the tasks will be different.

Let’s start with the simpler one.

Part 1 : VPC with Public subnet

This VPC will only contain one subnet, that we will call Public Subnet. All the instances located in that subnet will have both a private IP address and a public IP address. The public address will represent them when reaching the outside of the VPC. A Subnet is located in an Availability Zone (or AZ). Since we only have one subnet, all of the future EC2 resources in this VPC will be launched in that AZ.

Here is how this topology looks like :

SWAG Yeah

For this set of tasks we will use different Ansible modules created specifically for AWS. Go have a look at the documentation, there is a lot of them.

For creating this VPC topology, the roles/vpc/tasks/main.yml file should look like this :


---

# roles/vpc/tasks/main.yml


# First task : creating the VPC.
# We are using the variables set in the vars.yml file.
# The module gives us back its result,
# which contains information about our new VPC. 
# We register it in the variable my_vpc.

- name:               Create VPC
  ec2_vpc_net:
    name:             "{{ vpc_name }}"
    cidr_block:       "{{ vpc_cidr_block }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    state:            "present"
  register: my_vpc


# We now use the set_fact module 
# to save the id of the VPC in a new variable.

- name:               Set VPC ID in variable
  set_fact:
    vpc_id:           "{{ my_vpc.vpc.id }}"


# Creating our only Subnet in the VPC.
# A subnet needs to be located in an Availability Zone (or AZ).
# Again, we register the results in a variable for later.

- name:               Create Public Subnet
  ec2_vpc_subnet:
    state:            "present"
    vpc_id:           "{{ vpc_id }}"
    cidr:             "{{ public_subnet_1_cidr }}"
    az:               "{{ aws_region }}a"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    resource_tags:
      Name:           "Public Subnet"
  register: my_public_subnet


# We save the id of the Public Subnet in a new variable.

- name:               Set Public Subnet ID in variable
  set_fact:
    public_subnet_id: "{{ my_public_subnet.subnet.id }}"


# Every VPC needs at least one Internet Gateway.
# This component allows traffic between the VPC and the outside world.

- name:               Create Internet Gateway for VPC
  ec2_vpc_igw:
    vpc_id:           "{{ vpc_id }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    state:            "present"
  register: my_vpc_igw


# We save the id of the Internet Gateway in a new variable.

- name:               Set Internet Gateway ID in variable
  set_fact:
    igw_id:           "{{ my_vpc_igw.gateway_id }}"


# Now we set up a Route Table. 
# We attach that Route Table to the Public Subnet.
# The route we create here defines the default routing 
# of the table, redirecting requests to the Internet Gateway. 
# We don't see it here, but the route table will also contain 
# a route for resources inside the VPC, so that if we need 
# to reach an internal resource, we don't go to the Internet
# Gateway.

- name:               Set up public subnet route table
  ec2_vpc_route_table:
    vpc_id:           "{{ vpc_id }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    tags:
      Name:           "Public"
    subnets:
      - "{{ public_subnet_id }}"
    routes:
      - dest:         "0.0.0.0/0"
        gateway_id:   "{{ igw_id }}"


# Finally, we create our Main Security Group.
# Basically the idea here is to allow SSH access
# from your IP to the EC2 resources you will 
# start in your VPC.

- name:               Create Main Security Group
  ec2_group:
    name:             "My Security Group"
    description:      "My Security Group"
    vpc_id:           "{{ vpc_id }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    rules:
      - proto:        "tcp"
        from_port:    "22"
        to_port:      "22"
        cidr_ip:      "{{ my_ip }}/32"

Run the playbook :

ansible-playbook playbook.yml -i inventory -e @vars.yml

And that’s it! Now we have a working VPC we can use to launch instances. Don’t forget to add a public IP address to the EC2 machines you will start in that VPC, and attach the Security Group to them, so that you can connect via SSH.

This kind of VPC is great for development and testing purposes, but not a good choice for production systems. You don’t want your critical resources to sit in a public subnet. Those resources need to be protected, and only reachable from within the VPC. This is where our second VPC topology comes into play.

Part 2 : VPC with Public/Private subnets

With this new topology, we are approaching the needs we would have for a production system. We will create a VPC with a public subnet, and also a private one. The later will host critical resources for your application(s), such as database servers, web servers, etc… Some of those critical resources might need a connection to the internet. Since they are located in a private subnet, they won’t have a public IP. In order to give them access to the outside world, we will have to use a NAT system.

There are several ways to do NAT on AWS. Until recently, the usual way was to create a small EC2 instance in the public subnet, with a public IP address, and use it as a NAT instance. This is still the case for many AWS users, and it is actually a cheap way to do NAT. However, in late 2015, AWS created a new service, called NAT Gateway. The name speaks for itself : it is basically a NAT-as-a-Service for VPCs. We just need to create it, attach it to our VPC, attach a public IP address and that’s pretty much it. Not only we don’t have to maintain an EC2 instance, but the service also guaranties high availability within the AZ.

VPC Public Private Subnets

Note : I chose to use NAT Gateways for this tutorial, but feel free to use an EC2 instance instead. There are plenty of great tutorials online to follow. NAT Gateways will probably cost a bit more than EC2 NAT instances. But for a production system, I would recommend using NAT Gateways, as it means less maintenance.

Another note : In this example you will have to use the AWS CLI for certain tasks. This is simply because some steps are not possible with Ansible modules … yet. They will probably soon be. In the meantime, AWS CLI it is! You can install it with pip : sudo pip install awscli.

For creating this VPC topology, here is how roles/vpc/tasks/main.yml should look like :


---

# roles/vpc/tasks/main.yml


# First task : creating the VPC.
# We are using the variables set in the vars.yml file.
# The module gives us back its result,
# which contains information about our new VPC. 
# We register it in the variable my_vpc.

- name:               Create VPC
  ec2_vpc_net:
    name:             "{{ vpc_name }}"
    cidr_block:       "{{ vpc_cidr_block }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    state:            "present"
  register: my_vpc

- name:               Set VPC ID in variable
  set_fact:
    vpc_id:           "{{ my_vpc.vpc.id }}"


# Now let's create the subnets.
# One public, one private.
# Both subnets are located in the same AZ.
# Again, we save their ids in variables.

- name:               Create Public Subnet
  ec2_vpc_subnet:
    state:            "present"
    vpc_id:           "{{ vpc_id }}"
    cidr:             "{{ public_subnet_1_cidr }}"
    az:               "{{ aws_region }}a"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    resource_tags:
      Name:           "Public Subnet"
  register: my_public_subnet

- name:               Set Public Subnet ID in variable
  set_fact:
    public_subnet_id: "{{ my_public_subnet.subnet.id }}"

- name:               Create Private Subnet
  ec2_vpc_subnet:
    state:            "present"
    vpc_id:           "{{ vpc_id }}"
    cidr:             "{{ private_subnet_1_cidr }}"
    az:               "{{ aws_region }}a"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    resource_tags:
      Name:           "Private Subnet"
  register: my_private_subnet

- name:               Set Private Subnet ID in variable
  set_fact:
    private_subnet_id: "{{ my_private_subnet.subnet.id }}"


# Every VPC needs at least one Internet Gateway.
# This component allows traffic between the VPC and the outside world.

- name:               Create Internet Gateway for VPC
  ec2_vpc_igw:
    vpc_id:           "{{ vpc_id }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    state:            "present"
  register: my_vpc_igw

- name:               Set Internet Gateway ID in variable
  set_fact:
    igw_id:           "{{ my_vpc_igw.gateway_id }}"


# Now we create an AWS Elastic IP.
# This is the IP address we will attach to the NAT Gatway.
# From that moment, we will own that IP address.
# That means if later we want to use a different service for NAT,
# we will be able to use that IP. Pretty useful. 

- name: Setup AWS CLI (1/3)
  shell: >
    aws configure set aws_access_key_id "{{ aws_access_key }}"

- name: Setup AWS CLI (2/3)
  shell: >
    aws configure set aws_secret_access_key "{{ aws_secret_key }}"

- name: Setup AWS CLI (3/3)
  shell: >
    aws configure set region {{ aws_region }}

- name: Create Elastic IP
  shell: >
      aws ec2 allocate-address --domain vpc --query AllocationId | tr -d '"'
  register: eip

- debug: var=eip

- name: Set EIP in variable
  set_fact:
    my_elastic_ip: "{{ eip.stdout }}"


# Time to create the NAT Gateway.
# As you can see, we attach a NAT Gateway to a public subnet.
# This is where the service will be located.

- name: Create NAT Gateway
  shell: >
    aws ec2 create-nat-gateway \
    --subnet-id {{ public_subnet_id }} \
    --allocation-id {{ my_elastic_ip }} \
    --query NatGateway.NatGatewayId | tr -d '"'
  register: my_nat_gateway

- name: Set Nat Gateway ID in variable
  set_fact:
    nat_gateway_id: "{{ my_nat_gateway.stdout }}"


# We pause a few seconds for the NAT Gateway to be ready.

- pause: seconds=5


# Now we set up the Route Tables.
# We will have one RT for the public subnet,
# and one for the private subnet.
# You can see that the Route Table for the private subnet
# will redirect default destinations to the NAT Gateway
# and the Route Table for the public subnet will use the
# Internet Gateway.
# 
# We don't see it here, but the Route Tables will also contain 
# a route for resources inside the VPC, so that if we need 
# to reach an internal resource, we don't go to the Internet
# Gateway or the NAT Gateway.

- name: Set up public subnet route table
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    tags:
      Name: "Public"
    subnets:
      - "{{ public_subnet_id }}"
    routes:
      - dest: "0.0.0.0/0"
        gateway_id: "{{ igw_id }}"

- name: Set up private subnet route table
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    tags:
      Name: "Private"
    subnets:
      - "{{ private_subnet_id }}"
    routes:
      - dest: "0.0.0.0/0"
        gateway_id: "{{ nat_gateway_id }}"


# Finally, let's create the Security Groups.
# We will create two : one to attach to public instances,
# and one to attach to private instances.

- name: Create Main Security Group
  ec2_group:
    name: "External SSH Access"
    description: "External SSH Access"
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    rules:
      - proto: "tcp"
        from_port: "22"
        to_port: "22"
        cidr_ip: "{{ my_ip }}/32"
  register: my_main_sg

- name: Set Main SG ID
  set_fact:
    main_sg_id: "{{ my_main_sg.group_id }}"

- name: Create Private Security Group
  ec2_group:
    name: "Private Instances SG"
    description: "Private Instances SG"
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    rules:
      - proto: "tcp"
        from_port: "22"
        to_port: "22"
        group_id: "{{ main_sg_id }}"

Run the playbook :

ansible-playbook playbook.yml -i inventory -e @vars.yml

We now have a VPC with a private subnet where we can run our production systems. Any EC2 instance created in that subnet will have access to the outside world via the NAT Gateway. However, these instances won’t be directly accessible from the outside world.

Note : How to reach these instances from the outside ? It depends on your needs. If you want to SSH to an instance located in a private subnet, you will have to create an EC2 machine in your public subnet, and SSH to that one first. Then, from that machine (usually called a bastion), you can SSH to your private instance. This use case fits perfectly with the security groups we just created : “Private Instances SG” accepts SSH connections from instances attached with “External SSH Access SG”. Another example : if one of your private instance is a web server, you might want it to be accessible from the outside world. The best way here is to create an Elastic Load Balancer in AWS, and attach your web server to it.

VPC Public Private Subnets

This topology is nice and effective, but there’s one more thing missing : availability. For now, our VPC is located in one availability zone only. It means all resources in that VPC will be in the same physical Datacenter, using the same power supply. Therefore, if that AZ fails (we know it happened in the past), we could lose everything, at least temporarily. That means … downtime. Ugh. Not acceptable for any production system. This is why your production VPC needs to be Multi-AZ.

Part 3 : VPC with Public/Private subnets and Multi Availability Zones

This is the final part of this post. As I said at the very beginning, this topology is the one I recommend, especially if you want to use this VPC for production.

In this example, we are going to create a VPC with two Availability Zones. In an AWS region, Availability Zones are physically separated, and their power supply are different, so that in the case of a power outage, not all zones are impacted. Pretty awesome.

The topology is very similar to the previous one, except that almost everything is doubled. We will create 4 subnets (2 publics, 2 privates). We will create 2 Elastic IPs, and 2 NAT Gateways (one per AZ). We double the NAT Gateways so that each AZ can be completely independent.

VPC Public Private HA Subnets

For creating this VPC topology, the roles/vpc/tasks/main.yml file should look like this :


---

# roles/vpc/tasks/main.yml


# First task : creating the VPC.
# We are using the variables set in the vars.yml file.
# The module gives us back its result,
# which contains information about our new VPC. 
# We register it in the variable my_vpc.

- name:               Create VPC
  ec2_vpc_net:
    name:             "{{ vpc_name }}"
    cidr_block:       "{{ vpc_cidr_block }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    state:            "present"
  register: my_vpc

- name:               Set VPC ID in variable
  set_fact:
    vpc_id:           "{{ my_vpc.vpc.id }}"


# Now let's create the subnets.
# Two for AZ1, two for AZ2.
# For each AZ : one public, one private.
# Again, we save their ids in variables.

- name:               Create Public Subnet [AZ-1]
  ec2_vpc_subnet:
    state:            "present"
    vpc_id:           "{{ vpc_id }}"
    cidr:             "10.0.1.0/24"
    az:               "{{ aws_region }}a"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    resource_tags:
      Name:           "Public Subnet 1"
  register: my_public_subnet_az1

- name:               Set Public Subnet ID in variable [AZ-1]
  set_fact:
    public_subnet_az1_id: "{{ my_public_subnet_az1.subnet.id }}"

- name:               Create Private Subnet [AZ-1]
  ec2_vpc_subnet:
    state:            "present"
    vpc_id:           "{{ vpc_id }}"
    cidr:             "10.0.2.0/24"
    az:               "{{ aws_region }}a"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    resource_tags:
      Name:           "Private Subnet 1"
  register: my_private_subnet_az1

- name:               Set Private Subnet ID in variable [AZ-1]
  set_fact:
    private_subnet_az1_id: "{{ my_private_subnet_az1.subnet.id }}"

- name:               Create Public Subnet [AZ-2]
  ec2_vpc_subnet:
    state:            "present"
    vpc_id:           "{{ vpc_id }}"
    cidr:             "10.0.11.0/24"
    az:               "{{ aws_region }}b"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    resource_tags:
      Name:           "Public Subnet 2"
  register: my_public_subnet_az2

- name:               Set Public Subnet ID in variable [AZ-2]
  set_fact:
    public_subnet_az2_id: "{{ my_public_subnet_az2.subnet.id }}"

- name:               Create Private Subnet [AZ-2]
  ec2_vpc_subnet:
    state:            "present"
    vpc_id:           "{{ vpc_id }}"
    cidr:             "10.0.12.0/24"
    az:               "{{ aws_region }}b"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    resource_tags:
      Name:           "Private Subnet 2"
  register: my_private_subnet_az2

- name:               Set Private Subnet ID in variable [AZ-2]
  set_fact:
    private_subnet_az2_id: "{{ my_private_subnet_az2.subnet.id }}"


# Every VPC needs at least one Internet Gateway.
# This component allows traffic between the VPC and the outside world.
# Even though we have two AZ, we only need one Internet Gateway,
# as this component is external to our VPC, and highly available.

- name:               Create Internet Gateway for VPC
  ec2_vpc_igw:
    vpc_id:           "{{ vpc_id }}"
    region:           "{{ aws_region }}"
    aws_access_key:   "{{ aws_access_key }}"
    aws_secret_key:   "{{ aws_secret_key }}"
    state:            "present"
  register: my_vpc_igw

- name:               Set Internet Gateway ID in variable
  set_fact:
    igw_id:           "{{ my_vpc_igw.gateway_id }}"


# Now we create two AWS Elastic IPs.
# We will attach them to the two NAT Gateways.
# That basically means that each AZ will have its own gateway,
# and therefore your VPC will have 2 external IP addresses.

- name: Setup AWS CLI (1/3)
  shell: >
    aws configure set aws_access_key_id "{{ aws_access_key }}"

- name: Setup AWS CLI (2/3)
  shell: >
    aws configure set aws_secret_access_key "{{ aws_secret_key }}"

- name: Setup AWS CLI (3/3)
  shell: >
    aws configure set region {{ aws_region }}

- name: Create Elastic IP [AZ-1]
  shell: >
      aws ec2 allocate-address --domain vpc --query AllocationId | tr -d '"'
  register: eip_az1

- name: Set EIP in variable [AZ-1]
  set_fact:
    my_eip_az1: "{{ eip_az1.stdout }}"

- name: Create Elastic IP [AZ-2]
  shell: >
      aws ec2 allocate-address --domain vpc --query AllocationId | tr -d '"'
  register: eip_az2

- name: Set EIP in variable [AZ-2]
  set_fact:
    my_eip_az2: "{{ eip_az2.stdout }}"


# Time to create the NAT Gateways.
# As you can see, we attach one NAT Gateway to the public subnet of AZ1,
# and the other to the public subnet of AZ2.

- name: Create NAT Gateway [AZ-1]
  shell: >
    aws ec2 create-nat-gateway \
    --subnet-id {{ public_subnet_az1_id }} \
    --allocation-id {{ my_eip_az1 }} \
    --query NatGateway.NatGatewayId | tr -d '"'
  register: my_nat_gateway_z1

- name: Set Nat Gateway ID in variable [AZ-1]
  set_fact:
    nat_gateway_az1_id: "{{ my_nat_gateway_z1.stdout }}"

- name: Create NAT Gateway [AZ-2]
  shell: >
    aws ec2 create-nat-gateway \
    --subnet-id {{ public_subnet_az2_id }} \
    --allocation-id {{ my_eip_az2 }} \
    --query NatGateway.NatGatewayId | tr -d '"'
  register: my_nat_gateway_z2

- name: Set Nat Gateway ID in variable [AZ-2]
  set_fact:
    nat_gateway_az2_id: "{{ my_nat_gateway_z2.stdout }}"


# We pause a few seconds for the NAT Gateways to be ready.

- pause: seconds=5


# Now we set up the Route Tables.
# We will have one Route Table for the public subnet,
# and one for each of the private subnets.
# You can see that the Route Tables for the private subnets
# will redirect default destinations to the NAT Gateways
# and the Route Table for the public subnet will use the
# Internet Gateway.
# We can use the same Route Table for the two public subnets,
# as their configuration is identical : 
# they both use the internet gateway
# to reach the outside world.
# 
# We don't see it here, but the Route Tables will also contain 
# a route for resources inside the VPC, so that if we need 
# to reach an internal resource, we don't go to the Internet
# Gateway or the NAT Gateway.

- name: Set up public subnet route table
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    tags:
      Name: "Public"
    subnets:
      - "{{ public_subnet_az1_id }}"
      - "{{ public_subnet_az2_id }}"
    routes:
      - dest: "0.0.0.0/0"
        gateway_id: "{{ igw_id }}"

- name: Set up private subnet route table [AZ-1]
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    tags:
      Name: "Private 1"
    subnets:
      - "{{ private_subnet_az1_id }}"
    routes:
      - dest: "0.0.0.0/0"
        gateway_id: "{{ nat_gateway_az1_id }}"

- name: Set up private subnet route table [AZ-2]
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    tags:
      Name: "Private 2"
    subnets:
      - "{{ private_subnet_az2_id }}"
    routes:
      - dest: "0.0.0.0/0"
        gateway_id: "{{ nat_gateway_az2_id }}"


# Finally, let's create the Security Groups.
# We will create two : one to attach to public instances,
# and one to attach to private instances.

- name: Create Main Security Group
  ec2_group:
    name: "External SSH Access"
    description: "External SSH Access"
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    rules:
      - proto: "tcp"
        from_port: "22"
        to_port: "22"
        cidr_ip: "{{ my_ip }}/32"
  register: my_main_sg

- name: Set Main SG ID
  set_fact:
    main_sg_id: "{{ my_main_sg.group_id }}"

- name: Create Private Security Group
  ec2_group:
    name: "Private Instances SG"
    description: "Private Instances SG"
    vpc_id: "{{ vpc_id }}"
    region: "{{ aws_region }}"
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    rules:
      - proto: "tcp"
        from_port: "22"
        to_port: "22"
        group_id: "{{ main_sg_id }}"

Run the playbook :

ansible-playbook playbook.yml -i inventory -e @vars.yml

With this topology now created, you are ready to deploy highly available applications to your Virtual Private Cloud on AWS.

Conclusion

This use case is the perfect example of why automation is key when working with Cloud resources. Now that this script is done, creating another VPC will only take seconds. I hope this article will help you to decide which topology is best for your use case, and to learn how to use Ansible to create it from scratch. Feel free to ask any question in the comments below.