Search…

Terraform fundamentals

In this series (10 parts)
  1. Introduction to Infrastructure as Code
  2. Terraform fundamentals
  3. Terraform state management
  4. Terraform modules
  5. Terraform in CI/CD
  6. Ansible fundamentals
  7. Ansible roles and best practices
  8. Packer for machine images
  9. CloudFormation and CDK
  10. Managing drift and compliance

Terraform lets you define infrastructure in declarative config files, track state, and apply changes predictably. It works across cloud providers through a plugin system called providers.

Providers

A provider is a plugin that talks to an API. AWS, Azure, GCP, Cloudflare. Each provider exposes resources and data sources for that platform.

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

provider "aws" {
  region = "us-east-1"
}

Running terraform init downloads the provider binary. Pin your versions. Unpinned providers will break builds when a new major version ships.

Resources

Resources are the core of every config. Each block declares one infrastructure object.

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

The first string is the resource type. The second is the local name. Together they form the address aws_instance.web.

Data sources

Data sources read existing infrastructure. They never create anything.

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

Reference the result with data.aws_ami.ubuntu.id.

Variables, outputs, and locals

Variables parameterize configs. Outputs expose values after apply. Locals are computed constants you reuse within a module.

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

output "instance_ip" {
  value = aws_instance.web.public_ip
}

locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

Pass variable values via CLI flags, .tfvars files, or environment variables prefixed with TF_VAR_.

State

Terraform tracks real infrastructure in a state file. By default it writes terraform.tfstate locally. For teams, use a remote backend like S3 with locking.

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

Never edit state by hand. Never commit it to version control.

The plan/apply/destroy workflow

graph LR
  A[Write Config] --> B[terraform init]
  B --> C[terraform plan]
  C --> D[Review Changes]
  D --> E[terraform apply]
  E --> F[terraform destroy]

terraform plan shows what will change without touching anything. terraform apply executes the plan. terraform destroy removes every resource in state. Always run plan first and read the output carefully.

Complete example: VPC, subnet, and EC2

This config creates a VPC with a public subnet and launches an EC2 instance into it.

terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
  required_version = ">= 1.5.0"
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "instance_type" {
  type    = string
  default = "t3.micro"
}

variable "project" {
  type    = string
  default = "demo"
}

locals {
  tags = { Project = var.project, ManagedBy = "terraform" }
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = merge(local.tags, { Name = "${var.project}-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       = "${var.aws_region}a"
  tags = merge(local.tags, { Name = "${var.project}-public" })
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags   = merge(local.tags, { Name = "${var.project}-igw" })
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = merge(local.tags, { Name = "${var.project}-public-rt" })
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_security_group" "web" {
  name   = "${var.project}-web-sg"
  vpc_id = aws_vpc.main.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  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"]
  }
  tags = local.tags
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]
  tags = merge(local.tags, { Name = "${var.project}-web" })
}

output "vpc_id" { value = aws_vpc.main.id }
output "instance_id" { value = aws_instance.web.id }
output "instance_public_ip" { value = aws_instance.web.public_ip }

Save this as main.tf, run terraform init, then terraform plan. Review every line of the plan before applying.

What comes next

This config works but has gaps. State lives on your local disk. The security group allows SSH from everywhere. There is no module structure. Article 3 covers Terraform modules, remote state patterns, and strategies for organizing configs across teams.

Start typing to search across all content
navigate Enter open Esc close