Skip to main content

Terraform Fundamentals

What is Terraform?​

Terraform is an open-source Infrastructure as Code (IaC) tool developed by HashiCorp. It allows you to define, provision, and manage cloud infrastructure using declarative configuration files written in HCL (HashiCorp Configuration Language).

Key Features:

  • Declarative syntax: Describe desired state, not implementation steps
  • Multi-cloud support: AWS, Azure, GCP, and 1000+ providers
  • State management: Tracks resource state locally or remotely
  • Idempotent: Safe to run multiple times
  • Version control friendly: Configuration as code

Infrastructure as Code (IaC) Concepts​

IaC treats infrastructure management like software development:

  • Reproducibility: Create identical environments consistently
  • Version control: Track infrastructure changes in Git
  • Code review: Peer review infrastructure changes via PRs
  • Automation: Reduce manual operations and human error
  • Scalability: Manage hundreds of resources with code

IaC Categories:

  • Declarative (Terraform, CloudFormation): Describe desired state
  • Imperative (Ansible, Chef): Define step-by-step actions

HCL Basics​

HCL is human-readable and designed for infrastructure automation.

Basic Syntax​

# Comments start with #

# Block syntax: block_type "label" "name" { content }
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}

# String literals
variable "region" {
type = string
default = "us-east-1"
}

# Lists
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}

# Maps
variable "tags" {
type = map(string)
default = {
Environment = "production"
Team = "platform"
}
}

# Numbers (integers and floats)
variable "instance_count" {
type = number
default = 3
}

# Booleans
variable "enable_monitoring" {
type = bool
default = true
}

String Interpolation​

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

# String templates
user_data = <<-EOF
#!/bin/bash
echo "Hello ${var.name}"
EOF

Providers​

Providers are plugins that enable Terraform to interact with cloud platforms and APIs.

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

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

default_tags {
tags = {
Project = "CloudCaptain"
Managed = "Terraform"
}
}
}

Provider Configuration:

  • Authentication (keys, tokens, assume roles)
  • Region/location defaults
  • Default tags and settings
  • Multiple provider instances with aliases
provider "aws" {
alias = "us-west"
region = "us-west-2"
}

resource "aws_s3_bucket" "west" {
provider = aws.us-west
bucket = "my-bucket-west"
}

Resources and Data Sources​

Resources​

Resources are infrastructure objects you want to create and manage.

resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"

tags = {
Name = "MyInstance"
}
}

# Reference another resource
resource "aws_security_group" "web" {
vpc_id = aws_instance.example.vpc_id

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}

Data Sources​

Data sources query existing infrastructure (read-only).

# Get latest Ubuntu AMI
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 = "t2.micro"
}

Variables​

Variables allow parameterization and reusability.

variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
sensitive = false
}

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

variable "instance_count" {
description = "Number of instances"
type = number
default = 1
}

variable "tags" {
description = "Resource tags"
type = map(string)
default = {
Project = "CloudCaptain"
}
}

Setting Variables:

# Via CLI
terraform apply -var="instance_type=t3.small"

# Via file
terraform apply -var-file="prod.tfvars"

# Environment variables (TF_VAR_ prefix)
export TF_VAR_instance_type="t3.small"

Outputs​

Outputs expose computed or resource values.

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

output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.web.id
}

output "instance_public_ip" {
description = "Public IP address"
value = aws_instance.web.public_ip
}

output "all_instances" {
description = "All instance details"
value = aws_instance.web
sensitive = true # Don't display in terminal
}

Outputs are printed after terraform apply and accessible via state.

State Files​

The state file tracks your deployed resources.

State Management Diagram​

# terraform.tfstate (JSON format)
{
"version": 4,
"terraform_version": "1.5.0",
"resources": [
{
"type": "aws_instance",
"name": "web",
"instances": [
{
"attributes": {
"id": "i-0123456789abcdef0",
"instance_type": "t2.micro",
"public_ip": "203.0.113.42"
}
}
]
}
]
}

