AWSマルチアカウント環境下のTerraformにおける、ダイナミックに切り替え可能なAZの実装方法
こんにちは。
株式会社ソーシャルPLUSでインフラエンジニアをやっている、あーるです。
主にAWS、Terraform、kubernetes周りのことを書いていこうと思っています。
概要
弊社では長らくAWSシングルアカウント構成(1アカウントにStaging/Productionが同居)を採用していました。
しかし、この構成では誤操作によるリソースの停止や削除のリスクが高まり、また、コストの管理が難しいといった課題が生じていました。
これらの課題を解消するため、既存のアカウントのStagingリソースを、新しく作成したAWSアカウントに移行する作業を行っています。
本記事ではその際に発生した新旧アカウントで利用可能なアベイラビリティーゾーン(AZ)が異なる問題に対処し、Terraformコードを新旧アカウントで共通に保つための実装について述べます。
動作環境
- Terraform v1.6.0
- AWS Provider v5.22.0
東京リージョンで使えるAZについて
東京リージョンでは、通常、以下の4つのAZが利用可能です。
- ap-northeast-1a
- ap-northeast-1b
- ap-northeast-1c
- ap-northeast-1d
しかしながら、最近のAWSアカウントでは、ap-northeast-1b
は使用できなくなっています。
以下のコマンドを使用して利用可能なAZを取得し、確認するとap-northeast-1b
がリストに含まれていないことが確認できます。
$ aws ec2 describe-availability-zones --output text
AVAILABILITYZONES ap-northeast-1 ap-northeast-1 opt-in-not-required ap-northeast-1 available apne1-az4 ap-northeast-1a availability-zone
AVAILABILITYZONES ap-northeast-1 ap-northeast-1 opt-in-not-required ap-northeast-1 available apne1-az1 ap-northeast-1c availability-zone
AVAILABILITYZONES ap-northeast-1 ap-northeast-1 opt-in-not-required ap-northeast-1 available apne1-az2 ap-northeast-1d availability-zone
一方で、当社の最も古いAWSアカウント(2008-12-17作成)では、ap-northeast-1b
を含むすべてのAZが引き続き利用可能です。
$ aws ec2 describe-availability-zones --output text
AVAILABILITYZONES ap-northeast-1 ap-northeast-1 opt-in-not-required ap-northeast-1 available apne1-az3 ap-northeast-1a availability-zone
AVAILABILITYZONES ap-northeast-1 ap-northeast-1 opt-in-not-required ap-northeast-1 available apne1-az4 ap-northeast-1b availability-zone
AVAILABILITYZONES ap-northeast-1 ap-northeast-1 opt-in-not-required ap-northeast-1 available apne1-az1 ap-northeast-1c availability-zone
AVAILABILITYZONES ap-northeast-1 ap-northeast-1 opt-in-not-required ap-northeast-1 available apne1-az2 ap-northeast-1d availability-zone
最近のAWSアカウントでap-northeast-1bが使用できなくなった理由は、旧AZであるapne1-az3
が非推奨となったことに起因しています。
このAZは廃止が予定されており、新しいAWSアカウントでは初めから使用できないようになっています。
実際に、古いAWSアカウントと新しいAWSアカウントでの出力結果を比較すると、新しい方にはapne1-az3
のAZが存在せず、古い方にはapne1-az3
のAZがap-northeast-1a
として存在していることが分かります。
新旧アカウントで利用可能AZが異なるが、Terraformのコードは共通のものを使用したい
ここからが本題です。
旧アカウントでは、旧AZ(apne1-az3
)以外の3つのAZに対して、それぞれ1つずつのパブリックサブネットを作成しています。
- staging-1b-public
- staging-1c-public
- staging-1d-public
しかし、新アカウントでは a/c/d のAZしか利用できないため、同じ構成を再現する場合、以下のように変更する必要があります。
- staging-1a-public
- staging-1c-public
- staging-1d-public
さらに、Terraformの実装ではAZがハードコーディングされている状態で、このままでは共通化が難しい状態です。
# ap-northeast-1b
resource "aws_subnet" "eks-1b-public" {
cidr_block = local.cidr_blocks["eks_public_1b"]
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1b"
map_public_ip_on_launch = "1"
tags = {
"Name" = "${local.env}-1b-public"
}
}
resource "aws_route_table_association" "eks-1b-public" {
route_table_id = aws_route_table.public.id
subnet_id = aws_subnet.eks-1b-public.id
}
# ap-northeast-1c
resource "aws_subnet" "eks-1c-public" {
cidr_block = local.cidr_blocks["eks_public_1c"]
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = "1"
tags = {
"Name" = "${local.env}-1c-public"
}
}
resource "aws_route_table_association" "eks-1c-public" {
route_table_id = aws_route_table.public.id
subnet_id = aws_subnet.eks-1c-public.id
}
# ap-northeast-1d
resource "aws_subnet" "eks-1d-public" {
cidr_block = local.cidr_blocks["eks_public_1d"]
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1d"
map_public_ip_on_launch = "1"
tags = {
"Name" = "${local.env}-1d-public"
}
}
resource "aws_route_table_association" "eks-1d-public" {
route_table_id = aws_route_table.public.id
subnet_id = aws_subnet.eks-1d-public.id
}
この状態のTerraformコードを共通化するために、まずAWSアカウントごとに利用可能なAZをTerraformで判定する必要があります。
調査したところ、aws_availability_zones というData Sourceを使用すれば実現できそうでした。
実際にData Sourceを以下のように実装し、AWSアカウントごとのAZを取得します。
data "aws_availability_zones" "available" {
state = "available"
# 出力させたくないAZを指定する(旧AZを指定)
exclude_zone_ids = ["apne1-az3"]
}
terraform consoleでの動作確認では、以下のような結果が得られます。
# 旧アカウント
$ terraform console
> data.aws_availability_zones.available.names
tolist([
"ap-northeast-1b",
"ap-northeast-1c",
"ap-northeast-1d",
])
# 新アカウント
$ terraform console
> data.aws_availability_zones.available.names
tolist([
"ap-northeast-1a",
"ap-northeast-1c",
"ap-northeast-1d",
])
これにより、AZの差異を吸収し、ループ処理でコードを共通化することが可能です。
data.aws_availability_zones
を活用してパブリックサブネット用のmoduleを実装する
実際に弊社の環境ではsubnetで使用するCIDRをcidr_block = local.cidr_blocks["eks-public_1b"]
のように決定しているため、AZの末尾アルファベットのみを取得した方が都合が良いです。
以下はその実装例です。
# ap-northeast-1b
resource "aws_subnet" "eks-1b-public" {
cidr_block = local.cidr_blocks["eks-public_1b"]
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1b"
map_public_ip_on_launch = "1"
tags = {
"Name" = "${local.env}-1b-public"
}
}
AZの末尾アルファベットのみを取得するための実装は以下の通りです。
data "aws_availability_zones" "available" {
state = "available"
exclude_zone_ids = ["apne1-az3"]
}
locals {
# ap-northeast-1a などの末尾1文字を取得して格納し直す
availability_zone_identifiers = [for aws_availability_zone in toset(data.aws_availability_zones.available.names) : substr(aws_availability_zone, -1, 1)]
}
そして、パブリックサブネットmoduleの実装は以下の通りです。
resource "aws_subnet" "public" {
cidr_block = var.cidr_blocks["public_1${var.availability_zone_identifier}"]
vpc_id = var.vpc_id
availability_zone = "${var.aws_availability_zone_id}${var.availability_zone_identifier}"
map_public_ip_on_launch = "1"
tags = {
Name = var.tag
}
}
resource "aws_route_table_association" "public" {
route_table_id = var.aws_route_table_public_id
subnet_id = aws_subnet.public.id
}
variable "aws_availability_zone_id" {
type = string
description = "AZ(末尾アルファベット含まず)を格納する(例:ap-northeast-1)"
}
variable "availability_zone_identifier" {
type = string
description = "AZの末尾1文字(a|b|c|d)を格納する"
}
variable "vpc_id" {
type = string
description = "vpcのIDを格納する(例:aws_vpc.main.id)"
}
variable "cidr_blocks" {
type = map(string)
description = "locals.tfに定義しているCIDRマップを格納する"
}
variable "aws_route_table_public_id" {
type = string
description = "ルートテーブルのIDを格納する(例:aws_route_table.public.id)"
}
variable "service_name" {
type = string
description = "eks、rds、サービス名(socialplusなど)を格納する"
}
variable "tag" {
type = string
}
そして、module呼び出し側の実装は以下の通りです。
# 利用可能なAZ(旧AZ除く)を取得する
data "aws_availability_zones" "available" {
state = "available"
exclude_zone_ids = ["apne1-az3"]
}
locals {
# ap-northeast-1a などの末尾1文字を取得して格納し直す
availability_zone_identifiers = [for aws_availability_zone in toset(data.aws_availability_zones.available.names) : substr(aws_availability_zone, -1, 1)]
}
module "eks_public_subnet" {
source = "../../modules/aws_public_subnet"
# 新アカウントなら["a","c","d"]、旧アカウントなら["b","c","d"]が格納される
for_each = toset(local.availability_zone_identifiers)
# ap-northeast-1 を格納
aws_availability_zone_id = data.aws_availability_zones.available.id
# a,b,c,d のいずれかを格納
availability_zone_identifier = each.value
vpc_id = aws_vpc.main.id
cidr_blocks = local.cidr_blocks
aws_route_table_public_id = aws_route_table.public.id
service_name = "eks"
tag = "eks-${local.env}-1${each.value}-public"
}
これにより、AWSアカウントごとのAZの違いを気にせずにコードの共通化が行えるようになりました。
まとめ
- 古いAWSアカウントと新しいAWSアカウントで利用可能なAZが異なる
- TerraformのコードはStagingとProductionで共通のものを使用したかった
- 利用可能なAZをTerraformで動的に取得するために、aws_availability_zones を利用した
後からマルチアカウント化を目指した結果、新旧のAWSアカウントで利用可能なAZに差異が生まれる可能性は低いかもしれませんが、同じような境遇にある方のヒントになれば幸いです。
Discussion