🧪

巷の Terraform Module に違和感を感じたので納得できるものを作ってみた【AWS VPC編】

2024/07/20に公開

今日は最近 Terraform Module に感じていた使いにくさの理由と、その克服方法について AWS VPC を構築しながら整理していきます。

世間で使われる Terraform Module に対する違和感

早速ですが、巷で使われている Terraform Module に対して感じた違和感を挙げていこうと思います。

具体例があるとよりわかりやすいかと思いますので、 terraform-aws-modules/vpc/aws を例に取りながら見ていこうと思います。

以下にサンプルを示します。

構成としては、3 箇所の各 AZ にパブリックサブネット及びプライベートサブネットを構築するものとなっています。

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  tags = {
    Terraform = "true"
    Environment = "dev"
  }
}

注目してほしい部分は private_subnetspublic_subnets です。これらのパラメータを元にサブネットを作成するのですが、その際に azs を参照しています。これにより、インデックスを気にしながら定義していく必要がある上に、 azs はパブリックサブネットとプライベートサブネット両方に配慮する必要があります。

違和感その1: 1リソースを作成するために複数のパラメータを参照する必要があること

複数のパラメータを用いることで複数のリソースが構築される多対多の関係は扱いにくいです。
今回の例でいうと、パブリックサブネットを2個作るために、 azspublic_subnets を見ていく必要があり、同様にプライベートサブネットも azsprivate_subnets をみていく必要があります。

これにより複数のリソースを複数のパラメータから作成していく、という状況に陥ることになります。また、前述したように azs はパブリックサブネットとプライベートサブネットどちらからも依存しているので、どちらにも配慮しながら書かなければいけなくなるのです。

本来、1個のリソースに対して複数個のパラメータがあるのがわかりやすいと思っていて、どんな形であれ、 「1リソースに対応するパラメータリストがある」 という状態が望ましいはずです。

これを Terraform 的に書くのであれば、

<任意のリソース名> = {
  <パラメータ1> = ...
  <パラメータ2> = ...
  ...
}

きっとこういう形になるでしょう。

違和感その2: インデックス管理されているので変更(特に削除)に非常に弱い

先ほどのサンプルに以下のような変更を行います。

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1d"]
  private_subnets = ["10.0.1.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.103.0/24"]
  # 変更前はこうだった
  # azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  # private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  # public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]


  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

違いとしては、 ap-northeast-1c にあったパブリックサブネットととプライベートサブネットを1個ずつ削除しました。
理想としては、削除した分のリソース(パブリックサブネット、プライベートサブネット、ルートの関連付けなど)が単純に削除されれば良いのですが、実際には

Plan: 4 to add, 1 to change, 9 to destroy.

となってしまいます。

これは、 Terraform 内部でのリソースの扱いの問題で、 Terraform ではインデックスを含めた同じリソース名の差分を評価するので、変更前の aws_subnet.public[1]と 変更後の aws_subnet.public[1] の差分を評価します。そのために、 「aws_subnet.public[2] aws_subnet.private[2] が削除されて aws_subnet.public[1] aws_subnet.private[1] の AZ を変更する」という扱いになってしまうためです。

このように、 Terraform でインデックスでナンバリングをベースにリソースを定義すると、厳密にその順序と番号を意識しなければいけなくなるのです。

とりあえず作ってみた

ここまで、一通りの違和感を話したので、とりあえず kasaikou/vpc/aws でここまでの違和感を払拭するモジュールを作ってみました。

コンセプトとしては以下の通りです。

  • 「リソースに対応するパラメータリストがある」という関係にあること
  • インデックスにナンバリングを使わないこと

一旦上と同様のコードを書いてみます。

module "vpc" {
  source  = "kasaikou/vpc/aws"
  version = "v0.1.1"

  name       = "my-vpc"
  cidr_block = "10.0.0.0/16"

  subnets = {
    "public-primary" = {
      availability_zone = "ap-northeast-1a"
      cidr_block        = "10.0.1.0/24"
      route_tables      = ["igw"]
    }
    "public-secondary" = {
      availability_zone = "ap-northeast-1c"
      cidr_block        = "10.0.3.0/24"
      route_tables      = ["igw"]
    }
    "private-primary" = {
      availability_zone = "ap-northeast-1a"
      cidr_block        = "10.0.101.0/24"
    }
    "private-secondary" = {
      availability_zone = "ap-northeast-1c"
      cidr_block        = "10.0.103.0/24"
    }
  }
}

ミソなのはサブネットの定義の仕方です。型的に map(object()) で定義していて、「リソース名とそのパラメータ」という関係をここで定義しています。これによって違和感その1で取り上げた 「リソースに対応するパラメータリストがある」 を達成することができます。

現時点では aws_vpc 1個に対して複数の aws_subnet, aws_security_group, aws_vpc_endpoint (Interface 型, Gateway 型), aws_route_table を作成することができます。

また、インデックスにナンバリングを使わないことに関しては、ナンバリングでなければいいので文字列にすることで解決しました。これにより Terraform 上のリソース名も module.vpc.aws_subnet.subnets["public-secondary"] となるため、削除する際にはリソースの前後関係を意識せずに削除することができます。

おわりに

こんな感じで、最近感じた違和感といい感じの解決策を書いてみました。

あまり Terraform Module を使わずに直接リソース書く方が楽なんですが(諸説あり)、どうせ書くなら変更しやすくて融通の利く Terraform Module を作りたいものです。

Discussion