Why You Need IaC
Infrastructure created by clicking around cannot be reproduced. Defining infrastructure as code with Terraform enables version control, code review, and automation.
Project Directory Structure
terraform/
├── environments/
│ ├── prod/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ └── staging/
└── modules/
├── vpc/
├── eks/
└── rds/Module Design
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
tags = merge(var.common_tags, {
Name = "${var.project}-${var.environment}-vpc"
})
}
variable "cidr_block" {
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = " CIDR ."
}
}Remote State Management
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}# State
aws dynamodb create-table \
--table-name terraform-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUESTCI/CD Integration
name: Terraform
on:
pull_request:
paths: ['terraform/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform fmt -check -recursive
- run: terraform validate
- run: terraform plan -out=tfplanPractical Tips
Never commit tfstate files to git (.gitignore)
*.tfstate
*.tfstate.backup
.terraform/Masking sensitive variables
variable "db_password" {
type = string
sensitive = true
}Terraform is not just a tool. It is a cultural shift that changes how your team operates infrastructure.
Console Changes (Drift) and Avoiding Re-creation During Refactors
Even with remote state and locking, plan sometimes proposes unexpected deletes and re-creations because someone changed settings in the console or the module structure changed.
Causes
- During an incident, security group rules or instance sizes were edited directly in the console (drift)
- Moving a resource into a module or renaming it changes its address, which Terraform reads as "delete old + create new"
- A resource created outside Terraform was added to code but never imported into state
Verification
# drift only (compare state with reality, no infra changes)
terraform plan -refresh-only
# for CI: exit code 2 when there are changes
terraform plan -detailed-exitcode -out=tfplan; echo "exit=$?"
# list only resources slated for deletion
terraform show -json tfplan | jq -r '.resource_changes[] | select(.change.actions | index("delete")) | .address'Fix
- If the drift was intentional, put it in code; otherwise
applyto return to the coded state. - When a refactor changes addresses, add a
movedblock so only the state address moves, with no deletion.
moved {
from = aws_security_group.web
to = module.web.aws_security_group.this
}- Bring existing resources in with an
importblock (Terraform 1.5+) and adjust code untilplanshows no changes. - Note: recent Terraform versions support S3-native locking in the S3 backend via
use_lockfile = true; check your version's backend docs on whether DynamoDB locking is still recommended.
Prevention
- Run a
-detailed-exitcodedrift check on a daily schedule and alert on exit code 2.
References: Official Docs
The primary source for the behavior, configuration, and errors covered in this post is the official documentation below. Check there for version-specific options and exact behavior.
Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us
This article has no recorded human check (date and scope) yet.
Comments
Be the first to comment.