Terraform小ネタ ALBのリスナールール条件値制限（5/ルール）に動的に対応してみた

ALBのリスナールールって、[条件の数に上限（5つまで）がある](https://docs.aws.amazon.com/ja_jp//elasticloadbalancing/latest/application/load-balancer-limits.html)んですね。これを知らずにTerraformで6つ以上の条件を配列を基に入れようとしてエラーとなってしまいました。

その際に対応した方法をご紹介します。

# 結論
全くもって大した話ではありません。
要は、条件となる情報を保持してある配列を、[for 文のグループ化機能](https://www.terraform.io/language/expressions/for#grouping-results) で分割した、というだけになります。

```terraform
locals {
  # 要素数5以下ずつの配列に分割
  admin_allow_ip_packs_per5 = {
    for i, v in var.admin_allow_ip : floor(i / 5) => v...
  }
}
```

* (1) `var.admin_allow_ip` ... 条件で使用する値が配列で保持してある
* (2) `admin_allow_ip_packs_per5` ... (1) を要素数5個ずつに分割したもの


## (1) インプット `var.admin_allow_ip`
`var.admin_allow_ip` という変数に、リスナールールの条件に指定する値（今回の例だと「送信元 IP」に指定したいIPアドレス）が複数、配列で保持してある、という前提です。

```terraform
# 管理者権限でのアクセスを許可するIPアドレス
variable "admin_allow_ip" {
  default = [
    "122.220.1.1/32",
    "113.33.21.2/32",
    "126.243.7.3/32",
    "124.36.29.4/32",
    "3.113.169.5/32",
    "182.198.1.6/32",
    "54.234.39.7/32",
  ]
}
```

## (2) アウトプット `admin_allow_ip_packs_per5`
`for i, v in var.admin_allow_ip : floor(i / 5) => v...` の結果はこうなります。

```terraform
admin_allow_ip_packs_per5 = {
  "0" = [
    "122.220.1.1/32",
    "113.33.21.2/32",
    "126.243.7.3/32",
    "124.36.29.4/32",
    "3.113.169.5/32",
  ]
  "1" = [
    "182.198.1.6/32",
    "54.234.39.7/32",
  ]
}
```

`admin_allow_ip_packs_per5` は数字のkeyを持っていて、各keyに分割後の配列が入っています。
これらのkeyの数だけリスナールールを作成すればOKです。

key は `floor(i / 5)` の結果です。
すなわち、keyは、分割前の配列の各要素のインデックスを 5 で割った際の商になるので、商が等しくなる要素が同じグループにまとめられる、ということになります。

# 前提条件
* Terraform v1.2.9

# 詳細
上記の結論だけで十分かもしれませんが、念のため、AWS ALBリスナーの設定を行うところまで、順を追って説明します。

## 背景：ALBのリスナールールの条件
AWSコンソールで見た時のALBリスナールール設定はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588493/86de1297-f44f-1bd8-029e-58e9055e025a.png)

この例では。。。

* (条件A) パスが `/admin/*`
* (条件B) 送信元IPアドレスが ... (いろいろ)

という条件に合致したら `admin-target-group` に転送しようとしています。
[全ての条件の数の上限が5つ](https://docs.aws.amazon.com/ja_jp//elasticloadbalancing/latest/application/load-balancer-limits.html) のため、(条件B) に設定できるIPアドレスは4つまで（※）、となります（(条件A) で条件を1つ消費しているため）。

※「結論」に書いた例は説明を簡単にするために単純にIPアドレスの配列を5つずつに分割しましたが、実際はこんな感じで4つずつに分割する必要がありました。

## 要件：「送信元IP」は単純に配列で保持したい
 　『 最初から配列を分割して複数保持しておく』というやり方もあるかと思いますが、このIPアドレスの配列は **リスナールール以外の他のリソースの設定にも使用** していて、そちらでは分割の必要が無い（っていうか、もし分割してあったら結合しなければならない）ため、単純な1次元配列で保持する必要がありました。
　また、数が5以上となることは避けられない状況でしたので、IPアドレスの数がいくつであっても問題無い、という形にする必要がありました。

以上より、今回は『1次元配列を後で分割する』という方式をとることにしました。

今回の例ではパス `/admin/*` をルールの条件に必ず加える必要があるので、IPアドレスの配列は『4つずつ』に分割することになります。

## 実際のソースコードの例（部分的に抜粋）

（今回の記事に直接関係のない設定は極力省いています）
```terraform
locals {
  /*
   * ALBのリスナールールに設定するためのIPアドレス
   *  - 1つのルールにつき、最大5つの条件までしか設定できない。
   *    条件の1つに「パスが "/admin/*" である」というものを指定するため、
   *    残り4つのみIPアドレスを条件に加えられる。
   *    このため、ここでIPアドレスの配列を4つずつの小分けにする。
   */
  admin_allow_ip_packs_per5 = {
    for i, v in var.admin_allow_ip : floor(i / 4) => v...    #------(1)
  }
}

# ALB
resource "aws_lb" "app_alb" {
  name                       = "app-alb"
  load_balancer_type         = "application"
  internal                   = false
  enable_deletion_protection = false
}

# listner
resource "aws_lb_listener" "https" {
  load_balancer_arn  = aws_lb.app_alb.arn
  port               = 443
  protocol           = "HTTPS"
  ssl_policy         = "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"
  default_action {
    type             = "forward"
    target_group_arn = default_target_group.app.arn
  }
  lifecycle {
    create_before_destroy = true
  }
}

# listner rule for ADMINISTRATORS app
resource "aws_lb_listener_rule" "admin" {
  count = length(local.admin_allow_ip_packs_per5)  #----------------(2)
  priority = count.index + 1  #-------------------------------------(3)

  action {
    type             = "forward"
    target_group_arn = admin_target_group.app.arn
  }

  condition {
    path_pattern {
      values = ["/admin/*"]
    }
  }

  condition {
    source_ip {
      values = local.admin_allow_ip_packs_per5[count.index]  #------(4)
    }
  }
}
```

### ポイント

#### (1) IPアドレスの配列を最大4つの要素を持つ配列に分割
最初の「結論」にも記載しましたので、詳細はそちらをご覧ください。
`var.admin_allow_ip` の配列が要素数4つまでの配列に分割されます。
```terraform
  admin_allow_ip_packs_per5 = {
    for i, v in var.admin_allow_ip : floor(i / 4) => v...
  }
```

#### (2) ALBリスナールールを分割した配列の数分、生成
生成するリスナールールの数に、上記(1)で分割した『分割後の配列の数』を指定します。
```terraform
  count = length(local.admin_allow_ip_packs_per5)
```

#### (3) ルールの優先順位を設定
優先順位を指定します（[count.indexでいわゆるループ内のインデックス値を参照できる](https://www.terraform.io/language/meta-arguments/count#the-count-object)）。

:::note info
この記事で取り上げている話のみにフォーカスするとIPアドレスに優先順位は無いため `priority` の設定は不要なのですが、実は、リスナールールはここに記載している以外にも設定する必要があり、それらとの優先順位付けが必要だったのでここで設定しています（ちなみに`priority`の値は重複不可）。
:::

```terraform
  priority = count.index + 1
```



#### (4) 分割した配列を「送信元IP」に設定
`local.admin_allow_ip_packs_per5` の key は数字（"0", "1", ...）のため、`count.index`（ループ内のインデックス）を key に指定することで、該当の配列を取り出して設定します。

```terraform
  condition {
    source_ip {
      values = local.admin_allow_ip_packs_per5[count.index]  #------(4)
    }
  }
```

# おわりに
　このような配列の分割をやっている記事が見当たらなかったので書いてみました。まあ、そうそう必要になるケースは無さそうですが。。
　Terraform の [for 文のグループ化機能](https://www.terraform.io/language/expressions/for#grouping-results) は、ぱっと見『いったいいつ使うの？』という疑問が湧くものでしたが、『こんなケースで使いました！』というのが今回の内容になります。

以上、かなりピンポイントな小ネタでした。