前回は、会社から与えられた要件をもとに、
要件 → AWS構成 → Terraformリソース
という順番で考える練習をした。
今回はそこから一歩進んで、
«「AZ障害が起きてもWebサービスを止めたくない」»
という、より実務に近い要件を考えてみる。
📋 今回の要件
会社から次のような依頼が来たとする。
«「AZ障害が起きてもWebサービスを止めたくない。」»
条件は以下の通り。
- VPCは1つ
- Public Subnetは2つ(AZ-A、AZ-C)
- Private Subnetは2つ(AZ-A、AZ-C)
- ALBは2つのPublic Subnetに配置
- EC2は2つのPrivate Subnetに1台ずつ配置
- RDSはMulti-AZを利用する
構成イメージは以下。
Internet
│
IGW
│
ALB
┌────┴────┐
│ │
Public-A Public-C
│ │
Private-A Private-C
│ │
EC2-1 EC2-2
\ /
\ /
RDS
Multi-AZ
今回のポイントは、
「1つのAZに障害が起きても、別のAZでサービスを継続できる構成」
を考えること。
🎯 今日の問題
Q1
Terraformリソースとして、新しく増えるものは何だろう?
前回はPublic Subnet、Private Subnet、EC2がそれぞれ1つだった。
今回はAZ-AとAZ-Cの2つに分ける。
前回との違いを考えてみよう。
Q2
なぜALBは、
subnets = [
PublicA,
PublicC
]
のように2つ指定する必要があるんだろう?
⭐ 今日一番重要な問題
もし、
«AZ-Aが丸ごと停止した»
とする。
このとき、
- ALB
- EC2
- RDS
はそれぞれどう動く?
「どれが止まる?どれが動き続ける?」
を、自分の言葉で説明してみよう。
✍️ 今回の回答
Q1
«EC2からRDSの通信をするためにRDSセキュリティグループ、DBサブネットグループ。»
Q2
«AZ障害が起きてもサービスが止まらないようにするにはAZを複数作る必要があるから。»
⭐ AZ-Aが停止した場合
«AZ-Aが停止したらPrivate-AのEC2-1も止まる。
しかし、ALBがAZ-Cに通信を送るので、AZ-C、Private-C、EC2-2からRDSに通信が行く。»
🔎 今回のレビュー
今回の回答で特に良かったのは、
「リソースを覚えている」から「障害が起きたときの動きまで考えている」
ところまで進んでいること。
Terraformのコードを書くだけなら、必要なリソース名を調べれば書ける。
しかし実務では、
«「この構成でAZ-Aが落ちたらどうなる?」»
という問いに答えられることが重要になる。
今回の問題は、まさにそこを確認するための問題だった。
Q1のレビュー
回答:
«EC2からRDSの通信をするためにRDSセキュリティグループ、DBサブネットグループ。»
ここはRDS Security GroupとDB Subnet Groupを思い出せている点は正しい。
ただし、今回の問題では、
«「前回と比べて、AZを2つにしたことで何が増えるか」»
を考えることがポイントだった。
そのため、今回新しく増える代表的なリソースは、
Public Subnet-A
Public Subnet-C
Private Subnet-A
Private Subnet-C
EC2-A
EC2-C
などになる。
Terraformで考えると、
resource "aws_subnet" "public_a" {}
resource "aws_subnet" "public_c" {}
resource "aws_subnet" "private_a" {}
resource "aws_subnet" "private_c" {}
resource "aws_instance" "web_a" {}
resource "aws_instance" "web_c" {}
という形になる。
ここで大事なのは、
AZを増やす=単純にAZというTerraformリソースを追加するわけではない
ということ。
AWSでは、
AZ-A
↓
Subnet-A
↓
EC2-A
AZ-C
↓
Subnet-C
↓
EC2-C
というように、各AZにリソースを分散して配置する。
Q2のレビュー
回答:
«AZ障害が起きてもサービスが止まらないようにするにはAZを複数作る必要があるから。»
これはほぼ完璧。💯
もう少し実務的に表現すると、
«ALBを異なるAZのSubnetに配置することで、片方のAZに障害が発生しても、もう一方のAZにあるALB側の構成を利用してサービスを継続できるようにする。»
ということ。
つまり、
AZ-A
ALB
↓
EC2-A
だけでは、AZ-Aに障害が起きたときにまとめて影響を受ける。
一方、
AZ-A AZ-C
ALB ALB
↓ ↓
EC2-A EC2-C
としておけば、AZ-Aに障害が起きてもAZ-C側の構成を利用できる。
これがマルチAZ構成の基本的な考え方。
⭐ 一番重要な問題のレビュー
回答:
«AZ-Aが停止したらPrivate-AのEC2-1も止まる。しかし、ALBがAZ-Cに通信を送るので、AZ-C、Private-C、EC2-2からRDSに通信が行く。»
この考え方はかなり良い。
まず、
AZ-A障害
↓
Private-Aも影響
↓
EC2-1も停止
というところは正しい。
そして、
AZ-C
↓
Private-C
↓
EC2-2
という別AZ側の構成が残る。
そのため、EC2を複数AZに分散しておくことで、片方のAZの障害時にも残ったAZ側のEC2でサービスを継続できる。
そしてRDSが重要
今回の要件には、
«RDSはMulti-AZを利用する»
という条件があった。
そのためRDSについても、
AZ-A
RDS Primary
│
│ Multi-AZ
↓
AZ-C
RDS Standby
のように、異なるAZに構成される。
仮にPrimary側のAZで障害が発生した場合、RDSはStandby側へのフェイルオーバーを行い、DBサービスを継続できる構成になる。
つまり今回の設計では、
AZ-A 障害
↓
┌──────────────┐
│ AZ-A │
│ │
│ ALB │ ← 影響を受ける可能性
│ EC2-1 │ ← 停止
│ RDS Primary │ ← 障害の影響
└──────────────┘
↓
┌──────────────┐
│ AZ-C │
│ │
│ ALB │ ← 利用継続
│ EC2-2 │ ← 利用継続
│ RDS Standby │ ← フェイルオーバー
└──────────────┘
という考え方になる。
ここで大事なのは、
「AZを2つにすること」そのものが目的ではない。
目的は、
«障害が1つのAZに閉じ込められても、サービス全体が止まらないようにすること。»
これが高可用性設計の考え方。
🧠 今回の重要ポイント
今回の問題を通して、AWSの知識が一つにつながってきた。
これまでは、
VPC
Subnet
ALB
EC2
RDS
Security Group
を、それぞれ個別に勉強してきた。
しかし実務では、それぞれを単体で考えるのではなく、
要件
↓
障害を考える
↓
必要な構成を考える
↓
AWSリソースに落とす
↓
Terraformでコード化する
という順番で考える。
今回なら、
«「AZ障害でも止めたくない」»
という要件から、
複数AZ
↓
Subnetを複数AZに分散
↓
ALBを複数AZに配置
↓
EC2を複数AZに配置
↓
RDSをMulti-AZにする
という設計につながっている。
これが設計からTerraformを組み立てるということ。
📄 完成コードのイメージ
ここでは、今回の構成をTerraformで表すとどうなるのかを確認してみる。
まだ細かい値や設定は省略しているが、全体像を見るには十分な形にしている。
provider "aws" {
region = "ap-northeast-1"
}
==================================
VPC
==================================
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
==================================
Public Subnet
==================================
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
}
resource "aws_subnet" "public_c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-1c"
}
==================================
Private Subnet
==================================
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.11.0/24"
availability_zone = "ap-northeast-1a"
}
resource "aws_subnet" "private_c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.12.0/24"
availability_zone = "ap-northeast-1c"
}
==================================
Internet Gateway
==================================
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
}
==================================
Public Route Table
==================================
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
==================================
Route Table Association
==================================
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_c" {
subnet_id = aws_subnet.public_c.id
route_table_id = aws_route_table.public.id
}
==================================
NAT Gateway
==================================
resource "aws_eip" "nat" {}
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public_a.id
}
==================================
ALB
==================================
resource "aws_lb" "alb" {
name = "sample-alb"
internal = false
load_balancer_type = "application"
subnets = [
aws_subnet.public_a.id,
aws_subnet.public_c.id
]
security_groups = [
aws_security_group.alb.id
]
}
==================================
Target Group
==================================
resource "aws_lb_target_group" "app" {
name = "sample-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
}
==================================
Listener
==================================
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.alb.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
==================================
EC2
==================================
resource "aws_instance" "web_a" {
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
subnet_id = aws_subnet.private_a.id
vpc_security_group_ids = [
aws_security_group.ec2.id
]
}
resource "aws_instance" "web_c" {
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
subnet_id = aws_subnet.private_c.id
vpc_security_group_ids = [
aws_security_group.ec2.id
]
}
==================================
Target Group Attachment
==================================
resource "aws_lb_target_group_attachment" "web_a" {
target_group_arn = aws_lb_target_group.app.arn
target_id = aws_instance.web_a.id
port = 80
}
resource "aws_lb_target_group_attachment" "web_c" {
target_group_arn = aws_lb_target_group.app.arn
target_id = aws_instance.web_c.id
port = 80
}
==================================
Security Groups
==================================
resource "aws_security_group" "alb" {
name = "alb-sg"
vpc_id = aws_vpc.main.id
ingress {
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"]
}
}
resource "aws_security_group" "ec2" {
name = "ec2-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
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"]
}
}
resource "aws_security_group" "rds" {
name = "rds-sg"
vpc_id = aws_vpc.main.id
ingress {
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"]
}
}
==================================
DB Subnet Group
==================================
resource "aws_db_subnet_group" "main" {
name = "sample-db-subnet-group"
subnet_ids = [
aws_subnet.private_a.id,
aws_subnet.private_c.id
]
}
==================================
RDS
==================================
resource "aws_db_instance" "main" {
identifier = "sample-db"
engine = "mysql"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "admin"
password = "xxxxxxxx"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [
aws_security_group.rds.id
]
publicly_accessible = false
multi_az = true
}
🔍 完成コードを設計図として読む
ここで、いきなりコードを暗記する必要はない。
むしろ、
「このコードはAWSコンソールで何を作っているんだ?」
と考えてみる。
例えば、
subnets = [
aws_subnet.public_a.id,
aws_subnet.public_c.id
]
を見たら、
ALB
↓
Public-A
Public-C
を作っている。
subnet_id = aws_subnet.private_a.id
なら、
EC2
↓
Private-A
という配置。
security_groups = [
aws_security_group.alb.id
]
なら、
ALBのSecurity Group
↓
EC2のSecurity Group
↓
ALBからの通信を許可
という通信関係。
multi_az = true
なら、
RDS
↓
Multi-AZ構成
という設計をTerraformで表現している。
つまりTerraformのコードは、
AWS構成図をコードとして表現したもの
と考えると分かりやすい。
🚀 今回の大きな学び
これまでの授業では、
Terraformのコード
↓
AWSリソース
という方向から勉強してきた。
しかし今回からは逆方向も考えられるようになってきた。
会社の要件
↓
AWSの設計
↓
構成図
↓
必要なリソース
↓
Terraformコード
この考え方ができるようになると、
「Terraformの書き方を全部覚えていないと何もできない」
という状態から抜け出せる。
Terraformの構文は調べられる。
AWSリソースの引数も公式ドキュメントで確認できる。
でも、
«「なぜこの構成にするのか?」»
は自分で考えなければならない。
だから今後は、
コードを書く能力だけではなく、コードになる前の設計能力
を鍛えていく。
📚 次回予告
ここから、いよいよTerraformらしい機能に入っていく。
今までは、
resource "aws_instance" "web_a" {}
resource "aws_instance" "web_c" {}
のように、同じようなリソースを1つずつ書いていた。
では会社から、
«「Webサーバーを3台に増やして。ただしTerraformのコードはなるべく増やしたくない。」»
と言われたらどうする?
ここで登場するのが、
count
や
for_each
だ。
例えば、
resource "aws_instance" "web" {
count = 3
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
}
と書けば、同じリソースを複数作ることができる。
つまり、
Terraform
↓
同じコードを繰り返す
↓
EC2 × 3台
ということができる。
ここからは、
「AWSを設計する」
だけではなく、
「その設計をTerraformらしく効率的にコード化する」
という段階に進んでいく。
これがTerraformの面白いところ。
次回から、いよいよ
«Terraformだからできること»
を掘り下げていこう。🔥