TerraformでAWSの「マネージドプレフィックスリスト」を作成する

マネージドプレフィックスリストを使うことでIPアドレス/CIDRの定義を一箇所に集約し、更にTerraformモジュールを使うことでコード記述を省力化しよう。
2022.05.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

みなさん、こんにちは!
福岡オフィスの青柳です。

今回はTerraformでAWSの「マネージドプレフィックスリスト」を作成してみたいと思います。

基本的な書き方

Terraformでマネージドプレフィックスリストを記述するには、2通りの方法があります。

  • (1) エントリをインラインで記述する方法
  • (2) エントリを個別のリソースとして記述する方法

それぞれの方法を見て行きましょう。

(1) エントリをインラインで記述する方法

マネージドプレフィックスのリソースaws_ec2_managed_prefix_listの定義の中で、各エントリの内容をentry {}ブロックに記述する方法です。

Terraform公式ドキュメントに書き方のサンプルがありますので、そちらを参考にすれば難しくないでしょう。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_managed_prefix_list

resource "aws_ec2_managed_prefix_list" "onpremises" {
  name = "onpremises-prefix-list"

  address_family = "IPv4"
  max_entries    = 3

  entry {
    cidr        = "172.16.0.0/16"
    description = "Tokyo Office"
  }

  entry {
    cidr        = "172.18.0.0/16"
    description = "Osaka Office"
  }

  entry {
    cidr        = "10.128.0.0/16"
    description = "Yokohama DC"
  }

  tags = {
    Name = "onpremises-prefix-list"
  }
}

(2) エントリを個別のリソースとして記述する方法

各エントリの定義を、独立したリソースec2_managed_prefix_list_entryとして記述する方法です。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_managed_prefix_list_entry

VPCの「ルートテーブル」や「セキュリティグループ」にも似た書き方がありますので、そちらをイメージしてもらうと分かり易いかと思います。

resource "aws_ec2_managed_prefix_list" "onpremises" {
  name = "onpremises-prefix-list"

  address_family = "IPv4"
  max_entries    = 3

  tags = {
    Name = "onpremises-prefix-list"
  }
}

resource "aws_ec2_managed_prefix_list_entry" "onpremises_1" {
  prefix_list_id = aws_ec2_managed_prefix_list.onpremises.id

  cidr        = "172.16.0.0/16"
  description = "Tokyo Office"
}

resource "aws_ec2_managed_prefix_list_entry" "onpremises_2" {
  prefix_list_id = aws_ec2_managed_prefix_list.onpremises.id

  cidr        = "172.18.0.0/16"
  description = "Osaka Office"
}

resource "aws_ec2_managed_prefix_list_entry" "onpremises_3" {
  prefix_list_id = aws_ec2_managed_prefix_list.onpremises.id

  cidr        = "10.128.0.0/16"
  description = "Yokohama DC"
}

注意点: 2つの方法を混在して記述してはいけません

これらの2つの方法は混在して記述してはならないことに注意してください。

例えば、次のような書き方はNGということです。

resource "aws_ec2_managed_prefix_list" "onpremises" {
  name = "onpremises-prefix-list"

  address_family = "IPv4"
  max_entries    = 3

  entry {
    cidr        = "172.16.0.0/16"
    description = "Tokyo Office"
  }

  entry {
    cidr        = "172.18.0.0/16"
    description = "Osaka Office"
  }

  tags = {
    Name = "onpremises-prefix-list"
  }
}

resource "aws_ec2_managed_prefix_list_entry" "onpremises_3" {
  prefix_list_id = aws_ec2_managed_prefix_list.onpremises.id

  cidr        = "10.128.0.0/16"
  description = "Yokohama DC"
}

試しに、上記のコードを書いてterraform applyを実行したところ、正常に実行されて、作成されたマネージドプレフィックスリストにも問題が無いように見受けられました。

しかし、Terraform公式ドキュメントには「混在した書き方をすると、エントリの競合が発生してエントリが上書きされる」とありますので、混在して書くのは止めておいた方が良いでしょう。

注意点: terraform applyの並列度が「2」以上の場合、方式(2)でエラーが発生する

