実践!TerraformでWordPress環境を構築する(EC2, RDS)
はじめに
今回は、これまでのTerraformの知識を総動員して、より実践的なインフラ構築に挑戦します。具体的には、Terraformを使ってWordPressのブログ環境をAWS上に自動で構築します。これにより、EC2インスタンスとRDS(データベース)の連携方法を学び、IaCの真価を体験します。
注意: この記事のコードは学習目的であり、本番環境で使用する際は、追加のセキュリティ対策と冗長化が必要です。
構築するアーキテクチャ
以下のリソースをTerraformで構築します:
- VPC: 仮想ネットワーク
- パブリック/プライベートサブネット: EC2とRDSをそれぞれ配置(複数AZに分散)
- インターネットゲートウェイ: パブリックサブネットをインターネットに接続
- NATゲートウェイ: プライベートサブネットからのアウトバウンド通信用
- ルートテーブル: ネットワークトラフィックのルーティングを管理
- セキュリティグループ: EC2とRDS間の通信を制御
- EC2インスタンス: WordPressアプリケーションサーバー
- RDSインスタンス: MySQLデータベース(Multi-AZ対応)
アーキテクチャ図
Internet
|
Internet Gateway
|
Public Subnet (AZ-1a) --- Public Subnet (AZ-1c)
| |
EC2 Instance NAT Gateway
| |
Private Subnet (AZ-1a) --- Private Subnet (AZ-1c)
|
RDS Instance
ファイル構成の説明
複数の設定ファイルに分けてコードを記述することで、可読性と管理性を高めます。以下のような役割分担になっています:
-
versions.tf
: Terraformとプロバイダーのバージョン管理 -
variables.tf
: 入力変数の定義 -
locals.tf
: ローカル値と共通設定 -
data.tf
: 外部データソースの取得 -
network.tf
: ネットワークリソース(VPC、サブネット等) -
security.tf
: セキュリティグループの定義 -
rds.tf
: データベースリソース -
compute.tf
: EC2とAuto Scaling設定 -
load-balancer.tf
: ロードバランサー設定 -
outputs.tf
: 出力値の定義
各ファイルの詳細説明
versions.tf
- バージョン管理とプロバイダー設定
このファイルでは、Terraformとプロバイダーのバージョンを固定し、安定したデプロイを保証します。
# Terraformとプロバイダーのバージョン要件を定義
# バージョンを固定することで、チーム間での一貫性を保つ
terraform {
required_version = ">= 1.0" # Terraform本体のバージョン要件
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # AWS Provider v5.x系を使用(マイナーバージョン互換)
}
random = {
source = "hashicorp/random"
version = "~> 3.1" # パスワード生成用
}
}
}
# AWSプロバイダーの設定
provider "aws" {
region = var.aws_region # 変数で指定されたリージョンを使用
# すべてのリソースに共通で付与されるタグ
# コスト管理や運用管理で重要
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform" # Terraformで管理されていることを明示
}
}
}
variables.tf
- 入力変数の定義
このファイルでは、設定をカスタマイズ可能にするための変数を定義します。
# デプロイ先のAWSリージョン
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1" # 東京リージョンをデフォルト
}
# プロジェクト名(リソース名のプレフィックスに使用)
variable "project_name" {
description = "Name of the project"
type = string
default = "wordpress-blog"
}
# 環境名(dev、staging、prodなど)
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
# EC2インスタンスのタイプ
# t3.microは無料利用枠対象、本番環境ではより大きなインスタンスを検討
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
# RDSインスタンスのクラス
# db.t3.microは開発用、本番では性能要件に応じて選択
variable "db_instance_type" {
description = "RDS instance class"
type = string
default = "db.t3.micro"
}
# データベースのマスターユーザー名
variable "db_username" {
description = "RDS master username"
type = string
default = "wpuser"
sensitive = true # ログ出力時に隠される
}
# Webサーバーへのアクセスを許可するCIDRブロック
# 本番環境では適切なIP範囲に制限することを強く推奨
variable "allowed_cidr_blocks" {
description = "CIDR blocks allowed to access the web server"
type = list(string)
default = ["0.0.0.0/0"] # 全てのIPからのアクセスを許可(開発用)
}
# SSH接続用のキーペア名(オプション)
variable "key_name" {
description = "EC2 Key Pair name for SSH access"
type = string
default = "" # 空の場合はSSHアクセス無し
}
# 使用するアベイラビリティゾーン
# 高可用性のため複数AZを使用
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
locals.tf
- ローカル値と共通設定
計算値や複雑な設定を事前に定義し、コードの重複を避けます。
locals {
# VPC全体のCIDRブロック(10.0.0.0/16で65536個のIPアドレス)
vpc_cidr = "10.0.0.0/16"
# パブリックサブネットの設定
# インターネットからアクセス可能、ALBやNAT Gatewayを配置
public_subnets = {
"public-1a" = {
cidr_block = "10.0.1.0/24" # 256個のIPアドレス
availability_zone = var.availability_zones[0]
}
"public-1c" = {
cidr_block = "10.0.2.0/24" # 256個のIPアドレス
availability_zone = var.availability_zones[1]
}
}
# プライベートサブネットの設定
# インターネットから直接アクセス不可、EC2やRDSを配置
private_subnets = {
"private-1a" = {
cidr_block = "10.0.10.0/24" # 256個のIPアドレス
availability_zone = var.availability_zones[0]
}
"private-1c" = {
cidr_block = "10.0.20.0/24" # 256個のIPアドレス
availability_zone = var.availability_zones[1]
}
}
# 共通で使用するタグ
# リソースの管理や課金管理に使用
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
data.tf
- 外部データソースの取得
AWSから動的に情報を取得し、最新の状態を反映します。
# 最新のAmazon Linux 2 AMIを自動取得
# AMI IDはリージョンごとに異なり、定期的に更新されるため動的取得が重要
data "aws_ami" "amazon_linux" {
most_recent = true # 最新版を取得
owners = ["amazon"] # Amazon公式のAMIのみ
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"] # Amazon Linux 2の命名規則
}
filter {
name = "virtualization-type"
values = ["hvm"] # Hardware Virtual Machine(現在の標準)
}
}
# 現在のAWSアカウントIDを取得
# IAMポリシーやリソースARNの構築に使用
data "aws_caller_identity" "current" {}
# 現在のリージョン情報を取得
# リソース名やARNの構築に使用
data "aws_region" "current" {}
network.tf
- ネットワークリソース
VPC、サブネット、ゲートウェイなどのネットワーク基盤を構築します。
# VPC(Virtual Private Cloud)
# AWS上に独立したネットワーク空間を作成
resource "aws_vpc" "main" {
cidr_block = local.vpc_cidr
enable_dns_hostnames = true # DNS名前解決を有効(RDSエンドポイントに必要)
enable_dns_support = true # DNS解決を有効
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-vpc"
})
}
# インターネットゲートウェイ
# VPCをインターネットに接続するためのゲートウェイ
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-igw"
})
}
# パブリックサブネット
# インターネットからの直接アクセスが可能なサブネット
# ALB、NAT Gateway、Bastion Hostなどを配置
resource "aws_subnet" "public" {
for_each = local.public_subnets # 複数サブネットを一括作成
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
map_public_ip_on_launch = true # 起動時にパブリックIP自動割り当て
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-${each.key}"
Type = "Public" # 運用時の識別用タグ
})
}
# プライベートサブネット
# インターネットから直接アクセス不可、NAT Gateway経由でアウトバウンド通信
# アプリケーションサーバーやデータベースを配置
resource "aws_subnet" "private" {
for_each = local.private_subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-${each.key}"
Type = "Private"
})
}
# NAT Gateway用のElastic IP
# 静的パブリックIPアドレス(NAT Gatewayに必要)
resource "aws_eip" "nat" {
domain = "vpc" # VPC用のEIP
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-nat-eip"
})
depends_on = [aws_internet_gateway.main] # IGWが先に作成される必要
}
# NAT Gateway
# プライベートサブネットのリソースがインターネットにアクセスするためのゲートウェイ
# ソフトウェア更新、外部API呼び出しなどに使用
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public["public-1a"].id # パブリックサブネットに配置
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-nat"
})
depends_on = [aws_internet_gateway.main]
}
# パブリックルートテーブル
# パブリックサブネット用のルーティング設定
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
# デフォルトルート(0.0.0.0/0)をInternet Gatewayに向ける
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-public-rt"
})
}
# プライベートルートテーブル
# プライベートサブネット用のルーティング設定
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
# デフォルトルート(0.0.0.0/0)をNAT Gatewayに向ける
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-private-rt"
})
}
# パブリックサブネットとルートテーブルの関連付け
# サブネットに対してルーティング規則を適用
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
# プライベートサブネットとルートテーブルの関連付け
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
security.tf
- セキュリティグループの定義
ネットワークレベルのファイアウォール設定を定義します。
# ALB(Application Load Balancer)用セキュリティグループ
# インターネットからのHTTP/HTTPSトラフィックを受信
resource "aws_security_group" "alb" {
name = "${var.project_name}-${var.environment}-alb-sg"
description = "Security group for Application Load Balancer"
vpc_id = aws_vpc.main.id
# HTTP(ポート80)のインバウンドを許可
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
}
# HTTPS(ポート443)のインバウンドを許可
# SSL証明書設定時に使用
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
}
# 全てのアウトバウンドトラフィックを許可
# ALBからEC2への通信に必要
egress {
from_port = 0
to_port = 0
protocol = "-1" # 全プロトコル
cidr_blocks = ["0.0.0.0/0"] # 全ての宛先
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-alb-sg"
})
}
# EC2(Webサーバー)用セキュリティグループ
# ALBからのトラフィックのみを受信(セキュリティの向上)
resource "aws_security_group" "web" {
name = "${var.project_name}-${var.environment}-web-sg"
description = "Security group for WordPress EC2 instances"
vpc_id = aws_vpc.main.id
# ALBからのHTTP(ポート80)トラフィックのみ許可
# security_groupsパラメータで特定のセキュリティグループからのみ許可
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
# SSH接続用(キーペアが指定されている場合のみ)
# dynamic blockで条件付きルール作成
dynamic "ingress" {
for_each = var.key_name != "" ? [1] : [] # キーが設定されている場合のみ
content {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
}
}
# 全てのアウトバウンドトラフィックを許可
# ソフトウェア更新、外部API通信に必要
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-web-sg"
})
}
# RDS(データベース)用セキュリティグループ
# EC2からのデータベース接続のみを許可
resource "aws_security_group" "rds" {
name = "${var.project_name}-${var.environment}-rds-sg"
description = "Security group for RDS MySQL instance"
vpc_id = aws_vpc.main.id
# EC2セキュリティグループからのMySQLトラフィック(ポート3306)のみ許可
# 最小権限の原則に従い、必要最小限のアクセスのみ許可
ingress {
description = "MySQL from EC2"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
# RDSはアウトバウンド通信を行わないため、egressルールなし
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-rds-sg"
})
}
rds.tf
- データベースリソース
WordPressのデータを格納するMySQLデータベースを設定します。
# RDS用サブネットグループ
# RDSを配置するサブネットを指定(マルチAZ配置に必要)
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-${var.environment}-db-subnet-group"
subnet_ids = [for subnet in aws_subnet.private : subnet.id] # 全プライベートサブネット
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-db-subnet-group"
})
}
# データベース用ランダムパスワード生成
# 強固なパスワードを自動生成(手動設定によるセキュリティリスクを回避)
resource "random_password" "db_password" {
length = 16 # 16文字のパスワード
special = true # 特殊文字を含む
# override_special = "!#$%&*()-_=+[]{}<>:?" # 使用する特殊文字をカスタマイズ可能
}
# RDSインスタンス(MySQL)
resource "aws_db_instance" "wordpress" {
identifier = "${var.project_name}-${var.environment}-db"
# エンジン設定
engine = "mysql"
engine_version = "8.0" # MySQL 8.0系(WordPressと互換性良好)
instance_class = var.db_instance_type
# ストレージ設定
allocated_storage = 20 # 初期ストレージ容量(GB)
max_allocated_storage = 100 # 自動拡張の上限(GB)
storage_type = "gp2" # General Purpose SSD
storage_encrypted = true # 保存時暗号化を有効
# データベース設定
db_name = "wordpress" # 初期データベース名
username = var.db_username # マスターユーザー名
password = random_password.db_password.result # 生成されたパスワード
# ネットワーク設定
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false # パブリックアクセスを無効(セキュリティ向上)
# バックアップ設定
backup_retention_period = 7 # 7日間のバックアップ保持
backup_window = "03:00-04:00" # JST 12:00-13:00(UTC+9)
maintenance_window = "sun:04:00-sun:05:00" # JST 日曜13:00-14:00
# 可用性設定
multi_az = false # 開発環境用。本番環境ではtrueを推奨
# 削除設定(開発・学習用)
# 本番環境では以下を変更:
# skip_final_snapshot = false
# final_snapshot_identifier = "${var.project_name}-${var.environment}-final-snapshot"
# deletion_protection = true
skip_final_snapshot = true
deletion_protection = false
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-database"
})
}
user-data.sh
- EC2初期化スクリプト
EC2インスタンス起動時に実行されるスクリプトです。WordPressの自動インストールと設定を行います。
#!/bin/bash
# システムパッケージを最新に更新
# セキュリティパッチやバグ修正を適用
yum update -y
# 必要なパッケージのインストール
# httpd: Apache Webサーバー
# php: PHPランタイム
# php-mysql: MySQL接続用のPHP拡張
# mariadb: MySQLクライアント(WordPressセットアップ用)
# wget: ファイルダウンロード用
yum install -y httpd php php-mysql mariadb wget
# Apacheサービスの開始と自動起動設定
systemctl start httpd
systemctl enable httpd
# WordPressのダウンロードとインストール
cd /var/www/html
# WordPressの最新版をダウンロード
wget https://wordpress.org/latest.tar.gz
# アーカイブを展開
tar -xzf latest.tar.gz
# ファイルをドキュメントルートに移動
mv wordpress/* .
# 不要なファイルを削除
rm -rf wordpress latest.tar.gz
# WordPressの設定ファイルを作成
cp wp-config-sample.php wp-config.php
# データベース情報をwp-config.phpに設定
# Terraformから渡された変数を使用してsedコマンドで置換
sed -i "s/database_name_here/${db_name}/" wp-config.php
sed -i "s/username_here/${db_username}/" wp-config.php
sed -i "s/password_here/${db_password}/" wp-config.php
sed -i "s/localhost/${db_host}/" wp-config.php
# セキュリティキーと塩の設定
# WordPressの認証システムを強化
curl -s https://api.wordpress.org/secret-key/1.1/salt/ >> wp-config.php
# ファイルの所有権と権限を設定
# apache:apache でApacheからアクセス可能に
chown -R apache:apache /var/www/html/
chmod -R 755 /var/www/html/
# Apacheサービスを再起動して設定を反映
systemctl restart httpd
# 任意: WordPressのパーマリンク設定のため.htaccessファイルのmod_rewriteを有効化
# sed -i '/<Directory "\/var\/www\/html">/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/httpd/conf/httpd.conf
# systemctl restart httpd
compute.tf
- EC2とAuto Scaling設定
Webサーバーの設定とスケーラビリティを管理します。
# IAMロール(EC2インスタンス用)
# EC2がAWSサービス(CloudWatch、SSMなど)にアクセスするためのロール
resource "aws_iam_role" "ec2_role" {
name = "${var.project_name}-${var.environment}-ec2-role"
# EC2サービスがこのロールを引き受けることを許可
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
tags = local.common_tags
}
# SSM(Systems Manager)用のポリシーアタッチメント
# Session ManagerやPatch Managerなどの機能を使用可能
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# CloudWatch Agent用のポリシーアタッチメント
# システムメトリクスやログの収集に使用
resource "aws_iam_role_policy_attachment" "cloudwatch" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}
# IAMインスタンスプロファイル
# EC2インスタンスにIAMロールを関連付けるためのリソース
resource "aws_iam_instance_profile" "ec2_profile" {
name = "${var.project_name}-${var.environment}-ec2-profile"
role = aws_iam_role.ec2_role.name
tags = local.common_tags
}
# Launch Template
# Auto Scaling Groupでインスタンスをスケールアウトするためのテンプレート
resource "aws_launch_template" "wordpress" {
name_prefix = "${var.project_name}-${var.environment}-" # 名前の接頭辞
image_id = data.aws_ami.amazon_linux.id # 最新のAmazon Linux AMI
instance_type = var.instance_type
# セキュリティグループの指定
vpc_security_group_ids = [aws_security_group.web.id]
# SSH接続用キーペア(指定されている場合のみ)
key_name = var.key_name != "" ? var.key_name : null
# IAMインスタンスプロファイルの指定
iam_instance_profile {
name = aws_iam_instance_profile.ec2_profile.name
}
# User Dataスクリプト
# インスタンス起動時にWordPressをインストール・設定
# templatefile関数でスクリプト内の変数を置換
user_data = base64encode(templatefile("${path.module}/user-data.sh", {
db_name = aws_db_instance.wordpress.db_name
db_username = aws_db_instance.wordpress.username
db_password = random_password.db_password.result
db_host = aws_db_instance.wordpress.endpoint
}))
# インスタンス起動時のタグ設定
tag_specifications {
resource_type = "instance"
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-wordpress"
})
}
# ライフサイクル設定
# 新しいバージョンを作成してから古いものを削除(ダウンタイム最小化)
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Group
# トラフィック負荷に応じてEC2インスタンスを自動でスケール
resource "aws_autoscaling_group" "wordpress" {
name = "${var.project_name}-${var.environment}-asg"
vpc_zone_identifier = [for subnet in aws_subnet.public : subnet.id] # パブリックサブネットに配置
target_group_arns = [aws_lb_target_group.wordpress.arn] # ALBのターゲットグループに登録
health_check_type = "ELB" # ALBのヘルスチェックを使用
health_check_grace_period = 300 # 起動から300秒はヘルスチェック猶予期間
# スケーリング設定
min_size = 1 # 最小インスタンス数
max_size = 3 # 最大インスタンス数
desired_capacity = 1 # 希望インスタンス数
# Launch Templateの指定
launch_template {
id = aws_launch_template.wordpress.id
version = "$Latest" # 常に最新バージョンを使用
}
# Auto Scaling Group自体のタグ
tag {
key = "Name"
value = "${var.project_name}-${var.environment}-wordpress-asg"
propagate_at_launch = false # ASG自体のみにタグ付与
}
# インスタンスに伝播するタグ
dynamic "tag" {
for_each = local.common_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true # 起動されるインスタンスにもタグ付与
}
}
}
load-balancer.tf
- ロードバランサー設定
高可用性とトラフィック分散を提供するALB(Application Load Balancer)を設定します。
# Application Load Balancer
# レイヤー7(HTTP/HTTPS)でのロードバランシング
# 複数のEC2インスタンス間でトラフィックを分散
resource "aws_lb" "main" {
name = "${var.project_name}-${var.environment}-alb"
internal = false # インターネット向け(外部からアクセス可能)
load_balancer_type = "application" # ALB(HTTP/HTTPS対応)
security_groups = [aws_security_group.alb.id]
subnets = [for subnet in aws_subnet.public : subnet.id] # パブリックサブネットに配置
# 削除保護(本番環境では true を推奨)
enable_deletion_protection = false
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-alb"
})
}
# Target Group
# ALBがトラフィックを転送する先のEC2インスタンスグループ
resource "aws_lb_target_group" "wordpress" {
name = "${var.project_name}-${var.environment}-tg"
port = 80 # ターゲット(EC2)のリスニングポート
protocol = "HTTP"
vpc_id = aws_vpc.main.id
# ヘルスチェック設定
# インスタンスの健全性を監視し、異常なインスタンスを自動で除外
health_check {
enabled = true
healthy_threshold = 2 # 2回連続成功でHealthy判定
interval = 30 # 30秒間隔でチェック
matcher = "200,302" # HTTP 200または302を正常レスポンスとして判定
path = "/" # ヘルスチェックパス(WordPressトップページ)
port = "traffic-port" # トラフィックと同じポートを使用
protocol = "HTTP"
timeout = 5 # 5秒でタイムアウト
unhealthy_threshold = 2 # 2回連続失敗でUnhealthy判定
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-tg"
})
}
# Listener
# ALBがリクエストを受信し、ターゲットグループに転送するためのルール
resource "aws_lb_listener" "wordpress" {
load_balancer_arn = aws_lb.main.arn
port = "80" # リスニングポート
protocol = "HTTP"
# デフォルトアクション: 全てのトラフィックをwordpress target groupに転送
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.wordpress.arn
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-listener"
})
}
# 本番環境向けの追加設定(オプション)
# HTTPS Listener(SSL証明書が必要)
# resource "aws_lb_listener" "wordpress_https" {
# load_balancer_arn = aws_lb.main.arn
# port = "443"
# protocol = "HTTPS"
# ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
# certificate_arn = aws_acm_certificate.main.arn # SSL証明書のARN
#
# default_action {
# type = "forward"
# target_group_arn = aws_lb_target_group.wordpress.arn
# }
# }
# HTTP to HTTPS リダイレクト(SSL使用時)
# resource "aws_lb_listener" "wordpress_redirect" {
# load_balancer_arn = aws_lb.main.arn
# port = "80"
# protocol = "HTTP"
#
# default_action {
# type = "redirect"
#
# redirect {
# port = "443"
# protocol = "HTTPS"
# status_code = "HTTP_301" # 恒久的リダイレクト
# }
# }
# }
outputs.tf
- 出力値の定義
デプロイ完了後に重要な情報を出力し、他のシステムとの連携や運用に活用します。
# VPC ID
# 他のTerraformモジュールや手動作業でVPCを参照する際に使用
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
# Application Load BalancerのDNS名
# WordPressサイトにアクセスするためのエンドポイント
output "load_balancer_dns_name" {
description = "DNS name of the load balancer"
value = aws_lb.main.dns_name
}
# WordPressサイトの完全なURL
# ブラウザでアクセス可能な形式でURL出力
output "load_balancer_url" {
description = "URL of the WordPress site"
value = "http://${aws_lb.main.dns_name}"
}
# RDSエンドポイント
# データベースへの直接接続や監視設定で使用
# sensitive = true でログ出力時に隠される
output "rds_endpoint" {
description = "RDS instance endpoint"
value = aws_db_instance.wordpress.endpoint
sensitive = true # 機密情報として扱う
}
# データベース名
# アプリケーション設定やバックアップ設定で参照
output "database_name" {
description = "Database name"
value = aws_db_instance.wordpress.db_name
}
# データベースユーザー名
# 監査ログやアクセス管理で参照
output "database_username" {
description = "Database master username"
value = aws_db_instance.wordpress.username
sensitive = true
}
# Auto Scaling GroupのARN
# CloudWatch Alarmsや他のAWSサービスとの連携で使用
output "autoscaling_group_arn" {
description = "ARN of the Auto Scaling Group"
value = aws_autoscaling_group.wordpress.arn
}
# Application Load BalancerのARN
# WAF設定やCloudWatch監視設定で使用
output "load_balancer_arn" {
description = "ARN of the Application Load Balancer"
value = aws_lb.main.arn
}
# パブリックサブネットID一覧
# 追加リソースのデプロイ時に参照
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = [for subnet in aws_subnet.public : subnet.id]
}
# プライベートサブネットID一覧
# データベースやアプリケーションサーバーの追加デプロイ時に参照
output "private_subnet_ids" {
description = "IDs of the private subnets"
value = [for subnet in aws_subnet.private : subnet.id]
}
デプロイと確認手順
1. 前提条件の確認
デプロイ前に以下を確認してください:
- AWS CLI が設定済み(
aws configure
でアクセスキーとシークレットキーを設定) - Terraform がインストール済み(v1.0以上)
- 適切なIAM権限(EC2、VPC、RDS、IAM、ELBの作成権限)
2. ファイルの配置
すべての .tf
ファイルと user-data.sh
を同一ディレクトリに配置します。
wordpress-terraform/
├── versions.tf
├── variables.tf
├── locals.tf
├── data.tf
├── network.tf
├── security.tf
├── rds.tf
├── compute.tf
├── load-balancer.tf
├── outputs.tf
└── user-data.sh
3. 初期化
# Terraformの初期化(プロバイダーのダウンロード)
terraform init
4. 実行計画の確認
# 作成されるリソースを事前確認
terraform plan
# 変数を指定して実行計画を確認
terraform plan -var="project_name=my-wordpress" -var="environment=production"
5. デプロイ実行
# インフラをデプロイ
terraform apply
# 自動承認でデプロイ(注意: 本番環境では非推奨)
terraform apply -auto-approve
6. WordPressへのアクセス
デプロイ完了後(通常5-10分)、以下のコマンドでWordPressのURLを取得:
terraform output load_balancer_url
出力されたURLにブラウザでアクセスし、WordPressの初期セットアップを完了します。
7. データベース情報の確認(必要時)
# データベースのエンドポイント
terraform output rds_endpoint
# データベース名
terraform output database_name
# データベースユーザー名
terraform output database_username
本番環境向けのセキュリティ強化
1. Secrets Manager の使用
データベースパスワードをより安全に管理:
# secrets.tf
resource "aws_secretsmanager_secret" "db_password" {
name = "${var.project_name}/${var.environment}/db/password"
description = "Database password for WordPress"
# パスワードの自動ローテーション設定(オプション)
# rotation_lambda_arn = aws_lambda_function.rotate_secret.arn
# rotation_rules {
# automatically_after_days = 30
# }
tags = local.common_tags
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = jsonencode({
username = var.db_username
password = random_password.db_password.result
})
}
# EC2からSecrets Managerにアクセスするためのポリシー
resource "aws_iam_role_policy" "secrets_access" {
name = "${var.project_name}-${var.environment}-secrets-policy"
role = aws_iam_role.ec2_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = aws_secretsmanager_secret.db_password.arn
}
]
})
}
2. SSL/TLS証明書の追加
HTTPS通信を有効化:
# ssl.tf
# Route 53 Hosted Zone(ドメインが既に登録済みの場合)
data "aws_route53_zone" "main" {
count = var.domain_name != "" ? 1 : 0
name = var.domain_name
}
# ACM SSL証明書
resource "aws_acm_certificate" "main" {
count = var.domain_name != "" ? 1 : 0
domain_name = var.domain_name
validation_method = "DNS"
subject_alternative_names = [
"*.${var.domain_name}" # ワイルドカード証明書
]
lifecycle {
create_before_destroy = true
}
tags = local.common_tags
}
# DNS検証用のRoute 53レコード
resource "aws_route53_record" "cert_validation" {
for_each = var.domain_name != "" ? {
for dvo in aws_acm_certificate.main[0].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
} : {}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.main[0].zone_id
}
# 証明書の検証完了待機
resource "aws_acm_certificate_validation" "main" {
count = var.domain_name != "" ? 1 : 0
certificate_arn = aws_acm_certificate.main[0].arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
3. WAF (Web Application Firewall) の追加
Webアプリケーションを攻撃から保護:
# waf.tf
resource "aws_wafv2_web_acl" "main" {
name = "${var.project_name}-${var.environment}-waf"
scope = "REGIONAL" # ALB用はREGIONAL
# デフォルトアクション: 明示的にブロックされていないリクエストは許可
default_action {
allow {}
}
# AWSマネージドルール: 一般的な攻撃パターンをブロック
rule {
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 1
override_action {
none {} # ルールセットをそのまま適用
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
# 特定のルールを無効化したい場合
# excluded_rule {
# name = "SizeRestrictions_BODY"
# }
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "commonRuleSetMetric"
sampled_requests_enabled = true
}
}
# AWSマネージドルール: WordPress特化の保護
rule {
name = "AWS-AWSManagedRulesWordPressRuleSet"
priority = 2
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesWordPressRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "wordPressRuleSetMetric"
sampled_requests_enabled = true
}
}
# レート制限ルール: 1つのIPから短時間で大量のリクエストをブロック
rule {
name = "RateLimitRule"
priority = 3
action {
block {}
}
statement {
rate_based_statement {
limit = 2000 # 5分間で2000リクエスト以上をブロック
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "rateLimitMetric"
sampled_requests_enabled = true
}
}
tags = local.common_tags
}
# WAFをALBに関連付け
resource "aws_wafv2_web_acl_association" "main" {
resource_arn = aws_lb.main.arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}
4. CloudWatch監視とアラート
システムの健全性を監視:
# monitoring.tf
# CloudWatch Log Group(Apache/PHPログ用)
resource "aws_cloudwatch_log_group" "apache" {
name = "/aws/ec2/${var.project_name}/${var.environment}/apache"
retention_in_days = 14 # 14日間ログ保持
tags = local.common_tags
}
# CPU使用率アラート
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "${var.project_name}-${var.environment}-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "300" # 5分間
statistic = "Average"
threshold = "80" # CPU使用率80%以上
alarm_description = "This metric monitors ec2 cpu utilization"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.wordpress.name
}
# SNS通知(事前にSNSトピック作成が必要)
# alarm_actions = [aws_sns_topic.alerts.arn]
tags = local.common_tags
}
# RDS CPU使用率アラート
resource "aws_cloudwatch_metric_alarm" "rds_cpu" {
alarm_name = "${var.project_name}-${var.environment}-rds-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/RDS"
period = "300"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors RDS CPU utilization"
dimensions = {
DBInstanceIdentifier = aws_db_instance.wordpress.id
}
tags = local.common_tags
}
# ALBのレスポンス時間監視
resource "aws_cloudwatch_metric_alarm" "alb_response_time" {
alarm_name = "${var.project_name}-${var.environment}-alb-high-response-time"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "TargetResponseTime"
namespace = "AWS/ApplicationELB"
period = "300"
statistic = "Average"
threshold = "1" # 1秒以上
alarm_description = "This metric monitors ALB response time"
dimensions = {
LoadBalancer = aws_lb.main.arn_suffix
}
tags = local.common_tags
}
料金最適化のポイント
1. リソースサイジングの最適化
# 開発環境用の小さなリソース
terraform apply -var="instance_type=t3.nano" -var="db_instance_type=db.t3.micro"
# 本番環境用のリソース例
terraform apply -var="instance_type=t3.medium" -var="db_instance_type=db.t3.small"
2. 利用時間の管理
開発環境では使用しない時間帯にリソースを停止:
# Lambda関数でスケジュール停止(例)
resource "aws_lambda_function" "stop_instances" {
count = var.environment == "dev" ? 1 : 0
filename = "stop_instances.zip"
function_name = "${var.project_name}-${var.environment}-stop-instances"
role = aws_iam_role.lambda_role[0].arn
handler = "index.handler"
runtime = "python3.9"
# 毎日19:00 JSTにインスタンス停止
# CloudWatch Events(EventBridge)で定期実行
}
3. 月額料金概算(東京リージョン)
リソース | 設定 | 月額概算 |
---|---|---|
EC2 t3.micro | 24時間稼働 | $10-12 |
RDS db.t3.micro | 20GB、24時間稼働 | $15-20 |
ALB | 基本料金 + データ処理 | $20-25 |
NAT Gateway | データ転送量に依存 | $45-50 |
その他(EIP、VPC等) | - | $5-10 |
合計 | $95-120 |
クリーンアップとベストプラクティス
1. リソースの削除
# 全リソースを削除
terraform destroy
# 特定のリソースのみ削除
terraform destroy -target=aws_nat_gateway.main
2. 状態管理のベストプラクティス
# terraform.tf (Remote State設定例)
terraform {
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "wordpress/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
# DynamoDBテーブルによる状態ロック
dynamodb_table = "terraform-state-locks"
}
}
3. 環境別設定ファイル
# 環境別の変数ファイル
# terraform.tfvars.dev
project_name = "wordpress-blog"
environment = "dev"
instance_type = "t3.micro"
allowed_cidr_blocks = ["203.0.113.0/24"]
# terraform.tfvars.prod
project_name = "wordpress-blog"
environment = "prod"
instance_type = "t3.medium"
db_instance_type = "db.t3.small"
allowed_cidr_blocks = ["203.0.113.0/24", "198.51.100.0/24"]
# 実行時に環境指定
terraform apply -var-file="terraform.tfvars.dev"
まとめ
今回は、Terraformを使って本格的なWordPress環境を構築しました。この実践を通じて、以下の重要なポイントを理解できたはずです:
学習ポイント
- Multi-AZ構成の重要性: 障害時の可用性向上のため複数AZに分散配置
- セキュリティグループの適切な設定: 最小権限の原則に従った通信制御
- Load Balancerの活用: 単一障害点を避ける負荷分散とヘルスチェック
- Auto Scalingの設定: トラフィック変動に対する自動スケーリング
- Secretsの安全な管理: パスワードやAPIキーの暗号化と管理
- Infrastructure as Code: 再現可能で文書化されたインフラ管理
本番環境への発展
- セキュリティ: WAF、SSL/TLS、セキュリティ監査
- 監視: CloudWatch Alarms、ログ管理、パフォーマンス監視
- バックアップ: 自動バックアップ、災害対策
- CI/CD: GitHubActions、AWS CodePipeline との連携
- コスト最適化: Reserved Instances、Spot Instances の活用
このTerraform設定をベースに、要件に応じてカスタマイズし、本格的な本番環境を構築してください。
次回は、terraform import
コマンドを使って既存リソースをTerraformで管理する方法を解説します。お楽しみに!