0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform で構築する AWS Web3層構成を Auto Scaling 対応にする【ALB + EC2 + RDS】

0
Last updated at Posted at 2026-05-16

はじめに

前回の記事 では Terraform で ALB + EC2 + RDS の Web3層構成を構築しました

前回の構成では EC2 が単体のため、インスタンス障害やトラフィック増加に対応できません

本記事ではその続編として、EC2 を Auto Scaling 対応に変更します

前回との違い

項目 前回(単体 EC2) 今回(Auto Scaling)
EC2 起動設定 aws_instance aws_launch_template
インスタンス管理 手動 Auto Scaling Group が自動管理
AZ 分散 単一 AZ 複数 AZ に自動分散
障害時 手動復旧 自動置き換え
スケーリング 手動 CPU 使用率に応じて自動
ALB 登録 手動(aws_lb_target_group_attachment ASG が自動登録

この記事でわかること

  • Launch Template を使った EC2 起動設定の定義
  • Auto Scaling Group による複数 AZ への EC2 分散配置
  • ALB Target Group への自動登録
  • CPU 使用率によるスケールアウト・スケールインの設定
  • 単体 EC2 構成からの移行手順

前提条件

  • 前回の記事 の内容を理解していること
  • Terraform v1.5 以上がインストール済みであること
  • AWS CLI がインストール・設定済みであること

なお、本記事で使用するソースコードは、以下のGitHubリポジトリからダウンロードできます。

terraform-web3tier-asg-demo(GitHubリポジトリ)

本記事で構築するシステム

アーキテクチャ概要

Terraform で構築する AWS Web3層構成を Auto Scaling 対応にする【ALB + EC2 + RDS】_実務3.png

※上図は実運用を想定した冗長化イメージを含んでいるため、本記事の Terraform 構成とは一部異なります

本記事では学習用途およびコスト削減を目的として、NAT Gateway は 1 台構成、RDS は Single-AZ(1台構成)で構築しています

そのため、Private Subnet AZ-c 側の EC2 も、AZ-a に配置された NAT Gateway を経由してインターネットへ接続します

また、DB Subnet Group には複数 AZ の Private Subnet を登録していますが、multi_az = false を設定しているため、RDS インスタンス自体は 1 台のみ作成されます

本番環境では可用性向上のため、以下の構成を推奨します

  • NAT Gateway の AZ 冗長化
  • multi_az = true による RDS Multi-AZ 構成
  • deletion_protection = true の有効化

構成のポイント

Internet
    ↓
ALB(Public Subnet AZ-a / AZ-c)
    ↓
Auto Scaling Group
├─ EC2(Private Subnet AZ-a)- Flask アプリ
└─ EC2(Private Subnet AZ-c)- Flask アプリ
    ↓
RDS MySQL(Private Subnet AZ-a / AZ-c)
項目 内容
ネットワーク VPC, Public Subnet ×2, Private Subnet ×2
ALB インターネット向け、HTTP(80)
EC2 Private Subnet に複数 AZ 分散配置、Flask アプリ実行
RDS Private Subnet に配置、MySQL 8.0
セキュリティ 最小権限の原則に基づくSG設計
外部通信 NAT Gateway 経由で dnf/pip install 可能
スケーリング CPU 70% 超でスケールアウト、30% 未満でスケールイン

必要なAWSリソース

分類 必要なもの 役割
ネットワーク VPC 全体のネットワーク
Public Subnet ×2 ALB 配置用
Private Subnet ×2 EC2 / RDS 配置用
Internet Gateway ALB をインターネット公開
NAT Gateway Private EC2 から外部へ接続
Route Table 通信経路の設定
通信 ALB 外部アクセスを受ける入口
Target Group ALB から EC2 へ転送
Listener HTTP:80 などを受け付ける
サーバー Launch Template EC2 起動設定のテンプレート
Auto Scaling Group EC2 の自動管理・AZ 分散
Scaling Policy スケールアウト・イン条件
DB RDS MySQL データ保存
DB Subnet Group RDS を複数 Private Subnet に配置
セキュリティ Security Group 通信制御
運用 IAM Role EC2 から AWS 操作
監視 CloudWatch Alarm CPU 使用率・ALB エラー監視

セキュリティグループの考え方

対象 許可する通信
ALB SG Internet から HTTP:80
EC2 SG ALB SG から Flaskポート 5000
RDS SG EC2 SG から MySQL 3306

ルートの考え方

Public Subnet

  • 0.0.0.0/0 → Internet Gateway

Private Subnet

  • 0.0.0.0/0 → NAT Gateway

※Private EC2で dnf installpip install をするなら必要です。

プロジェクトのディレクトリ構成

terraform-web3tier-asg-demo/
├── main.tf              # プロバイダー・データソース
├── versions.tf          # バージョン管理
├── variables.tf         # 変数定義
├── terraform.tfvars     # 変数値(Git 管理外推奨)
├── vpc.tf               # VPC・サブネット・IGW・NAT
├── security_groups.tf   # セキュリティグループ
├── alb.tf               # ALB・ターゲットグループ・リスナー
├── ec2.tf               # ★ Launch Template + Auto Scaling Group
├── rds.tf               # RDS MySQL
├── cloudwatch.tf        # ★ 新規追加:スケーリングアラーム
└── outputs.tf           # 出力値

前回からの変更点は ec2.tfaws_instance → Launch Template + ASG)と cloudwatch.tf(新規追加)です。

