作りながら覚えるTerraform入門(6) - ELB編

作りながら覚えるTerraform入門シリーズの第6回です。
今回はロードバランサを作成して独自ドメインでHTTPS接続できるようにしていきます。

##### 作りながら覚えるTerraform入門シリーズ

1. [インストールと初期設定](https://qiita.com/m-oka-system/items/6103bbb9f103db1fea0e)
1. [基本編](https://qiita.com/m-oka-system/items/983653f611fe305820c8)
1. [VPC編](https://qiita.com/m-oka-system/items/bba8d32d57142a870ce5)
1. [EC2編](https://qiita.com/m-oka-system/items/8280f748f88ac2e28ad7)
1. [Route53 + ACM編](https://qiita.com/m-oka-system/items/34f3ac3d98210d06dbc8)
1. **ELB編** => 今回はコチラ
1. [RDS編](https://qiita.com/m-oka-system/items/a45867495f95461b0a2c)

今回の学習ポイントは・・・
Terraform観点だともう基本的なポイントはご紹介したので特にないのですが、強いて挙げれば

- toset関数
- for_each

でしょうか。
あと、これまでに作成してきたリソースが組み合わさって繋がるので最後の接続確認が気持ちいい。笑

# ELBの作成

まず、アプリケーションロードバランサ(ALB)を作成します。
`elb.tf`を作成して、以下のコードを貼り付けます。

```terraform:elb.tf
################################
# ELB
################################
# ELB
resource "aws_lb" "alb" {
  name                       = "${var.prefix}-alb"
  load_balancer_type         = "application"
  internal                   = false
  idle_timeout               = 60
  enable_deletion_protection = false

  subnets = [
    aws_subnet.public_subnet_1a.id,
    aws_subnet.public_subnet_1c.id
  ]

  security_groups = [
    aws_security_group.elb_sg.id
  ]
}
```

`aws_lb`でロードバランサを作成します。
`load_balancer_type`は現時点で次の3種類があります。

- application
- network
- gateway

コンソール画面で作成するときに見るアレですね。
ちなみに、`aws_elb`を使うとClassic Load Balancerを作成できます。紛らわしいので念のため。
![5A153436-5472-4C59-98A3-E4E6F58B08A0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/3ba88628-25aa-bd93-6e43-db63c03947bc.png)

インターネット向けのELBを作成する場合は`internal = false`にします。
`subnets`にはパブリックサブネットのIDを、`security_groups`には事前に作成してあるELB向けのセキュリティグループのIDを指定します。

# ターゲットグループの作成

次に、ターゲットグループを作成します。
`elb.tf`に以下のコードを追加します。

```terraform:elb.tf
# Target group
resource "aws_lb_target_group" "alb_tg" {
  name                 = "${var.prefix}-alb-tg"
  target_type          = "ip"
  vpc_id               = aws_vpc.vpc.id
  port                 = 80
  protocol             = "HTTP"
  deregistration_delay = 300

  health_check {
    protocol            = "HTTP"
    path                = "/"
    port                = "traffic-port"
    healthy_threshold   = 5
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 30
    matcher             = 200
  }
}

resource "aws_lb_target_group_attachment" "alb_tg" {
  for_each         = toset(["10.0.11.11", "10.0.12.11"])
  target_group_arn = aws_lb_target_group.alb_tg.arn
  target_id        = each.key
}
```

まず、`aws_lb_target_group`でターゲットグループを作成します。
`target_type`は`ip`にしていますが、インスタンスIDを使う場合は`instance`を指定します。（デフォルトが`instance`なので省略できます）

ターゲットグループにインスタンスを登録するには`aws_lb_target_group_attachment`で関連付けます。ここが少しハマったのですが、、

直感的には、`target_id`には配列でIPを渡したいのですが、文字列にせよと怒られます。

```bash
Error: Incorrect attribute value type
│ 
│   on elb.tf line 52, in resource "aws_lb_target_group_attachment" "alb_tg":
│   52:   target_id        = ["10.0.11.11", "10.0.12.11"]
│ 
│ Inappropriate value for attribute "target_id": string required.
```

`for_each`を使って配列を回そうとしても、mapかsetでないとダメといわれます。
`for_each`には配列を渡すことができないんです。。

```bash
 Error: Invalid for_each argument
│ 
│   on elb.tf line 50, in resource "aws_lb_target_group_attachment" "alb_tg":
│   50:   for_each         = ["10.0.11.11", "10.0.12.11"]
│ 
│ The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type tuple.
```

ということで、[toset](https://www.terraform.io/docs/language/functions/toset.html) 関数を使って、set型に変換しています。
恥ずかしながら、setという型を初めて知ったのですが、

- 重複した要素がない
- 要素に順番がない

という特徴があるみたいです。`toset`で変換すると重複が取り除かれます。

```bash
terraform console

> toset(["10.0.11.11", "10.0.12.11", "10.0.12.11"])
toset([
  "10.0.11.11",
  "10.0.12.11",
])
```

`for_each`で渡した配列もどきのリストは`each.key`で読み込むことができます。
今回は関係ありませんが、もし、mapを渡した場合は`each.key`と`each.value`でキーと値をそれぞれ読み込むことができます。

# リスナーの作成

続いて、リスナーを作成します。
`elb.tf`に以下のコードを追加します。

```terraform:elb.tf
# Listener
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect" # forwad / fixed-response / redirect

    redirect {
      protocol    = "HTTPS"
      port        = "443"
      host        = "#{host}"
      path        = "/#{path}"
      query       = "#{query}"
      status_code = "HTTP_301"
    }
  }
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate.public.arn
  ssl_policy        = "ELBSecurityPolicy-2016-08"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb_tg.arn
  }
}
```

`aws_lb_listener`でHTTPとHTTPSのリスナーを追加しています。
`port`と`protocol`の組み合わせで受付ポートとプロトコルを、`default_action`でアクションを定義します。

HTTPのリスナーでは`default_action`のタイプを`redirect`にして、HTTPSにリダイレクトするようにしています。`redirect {}`の中にはリダイレクト時に書き換える内容を指定します。

`protocol`がHTTP → HTTPSに、`port`が80 → 443に書き換えられますが、残りのホスト名、パス、クエリは変更を加えずデフォルトを利用するという意味になります。`status_code = "HTTP_301"`は「完全に移動」を表します。

コンソール画面で以下のように入力した場合と同じですね。
![963417EE-621B-4578-AFE6-7756DE1E918F.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/d689e390-fd9d-00cb-a2ae-9c46e51096b8.png)

HTTPSのリスナーでは`default_action`のタイプを`forward `にして、先に作成したターゲットグループに転送するようにしています。また、前回の[Route53 + ACM編](https://qiita.com/m-oka-system/items/34f3ac3d98210d06dbc8)で作成したパブリック証明書をここで関連付けします。

# エイリアスレコードの登録

最後に独自ドメインとELBを紐付けるエイリアスレコードを登録します。
`route53.tf`に追加したくなるかもしれませんが、ここは関連するリソースのライフサイクルに合わせて`elb.tf`に追加します。

```terraform:elb.tf
################################
# Route 53 (Alias record)
################################
resource "aws_route53_record" "alb" {
  name    = "www.${aws_route53_zone.public.name}"
  zone_id = aws_route53_zone.public.zone_id
  type    = "A"

  alias {
    name                   = aws_lb.alb.dns_name
    zone_id                = aws_lb.alb.zone_id
    evaluate_target_health = true
  }
}
```

レコードの名前は独自ドメインの頭に`www.`を付けた形にしていますが、ACMをワイルドカード証明書にしてあるので`app.`とかでも構いませんし、何も付け加えずに`aws_route53_zone.public.name`としてネイキッドドメインで接続する形にしてもOKです。ネイキッドドメインを登録できるのは、エイリアスレコードならではの特徴ですね。

ELB編のコードは以上です。
`terraform apply`を実行して、もろもろ作成されていることを確認しましょう。

問題なければ独自ドメインでアクセスしてみましょう。
鍵アイコンをクリックして証明書を確認するとAmazon発行のものであることがわかります。
また、ELBには2台のEC2がぶら下がっているので、ブラウザを更新するたびに`web-01`と`web-02`が交互に切り替わるかと思います。（ラウンドロビン）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/44788aa2-6f85-ba3d-5ac9-c0c77c0c55ce.png)

ブラウザのURLをhttpに変更してもリダイレクトされることが確認できますが、`curl`コマンドでも確認することができます。`Location`を見るとプロトコルとポートが書き換わっていることがわかります。

```bash
curl -I http://www.example.com
HTTP/1.1 301 Moved Permanently
Server: awselb/2.0
Date: Wed, 09 Jun 2021 04:59:29 GMT
Content-Type: text/html
Content-Length: 134
Connection: keep-alive
Location: https://www.example.com:443/
```

今回は以上です。次回はRDS編ということで、MySQLのRDSをマルチAZ構成で構築してみたいと思います！

# 参考リンク

- [toset Function](https://www.terraform.io/docs/language/functions/toset.html)
- [[新機能]Webサーバでの実装不要！ALBだけでリダイレクト出来るようになりました！](https://dev.classmethod.jp/articles/alb-redirects/)
- [Amazon Route 53のALIASレコード利用のススメ](https://dev.classmethod.jp/articles/amazon-route-53-alias-records/)