ElasticBeanstalk環境下のALBで301リダイレクトさせる

本エントリは下書きにしたまま放置してあったんですが、ElasticBeanstalkとTerraformの記事って、ネットであまり見かけないしバッドノウハウは需要あると思い、掘り起こしてブラッシュアップしました。
https://youtu.be/e2W4BTR_9eA:embed:cite

ApplicationLoadBalancerのリスナーは下記をサポートしており、そのリスナーにはデフォルトのルールとオプションルールを定義することが可能です。
各ルールは優先度、1 つ以上のアクション、1 つ以上の条件で構成されます。

* プロトコル: HTTP,HTTPS
* ポート: 1 ～ 65535


ルールアクションの中に`redirect`というタイプがあるので、これを使用することでHTTP=>HTTPSへリダイレクトさせることが可能になります。
（今から1年前くらいに実装された機能です）
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#redirect-actions:embed:cite

そもそもElasticBeanstalkの標準の[名前空間](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/command-options-general.html#command-options-general-elbv2-listener-default)においてALBのリスナーのルールでリダイレクトを設定する機能が存在しません。そのため、.ebextensionsでALBにリダイレクトルールを入れようとしました。下記がその時の設定です。
<details><summary>`elastic_beanstalk_environment.tf`</summary><div>

```tf
(snip)
/* Network Tier */
#
# Load Balancing
#
// EnvironmentType
setting {
  namespace = "aws:elasticbeanstalk:environment"
  name      = "EnvironmentType"
  value     = "LoadBalanced"
}

// 環境のロードバランサーのタイプ
setting {
  namespace = "aws:elasticbeanstalk:environment"
  name      = "LoadBalancerType"
  value     = "application"
}

#
# process:default
#
// Elastic Load Balancing がアプリケーションの Amazon EC2 インスタンスの状態をチェックする間隔 (秒)
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "HealthCheckInterval"
  value     = "15"
}

// ヘルスチェックの HTTP リクエストを送信するパス
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "HealthCheckPath"
  value     = "/healthcheck"
}

// ヘルスチェック中のレスポンスの待機時間 (秒)
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "HealthCheckTimeout"
  value     = "5"
}

// インスタンスのヘルスステータスを変更するために必要な、連続して成功したリクエストの数
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "HealthyThresholdCount"
  value     = "3"
}

// インスタンスが正常であることを示す HTTP コードのカンマ区切りのリスト
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "MatcherHTTPCode"
  value     = "200"
}

// プロセスがリッスンしているポート
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "Port"
  value     = "80"
}

// プロセスで使用するプロトコル
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "Protocol"
  value     = "HTTP"
}

// スティッキーセッション
setting {
  namespace = "aws:elasticbeanstalk:environment:process:default"
  name      = "StickinessEnabled"
  value     = "false"
}

// 内部ロードバランサーを作成する場合は internal を指定
setting {
  namespace = "aws:ec2:vpc"
  name      = "ELBScheme"
  value     = "external"
}

setting {
  namespace = "aws:elbv2:listener:80"
  name      = "ListenerEnabled"
  value     = "true"
}

// HTTP リスナーによって使用されるプロトコル
setting {
  namespace = "aws:elbv2:listener:80"
  name      = "Protocol"
  value     = "HTTP"
}

// HTTPS リスナーによって使用されるプロトコル
setting {
  namespace = "aws:elbv2:listener:443"
  name      = "Protocol"
  value     = "HTTPS"
}

// サーバ証明書
setting {
  namespace = "aws:elbv2:listener:443"
  name      = "SSLCertificateArns"
  value     = "arn:aws:acm:ap-northeast-1:xxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

// セキュリティポリシー
setting {
  namespace = "aws:elbv2:listener:443"
  name      = "SSLPolicy"
  value     = "ELBSecurityPolicy-2016-08"
}

// HTTP リスナーに適用するルールのリスト
setting {
  namespace = "aws:elbv2:listener:80"
  name      = "DefaultProcess"
  value     = "default"
}

// HTTPS リスナーに適用するルールのリスト
setting {
  namespace = "aws:elbv2:listener:443"
  name      = "DefaultProcess"
  value     = "default"
}

#
# default
#
// HTTP リスナーに適用するルールのリスト
setting {
  namespace = "aws:elbv2:listener:80"
  name      = "Rules"
  value     = "default"
}

// HTTPS リスナーに適用するルールのリスト
setting {
  namespace = "aws:elbv2:listener:443"
  name      = "Rules"
  value     = "default"
}

setting {
  namespace = "aws:elbv2:listenerrule:default"
  name      = "Process"
  value     = "default"
}

// 一致するホスト名のリスト
setting {
  namespace = "aws:elbv2:listenerrule:default"
  name      = "HostHeaders"
  value     = "xxxxx.jp"
}

// 複数のルールが一致する場合の、このルールの優先順位。低い番号が優先される
setting {
  namespace = "aws:elbv2:listenerrule:default"
  name      = "Priority"
  value     = "1"
}

// アクセスログを保存する Amazon S3 バケット
setting {
  namespace = "aws:elbv2:loadbalancer"
  name      = "AccessLogsS3Bucket"
  value     = "xxxxx"
}

// アクセスログストレージを有効
setting {
  namespace = "aws:elbv2:loadbalancer"
  name      = "AccessLogsS3Enabled"
  value     = "true"
}

// アクセスログ名に追加するプレフィックス
setting {
  namespace = "aws:elbv2:loadbalancer"
  name      = "AccessLogsS3Prefix"
  value     = "xxxx/production"
}

// クライアントとインスタンスへの接続を閉じる前に、リクエストの完了を待機する時間
setting {
  namespace = "aws:elbv2:loadbalancer"
  name      = "IdleTimeout"
  value     = "60"
}

// EBでSGを新規作成せずに、既存SGを充てる
setting {
  namespace = "aws:elbv2:loadbalancer"
  name      = "SecurityGroups"
  value     = aws_security_group.xxxxx_elb_sg.id
}

// EBでSGを新規作成せずに、既存SGを充てる
setting {
  namespace = "aws:elbv2:loadbalancer"
  name      = "ManagedSecurityGroup"
  value     = aws_security_group.xxxxx_elb_sg.id
}
(snip)
```
</div></details>


<details><summary>`alb_redirect.config`</summary><div>

```config
Resources:
  AWSEBV2LoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn:
        Ref: AWSEBV2LoadBalancer
      DefaultActions:
        - Type: redirect
          RedirectConfig:
            Protocol: HTTPS
            Port: 443
            StatusCode: 'HTTP_301'
      Port: 80
      Protocol: HTTP
```
</div></details>

 結果、デプロイは正常に終了しましたがALBにルールが適用されませんでした...

原因は設定オプションの優先順位に起因するものでした。

ElasticBeanstalk環境では、環境作成時にCreateEnvironmentAPIのoptionSettingsパラメータにて指定した設定が最優先されます。
Terraformから実行されたCreateEnvironmentAPIのリクエストパラメータoptionSettingsにおいて、名前空間aws:elbv2:listener:80に対する設定が優先されたため、.ebextensions配下のconfigが反映されなかったのが原因でした。

https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/command-options.html#configuration-options-precedence:embed:cite

以下の名前空間を削除して置き換えました。

```tf
setting {
  namespace = "aws:elbv2:listener:80"
  name      = "ListenerEnabled"
  value     = "true"
}

setting {
  namespace = "aws:elbv2:listener:80"
  name      = "Protocol"
  value     = "HTTP"
}

setting {
  namespace = "aws:elbv2:listener:80"
  name      = "DefaultProcess"
  value     = "default"
}

setting {
  namespace = "aws:elbv2:listener:80"
  name      = "Rules"
  value     = "default"
}

setting {
  namespace = "aws:elbv2:listener:443"
  name      = "Rules"
  value     = "default"
}

setting {
  namespace = "aws:elbv2:listenerrule:default"
  name      = "Process"
  value     = "default"
}

setting {
  namespace = "aws:elbv2:listenerrule:default"
  name      = "HostHeaders"
  value     = "xxxxx.jp"
}

setting {
  namespace = "aws:elbv2:listenerrule:default"
  name      = "Priority"
  value     = "1"
}
```

* .ebextensionが優先されるように更新した設定ファイル

<details><summary>`elastic_beanstalk_environment.tf`</summary><div>

```tf
(snip)
   /* Network Tier */
  #
  # Load Balancing
  #
  // EnvironmentType
  setting {
    namespace = "aws:elasticbeanstalk:environment"
    name      = "EnvironmentType"
    value     = "LoadBalanced"
  }

  // 環境のロードバランサーのタイプ
  setting {
    namespace = "aws:elasticbeanstalk:environment"
    name      = "LoadBalancerType"
    value     = "application"
  }

  #
  # process:default
  #
  // Elastic Load Balancing がアプリケーションの Amazon EC2 インスタンスの状態をチェックする間隔 (秒)
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "HealthCheckInterval"
    value     = "15"
  }

  // ヘルスチェックの HTTP リクエストを送信するパス
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "HealthCheckPath"
    value     = "/healthcheck"
  }

  // ヘルスチェック中のレスポンスの待機時間 (秒)
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "HealthCheckTimeout"
    value     = "5"
  }

  // インスタンスのヘルスステータスを変更するために必要な、連続して成功したリクエストの数
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "HealthyThresholdCount"
    value     = "3"
  }

  // インスタンスが正常であることを示す HTTP コードのカンマ区切りのリスト
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "MatcherHTTPCode"
    value     = "200"
  }

  // プロセスがリッスンしているポート
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "Port"
    value     = "80"
  }

  // プロセスで使用するプロトコル
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "Protocol"
    value     = "HTTP"
  }

  // スティッキーセッション
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "StickinessEnabled"
    value     = "false"
  }

  // 内部ロードバランサーを作成する場合は internal を指定
  setting {
    namespace = "aws:ec2:vpc"
    name      = "ELBScheme"
    value     = "external"
  }

  // HTTPS リスナーによって使用されるプロトコル
  setting {
    namespace = "aws:elbv2:listener:443"
    name      = "Protocol"
    value     = "HTTPS"
  }

  // サーバ証明書
  setting {
    namespace = "aws:elbv2:listener:443"
    name      = "SSLCertificateArns"
    value     = "arn:aws:acm:ap-northeast-1:xxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  }

  // セキュリティポリシー
  setting {
    namespace = "aws:elbv2:listener:443"
    name      = "SSLPolicy"
    value     = "ELBSecurityPolicy-2016-08"
  }

  // HTTPS リスナーに適用するルールのリスト
  setting {
    namespace = "aws:elbv2:listener:443"
    name      = "DefaultProcess"
    value     = "default"
  }

  #
  # default
  #
  setting {
    namespace = "aws:elbv2:listener:443"
    name      = "Rules"
    value     = "tls"
  }

  setting {
    namespace = "aws:elbv2:listenerrule:tls"
    name      = "HostHeaders"
    value     = "xxxxx.jp"
  }

  // アクセスログを保存する Amazon S3 バケット
  setting {
    namespace = "aws:elbv2:loadbalancer"
    name      = "AccessLogsS3Bucket"
    value     = "xxxxxx"
  }

  // アクセスログストレージを有効
  setting {
    namespace = "aws:elbv2:loadbalancer"
    name      = "AccessLogsS3Enabled"
    value     = "true"
  }

  // アクセスログ名に追加するプレフィックス
  setting {
    namespace = "aws:elbv2:loadbalancer"
    name      = "AccessLogsS3Prefix"
    value     = "xxxx/production"
  }

  // クライアントとインスタンスへの接続を閉じる前に、リクエストの完了を待機する時間
  setting {
    namespace = "aws:elbv2:loadbalancer"
    name      = "IdleTimeout"
    value     = "60"
  }

  // EBでSGを新規作成せずに、既存SGを充てる
  setting {
    namespace = "aws:elbv2:loadbalancer"
    name      = "SecurityGroups"
    value     = aws_security_group.xxxxx_elb.id
  }

  // EBでSGを新規作成せずに、既存SGを充てる
  setting {
    namespace = "aws:elbv2:loadbalancer"
    name      = "ManagedSecurityGroup"
    value     = aws_security_group.xxxxx_elb.id
  }
(snip)
```
</div></details>

これでリスナールールが更新されるはず！と思ってapplyするも、正常終了しても既存のルールがそのまま残っており、優先順位が変更できない状態に陥りました。
さんざん悩んだ結果、環境を再作成することで.ebextensions配下の設定ファイルを適用することができました。このあたりはTerraformとCloudFormationの問題だと思うのですが、
今現在も解決していないため、ISSUEを出したいと思っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58788/3b5afb44-a710-690a-22a0-0834f151a0e6.png)

301でリダイレクトされています。

```bash
$ curl --head xxxxx.jp
HTTP/1.1 301 Moved Permanently
Server: awselb/2.0
Date: Tue, 23 Jul 2019 03:23:34 GMT
Content-Type: text/html
Content-Length: 150
Connection: keep-alive
Location: https://xxxxx.jp:443/
```

ALBのログには`redirect`されたことがわかるように出ています。

```bash
http 2019-07-23T04:31:30.708115Z app/awseb-AWSEB-************/*********** ***.***.***.***:49488 - -1 -1 -1 301 - 185 349 "GET http://***.***.***.***:80/ HTTP/1.1" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" - - - "Root=1-5d368da2-89854ad4ee4a72105965a0c8" "-" "-" 0 2019-07-23T04:31:30.707000Z "redirect" "https://***.***.***.***:443/" "-"
```