手順1: プロジェクトの初期化

mkdir terraform-web3tier-asg-demo
cd terraform-web3tier-asg-demo

# ファイルを一括作成
"main.tf","versions.tf","variables.tf","terraform.tfvars","vpc.tf","security_groups.tf","alb.tf","ec2.tf","rds.tf","cloudwatch.tf","outputs.tf" | % {New-Item -ItemType File -Name $_}

手順2: 基本設定ファイルの作成

2-1. versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

2-2. main.tf

provider "aws" {
  region = var.region
}

data "aws_caller_identity" "current" {}

data "aws_availability_zones" "available" {
  state = "available"
}

2-3. variables.tf

前回から Auto Scaling 関連の変数を追加しています

variable "region" {
  description = "AWS リージョン"
  type        = string
  default     = "ap-northeast-1"
}

variable "project_name" {
  description = "プロジェクト名"
  type        = string
  default     = "web3tier-demo"
}

variable "ec2_instance_type" {
  description = "EC2 インスタンスタイプ"
  type        = string
  default     = "t3.micro"
}

variable "db_instance_class" {
  description = "RDS インスタンスクラス"
  type        = string
  default     = "db.t3.micro"
}

variable "db_name" {
  description = "データベース名"
  type        = string
  default     = "flaskdb"
}

variable "db_username" {
  description = "データベースユーザー名"
  type        = string
  default     = "flaskuser"
}

variable "db_password" {
  description = "データベースパスワード"
  type        = string
  sensitive   = true
}

# ★ 追加:Auto Scaling 関連
variable "asg_min_size" {
  description = "Auto Scaling 最小インスタンス数"
  type        = number
  default     = 2
}

variable "asg_max_size" {
  description = "Auto Scaling 最大インスタンス数"
  type        = number
  default     = 4
}

variable "asg_desired_capacity" {
  description = "Auto Scaling 希望インスタンス数"
  type        = number
  default     = 2
}

2-4. terraform.tfvars

region            = "ap-northeast-1"
project_name      = "web3tier-demo"
ec2_instance_type = "t3.micro"
db_instance_class = "db.t3.micro"
db_name           = "flaskdb"
db_username       = "flaskuser"
db_password       = "YourSecurePassword123!"  # 本番環境では環境変数を使用

# ★ 追加:Auto Scaling 関連
asg_min_size         = 2
asg_max_size         = 4
asg_desired_capacity = 2

⚠️ terraform.tfvars.gitignore に追加してください

手順3: VPC とネットワークの構築(vpc.tf)

Public Subnet(ALB用)と Private Subnet(EC2・RDS用)を作成します

# ==================================================
# VPC
# ==================================================
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# ==================================================
# Public Subnet(ALB 用)
# ==================================================
resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-1"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = data.aws_availability_zones.available.names[1]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-2"
  }
}

# ==================================================
# Private Subnet(EC2・RDS 用)
# ==================================================
resource "aws_subnet" "private_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.11.0/24"
  availability_zone = data.aws_availability_zones.available.names[0]

  tags = {
    Name = "${var.project_name}-private-1"
  }
}

resource "aws_subnet" "private_2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.12.0/24"
  availability_zone = data.aws_availability_zones.available.names[1]

  tags = {
    Name = "${var.project_name}-private-2"
  }
}

# ==================================================
# Internet Gateway
# ==================================================
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

# ==================================================
# Elastic IP for NAT Gateway
# ==================================================
resource "aws_eip" "nat" {
  domain = "vpc"

  tags = {
    Name = "${var.project_name}-nat-eip"
  }

  depends_on = [aws_internet_gateway.main]
}