terraform applyを実行する際、並列度のオプション--parallelismを指定することで効率良くリソース作成を行うテクニックは良く知られています。

しかし、方式(2)の記述方法をapplyする時に--parallelismで「2」以上の値を指定すると、以下のようなエラーが発生してapplyが失敗します。

Error: error creating EC2 Managed Prefix List Entry (pl-XXXXXXXXXXXXXXXXX,172.18.0.0/16): IncorrectState: The request cannot be completed while the prefix list (pl-XXXXXXXXXXXXXXXXX) is in the current state (modify-in-progress). Target state is: (modify-in-progress)

「マネージドプレフィックスリストの変更を行おうとしたが、別の変更がまだ行われている最中である」というエラーのようです。

このことはIssueとして報告されています。
Multiple aws_ec2_managed_prefix_list_entry resources fail to create · Issue #21835 · hashicorp/terraform-provider-aws

修正が行われる予定ではあるようですが、解消されるまでは以下の回避策を取るしかないようです。

  • 回避策 1: apply時に--parallelism=1を指定することで、エントリ作成が並列で実行されないようにする。
  • 回避策 2: 2つめ以降のエントリにdepends_onを記述して、前のエントリが作成されてから次のエントリの作成が行われるようにする。

どちらの回避先もあまりイケている方法ではないですね・・・(苦笑)

方式(2)の利点は「一旦マネージドプレフィックスリストを作成した後、別のモジュール等からエントリを追加する」といったことが可能である点です。

ただ、ルートテーブルやセキュリティグループとは違い、マネージドプレフィックスリストの場合はそのような場面は少ないのではないかと思います。

ですので、方式(1)を採用するというのが無難ではないかと思います。

モジュール化してみる

さて、これで基本的な書き方は理解して頂けたかと思いますが、実際にAWS環境を構築する際は

  • 複数のマネージドプレフィックスリストを作成したい
  • エントリの内容をlocalsやvariablesで与えて、環境によって異なる内容で作成したい

といった要件・要望がある場合が多いのではないかと思います。

これらを実現しようとすると・・・そうです、Terraformの「モジュール」を使うのが常套手段ですね。

それでは早速、マネージドプレフィックスリストの作成をモジュール化したいと思います。

なお、今回は前節で説明した2つの方法のうち「(1) エントリをインラインで記述する方法」で実装することにします。

ディレクトリ構成

全体のディレクトリ構成は以下のようになります。(標準的な構成ですね)

├── environments
│   └── test
│       ├── locals.tf
│       ├── main.tf
│       └── versions.tf
└── modules
    └── prefix_list
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

構成ファイルの内容は、以下のリポジトリを参照してください。
https://github.com/hideakiaoyagi/developers-io/tree/main/terraform-aws-managed-prefix-list

モジュールの記述内容

モジュール引数の定義 (variables.tf)

まず、モジュールに対して外部から与える「引数」を定義します。

マネージドプレフィックスリストの作成時に指定が必要なパラメーターは以下の通りです。(タグは除く)

  • プレフィックスリスト名
  • アドレスファミリー (IPv4/IPv6)
  • 最大エントリ数
  • エントリのリスト
    • CIDRブロック
    • 説明

これらのうち「アドレスファミリー」以外を外部から与えることにします。
(今回、アドレスファミリーは「IPv4」で固定とします)

variables.tf

variable "name" {
  type        = string
  description = "Resource name"
}

variable "max" {
  type        = number
  default     = 1
  description = "Maximum entries of Managed Prefix List"
}

variable "entries" {
  type        = list(any)
  default     = []
  description = "List of Managed Prefix List entries"
}

最後の「entries」は、「マップ型のリスト」として値の受け渡しを行います。
(list(map)という定義はできないので、代わりにlist(any)を指定しています)

「マップ型のリスト」とは以下のようなイメージです。

[
  {
    cidr        = "172.16.0.0/16",
    description = "Tokyo Office",
  },
  {
    cidr        = "172.18.0.0/16",
    description = "Osaka Office",
  },
  {
    cidr        = "10.128.0.0/16",
    description = "Yokohama DC",
  },
]

