Terraform Tutorial — Blocks, Arguments, and Expressions

3 minute read

Every Terraform configuration is built from blocks. Understanding block anatomy is the single most important thing you need to learn in HCL.


What is a Block?

A block is a container for configuration. It has a type, zero or more labels, and a body enclosed in curly braces.

block_type "label_1" "label_2" {
  argument1 = "value1"
  argument2 = 42
}

Think of blocks as building blocks (literally). Each block defines one piece of your infrastructure puzzle.


Block Types in Terraform

Terraform has eight core block types:

Block Type Purpose Labels
resource Create infrastructure Type + name
data Read existing infrastructure Type + name
variable Declare an input variable Name
output Declare an output value Name
provider Configure a cloud provider Provider name
module Call a reusable module Name
terraform Global settings (versions, backend) None
locals Define local values None

Resource Block

The most common block. It tells Terraform to create a piece of infrastructure:

resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-unique-bucket-2024"

  tags = {
    Environment = "dev"
    Team        = "backend"
  }
}
  • Block type: resource
  • First label: "aws_s3_bucket" (resource type — provider + resource)
  • Second label: "my_bucket" (local name — how you reference it elsewhere)
  • Body: Arguments that configure the resource

Variable Block

Declares an input that can be set when running Terraform:

variable "region" {
  type        = string
  default     = "us-east-1"
  description = "AWS region for all resources"
}

Output Block

Declares a value to display after terraform apply:

output "bucket_arn" {
  value       = aws_s3_bucket.my_bucket.arn
  description = "The ARN of the S3 bucket"
}

The terraform Block

Global settings — version constraints and backend configuration:

terraform {
  required_version = ">= 1.7.0"

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

Arguments

Arguments are key-value pairs inside a block body. The key is always on the left, the = sign separates them, and the value is on the right.

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"    # string
  instance_type = "t3.micro"                  # string
  count         = 3                           # number
  monitoring    = true                        # boolean
}

Argument Types

Type Example
String "us-east-1"
Number 8080, 3.14
Boolean true, false
List ["us-east-1a", "us-east-1b"]
Map { Name = "web", Env = "prod" }

Expressions

Expressions compute values. They can be simple literals or complex combinations of references, operators, and functions.

Literal Values

name   = "my-bucket"          # string literal
count  = 3                     # number literal
enable = true                  # boolean literal

References

Reference other resources, variables, and data sources:

# Reference a variable
region = var.region

# Reference a resource attribute
vpc_id = aws_vpc.main.id

# Reference a data source attribute
ami = data.aws_ami.ubuntu.id

# Reference a local value
name = local.project_name

# Reference a module output
subnet_id = module.vpc.public_subnet_ids[0]

Operators

# Arithmetic
total = var.base_count * 2

# Comparison
is_prod = var.environment == "production"

# Logical
enable_monitoring = var.environment == "production" && var.monitoring_enabled

Comments

HCL supports three comment styles:

# This is a single-line comment (most common)

// This is also a single-line comment (C-style)

/*
  This is a multi-line comment.
  Use it for longer explanations.
*/

Most Terraform code uses # for comments. Use /* */ when you need to comment out a large block of code temporarily.


Nested Blocks

Some blocks contain other blocks inside them:

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP traffic"
  vpc_id      = aws_vpc.main.id

  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"]
  }
}

The ingress and egress blocks are nested blocks inside the resource block. They don’t use = — they’re block definitions, not arguments.


What’s Next?

You understand blocks and arguments — the skeleton of every .tf file. Next, let’s learn about string interpolation and the type system that makes HCL powerful.


← HCL Syntax Hub String Interpolation & Types →