⚒️

AWSマルチアカウント環境下のTerraformにおける、ダイナミックに切り替え可能なAZの実装方法

2024/01/30に公開

こんにちは。
株式会社ソーシャル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がハードコーディングされている状態で、このままでは共通化が難しい状態です。

subnet-eks.tf
# 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の末尾アルファベットのみを取得した方が都合が良いです。
以下はその実装例です。

subnet-eks.tf
# 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の実装は以下の通りです。

terraform/modules/aws_public_subnet/main.tf
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
}
terraform/modules/aws_public_subnet/variables.tf
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呼び出し側の実装は以下の通りです。

subnet-eks.tf
# 利用可能な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に差異が生まれる可能性は低いかもしれませんが、同じような境遇にある方のヒントになれば幸いです。

SocialPLUS Tech Blog

Discussion