はじめに
前回の記事 では 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 構成とは一部異なります
本記事では学習用途およびコスト削減を目的として、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 install や pip 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.tf(aws_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_instance と aws_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リソースマップから以下のように確認できます
作成したAuto Scaling Group 、EC2インスタンスおよびRDSインスタンスについても、AWSコンソール上で以下のように確認できます
10-4. 動作確認
# アプリ URL を確認
terraform output app_url
出力例:
app_url = "http://web3tier-demo-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com"
ブラウザで表示された URL にアクセスし、以下が確認できれば成功です
手順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
| 項目 | 意味 | 今回の状態 |
|---|---|---|
| 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
スケールアウトのテスト
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 使用率を確認します
- CloudWatch のメトリクス画面で「EC2」を選択
- 「Auto Scaling グループ別メトリクス」をクリック
- 「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
すべてのインスタンスが 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 によるセキュリティ強化








