Monday 28 March 2022

Introduction to Terraform


 

Benefits of using Terraform:
  • all resources are defined in the code => 
  • From Terraform for Site24x7 | Online Help Site24x7:
    • Ensure error-free deployments
    • Handle heterogeneous cloud deployments for fault tolerance
    • Collaborate, obtain specific versions, and create your own environment
    • Spawn and remove testing environments easily
    • Improve deployment speeds
    • Build, version, or change infrastructure safely and efficiently
    • Trace and correct configuration-level issues

Terraform uses configuration files with extension .tf to define resources that need to be deployed.

General format of tf file content blocks is:
 
<block><parameters> {
    key1 = value1
    key2 = value2
}

block contains information about infrastructure platform and a set of resources within that platform that we want to create.
 
resource Block - A resource block declares a resource of a given type with a given local name. The name is used to refer to this resource from elsewhere in the same Terraform module, but has no significance outside of the scope of a module.

Example: Create a file on a local host where Terraform is installed. 


# https://www.terraform.io/language/expressions/references#filesystem-and-workspace-info
resource "local_file" "foo" {
    filename = "${path.cwd}/temp/foo.txt"
    content = "This is a text content of the foo file!"
}

resource is the reserved word and is the block name. Each resource is uniquely defined by the combination of the resource type and name. In our example:

  • "local_file" is a predefined string which denotes the resource type:
    • local - resource provider (by convention, resource type names start with their provider's preferred local name)
    • file - resource type
  • "foo" is the logical resource name; this is a local name that can be used from the same Terraform module

Inside the curly braces (resource block) are arguments for resource. They are specific for the given resource type. In this example, we are using local_file | Resources | hashicorp/local | Terraform Registry.

Here are some resource providers:
  • local
  • aws
  • azure
  • gcp
  • ...

Each resource type has its own set of arguments.


Example: Create Amazon Machine Image (AMI)

aws-ec2.tf:

resource "aws_instance" "my_webserver" {
    ami = "ami-0123456789abcdef" 
    instance_type = "t2.micro"
}


Example: S3

aws-s3.tf:

resource "aws_s3_bucket" "my_data" {
    bucket = "webserver-bucket-org-1234" 
    acl = "private"
}



Simple Terraform workflow:
  • write .tf file
  • (optional) run terraform fmt command which scans .tf files in current working directory and formats them in the canonical format. This improves the readability of configuration files.
  • run terraform init command. It:
    • checks the .tf config file
    • initializes the working directory containing .tf file
    • from .tf file it finds out what type of resource(s) we want to create
    • it initializes specified providers - it downloads plugin(s) required in order to work on these resources
    • is a safe command. It can be many times without affecting the infrastructure that is being deployed.
  • (optional) run terraform validate to validate configuration file syntax
  • run terraform plan command in order to review the execution plan 
  • run terraform apply command to apply changes
  • run terraform show command in order to see the changes made; it prints the current state of the infrastructure; it prints all resources and values of their attributes. Use -json flag to print it in JSON format.
  • run terraform destroy to destroy created resources
 

terraform init

 
Let's first initialize working directory where .tf file resides:
 
/demo-terraform/local_resources_demo$ ls -a
.  ..  create_local_file_demo.tf
 
/demo-terraform/local_resources_demo$ cat create_local_file_demo.tf
resource "local_file" "foo" {
    filename = "${path.cwd}/temp/foo.txt"
    content = "This is a text content of the foo file!"
}
 
/demo-terraform/local_resources_demo$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/local...
- Installing hashicorp/local v2.2.2...
- Installed hashicorp/local v2.2.2 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

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.

 
 
From the output above, we can see that Terraform used resource type information ("local_file") to deduct that it needs to download and install hashicorp/local plugin.

hashicorp/local is an example of the source address. It is the identifier used by Terraform to locate and download the plugin from the registry. The first value (e.g. hashicorp) represents the organisational namespace and the second one is type which is the name of provider (e.g. local, aws, google etc...). Source address can also optionally start with the hostname which is the name of the registry where plugin is located:

registry.terraform.io/hashicorp/local
 
By default, Terraform downloads the latest version of plugins but it is possible to specify desired version as well, if we want to prevent using the (latest) version which introduces breaking changes.
 
If we look at the directory, we can see that terraform init created one directory and one file:
 
/demo-terraform/local_resources_demo$ ls -a
.  ..  create_local_file_demo.tf  .terraform  .terraform.lock.hcl

 
 
.terraform directory contains downloaded plugins (executable binaries) for providers that are specified in .tf file. In our case, provider is named local:

/demo-terraform/local_resources_demo/.terraform$ ls -la providers/registry.terraform.io/hashicorp/local/2.2.2/linux_amd64/terraform-provider-local_v2.2.2_x5
-rwxr-xr-x 1 bojan bojan 13864960 Mar 28 13:27 providers/registry.terraform.io/hashicorp/local/2.2.2/linux_amd64/terraform-provider-local_v2.2.2_x5

.hcl file contains list of providers with their details. This is something like package-lock.json file in Node projects:

/demo-terraform/local_resources_demo$ cat .terraform.lock.hcl
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/local" {
  version = "2.2.2"
  hashes = [
    "h1:5UYW2wJ320IggrzLt8tLD6MowePqycWtH1b2RInHZkE=",
    "zh:027e4873c69da214e2fed131666d5de92089732a11d096b68257da54d30b6f9d",
    "zh:0ba2216e16cfb72538d76a4c4945b4567a76f7edbfef926b1c5a08d7bba2a043",
    "zh:1fee8f6aae1833c27caa96e156cf99a681b6f085e476d7e1b77d285e21d182c1",
    "zh:2e8a3e72e877003df1c390a231e0d8e827eba9f788606e643f8e061218750360",
    "zh:719008f9e262aa1523a6f9132adbe9eee93c648c2981f8359ce41a40e6425433",
    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
    "zh:9a70fdbe6ef955c4919a4519caca116f34c19c7ddedd77990fbe4f80fe66dc84",
    "zh:abc412423d670cbb6264827fa80e1ffdc4a74aff3f19ba6a239dd87b85b15bec",
    "zh:ae953a62c94d2a2a0822e5717fafc54e454af57bd6ed02cd301b9786765c1dd3",
    "zh:be0910bdf46698560f9e86f51a4ff795c62c02f8dc82b2b1dab77a0b3a93f61e",
    "zh:e58f9083b7971919b95f553227adaa7abe864fce976f0166cf4d65fc17257ff2",
    "zh:ff4f77cbdbb22cc98182821c7ef84dce16298ab0e997d5c7fae97247f7a4bcb0",
  ]
}

 