# ==================================================
# NAT Gateway
# ==================================================
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public_1.id

  tags = {
    Name = "${var.project_name}-nat"
  }

  depends_on = [aws_internet_gateway.main]
}

# ==================================================
# Route Table - Public
# ==================================================
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}-public-rt"
  }
}

resource "aws_route_table_association" "public_1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_2" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.public.id
}

# ==================================================
# Route Table - Private
# ==================================================
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }

  tags = {
    Name = "${var.project_name}-private-rt"
  }
}

resource "aws_route_table_association" "private_1" {
  subnet_id      = aws_subnet.private_1.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private_2" {
  subnet_id      = aws_subnet.private_2.id
  route_table_id = aws_route_table.private.id
}

NAT Gateway を使うことで、Private Subnet の EC2 から外部への通信(パッケージインストールなど)が可能になります

手順4: セキュリティグループの作成(security_groups.tf)

3層それぞれに対して、最小権限の原則に基づいたセキュリティグループを設定します

# ==================================================
# ALB 用セキュリティグループ
# ==================================================
resource "aws_security_group" "alb" {
  name        = "${var.project_name}-alb-sg"
  description = "Security group for ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP from Internet"
    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}-alb-sg"
  }
}

# ==================================================
# EC2 用セキュリティグループ
# ==================================================
resource "aws_security_group" "ec2" {
  name        = "${var.project_name}-ec2-sg"
  description = "Security group for EC2 instances"
  vpc_id      = aws_vpc.main.id

  # ALB からの Flask ポート 5000 のみ許可
  ingress {
    description     = "Flask from ALB"
    from_port       = 5000
    to_port         = 5000
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-ec2-sg"
  }
}

# ==================================================
# RDS 用セキュリティグループ
# ==================================================
resource "aws_security_group" "rds" {
  name        = "${var.project_name}-rds-sg"
  description = "Security group for RDS"
  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.ec2.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-rds-sg"
  }
}

EC2 へのインバウンドは ALB からのポート 5000 のみ、RDS へのインバウンドは EC2 からのポート 3306 のみ許可しています
SSH が必要な場合は SSM Session Manager の利用を推奨します

手順5: ALB の構築(alb.tf)

# ==================================================
# ALB 本体
# ==================================================
resource "aws_lb" "main" {
  name               = "${var.project_name}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [aws_subnet.public_1.id, aws_subnet.public_2.id]

  tags = {
    Name = "${var.project_name}-alb"
  }
}

# ==================================================
# ターゲットグループ
# ==================================================
resource "aws_lb_target_group" "main" {
  name        = "${var.project_name}-tg"
  port        = 5000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "instance"

  health_check {
    path                = "/health"
    protocol            = "HTTP"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    matcher             = "200"
  }

  tags = {
    Name = "${var.project_name}-tg"
  }
}

# ==================================================
# HTTP リスナー
# ==================================================
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

ターゲットグループのポートを 5000 に設定し、Flask アプリのヘルスチェックエンドポイント /health を指定しています

手順6: EC2 の Auto Scaling 対応(ec2.tf)

前回の aws_instanceaws_lb_target_group_attachment を削除し、Launch Template + Auto Scaling Group に置き換えます

Launch Template とは

Launch Template は EC2 インスタンスの起動設定をテンプレート化したものです
AMI・インスタンスタイプ・セキュリティグループ・ユーザーデータなどを一元管理できます
Auto Scaling Group はこのテンプレートを使ってインスタンスを自動的に起動します

単体 EC2 構成との比較

前回(単体 EC2)

aws_instance → aws_lb_target_group_attachment(手動登録)

今回(Auto Scaling)