リソースの作成 (main.tf)

続いて、マネージドプレフィックスリストのリソース作成を記述します。

main.tf

resource "aws_ec2_managed_prefix_list" "main" {
  name = "${var.name}-prefix-list"

  address_family = "IPv4"
  max_entries    = var.max

  dynamic "entry" {
    for_each = var.entries
    content {
      cidr        = entry.value["cidr"]
      description = entry.value["description"]
    }
  }

  tags = {
    Name = "${var.name}-prefix-list"
  }
}

nameaddress_familymax_entriesについては特に説明は不要かと思います。

ここでのポイントは、エントリを定義するentryブロックをdynamicで動的に生成しているところです。

dynamicの内容を順番に解説して行きましょう。

    for_each = var.entries

for_eachで、引数として与えられたvar.entriesのリストから1つずつ中身 (=エントリを定義したマップ) を取り出しながら、entryブロックの生成を繰り返し行います。

    content {
      cidr        = entry.value["cidr"]
      description = entry.value["description"]
    }

content {...}内には、entryブロックの内容を記述します。
cidrおよびdescriptionの代入式の右辺に登場するentryは、いわゆる「イテレーター」であり、for_eachでリストvar.entriesから取り出された中身が入っています。
(なお、イテレーターの名前はdynamicで指定した名前がそのまま使われる仕様です)

イテレーターentryはマップ型ですが、マップ型の属性 (attribute) を参照する時にはvalue["属性名"]と記述します。
(entry.cidrのように記述できそうですが、この書き方はエラーとなりますので上記の書き方をしてください)

このようにdynamicを記述することで、terraform apply時にdynamic "entry" {...}の内容が動的に展開され、以下のようなentryブロックの列挙が生成されるという訳です。

  entry {
    cidr        = "172.16.0.0/16"
    description = "Tokyo Office"
  }

  entry {
    cidr        = "172.18.0.0/16"
    description = "Osaka Office"
  }

  entry {
    cidr        = "10.128.0.0/16"
    description = "Yokohama DC"
  }

モジュール出力の定義 (outputs.tf)

最後に、モジュール内で作成したマネージドプレフィックスリストを、他のモジュール等から利用できるように、リソースのIDを出力しておきます。

outputs.tf

output "prefix_list_id" {
  value       = aws_ec2_managed_prefix_list.main.id
  description = "Resource ID of Managed Prefix List"
}

シンプルにリソースのidを出力しているだけです。

モジュール呼び出し側の記述内容

ローカル変数の定義 (locals.tf)

マネージドプレフィックスリストを作成するために「prefix_list」モジュールに与える諸元を定義します。

以下の例では「onpremises (オンプレミス拠点のネットワークアドレス)」「administrators (管理者PCのIPアドレス)」という2つのリストの諸元を定義しています。

locals.tf

locals {
  prefix_lists = {
    onpremises = {
      name = "onpremises"
      max = 3
      entries = [
        {
          cidr        = "172.16.0.0/16",
          description = "Tokyo Office",
        },
        {
          cidr        = "172.18.0.0/16",
          description = "Osaka Office",
        },
        {
          cidr        = "10.128.0.0/16",
          description = "Yokohama DC",
        },
      ]
    }
    administrators = {
      name = "administrators"
      max = 3
      entries = [
        {
          cidr        = "172.16.1.11/32",
          description = "Nakamoto PC",
        },
        {
          cidr        = "172.16.2.22/32",
          description = "Mizuno PC",
        },
        {
          cidr        = "172.18.1.33/32",
          description = "Kikuchi PC",
        },
      ]
    }
  }
}

ローカル変数の構造について解説します。

最上位のprefix_listsは、複数のリスト諸元を格納する「マップ型」となっています。

onpremisesおよびadministratorsも「マップ型」となっていて、1つのマネージドプレフィックスリストを作成するために指定する以下のパラメーター群を格納しています。

  • プレフィックスリスト名 (文字列型)
  • 最大エントリ数 (数値型)
  • エントリのリスト (マップ型のリスト)

