Home Terraform Terraform Tutorial For Beginners | AWS

Terraform Tutorial For Beginners | AWS

by jmbharathram

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:

  1. Create an EC2 instance on AWS console where you will run terraform.
  2. Ensure you can ssh to the above EC2 instance by adding appropriate rules to security group.
  3. Install wget and unzip packages in the above EC2 instance. Download & Extract the latest terraform version from their website.
  4. Create an EC2 Admin Role
  5. Modify IAM Role of your EC2 instance
  6. Initialize terraform
  7. Code and Execute Terraform Module

Create an EC2 instance on AWS console where you will run terraform

  1. Login to your AWS console
  2. Navigate to EC2 service
  3. 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.

  1. Open a terminal or putty session on your laptop.
  2. 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