SynfraCore
Synfracore
Start Learning
Navigation

Academies

Platform

RoadmapsLabsCertificationsInterviewPYQsAI AssistantCareer
Start Learning Free Learning Roadmaps

TerraformFundamentals

Core concepts and commands — hands-on from the start

📄
Last updated Jul 2026
Expert Content

Terraform Fundamentals

Analogy — A .tf file is like a shopping list with substitutions built in. resource blocks are the actual items you want ("1 VPC, 2 subnets"); variable blocks are the sticky notes saying "but let the person shopping swap in the right size/color depending on the store" (dev vs. staging vs. prod); output blocks are what you report back after shopping ("here's the receipt, here's the VPC ID you'll need for the next list"). Nothing on the list is bought (no real infrastructure created) until terraform apply — writing the file is just planning the trip.

variable block   →  the substitutable inputs ("how many, what size")
resource block   →  the actual thing to create
output block     →  what to hand back once it exists

Core Syntax (HCL)

hcl
# terraform/main.tf

# Configure the required provider
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  
  # Remote state (use this in teams!)
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

# Configure the provider
provider "aws" {
  region = var.aws_region
}

# Variable declaration
variable "aws_region" {
  description = "AWS region to deploy into"
  type        = string
  default     = "us-east-1"
}

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

variable "allowed_cidr" {
  description = "CIDR blocks allowed to SSH"
  type        = list(string)
  default     = ["10.0.0.0/8"]
}

# Local values (computed/reusable)
locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
    Team        = "Platform"
  }
}

# Resource definition
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = merge(local.common_tags, {
    Name = "main-vpc"
  })
}

# Output values
output "vpc_id" {
  description = "ID of the main VPC"
  value       = aws_vpc.main.id
}

Data Sources

Data sources let you fetch existing infrastructure:

hcl
# Get the latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

# Reference it
resource "aws_instance" "web" {
  ami = data.aws_ami.amazon_linux.id  # Use the fetched AMI
  instance_type = "t3.micro"
}

# Fetch existing VPC
data "aws_vpc" "existing" {
  tags = {
    Name = "production-vpc"
  }
}

Complete AWS VPC Example

hcl
# VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = { Name = "main-vpc" }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "main-igw" }
}

# Public Subnets (2 AZs)
resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true
  
  tags = { Name = "public-subnet-${count.index + 1}" }
}

# Private Subnets (2 AZs)
resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 10}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  
  tags = { Name = "private-subnet-${count.index + 1}" }
}

# NAT Gateway (allows private subnets to reach internet)
resource "aws_eip" "nat" {
  count  = 1
  domain = "vpc"
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat[0].id
  subnet_id     = aws_subnet.public[0].id
  depends_on    = [aws_internet_gateway.main]
  tags          = { Name = "main-nat-gw" }
}

# Route Tables
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  tags = { Name = "public-rt" }
}

resource "aws_route_table_association" "public" {
  count          = 2
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

data "aws_availability_zones" "available" {
  state = "available"
}

Terraform Commands

bash
terraform init              # Initialize, download providers
terraform init -upgrade     # Upgrade providers
terraform validate          # Check syntax
terraform fmt               # Format code
terraform fmt -recursive    # Format all .tf files

terraform plan              # Preview changes
terraform plan -out=tfplan  # Save plan to file
terraform apply             # Apply changes
terraform apply tfplan      # Apply saved plan
terraform apply -auto-approve  # Skip confirmation (CI/CD)

terraform destroy           # Destroy all resources
terraform destroy -target=aws_instance.web  # Destroy specific

terraform state list        # List resources in state
terraform state show aws_instance.web  # Show resource state
terraform state rm resource.name       # Remove from state
terraform import aws_s3_bucket.main my-bucket  # Import existing

terraform output            # Show all outputs
terraform output vpc_id     # Show specific output
terraform refresh           # Sync state with real infra

Try It (2 Minutes)

Using the local_file example from the Overview tab, add a variable instead of hardcoding the content:

hcl
variable "message" {
  description = "Content to write into the file"
  type        = string
  default     = "hello from terraform"
}

resource "local_file" "demo" {
  filename = "hello.txt"
  content  = var.message
}

output "file_path" {
  value = local_file.demo.filename
}

Run terraform apply -var="message=a custom message" — the file's content changes to your override, without editing the .tf file itself. Then run terraform output file_path — it prints the output value without needing to re-run apply. This is the substitutable-shopping-list idea directly: the same resource block, different inputs, and a clean way to report back what was created.

Share:
Join our Community
Daily tips, job alerts, interview help — join engineers learning together
Up Next
TerraformIntermediate
Real-world patterns, best practices, and deeper topics
Also Worth Exploring
← Back to all Terraform modules
InstallationIntermediate