TerraformでAWSの「マネージドプレフィックスリスト」を作成する
みなさん、こんにちは!
福岡オフィスの青柳です。
今回は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」で固定とします)
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)
続いて、マネージドプレフィックスリストのリソース作成を記述します。
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" } }
name
、address_family
、max_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を出力しておきます。
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 { 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)
いよいよ最後に、モジュールを呼び出す部分の記述です。
と言っても、モジュール側の定義に従って、各引数に値をセットしつつモジュールを呼び出しているだけです。
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.max
、local.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モジュール」に置き換えてみませんか?