In this article, you will learn how to use Terraform to create an EC2 instance and deploy a simple web application.
Before we look at the definition of Terraform, let’s first understand another key concept.
If you’d like to watch tutorial videos, here you go. Otherwise, please read on.
Infrastructure as Code
Infrastructure as Code (IAC) is the process of provisioning and managing infrastructure (VM/Storage/Network etc.) by means of using code / configuration files.
Terraform
Terraform is an open-source Infrastructure as code tool that can work with multiple cloud providers such as AWS, GCP, Azure etc.
As always the best way to learn is by getting your hands dirty. Let’s jump into a practical example.
Terraform Example
The goal is to be able to invoke a terraform call to provision an EC2 instance, install and configure a web server, and deploy a simple website on the instance.
Pre-requisites:
- Sign up for an AWS account (If you’re new to AWS, you will be able to use free-tier services)
- Default Virtual Private Cloud (VPC)
- Default subnet in the above VPC
I believe by default you will get the above AWS components when you create an AWS account for the first time. Please refer to this video if you need to understand VPC more.
High-level steps:
- Create an EC2 instance on AWS console where you will run terraform.
- Ensure you can ssh to the above EC2 instance by adding appropriate rules to security group.
- Install wget and unzip packages in the above EC2 instance. Download & Extract the latest terraform version from their website.
- Create an EC2 Admin Role
- Modify IAM Role of your EC2 instance
- Initialize terraform
- Code and Execute Terraform Module
Create an EC2 instance on AWS console where you will run terraform
- Login to your AWS console
- Navigate to EC2 service
- Click “Launch Instance”
- Select Amazon Linux 2 AMI
- Choose “t2.micro” instance type
- Configure Instance details
- You may click “Next” button for “Add Storage” (Step 4) and “Add Tags” (Step 5). No need to change anything on these steps.
- Create a new security group. You can change its name if you need to. Please note the ssh/http port rules added to the security group.
- Review your choices
- Create a key pair to use for ssh-ing into your instance. Make sure to download key pair on your laptop.
- Launch Instance.
- Your instance will be created and start running in a few minutes.
Ensure you can ssh to the above EC2 instance by adding appropriate rules to security group.
- Open a terminal or putty session on your laptop.
- change your directory to your key pair location, ssh into your instance (ec2-user) and sudo to root.
Install wget and unzip packages in the above EC2 instance. Download & Extract the latest terraform version from their website.
[root@ip-172-31-0-191 ~]# yum install wget unzip
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
amzn2-core | 3.7 kB 00:00:00
Package wget-1.14-18.amzn2.1.x86_64 already installed and latest version
Package unzip-6.0-21.amzn2.x86_64 already installed and latest version
Nothing to do
[root@ip-172-31-0-191 ~]#
[root@ip-172-31-0-191 ~]# wget https://releases.hashicorp.com/terraform/0.14.2/terraform_0.14.2_linux_amd64.zip
--2020-12-14 02:39:25-- https://releases.hashicorp.com/terraform/0.14.2/terraform_0.14.2_linux_amd64.zip
Resolving releases.hashicorp.com (releases.hashicorp.com)... 151.101.201.183, 2a04:4e42:3b::439
Connecting to releases.hashicorp.com (releases.hashicorp.com)|151.101.201.183|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 33609981 (32M) [application/zip]
Saving to: ‘terraform_0.14.2_linux_amd64.zip’
100%[=====================================================================================>] 33,609,981 46.8MB/s in 0.7s
2020-12-14 02:39:25 (46.8 MB/s) - ‘terraform_0.14.2_linux_amd64.zip’ saved [33609981/33609981]
[root@ip-172-31-0-191 ~]#
[root@ip-172-31-0-191 ~]# unzip terraform_0.14.2_linux_amd64.zip
Archive: terraform_0.14.2_linux_amd64.zip
inflating: terraform
[root@ip-172-31-0-191 ~]# mv terraform /usr/local/bin/
[root@ip-172-31-0-191 ~]#
[root@ip-172-31-0-191 ~]#
[root@ip-172-31-0-191 ~]# terraform -v
Terraform v0.14.2
Create an EC2 Admin Role
- Navigate to Identity and Access Management. Click on “Create Role”
- Select EC2 instance since the role will be assigned to EC2 instance
- Select “AdministratorAccess” and Click Next.
- Creating tag is optional. Click Next
- Click “Create Role”
Modify IAM Role of your EC2 instance
- Navigate back to EC2 service and Instance page. And Modify IAM role.
- Choose the role created in the prior step
Initialize Terraform
Switch back to your terminal session and run “terraform init” command. Terraform init command downloads necessary plugins based on the providers specified in the configuration file.
Code and Execute Terraform Module
Create a file called “webapp.tf” using vi editor and save the below code in it.
// this section declares which cloud provider we are going to use.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
// this section declares the query to get ami info out of AWS
data "aws_ami" "amazon_linux2_ami" {
most_recent = true
owners = ["amazon"]
filter {
name = "image-id"
values = ["ami-04d29b6f966df1537"]
}
}
// this section declares that we need a security group resource with its rules
resource "aws_security_group" "allow_webapp_traffic" {
name = "allow_webapp_traffic"
description = "Allow inbound traffic"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "allow_my_laptop"
}
}
// this section declares that we need an aws instance along with its configuration
resource "aws_instance" "webapp" {
ami = data.aws_ami.amazon_linux2_ami.id
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.allow_webapp_traffic.id]
key_name = "temp_key"
user_data = <<-EOF
#!/bin/bash
sudo yum update -y
sudo yum install httpd -y
sudo service httpd start
sudo chkconfig httpd on
echo "<html><h1>Your terraform deployment worked !!!</h1></html>" | sudo tee /var/www/html/index.html
hostname -f >> /var/www/html/index.html
EOF
tags = {
Name = "myfirsttfinstance"
}
}
output "instance_ip" {
value = aws_instance.webapp.public_ip
}
Run “terraform plan” command
This command will make it explicit the change to be made. It’s analogous to “git diff” or even sql execution plan. Before commiting your terraform configuration changes, you can run this and confirm that you’re making the changes correctly.
[root@ip-172-31-0-191 ~]# terraform plan
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.webapp will be created
+ resource "aws_instance" "webapp" {
+ ami = "ami-04d29b6f966df1537"
+ 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 = "temp_key"
+ 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)
+ tags = {
+ "Name" = "myfirsttfinstance"
}
+ tenancy = (known after apply)
+ user_data = "9c208e6d7a2edb2d4ec410b992a5cde5d69d59a5"
+ 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)
}
+ 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)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}
# aws_security_group.allow_webapp_traffic will be created
+ resource "aws_security_group" "allow_webapp_traffic" {
+ arn = (known after apply)
+ description = "Allow inbound traffic"
+ egress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "-1"
+ security_groups = []
+ self = false
+ to_port = 0
},
]
+ 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
},
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 80
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 80
},
]
+ name = "allow_webapp_traffic"
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags = {
+ "Name" = "allow_my_laptop"
}
+ vpc_id = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_ip = (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.
Run “terraform apply” command
This command will apply the changes in your configuration files to reach the desired state.
[root@ip-172-31-0-191 ~]# 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.webapp will be created
+ resource "aws_instance" "webapp" {
+ ami = "ami-04d29b6f966df1537"
+ 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 = "temp_key"
+ 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)
+ tags = {
+ "Name" = "myfirsttfinstance"
}
+ tenancy = (known after apply)
+ user_data = "9c208e6d7a2edb2d4ec410b992a5cde5d69d59a5"
+ 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)
}
+ 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)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}
# aws_security_group.allow_webapp_traffic will be created
+ resource "aws_security_group" "allow_webapp_traffic" {
+ arn = (known after apply)
+ description = "Allow inbound traffic"
+ egress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "-1"
+ security_groups = []
+ self = false
+ to_port = 0
},
]
+ 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
},
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 80
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 80
},
]
+ name = "allow_webapp_traffic"
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags = {
+ "Name" = "allow_my_laptop"
}
+ vpc_id = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_ip = [
+ (known after apply),
]
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_security_group.allow_webapp_traffic: Creating...
aws_security_group.allow_webapp_traffic: Creation complete after 1s [id=sg-0120ec8b63972d7b5]
aws_instance.webapp: Creating...
aws_instance.webapp: Still creating... [10s elapsed]
aws_instance.webapp: Still creating... [20s elapsed]
aws_instance.webapp: Still creating... [30s elapsed]
aws_instance.webapp: Creation complete after 33s [id=i-01d2d7c58df4decc0]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
instance_ip = "3.219.233.31"
Run “terraform output” command
This command prints the output variables defined in the configuration file.
[root@ip-172-31-0-191 ~]# terraform output
instance_ip = "3.219.233.31"
Run “terraform destroy” command
[root@ip-172-31-18-39 learn_terraform]# terraform destroy
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# aws_instance.webapp will be destroyed
- resource "aws_instance" "webapp" {
- ami = "ami-04d29b6f966df1537" -> null
- arn = "arn:aws:ec2:us-east-1:651299717062:instance/i-024f4d2ac530440a6" -> null
- associate_public_ip_address = true -> null
- availability_zone = "us-east-1b" -> null
- cpu_core_count = 1 -> null
- cpu_threads_per_core = 1 -> null
- disable_api_termination = false -> null
- ebs_optimized = false -> null
- get_password_data = false -> null
- hibernation = false -> null
- id = "i-024f4d2ac530440a6" -> null
- instance_state = "running" -> null
- instance_type = "t2.micro" -> null
- ipv6_address_count = 0 -> null
- ipv6_addresses = [] -> null
- key_name = "temp_key" -> null
- monitoring = false -> null
- primary_network_interface_id = "eni-0552525e63b5114ac" -> null
- private_dns = "ip-172-31-22-53.ec2.internal" -> null
- private_ip = "172.31.22.53" -> null
- public_dns = "ec2-100-27-33-82.compute-1.amazonaws.com" -> null
- public_ip = "100.27.33.82" -> null
- secondary_private_ips = [] -> null
- security_groups = [
- "allow_webapp_traffic",
] -> null
- source_dest_check = true -> null
- subnet_id = "subnet-0edbcce323dcc729d" -> null
- tags = {
- "Name" = "myfirsttfinstance"
} -> null
- tenancy = "default" -> null
- user_data = "9c208e6d7a2edb2d4ec410b992a5cde5d69d59a5" -> null
- volume_tags = {} -> null
- vpc_security_group_ids = [
- "sg-09cc1d86b1f7bf752",
] -> null
- credit_specification {
- cpu_credits = "standard" -> null
}
- enclave_options {
- enabled = false -> null
}
- metadata_options {
- http_endpoint = "enabled" -> null
- http_put_response_hop_limit = 1 -> null
- http_tokens = "optional" -> null
}
- root_block_device {
- delete_on_termination = true -> null
- device_name = "/dev/xvda" -> null
- encrypted = false -> null
- iops = 100 -> null
- throughput = 0 -> null
- volume_id = "vol-0bcea9b9d281eb9d5" -> null
- volume_size = 8 -> null
- volume_type = "gp2" -> null
}
}
# aws_security_group.allow_webapp_traffic will be destroyed
- resource "aws_security_group" "allow_webapp_traffic" {
- arn = "arn:aws:ec2:us-east-1:651299717062:security-group/sg-09cc1d86b1f7bf752" -> null
- description = "Allow inbound traffic" -> null
- egress = [
- {
- cidr_blocks = [
- "0.0.0.0/0",
]
- description = ""
- from_port = 0
- ipv6_cidr_blocks = []
- prefix_list_ids = []
- protocol = "-1"
- security_groups = []
- self = false
- to_port = 0
},
] -> null
- id = "sg-09cc1d86b1f7bf752" -> null
- 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
},
- {
- cidr_blocks = [
- "0.0.0.0/0",
]
- description = ""
- from_port = 80
- ipv6_cidr_blocks = []
- prefix_list_ids = []
- protocol = "tcp"
- security_groups = []
- self = false
- to_port = 80
},
] -> null
- name = "allow_webapp_traffic" -> null
- owner_id = "651299717062" -> null
- revoke_rules_on_delete = false -> null
- tags = {
- "Name" = "allow_my_laptop"
} -> null
- vpc_id = "vpc-f2cbd488" -> null
}
Plan: 0 to add, 0 to change, 2 to destroy.
Changes to Outputs:
- instance_ip = "100.27.33.82" -> null
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
aws_instance.webapp: Destroying... [id=i-024f4d2ac530440a6]
aws_instance.webapp: Still destroying... [id=i-024f4d2ac530440a6, 10s elapsed]
aws_instance.webapp: Still destroying... [id=i-024f4d2ac530440a6, 20s elapsed]
aws_instance.webapp: Still destroying... [id=i-024f4d2ac530440a6, 30s elapsed]
aws_instance.webapp: Destruction complete after 40s
aws_security_group.allow_webapp_traffic: Destroying... [id=sg-09cc1d86b1f7bf752]
aws_security_group.allow_webapp_traffic: Destruction complete after 0s