こんにちは。レイブパーティーで一晩中踊り明かしたい衝動に駆られてミニマル・テクノ界隈を徘徊しているCTO室のkazです。 本エントリは下書きにしたまま放置してあったんですが、ElasticBeanstalkとTerraformの記事って、ネットであまり見かけないしバッドノウハウは需要あると思い、掘り起こしてブラッシュアップしました。
ENECHANGEでは一部を除くプロダクト以外ではElasticBeanstalkを使用して運用しています。
ApplicationLoadBalancerのリスナーは下記をサポートしており、そのリスナーにはデフォルトのルールとオプションルールを定義することが可能です。 各ルールは優先度、1 つ以上のアクション、1 つ以上の条件で構成されます。
- プロトコル: HTTP,HTTPS
- ポート: 1 ~ 65535
ルールアクションの中にredirect
というタイプがあるので、これを使用することでHTTP=>HTTPSへリダイレクトさせることが可能になります。
(今から1年前くらいに実装された機能です)
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#redirect-actionsdocs.aws.amazon.com
そもそもElasticBeanstalkの標準の名前空間においてALBのリスナーのルールでリダイレクトを設定する機能が存在しません。そのため、.ebextensionsでALBにリダイレクトルールを入れようとしました。下記がその時の設定です。
elastic_beanstalk_environment.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.enechange.jp" } // 複数のルールが一致する場合の、このルールの優先順位。低い番号が優先される setting { namespace = "aws:elbv2:listenerrule:default" name = "Priority" value = "1" } // アクセスログを保存する Amazon S3 バケット setting { namespace = "aws:elbv2:loadbalancer" name = "AccessLogsS3Bucket" value = "enechage" } // アクセスログストレージを有効 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)
alb_redirect.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
結果、デプロイは正常に終了しましたが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-precedencedocs.aws.amazon.com
以下の名前空間を削除して置き換えました。
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.enechange.jp" } setting { namespace = "aws:elbv2:listenerrule:default" name = "Priority" value = "1" }
- .ebextensionが優先されるように更新した設定ファイル
elastic_beanstalk_environment.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.enechange.jp" } // アクセスログを保存する Amazon S3 バケット setting { namespace = "aws:elbv2:loadbalancer" name = "AccessLogsS3Bucket" value = "enechage" } // アクセスログストレージを有効 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)
これでリスナールールが更新されるはず!と思ってapplyするも、正常終了しても既存のルールがそのまま残っており、優先順位が変更できない状態に陥りました。 さんざん悩んだ結果、環境を再作成することで.ebextensions配下の設定ファイルを適用することができました。このあたりはTerraformとCloudFormationの問題だと思うのですが、 今現在も解決していないため、ISSUEを出したいと思っています。
301でリダイレクトされています。
$ curl --head xxxxx.enechange.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.enechange.jp:443/
ALBのログにはredirect
されたことがわかるように出ています。
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/" "-"