An Introduction to Terraform

This articles outlines the basic Terraform concepts and explains Terraform command line options in greater depth!

As explained in previous article How to Install Terraform, Terraform is Infrastructure as a Code offering from Hashicorp. The Terraform code is written in Hashicorp Configuration Language (HCL).

Overall Workflow

The overall steps followed in order to use Terraform for creating Immutable Infrastructure are as follows:

  1. Install terraform as mentioned in How to Install Terraform
  2. Identify the resources needed to be created.
  3. Create configuration files in HCL language. General file extension for Terraform configuration files is .tf
  4. Execute terraform init command for the very first time. This will create Terraform workspace. Read the article below for complete detail on terraform initiation command and what it does.
  5. Run terraform plan command. This command will show you the planned infrastructure changes. Review the changes being made to infrastructure.
  6. Run terraform apply command. This command actually carries out real infrastructure changes.

Terraform Terminology

Lets now have in-depth explanation of what each of the steps above means with simple example. We would create simple AWS EC2 instance using Terraform. Before jumping into how to write Terraform scripts, lets familiarize ourselves with Terraform basic terminology:

Terraform Provider

The main function of Terraform provider is to understand API interaction and expose the resources. Terraform supports mulitple IssS/SaaS providers as listed on Terraform Documentation

Terraform Resource

Terraform resource is infrastructure object that can be managed with Terraform. Terraform resource belongs to Terraform providers. For example, EC2 is one of the resource that can be managed with Terraform.

Terraform Data Source

Data source do not create. modify or delete any infrastructure component. It is mainly used to get the information of existing infrastructure component. For example, there is an EC2 instance and you would need to use EC2 Instance Id at some other part of HCL script. The data source can be utilized in such cases to fetch instance id.

Terraform Variables

As the name suggest, variables are used to store values associated with them.

Terraform Output

When Terraform creates any infrastructure component, it returns the information related to component it created. Sch information can be accessed using Terraform Output.

Terraform Module

Multiple closely related resources together can be defined as Terraform module. For instance, AWS ASG consist of various resources like EC2 information, scale up and scale down policies, vpc information and so on. All these resources can be bundled as module and be used.

Terraform Directory Structure

After having gone through all the technical jargan, lets now proceed with writing actual Terraform scripts. The prerequisites for next part are as follows:

  1. Having Terraform installed as explained in previous article.
  2. Aws-cli cofigured to access AWS environment programatically.

Create a directory structure containing following empty files. This is general recommended structure of files.

Terraform
    |--- main.tf
    |--- variables.tf
    |--- terraform.tfvars
    |--- output.tf

The purpose of these files are as below:

  1. main.tf: This file should contain all the resources being created. Additionally, it could contain the call to Terraform modules.
  2. variables.tf: This file should contain all the supporting variables needed by main.tf.
  3. terraform.tfvars: This file should contain the Terraform variable values.
  4. output.tf: This file should contain all the output attributes about the infrastructure components/Terraform resources created.

Terraform HCL Scripts

Once these files are created, edit the file contents as below:

main.tf

    provider "aws" {
        region = var.aws_region
    } 
    resource "aws_instance" "example" {
        ami = var.ami
        instance_type = var.instance_type
    }
  • Note that the main.tf file is having provider defined. In this case, it is AWS as we are going to create ec2 instance.
  • The variables defined in variable.tf file can be accessed in any other terraform file using var keyword with dot operator.
  • Resource block follows resource “resource_type” “resource_name” {…} format
  • Resource name can be used to reference this resource in any Terraform file within same Terraform directory.

variables.tf

    variable "aws_region" {}

    variable "ami" {
        description = "Base AMI ID"
        type = string
     }

    variable "instance_type" {
        description = "The type of instance to start"
        type = string
    } 
  • Terraform variable definition follows variable “variable_name” {…} format.
  • The variable code block can be empty or can have attributes like description, type and/or default value.
  • Variables can be references as var.variable_name as shown in main.tf

terraform.tfvars

    aws_region = "us-east-1"
    ami = "ami-02354e95b39ca8dec"
    instance_type = "t2.micro"
  • This file contains the values of variables defined in variables.tf
  • The variable values here are defined to create Amazon Linux 2 te.micro instance.

output.tf

    output "id" {
        description = "Instance ID"
        value = aws_instance.example.id
    }

    output "arn" {
         description = "Instance ARN"
         value = aws_instance.example.arn
    }
  • It contains the output attributes.
  • Note how the output values are defined as resource_type.resource_name.attribute

Terraform Commands

terraform init

This command initializes Terraform backend, provider plugins etc. Execute this command from the terraform directory which contains the files created in previous steps. The output of this command would look similar to:

     $ terraform init
     
     Initializing the backend...
     
     Initializing provider plugins...
     
     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 = "~> 3.0"
     
     Terraform has been successfully initialized!
     
     You may now begin working with Terraform. Try running "terraform plan" to see
     any changes that are required for your infrastructure. All Terraform commands
     should now work.
     
     If you ever set or change modules or backend configuration for Terraform,
     rerun this command to reinitialize your working directory. If you forget, other
     commands will detect it and remind you to do so if necessary.

terraform validate

This command validates the terraform configuration files. Execute this command in same same directory as of above and you should see output similar to:

    Success! The configuration is valid.

terraform plan

This command provides the summary of infrastructure component changes those will be made. It actually doesn’t make any real change on infrastructure. Execute this command and you should see output similar to:

     $ 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.example will be created
       + resource "aws_instance" "example" {
           + ami                          = "ami-02354e95b39ca8dec"
           + 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)
           + 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)
           + 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)
             }
         }
     
     Plan: 1 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.

terraform apply

This command makes actual infrastructure changes. Once you execute this command, it will show you the list of changes and asks for confirmation. Type yes after reviewing changes and hit enter. This will create EC2 t2.miro instance. The command output also contains the output variable values at the end. Execute this command and you should see output similar to:

     $ 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.example will be created
       + resource "aws_instance" "example" {
           + ami                          = "ami-02354e95b39ca8dec"
           + 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)
           + 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)
           + 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)
             }
         }
     
     Plan: 1 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_instance.example: Creating...
     aws_instance.example: Still creating... [10s elapsed]
     aws_instance.example: Still creating... [20s elapsed]
     aws_instance.example: Still creating... [30s elapsed]
     aws_instance.example: Still creating... [40s elapsed]
     aws_instance.example: Creation complete after 43s [id=i-xxxxxxxxxxxxxxxx]
     
     Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
     
     Outputs:
     
     arn = arn:aws:ec2:us-east-1:xxxxxxxxxxxx:instance/i-xxxxxxxxxxxxxxxx
     id = i-xxxxxxxxxxxxxxxx

Congratulations! You just created first infrastructure component with Terraform. You can login to AWS Console and verify the same.

Please note, terraform apply command would create terraform.tfstate file. This file contains all the information of infrastructure component maintained by Terraform. In production environment, it is recommended to store tfstate files remotely.

updatedupdated2020-09-022020-09-02