aws_launch_template → aws_autoscaling_group(ALB へ自動登録)
# ==================================================
# Amazon Linux 2023 の最新 AMI を取得
# ==================================================
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# ==================================================
# EC2 用 IAM ロール(SSM アクセス)
# ==================================================
resource "aws_iam_role" "ec2" {
  name = "${var.project_name}-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ec2_ssm" {
  role       = aws_iam_role.ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2" {
  name = "${var.project_name}-ec2-profile"
  role = aws_iam_role.ec2.name
}

# ==================================================
# Launch Template
# ==================================================
resource "aws_launch_template" "main" {
  name_prefix   = "${var.project_name}-lt-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = var.ec2_instance_type

  iam_instance_profile {
    name = aws_iam_instance_profile.ec2.name
  }

  network_interfaces {
    # Private Subnet に配置するためパブリック IP は不要
    associate_public_ip_address = false
    security_groups             = [aws_security_group.ec2.id]
  }

  # ==================================================
  # user_data
  # ==================================================
  user_data = base64encode(<<-EOF
#!/bin/bash
set -eux

# ログ出力
exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1

# ==================================================
# パッケージ更新
# ==================================================
dnf update -y

# ==================================================
# SSM Agent インストール、起動
# ==================================================
dnf install -y amazon-ssm-agent
systemctl enable amazon-ssm-agent || true
systemctl start amazon-ssm-agent || true

# ==================================================
# Python / pip / MySQL Client
# ==================================================
dnf install -y python3 python3-pip mariadb105

# ==================================================
# Flask アプリ用ディレクトリ
# ==================================================
mkdir -p /opt/flask-app/templates

# ==================================================
# requirements.txt
# ==================================================
cat > /opt/flask-app/requirements.txt <<'REQUIREMENTS'
Flask==3.0.0
mysql-connector-python==8.2.0
REQUIREMENTS

# ==================================================
# Python ライブラリインストール
# ==================================================
pip3 install -r /opt/flask-app/requirements.txt

# ==================================================
# Flask アプリ作成
# ==================================================
cat > /opt/flask-app/app.py <<'APPPY'
from flask import Flask, render_template, request, redirect, url_for
import mysql.connector
import os
import time

app = Flask(__name__)

db_config = {
    'host': os.getenv('DB_HOST', 'localhost'),
    'user': os.getenv('DB_USER', 'flaskuser'),
    'password': os.getenv('DB_PASSWORD', 'flaskpass'),
    'database': os.getenv('DB_NAME', 'flaskdb')
}

def get_db_connection():
    max_retries = 5
    retry_interval = 2

    for attempt in range(max_retries):
        try:
            conn = mysql.connector.connect(**db_config)
            return conn

        except mysql.connector.Error as err:
            if attempt < max_retries - 1:
                print(f"DB接続失敗(試行 {attempt + 1}/{max_retries}): {err}")
                time.sleep(retry_interval)
            else:
                raise

@app.route('/')
def index():
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)

        cursor.execute(
            'SELECT * FROM users ORDER BY created_at DESC'
        )

        users = cursor.fetchall()

        cursor.close()
        conn.close()

        return render_template(
            'index.html',
            users=users,
            error=None
        )

    except Exception as e:
        return render_template(
            'index.html',
            users=[],
            error=str(e)
        )

@app.route('/add', methods=['POST'])
def add_user():
    name = request.form.get('name')
    email = request.form.get('email')

    if not name or not email:
        return redirect(url_for('index'))

    try:
        conn = get_db_connection()
        cursor = conn.cursor()

        cursor.execute(
            'INSERT INTO users (name, email) VALUES (%s, %s)',
            (name, email)
        )

        conn.commit()

        cursor.close()
        conn.close()

    except Exception as e:
        print(f"ユーザー追加エラー: {e}")

    return redirect(url_for('index'))

@app.route('/delete/<int:user_id>')
def delete_user(user_id):
    try:
        conn = get_db_connection()
        cursor = conn.cursor()

        cursor.execute(
            'DELETE FROM users WHERE id = %s',
            (user_id,)
        )

        conn.commit()

        cursor.close()
        conn.close()

    except Exception as e:
        print(f"ユーザー削除エラー: {e}")

    return redirect(url_for('index'))

@app.route('/health')
def health():
    return {'status': 'healthy'}, 200

if __name__ == '__main__':
    app.run(
        host='0.0.0.0',
        port=5000,
        debug=False
    )
APPPY

# ==================================================
# 環境変数
# ==================================================
cat > /opt/flask-app/.env <<ENV
DB_HOST=${aws_db_instance.main.address}
DB_USER=${var.db_username}
DB_PASSWORD=${var.db_password}
DB_NAME=${var.db_name}
ENV

# ==================================================
# RDS 起動待ち
# ==================================================
sleep 120

# ==================================================
# users テーブル作成
# ==================================================
mysql -h ${aws_db_instance.main.address} \
  -u ${var.db_username} \
  -p${var.db_password} \
  ${var.db_name} <<'SQL'

CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- サンプルデータを挿入
INSERT INTO users (name, email) VALUES
('YamadaTaro', 'taro@example.com'),
('SatoHanako', 'hanako@example.com'),
('SuzukiJiro', 'jiro@example.com')
ON DUPLICATE KEY UPDATE
name = VALUES(name);

SQL

# ==================================================
# HTML 作成
# ==================================================
cat > /opt/flask-app/templates/index.html <<'HTML'
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ユーザー管理アプリ</title>
</head>
<body>

<h1>ユーザー管理アプリ</h1>

{% if error %}
<p style="color:red;">
  エラー: {{ error }}
</p>
{% endif %}

<form action="/add" method="POST">
    <input
      type="text"
      name="name"
      placeholder="名前"
      required
    >

    <input
      type="email"
      name="email"
      placeholder="メールアドレス"
      required
    >

    <button type="submit">
      登録
    </button>
</form>

<h2>ユーザー一覧</h2>

{% if users %}
<ul>
{% for user in users %}
<li>
  {{ user.name }} / {{ user.email }}

  <a
    href="{{ url_for('delete_user', user_id=user.id) }}"
    onclick="return confirm('このユーザーを削除しますか?');"
  >
    削除
  </a>
</li>
{% endfor %}
</ul>

{% else %}
<p>ユーザーが登録されていません</p>
{% endif %}

</body>
</html>
HTML

# ==================================================
# systemd サービス
# ==================================================
cat > /etc/systemd/system/flask-app.service <<'SERVICE'
[Unit]
Description=Flask Application
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/flask-app
EnvironmentFile=/opt/flask-app/.env

ExecStart=/usr/bin/python3 /opt/flask-app/app.py

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
SERVICE

# ==================================================
# Flask 起動
# ==================================================
systemctl daemon-reload
systemctl enable flask-app
systemctl start flask-app

EOF
  )

  # ==================================================
  # EC2 タグ
  # ==================================================
  tag_specifications {
    resource_type = "instance"

    tags = {
      Name = "${var.project_name}-ec2"
    }
  }

  # ==================================================
  # Launch Template 更新時
  # ==================================================
  lifecycle {
    create_before_destroy = true
  }
}

