0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【IaC超入門】30日でAWS CloudFormationとTerraformをマスターするロードマップ - 23日目: 実践!TerraformでWordPress環境を構築する(EC2, RDS)

Posted at

実践!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環境を構築しました。この実践を通じて、以下の重要なポイントを理解できたはずです:

学習ポイント

  1. Multi-AZ構成の重要性: 障害時の可用性向上のため複数AZに分散配置
  2. セキュリティグループの適切な設定: 最小権限の原則に従った通信制御
  3. Load Balancerの活用: 単一障害点を避ける負荷分散とヘルスチェック
  4. Auto Scalingの設定: トラフィック変動に対する自動スケーリング
  5. Secretsの安全な管理: パスワードやAPIキーの暗号化と管理
  6. Infrastructure as Code: 再現可能で文書化されたインフラ管理

本番環境への発展

  • セキュリティ: WAF、SSL/TLS、セキュリティ監査
  • 監視: CloudWatch Alarms、ログ管理、パフォーマンス監視
  • バックアップ: 自動バックアップ、災害対策
  • CI/CD: GitHubActions、AWS CodePipeline との連携
  • コスト最適化: Reserved Instances、Spot Instances の活用

このTerraform設定をベースに、要件に応じてカスタマイズし、本格的な本番環境を構築してください。

次回は、terraform importコマンドを使って既存リソースをTerraformで管理する方法を解説します。お楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?