Welcome to Day 3 of 21 Days of AWS using Terraform, Let continue our journey, yesterday I discussed how to build AWS VPC using terraform.
In order to deploy EC2 instance we need a bunch of resources
AMI
Key Pair
EBS Volumes Creation
User data
The first step in deploying EC2 instance is choosing correct AMI and in terraform, there are various ways to do that
We can hardcore the value of AMI
We can use data resource(similar to what we used for Availability Zone in VPC section) to query and filter AWS and get the latest AMI based on the region, as the AMI id is different in a different region.
data "aws_ami" "centos" {
owners = ["679593333241"]
most_recent = true
filter {
name = "name"
values = ["CentOS Linux 7 x86_64 HVM EBS *"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
}
NOTE: Use of data resource is not ideal and each and every used case, eg: In the case of Production we might want to use a specific version of CentOS.
The above code will help us to get the latest Centos AMI, the code is self-explanatory but one important parameter we used is owners
owners – Limit search to specific AMI owners. Valid items are the numeric account ID, amazon, or self.
most_recent – If more than one result is returned, use the most recent AMI.This is to get the latest Centos AMI as per our use case.
As we are able to figure out the AMI part, the next step is to create and use the key pair
Either we can hardcode the value of key pair or generate a new key via command line and then refer to this file.
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/prashant/.ssh/id_rsa): /tmp/id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /tmp/id_rsa.
Your public key has been saved in /tmp/id_rsa.pub.
The key fingerprint is:
SHA256:A7lYe+KDAVoUFEDsC14wnb+NePzCntjZruJdJf0+apM prashant@prashant-ThinkPad-T440s
The key's randomart image is:
+---[RSA 2048]----+
|++*o. |
| = o . |
|. = . + |
|.+ o + +. |
|+ o = B.So |
| o . O +o.. |
| + +. .. |
| .+o=o E.. |
| .ooB+o..o.. |
+----[SHA256]-----+
There is one more resource, I want to use here called template_file. The template_file data source renders a template from a template string, which is usually loaded from an external file. This you can use with user_data resource to execute any script during instance boot time
data "template_file" "init" {
template = "${file("${path.module}/userdata.tpl")}"
}
userdata.tpl file will be like
#!/bin/bash
yum -y install httpd
echo "hello from terraform" >> /var/www/html/index.html
service httpd start
chkconfig httpd on
After AMI and Keys out of our way, let start building EC2 instance
Most of these parameters I already discussed in the first section, but let's quickly review it and check the new one
* count: The number of instance, we want to create
* ami: This time we are pulling ami using data resource
* instance_type: Important parameter in AWS Realm, the type of instance we want to create
* key_name: Resource we create earlier and we are just referencing it here* tags: Tags are always helpful to assign label to your resources.
Below ones are special(vpc_security_group_ids & subnet_id), because both of these resource we created during the vpc section, so now what we need to do is to output it during VPC module and use there output as the input to this module.
If you notice the above code, one thing which is interesting here is vpc_security_group_ids and subnet_id
The interesting part, we already created these as a part of VPC code, so we just need to call in our EC2 terraform and the way to do it using outputs.tf.
output "public_subnets" {
value = "${aws_subnet.public_subnet.*.id}"
}
output "security_group" {
value = "${aws_security_group.test_sg.id}"
}
After calling these values here, we just need to define as the part of main module and the syntax of doing that is
* To create EBS Volumes, I am using ebs_volume resource and to attach it use aws_volume_attachment
* We are creating two Volumes here
* As Volume is specific to Availibility Zone, I am using aws_availibility_zone data resource
* Size of the Volume is 1GB
* Type is gp2(other available options "standard", "gp2", "io1", "sc1" or "st1" (Default: "standard"))
Welcome to Day 2 of 21 Days of AWS using Terraform, Let continue our journey, yesterday I discussed terraform, today let’s build VPC using terraform
What is VPC?
Without going to all the nitty-gritty details of VPC, first, let’s try to understand VPC in the simplest term. Before the cloud era, we use to have datacenters where we deploy all of our infrastructures.
You can think of VPC as your datacentre in a cloud but rather than spending months or weeks to set up that datacenter it’s now just a matter of minutes(API calls). It’s the place where you define your network which closely resembles your own traditional data centers with the benefits of using the scalable infrastructure provided by AWS.
Today we are going to build the first half of the equation i.e VPC
Once we create the VPC using AWS Console, these things created for us by-default
* Network Access Control List(NACL)* Security Group* Route Table
We need to take care of
* Internet Gateways * Subnets * Custom Route Table
But the bad news is as we are creating this via terraform we need to create all these things manually but this is just one time task, later on, if we need to build one more VPC we just need to call this module with some minor changes(eg: Changes in CIDR Range, Subnet) true Infrastructure as a Code(IAAC)
So the first step is to create a data resource, what data resource did is to query/list all the AWS available Availablity zone in a given region and then allow terraform to use those resource.
enable_dns_support – (Optional) A boolean flag to enable/disable DNS support in the VPC. Defaults true. Amazon provided DNS server(AmazonProvidedDNS) can resolve Amazon provided private DNS hostnames, that we specify in a private hosted zones in Route53.
enable_dns_hostnames – (Optional) A boolean flag to enable/disable DNS hostnames in the VPC. Defaults false. This will ensure that instances that are launched into our VPC receive a DNS hostname.
* Internet gateway is a horizontally scaled, redundant and highly avilable VPC component. * Internet gateway serves one more purpose, it performs NAT for instances that have been assigned public IPv4 addresses.
Network Access Control List(NACL) A network access control list (ACL) is an optional layer of security for your VPC that acts as a firewall for controlling traffic in and out of one or more subnets.
Security Group acts as a virtual firewall and is used to control the traffic for its associated instances.
$ terraform init
Initializing modules...
- vpc in vpc
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.35.0...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.aws: version = "~> 2.35"
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.
Execute terraform plan
Generate and show an execution plan
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
module.vpc.data.aws_availability_zones.available: Refreshing state...
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.vpc.aws_default_route_table.private_route will be created
+ resource "aws_default_route_table" "private_route" {
+ default_route_table_id = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ route = (known after apply)
+ tags = {
+ "Name" = "my-private-route-table"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_internet_gateway.gw will be created
+ resource "aws_internet_gateway" "gw" {
+ id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-igw"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_route_table.public_route will be created
+ resource "aws_route_table" "public_route" {
+ id = (known after apply)
+ owner_id = (known after apply)
+ propagating_vgws = (known after apply)
+ route = [
+ {
+ cidr_block = "0.0.0.0/0"
+ egress_only_gateway_id = ""
+ gateway_id = (known after apply)
+ instance_id = ""
+ ipv6_cidr_block = ""
+ nat_gateway_id = ""
+ network_interface_id = ""
+ transit_gateway_id = ""
+ vpc_peering_connection_id = ""
},
]
+ tags = {
+ "Name" = "my-test-public-route"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_route_table_association.private_subnet_assoc[0] will be created
+ resource "aws_route_table_association" "private_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_route_table_association.private_subnet_assoc[1] will be created
+ resource "aws_route_table_association" "private_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_route_table_association.public_subnet_assoc[0] will be created
+ resource "aws_route_table_association" "public_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_route_table_association.public_subnet_assoc[1] will be created
+ resource "aws_route_table_association" "public_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_security_group.test_sg will be created
+ resource "aws_security_group" "test_sg" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = (known after apply)
+ id = (known after apply)
+ ingress = (known after apply)
+ name = "my-test-sg"
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ vpc_id = (known after apply)
}
# module.vpc.aws_security_group_rule.all_outbound_access will be created
+ resource "aws_security_group_rule" "all_outbound_access" {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ from_port = 0
+ id = (known after apply)
+ protocol = "-1"
+ security_group_id = (known after apply)
+ self = false
+ source_security_group_id = (known after apply)
+ to_port = 0
+ type = "egress"
}
# module.vpc.aws_security_group_rule.ssh_inbound_access will be created
+ resource "aws_security_group_rule" "ssh_inbound_access" {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ from_port = 22
+ id = (known after apply)
+ protocol = "tcp"
+ security_group_id = (known after apply)
+ self = false
+ source_security_group_id = (known after apply)
+ to_port = 22
+ type = "ingress"
}
# module.vpc.aws_subnet.private_subnet[0] will be created
+ resource "aws_subnet" "private_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-west-2a"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.3.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-private-subnet.1"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_subnet.private_subnet[1] will be created
+ resource "aws_subnet" "private_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-west-2b"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.4.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-private-subnet.2"
}
+ vpc_id = (known after apply)
}
# module.vpc.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-west-2a"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.1.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-public-subnet.1"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_subnet.public_subnet[1] will be created
+ resource "aws_subnet" "public_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-west-2b"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.2.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-public-subnet.2"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ arn = (known after apply)
+ assign_generated_ipv6_cidr_block = false
+ cidr_block = "10.0.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 = {
+ "Name" = "my-new-test-vpc"
}
}
Plan: 15 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
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.
Final step terraform apply
Builds or changes the infrastructure
$ terraform apply
module.vpc.data.aws_availability_zones.available: Refreshing state...
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.vpc.aws_default_route_table.private_route will be created
+ resource "aws_default_route_table" "private_route" {
+ default_route_table_id = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ route = (known after apply)
+ tags = {
+ "Name" = "my-private-route-table"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_internet_gateway.gw will be created
+ resource "aws_internet_gateway" "gw" {
+ id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-igw"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_route_table.public_route will be created
+ resource "aws_route_table" "public_route" {
+ id = (known after apply)
+ owner_id = (known after apply)
+ propagating_vgws = (known after apply)
+ route = [
+ {
+ cidr_block = "0.0.0.0/0"
+ egress_only_gateway_id = ""
+ gateway_id = (known after apply)
+ instance_id = ""
+ ipv6_cidr_block = ""
+ nat_gateway_id = ""
+ network_interface_id = ""
+ transit_gateway_id = ""
+ vpc_peering_connection_id = ""
},
]
+ tags = {
+ "Name" = "my-test-public-route"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_route_table_association.private_subnet_assoc[0] will be created
+ resource "aws_route_table_association" "private_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_route_table_association.private_subnet_assoc[1] will be created
+ resource "aws_route_table_association" "private_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_route_table_association.public_subnet_assoc[0] will be created
+ resource "aws_route_table_association" "public_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_route_table_association.public_subnet_assoc[1] will be created
+ resource "aws_route_table_association" "public_subnet_assoc" {
+ id = (known after apply)
+ route_table_id = (known after apply)
+ subnet_id = (known after apply)
}
# module.vpc.aws_security_group.test_sg will be created
+ resource "aws_security_group" "test_sg" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = (known after apply)
+ id = (known after apply)
+ ingress = (known after apply)
+ name = "my-test-sg"
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ vpc_id = (known after apply)
}
# module.vpc.aws_security_group_rule.all_outbound_access will be created
+ resource "aws_security_group_rule" "all_outbound_access" {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ from_port = 0
+ id = (known after apply)
+ protocol = "-1"
+ security_group_id = (known after apply)
+ self = false
+ source_security_group_id = (known after apply)
+ to_port = 0
+ type = "egress"
}
# module.vpc.aws_security_group_rule.ssh_inbound_access will be created
+ resource "aws_security_group_rule" "ssh_inbound_access" {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ from_port = 22
+ id = (known after apply)
+ protocol = "tcp"
+ security_group_id = (known after apply)
+ self = false
+ source_security_group_id = (known after apply)
+ to_port = 22
+ type = "ingress"
}
# module.vpc.aws_subnet.private_subnet[0] will be created
+ resource "aws_subnet" "private_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-west-2a"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.3.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-private-subnet.1"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_subnet.private_subnet[1] will be created
+ resource "aws_subnet" "private_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-west-2b"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.4.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-private-subnet.2"
}
+ vpc_id = (known after apply)
}
# module.vpc.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-west-2a"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.1.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-public-subnet.1"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_subnet.public_subnet[1] will be created
+ resource "aws_subnet" "public_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-west-2b"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.2.0/24"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "my-test-public-subnet.2"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ arn = (known after apply)
+ assign_generated_ipv6_cidr_block = false
+ cidr_block = "10.0.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 = {
+ "Name" = "my-new-test-vpc"
}
}
Plan: 15 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: yes
module.vpc.aws_vpc.main: Creating...
module.vpc.aws_vpc.main: Creation complete after 5s [id=vpc-0b97129b9e91cac4d]
module.vpc.aws_internet_gateway.gw: Creating...
module.vpc.aws_default_route_table.private_route: Creating...
module.vpc.aws_subnet.private_subnet[0]: Creating...
module.vpc.aws_subnet.private_subnet[1]: Creating...
module.vpc.aws_security_group.test_sg: Creating...
module.vpc.aws_subnet.public_subnet[1]: Creating...
module.vpc.aws_subnet.public_subnet[0]: Creating...
module.vpc.aws_default_route_table.private_route: Creation complete after 2s [id=rtb-034ed4f91b0c6e970]
module.vpc.aws_subnet.private_subnet[0]: Creation complete after 2s [id=subnet-0fb890defa508e1bd]
module.vpc.aws_subnet.private_subnet[1]: Creation complete after 2s [id=subnet-0f210acfcdbb26b1c]
module.vpc.aws_route_table_association.private_subnet_assoc[1]: Creating...
module.vpc.aws_route_table_association.private_subnet_assoc[0]: Creating...
module.vpc.aws_subnet.public_subnet[1]: Creation complete after 2s [id=subnet-0c1da219075f58bbf]
module.vpc.aws_internet_gateway.gw: Creation complete after 2s [id=igw-048be706e974b88cb]
module.vpc.aws_subnet.public_subnet[0]: Creation complete after 2s [id=subnet-0d881fbf8d72978ab]
module.vpc.aws_route_table.public_route: Creating...
module.vpc.aws_route_table_association.private_subnet_assoc[1]: Creation complete after 0s [id=rtbassoc-08c8b58da17951c27]
module.vpc.aws_route_table_association.private_subnet_assoc[0]: Creation complete after 0s [id=rtbassoc-033c1036cbb76fbd3]
module.vpc.aws_security_group.test_sg: Creation complete after 2s [id=sg-05a2779f164a402c7]
module.vpc.aws_security_group_rule.ssh_inbound_access: Creating...
module.vpc.aws_security_group_rule.all_outbound_access: Creating...
module.vpc.aws_security_group_rule.all_outbound_access: Creation complete after 2s [id=sgrule-4135805673]
module.vpc.aws_route_table.public_route: Creation complete after 2s [id=rtb-07f3c3293741dfbaf]
module.vpc.aws_route_table_association.public_subnet_assoc[0]: Creating...
module.vpc.aws_route_table_association.public_subnet_assoc[1]: Creating...
module.vpc.aws_route_table_association.public_subnet_assoc[1]: Creation complete after 0s [id=rtbassoc-06332e5c154dae9d8]
module.vpc.aws_route_table_association.public_subnet_assoc[0]: Creation complete after 0s [id=rtbassoc-02aa98f854d51e7aa]
module.vpc.aws_security_group_rule.ssh_inbound_access: Creation complete after 3s [id=sgrule-2460989827]
Apply complete! Resources: 15 added, 0 changed, 0 destroyed.
Terraform Module
You can think of Terraform Module like any other language module eg: Python, it’s the same terraform file but just that after creating a module out it we can re-use that code OR Instead copy-pasting the code the same code in different places we can turn into reusable modules.
Hello everyone and welcome to Day 1 of 21 Days of AWS using Terraform, the topic for today is Introduction to Terraform. Let’s begin our AWS automation journey using Terraform.
What is terraform?
Terraform is a tool for provisioning infrastructure(or managing Infrastructure as Code). It supports multiple providers(eg, AWS, Google Cloud, Azure, OpenStack..).
Installing Terraform is pretty straightforward, download it from Terraform download page and select the appropriate package based on your operating system.
I am using Centos7, so these are the steps I need to follow, to install terraform on Centos7.
Step1: Download the Package
$ wget https://releases.hashicorp.com/terraform/0.12.13/terraform_0.12.13_linux_amd64.zip
--2019-11-08 18:47:29-- https://releases.hashicorp.com/terraform/0.12.13/terraform_0.12.13_linux_amd64.zip
Resolving releases.hashicorp.com (releases.hashicorp.com)... 2a04:4e42:2f::439, 151.101.201.183
Connecting to releases.hashicorp.com (releases.hashicorp.com)|2a04:4e42:2f::439|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16341595 (16M) [application/zip]
Saving to: ‘terraform_0.12.13_linux_amd64.zip’
100%[==================================================================================================================================================================>] 16,341,595 --.-K/s in 0.09s
2019-11-08 18:47:29 (172 MB/s) - ‘terraform_0.12.13_linux_amd64.zip’ saved [16341595/16341595]
Step2: Unzip it
$ unzip terraform_0.12.13_linux_amd64.zip
Archive: terraform_0.12.13_linux_amd64.zip
inflating: terraform
Step3: Add the binary to PATH environment variable
sudo cp terraform /usr/local/bin/
sudo chmod +x /usr/local/bin/terraform
Step4: Logout and log back in
To verify terraform is installed properly
$ terraform version
Terraform v0.12.13
As mentioned above terraform support many providers, for my use case I am using AWS.
Prerequisites1: Existing AWS Account(OR Setup a new account) 2: IAM full access(OR at least have AmazonEC2FullAccess) 3: AWS Credentials(AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)
Once you have pre-requisites 1 and 2 done, the first step is to export Keys AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
export AWS_ACCESS_KEY_ID="your access key id here"
export AWS_SECRET_ACCESS_KEY="your secret access key id here"
NOTE: These two variables are bound to your current shell, in case of reboot, or if open a new shell window, these changes will be lost
With all pre-requisites in place, it’s time to write your first terraform code, but before that just a brief overview about terraform language
Terraform code is written in the HashiCorp Configuration Language(HCL)
All the code ends with the extension of .tf
It’s a declarative language(We need to define what infrastructure we want and terraform will figure out how to create it)
In this first example I am going to build EC2 instance, but before creating EC2 instance go to AWS console and think what the different things we need to build EC2 instance are
Amazon Machine Image(AMI)
Instance Type
Network Information(VPC/Subnet)
Tags
Security Group
Key Pair
Let break each of these steps by step
Amazon Machine Image(AMI): It’s an Operating System Image used to run EC2 instance. For this example, I am using Centos 7 ami-01ed306a12b7d1c96. We can create our own AMI using AWS console or Packer.
Instance Type: Type of EC2 instance to run, as every instance type provides different capabilities(CPU, Memory, I/O). For this example, I am using t2.micro(1 Virtual CPU, 1GB Memory)
For more info please refer to Amazon EC2 instance type
Network Information(VPC/Subnet Id): Virtual Private Cloud(VPC) is an isolated area of AWS account that has it’s own virtual network and IP address space. For this example, I am using default VPC which is part of a new AWS account. In case if you want to set up your instance in custom VPC, you need to add two additional parameters(vpc_security_group_ids and subnet_id) to your terraform code
Let review all these parameters and see what we already have and what we need to create to spun our first EC2 instance
Amazon Machine Image(AMI) → ami-01ed306a12b7d1c96
Instance Type → t2.micro
Network Information(VPC/Subnet) → Default VPC
Tags
Security Group
Key Pair
So we already have AMI, Instance Type and Network Information, we need to write terraform code for rest of the parameter to spun our first EC2 instance.
Let start with Key Pair
Go to terraform documentation and search for aws key pair
Whenever you see a $ sign and curly braces inside the double quotes, that means terraform is going to interpolate that code specially. To get the id of the security group
"${aws_security_group.examplesg.id}"
Same thing applied to key pair
"${aws_key_pair.example.id}"
Our code is ready but we are missing one thing, provider before starting any code we need to tell terraform which provider we are using(aws in this case)
provider "aws" { region = "us-west-2" }
This tells terraform that you are going to use AWS as provider and you want to deploy your infrastructure in us-west-2 region
AWS has datacenter all over the world, which are grouped in region and availability zones. Region is a separate geographic area(Oregon, Virginia, Sydney) and each region has multiple isolated datacenters(us-west-2a,us-west-2b..)
For more info about regions and availability zones, please refer to the below doc
Before running terraform command to spun our first EC2 instance, run terraform fmt command which will rewrite terraform configuration files to a canonical format and style
$ terraform fmt
main.tf
The first command we are going to run to setup our instance is terraform init, what this will do is going to download code for a provider(aws) that we are going to use.
Terraform binary contains the basic functionality for terraform but it doesn’t come with the code for any of the providers(eg: AWS, Azure and GCP), so when we are first starting to use terraform we need to run terraform init to tell terraform to scan the code and figure out which providers we are using and download the code for them.
By default, the provider code will be downloaded into a .terraform directory which is a scrarch directory(we may want to add it in a .gitignore).
NOTE: It’s safe to run terraform init command multiple times as it’s idempotent.
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.35.0...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.aws: version = "~> 2.35"
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.
Next command we are going to run is “terraform plan”, this will tell what terraform actually do before making any changes
This is good way of making any sanity check before making actual changes to env
Output of terraform plan command looks similar to Linux diff command
1: (+ sign): Resource going to be created
2: (- sign): Resources going to be deleted
3: (~ sign): Resource going to be modified
t$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An 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.ec2_instance will be created
+ resource "aws_instance" "ec2_instance" {
+ ami = "ami-01ed306a12b7d1c96"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ 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 = (known after apply)
+ network_interface_id = (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)
+ security_groups = (known after apply)
+ source_dest_check = true
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "my-first-ec2-instance"
}
+ tenancy = (known after apply)
+ volume_tags = (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)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ ephemeral_block_device {
+ device_name = (known after apply)
+ no_device = (known after apply)
+ virtual_name = (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)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}
# aws_key_pair.example will be created
+ resource "aws_key_pair" "example" {
+ fingerprint = (known after apply)
+ id = (known after apply)
+ key_name = "example-key"
+ public_key = ""
}
# aws_security_group.examplesg will be created
+ resource "aws_security_group" "examplesg" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = (known after apply)
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 22
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 22
},
]
+ name = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ vpc_id = (known after apply)
}
Plan: 3 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
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.
To apply these changes, run terraform apply
$ terraform apply
An 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.ec2_instance will be created
+ resource "aws_instance" "ec2_instance" {
+ ami = "ami-01ed306a12b7d1c96"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ 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 = (known after apply)
+ network_interface_id = (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)
+ security_groups = (known after apply)
+ source_dest_check = true
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "my-first-ec2-instance"
}
+ tenancy = (known after apply)
+ volume_tags = (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)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ ephemeral_block_device {
+ device_name = (known after apply)
+ no_device = (known after apply)
+ virtual_name = (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)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}
# aws_key_pair.example will be created
+ resource "aws_key_pair" "example" {
+ fingerprint = (known after apply)
+ id = (known after apply)
+ key_name = "example-key"
+ public_key = ""
}
# aws_security_group.examplesg will be created
+ resource "aws_security_group" "examplesg" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = (known after apply)
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 22
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 22
},
]
+ name = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ vpc_id = (known after apply)
}
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: yes <------------------------
aws_key_pair.example: Creating...
aws_security_group.examplesg: Creating...
aws_key_pair.example: Creation complete after 0s [id=example-key]
aws_security_group.examplesg: Creation complete after 2s [id=sg-0e1a943b062aa2315]
aws_instance.ec2_instance: Creating...
aws_instance.ec2_instance: Still creating... [10s elapsed]
aws_instance.ec2_instance: Still creating... [20s elapsed]
aws_instance.ec2_instance: Still creating... [30s elapsed]
aws_instance.ec2_instance: Creation complete after 33s [id=i-0d1ab0d8c1fc57f3d]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
What terraform is doing here is reading code and translating it to api calls to providers(aws in this case)
W00t you have deployed your first EC2 server using terraform
Go back to the EC2 console to verify your first deployed server
Let say after verification you realize that I need to give more meaningful tag to this server, so the rest of the code remain the same and you modified the tag parameter
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_key_pair.example: Refreshing state... [id=example-key]
aws_security_group.examplesg: Refreshing state... [id=sg-0e1a943b062aa2315]
aws_instance.ec2_instance: Refreshing state... [id=i-0d1ab0d8c1fc57f3d]
------------------------------------------------------------------------
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_instance.ec2_instance will be updated in-place
~ resource "aws_instance" "ec2_instance" {
ami = "ami-01ed306a12b7d1c96"
arn = "arn:aws:ec2:us-west-2:355622012945:instance/i-0d1ab0d8c1fc57f3d"
associate_public_ip_address = true
availability_zone = "us-west-2a"
cpu_core_count = 1
cpu_threads_per_core = 1
disable_api_termination = false
ebs_optimized = false
get_password_data = false
id = "i-0d1ab0d8c1fc57f3d"
instance_state = "running"
instance_type = "t2.micro"
ipv6_address_count = 0
ipv6_addresses = []
key_name = "example-key"
monitoring = false
primary_network_interface_id = "eni-0e5ddbe136b7d599d"
private_dns = "ip-172-31-28-131.us-west-2.compute.internal"
private_ip = "172.31.28.131"
public_dns = "ec2-54-185-56-146.us-west-2.compute.amazonaws.com"
public_ip = "54.185.56.146"
security_groups = [
"terraform-20191110052042521000000001",
]
source_dest_check = true
subnet_id = "subnet-3b929b42"
~ tags = {
~ "Name" = "my-first-ec2-instance" -> "my-webserver-instance"
}
tenancy = "default"
volume_tags = {}
vpc_security_group_ids = [
"sg-0e1a943b062aa2315",
]
credit_specification {
cpu_credits = "standard"
}
root_block_device {
delete_on_termination = false
encrypted = false
iops = 100
volume_id = "vol-0f48e9a8f42ac6dc4"
volume_size = 8
volume_type = "gp2"
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
------------------------------------------------------------------------
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 if we can think about it, how does terraform knows that there only change in the tag parameter and nothing else
Terraform keep track of all the resources it already created in .tfstate files, so its aware of the resources that already exist.
$ ls -la
total 28
drwxr-xr-x 4 prashant prashant 4096 Nov 9 21:25 .
drwxr-xr-x 31 prashant prashant 4096 Nov 9 21:14 ..
-rw-r--r-- 1 prashant prashant 1009 Nov 9 21:24 main.tf
drwxr-xr-x 3 prashant prashant 4096 Nov 9 21:15 .terraform
-rw-rw-r-- 1 prashant prashant 5348 Nov 9 21:21 terraform.tfstate
If you notice at the top it says “Refreshing Terraform state in-memory prior to plan…”
If I refresh my webbrowser after running terraform apply.
$ terraform apply
aws_key_pair.example: Refreshing state... [id=example-key]
aws_security_group.examplesg: Refreshing state... [id=sg-0e1a943b062aa2315]
aws_instance.ec2_instance: Refreshing state... [id=i-0d1ab0d8c1fc57f3d]
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_instance.ec2_instance will be updated in-place
~ resource "aws_instance" "ec2_instance" {
ami = "ami-01ed306a12b7d1c96"
arn = "arn:aws:ec2:us-west-2:355622012945:instance/i-0d1ab0d8c1fc57f3d"
associate_public_ip_address = true
availability_zone = "us-west-2a"
cpu_core_count = 1
cpu_threads_per_core = 1
disable_api_termination = false
ebs_optimized = false
get_password_data = false
id = "i-0d1ab0d8c1fc57f3d"
instance_state = "running"
instance_type = "t2.micro"
ipv6_address_count = 0
ipv6_addresses = []
key_name = "example-key"
monitoring = false
primary_network_interface_id = "eni-0e5ddbe136b7d599d"
private_dns = "ip-172-31-28-131.us-west-2.compute.internal"
private_ip = "172.31.28.131"
public_dns = "ec2-54-185-56-146.us-west-2.compute.amazonaws.com"
public_ip = "54.185.56.146"
security_groups = [
"terraform-20191110052042521000000001",
]
source_dest_check = true
subnet_id = "subnet-3b929b42"
~ tags = {
~ "Name" = "my-first-ec2-instance" -> "my-webserver-instance"
}
tenancy = "default"
volume_tags = {}
vpc_security_group_ids = [
"sg-0e1a943b062aa2315",
]
credit_specification {
cpu_credits = "standard"
}
root_block_device {
delete_on_termination = false
encrypted = false
iops = 100
volume_id = "vol-0f48e9a8f42ac6dc4"
volume_size = 8
volume_type = "gp2"
}
}
Plan: 0 to add, 1 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: yes
aws_instance.ec2_instance: Modifying... [id=i-0d1ab0d8c1fc57f3d]
aws_instance.ec2_instance: Modifications complete after 2s [id=i-0d1ab0d8c1fc57f3d]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
In most of the cases we are working in team where we want to share this code with rest of team members and the best way to share code is by using GIT
git add main.tf
git commit -m "first terraform EC2 instance"
vim .gitignore
git add .gitignore
git commit -m "Adding gitignore file for terraform repository"
Via .gitignore we are telling terraform to ignore(.terraform folder(temporary directory for terraform)and all *.tfstates file(as this file may contain secrets))
Thanks, everyone who was the part of my earlier journey
100 Days of DevOps
21 Days of Docker
Starting from November 10, I am starting a Program, 21 Days of AWS using Terraform and the main idea behind this is to spend at least one hour of every day for next 21 days in Sharing AWS knowledge using terraform and then share progress via