Terraform Guide: Infrastructure as Code for the Cloud

Terraform by HashiCorp lets you define cloud infrastructure in declarative configuration files (HCL), plan changes before applying them, and manage state across teams. This guide covers the core workflow and essential patterns.

HCL Syntax

HashiCorp Configuration Language (HCL) uses blocks, arguments, and expressions:

# A block has a type, optional labels, and a body
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = var.instance_type

  tags = {
    Name = "web-${var.environment}"
  }
}

Provider Block

Providers are plugins that interact with APIs. Configure them at the top of your configuration:

terraform {
  required_version = ">= 1.7"

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

provider "aws" {
  region  = var.aws_region
  profile = "production"
}

Resources

Resources are the primary building blocks:

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "main-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "us-east-1a"
}

resource "aws_security_group" "web" {
  vpc_id = aws_vpc.main.id

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

Variables and Outputs

Variables parameterize your configuration:

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

variable "instance_type" {
  description = "EC2 instance size"
  type        = string
  default     = "t3.micro"
}

variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

Outputs expose values for other configurations or scripts:

output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_id" {
  value = aws_subnet.public.id
}

Core Workflow

# Initialize the working directory (download providers)
terraform init

# Preview the changes Terraform will make
terraform plan -out=tfplan

# Apply the planned changes
terraform apply tfplan

# Destroy all managed resources
terraform destroy

Always review the plan output before applying. In CI pipelines, save the plan file and apply that exact plan to avoid drift between plan and apply.

Remote State

Store state in a shared backend so teams can collaborate safely:

terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

The DynamoDB table provides state locking to prevent concurrent modifications.

Modules

Modules encapsulate reusable infrastructure patterns:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.1"

  name = "prod-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.11.0/24"]

  enable_nat_gateway = true
}

Write your own modules by organizing .tf files in a subdirectory and calling them with source = "./modules/my-module".

Workspaces

Workspaces let you manage multiple environments from the same configuration:

terraform workspace new staging
terraform workspace new prod
terraform workspace select staging
terraform workspace list

Reference the workspace name in your config:

resource "aws_instance" "web" {
  instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
}

Data Sources

Data sources let you query existing infrastructure:

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
}

Return to the DevOps hub or continue to Ansible Guide and Kubernetes Fundamentals.