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
| File | Purpose |
|---|---|
main.tf | Primary resource definitions |
variables.tf | Variable declarations (type, description, default) |
outputs.tf | Output value definitions |
versions.tf | terraform {} block — Terraform and provider version constraints |
locals.tf | Local computed values (optional but recommended for complex expressions) |
terraform.tfvars | Variable values — auto-loaded by Terraform |
*.auto.tfvars | Auto-loaded variable value files |
.terraform.lock.hcl | Provider dependency lock file — commit to git |
terraform.tfstate | State file — never edit manually, ideally stored remotely |
versions.tf — Lock Your Tooling
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 secretsenvironment = "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:
export TF_VAR_database_password="s3cur3P@ssword!"terraform applyEnvironment-specific tfvars:
terraform apply -var-file="environments/production.tfvars"terraform apply -var-file="environments/staging.tfvars"main.tf — Resource Definitions
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
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 mergeThis structure keeps environment-specific concerns separate from reusable module logic.