Provisioning VPC using Boto3, Terraform, CloudFormation and Terraform with CloudFormation

Paul Zhao
Paul Zhao Projects
Published in
30 min readApr 24, 2021

Before we even get started, I’d like to discuss the objective of the project — explore varities of methods to provision VPC using IaC and find out the pros and cons of each option. At the end of the day, choose the one that fits you and your organization

Boto3 (AWS SDK for Python)

You use the AWS SDK for Python (Boto3) to create, configure, and manage AWS services, such as Amazon Elastic Compute Cloud (Amazon EC2) and Amazon Simple Storage Service (Amazon S3). The SDK provides an object-oriented API as well as low-level access to AWS services

Terraform

Terraform is an open-source infrastructure as code software tool that provides a consistent CLI workflow to manage hundreds of cloud services. Terraform codifies cloud APIs into declarative configuration files

Tips: My personal takeaway of Terraform

  • provision infrastructure seamlessly
  • update infrastructure with ease
  • destroy infrastructure without hassle
  • check codes errors with click of a button
  • plan out infrastructures prior to deployment
  • track records from state file

CloudFormation

AWS CloudFormation gives you an easy way to model a collection of related AWS and third-party resources, provision them quickly and consistently, and manage them throughout their lifecycles, by treating infrastructure as code. A CloudFormation template describes your desired resources and their dependencies so you can launch and configure them together as a stack. You can use a template to create, update, and delete an entire stack as a single unit, as often as you need to, instead of managing resources individually. You can manage and provision stacks across multiple AWS accounts and AWS Regions

Notes: CloudForamation here is for AWS only, in other word, AWS native template. So it may not b a good fit for you if AWS is not your provider of Cloud

Terraform + CloudFormation

Here the real meats! With AWS, we can utilize CloudFormation. With other cloud providers, we can take advantage of other resources. Either way, terraform could be seen as an IaC hub for all our Cloud projects! With that said, we can

  • provision infrastructure seamlessly
  • update infrastructure with ease
  • destroy infrastrcuture without hassle
  • check codes errors with click of a button
  • plan out infrastructures prior to deployment
  • track records from state file

Doing all of above using Terraform and build up infrastructure along with native IaC tool from any specific almost all Cloud providers!

Reminder: This should be part 1 of the project since I will dive in AWS CDK with Terraform and attempt to provision VPC using CDK with Terraform. Stay tune!

Here are the options I would touch upon throughout this project

Project Architecture

Prerequites:

  • An AWS account — with non-root user (take security into consideration)
  • In terms of system, we will be using RHEL 8.3 by Oracle Virtual Box on Windows 10 using putty
  • AWSCLI installed
  • Install Terraform

Let us work on them one by one.

Creating a non-root user

Based on AWS best practice, root user is not recommended to perform everyday tasks, even the administrative ones. The root user, rather is used to to create your first IAM user, groups and roles. Then you need to securely lock away the root user credentials and use them to perform only a few account and service management tasks.

Notes: If you would like to learn more about why we should not use root user for operations and more about AWS account, please find more here.

Login as a Root user
Create a user under IAM service
Choose programmatic access
Choose programmatic access
Create user without tags
Keep credentials (Access key ID and Secret access key)

Set up RHEL 8.3 by Oracle Virtual Box on Windows 10 using putty

First, we will download Oracle Virtual Box on Windows 10, please click Windows hosts

Second, we will also download RHEL iso

Let us make it work now!

Click Oracle VirtualBox and open the application and follow instructions here, you will install RHEL 8.3 as shown below

Oracle VM VirtualBox

Notes: In case you are unable to install RHEL 8.3 successfully, please find solutions here. Also, after you create your developer’s account with Red Hat, you have to wait for sometime before register it. Otherwise, you may receive errors as well.

Now it’s time for us to connect to RHEL 8.3 from Windows 10 using VirtualBox.

Login RHEL 8.3

Click activities and open terminal

Open terminal

Notes: In order to be able to connect to RHEL 8.3 from Windows 10 using putty later, we must enable what it is shown below.

Bridged Adapter selectedBridged Adapter selected

Now we will get the ip that we will be using to connect to RHEL 8.3 from Windows 10 using Putty (highlighted ip address for enp0s3 is the right one to use)

IP address

Then we will install Putty.

ssh-keygen with a password

Creating a password-protected key looks something like this:

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/pzhao/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/pzhao/.ssh/id_rsa.
Your public key has been saved in /home/pzhao/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:RXPnUZg/fGgRGTOxEfbo3VOMo/Yp4Gi80has/iR4m/A pzhao@localhost.localdomain
The key's randomart image is:
+---[RSA 3072]----+
| o . %X.|
| . o +=@ |
| . B++|
| . oo==|
| .S . o...=|
| . .oo o . ..|
| o oo=.. . o |
| +o*o. . |
| .E+o |
+----[SHA256]-----+

To find out private key

$ cat .ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAwoavXHvZCYPO/sbMD0ibtkvF+9/NmSm2m/Z8wRy7O2A012YS98ap
8aq18PXfKPyyAMNF3hdG3xi1KMD7DSIb/C1gunjTREEJRfYjydOjFBFtZWY78Mj4eQkrPJ
.
.
.
-----END OPENSSH PRIVATE KEY-----

Notes: You may take advantage of GUI of RHEL to send Private Key as an email, then open the mail and copy the private key from email

Open the Notepad in Windows 10 and save private key as ansiblekey.pem file

Ansiblekey.pem

Then open PuTTY Key Generator and load the private key ansiblekey.pem

Load private key in putty key generator

Then save it as a private key as ansible.ppk file

We now open Putty and input IP address we saved previously as Host Name (or IP address) 192.168.0.18

Load private key in putty

We then move on to Session and input IP address

IP address saved

For convenience, we may save it as a predefined session as shown below

Saved session

You should see the pop up below if you log in for the very first time

First time log in

Then you input your username and password to login. You see below image after log in.

