Skip to main content

Terraform

What is Terraform?

Terraform is a declarative (i.e. end state to be declared/defined) infrastructure as code tool to automate build, change, and versioning of infrastructure safely and efficiently with

  • low-level components: compute instances, storage, networking etc.
  • high-level components: DNS entries and SaaS features

Terraform vs. Ansible

Terraform mainly focuses on infrastructure provisioning while Ansible aims to configure that infrastructure.

Provisioning cloud resources

Approaches:

  • GUI
  • API/CLI
  • IaC (Infrastructure as Code)

Categories of IaC tools

  • Ad hoc scripts
  • Configuration management tools
  • Server templating tools
  • Orchestration tools
  • Provisioning tools

IaC Provisioning tools landscape

Cloud specific:

  • Cloud Foundation
  • Azure Resource Manager
  • Google Cloud Deployment Manager

Cloud agnostic:

  • Terraform
  • Pulumi

Common patterns of terraform usage

  • Terraform (Provisioning) + Ansible (Config Management)
  • Terraform (Provisioning) + Packer (Server Templating)
  • Terraform (Provisioning) + Kubernetes (Orchestration)

Terraform architecture

+------------------+
| Terraform State |
+------------------+ <-----> +----------------+ +--------------+ +-----+
| Terraform Core | <-----> | AWS Provider | <-----> | AWS |
+------------------+ ------> +----------------+ +--------------+ +-----+
| Terraform Config |
+------------------+

Terraform project structure

project
.terraform
modules
modules.json
...
provides
...
terraform.tfstate
.terraform.lock.hcl
main.tf

Providers

Terraform providers are plugins that enable interaction with a resource API.

terraform {
required_providers {
ibm = {
source = "IBM-Cloud/ibm"
version = ">= 1.12.0"
}
}
}

Modules

  • Modules are containers for multiple resources that are used together. A module consists of a collection of .tf and/or .tf.json files kept together in a directory
  • Modules are the main way to package and reuse resource configurations with terraform

Types of modules

  • Root module is a default module containing all .tf files in main working directory
  • Child module is a separate external module referred to from a .tf file

Module sources

  • Local paths
    module "consul" {
    source = "./consul"
    }
  • Terraform Registry
    module "consul" {
    source = "hashicorp/consul/aws"
    version = "0.1.0"
    }
  • GitHub
    module "consul" {
    source = "git@github.com:hashicorp/example.git"
    }
  • Bitbucket
  • Generic Git, Mercurial repositories
  • HTTP URLs
  • S3 buckets
  • GCS buckets
  • Modules in Package Sub-directories

Good modules

  • Raises the abstraction level from base resource types
  • Groups resources in a logical fashion
  • Exposes input variables to allow necessary customization + composition
  • Provides useful defaults
  • Returns outputs to make further integrations possible

State file

terraform.tfstate:

  • Terraform's representation of the world
  • JSON file containing information about every resource and data object
  • Contains sensitive info (e.g. database password), hence it should have restricted access and be encrypted
  • Can be stored locally/local_backend or remotely/remote_backend (in object store like S3 bucket, google cloud storage)

Terraform basic usage sequence

  • terraform init
    • inits project and downloads associated providers to working directory from terraform registry
  • terraform plan
    • compares desired state (terraform config) with actual state(terraform state).
  • terraform apply
    • apply terraform plan using providers
  • terraform destroy
    • destroys all resources and data

Variables and Outputs

Variable types

  • Input variables

    • var.<name>
    variable "instance_type" {
    description = "es2 instance type"
    type = string
    default = "t2.micro"
    }
  • Local variables

    • local.<name>
    locals {
    service_name = "My Service"
    owner = "Devops Directive"
    }
  • Output variables

    output "instance_ip_addr" {
    value = aws_instance.instance.public_ip
    }

Setting input variables

(in order of precedence, lowest -> highest)

  • Manual entry during plan/apply
  • Default value in declaration block
  • TF_VAR_\<name\> environment variables
  • terraform.tfvars file
  • *.auto.tfvars file
  • Command line -var or -var-file. E.g. -var="var_name=var_value"

Sensitive variables

  • Mark variables as sensitive:
    • sensitive = true
  • Pass to terraform apply with:
    • TF_VAR_variable
    • -var (retrieve from secret manager at runtime)
  • Can also use external secret store
    • E.g. AWS Secrets Manager

Types and validation

Primitive types

  • string
  • number
  • bool

Complex types

  • list(<TYP\E>)
  • set(<TYPE>)
  • map(<TYPE>)
  • object({<ATTR NAME> = <TYPE>, ...})
  • tuple([<TYPE>, ...])

Validation

  • Type checking happens automatically
  • Custom conditions can also be enforced

Expressions

  • Types and Values
  • Strings and Templates
  • References to Values
  • Operators - !, -, *, /, %, >, ==, etc.
  • Function Calls
  • Conditional Expressions - <CONDITION> ? <TRUE VAL> : <FALSE VAL>
  • For Expressions - [for o in var.list : o.id]
  • Splat Expressions - var.list[*].id
  • Dynamic Blocks
  • Type Constraints
  • Version Constraints

Functions

  • Numeric
  • String
  • Collection
  • Encoding
  • Filesystem
  • Data & Time
  • Hash & Crypto
  • IP Network
  • Type conversion

Meta arguments

depends_on

  • Terraform automatically generates dependency graph based on references
  • If two resources depends on each other (but not each others data), depends_on specifies that dependency to enforce ordering
    • e.g. if software on the instance needs access to S3, trying to create the aws_instance would fail if atempting to create it before the aws_aim_role_policy.
    resource "aws_instance" "example" {
    ami = "ami-1k2j123j"
    instance_type = "t2.micro"

    iam_instance_profile = aws_iam_instance_profile.example

    depends_on = [
    aws_iam_role_policy.example,
    ]
    }

