ENECHANGE Developer Blog

ENECHANGE開発者ブログ

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

こんにちは。レイブパーティーで一晩中踊り明かしたい衝動に駆られてミニマル・テクノ界隈を徘徊しているCTO室のkazです。 本エントリは下書きにしたまま放置してあったんですが、ElasticBeanstalkとTerraformの記事って、ネットであまり見かけないしバッドノウハウは需要あると思い、掘り起こしてブラッシュアップしました。

ENECHANGEでは一部を除くプロダクト以外ではElasticBeanstalkを使用して運用しています。

youtu.be

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を出したいと思っています。

f:id:dev-enechange:20190723105546p:plain

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/" "-"