Login successfully

Installing AWS CLI

To install AWS CLI after logging into Redhat8

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

To verify the installation

$ aws --version
aws-cli/2.0.46 Python/3.7.4 Darwin/19.6.0 exe/x86_64

To use aws cli, we need to configure it using aws access key, aws secret access key, aws region and aws output format

$ aws configure
AWS Access Key ID [****************46P7]:
AWS Secret Access Key [****************SoXF]:
Default region name [us-east-1]:
Default output format [json]:

Installing Terraform

To install terraform, simply use the following command:

Install yum-config-manager to manage your repositories.

$ sudo yum install -y yum-utils

Use yum-config-manager to add the official HashiCorp Linux repository.

$ sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo

Install terraform

$ sudo yum -y install terraform

Notes: In case of a wrong symbolic link set up, please check out this link. Also, you may need to re login after changing the symbolic link.

To check out installation of terraform

$ terraform version
Terraform v0.14.3
+ provider registry.terraform.io/hashicorp/aws v3.21.0

Installing Python3 and Boto3

Though you may have Python2 preinstalled in your system. It is, however, preferred to install latest version of Python3 for this project

For detailed installation, please visit here

Make sure you do check installation after installing

$ python3 --version
Python 3.6.8
$ pip3 --version
pip 21.0.1 from /home/pzhao/.local/lib/python3.6/site-packages/pip (python 3.6)
$ pip3 show boto3
Name: boto3
Version: 1.16.35
Summary: The AWS SDK for Python
Home-page: https://github.com/boto/boto3
Author: Amazon Web Services
Author-email: UNKNOWN
License: Apache License 2.0
Location: /home/pzhao/.local/lib/python3.6/site-packages
Requires: s3transfer, botocore, jmespath
Required-by: aws-vpc

— Here we go after our prerequisites are all set! —

To kick off our project, we need to make a directory for the project and change into the directory

$ mkdir Boto3forVPC && cd Boto3forVPC/

Create a Python file using Boto3 to provision our VPC

vim vpc.py

vpc.py

To provision VPC

$ python3 vpc.py
ec2.Vpc(id='vpc-005382a4c773378a7')
[ec2.Tag(resource_id='vpc-005382a4c773378a7', key='Name', value='boto3_vpc')]
ec2.InternetGateway(id='igw-0e1ca1c17c56dea96')
ec2.RouteTable(id='rtb-0b0b2afb80ba70677')
ec2.Route(route_table_id='rtb-0b0b2afb80ba70677', destination_cidr_block='0.0.0.0/0')
ec2.Subnet(id='subnet-0b3fcb0b24adadd8b')
ec2.SecurityGroup(id='sg-03090bf8dcd216d50')
[ec2.Instance(id='i-0db6a00626adcd7d7')]

Then we will cross check our creations in AWS console

VPC named boto3_vpc created

Boto3_vpc

IGW created, which is attached to boto3_vpc

Igw

Subnet and associate it with route table

Subnet with route table

Route table with a public route

Route table with public route

Security group and allow SSH inbound rule through the VPC via your own ip address (Notes: for your own security, you should never expose your ip address)

Ssh with your own ip address

Create a file to store the key locally and Call the boto3 ec2 function to create a key pair named ec2-keypair , then capture the key and store it in a file ec2-keypair.pem

$ ls 
ec2-keypair.pem rmvpc.py vpc.py

Amazon Linux 2 instance in the subnet

Ec2 created

To clean up VPC created

vim vpc_destroy.py

Vpc_destroy.py

Now let us clean up VPC using it

$ python3 vpc_destroy.py --vpc_id vpc-005382a4c773378a7 --region us-east-1 --services ec2
type: <class 'str'>
[vpc_destroy.py:243 - <module>() ] calling destroy_services with ec2
[credentials.py:1217 - load() ] Found credentials in shared credentials file: ~/.aws/credentials
[vpc_destroy.py:58 - destroy_ec2() ] instance deletion list: ['i-0db6a00626adcd7d7']
[vpc_destroy.py:60 - destroy_ec2() ] Waiting for instances to terminate
[vpc_destroy.py:246 - <module>() ] calling delete_vpc with vpc-005382a4c773378a7
[vpc_destroy.py:162 - delete_vpc() ] no ENIs remaining
destroyed vpc-005382a4c773378a7 in us-east-1

Notes: As shown above, you must provide vpc_id, region and services. In case you may want to learn more about how this VPC cleanup using Boto3, please refer to this post

Notes: Please make sure every time you apply vpc.py to create a brand new VPC, you need to provide with a brand new keypair name. Otherwise, creation can’t be done properly. To do so, you can use either AWS or AWS CLI

For AWS CLI, apply code below

$ aws ec2 delete-key-pair --key-name <name of your key pair>

Here I also would like to provide Terraforming VPC and Terraforming VPC using AWS CloudFormation

The reason behind it is to provide various ways to provision VPC to meet different needs and requirements. Also, let us figure out which option is more efficient and effective

Here we go with Terraforming VPC

We need to create vpc.tf file

vim vpc.tf

Vpc.tf

Create a variables.tf file

vim variables.tf

Variables.tf

Create a terraform.tfvars file to provide variables required

vim terraform.tfvars

Terraform.tfvars

Lastly, create an outputs.tf file to provide outputs

vim outputs.tf

Outputs.tf

Now we will terrafrom our vpc

$ terraform initInitializing the backend...Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Reusing previous version of hashicorp/tls from the dependency lock file
- Using previously-installed hashicorp/aws v3.37.0
- Using previously-installed hashicorp/tls v3.1.0
Terraform has been successfully initialized!You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Validate our terraform codes

$ terraform validate
Success! The configuration is valid.

Then, we will plan our terraform infrastructure