# ==================================================
# Auto Scaling Group
# ==================================================
resource "aws_autoscaling_group" "main" {
  name             = "${var.project_name}-asg"
  min_size         = var.asg_min_size
  max_size         = var.asg_max_size
  desired_capacity = var.asg_desired_capacity

  # Private Subnet の複数 AZ に分散配置
  vpc_zone_identifier = [aws_subnet.private_1.id, aws_subnet.private_2.id]

  # ALB ターゲットグループへ自動登録
  target_group_arns = [aws_lb_target_group.main.arn]

  # ALB のヘルスチェック結果で異常インスタンスを自動置き換え
  health_check_type         = "ELB"
  health_check_grace_period = 300

  launch_template {
    id      = aws_launch_template.main.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "${var.project_name}-asg"
    propagate_at_launch = true
  }
}

# ==================================================
# Auto Scaling ポリシー(CPU 使用率ベース)
# ==================================================
resource "aws_autoscaling_policy" "scale_out" {
  name                   = "${var.project_name}-scale-out"
  autoscaling_group_name = aws_autoscaling_group.main.name
  adjustment_type        = "ChangeInCapacity"
  scaling_adjustment     = 1
  cooldown               = 300
}

resource "aws_autoscaling_policy" "scale_in" {
  name                   = "${var.project_name}-scale-in"
  autoscaling_group_name = aws_autoscaling_group.main.name
  adjustment_type        = "ChangeInCapacity"
  scaling_adjustment     = -1
  cooldown               = 300
}

Launch Template のポイント

設定 内容
associate_public_ip_address = false Private Subnet に配置するためパブリック IP 不要
create_before_destroy = true テンプレート更新時に新しいものを先に作成
version = "$Latest" 常に最新バージョンのテンプレートを使用
${aws_db_instance.main.address} RDS エンドポイントを Terraform が自動埋め込み(前回は手動設定が必要だった)

Auto Scaling Group のポイント

設定 内容
vpc_zone_identifier Private Subnet を複数指定して AZ 分散
target_group_arns ALB への自動登録(前回の aws_lb_target_group_attachment が不要に)
health_check_type = "ELB" ALB のヘルスチェック結果で異常インスタンスを自動置き換え
health_check_grace_period = 300 起動後 300 秒はヘルスチェックを猶予(Flask 起動待ち)

手順7: CloudWatch アラームの設定(cloudwatch.tf)

CPU 使用率に応じてスケールアウト・スケールインをトリガーするアラームと、ALB の 5xx エラーを監視するアラームを設定します

