TerraformでEC2を2AZ・2台構成にして学んだMulti-AZ設計
はじめに
Terraformで構築しているAWS環境について、これまで1台構成だったEC2を、2つのAvailability Zone(AZ)に1台ずつ配置する構成へ変更しました。
今回の変更では、単純にEC2を2台に増やすだけではなく、Target Group、CloudWatch Alarm、Output、GitHub Actions、Ansible実行時に参照するInstance IDなど、EC2を参照している周辺設定にも修正が必要になりました。
また、今回の実装を通して、Multi-AZは「システム全体を一括でMulti-AZ化する」というより、どの層を複数AZに分散しているのかを意識して考える必要があると理解できました。
変更前の構成
変更前は、ALB配下にEC2を1台配置していました。
Internet
|
ALB
|
EC2
|
RDS
EC2はPrivate Subnetに配置し、インターネットから直接アクセスできない構成としています。
変更後の構成
今回、EC2を1aと1cのPrivate Subnetに1台ずつ配置しました。
ALB
/ \
/ \
EC2(1a) EC2(1c)
\ /
\ /
RDS
ALBから2台のEC2へ通信を振り分けることで、EC2の片方に障害が発生した場合でも、もう1台で通信を継続できる構成を目指しています。
EC2を2台作るためにmapとfor_eachを使う
EC2を1aと1cに1台ずつ作成するため、AZごとのSubnet IDをキーと値で管理し、for_eachで処理する構成にしました。
最初はmap、key、value、eachの関係がよく分かっていなかったため、ここで整理します。
mapとは
mapは、キーと値をセットで管理するデータの集合です。
今回の構成では、以下のようにAZを表す名前とPrivate Subnet IDを対応させています。
locals {
app_instances = {
"1a" = module.network.private_subnet_1a_id
"1c" = module.network.private_subnet_1c_id
}
}
イメージすると以下の関係です。
key value
1a → Private Subnet 1aのID
1c → Private Subnet 1cのID
1aや1cがキー、Private Subnet IDが値です。
listとの違い
listは、要素を番号(インデックス)で扱います。
例えば、
subnets = [
module.network.private_subnet_1a_id,
module.network.private_subnet_1c_id
]
であれば、
[0] → Private Subnet 1a
[1] → Private Subnet 1c
という形で参照します。
一方、今回のようにキーと値で管理すると、
"1a" → Private Subnet 1a
"1c" → Private Subnet 1c
のように、AZを表す名前で各要素を識別できます。
今回の構成では、EC2を1a、1cという名前で識別したかったため、この形を採用しました。
for_eachでEC2を2台作成する
EC2側では、先ほどのlocal.app_instancesをfor_eachで処理します。
resource "aws_instance" "app" {
for_each = local.app_instances
subnet_id = each.value
tags = {
Name = "AwsStudyEC2-${each.key}"
}
}
local.app_instancesには以下の2つの要素があります。
1a → Private Subnet 1aのID
1c → Private Subnet 1cのID
for_eachを使うことで、それぞれの要素を使ってEC2を作成できます。
each.keyとeach.value
for_eachを使用したリソースでは、
each.key
each.value
で、現在扱っているキーと値を参照できます。
例えば1a側では、
each.key = "1a"
each.value = Private Subnet 1aのID
となります。
そのため、
subnet_id = each.value
ではEC2を配置するSubnetを指定し、
Name = "AwsStudyEC2-${each.key}"
ではAZを識別できる名前を付けています。
結果として、以下のような名前になります。
AwsStudyEC2-1a
AwsStudyEC2-1c
なぜcountではなくfor_eachを使ったのか
EC2を複数作るだけであれば、countを使う方法もあります。
countではリソースを、
0
1
2
といったインデックスで識別します。
一方、for_eachでは、
aws_instance.app["1a"]
aws_instance.app["1c"]
のようにキーでリソースを識別できます。
今回はAZとEC2の対応関係を明確にしたかったため、番号ではなく1a、1cという名前で管理できるfor_eachを選択しました。
EC2を2台にしたことで影響した箇所
EC2を2台にすると、EC2リソースだけ修正すれば終わりではありませんでした。
EC2を参照している周辺設定についても、1台前提の記述を複数台に対応させる必要がありました。
主に確認・修正した箇所は以下です。
- ALB Target Group Attachment
- EC2のOutput
- CloudWatch Alarm
- GitHub Actions
- Ansible実行時に使用するEC2 Instance ID
今回の作業で、あるリソースを複数台化するときは、そのリソースを参照している箇所まで影響範囲を確認する必要があると学びました。
Target Group Attachmentも複数台対応
2台のEC2をTarget Groupに登録するため、Target Group Attachmentでもfor_eachを使用しました。
resource "aws_lb_target_group_attachment" "app" {
for_each = aws_instance.app
target_group_arn = aws_lb_target_group.this.arn
target_id = each.value.id
port = 8080
}
ここではaws_instance.appに含まれる各EC2を1台ずつ処理しています。
target_id = each.value.id
によって、それぞれのEC2のInstance IDをTarget Groupへ登録します。
これにより、1aと1cのEC2を同じTarget Groupのターゲットとして扱えるようになりました。
Outputも複数台対応
EC2が1台のときは、Instance IDやPrivate IPを1つの値として出力していました。
EC2をfor_eachで複数台作成したため、Outputについても複数台に対応させる必要があります。
そこで、各EC2のキーとInstance IDを対応させた形で出力するよう変更しました。
output "ec2_instance_ids" {
value = {
for key, instance in aws_instance.app :
key => instance.id
}
}
for key, instance in aws_instance.appは何をしているのか
aws_instance.appは、for_eachによって作成された複数のEC2リソースを表しています。
イメージすると、以下のようにキーとEC2リソースが対応しています。
"1a" → EC2(1a)の情報
"1c" → EC2(1c)の情報
次の部分では、
for key, instance in aws_instance.app :
各要素を1つずつ取り出し、
key = キー
instance = そのキーに対応するEC2リソース
として扱っています。
例えば1aの場合は、
key = "1a"
instance = EC2(1a)の情報
というイメージです。
keyやinstanceという変数名は自分で決めることができます。
instance.idとは
instanceにはInstance IDだけではなく、EC2リソースが持つ複数の属性を参照できる情報が入っています。
例えば、
instance.id
instance.private_ip
instance.subnet_id
instance.arn
などを参照できます。
そのため、
key => instance.id
は、
左側の
keyを新しいmapのキーにし、右側のinstance.idを値にする
という意味になります。
例えば、
key = "1a"
instance.id = "i-xxxxxxxx"
であれば、
"1a" → "i-xxxxxxxx"
という組み合わせが作られます。
同じ処理を各EC2に対して行うため、最終的には以下のように出力されます。
ec2_instance_ids = {
"1a" = "i-xxxxxxxx"
"1c" = "i-yyyyyyyy"
}
つまり、このfor式はInstance IDだけを最初から取り出しているわけではありません。
まずaws_instance.appからキーとEC2リソースを1つずつ取り出し、最後のinstance.idでEC2のどの属性を出力するかを選択していると理解しました。
例えば、Private IPを出力したければ、
output "ec2_private_ips" {
value = {
for key, instance in aws_instance.app :
key => instance.private_ip
}
}
とすることで、
1a → Private IP
1c → Private IP
という形でも出力できます。
CloudWatch AlarmもEC2ごとに作成
CPU使用率やStatus CheckのAlarmも、これまではEC2が1台であることを前提としていました。
こちらも複数台を前提とした構成に変更し、1aと1cそれぞれのEC2を監視できるようにしました。
例として、Alarm名は以下のようにEC2ごとに分けています。
AwsStudy-Ec2HighCpu-1a
AwsStudy-Ec2HighCpu-1c
AwsStudy-Ec2StatusCheckFailed-1a
AwsStudy-Ec2StatusCheckFailed-1c
EC2を複数台にする場合は、リソースの作成だけでなく、監視についても「どのEC2を監視しているのか」を識別できるようにする必要があります。
GitHub ActionsやAnsible側も1台前提を見直す
Terraform側でEC2を2台にしても、後続処理が1つのInstance IDだけを前提としていると、片方のEC2にしか処理できません。
そのため、GitHub ActionsやAnsibleについても、Terraformから複数のInstance IDを取得して処理できるように見直しました。
今回の変更を通して、IaCでリソースを複数台化するときは、Terraformコードだけではなく、CI/CDや構成管理まで含めて依存関係を追う必要があると分かりました。
Multi-AZについて整理したこと
今回、EC2を1aと1cに1台ずつ配置したため、ALB配下のアプリケーション層は複数AZに分散した構成になりました。
ただし、現在の環境では、すべてのリソースをMulti-AZ化しているわけではありません。
現在の構成は以下です。
ALB : 複数AZ
EC2 : 1a / 1c
NAT Gateway : 1aに1台(Zonal)
RDS : Single-AZ
そのため、
システム全体を完全にMulti-AZ化した
というより、
EC2を2つのAZに配置し、
アプリケーション層をMulti-AZ化した
と説明する方が正確だと理解しました。
Multi-AZを考えるときは、単に「2つのAZを使っているか」ではなく、どのリソースに単一障害点が残っているのかを見る必要があります。
NAT Gatewayまで高可用性を考える場合
現在の構成では、1aにZonal NAT Gatewayを1台配置しています。
Private Subnet 1a ─┐
├─ NAT Gateway(1a)→ Internet
Private Subnet 1c ─┘
この構成では、EC2は1aと1cに分散していますが、インターネット向けの外向き通信は1aのNAT Gatewayに依存しています。
Zonal NAT Gatewayを使ってAZ障害への耐性を高める場合は、例えば次のようにAZごとにNAT Gatewayを配置し、それぞれのPrivate Subnetから同一AZのNAT Gatewayへルーティングする方法があります。
Private Subnet 1a → NAT Gateway 1a → Internet
Private Subnet 1c → NAT Gateway 1c → Internet
また、現在のAWSには、複数AZへ自動的に展開するRegional NAT Gatewayという選択肢もあります。
今回の環境では学習用途とコストのバランスを考え、既存のZonal NAT Gateway 1台構成を維持しています。
RDSまでMulti-AZ化する場合
現在のRDSは以下の設定です。
multi_az = false
RDS DB InstanceをMulti-AZ構成にする場合は、
multi_az = true
とすることで、別AZにStandby DB Instanceを持つMulti-AZ DB Instance Deploymentにできます。
このStandbyは主に高可用性とフェイルオーバーのために使用され、通常の読み取り処理を分散するためのRead Replicaとは役割が異なります。
現在は学習環境のコストを考慮してSingle-AZとしています。
今回の検証状況
Terraformをapplyし、EC2を1aと1cに1台ずつ作成した2台構成で、実際に動作することまで確認しました。
今回の作業では、コード上で複数台に見えるだけではなく、実際のAWS環境で2台構成として動作するところまで確認できたため、EC2の2AZ・2台化は完了としています。
一方、システム全体として見ると、NAT GatewayとRDSには単一AZ構成が残っています。
そのため、今回のゴールは「システム全体の完全なMulti-AZ化」ではなく、EC2を中心としたアプリケーション層のMulti-AZ化としています。
今後さらに検証する場合は、EC2を片方停止した状態でALB経由の通信継続を確認するなど、障害時の挙動まで確認したいと考えています。
まとめ
今回、EC2を1台から2台に変更する中で、以下を学びました。
-
mapとfor_eachを使った複数リソースの管理 -
each.keyとeach.valueの役割 - Terraformの
for式を使った複数リソースのOutput -
instance.idやinstance.private_ipなど、リソース属性の参照方法 - EC2を複数台化すると、Target Group、監視、Output、CI/CDなど周辺設定にも影響すること
- Multi-AZは「どの層を複数AZに分散しているのか」を明確にする必要があること
- 可用性を高めるほど、構成やコストとのトレードオフが発生すること
最初は「EC2を2台に増やす」という変更だけを考えていましたが、実際にはEC2を参照している周辺リソースやCI/CDにも影響がありました。
今回の変更を通して、Terraformの書き方だけでなく、構成全体の依存関係と、Multi-AZを設計するときの考え方を整理することができました。