Terraform Infrastructure as Code: Providers, Resources, Modules and State
Terraform is the most widely used Infrastructure as Code (IaC) tool. It lets you define your cloud infrastructure in declarative configuration files, version them in Git, and apply changes consistently and repeatably. Understanding Terraform is now expected for any developer who touches production infrastructure.
Why Infrastructure as Code?
Without IaC, infrastructure is often:
- Created manually via console β not reproducible, not reviewable
- Undocumented β nobody knows what was changed and when
- Inconsistent across environments β staging differs from production in unknown ways
With Terraform:
- Infrastructure is defined in code β reviewed like code, versioned in Git
- Changes go through pull requests β reviewable, auditable
- Environments are reproducible β staging and production use the same code
- Drift is detectable β
terraform planshows what changed outside Terraform
Core Concepts
hcl-- main.tf -- Provider: tells Terraform which cloud to target terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } -- Store state remotely backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "eu-west-1" } } provider "aws" { region = var.aws_region } -- Resource: a piece of infrastructure to create/manage resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true tags = { Name = "${var.project_name}-vpc" Environment = var.environment } } resource "aws_subnet" "public" { count = 2 vpc_id = aws_vpc.main.id -- reference to above resource cidr_block = "10.0.${count.index}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index] tags = { Name = "${var.project_name}-public-${count.index}" } } -- Data source: read existing infrastructure data "aws_availability_zones" "available" { state = "available" }
Variables and Outputs
hcl-- variables.tf variable "aws_region" { description = "AWS region to deploy resources" type = string default = "eu-west-1" } 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 "project_name" { type = string } variable "instance_config" { description = "EC2 instance configuration" type = object({ type = string ami = string disk_size_gb = number }) default = { type = "t3.micro" ami = "ami-0c02fb55956c7d316" disk_size_gb = 20 } } -- outputs.tf output "vpc_id" { description = "ID of the created VPC" value = aws_vpc.main.id } output "public_subnet_ids" { value = aws_subnet.public[*].id } output "load_balancer_dns" { value = aws_lb.main.dns_name description = "DNS name of the load balancer" }
bash-- Pass variables at apply time terraform apply -var="environment=prod" -var="project_name=myapp" -- Or use a .tfvars file terraform apply -var-file="prod.tfvars"
hcl-- prod.tfvars environment = "prod" project_name = "myapp" aws_region = "us-east-1" instance_config = { type = "t3.medium" ami = "ami-0c02fb55956c7d316" disk_size_gb = 50 }
A Real AWS Example
hcl-- EC2 instance with security group resource "aws_security_group" "app" { name = "${var.project_name}-app-sg" vpc_id = aws_vpc.main.id ingress { from_port = 443 to_port = 443 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"] } } resource "aws_instance" "app" { ami = var.instance_config.ami instance_type = var.instance_config.type subnet_id = aws_subnet.public[0].id vpc_security_group_ids = [aws_security_group.app.id] key_name = aws_key_pair.deployer.key_name root_block_device { volume_size = var.instance_config.disk_size_gb encrypted = true } user_data = <<-EOF #!/bin/bash apt-get update apt-get install -y docker.io systemctl start docker EOF tags = { Name = "${var.project_name}-app" Environment = var.environment } } -- RDS database resource "aws_db_instance" "main" { identifier = "${var.project_name}-db" engine = "postgres" engine_version = "16.1" instance_class = "db.t3.micro" allocated_storage = 20 storage_encrypted = true db_name = "appdb" username = "postgres" password = var.db_password -- sensitive variable, never hardcoded vpc_security_group_ids = [aws_security_group.db.id] db_subnet_group_name = aws_db_subnet_group.main.name backup_retention_period = 7 skip_final_snapshot = var.environment != "prod" tags = { Environment = var.environment } }
Modules
Modules package reusable infrastructure. They are just directories of Terraform files:
hcl-- modules/ec2-app/main.tf variable "name" { type = string } variable "instance_type" { type = string } variable "subnet_id" { type = string } variable "sg_ids" { type = list(string) } variable "environment" { type = string } resource "aws_instance" "this" { ami = data.aws_ami.amazon_linux.id instance_type = var.instance_type subnet_id = var.subnet_id ... tags = { Name = var.name, Environment = var.environment } } output "instance_id" { value = aws_instance.this.id } output "private_ip" { value = aws_instance.this.private_ip }
hcl-- Using the module in root configuration module "api_server" { source = "./modules/ec2-app" name = "api-server" instance_type = "t3.small" subnet_id = aws_subnet.public[0].id sg_ids = [aws_security_group.app.id] environment = var.environment } module "worker" { source = "./modules/ec2-app" name = "background-worker" instance_type = "t3.medium" subnet_id = aws_subnet.private[0].id sg_ids = [aws_security_group.worker.id] environment = var.environment } -- Access module outputs output "api_ip" { value = module.api_server.private_ip }
You can also use modules from the Terraform Registry:
hclmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.0.0" name = var.project_name cidr = "10.0.0.0/16" azs = ["eu-west-1a", "eu-west-1b"] ... }
State Management
Terraform state tracks what resources it manages. Remote state is essential for teams:
hclterraform { backend "s3" { bucket = "my-terraform-state-bucket" key = "environments/prod/terraform.tfstate" region = "eu-west-1" encrypt = true dynamodb_table = "terraform-locks" -- prevents concurrent applies } }
State locking with DynamoDB prevents two people from applying simultaneously (which could corrupt state).
Key state commands:
bashterraform state list -- list all resources in state terraform state show aws_instance.app -- inspect a specific resource terraform state mv aws_instance.old aws_instance.new -- rename a resource terraform import aws_instance.imported i-1234567890 -- import existing resource terraform state rm aws_instance.app -- remove from state (does not delete resource)
Typical Workflow
bash-- Initialize -- downloads providers and modules terraform init -- Preview changes terraform plan -- Preview and save plan terraform plan -out=tfplan -- Apply the saved plan terraform apply tfplan -- Apply with auto-approve (CI/CD) terraform apply -auto-approve -- Destroy all resources terraform destroy
Workspaces
Manage multiple environments with workspaces:
bashterraform workspace new staging terraform workspace new prod terraform workspace select prod terraform workspace list
hcl-- Use workspace name in configuration resource "aws_instance" "app" { instance_type = terraform.workspace == "prod" ? "t3.medium" : "t3.micro" tags = { Environment = terraform.workspace } }
Common Interview Questions
Q: What is Terraform state and why must it be stored remotely for teams?
Terraform state is a JSON file that maps your configuration to real infrastructure. It tracks resource IDs, metadata, and dependencies. If two developers keep state locally, each has an incomplete view of the infrastructure β applying changes risks creating duplicates or destroying resources the other person created. Remote state (S3 + DynamoDB) is a single source of truth and supports locking to prevent concurrent applies.
Q: What is the difference between terraform plan and terraform apply?
plan generates an execution plan showing what would change without making changes β additions (green +), modifications (yellow ~), and deletions (red -). apply actually makes those changes. Always review the plan before applying, especially for destructions.
Q: What is a Terraform module and when would you create one?
A module is a reusable package of Terraform resources grouped together. Create a module when you find yourself copying the same infrastructure pattern across multiple places β it reduces duplication, makes code more readable, and lets you enforce standards (like tagging, encryption) centrally. Public modules for common AWS patterns are available on the Terraform Registry.
Practice DevOps on Froquiz
Infrastructure and DevOps concepts are tested in backend and platform engineering interviews. Explore our Docker and DevOps quizzes on Froquiz to test your knowledge.
Summary
- Terraform defines infrastructure declaratively in HCL β
terraform applymakes it real - Resources reference each other by name β Terraform computes the dependency graph
- Variables and outputs make configurations reusable across environments
- Modules package reusable patterns β use them to avoid repetition
- Remote state (S3 + DynamoDB) is essential for team workflows β enables locking
planpreviews changes;applyexecutes them β always review the planterraform importbrings existing resources under Terraform management