Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

Terraform Configuration Files

Terraform projects are built from .tf files containing HCL (HashiCorp Configuration Language). Terraform reads all .tf files in the working directory and merges them before processing. Understanding how to organize these files is the foundation of a maintainable IaC codebase.


File Types in a Terraform Project

FilePurpose
main.tfPrimary resource definitions
variables.tfVariable declarations (type, description, default)
outputs.tfOutput value definitions
versions.tfterraform {} block — Terraform and provider version constraints
locals.tfLocal computed values (optional but recommended for complex expressions)
terraform.tfvarsVariable values — auto-loaded by Terraform
*.auto.tfvarsAuto-loaded variable value files
.terraform.lock.hclProvider dependency lock file — commit to git
terraform.tfstateState file — never edit manually, ideally stored remotely

versions.tf — Lock Your Tooling

versions.tf
terraform {
required_version = ">= 1.6, < 2.0" # Minimum Terraform CLI version
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.50" # ~> means >= 5.50, < 6.0
}
random = {
source = "hashicorp/random"
version = ">= 3.5"
}
}
# Remote backend (recommended for teams)
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}

Always lock provider versions. The ~> operator pins the major version while allowing minor/patch updates — this prevents breaking changes from auto-upgrading providers.


variables.tf — Declare, Don’t Assign

# variables.tf — only declarations here, not values
variable "environment" {
description = "Deployment environment: dev, staging, or production"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
variable "instance_type" {
description = "EC2 instance type for the application servers"
type = string
default = "t3.micro"
}
variable "allowed_cidr_blocks" {
description = "List of CIDR blocks allowed to access the load balancer"
type = list(string)
default = ["0.0.0.0/0"]
}
variable "tags" {
description = "Additional resource tags"
type = map(string)
default = {}
}
variable "database_password" {
description = "RDS master password"
type = string
sensitive = true # Never printed in logs or plan output
}

terraform.tfvars — Provide Variable Values

# terraform.tfvars — auto-loaded; do NOT commit if it contains secrets
environment = "production"
instance_type = "t3.large"
allowed_cidr_blocks = [
"10.0.0.0/8",
"192.168.1.0/24"
]
tags = {
Team = "platform"
Project = "commerce"
}

For sensitive values, use environment variables instead:

Terminal window
export TF_VAR_database_password="s3cur3P@ssword!"
terraform apply

Environment-specific tfvars:

Terminal window
terraform apply -var-file="environments/production.tfvars"
terraform apply -var-file="environments/staging.tfvars"

main.tf — Resource Definitions

main.tf
locals {
name_prefix = "${var.environment}-${var.project_name}"
common_tags = merge(var.tags, {
Environment = var.environment
ManagedBy = "terraform"
})
}
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 = "${local.name_prefix}-vpc"
})
}
resource "aws_subnet" "public" {
for_each = {
"a" = "10.0.1.0/24"
"b" = "10.0.2.0/24"
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = "${data.aws_region.current.name}${each.key}"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-${each.key}"
Tier = "public"
})
}
data "aws_region" "current" {}

outputs.tf — Export Values

outputs.tf
output "vpc_id" {
description = "ID of the provisioned VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of public subnets (map keyed by AZ suffix)"
value = { for k, s in aws_subnet.public : k => s.id }
}
output "load_balancer_dns" {
description = "DNS name of the application load balancer"
value = aws_lb.main.dns_name
}
output "database_endpoint" {
description = "RDS connection endpoint"
value = aws_db_instance.main.endpoint
sensitive = true # Hidden in CLI output, accessible programmatically
}

.terraform.lock.hcl — The Provider Lock File

Created automatically by terraform init, this file records the exact provider versions and checksums:

# .terraform.lock.hcl — commit this to git!
provider "registry.terraform.io/hashicorp/aws" {
version = "5.54.1"
constraints = "~> 5.50"
hashes = [
"h1:8z...",
"zh:abc123...",
]
}

Committing this file ensures everyone on the team (and CI/CD) uses identical provider binaries. Run terraform init -upgrade to update providers within the constraint range.


Project Layout for Real Teams

infrastructure/
├── modules/ # Reusable internal modules
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── app-service/
├── environments/
│ ├── dev/
│ │ ├── main.tf # Calls modules with dev settings
│ │ ├── versions.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── production/
└── .github/
└── workflows/
└── terraform.yml # CI/CD: plan on PR, apply on merge

This structure keeps environment-specific concerns separate from reusable module logic.