count

  • Allows for creation of multiple resources/modules from a single block
  • Useful when the multiple necessary resources are nearly identical
resource "aws_instance" "server" {
count = 4 # create four similar EC2 instances

ami = "ami-a1b2c3d4"
instance_type = "t2.micro"

tags = {
Name = "Server ${count.index}"
}
}

for_each

  • Allows for creation of multiple resources/modules from a single block
  • Allows more control to customize each resource than count
locals {
subnet_ids = toset([
"subnet-abcdef",
"subnet-012345",
])
}

resource "aws_instance" "server" {
for_each = local.subnet_ids

ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = each.key # note: each.key and each.value are the same for a set

tags = {
Name = "Server ${each.key}"
}
}

lifecycle

  • A set of meta arguments to control terraform behavior for specific resources
  • create_before_destroy can help with zero downtime deployments
  • ignore_changes prevents terraform from trying to revert metadata being set elsewhere
  • prevent_destroy causes terraform to reject any plan which would destroy this resource
resource "azurerm_resource_group" "example" {
# ...

lifecycle {
create_before_destroy = true
}
}

Inputs + Meta arguments

  • input variables are passed in via module block
module "web_app" {
source = "./web-app-module"

# Input variables
bucket_name = "devops-directive-web-app-data"
domain = "example.com"
db_name = "mydb"
db_user = "foo"
db_pass = var.db_pass
}

Provisioners

Perform action on local or remote machine

  • file
  • local-exec
  • remote-exec
  • vendor
    • chef
    • puppet

Managing multiple environments

Approaches:

  • Workspaces: multiple named section within a single backend
~ terraform workspace list
default
* dev
staging
production
  • File structure: directory layout provides separation, modules provide reuse
_modules
module-1
main.tf
variables.tf
module-2
main.tf
variables.tf
dev
main.tf
terraform.tfvars
staging
main.tf
terraform.tfvars
production
main.tf
terraform.tfvars

Terraform workspaces

Pros:

  • Easy to get started
  • Convenient terraform.workspace expression
  • Minimize code duplication Cons:
  • Prone to human error
  • State stored within same backend
  • Codebase doesn't unambiguously show deployment configurations

File structure

  • Further separation (at logical component groups) useful for larger projects
    • Isolate things that change frequently from those which don't
  • References resources across configurations is possible using terraform_remote_state
_modules
compute-module
main.tf
variables.tf
networking-module
main.tf
variables.tf
dev
compute
main.tf
terraform.tfvars
networking
main.tf
terraform.tfvars
staging
compute
main.tf
terraform.tfvars
networking
main.tf
terraform.tfvars
production
compute
main.tf
terraform.tfvars
networking
main.tf
terraform.tfvars

Pros:

  • Isolation of backends
    • Improved security
    • Decreased potential for human error
  • Codebase fully represents deployed state Cons:
  • Multiple terraform apply required to provision environments
  • More code duplication, but can be minimized with modules

Terragrunt

  • Tool by gruntwork.io that provides utilities to make certain terraform use cases easier
    • Keeping terraform code DRY
    • Executing commands across multiple TG configs
    • Working with multiple cloud accounts

Testing terraform code

Static checks

  • Build-in
    • terraform fmt
      terraform fmt -check # checks if formatter would make chances
      terraform fmt # applies those changes
    • terraform validate
      terraform validate
    • terraform plan
      terraform plan
    • custom validation rules
      variable "short_variable" {
      type = string

      validation {
      condition = length(var.short_variable) < 4
      error_message = "The short_variable value must be less than 4 characters!"
      }
      }
  • External
    • tflint
    • checkov, tfsec, terrascan, terraform-compliance, snyk
    • Terraform Sentinal (enterprise only)
  • Manual testing
    • Follow the standard Terraform workflow, running terraform init, terraform apply, and terraform destroy to test your configuration manually.
  • Automated testing
    • Automate the manual testing steps using a shell script or a more robust method, such as utilizing a testing framework like TerraTest with Go to write complex tests and make assertions about your infrastructure.

Developer workflows

General workflow

  • Write/update code
  • Run changes locally (for development environment)
  • Create pull request
  • Run tests via CI
  • Deploy to staging via CD (on merge to main)
  • Deploy to production via CD (on release)

Multi account/project

  • Simplify IAM (Identity and Access Management) policies for enforcing controls for different environments (and remote TF backends)
  • Isolate environments to protect minimize blast radius
  • Reduce naming conflicts for resources
  • Con: Adds complexity to TF config (but still worth it + tooling can help)

Additional tools

  • Terragrunt
    • Minimizes code repetition
    • Enables multi-account separation (improved isolation/security)
  • Cloud Nuke
    • Easy cleanup of cloud resources
  • Makefiles
    • Prevent human error

Potential pitfalls

  • Name changes when refactoring may cause destroy/recreate resources
  • Sensitive data in terraform state file
  • Cloud timeouts on command execution
  • Naming conflicts
  • Forgetting to destroy test-infra
  • Uni-directional version upgrades with different terraform versions
    • terraform state files can be associated with the version of terraform binaries
    • make sure to use the same terraform version in e.g. developers local environment, CI/CD system etc.
  • Multiple ways to accomplish same configuration
  • Some Params are immutable
  • Out of band changes, e.g. without terraform manually or other tools rather than terraform

Commands

# INIT
terraform init

# PLAN
terraform plan

# APPLY
terraform apply

# DESTROY
terraform destroy

# WORKSPACE
terraform workspace list
terraform workspace new <name-of-workspace>

References