$ terraform planAn execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:# aws_instance.bastion_host will be created
+ resource "aws_instance" "bastion_host" {
+ ami = "ami-0742b4e673072066f"
+ arn = (known after apply)
+ associate_public_ip_address = true
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ id = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = "ec2-keypair"
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ subnet_id = (known after apply)
+ tenancy = (known after apply)
+ vpc_security_group_ids = (known after apply)
+ ebs_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ snapshot_id = (known after apply)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ enclave_options {
+ enabled = (known after apply)
}
+ ephemeral_block_device {
+ device_name = (known after apply)
+ no_device = (known after apply)
+ virtual_name = (known after apply)
}
+ metadata_options {
+ http_endpoint = (known after apply)
+ http_put_response_hop_limit = (known after apply)
+ http_tokens = (known after apply)
}
+ network_interface {
+ delete_on_termination = (known after apply)
+ device_index = (known after apply)
+ network_interface_id = (known after apply)
}
+ root_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}
# aws_internet_gateway.igw will be created
+ resource "aws_internet_gateway" "igw" {
+ arn = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Environment" = "Development"
+ "Name" = "boto3_vpc"
}
+ vpc_id = (known after apply)
}
# aws_key_pair.bastion_host_key will be created
+ resource "aws_key_pair" "bastion_host_key" {
+ arn = (known after apply)
+ fingerprint = (known after apply)
+ id = (known after apply)
+ key_name = "ec2-keypair"
+ key_pair_id = (known after apply)
+ public_key = (known after apply)
}
# aws_route_table.public_route_table will be created
+ resource "aws_route_table" "public_route_table" {
+ arn = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ propagating_vgws = (known after apply)
+ route = [
+ {
+ carrier_gateway_id = ""
+ cidr_block = "0.0.0.0/0"
+ destination_prefix_list_id = ""
+ egress_only_gateway_id = ""
+ gateway_id = (known after apply)
+ instance_id = ""
+ ipv6_cidr_block = ""
+ local_gateway_id = ""
+ nat_gateway_id = ""
+ network_interface_id = ""
+ transit_gateway_id = ""
+ vpc_endpoint_id = ""
+ vpc_peering_connection_id = ""
},
]
+ tags = {
+ "Environment" = "var.environment_tag"
}
+ vpc_id = (known after apply)
}
# aws_route_table_association.public_rt_association[0] will be created
+ resource "aws_route_table_association" "public_rt_association" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# aws_security_group.bastion_host will be created
+ resource "aws_security_group" "bastion_host" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = [
+ {
+ cidr_blocks = []
+ description = ""
+ from_port = 22
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "var.protocol"
+ security_groups = []
+ self = false
+ to_port = 22
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ "72.137.76.221/32",
]
+ description = ""
+ from_port = 22
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 22
},
]
+ name = (known after apply)
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ vpc_id = (known after apply)
}
# aws_subnet.public_subnet[0] will be created
+ resource "aws_subnet" "public_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-east-1a"
+ availability_zone_id = (known after apply)
+ cidr_block = "172.16.1.0/24"
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ tags = {
+ "Environment" = "var.environment_tag"
}
+ tags_all = {
+ "Environment" = "var.environment_tag"
}
+ vpc_id = (known after apply)
}
# aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ arn = (known after apply)
+ assign_generated_ipv6_cidr_block = false
+ cidr_block = "172.16.0.0/16"
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_classiclink = (known after apply)
+ enable_classiclink_dns_support = (known after apply)
+ enable_dns_hostnames = true
+ enable_dns_support = true
+ id = (known after apply)
+ instance_tenancy = "default"
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Environment" = "Development"
+ "Name" = "boto3_vpc"
}
+ tags_all = {
+ "Environment" = "Development"
+ "Name" = "boto3_vpc"
}
}
# tls_private_key.public_key will be created
+ resource "tls_private_key" "public_key" {
+ algorithm = "RSA"
+ ecdsa_curve = "P224"
+ id = (known after apply)
+ private_key_pem = (sensitive value)
+ public_key_fingerprint_md5 = (known after apply)
+ public_key_openssh = (known after apply)
+ public_key_pem = (known after apply)
+ rsa_bits = 4096
}
Plan: 9 to add, 0 to change, 0 to destroy.Changes to Outputs:
+ cidr_block = "172.16.0.0/16"
+ gateway_id = (known after apply)
+ key_name = "ec2-keypair"
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ Environment = "Development"
}
+ vpc_security_group_ids = [
+ (known after apply),
]
------------------------------------------------------------------------Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

After all, we will provision the infrastructure

& terraform apply
.
.
.
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
Outputs:cidr_block = "172.16.0.0/16"
gateway_id = "igw-00c878177091ee5ff"
key_name = "ec2-keypair"
route_table_id = "rtb-09abf7d333fb323a9"
subnet_id = "subnet-0266d2520570ebc91"
tags = {
"Environment" = "Development"
}
vpc_security_group_ids = [
"sg-010c0c6a1bcd73207",
]

Then we will cross check our creations in AWS console

VPC named boto3_vpc created

Boto3_vpc

IGW created, which is attached to boto3_vpc

Igw

Subnet and associate it with route table

Subnet with route table

Route table with a public route

Route table with public route

Security group and allow SSH inbound rule through the VPC via your own ip address (Notes: for your own security, you should never expose your ip address)

Ssh with own ip address
Ec2 created

To clear up infrastructure

