Provisioning VPC using Boto3, Terraform, CloudFormation and Terraform with CloudFormation
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
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.
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
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.
Click activities and 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.
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)
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
Then open PuTTY Key Generator and load the private key ansiblekey.pem
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
We then move on to Session and input IP address
For convenience, we may save it as a predefined session as shown below
You should see the pop up below if you log in for the very first time
Then you input your username and password to login. You see below image after log in.
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
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
IGW created, which is attached to boto3_vpc
Subnet and associate it with route table
Route table with a 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)
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
To clean up VPC created
vim 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
Create a variables.tf
file
vim variables.tf
Create a terraform.tfvars
file to provide variables required
vim terraform.tfvars
Lastly, create an outputs.tf
file to provide outputs
vim 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.0Terraform 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:
+ createTerraform 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
IGW created, which is attached to boto3_vpc
Subnet and associate it with route table
Route table with a 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)
To clear up infrastructure
$ terraform destroy
.
.
.
Enter a value: yesaws_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 1sDestroy 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
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
IGW created, which is attached to Development
Subnet and associate it with route table
Route table with a 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)
Why CloudFormation?
With CloudFormation, we will have following information collected in AWS CloudFormation Console
Notes: In case of any error during CloudFormation provisioning, we may take advantage of CloudFormation to do troubleshooting
Notes: Resources section will provide us with what resources are created
Notes: Using CloudFormation, we will also be able to print out Outputs for reference in the future
Notes: Parameters allow us to record the parameters we input initially
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
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
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.0Terraform 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:
+ createTerraform 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 VPCParameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH acces s to the instance
Type: AWS::EC2::KeyPair::KeyName
Default: ec2-keypairInstanceType:
Description: EC2 instance type
Type: String
Default: t2.microEnvironmentName:
Description: An environment name that is prefixed to resource na mes
Type: String
Default: DevelopmentVpcCIDR:
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-0742b4e673072066fResources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
Ref: VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value:
Ref: EnvironmentNameInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value:
Ref: EnvironmentNameInternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: InternetGateway
VpcId:
Ref: VPCPublicSubnet1:
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 RoutesDefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGatewayPublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId:
Ref: PublicSubnet1VPCEC2SecurityGroup:
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: VPCVPCEC2:
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: VPCPublicSubnet1:
Description: A reference to the public subnet in the 1st Availab ility Zone
Value:
Ref: PublicSubnet1VPCEC2SecurityGroup:
Description: Security group with no ingress rule
Value:
Ref: VPCEC2SecurityGroupInternetGateway:
Description: InternetGateway Information
Value:
Ref: InternetGatewayPublicRouteTable:
Description: Public Route Table Information
Value:
Ref: PublicRouteTableVPCEC2:
Description: EC2 Information
Value:
Ref: VPCEC2PublicIpAddress:
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 planTerraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ createTerraform 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 VPCParameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH acces s to the instance
Type: AWS::EC2::KeyPair::KeyName
Default: ec2-keypairInstanceType:
Description: EC2 instance type
Type: String
Default: t2.microEnvironmentName:
Description: An environment name that is prefixed to resource na mes
Type: String
Default: DevelopmentVpcCIDR:
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-0742b4e673072066fResources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
Ref: VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value:
Ref: EnvironmentNameInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value:
Ref: EnvironmentNameInternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: InternetGateway
VpcId:
Ref: VPCPublicSubnet1:
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 RoutesDefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGatewayPublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId:
Ref: PublicSubnet1VPCEC2SecurityGroup:
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: VPCVPCEC2:
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: VPCPublicSubnet1:
Description: A reference to the public subnet in the 1st Availab ility Zone
Value:
Ref: PublicSubnet1VPCEC2SecurityGroup:
Description: Security group with no ingress rule
Value:
Ref: VPCEC2SecurityGroupInternetGateway:
Description: InternetGateway Information
Value:
Ref: InternetGatewayPublicRouteTable:
Description: Public Route Table Information
Value:
Ref: PublicRouteTableVPCEC2:
Description: EC2 Information
Value:
Ref: VPCEC2PublicIpAddress:
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:
+ createTerraform 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 VPCParameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH acces s to the instance
Type: AWS::EC2::KeyPair::KeyName
Default: ec2-keypairInstanceType:
Description: EC2 instance type
Type: String
Default: t2.microEnvironmentName:
Description: An environment name that is prefixed to resource na mes
Type: String
Default: DevelopmentVpcCIDR:
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-0742b4e673072066fResources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
Ref: VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value:
Ref: EnvironmentNameInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value:
Ref: EnvironmentNameInternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: InternetGateway
VpcId:
Ref: VPCPublicSubnet1:
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 RoutesDefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGatewayPublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId:
Ref: PublicSubnet1VPCEC2SecurityGroup:
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: VPCVPCEC2:
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: VPCPublicSubnet1:
Description: A reference to the public subnet in the 1st Availab ility Zone
Value:
Ref: PublicSubnet1VPCEC2SecurityGroup:
Description: Security group with no ingress rule
Value:
Ref: VPCEC2SecurityGroupInternetGateway:
Description: InternetGateway Information
Value:
Ref: InternetGatewayPublicRouteTable:
Description: Public Route Table Information
Value:
Ref: PublicRouteTableVPCEC2:
Description: EC2 Information
Value:
Ref: VPCEC2PublicIpAddress:
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
IGW created, which is attached to Development
Subnet and associate it with route table
Route table with a 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)
The website to present
Here are two ways we may find our public ip address
- Go to our CloudFormation 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)
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-placeTerraform 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: yesaws_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
Now let us tear down the whole infrastructure for the one last time
$ terraform destroy
.
.
.
Enter a value: yesaws_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 1m28sDestroy 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
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!