# ==================================================
# CPU 使用率 高(スケールアウトトリガー)
# ==================================================
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  alarm_name          = "${var.project_name}-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 60
  statistic           = "Average"
  threshold           = 70
  alarm_description   = "CPU 使用率が 70% を超えた場合にスケールアウト"
  alarm_actions       = [aws_autoscaling_policy.scale_out.arn]

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.main.name
  }
}

# ==================================================
# CPU 使用率 低(スケールイントリガー)
# ==================================================
resource "aws_cloudwatch_metric_alarm" "cpu_low" {
  alarm_name          = "${var.project_name}-cpu-low"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 60
  statistic           = "Average"
  threshold           = 30
  alarm_description   = "CPU 使用率が 30% を下回った場合にスケールイン"
  alarm_actions       = [aws_autoscaling_policy.scale_in.arn]

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.main.name
  }
}

# ==================================================
# ALB 5xx エラー監視
# ==================================================
resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
  alarm_name          = "${var.project_name}-alb-5xx"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "HTTPCode_Target_5XX_Count"
  namespace           = "AWS/ApplicationELB"
  period              = 60
  statistic           = "Sum"
  threshold           = 10
  alarm_description   = "ALB の 5xx エラーが 1 分間に 10 件を超えた場合"
  treat_missing_data  = "notBreaching"

  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
  }
}

evaluation_periods = 2 は「2 回連続でしきい値を超えた場合にアラーム」を意味します
一時的なスパイクでスケーリングが発動しないよう調整しています

手順8: RDS MySQL の構築(rds.tf)

# ==================================================
# DB Subnet Group
# ==================================================
resource "aws_db_subnet_group" "main" {
  name       = "${var.project_name}-db-subnet-group"
  subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id]

  tags = {
    Name = "${var.project_name}-db-subnet-group"
  }
}

# ==================================================
# RDS MySQL
# ==================================================
resource "aws_db_instance" "main" {
  identifier        = "${var.project_name}-mysql"
  engine            = "mysql"
  engine_version    = "8.0"
  instance_class    = var.db_instance_class
  allocated_storage = 20
  storage_type      = "gp3"
  storage_encrypted = true

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  # バックアップ設定
  backup_retention_period = 7
  backup_window           = "03:00-04:00"
  maintenance_window      = "mon:04:00-mon:05:00"

  # マルチAZ(本番環境では true 推奨)
  multi_az = false

  # パブリックアクセス無効
  publicly_accessible = false

  # 削除保護(本番環境では true 推奨)
  deletion_protection = false
  skip_final_snapshot = true

  # パラメータグループ
  parameter_group_name = aws_db_parameter_group.main.name

  tags = {
    Name = "${var.project_name}-mysql"
  }
}

# ==================================================
# DB Parameter Group
# ==================================================
resource "aws_db_parameter_group" "main" {
  name   = "${var.project_name}-mysql-params"
  family = "mysql8.0"

  parameter {
    name  = "character_set_server"
    value = "utf8mb4"
  }

  parameter {
    name  = "collation_server"
    value = "utf8mb4_unicode_ci"
  }

  tags = {
    Name = "${var.project_name}-mysql-params"
  }
}

RDS は Private Subnet に配置し、publicly_accessible = false を設定することで、インターネットから直接アクセスできない構成としています
また、DB Subnet Group には複数 AZ の Private Subnet を指定しています
本記事では学習用途およびコスト削減を目的として multi_az = false としていますが、本番環境では可用性向上のため multi_az = true を推奨します
さらに、誤削除防止の観点から、本番環境では deletion_protection = true の設定を推奨します

手順9: 出力値の定義(outputs.tf)

output "app_url" {
  description = "アプリケーション URL"
  value       = "http://${aws_lb.main.dns_name}"
}

output "alb_dns_name" {
  description = "ALB の DNS 名"
  value       = aws_lb.main.dns_name
}

output "target_group_arn" {
  description = "Target Group ARN"
  value       = aws_lb_target_group.main.arn
}

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "asg_name" {
  description = "Auto Scaling Group 名"
  value       = aws_autoscaling_group.main.name
}

output "launch_template_id" {
  description = "Launch Template ID"
  value       = aws_launch_template.main.id
}

output "rds_endpoint" {
  description = "RDS エンドポイント"
  value       = aws_db_instance.main.endpoint
}

output "rds_address" {
  description = "RDS アドレス"
  value       = aws_db_instance.main.address
}

手順10: Terraform の実行

10-1. 初期化