$ terraform destroy
.
.
.
Enter a value: yes
aws_route_table_association.public_rt_association[0]: Destroying... [id=rtbassoc-00481172028db8966]
aws_instance.bastion_host: Destroying... [id=i-01e2bb7abb2db174c]
aws_route_table_association.public_rt_association[0]: Destruction complete after 0s
aws_route_table.public_route_table: Destroying... [id=rtb-09abf7d333fb323a9]
aws_route_table.public_route_table: Destruction complete after 1s
aws_internet_gateway.igw: Destroying... [id=igw-00c878177091ee5ff]
aws_instance.bastion_host: Still destroying... [id=i-01e2bb7abb2db174c, 10s elapsed]
aws_internet_gateway.igw: Still destroying... [id=igw-00c878177091ee5ff, 10s elapsed]
aws_instance.bastion_host: Still destroying... [id=i-01e2bb7abb2db174c, 20s elapsed]
aws_internet_gateway.igw: Still destroying... [id=igw-00c878177091ee5ff, 20s elapsed]
aws_instance.bastion_host: Still destroying... [id=i-01e2bb7abb2db174c, 30s elapsed]
aws_internet_gateway.igw: Still destroying... [id=igw-00c878177091ee5ff, 30s elapsed]
aws_internet_gateway.igw: Destruction complete after 38s
aws_instance.bastion_host: Still destroying... [id=i-01e2bb7abb2db174c, 40s elapsed]
aws_instance.bastion_host: Destruction complete after 41s
aws_key_pair.bastion_host_key: Destroying... [id=ec2-keypair]
aws_security_group.bastion_host: Destroying... [id=sg-010c0c6a1bcd73207]
aws_subnet.public_subnet[0]: Destroying... [id=subnet-0266d2520570ebc91]
aws_key_pair.bastion_host_key: Destruction complete after 0s
tls_private_key.public_key: Destroying... [id=396df559ec4e67fa77a64fa2a06ef9dc4de95f81]
tls_private_key.public_key: Destruction complete after 0s
aws_security_group.bastion_host: Destruction complete after 0s
aws_subnet.public_subnet[0]: Destruction complete after 0s
aws_vpc.main: Destroying... [id=vpc-02bbdf024bf575f02]
aws_vpc.main: Destruction complete after 1s
Destroy complete! Resources: 9 destroyed.

Now we will be provisioning VPC using CloudFormation

Here is the AWS official template for VPC as reference

Here is the vpc.yaml file

vim vpc.yaml

Vpc.yaml

Then we will build our VPC using AWS CLI

$ aws cloudformation create-stack --stack-name cfvpc --template-body file://vpc.yaml
{
"StackId": "arn:aws:cloudformation:us-east-1:464392538707:stack/cfvpc/2ffd0070-a49e-11eb-96f6-0ed5acf75591"
}

Notes: Since CloudFormation can’t create a keypair on its own, we must create a keypair. Here we use AWSCLI to do so

$ aws ec2 create-key-pair --key-name ec2-keypair
{
"KeyFingerprint": "ab:b2:0c:81:9e:1e:07:d5:15:bb:a9:b1:41:b0:5d:e2:af:b5:38:23",
"KeyMaterial": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAmB0Ani+OiUMiqrlgrMc50oGo9KjsVj3MNnxbmFSyMNxuLrSp\nXLiRsSHcJK7cH6IxOXuSOE0fczJmhFQZnFLTpkOqx7ELpRBDSTFdnYfqXf5oxrjs\nf5OZRXuZqjncgxxd7f/KLEBrn5xBR03aiUkc/w5lYTR0JMjWIlIsPxIxRwF9NZ/o\n2AA6K/gX2lqteFV9mUizq0Z301Ryd/RNyeABEHhJ4fuJTb1MlBc1wEGZj2pM0aVr\n67nRV0RZhTP8uj9MKg1OAvb5yaZuvBiccoiUOshoYjgrPJsnuywH6kNLHNahbrjj\n9XirKQdQ4TogqJnaW/Vd3d9J8IwNA8LcYs88owIDAQABAoIBACxeaUu6u2y2NGpv\n4A8FnYwVXd7fVvBg3iwWYfEw4zj1Uv40nCH7hCOSqM/aYUKo4IrPzHq3pDDJxrVa\ngo3iavHYUvwkXC0tbTLwP0ov1uDL0GwGjJU5zD9EKjJI5lUn9Q3yylnWAI5x2Wif\nANuCg/6xiEiuMCJ6olsodNeAyvbWuIhR+uyFfVE4vEdoqK4l8UYWQNACOnppKgaJ\nrbcKnfi+dj0AejQInpk8Ov6UUdTEINQZ4JG/d/2DFYWktGBmHVhEm9h4WIr9HRV6\nQT6LklIZYF6e9W379Z5ZXX1aGUdSeHPAefFkNwPrNCMjLCRur+C16dUAzO6jBRpG\nYZPcauECgYEA3F/9ioQ0x4//wAQkzZb81tFqRRjKTSggx2+lTKkb/UhXtCN67XJD\nMzJoGOIVPVbHdcmpP0v0cG7g71e0gyRZ7zN0m28n6L415UXOx2/PL3qZoRb3TcAq\njm4uaTxp0pCXLRmfryqiCcZzu6cmxG6YujmtKNdBrGjuvIqNdZCutu8CgYEAsLQR\nNcvxBxAGFwSWZv5ztf5agmpgK8j9gVxcO3Qc0QIU67o78hgvVkNhYgopWt4UmKzi\nPRMMVRwX82iNpLFXwqgKQnIcueYiYXdC8F9+FSByq1TnkTFtLQJwJlRln0/TreNO\n6XN+gZBmldacExyqPAz93+vZo4lvu1LcbXybNY0CgYAkPf0afKeZcksjLwtGbGBk\ni8goWO1cRw8s/WV3+A/MVctmqrcaucHnd5C7FuNbVRw0eNfGux0WKIYBlrDvKFlK\nB3JT5bHwiueeLx7UmcS/EDCX14kQVlwpVGF5mR/mKzVRi3dBfYdsiCCcad7sSyv+\n5GFf6Ba63f71LuwYu5SgLQKBgQCTxzQxcorz5iHBtFN4dUseJEdblE0zsRbZzf1Q\nt421+nC2p/ykPjewhA94Z5koZlyBRuy6OSjyMNmS9pim6K3FnLVf1oFRszaDnrL7\nxlDyqD1eLlavpc9xef2DAMgwURlt7pE7Shy9jJ9OprnGfg2cxRy43U0ZqMIpvmWc\npz5CrQKBgCttNZrapWuh8Y6QXlMDp5sCovUAj9Fw5J11j0yZyf+i704hnEuv+/5H\nvzFuvTwaRlSIoz1Frnvhb9NF+9NYqrno2T3GOATRgThgHKN9soqJism5tG/jLGDS\n2x9P99HC94Ybd13IKDXrLK/gCVh97Zlel21CY2+Pu6K/6L3kjogH\n-----END RSA PRIVATE KEY-----",
"KeyName": "ec2-keypair",
"KeyPairId": "key-023bd02d2f97b4a31"
}