terraform validate


It requires terraform init to have been executed. If not, if configuration directory is not initialized, we might get the error like:

$ terraform validate
Error: Missing required provider
│ 
│ This configuration requires provider registry.terraform.io/hashicorp/local, but that provider isn't available. You may be able to
│ install it automatically by running:
│   terraform init

Error: Missing required provider
│ 
│ This configuration requires provider registry.terraform.io/hashicorp/tls, but that provider isn't available. You may be able to
│ install it automatically by running:
│   terraform init

terraform apply might fail in spite of terraform validate reporting no errors. This is because the validate command only carries out a general verification of the configuration. It validates the resource block and the argument syntax but not the values the arguments expect for a specific resource.


terraform plan


Before creating resource we can see the execution plan that will be carried out by Terraform. It is always good to review these actions before they get executed. terraform plan shows actions that will be used to create resources. The output is similar to the one of git diff:

/demo-terraform/local_resources_demo$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.foo will be created
  + resource "local_file" "foo" {
      + content              = "This is a text content of the foo file!"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "/home/bojan/dev/github/demo-terraform/local_resources_demo/temp/foo.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.


+ symbol denotes resources that will be created.
Resource is listed with all its attributes showing default values for those that we didn't specify explicitly in our .tf file.

terraform apply



This command creates resources but it also shows the plan first and asks us to confirm to go ahead with it:
 
/demo-terraform/local_resources_demo$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.foo will be created
  + resource "local_file" "foo" {
      + content              = "This is a text content of the foo file!"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "/home/bojan/dev/github/demo-terraform/local_resources_demo/temp/foo.txt"
      + id                   = (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

local_file.foo: Creating...
local_file.foo: Creation complete after 0s [id=db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
 
File is created indeed:
 
/demo-terraform/local_resources_demo$ cat ./temp/foo.txt
This is a text content of the foo file!
 
terraform apply also creates terraform.tfstate file which contains details of the created resource:
 
/demo-terraform/local_resources_demo$ cat terraform.tfstate
{
  "version": 4,
  "terraform_version": "1.1.7",
  "serial": 1,
  "lineage": "921cfa36-9ad5-9a2e-66db-d2ef62e7509b",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "local_file",
      "name": "foo",
      "provider": "provider[\"registry.terraform.io/hashicorp/local\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "content": "This is a text content of the foo file!",
            "content_base64": null,
            "directory_permission": "0777",
            "file_permission": "0777",
            "filename": "/home/bojan/dev/github/demo-terraform/local_resources_demo/temp/foo.txt",
            "id": "db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e",
            "sensitive_content": null,
            "source": null
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

 

If we want to prevent terraform plan|apply commands to print out the file content in their output in terminal, we can use sensitive_content argument (instead of content):
 
sensitive_content = "This is a text content of the foo file!"
 
Execution plan in terminal will now contain this line:
 
    + sensitive_content    = (sensitive value)
 
 

terraform show

 
 To see details of the resource created we can use terraform show command:
 
/demo-terraform/local_resources_demo$ terraform show
# local_file.foo:
resource "local_file" "foo" {
    content              = "This is a text content of the foo file!"
    directory_permission = "0777"
    file_permission      = "0777"
    filename             = "/home/bojan/dev/github/demo-terraform/local_resources_demo/temp/foo.txt"
    id                   = "db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e"
}

 
 

Updating already created resources

 
 
In the example above, we created a file with permissions 0777. Let's say we want to change this to permissions 0700.

We first need to edit our original .tf file:
 
/demo-terraform/local_resources_demo$ cat create_local_file_demo.tf
resource "local_file" "foo" {
    filename = "${path.cwd}/temp/foo.txt"
    content = "This is a text content of the foo file!"
    file_permission = "0700"
}
 

Let's review the changes:

/demo-terraform/local_resources_demo$ terraform plan
local_file.foo: Refreshing state... [id=db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # local_file.foo must be replaced
-/+ resource "local_file" "foo" {
      ~ file_permission      = "0777" -> "0700" # forces replacement
      ~ id                   = "db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e" -> (known after apply)
        # (3 unchanged attributes hidden)
    }


Plan: 1 to add, 0 to change, 1 to destroy.

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

-/+ means that resource will be deleted and then recreated. 
 
Line with # forces replacement comment shows the attribute whose change actually forces the resource replacement.
 
This type of infrastructure is called an immutable infrastructure: change of a single attribute requires re-creation of the entire resource.

demo-terraform/local_resources_demo$  terraform apply
local_file.foo: Refreshing state... [id=db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # local_file.foo must be replaced
-/+ resource "local_file" "foo" {
      ~ file_permission      = "0777" -> "0700" # forces replacement
      ~ id                   = "db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 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

local_file.foo: Destroying... [id=db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e]
local_file.foo: Destruction complete after 0s
local_file.foo: Creating...
local_file.foo: Creation complete after 0s [id=db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

 
Let's check new permissions on the file:

 /demo-terraform/local_resources_demo$ ls -la ./temp/
total 12
-rwx------ 1 bojan bojan   39 Mar 28 16:17 foo.txt


Deleting resources



To delete created resources we need to use terraform destroy command:


$ terraform destroy
local_file.foo: Refreshing state... [id=db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # local_file.foo will be destroyed
  - resource "local_file" "foo" {
     
- content              = "This is a text content of the foo file!" -> null
     
- directory_permission = "0777" -> null
     
- file_permission      = "0700" -> null
     
- filename             = "/home/bojan/dev/github/demo-terraform/local_resources_demo/temp/foo.txt" -> null
     
- id                   = "db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

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

local_file.foo: Destroying... [id=db5ca40b5588d44e9ec6c1b4005e11a6fd0c910e]
local_file.foo: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.


We can verify that temp folder does not contain the test file. Note that temp folder itself remained, although it was created in process of creating a file resource.



Configuration Directory


Configuration directory is the one which contains one or more configuration (.tf) files. Each configuration file can specify one or more resources (resource blocks required to provision the infrastructure). 

We can combine all resources from multiple (smaller) configuration file into a single (large) configuration file.  The common convention is to name that file main.tf

Configuration files can be organised as here:
  • main.tf - main config file containing definition of resources
  • variables.tf - contains variable declarations
  • outputs.tf - contains outputs from resources
  • provider.tf - contains provider definitions

It is possible to use multiple providers (e.g. local, random etc...) in the same configuration directory. 

Whenever we add a resource for a provider that has not been used so far in the configuration directory, we have to initialize the directory by running terraform init command.


Debugging (Logging)


To enable logging we need to set TF_LOG environment variable to a desired log level which can be one of following:
  • INFO
  • WARNING
  • ERROR
  • DEBUG
  • TRACE (most verbose)
Example:

$ export TF_LOG=TRACE

To persist log into a file we need to:
  • set TF_LOG_PATH environment variable to a path to the log file
  • make sure that TF_LOG is set
Example:

$ export TF_LOG_PATH=/tmp/terraform.log

To disable logging into a file:

$ unset TF_LOG_PATH

---





No comments: