Infrastructure as Code (Terraform)
Infrastructure as Code (Terraform)¶
1. IaC Overview¶
1.1 What is Infrastructure as Code?¶
IaC is the practice of defining and managing infrastructure through code.
Benefits: - Version control (Git) - Reproducibility - Automation - Documentation - Collaboration
1.2 IaC Tool Comparison¶
| Tool | Type | Language | Multi-Cloud |
|---|---|---|---|
| Terraform | Declarative | HCL | ✅ |
| CloudFormation | Declarative | JSON/YAML | AWS only |
| Deployment Manager | Declarative | YAML/Jinja | GCP only |
| Pulumi | Declarative | Python/TS etc | ✅ |
| Ansible | Procedural | YAML | ✅ |
2. Terraform Basics¶
2.1 Installation¶
# macOS
brew install terraform
# Linux
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Check version
terraform version
2.2 Core Concepts¶
┌─────────────────────────────────────────────────────────────┐
│ Terraform Workflow │
│ │
│ 1. Write → Write .tf files │
│ 2. Init → terraform init (download providers) │
│ 3. Plan → terraform plan (preview changes) │
│ 4. Apply → terraform apply (apply infrastructure) │
│ 5. Destroy → terraform destroy (destroy infrastructure) │
└─────────────────────────────────────────────────────────────┘
2.3 HCL Syntax¶
# Provider configuration
provider "aws" {
region = "ap-northeast-2"
}
# Resource definition
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
tags = {
Name = "WebServer"
}
}
# Variables
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
# Outputs
output "public_ip" {
value = aws_instance.web.public_ip
}
# Local values
locals {
environment = "production"
common_tags = {
Environment = local.environment
ManagedBy = "Terraform"
}
}
# Data source (reference existing resources)
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
3. AWS Infrastructure Setup¶
3.1 VPC + EC2 Example¶
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "${var.project}-vpc"
}
}
# Public subnet
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project}-public-${count.index + 1}"
}
}
# Internet gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project}-igw"
}
}
# Route table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Security group
resource "aws_security_group" "web" {
name = "${var.project}-web-sg"
description = "Web server security group"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.ssh_allowed_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# EC2 instance
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.web.id]
key_name = var.key_name
user_data = <<-EOF
#!/bin/bash
dnf update -y
dnf install -y nginx
systemctl start nginx
systemctl enable nginx
echo "<h1>Hello from Terraform!</h1>" > /usr/share/nginx/html/index.html
EOF
tags = {
Name = "${var.project}-web"
}
}
# Data sources
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# variables.tf
variable "region" {
description = "AWS region"
type = string
default = "ap-northeast-2"
}
variable "project" {
description = "Project name"
type = string
default = "myapp"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "key_name" {
description = "SSH key pair name"
type = string
}
variable "ssh_allowed_cidr" {
description = "CIDR block allowed for SSH"
type = string
default = "0.0.0.0/0"
}
# outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_ip" {
description = "Web server public IP"
value = aws_instance.web.public_ip
}
output "website_url" {
description = "Website URL"
value = "http://${aws_instance.web.public_ip}"
}
4. GCP Infrastructure Setup¶
4.1 VPC + Compute Engine Example¶
# main.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
# VPC
resource "google_compute_network" "main" {
name = "${var.name_prefix}-vpc"
auto_create_subnetworks = false
}
# Subnet
resource "google_compute_subnetwork" "public" {
name = "${var.name_prefix}-subnet"
ip_cidr_range = "10.0.1.0/24"
region = var.region
network = google_compute_network.main.id
}
# Firewall rule - HTTP
resource "google_compute_firewall" "http" {
name = "${var.name_prefix}-allow-http"
network = google_compute_network.main.name
allow {
protocol = "tcp"
ports = ["80", "443"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["http-server"]
}
# Firewall rule - SSH
resource "google_compute_firewall" "ssh" {
name = "${var.name_prefix}-allow-ssh"
network = google_compute_network.main.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = [var.ssh_allowed_cidr]
target_tags = ["ssh-server"]
}
# Compute Engine instance
resource "google_compute_instance" "web" {
name = "${var.name_prefix}-web"
machine_type = var.machine_type
zone = "${var.region}-a"
boot_disk {
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2204-lts"
size = 20
}
}
network_interface {
network = google_compute_network.main.name
subnetwork = google_compute_subnetwork.public.name
access_config {
// Assign external IP
}
}
metadata_startup_script = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y nginx
echo "<h1>Hello from Terraform on GCP!</h1>" > /var/www/html/index.html
EOF
tags = ["http-server", "ssh-server"]
labels = {
environment = var.environment
}
}
# variables.tf
variable "project_id" {
description = "GCP project ID"
type = string
}
variable "region" {
description = "GCP region"
type = string
default = "asia-northeast3"
}
variable "name_prefix" {
description = "Resource name prefix"
type = string
default = "myapp"
}
variable "machine_type" {
description = "Compute Engine machine type"
type = string
default = "e2-micro"
}
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
variable "ssh_allowed_cidr" {
description = "CIDR allowed for SSH"
type = string
default = "0.0.0.0/0"
}
# outputs.tf
output "instance_ip" {
description = "Instance external IP"
value = google_compute_instance.web.network_interface[0].access_config[0].nat_ip
}
output "website_url" {
description = "Website URL"
value = "http://${google_compute_instance.web.network_interface[0].access_config[0].nat_ip}"
}
5. State Management¶
5.1 Remote State Backend¶
AWS S3 Backend:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-locks" # State locking
}
}
GCP Cloud Storage Backend:
terraform {
backend "gcs" {
bucket = "my-terraform-state"
prefix = "prod/terraform.tfstate"
}
}
5.2 State Commands¶
# List state
terraform state list
# Show state
terraform state show aws_instance.web
# Remove resource from state (keep actual resource)
terraform state rm aws_instance.web
# Move state (refactoring)
terraform state mv aws_instance.old aws_instance.new
# Import state (existing resource)
terraform import aws_instance.web i-1234567890abcdef0
6. Modules¶
6.1 Module Structure¶
modules/
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── ec2/
├── main.tf
├── variables.tf
└── outputs.tf
6.2 Module Definition¶
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
tags = merge(var.tags, {
Name = var.name
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name}-public-${count.index + 1}"
})
}
# modules/vpc/variables.tf
variable "name" {
type = string
}
variable "cidr_block" {
type = string
default = "10.0.0.0/16"
}
variable "public_subnets" {
type = list(string)
}
variable "availability_zones" {
type = list(string)
}
variable "tags" {
type = map(string)
default = {}
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
6.3 Module Usage¶
# main.tf
module "vpc" {
source = "./modules/vpc"
name = "myapp"
cidr_block = "10.0.0.0/16"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
availability_zones = ["ap-northeast-2a", "ap-northeast-2c"]
tags = {
Environment = "production"
}
}
module "ec2" {
source = "./modules/ec2"
name = "myapp-web"
subnet_id = module.vpc.public_subnet_ids[0]
instance_type = "t3.micro"
}
7. Workspaces¶
# List workspaces
terraform workspace list
# Create new workspace
terraform workspace new dev
terraform workspace new prod
# Switch workspace
terraform workspace select prod
# Show current workspace
terraform workspace show
# Workspace-specific configuration
locals {
environment = terraform.workspace
instance_type = {
dev = "t3.micro"
prod = "t3.large"
}
}
resource "aws_instance" "web" {
instance_type = local.instance_type[local.environment]
# ...
}
8. Best Practices¶
8.1 Directory Structure¶
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
├── modules/
│ ├── vpc/
│ ├── ec2/
│ └── rds/
└── global/
└── iam/
8.2 Code Style¶
# Resource naming convention
resource "aws_instance" "web" { } # Singular
resource "aws_subnet" "public" { } # Use count/for_each for plural
# Variable defaults
variable "instance_type" {
description = "EC2 instance type" # Always include description
type = string
default = "t3.micro"
}
# Tag consistency
locals {
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "Terraform"
}
}
8.3 Security¶
# Sensitive variables
variable "db_password" {
type = string
sensitive = true
}
# Sensitive outputs
output "db_password" {
value = var.db_password
sensitive = true
}
9. CI/CD Integration¶
9.1 GitHub Actions¶
# .github/workflows/terraform.yml
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.6.0
- name: Terraform Init
run: terraform init
- name: Terraform Plan
run: terraform plan -no-color
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
10. Next Steps¶
- 17_Monitoring_Logging_Cost.md - Monitoring
- Docker/ - Kubernetes IaC