Then we will cross check our creations in AWS console

VPC named Development created

Development

IGW created, which is attached to Development

Igw

Subnet and associate it with route table

Subnet with route table

Route table with a public route

Route table with public route

Security group and allow SSH inbound rule through the VPC via your own ip address (Notes: for your own security, you should never expose your ip address)

Ssh with your own ip address
Ec2 created

Why CloudFormation?

With CloudFormation, we will have following information collected in AWS CloudFormation Console

Events

Notes: In case of any error during CloudFormation provisioning, we may take advantage of CloudFormation to do troubleshooting

Resources

Notes: Resources section will provide us with what resources are created

Outputs

Notes: Using CloudFormation, we will also be able to print out Outputs for reference in the future

Parameters

Notes: Parameters allow us to record the parameters we input initially

Template

Notes: We are able to store an original CloudFormation template file as a backup in this section

Now it’s time for us to clean up the infrastructure

$ aws cloudformation delete-stack --stack-name cfvpc
Clean up

It is all cleaned up now

Ultimately, let us take advantage of both Terraform and CloudFormation to accomplish Terraforming CloudFormation to provision VPC

**Special Tribute to my fellow lovely coach at Level Up In Tech — Charles**

For providing his code for a gorgeous simple website, please refer to his git repo and medium post for terraforming the blog on its own!

Now with this project, you’ll have a website page presented as well

To do this project, we will need to create a brand new directory and change into it

$ mkdir tf_cf_vpc && cd tf_cf_vpc/

For the CloudFormation, we still will be using our terraform_vpc_userdata.yaml file created previously

vim terraform_vpc_userdata.yaml

Vpc.yaml

Then we create our tf_cf_vpc.tf file

Tf_cf_vpc.tf

As we get ready to terraform our infrastructure

$ terraform initInitializing the backend...Initializing provider plugins...
- Reusing previous version of hashicorp/tls from the dependency lock file
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/tls v3.1.0
- Using previously-installed hashicorp/aws v3.39.0
Terraform has been successfully initialized!You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Then we validate our code

$ terraform validate
Success! The configuration is valid.

After that, we will plan our infrastructure

$ terraform planTerraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:# aws_cloudformation_stack.tf_cf_vpc will be created
+ resource "aws_cloudformation_stack" "tf_cf_vpc" {
+ id = (known after apply)
+ name = "TfCfVpc"
+ outputs = (known after apply)
+ parameters = {
+ "InstanceType" = "t3.nano"
+ "KeyName" = "tf_cf_keypair"
}
+ policy_body = (known after apply)
+ tags_all = (known after apply)
+ template_body = <<-EOT
Description: This CloudFormation YAML file will provision a VPC
Parameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH acces s to the instance
Type: AWS::EC2::KeyPair::KeyName
Default: ec2-keypair
InstanceType:
Description: EC2 instance type
Type: String
Default: t2.micro
EnvironmentName:
Description: An environment name that is prefixed to resource na mes
Type: String
Default: Development
VpcCIDR:
Description: Please enter the IP range (CIDR notation) for this VPC
Type: String
Default: "172.16.0.0/16"
PublicSubnet1CIDR:
Description: Please enter the IP range (CIDR notation) for the p ublic subnet in the first Availability Zone
Type: String
Default: "172.16.1.0/24"
Mappings:
AWSRegionToAMI:
us-east-1:
AMIID: ami-0742b4e673072066f
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
Ref: VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value:
Ref: EnvironmentName
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value:
Ref: EnvironmentName
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: InternetGateway
VpcId:
Ref: VPC
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: VPC
AvailabilityZone: !Select [ 0, !GetAZs '' ]
CidrBlock:
Ref: PublicSubnet1CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Subnet (AZ1)
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Routes
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId:
Ref: PublicSubnet1
VPCEC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: only allow SSH traffic
GroupName: SG - 20 and 80
SecurityGroupIngress:
- CidrIp: 72.137.76.221/32
FromPort: 22
IpProtocol: tcp
ToPort: 22
- CidrIp: 0.0.0.0/0
FromPort: 80
IpProtocol: tcp
ToPort: 80
Tags:
-
Key: Name
Value: CloudFormationSecurityGroup
VpcId:
Ref: VPC
VPCEC2:
Type: AWS::EC2::Instance
Properties:
ImageId:
!FindInMap
- AWSRegionToAMI
- !Ref AWS::Region
- AMIID
InstanceType: !Ref InstanceType
SecurityGroupIds:
- !GetAtt "VPCEC2SecurityGroup.GroupId"
SubnetId: !Ref PublicSubnet1
KeyName:
Ref: KeyName
UserData:
!Base64 | # No more Fn::Join needed
#!/bin/bash
sudo yum update -y
sudo yum install git -y
sudo yum install httpd -y
sudo service httpd start
sudo chkconfig httpd on
cd /var/www/html/
sudo git clone https://github.com/cawoodruff/SimpleWebsite .git
cd ./SimpleWebsite
sudo cp -R * ../
Outputs:
VPC:
Description: A reference to the created VPC
Value:
Ref: VPC
PublicSubnet1:
Description: A reference to the public subnet in the 1st Availab ility Zone
Value:
Ref: PublicSubnet1
VPCEC2SecurityGroup:
Description: Security group with no ingress rule
Value:
Ref: VPCEC2SecurityGroup
InternetGateway:
Description: InternetGateway Information
Value:
Ref: InternetGateway
PublicRouteTable:
Description: Public Route Table Information
Value:
Ref: PublicRouteTable
VPCEC2:
Description: EC2 Information
Value:
Ref: VPCEC2
PublicIpAddress:
Description: Public Ip Address
Value: !GetAtt VPCEC2.PublicIp
EOT
}
# aws_key_pair.bastion_host_key will be created
+ resource "aws_key_pair" "bastion_host_key" {
+ arn = (known after apply)
+ fingerprint = (known after apply)
+ id = (known after apply)
+ key_name = "tf_cf_keypair"
+ key_pair_id = (known after apply)
+ public_key = (known after apply)
+ tags_all = (known after apply)
}
# tls_private_key.public_key will be created
+ resource "tls_private_key" "public_key" {
+ algorithm = "RSA"
+ ecdsa_curve = "P224"
+ id = (known after apply)
+ private_key_pem = (sensitive value)
+ public_key_fingerprint_md5 = (known after apply)
+ public_key_openssh = (known after apply)
+ public_key_pem = (known after apply)
+ rsa_bits = 4096
}
Plan: 3 to add, 0 to change, 0 to destroy.───────────────────────────────────────────────────────────────────────────────Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.$ terraform plan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:# aws_cloudformation_stack.tf_cf_vpc will be created
+ resource "aws_cloudformation_stack" "tf_cf_vpc" {
+ id = (known after apply)
+ name = "TfCfVpc"
+ outputs = (known after apply)
+ parameters = {
+ "InstanceType" = "t3.nano"
+ "KeyName" = "tf_cf_keypair"
}
+ policy_body = (known after apply)
+ tags_all = (known after apply)
+ template_body = <<-EOT
Description: This CloudFormation YAML file will provision a VPC
Parameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH acces s to the instance
Type: AWS::EC2::KeyPair::KeyName
Default: ec2-keypair
InstanceType:
Description: EC2 instance type
Type: String
Default: t2.micro
EnvironmentName:
Description: An environment name that is prefixed to resource na mes
Type: String
Default: Development
VpcCIDR:
Description: Please enter the IP range (CIDR notation) for this VPC
Type: String
Default: "172.16.0.0/16"
PublicSubnet1CIDR:
Description: Please enter the IP range (CIDR notation) for the p ublic subnet in the first Availability Zone
Type: String
Default: "172.16.1.0/24"
Mappings:
AWSRegionToAMI:
us-east-1:
AMIID: ami-0742b4e673072066f
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
Ref: VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value:
Ref: EnvironmentName
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value:
Ref: EnvironmentName
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: InternetGateway
VpcId:
Ref: VPC
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: VPC
AvailabilityZone: !Select [ 0, !GetAZs '' ]
CidrBlock:
Ref: PublicSubnet1CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Subnet (AZ1)
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Routes
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId:
Ref: PublicSubnet1
VPCEC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: only allow SSH traffic
GroupName: SG - 20 and 80
SecurityGroupIngress:
- CidrIp: 72.137.76.221/32
FromPort: 22
IpProtocol: tcp
ToPort: 22
- CidrIp: 0.0.0.0/0
FromPort: 80
IpProtocol: tcp
ToPort: 80
Tags:
-
Key: Name
Value: CloudFormationSecurityGroup
VpcId:
Ref: VPC
VPCEC2:
Type: AWS::EC2::Instance
Properties:
ImageId:
!FindInMap
- AWSRegionToAMI
- !Ref AWS::Region
- AMIID
InstanceType: !Ref InstanceType
SecurityGroupIds:
- !GetAtt "VPCEC2SecurityGroup.GroupId"
SubnetId: !Ref PublicSubnet1
KeyName:
Ref: KeyName
UserData:
!Base64 | # No more Fn::Join needed
#!/bin/bash
sudo yum update -y
sudo yum install git -y
sudo yum install httpd -y
sudo service httpd start
sudo chkconfig httpd on
cd /var/www/html/
sudo git clone https://github.com/cawoodruff/SimpleWebsite .git
cd ./SimpleWebsite
sudo cp -R * ../
Outputs:
VPC:
Description: A reference to the created VPC
Value:
Ref: VPC
PublicSubnet1:
Description: A reference to the public subnet in the 1st Availab ility Zone
Value:
Ref: PublicSubnet1
VPCEC2SecurityGroup:
Description: Security group with no ingress rule
Value:
Ref: VPCEC2SecurityGroup
InternetGateway:
Description: InternetGateway Information
Value:
Ref: InternetGateway
PublicRouteTable:
Description: Public Route Table Information
Value:
Ref: PublicRouteTable
VPCEC2:
Description: EC2 Information
Value:
Ref: VPCEC2
PublicIpAddress:
Description: Public Ip Address
Value: !GetAtt VPCEC2.PublicIp
EOT
}
# aws_key_pair.bastion_host_key will be created
+ resource "aws_key_pair" "bastion_host_key" {
+ arn = (known after apply)
+ fingerprint = (known after apply)
+ id = (known after apply)
+ key_name = "tf_cf_keypair"
+ key_pair_id = (known after apply)
+ public_key = (known after apply)
+ tags_all = (known after apply)
}
# tls_private_key.public_key will be created
+ resource "tls_private_key" "public_key" {
+ algorithm = "RSA"
+ ecdsa_curve = "P224"
+ id = (known after apply)
+ private_key_pem = (sensitive value)
+ public_key_fingerprint_md5 = (known after apply)
+ public_key_openssh = (known after apply)
+ public_key_pem = (known after apply)
+ rsa_bits = 4096
}
Plan: 3 to add, 0 to change, 0 to destroy.───────────────────────────────────────────────────────────────────────────────Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Let’s apply the code

