AAtsushi's Blog
DevOps

Book Notes: Practical Terraform

→ 日本語版を読む
  • Overview
    • Versioning
    • Commands
    • Symbols
    • Files
    • variable
    • Variable types
    • local
    • output
    • data
    • provider
    • Lists []
    • Built-in functions
    • Modules
    • Ternary operator
    • count
    • random provider
    • dynamic and for_each
    • Best practices
    • Structuring and resource reference patterns (Chapters 22–23)

Overview

  • Notes on key points from "Practical Terraform" for future reference
  • Mainly covers Chapters 1–3 and Chapter 17 onward
  • No coverage of for loops; for_each is also not covered in depth and needs separate study
  • Official documentation: Overview - Configuration Language | Terraform by HashiCorp

Versioning

Terraform 0.12, released in May 2019, is not backwards compatible with versions 0.11 and earlier — be careful.

Commands

  • terraform init
  • terraform plan
  • terraform apply
  • terraform destroy
  • terraform fmt -recursive
  • terraform validate

Symbols

  • +: resource being added
  • -: resource being deleted
  • -/+: resource being deleted and re-created

Files

  • tfstate file: Running terraform plan at least once creates a terraform.tfstate file. It records the current state of resources. If there is a diff between the tfstate file and HCL code, only the diff is changed.

variable

  • Variables come in two types: variable and local
  • You can override variables at runtime: terraform apply -var 'variable_name'
  • Default values can also be set
variable "example_instance_type" {
  default = "t3.micro"
}

resource "aws_instance" "example" {
  ami = "ami0c3fd0f5d33134a76"
  instance_type = var.example_instance_type
}

Variable Types

  • 7 types total
  • string, number, bool, list, tuple, map, object
  • Examples of list, object, and tuple:
variable "ports" {
  type = list(number)
}
variable "person" {
  type = object({ name=string, age=number})
}
variable "person" {
  type = tuple([string, number])
}

local

  • As mentioned above, variables come in two types: variable and local
  • Cannot be overridden at runtime
  • No default value setting
locals {
  example_instance_type = "t3.micro"
}

resource "aws_instance" "example" {
  ami = "ami0c3fd0f5d33134a76"
  instance_type = local.example_instance_type
}

output

  • Defines values output after terraform apply runs
  • In the example below, example_instance_id = i02bd77505ab68856f is printed to the display
resource "aws_instance" "example" {
  ami = "ami0c3fd0f5d33134a76"
  instance_type = "t3.micro"
}

output "example_instance_id" {
  value = aws_instance.example.id
}

data

  • References external data such as AWS AMI images
  • The example below filters by image name and state (available), and uses most_recent to get the latest AMI
data "aws_ami" "recent_amazon_linux_2" {
  most_recent = true
  owners = ["amazon"]

  filter {
    name = "name"
    values = ["amzn2-ami-hvm-2.0.????????-x86_64-gp2"]
  }

  filter {
    name = "state"
    values = ["available"]
  }
}

resource "aws_instance" "example" {
  ami= data.aws_ami.recent_amazon_linux_2.image_id
  instance_type = "t3.micro"
}

provider

  • Abstracts API differences between AWS, GCP, etc.
  • terraform init downloads the provider binary files
  • Terraform can detect providers implicitly, but explicit declaration is preferred for manageability
  • Default region and other settings can be specified
provider "aws" {
  region = "apnortheast1"
}

Lists []

  • [ ] can be used to pass a list
  • In the example below, two security groups (ingress and egress) are passed as a list via [aws_security_group.example_ec2.id]
resource "aws_security_group" "example_ec2" {
  name = "example-ec2

  ingress {
    from_port = 80
    to_port = 80
    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"]
  }
}

resource "aws_instance" "example"{
  ami = "ami0c3fd0f5d33134a76"
  instance_type = "t3.micro"
  vpc_security_group_ids = [aws_security_group.example_ec2.id]
}

Built-in Functions

  • Common operations like string manipulation are provided as built-in functions
  • file('file_path') can be used to read an external file

Modules

  • Placed in a directory
module "web_server" {
  source        = "./http_server"
  instance_type = "t3.micro"
}

Create main.tf in the ./http_server directory and implement the following code. The module takes instance_type as an argument passed from the caller and creates an instance.

variable "instance_type" {}

resource "aws_instance" "default" {
  ami                    = "ami-0c3fd0f5d33134a76"
  instance_type          = var.instance_type
}

Ternary Operator

  • Conditional branching can be implemented with the ternary operator.
  • In the example below, running terraform plan -var 'env=prod' sets instance_type to m5.large. Any value other than prod sets it to t3.micro.
variable "env" {}

resource "aws_instance" "example" {
  ami           = "ami-0c3fd0f5d33134a76"
  instance_type = var.env == "prod" ? "m5.large" : "t3.micro"
}

count

  • Can be used to create multiple resources at once.
  • With count = 3, count.index takes values {0, 1, 2}.
  • The example below creates 3 VPCs (10.0.0.0/16, 10.1.0.0/16, 10.2.0.0/16).
resource "aws_vpc" "examples" {
  count      = 3
  cidr_block = "10.${count.index}.0.0/16"
}

random Provider

  • Automatically generates random strings
  • Can specify string length and whether special characters are included
  • The example below generates a 32-character random string with no special characters
  • The result is returned in random_string.password.result
provider "random" {}

resource "random_string" "password" {
  length  = 32
  special = false
}

dynamic and for_each

  • dynamic enables dynamic resource generation
  • In the example below, the value set in for_each is extracted as ingress.value, i.e., <dynamic_block_name>.value
variable "ports" {
  type = list(number)
}

resource "aws_security_group" "default" {
  name = "simple-sg"

  dynamic "ingress" {
    for_each = var.ports

    content {
      from_port   = each.value
      to_port     = ins.value
      cidr_blocks = ["0.0.0.0/0"]
      protocol    = "tcp"
    }
  }
}

Best Practices

  • Pin the Terraform version
  • Pin provider versions as well (public cloud evolves quickly, causing environment discrepancies)
  • Suppress delete operations
  • But: removing a resource definition from a .tf file and running terraform apply will delete the resource (※ This is critical — be very careful!)
  • Always format code
  • Always validate code (additional options needed to cover subdirectories)
  • Use TFLint to detect invalid code (tflint --deep)
  • Understand implicit dependencies and define them explicitly with depends_on

Structuring and Resource Reference Patterns (Chapters 22–23)

Skipping as it's time-consuming, but it's important — revisit if forgotten.