Infrastructure as Code: Terraform Getting Started Guide
Learn Infrastructure as Code with Terraform. This beginner guide covers HCL syntax, providers, state management, modules, and deploying your first cloud resources step by step.
What you'll learn
- ✓What Infrastructure as Code is and why it matters
- ✓How to write Terraform configurations in HCL
- ✓Managing state, variables, and outputs
- ✓Building reusable modules for real infrastructure
Prerequisites
- •Basic command-line experience
- •An AWS, GCP, or Azure free-tier account
- •Terraform CLI installed (v1.5+)
What Is Infrastructure as Code?
Infrastructure as Code (IaC) is the practice of managing and provisioning computing infrastructure through machine-readable configuration files rather than manual processes. Instead of clicking through a cloud console to create a server, you write a file that describes exactly what you need, and a tool builds it for you.
This approach brings software engineering practices to infrastructure: version control, code review, testing, and repeatable deployments. When your infrastructure is code, you can track every change, roll back mistakes, and spin up identical environments in minutes.
Terraform, built by HashiCorp, is the most widely adopted IaC tool. It uses a declarative language called HCL (HashiCorp Configuration Language) and supports hundreds of cloud providers through a plugin system.
Why Terraform Over Other Tools
Several IaC tools exist, but Terraform stands out for a few reasons:
- Cloud-agnostic: One tool works across AWS, GCP, Azure, and dozens of other providers.
- Declarative syntax: You describe the desired end state; Terraform figures out how to get there.
- State management: Terraform tracks what it has created, so it knows what to change or destroy.
- Large ecosystem: Thousands of community-maintained providers and modules are available.
- Plan before apply: You can preview every change before it happens.
Other tools like Pulumi (imperative, uses general-purpose languages) and CloudFormation (AWS-only) have their place, but Terraform’s balance of simplicity and power makes it the standard starting point.
Installing Terraform
On macOS with Homebrew:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform --version
On Linux (Ubuntu/Debian):
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
Verify the installation:
terraform --version
# Terraform v1.9.x
Your First Terraform Configuration
Create a project directory and a main configuration file:
mkdir terraform-demo && cd terraform-demo
Create main.tf with a simple AWS EC2 instance:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.5.0"
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web_server" {
ami = "ami-0c02fb55956c7d316" # Amazon Linux 2023
instance_type = "t2.micro"
tags = {
Name = "terraform-demo-server"
Environment = "development"
}
}
This file does three things: declares the AWS provider, configures the region, and defines an EC2 instance resource.
The Terraform Workflow
Terraform follows a predictable four-step workflow:
Step 1: Initialize
terraform init
This downloads the AWS provider plugin and sets up the working directory. You will see output confirming the provider was installed.
Step 2: Plan
terraform plan
Terraform compares your configuration to the current state and shows what it will create, modify, or destroy. Always review the plan before applying.
Plan: 1 to add, 0 to change, 0 to destroy.
Step 3: Apply
terraform apply
Terraform asks for confirmation, then creates the resources. Type yes to proceed.
Step 4: Destroy (when done)
terraform destroy
This tears down everything Terraform created. Essential for cleaning up development resources and avoiding unnecessary costs.
Understanding HCL Syntax
HCL is designed to be both human-readable and machine-friendly. Here are the core building blocks:
Variables
Define input variables in variables.tf:
variable "instance_type" {
description = "EC2 instance size"
type = string
default = "t2.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."
}
}
variable "allowed_ports" {
description = "List of allowed ingress ports"
type = list(number)
default = [80, 443]
}
Reference variables with var.variable_name:
resource "aws_instance" "web_server" {
instance_type = var.instance_type
# ...
}
Outputs
Define outputs in outputs.tf to expose useful information:
output "instance_public_ip" {
description = "Public IP of the web server"
value = aws_instance.web_server.public_ip
}
output "instance_id" {
description = "EC2 Instance ID"
value = aws_instance.web_server.id
}
After terraform apply, outputs are printed to the terminal. You can also retrieve them later with terraform output.
Local Values
Locals let you define computed values to avoid repetition:
locals {
common_tags = {
Project = "terraform-demo"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_instance" "web_server" {
ami = "ami-0c02fb55956c7d316"
instance_type = var.instance_type
tags = local.common_tags
}
Managing Terraform State
Terraform state is a JSON file that maps your configuration to real-world resources. By default, it is stored locally in terraform.tfstate. For team use, store state remotely.
Remote State with S3
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "demo/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
The DynamoDB table provides state locking, preventing two people from modifying state simultaneously. Create these resources manually (or with a separate Terraform config) before using them as a backend.
State Commands
# List all resources in state
terraform state list
# Show details of a specific resource
terraform state show aws_instance.web_server
# Remove a resource from state (without destroying it)
terraform state rm aws_instance.web_server
# Move a resource to a new address (after refactoring)
terraform state mv aws_instance.web_server aws_instance.app_server
Building a Real Example
Let us build a more complete setup: a VPC with a public subnet, security group, and an EC2 instance.
# networking.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = merge(local.common_tags, { Name = "main-vpc" })
}
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 = merge(local.common_tags, { Name = "public-subnet" })
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = local.common_tags
}
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
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
# security.tf
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
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"]
}
}
# compute.tf
resource "aws_instance" "web_server" {
ami = "ami-0c02fb55956c7d316"
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from Terraform</h1>" > /var/www/html/index.html
EOF
tags = merge(local.common_tags, { Name = "web-server" })
}
Creating Reusable Modules
Modules let you package and reuse configurations. Create a module directory structure:
modules/
web-server/
main.tf
variables.tf
outputs.tf
# modules/web-server/variables.tf
variable "instance_type" {
type = string
default = "t2.micro"
}
variable "subnet_id" {
type = string
}
variable "security_group_ids" {
type = list(string)
}
# modules/web-server/main.tf
resource "aws_instance" "this" {
ami = "ami-0c02fb55956c7d316"
instance_type = var.instance_type
subnet_id = var.subnet_id
vpc_security_group_ids = var.security_group_ids
tags = {
Name = "web-server"
}
}
Use the module from your root configuration:
module "web" {
source = "./modules/web-server"
instance_type = "t3.small"
subnet_id = aws_subnet.public.id
security_group_ids = [aws_security_group.web.id]
}
You can also use modules from the Terraform Registry:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "my-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.3.0/24", "10.0.4.0/24"]
}
Best Practices
File organization: Split configurations into logical files (main.tf, variables.tf, outputs.tf, providers.tf). Terraform loads all .tf files in a directory.
Use .tfvars files for environment-specific values:
# prod.tfvars
instance_type = "t3.large"
environment = "prod"
terraform apply -var-file="prod.tfvars"
Pin provider versions to avoid unexpected changes. Use the ~> constraint for minor version flexibility.
Never commit state files to version control. Add *.tfstate and .terraform/ to .gitignore.
Use terraform fmt to keep formatting consistent, and terraform validate to catch syntax errors early.
Tag everything with a consistent tagging strategy so you can track costs and ownership.
Wrapping Up
Infrastructure as Code with Terraform transforms how you manage cloud resources. You started with a single EC2 instance and progressed to a full VPC setup with reusable modules. The key concepts to remember are the init-plan-apply workflow, state management with remote backends, and modular design for reusability. From here, explore Terraform workspaces for managing multiple environments, terraform import for bringing existing resources under management, and CI/CD integration for automated infrastructure deployments.
Related articles
- DevOps Ansible vs Terraform: Configuration vs Infrastructure
Compare Ansible and Terraform for DevOps automation. Learn when to use each tool, how they differ in approach, and how to combine them effectively.
- DevOps Terraform vs Pulumi: Infrastructure as Code Compared
Compare Terraform and Pulumi for infrastructure as code. Understand the tradeoffs between HCL and general-purpose languages for managing cloud resources.
- DevOps Infrastructure Drift Detection and Remediation
Detect and fix infrastructure drift with Terraform, automated CI checks, and policy enforcement to keep your cloud state in sync with code.
- DevOps Terraform Modules for Reusable Infrastructure
Learn how to build, structure, and publish Terraform modules to create reusable, composable infrastructure as code.