$ terraform applyTerraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:# aws_cloudformation_stack.tf_cf_vpc will be created
+ resource "aws_cloudformation_stack" "tf_cf_vpc" {
+ id = (known after apply)
+ name = "TfCfVpc"
+ outputs = (known after apply)
+ parameters = {
+ "InstanceType" = "t3.nano"
+ "KeyName" = "tf_cf_keypair"
}
+ policy_body = (known after apply)
+ tags_all = (known after apply)
+ template_body = <<-EOT
Description: This CloudFormation YAML file will provision a VPC
Parameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH acces s to the instance
Type: AWS::EC2::KeyPair::KeyName
Default: ec2-keypair
InstanceType:
Description: EC2 instance type
Type: String
Default: t2.micro
EnvironmentName:
Description: An environment name that is prefixed to resource na mes
Type: String
Default: Development
VpcCIDR:
Description: Please enter the IP range (CIDR notation) for this VPC
Type: String
Default: "172.16.0.0/16"
PublicSubnet1CIDR:
Description: Please enter the IP range (CIDR notation) for the p ublic subnet in the first Availability Zone
Type: String
Default: "172.16.1.0/24"
Mappings:
AWSRegionToAMI:
us-east-1:
AMIID: ami-0742b4e673072066f
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
Ref: VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value:
Ref: EnvironmentName
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value:
Ref: EnvironmentName
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: InternetGateway
VpcId:
Ref: VPC
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: VPC
AvailabilityZone: !Select [ 0, !GetAZs '' ]
CidrBlock:
Ref: PublicSubnet1CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Subnet (AZ1)
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Routes
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId:
Ref: PublicSubnet1
VPCEC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: only allow SSH traffic
GroupName: SG - 20 and 80
SecurityGroupIngress:
- CidrIp: 72.137.76.221/32
FromPort: 22
IpProtocol: tcp
ToPort: 22
- CidrIp: 0.0.0.0/0
FromPort: 80
IpProtocol: tcp
ToPort: 80
Tags:
-
Key: Name
Value: CloudFormationSecurityGroup
VpcId:
Ref: VPC
VPCEC2:
Type: AWS::EC2::Instance
Properties:
ImageId:
!FindInMap
- AWSRegionToAMI
- !Ref AWS::Region
- AMIID
InstanceType: !Ref InstanceType
SecurityGroupIds:
- !GetAtt "VPCEC2SecurityGroup.GroupId"
SubnetId: !Ref PublicSubnet1
KeyName:
Ref: KeyName
UserData:
!Base64 | # No more Fn::Join needed
#!/bin/bash
sudo yum update -y
sudo yum install git -y
sudo yum install httpd -y
sudo service httpd start
sudo chkconfig httpd on
cd /var/www/html/
sudo git clone https://github.com/cawoodruff/SimpleWebsite .git
cd ./SimpleWebsite
sudo cp -R * ../
Outputs:
VPC:
Description: A reference to the created VPC
Value:
Ref: VPC
PublicSubnet1:
Description: A reference to the public subnet in the 1st Availab ility Zone
Value:
Ref: PublicSubnet1
VPCEC2SecurityGroup:
Description: Security group with no ingress rule
Value:
Ref: VPCEC2SecurityGroup
InternetGateway:
Description: InternetGateway Information
Value:
Ref: InternetGateway
PublicRouteTable:
Description: Public Route Table Information
Value:
Ref: PublicRouteTable
VPCEC2:
Description: EC2 Information
Value:
Ref: VPCEC2
PublicIpAddress:
Description: Public Ip Address
Value: !GetAtt VPCEC2.PublicIp
EOT
}
# aws_key_pair.bastion_host_key will be created
+ resource "aws_key_pair" "bastion_host_key" {
+ arn = (known after apply)
+ fingerprint = (known after apply)
+ id = (known after apply)
+ key_name = "tf_cf_keypair"
+ key_pair_id = (known after apply)
+ public_key = (known after apply)
+ tags_all = (known after apply)
}
# tls_private_key.public_key will be created
+ resource "tls_private_key" "public_key" {
+ algorithm = "RSA"
+ ecdsa_curve = "P224"
+ id = (known after apply)
+ private_key_pem = (sensitive value)
+ public_key_fingerprint_md5 = (known after apply)
+ public_key_openssh = (known after apply)
+ public_key_pem = (known after apply)
+ rsa_bits = 4096
}
Plan: 3 to add, 0 to change, 0 to destroy.Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yestls_private_key.public_key: Creating...
aws_cloudformation_stack.tf_cf_vpc: Creating...
tls_private_key.public_key: Creation complete after 2s [id=642909581a47cc164ce6f 70c9ef125c3c5c372a0]
aws_key_pair.bastion_host_key: Creating...
aws_key_pair.bastion_host_key: Creation complete after 0s [id=tf_cf_keypair]
aws_cloudformation_stack.tf_cf_vpc: Still creating... [10s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still creating... [20s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still creating... [30s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still creating... [40s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still creating... [50s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still creating... [1m0s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Creation complete after 1m9s [id=arn:aws:cloudformation:us-east-1:464392538707:stack/TfCfVpc/6faf5630-af6d-11eb-a247-0ad5dcc6785b]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Then we will cross check our creations in AWS console

VPC named Development created

Development

IGW created, which is attached to Development

Igw

Subnet and associate it with route table

Subnet with route table

Route table with a public route

Route table with public route

Security group and allow SSH inbound rule through the VPC via your own ip address (Notes: for your own security, you should never expose your ip address)

Ssh with your own ip address
Ec2 created

The website to present

Here are two ways we may find our public ip address

  • Go to our CloudFormation Outputs page
Outputs page
  • Or grep from terraform.tfstate file
$ cat terraform.tfstate | grep -i publicipaddress | head -1
"PublicIpAddress": "52.90.93.181",

Here you go with a gorgeous single page blog (this is due to userdata section of EC2 in our .yaml file)

Blog

Notes: Just a quick reminder, userdata may take quick a while to be executed, please have patience!

Notes: Here we will be updating one InstanceType value in our .tf file and test the power of terraform. Along with CloudFormation, we can take advantage of both Terraform’s flexibility and CloudFormation’s AWS native and easy provision

Here we will provide InstanceType as t3.large

vim tf_cf_vpc.tf

provider "aws" {region = "us-east-1"}# Keypair
resource "tls_private_key" "public_key" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "aws_key_pair" "bastion_host_key" {
key_name = "tf_cf_keypair"
public_key = "${tls_private_key.public_key.public_key_openssh}"
}
resource "aws_cloudformation_stack" "tf_cf_vpc" {name = "TfCfVpc"parameters = {KeyName = "tf_cf_keypair"InstanceType = "t3.large"}template_body = "${file("vpc.yaml")}"}

Let us plan it again to see the difference

$ terraform plan
tls_private_key.public_key: Refreshing state... [id=421ec2f7302a83853afda93bcc066066fd87cb1b]
aws_key_pair.bastion_host_key: Refreshing state... [id=tf_cf_keypair]
aws_cloudformation_stack.tf_cf_vpc: Refreshing state... [id=arn:aws:cloudformation:us-east-1:464392538707:stack/TfCfVpc/b8b5f360-a4b5-11eb-b4b0-12d101416601]
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_cloudformation_stack.tf_cf_vpc will be updated in-place
~ resource "aws_cloudformation_stack" "tf_cf_vpc" {
id = "arn:aws:cloudformation:us-east-1:464392538707:stack/TfCfVpc/b8b5f360-a4b5-11eb-b4b0-12d101416601"
name = "TfCfVpc"
~ parameters = {
~ "InstanceType" = "t3.nano" -> "t3.large"
# (1 unchanged element hidden)
}
tags = {}
# (3 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.Warning: Interpolation-only expressions are deprecatedon tf_cf_vpc.tf line 17, in resource "aws_key_pair" "bastion_host_key":
17: public_key = "${tls_private_key.public_key.public_key_openssh}"
Terraform 0.11 and earlier required all non-constant expressions to be
provided via interpolation syntax, but this pattern is now deprecated. To
silence this warning, remove the "${ sequence from the start and the }"
sequence from the end of this expression, leaving just the inner expression.
Template interpolation syntax is still used to construct strings from
expressions when the template includes multiple interpolation sequences or a
mixture of literal strings and interpolations. This deprecation applies only
to templates that consist entirely of a single interpolation sequence.
(and one more similar warning elsewhere)------------------------------------------------------------------------Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Now let us accomplish it

$ terraform apply
.
.
.
Enter a value: yes
aws_cloudformation_stack.tf_cf_vpc: Modifying... [id=arn:aws:cloudformation:us-east-1:464392538707:stack/TfCfVpc/b8b5f360-a4b5-11eb-b4b0-12d101416601]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 10s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 20s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 30s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 40s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 50s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 1m0s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 1m10s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 1m20s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still modifying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 1m30s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Modifications complete after 1m39s [id=arn:aws:cloudformation:us-east-1:464392538707:stack/TfCfVpc/b8b5f360-a4b5-11eb-b4b0-12d101416601]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Only InstanceType is changed to t3.large so that less time is spent on provisioning

Let us cross check in AWS console

T3.nano termiated and t3.large up running

Now let us tear down the whole infrastructure for the one last time

$ terraform destroy 
.
.
.
Enter a value: yes
aws_key_pair.bastion_host_key: Destroying... [id=tf_cf_keypair]
aws_cloudformation_stack.tf_cf_vpc: Destroying... [id=arn:aws:cloudformation:us-east-1:464392538707:stack/TfCfVpc/b8b5f360-a4b5-11eb-b4b0-12d101416601]
aws_key_pair.bastion_host_key: Destruction complete after 1s
tls_private_key.public_key: Destroying... [id=421ec2f7302a83853afda93bcc066066fd87cb1b]
tls_private_key.public_key: Destruction complete after 0s
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 10s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 20s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 30s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 40s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 50s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 1m0s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 1m10s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Still destroying... [id=arn:aws:cloudformation:us-east-1:464392...c/b8b5f360-a4b5-11eb-b4b0-12d101416601, 1m20s elapsed]
aws_cloudformation_stack.tf_cf_vpc: Destruction complete after 1m28s
Destroy complete! Resources: 3 destroyed.

Voila! C’est fini!

Conclusion:

When we wrap up our project, let us recap our project’s objective and what we accomplish

Project Architecture

As shown from project architecture, we dive into 4 different ways to provision VPC using Iac

  • Boto3
  • CloudFormation (AWS native)
  • Terraform (Cloud native)
  • Terraform + CloudFormation

First of all, using Boto3, it may require 2 different files to create vpc in the first place and destroy it at the end of the day. With that said, it could be a concern in terms of management

Secondly, we built up our VPC infrastructure using CloudFormation. Since it is AWS native, it did work perfectly. However, in terms of udpates, we may need to work on the same file for parameters as per say. But it could still be well managed

Thirdly, Terraform as IaC tool did shine with its flexibility. From building infrastructure, through updating to destroying at the end of the day, every single move was seamless. However, we need to spend time building infrastructure using Terraform as a brand new language. With that said, it could be time-consusing

Lastly, it was a match made by God Terraform + CloudFormation for AWS. Taking advantage of every aspect of Terraform, we advance our AWS infrastructure using CloudFormation. Templates were readily available on AWS official website and plenty of cloud related blogs and websites. We can simply apply the CloudFormation .yaml file with Terraform. More importantly, Terraform’s flexibility still applies. For instance, in project, we update our InstanceType using a parameters in .tf file. This infrastructure for AWS is fantabulous

I assume Terraform + Cloud Native IaC Tool for your Cloud provider should be the way to go in Cloud!

Maybe I should explore more in part 2 of this post before jumping into conclusion — Terraform with CDK

Stay tune, guys!

--

--

Paul Zhao
Paul Zhao Projects

Amazon Web Service Certified Solutions Architect Professional & Devops Engineer