cd terraform-web3tier-asg-demo
terraform init

10-2. 実行計画の確認

terraform plan

作成されるリソースの概要:

リソース 内容
VPC 関連 13 VPC, サブネット×4, IGW, NAT, EIP, ルートテーブル×2, 関連付け×4
セキュリティグループ 3 ALB 用, EC2 用, RDS 用
ALB 3 ALB, ターゲットグループ, HTTP リスナー
EC2 関連 6 AMI データソース, Launch Template, ASG, スケーリングポリシー×2, IAM
RDS 3 RDS インスタンス, DB Subnet Group, Parameter Group
CloudWatch 3 CPU 高, CPU 低, ALB 5xx アラーム

10-3. リソースの作成

terraform apply

yes を入力して実行します

RDS の作成には 5〜10 分かかります

terraform apply が正常に完了すると、作成したVPCをAWSコンソールのVPCリソースマップから以下のように確認できます

image.png

作成したAuto Scaling Group 、EC2インスタンスおよびRDSインスタンスについても、AWSコンソール上で以下のように確認できます

Auto Scaling Group
image.png

EC2 インスタンス(複数 AZ に分散)
image.png

RDS インスタンス
image.png

10-4. 動作確認

# アプリ URL を確認
terraform output app_url

出力例:

app_url = "http://web3tier-demo-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com"

ブラウザで表示された URL にアクセスし、以下が確認できれば成功です

image.png

手順11: Auto Scaling の動作確認

ASG の状態確認

Auto Scaling Group の現在の状態を確認します

aws autoscaling describe-auto-scaling-groups `
  --auto-scaling-group-names $(terraform output -raw asg_name) `
  --query 'AutoScalingGroups[0].{Min:MinSize,Max:MaxSize,Desired:DesiredCapacity,Instances:length(Instances)}' `
  --output table

image.png

項目 意味 今回の状態
Desired 現在、維持したいインスタンス数 2台
Instances 実際に稼働しているインスタンス数 2台
Min これ以上は減らない最低台数 2台
Max スケールアウト時の上限 最大4台

インスタンスの AZ 分散確認

複数 AZ に分散配置されているか確認します

aws autoscaling describe-auto-scaling-groups `
  --auto-scaling-group-names $(terraform output -raw asg_name) `
  --query 'AutoScalingGroups[0].Instances[*].{InstanceId:InstanceId,AZ:AvailabilityZone,State:LifecycleState}' `
  --output table

出力例:
image.png

スケールアウトのテスト

EC2 インスタンスに SSM Session Manager で接続し、CPU 負荷をかけます

# インスタンス ID の一覧を取得
aws autoscaling describe-auto-scaling-groups `
  --auto-scaling-group-names $(terraform output -raw asg_name) `
  --query 'AutoScalingGroups[0].Instances[*].InstanceId' `
  --output text
# SSM Session Manager で接続(インスタンス ID を指定)
aws ssm start-session --target i-xxxxxxxxxxxxxxxxx

# CPU 負荷をかける
sudo dnf install -y stress
stress --cpu 4 --timeout 300

CloudWatch アラームが ALARM 状態になり、インスタンスが自動追加されることを確認します

# ASG の状態確認
aws autoscaling describe-auto-scaling-groups `
  --auto-scaling-group-names $(terraform output -raw asg_name) `
  --query 'AutoScalingGroups[0].{Min:MinSize,Max:MaxSize,Desired:DesiredCapacity,Instances:length(Instances)}' `
  --output table

AWSコンソール(CloudWatch)にて、EC2インスタンスのCPU使用率 確認手順

CloudWatch のメトリクス画面で CPU 使用率を確認します

  1. CloudWatch のメトリクス画面で「EC2」を選択
  2. 「Auto Scaling グループ別メトリクス」をクリック
  3. 「CPUUtilization」にチェックを入れる

これにより、Auto Scaling Group 配下の EC2 インスタンスの CPU 使用率を確認できます

CloudWatch のメトリクスはデフォルトでは平均値(Average)で表示されるため、複数インスタンス構成では負荷が分散され、CPU 使用率が低く見える場合があります
統計値を「Maximum」に変更することで、最も負荷がかかっているインスタンスの CPU 使用率を確認できます
また、期間を 1 分に設定することで、よりリアルタイムに変化を把握できます

CPU 負荷テストにより、CPU 使用率が上昇していることが確認できます

手順12: ヘルスチェックの確認

ALB のヘルスチェックが正常に動作しているか確認します