エントリのリストentryについては、モジュールの「variables.tf」の解説で挙げた「『マップ型のリスト』のイメージ」が参考になるかと思います。

モジュールの呼び出し (main.tf)

いよいよ最後に、モジュールを呼び出す部分の記述です。

と言っても、モジュール側の定義に従って、各引数に値をセットしつつモジュールを呼び出しているだけです。

main.tf

module "prefix_list_onpremises" {
  source = "../../modules/prefix_list"

  name    = local.prefix_lists.onpremises.name
  max     = local.prefix_lists.onpremises.max
  entries = local.prefix_lists.onpremises.entries
}

module "prefix_list_administrators" {
  source = "../../modules/prefix_list"

  name    = local.prefix_lists.administrators.name
  max     = local.prefix_lists.administrators.max
  entries = local.prefix_lists.administrators.entries
}

local.prefix_lists.onpremises.nameという記述は、「locals.tf」で定義したローカル変数から以下の部分の値を参照するという意味になります。

locals {
  prefix_lists = {
    onpremises = {
      name = "onpremises"
      max = 3
      entries = [
        {
          cidr        = "172.16.0.0/16",
          description = "Tokyo Office",
        },
        {
          cidr        = "172.18.0.0/16",
          description = "Osaka Office",
        },
        {
          cidr        = "10.128.0.0/16",
          description = "Yokohama DC",
        },
      ]
    }
(以下略)

local.prefix_lists.onpremises.maxlocal.prefix_lists.onpremises.entriesについても同様に「locals.tf」の定義を参照しています。

これで、全てのコードの記述が終わりました。
(なお、versions.tfの記述内容についての解説は割愛します。みなさんの環境に合わせて適宜記述してください)

あとは、environments/testディレクトリへ移動してterraform applyを実行すれば、「onpremises」「administrators」という2つのマネージドプレフィックスリストが作成されると思います。

エントリの変更・追加・削除

マネージドプレフィックスリストを作成した後に「エントリ」の変更・追加・削除を行う場合は、「locals.tf」の内容を変更してterraform applyを実行するだけです。

ただし、追加と削除についてはマネージドプレフィックスリストの仕様に起因する注意点があります。

エントリの「追加」時の注意点

エントリを追加する場合、現在の最大エントリ数を実際のエントリ数が超えてしまう場合は、最大エントリ数の値も増やす必要があります。

最大エントリ数を超えてエントリを追加しようとすると、当然ながらエラーとなります。

Error: error updating EC2 Managed Prefix List (pl-XXXXXXXXXXXXXXXXX): PrefixListMaxEntriesExceeded: You've reached the maximum number of entries for the prefix list. This modification saves (4) entries and the maximum number of entries for this prefix list is (3).

エントリの「削除」時の注意点

エントリを削除する場合、最大エントリ数の指定を実際のエントリ数に合わせようとして「最大エントリ数」の値も変更しようと考えるかもしれません。

しかし、最大エントリ数の値を現在よりも少ない値に変更しようとすると、エラーが発生します。

Error: error updating EC2 Managed Prefix List (pl-XXXXXXXXXXXXXXXXX): InvalidParameterCombination: You cannot modify the entries and the maximum entries for the prefix list in the same request.

マネージドプレフィックスリストの「最大エントリ数」は、増加させることは可能ですが、減少させることは不可能であるからです。
(なお、以前は増加させることも含めて変更は一切不可能だったのですが、その後のアップデートで増加させるのはOKになりました)

ということで、「最大エントリ数」を変更する際は、現在の設定値から減少させるような変更は行わないように、気を付けましょう。

おわりに

マネージドプレフィックスリストを使うことで、セキュリティグループやTransit Gatewayルートテーブルなどに記述するIPアドレス/CIDRの定義を一箇所に集約することができます。

更に、Terraformモジュールを使うことによって、コード記述の省力化が行え、運用・保守など管理面でのメリットも生まれます。

IPアドレスの直書きをやめて「マネージドプレフィックスリスト」&「Terraformモジュール」に置き換えてみませんか?