Terraform Infrastructure as Code: Complete 2026 Guide
in Development on Terraform, DevOps, Infrastructure, Cloud
Terraform Infrastructure as Code: Complete 2026 Guide
Terraform has become the de facto standard for Infrastructure as Code (IaC). This comprehensive guide covers Terraform best practices, advanced patterns, and real-world examples for 2026.
Why Terraform?
Terraform by HashiCorp enables you to define cloud infrastructure in declarative configuration files. Key advantages include:
- Multi-cloud support - AWS, GCP, Azure, and 3000+ providers
- State management - Track resource changes over time
- Plan before apply - Preview changes before execution
- Module system - Reusable infrastructure components
- HCL syntax - Human-readable configuration language
Installation
# macOS
brew install terraform
# 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 installation
terraform version
Project Structure
infrastructure/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── modules/
│ ├── vpc/
│ ├── eks/
│ ├── rds/
│ └── s3/
├── versions.tf
└── backend.tf
Basic Configuration
Provider Configuration
# versions.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
# provider.tf
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
}
}
}
Remote Backend
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Essential Commands
# Initialize project
terraform init
# Format code
terraform fmt -recursive
# Validate configuration
terraform validate
# Plan changes
terraform plan -out=tfplan
# Apply changes
terraform apply tfplan
# Destroy resources
terraform destroy
# Import existing resources
terraform import aws_instance.web i-1234567890abcdef0
# Show state
terraform state list
terraform state show aws_instance.web
Creating Reusable Modules
VPC Module
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
Type = "Public"
}
}
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + length(var.availability_zones))
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-private-${count.index + 1}"
Type = "Private"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? length(var.availability_zones) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-nat-${count.index + 1}"
}
}
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? length(var.availability_zones) : 0
domain = "vpc"
tags = {
Name = "${var.project_name}-eip-${count.index + 1}"
}
}
# modules/vpc/variables.tf
variable "project_name" {
type = string
description = "Project name for resource naming"
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
description = "CIDR block for VPC"
}
variable "availability_zones" {
type = list(string)
description = "List of availability zones"
}
variable "enable_nat_gateway" {
type = bool
default = true
description = "Enable NAT Gateway for private subnets"
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
description = "VPC ID"
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
description = "Public subnet IDs"
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
description = "Private subnet IDs"
}
Using Modules
# environments/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
project_name = var.project_name
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
enable_nat_gateway = true
}
module "eks" {
source = "../../modules/eks"
cluster_name = "${var.project_name}-cluster"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
kubernetes_version = "1.29"
}
Advanced Patterns
Dynamic Blocks
resource "aws_security_group" "main" {
name = "${var.project_name}-sg"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = ingress.value.description
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
For Expressions
# Transform list to map
locals {
instance_map = {
for idx, instance in aws_instance.web : instance.tags.Name => instance.id
}
# Filter and transform
production_instances = [
for instance in aws_instance.web : instance.id
if instance.tags.Environment == "production"
]
}
Conditional Resources
resource "aws_cloudwatch_log_group" "main" {
count = var.enable_logging ? 1 : 0
name = "/aws/application/${var.project_name}"
retention_in_days = var.log_retention_days
}
# Reference conditional resource
resource "aws_lambda_function" "main" {
# ...
environment {
variables = {
LOG_GROUP = var.enable_logging ? aws_cloudwatch_log_group.main[0].name : ""
}
}
}
Workspaces
# Create workspace
terraform workspace new staging
terraform workspace new production
# Switch workspace
terraform workspace select staging
# List workspaces
terraform workspace list
# Use workspace in configuration
locals {
environment = terraform.workspace
instance_count = {
default = 1
staging = 2
production = 5
}
}
resource "aws_instance" "web" {
count = local.instance_count[local.environment]
# ...
}
State Management
State Locking
# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
State Operations
# Move resources between states
terraform state mv aws_instance.old aws_instance.new
# Remove from state (without destroying)
terraform state rm aws_instance.web
# Pull remote state
terraform state pull > state.json
# Push state (careful!)
terraform state push state.json
Terraform Cloud / Enterprise
terraform {
cloud {
organization = "my-org"
workspaces {
tags = ["app:myapp"]
}
}
}
Testing with Terratest
// vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVPCModule(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"project_name": "test",
"vpc_cidr": "10.0.0.0/16",
"availability_zones": []string{"us-east-1a", "us-east-1b"},
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
}
Best Practices
1. Use Variables Wisely
variable "instance_type" {
type = string
description = "EC2 instance type"
default = "t3.micro"
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "Instance type must be t3 family."
}
}
2. Sensitive Data
variable "db_password" {
type = string
sensitive = true
}
output "db_connection" {
value = "postgresql://${var.db_user}:${var.db_password}@${aws_db_instance.main.endpoint}"
sensitive = true
}
3. Lifecycle Rules
resource "aws_instance" "web" {
# ...
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [tags["LastModified"]]
}
}
CI/CD Integration
GitHub Actions
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Plan
run: terraform plan -out=tfplan
env:
AWS_ACCESS_KEY_ID: $
AWS_SECRET_ACCESS_KEY: $
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan
Conclusion
Terraform remains the industry standard for infrastructure as code in 2026. By following these patterns and best practices, you can build maintainable, scalable, and secure infrastructure across any cloud provider.
Key takeaways:
- Use modules for reusability
- Implement remote state with locking
- Test your infrastructure code
- Follow the principle of least privilege
- Document your configurations
Resources
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)