# ターゲットの状態確認
aws elbv2 describe-target-health `
  --target-group-arn $(terraform output -raw target_group_arn) `
  --query 'TargetHealthDescriptions[*].{ID:Target.Id,AZ:Target.AvailabilityZone,State:TargetHealth.State}' `
  --output table

出力例:
image.png

すべてのインスタンスが healthy になっていれば正常です

全リソースのクリーンアップ

terraform destroy

yes を入力すると、作成したすべてのリソースが削除されます

⚠️ NAT Gateway と RDS は起動しているだけで課金されます。検証後は必ず削除してください

トラブルシューティング

1. ターゲットグループのヘルスチェックが失敗する

症状: EC2 インスタンスが unhealthy になります

解決方法:

  • Flask アプリが起動しているか確認(SSM Session Manager で接続)
  • セキュリティグループで ALB → EC2 のポート 5000 が許可されているか確認
  • RDS エンドポイントが正しく設定されているか確認
# SSM で接続して Flask の状態確認
sudo systemctl status flask-app

# ログ確認
sudo journalctl -u flask-app -n 50

# 環境変数の確認
cat /opt/flask-app/.env

2. Auto Scaling が動作しない

症状: CPU 負荷をかけてもインスタンスが増えません

解決方法:

  • CloudWatch アラームの状態を確認
aws cloudwatch describe-alarms `
  --alarm-names "web3tier-demo-cpu-high" `
  --query 'MetricAlarms[0].{State:StateValue,Reason:StateReason}' `
  --output table
  • ASG のアクティビティ履歴を確認
aws autoscaling describe-scaling-activities `
  --auto-scaling-group-name $(terraform output -raw asg_name) `
  --max-records 10 `
  --query 'Activities[*].{Time:StartTime,Status:StatusCode,Description:Description}' `
  --output table

3. 新しいインスタンスが起動しない

症状: ASG がインスタンスを起動しようとするが失敗します

解決方法:

  • Launch Template の user_data にエラーがないか確認
  • IAM ロールが正しく設定されているか確認
  • Private Subnet から NAT Gateway 経由で外部通信できるか確認
# SSM で接続して外部通信確認
curl -I https://www.google.com

4. Flask アプリが DB に接続できない

症状: アプリにアクセスすると DB 接続エラーが表示されます

解決方法:

  • 環境変数ファイルの DB_HOST が正しいか確認
  • RDS のセキュリティグループで EC2 SG からのポート 3306 が許可されているか確認
# 環境変数の確認
cat /opt/flask-app/.env

# RDS への疎通確認
nc -zv $(cat /opt/flask-app/.env | grep DB_HOST | cut -d'=' -f2) 3306

5. ALB の DNS 名でアクセスできない

症状: ブラウザでタイムアウトします

解決方法:

  • ALB のセキュリティグループでポート 80 が許可されているか確認
  • ターゲットグループのヘルスチェックが healthy になっているか確認
# ターゲットの状態確認
aws elbv2 describe-target-health `
  --target-group-arn $(terraform output -raw target_group_arn) `
  --query 'TargetHealthDescriptions[*].{ID:Target.Id,State:TargetHealth.State}' `
  --output table

単体 EC2 構成との比較まとめ

項目 単体 EC2 Auto Scaling
起動設定 aws_instance aws_launch_template
インスタンス管理 手動 ASG が自動管理
AZ 分散 単一 AZ 複数 AZ に自動分散
障害時 手動復旧 自動置き換え
スケーリング 手動 CPU 使用率に応じて自動
ALB 登録 aws_lb_target_group_attachment ASG が自動登録
RDS 設定 起動後に手動設定 user_data で自動設定
コスト 固定(1台分) 変動(負荷に応じて増減)

まとめ

本記事では、前回の単体 EC2 構成を Auto Scaling 対応に変更しました

  • Launch Template で EC2 起動設定を一元管理
  • Auto Scaling Group で複数 AZ への自動分散配置
  • ALB Target Group への自動登録(手動登録が不要に)
  • CloudWatch アラーム で CPU 使用率に応じた自動スケーリング
  • ヘルスチェック による異常インスタンスの自動置き換え

Auto Scaling により、トラフィック増加や障害に自動対応できる高可用性な構成を実現しました

この構成をベースに、以下のような拡張が可能です

  • RDS のマルチ AZ 化(multi_az = true
  • CloudFront による CDN 配信
  • Route 53 による独自ドメイン設定
  • AWS WAF によるセキュリティ強化

関連記事

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?