Terraformは、HashiCorpが開発したInfrastructure as Code(IaC)ツールです。AWSをはじめとするクラウドプロバイダーのリソースを、宣言的なコードで管理できます。本記事では、実践的なAWSインフラ構築を通じて、Terraformの基礎から本番運用レベルまでを学びます。
この記事で学ぶこと
- Terraformの基本概念とHCL構文
- AWSリソースの構築(VPC、EC2、RDS、S3)
- モジュール設計とベストプラクティス
- 状態管理とチーム開発
- CI/CDパイプラインへの統合
環境構築
インストール
# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Linux
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
# バージョン確認
terraform version
AWS認証設定
# AWS CLI設定
aws configure
# AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name: ap-northeast-1
# または環境変数
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="ap-northeast-1"
プロジェクト構成
terraform-project/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ ├── database/
│ └── storage/
├── .terraform-version
└── .gitignore
HCL基本構文
プロバイダー設定
# main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# リモートバックエンド(後述)
backend "s3" {
bucket = "my-terraform-state"
key = "dev/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
}
}
}
変数の定義
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project_name" {
description = "Project name"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "db_config" {
description = "Database configuration"
type = object({
instance_class = string
allocated_storage = number
engine_version = string
})
default = {
instance_class = "db.t3.micro"
allocated_storage = 20
engine_version = "16.1"
}
}
変数値ファイル
# terraform.tfvars
aws_region = "ap-northeast-1"
environment = "dev"
project_name = "my-app"
vpc_cidr = "10.0.0.0/16"
instance_type = "t3.small"
db_config = {
instance_class = "db.t3.small"
allocated_storage = 50
engine_version = "16.1"
}
VPCネットワーク構築
モジュール設計
# modules/networking/main.tf
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 3)
}
data "aws_availability_zones" "available" {
state = "available"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
# インターネットゲートウェイ
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-igw"
}
}
# パブリックサブネット
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-${var.environment}-public-${local.azs[count.index]}"
Tier = "Public"
}
}
# プライベートサブネット
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + length(local.azs))
availability_zone = local.azs[count.index]
tags = {
Name = "${var.project_name}-${var.environment}-private-${local.azs[count.index]}"
Tier = "Private"
}
}
# NAT Gateway(本番環境用)
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? length(local.azs) : 0
domain = "vpc"
tags = {
Name = "${var.project_name}-${var.environment}-nat-eip-${count.index}"
}
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? length(local.azs) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-${var.environment}-nat-${count.index}"
}
depends_on = [aws_internet_gateway.main]
}
# ルートテーブル(パブリック)
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_name}-${var.environment}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(local.azs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# ルートテーブル(プライベート)
resource "aws_route_table" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
}
tags = {
Name = "${var.project_name}-${var.environment}-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private" {
count = length(local.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
モジュールの変数と出力
# modules/networking/variables.tf
variable "project_name" {
type = string
}
variable "environment" {
type = string
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "enable_nat_gateway" {
type = bool
default = false
}
# modules/networking/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
EC2インスタンス構築
セキュリティグループ
# modules/compute/main.tf
resource "aws_security_group" "web" {
name = "${var.project_name}-${var.environment}-web-sg"
description = "Security group for web servers"
vpc_id = var.vpc_id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-web-sg"
}
}
Launch Template と Auto Scaling
# AMI データソース
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Launch Template
resource "aws_launch_template" "web" {
name_prefix = "${var.project_name}-${var.environment}-web-"
image_id = data.aws_ami.amazon_linux_2023.id
instance_type = var.instance_type
network_interfaces {
associate_public_ip_address = false
security_groups = [aws_security_group.web.id]
}
iam_instance_profile {
name = aws_iam_instance_profile.web.name
}
user_data = base64encode(templatefile("${path.module}/templates/user_data.sh", {
environment = var.environment
region = var.aws_region
}))
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.project_name}-${var.environment}-web"
}
}
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Group
resource "aws_autoscaling_group" "web" {
name = "${var.project_name}-${var.environment}-web-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
min_size = var.asg_min_size
max_size = var.asg_max_size
desired_capacity = var.asg_desired_capacity
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
}
tag {
key = "Name"
value = "${var.project_name}-${var.environment}-web"
propagate_at_launch = true
}
}
# スケーリングポリシー
resource "aws_autoscaling_policy" "web_scale_up" {
name = "${var.project_name}-${var.environment}-web-scale-up"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 1
cooldown = 300
}
resource "aws_autoscaling_policy" "web_scale_down" {
name = "${var.project_name}-${var.environment}-web-scale-down"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = -1
cooldown = 300
}
RDSデータベース構築
# modules/database/main.tf
# サブネットグループ
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-${var.environment}-db-subnet"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.project_name}-${var.environment}-db-subnet"
}
}
# セキュリティグループ
resource "aws_security_group" "db" {
name = "${var.project_name}-${var.environment}-db-sg"
description = "Security group for RDS"
vpc_id = var.vpc_id
ingress {
description = "PostgreSQL from web servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.web_security_group_id]
}
tags = {
Name = "${var.project_name}-${var.environment}-db-sg"
}
}
# パラメータグループ
resource "aws_db_parameter_group" "main" {
family = "postgres16"
name = "${var.project_name}-${var.environment}-pg16"
parameter {
name = "log_min_duration_statement"
value = "1000" # 1秒以上のクエリをログ
}
parameter {
name = "shared_preload_libraries"
value = "pg_stat_statements"
}
}
# RDSインスタンス
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-${var.environment}-db"
engine = "postgres"
engine_version = var.db_config.engine_version
instance_class = var.db_config.instance_class
allocated_storage = var.db_config.allocated_storage
max_allocated_storage = var.db_config.allocated_storage * 2
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password # Secrets Manager推奨
multi_az = var.environment == "prod"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.main.name
backup_retention_period = var.environment == "prod" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "mon:04:00-mon:05:00"
skip_final_snapshot = var.environment != "prod"
final_snapshot_identifier = var.environment == "prod" ? "${var.project_name}-${var.environment}-final" : null
deletion_protection = var.environment == "prod"
performance_insights_enabled = true
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
tags = {
Name = "${var.project_name}-${var.environment}-db"
}
}
状態管理
リモートバックエンド設定
# backend-setup/main.tf
# 状態管理用のS3バケットとDynamoDBテーブルを作成
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-${data.aws_caller_identity.current.account_id}"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
CI/CD統合
GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths:
- 'terraform/**'
pull_request:
branches: [main]
paths:
- 'terraform/**'
env:
TF_VERSION: '1.6.0'
AWS_REGION: 'ap-northeast-1'
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
working-directory: terraform/environments/dev
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
if: github.event_name == 'pull_request'
run: terraform plan -no-color -input=false
working-directory: terraform/environments/dev
continue-on-error: true
- name: Update Pull Request
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format 🖌 \`${{ steps.fmt.outcome }}\`
#### Terraform Init ⚙️ \`${{ steps.init.outcome }}\`
#### Terraform Validate 🤖 \`${{ steps.validate.outcome }}\`
#### Terraform Plan 📖 \`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`terraform
${{ steps.plan.outputs.stdout }}
\`\`\`
</details>`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
working-directory: terraform/environments/dev
ベストプラクティス
命名規則
# 一貫した命名規則
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_vpc" "main" {
tags = {
Name = "${local.name_prefix}-vpc"
}
}
resource "aws_subnet" "public" {
tags = {
Name = "${local.name_prefix}-public-subnet-${count.index + 1}"
}
}
データソースの活用
# 既存リソースの参照
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["existing-vpc"]
}
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.existing.id]
}
filter {
name = "tag:Tier"
values = ["Private"]
}
}
条件分岐
# 環境による条件分岐
resource "aws_nat_gateway" "main" {
count = var.environment == "prod" ? length(local.azs) : 1
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
}
# 動的ブロック
resource "aws_security_group" "example" {
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
コマンドリファレンス
# 初期化
terraform init
# フォーマット
terraform fmt -recursive
# バリデーション
terraform validate
# プラン確認
terraform plan -out=plan.tfplan
# 適用
terraform apply plan.tfplan
# 破棄
terraform destroy
# 状態の確認
terraform state list
terraform state show aws_vpc.main
# インポート
terraform import aws_vpc.main vpc-12345678
# 出力の確認
terraform output
terraform output -json
まとめ
Terraformを使ったインフラ構築のポイントをまとめます。
設計原則
- モジュール化: 再利用可能なコンポーネント設計
- 環境分離: 環境ごとに独立した状態管理
- 命名規則: 一貫性のある命名
- ドキュメント: 変数の説明を充実させる
運用のポイント
- リモートバックエンド: S3 + DynamoDBで状態管理
- CI/CD統合: GitHub Actionsでの自動化
- プランレビュー: 適用前に必ず確認
- バージョン管理: プロバイダーとTerraformのバージョン固定
インフラのコード化により、再現性・追跡可能性・自動化が実現できます。