State Best Practices:

  • Never commit to Git: Add terraform.tfstate* to .gitignore
  • Use remote state: Store in S3, Terraform Cloud, or backends
  • Enable locking: Prevent concurrent modifications
  • Backup regularly: Enable versioning on backend storage
  • Restrict access: Limit who can read state (contains secrets)

Terraform Workflow​

Complete Workflow Diagram​

1. Write (terraform init)​

Initialize your working directory:

terraform init

This creates .terraform/ directory, downloads providers, and initializes backends.

2. Plan (terraform plan)​

Preview changes before applying:

terraform plan -out=tfplan

Output shows:

  • + resources to create
  • ~ resources to modify
  • - resources to destroy
Terraform will perform the following actions:

# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t2.micro"
+ id = (known after apply)
}

Plan: 1 to add, 0 to change, 0 to destroy.

3. Apply (terraform apply)​

Create/update resources:

terraform apply tfplan

Or review inline:

terraform apply  # Shows plan, prompts for confirmation

4. Destroy (terraform destroy)​

Remove all resources:

terraform destroy

# Destroy specific resource
terraform destroy -target="aws_instance.web"

Terraform vs Alternatives​

Terraform vs CloudFormation​

FeatureTerraformCloudFormation
Multi-cloudYes (AWS, Azure, GCP, others)AWS only
SyntaxHCL (readable)JSON/YAML (verbose)
StateExplicit state fileImplicit (AWS managed)
CommunityLarge ecosystemAWS-focused
Learning curveModerateSteeper

When to choose:

  • Terraform: Multi-cloud, better UX, open-source
  • CloudFormation: AWS-native only, full AWS API coverage

Terraform vs Pulumi​

FeatureTerraformPulumi
LanguageHCL (domain-specific)Python, Go, TypeScript, etc.
StateFile-basedFile or Pulumi Service
FlexibilityLimited to HCLFull programming language
Learning curveModerateEasier for programmers
MaturityMature (2014)Newer (2018)

When to choose:

  • Terraform: Standard IaC, HCL comfort
  • Pulumi: Complex logic, language flexibility

Practical Example: AWS VPC and EC2​

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

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

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

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

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

tags = {
Name = "public-subnet"
}
}

# Security Group
resource "aws_security_group" "web" {
name = "web-sg"
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"]
}

tags = {
Name = "web-sg"
}
}

# Get latest Ubuntu AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]

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

# EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]

tags = {
Name = "web-server"
}
}

# Outputs
output "instance_public_ip" {
description = "Public IP of web server"
value = aws_instance.web.public_ip
}

output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}

Exercises​

Exercise 1: Basic EC2 Deployment​

Create a main.tf that deploys a single t2.micro EC2 instance in us-east-1 with a Name tag. Use terraform init, plan, and apply. Verify the instance exists in AWS Console. Clean up with terraform destroy.

Hint: Use the aws_instance resource and aws_ami data source.

Exercise 2: Variables and Configuration​

Modify your configuration to use variables for instance_type, region, and environment. Create a terraform.tfvars file setting these values. Apply changes and verify outputs display correctly.

Hint: Add variable blocks and define outputs for instance_id and public_ip.

Exercise 3: Multi-Resource Deployment​

Create VPC, subnet, security group, and EC2 instance. Security group should allow SSH (22) and HTTP (80) inbound. Use variable for CIDR blocks.

Hint: Use aws_vpc, aws_subnet, aws_security_group, and aws_instance resources.

Exercise 4: State Management​

Initialize a local S3 backend in your AWS account for state storage. Migrate your existing state. Verify state is in S3 and .terraform/terraform.tfstate no longer exists locally.

Hint: Use backend configuration in terraform block.

Exercise 5: Data Sources and Outputs​

Fetch the latest Amazon Linux 2 AMI using aws_ami data source. Create two EC2 instances (web and app tier) in different subnets. Output instance IDs and private IPs.

Hint: Use for_each or separate resource blocks for multiple instances.


Next Steps: