Skip to main content

Terraform basics

Use the short tf

Add to ~/.zshrc:

# alias
alias tf="terraform"

# or function
tf() {
terraform "$@"
}

Cli


# format the configuration, by default only the current directly is processed.
tf fmt -recursive -check

# validate the configuration
tf validate

# inspect the current state
tf show

# list all the resources in your project's state
tf state list

# reads and output variables from the state file
tf output

# output the `bucket-name`
tf output -raw bucket-name

Providers

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}

required_version = ">= 1.2.0"
}
  • source: hostname + namespace + provider type
  • Terraform installs providers from Terraform Registry by default
    • The full name is: registry.terraform.io/hashicorp/aws

Versions

# exact version 3.1.0
version = "3.1.0"

# the latest version that is at greater than 4.5.9
version = ">= 4.5.0"

# the latest version of V1.x, but not v2 and beyond
version = "~> 1.2"
  • If you have the lock file .terraform.lock.hcl, tf init will install the version specified in the lock file.
  • If TF did not find a lock file, it would download the latest versions of the providers that fulfils the version constraints you defined in the required_providers block.
  • If the versions defined in the lock files' provider block do not match the versions defined in your configuration's required_providers block, Terraform will prompt you to re-initialize your configuration using the -upgrade flag.
tf init --upgrade
  • The init command will download and install the providers defined in the configuraion.
  • The --upgrade flag will upgrade all providers to the latest version consistent with the version constraints specified in your configuration
  • You should NEVER directly modify the lock file.

Resource

resource "aws_instance" "app_server" {
ami = "ami-0cc51e967b1cbe471"
instance_type = "t2.micro"

tags = {
Name = var.instance_name
}
}
  • Resource type (aws_instance) and resource name (app_server) form a unique ID for the resource.

Terraform Cloud

terraform {
cloud {
organization = "daniel-tf-aws"
workspaces {
name = "learn-terraform-aws"
}
}
}
  • Need to create an account in Terraform Cloud and create an organization.

tf login
  • The token will be saved to ~/.terraform.d/credentials.tfrc.json
  • Execution Mode:
    • Remote: Your plans and applies occur on HCP Terraform's infrastructure. You and your team have the ability to review and collaborate on runs within the app.
      • Need to configure AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as Environment variables in the workspace
    • Local: Your plans and applies occur on machines you control. HCP Terraform is only used to store and synchronize state.

Plan

tf plan -out tfplan

tf show tfplan

tf show -json tfplan | jq > tfplan.json

tf '.configuration.provider_config' tfplan.json

tf apply tfplan

tf plan -destroy -out tfplan-destory

tf destroy
  • Generate a saved plan with the out flag.
  • Use the show command to print out the saved plan.
  • Pass the -json flag to show command to convert the saved plan into JSON; You can pass it to jq to format it, and save the output into a new file.
  • You can create a destroy plan with the destroy flag.
  • The destroy command is a shortcut that creates a destroy plan and then waits for you to approve it.
tf plan --replace="aws_instance.example"
tf apply --replace="aws_instance.example"
  • -replace flag:
    • you can use it on plan and apply operations
    • to safely recreate resources in your environment even if you have not edited the configuration
    • it allows you to target specific resources and avoid destroying all the resources in your workspace just to fix one of them
tf plan --refresh-only
tf apply --refresh-only
  • The -refresh-only mode makes it safer to check Terraform state against real infrastructure by letting you review proposed changes to the state file.
  • If you suspect that your infrastructure configuration changed outside of the Terraform workflow, you can use -refresh-only flag to inspect what the changes to your state file would be.
  • the -refresh-only flag for the apply operation does not attempt to modify your infrastructure to match your Terraform configuration -- it only gives you the option to review and track the drift in your state file.

NOTES:

  • Although you mark an input variable as sensitive, Terraform still stores the value in plaintext in the plan file. Never commit a plan file to version control, whether as a binary or in JSON format.
  • When you apply a saved plan file, Terraform will not prompt you for approval and instead immediately execute the changes. This workflow is primarily used in automation.
  • Never commit .tfvars files to version control.

Plan output

  • +: the resource will be created
  • ~: the resource will be updated in place
  • -/+: the resource will be destroyed and recreated

Apply

Apply process

When you approve the plan and apply the configuration, Terraform will:

  1. Lock your workspace's state, so that no other instances of Terraform will attempt to modify your state or apply changes to your resources. If Terraform detects an existing lock file (.terraform.tfstate.lock.info), it will report an error and exit.
  2. Create a plan, and wait for you to approve it. Alternatively, you can provide a saved plan created with the terraform plan command, in which case Terraform will not prompt for approval.
  3. Execute the steps defined in the plan using the providers you installed when you initialized your configuration. Terraform executes steps in parallel when possible, and sequentially when one resource depends on another.
  4. Update your workspace's state with a snapshot of the new state of your resources.
  5. Unlock your workspace's state.
  6. Report the changes it made, as well as any output values defined in your configuration.

Error

When Terraform encounters an error during an apply step, it will:

  1. Log the error and report it to the console.
  2. Update the state file with any changes to your resources.
  3. Unlock the state file.
  4. Exit.

Your infrastructure may be in an invalid state after a Terraform apply step errors out. Terraform does not support automatically rolling back a partially-completed apply. After you resolve the error, you must apply your configuration again to update your infrastructure to the desired state.

Common reasons for apply errors include:

  1. A change to a resource outside of Terraform's control.
  2. Networking or other transient errors.
  3. An expected error from the upstream API, such as a duplicate resource name or reaching a resource limit.
  4. An unexpected error from the upstream API, such as an internal server error.
  5. A bug in the Terraform provider code, or Terraform itself.

Example: Your existing configuration has created a S3 bucket, now you update the configuration to add an object to the bucket. You created the plan, but then deleted the s3 bucket from the AWS console, then you try to apply the plan you created before deleting the bucket. You need to create a new plan and apply it.

Variables

Variable types

  • string

  • number

  • bool

  • list: a sequence of values of the same type, example: list(string)

    • slice(): get a subnet of a list
  • map: a lookup table, matching keys to values, all of the same type, example: map(string)

  • set: an unordered collection of unique values, all of the same type, example: set(string)

  • tuple: a fixed-length sequence of values of specified types

  • object: a lookup table, matching a fixed set of keys to values of specified types

variable "aws_region" {
description = "AWS region"
type = string
default = "us-west-2"
}

variable "resource_tags" {
description = "Tags to set for all resources"
type = map(string)
default = {
project = "project-alpha",
environment = "dev"
}
}
var.resource_tags["project"]
var.resource_tags["environment"]
  • Terraform automatically loads all files in the current directory with the exact name terraform.tfvars, or matching *.auto.tfvars. You can use the -var-file flag to specify other files by name.

Interpolate variables in strings

module "lb_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/web"
version = "4.17.0"

name = "lb-${var.resource_tags["project"]}-${var.resource_tags["environment"]}"
}

Validation

variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

validation {
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

Sensitive variables

variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true
}
  • How to set value for sensitive variables:

    • Option 1: provide the sensitive variable values when you run tf apply
    • Option 2: set the values in secret.tfvars
    • Option 2: use environment variables matching the pattern TF_VAR_<VARIABLE_NAME>, for example: TF_VAR_db_username
  • Store:

    • Redacted in command output and log files
    • Plain text in state files

Output

output "lb_url" {
description = "The URL of the load balancer"
value = "http://${module.elb_http.elb_dns_name}/"
}

output "db_password" {
description = "The password for the database"
value = var.db_password
sensitive = true
}
  • Terraform stores output values in the configuration's state file.
  • You can use string interpolation in the output
  • You can mark an output variable as sensitive
    • Terraform will redact sensitive outputs when planing, applying and destroying the configuration, and query all outputs
    • The sensitive outputs will be in plain text when you query a specific output by name, query all outputs in JSON format, or when you use outputs from a child module in your root module.
❯ tf output                                                                   
db_password = <sensitive>
db_username = <sensitive>
lb_url = "http://lb-bNV-project-alpha-dev-447970585.us-east-1.elb.amazonaws.com/"
web_server_count = 4

❯ tf output db_password
"notasecurepassword"

❯ tf output -raw db_password
notasecurepassword

❯ terraform output -json
  • Terraform wraps string outputs in quotes by default, use -raw flag to get the output without quotes.
  • Use the -json format to get machine-readable format for automation. Remember sensitive outputs will be in plain text because it assumes that an automation tool will use it.

Expressions

resource "aws_instance" "ubuntu" {
count = var.high_availability == true ? 3 : 1
associate_public_ip_address = count.index == 0 ? true : false

tags = local.common_tags
}

resource "aws_elb" "learn" {
instances = aws_instance.ubuntu[*].id
}

output "first_tags" {
value = aws_instance.ubuntu[0].tags
}

output "private_addresses" {
value = aws_instance.ubuntu[*].private_dns
description = "The private IP addresses of the instances"
}
  • ? :: condition expression
  • *: splat expression, returns an array

Workspace

❯ tf workspace list
* default


❯ tf workspace new dev
Created and switched to workspace "dev"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

❯ tf workspace select dev
Switched to workspace "dev".

HCP Terraform workspaces behave differently from Terraform CLI workspaces. Terraform CLI workspaces allow multiple state files to exist within a single directory, letting you use one configuration for multiple environments. HCP Terraform workspaces contain everything needed to manage a given set of infrastructure, and function like separate working directories.

Logging

export TF_LOG_CORE=TRACE
export TF_LOG_PROVIDER=TRACE
export TF_LOG_PATH=logs.txt
  • Set the above environment variables to enable logging for Terraform core and providers.

HCP Terraform

Migrate State

  • Add Cloud configuration
  • tf init
  • Remove local state files
  • tf apply
terraform {
cloud {
organization = "daniel-tf-aws"
workspaces {
name = "learn-state-migration"
}
}
}

tf init
rm -f terraform.tfstate*
tf apply