AWS
ElasticBeanstalk
Terraform
ALB

ElasticBeanstalkのEC2にALBを後から被せる

この記事はフィジビリティが完全には取れていないので注意。

2018/03/16 追記 フィジビリティ取れました。
ElasticBeanstalkのImmutableデプロイでAutoScalingGroupが削除/新規作成されても新しい方のインスタンスをターゲットグループに自動追加してくれました。

背景

APIサーバとフロントエンドサーバがAWS上のElasticBeanstalkで動いていて、APIサーバはフロントエンドサーバからのアクセスのみを許可するためInternal ELBにPrivate DNSレコードを割り当てて運用しています。

特定のIPアドレスからのみ、特定のパスに対してAPIを直接実行させなければならない要件が出てきました。
フロントエンドをProxyにしてもいいですが、フロントエンドのSecurityGroupは0.0.0.0/0を許可しているため、SecurityGroupの内側(Nginxなど)でIP制限をかけると責任範囲がAWSでなくなるし、セキュリティの担保が自前になるためやりたくありません。

特定のパスのみIP制限つきのPublicにしたい、という要件を満たすためにALBを後から追加する方法を試しました。

ちなみに、AWS WAFを使えば似たようなことを簡単に実現できますが、全てのパスに対して毎回チェックが入ってオーバーヘッドが気になるのと、コスト面で選択しませんでした。

要は、これを

通常のElasticBeanstalkとALB

こうしたかったわけです。

別のALBを追加した図

概要手順

  • ElasticBeanstalk内のEC2上でLISTENポートを増やす。
  • 空(ターゲット未登録)のTargetGroupを作成する。
  • ElasticBeanstalkのAutoScalingGroupTargetGroupに登録する。
  • ALBを作成し、作成したTargetGroupを紐付ける。
  • ALBからのアクセス許可をElasticBeanstalkのSecurityGroupに追加する。

マネジメントコンソールやaws elbのCLIリファレンスを見ると、ALBにはIPアドレスもしくはインスタンスIDを紐付けなければいけないようにしか見えませんが、これが罠で、aws autoscalingのCLIリファレンスを見ると、attach-load-balancer-target-groupsというコマンドがあります。

aws autoscaling attach-load-balancer-target-groups \
  --auto-scaling-group-name <value> \
  --target-group-arns <value>

このコマンドを使うとElasticBeanstalkがEC2インスタンスを増減させても自動的にALBの到達先も変わってくれます。超便利!!

詳細手順

前置きが長くなりましたが、実際にこれらを実装する手順は以下となります。

ElasticBeanstalkのEC2内でLISTENポートを増やす

ここでは、JavaのElasticBeanstalkを例として説明します。

ElasticBeanstalkはプラットフォーム言語によってEC2の内部構成がだいぶ異なるので、Rubyなど他のプラットフォームを利用している場合とNginxの設定が異なるので注意してください。

Java版のElasticBeanstalkでは親切なことにソースディレクトリに .ebextensions/nginx というディレクトリがあれば自動でその中身を/etc/nginxに展開してくれます。

ElasticBeanstalk Javaの標準のnginx.confはこんな感じの設定になっていて、80番ポートからlocalhostの5000番ポートにプロキシしています。

.ebextensions/nginx/nginx.conf
http {
    server {
        listen        80 default_server;
        access_log    /var/log/nginx/access.log main;
        include conf.d/elasticbeanstalk/*.conf;
    }
}

なので、単純にserverディレクティブを追加してあげればポートを増やすことができます。

.ebextensions/nginx/nginx.conf
http {
    server {
        listen        80 default_server;
        access_log    /var/log/nginx/access.log main;
        include conf.d/elasticbeanstalk/*.conf;
    }
    # もう1台のALB用LISTENポート
    server {
        listen        8080;
        access_log    /var/log/nginx/access-public.log main;
        include conf.d/elasticbeanstalk/*.conf;
    }
}

ただ、この方法だと 特定のパスのみ という要件にはマッチしないので、locationディレクティブを追加してあげます。

.ebextensions/nginx/nginx.conf
    # もう1台のALB用LISTENポート
    server {
        listen        8080;
        access_log    /var/log/nginx/access-public.log main;
        # 外部からのアクセスを許可するパスを指定
        location /public {
            proxy_pass http://localhost:5000;
            proxy_set_header xxxx ... ;
        }
        # それ以外は404にしちゃう
        location / { return 404; }
    }

空のTargetGroupを作成

ポートをnginx.confに追加したポートを向くようにTargetGroupを作成します。

TargetGroupの作成

CLIだとこんな感じ。

aws elbv2 create-target-group --name <value> --vpc-id <value> --protocol 'HTTP' \
  --port 8080 --target-type 'instance' --health-check-path '/public/'

ポイント

  • ポートにはNginxに追加したポート番号を指定する。
  • ターゲットの種類ではinstanceを選択する。
  • ヘルスチェックパスでNginxで指定したパス配下を指定する。(/は404になるため)

TargetGroupにElasticBeanstalkのAutoScalingGroupをAttatchする

この操作はマネジメントコンソール上からはできません。なのでCLIを利用します。

まずはElasticBeanstalkが自動生成したAutoScalingGroupNameを取得する必要があります。

aws elasticbeanstalk describe-environment-resources --environment-name <value> \
  --query 'EnvironmentResources.AutoScalingGroups' --output text

上記コマンドを実行すると、awseb-e-abcdefghij-stack-AWSEBAutoScalingGroup-123456789ABCのようなAutoScalingGroupのNameが取得できます。(インタフェース上は複数返ってきますが、通常は1つのはずです)

こいつを、先に作成したTargetGroupにアタッチしてあげます。

aws autoscaling attach-load-balancer-target-groups \
  --auto-scaling-group-name '<取得したAutoScalingGroupのName>' \
  --target-group-arns '<作成したTargetGroupのARN>'

コマンド出力はありません。うまくいったかどうかはマネジメントコンソールで確認します。
うまくいっていれば、登録済みターゲットにElasticBeanstalk配下のインスタンスIDが表示されます。
(表示されるまで少し時間がかかります。)

ALBを作成し、作成したTargetGroupを紐付ける

普通にALBを作成して既存のターゲットグループを選択するだけなので手順は割愛します。

今回の目的であったIP制限は、このALBのSecurityGroupに対して指定したIPアドレス(CIDR)のIngressを追加することで実現可能です!!

あらかじめACMで証明書を発行しておいてHTTPS化したり、Route53でALIASレコードを作成してあげれば実運用可能な状態となります。

ALBからのアクセス許可をElasticBeanstalkのSecurityGroupに追加

これはElasticBeanstalkをどうやって構築したかによって追加の仕方がいくつもあるので詳しい手順は割愛しますが、ElasitcBeanstalkの汎用オプションを使った例では以下で指定したSecurityGroupに追加すればOKです。

aws:autoscaling:launchconfiguration:
  SecurityGroups: sg-xxxxxxxxx # <- ここのSecurityGroupに追加

なお、マネジメントコンソールで環境を作成した場合はSecurityGroupは勝手に生成されてユーザ管理外となるため、手運用にならざるを得ません。(そもそも手運用であれば今回の要件も手運用で実現可能です)

ElasticBeanstalkの環境をマネジメントコンソールで作成するのはダメ!絶対!

Tips

ルートパスを特定のパスにProxyする

これはAWSは関係なく、Nginxの話ですが、以下のように設定すると/publicでアクセスしなくても、/がProxy先の/public/にアクセス可能になります。

.ebextensions/nginx/nginx.conf
    # もう1台のALB用LISTENポート
    server {
        listen        8080;
        access_log    /var/log/nginx/access-public.log main;
        location / {
            proxy_pass http://localhost:5000/public/; # 末尾にスラッシュをつける
            proxy_set_header xxxx ... ;
        }
    }

ヘルスチェックのエンドポイントを共有する

上記例では/public配下のみをアクセスさせるようにしたので、既存のヘルスチェックエンドポイントが/public配下にないと、新規でエンドポイントを作成する必要があります。
既存のエンドポイントを利用する場合はそこだけlocationディレクティブを追加します。

.ebextensions/nginx/nginx.conf
    # もう1台のALB用LISTENポート
    server {
        listen        8080;
        access_log    /var/log/nginx/access-public.log main;
        # ヘルスチェックだけ別パスにProxy
        location /healthcheck {
            proxy_pass http://localhost:5000;
            proxy_set_header xxxx ... ;
        }
        location / {
            proxy_pass http://localhost:5000/public/; # 末尾にスラッシュをつける
            proxy_set_header xxxx ... ;
        }
    }

Terraformを利用した設定手順

# ElasticBeanstalk Environment
resource "aws_elastic_beanstalk_environment" "api" { /* 割愛 */ }

# ALB本体
resource "aws_alb" "public" { /* 割愛 */ }

# ALBのTargetGroup
resource "aws_alb_target_group" "public" {
  load_balancer_arn = "${aws_alb.public.arn}"
  # 割愛
}

# ElasticBeanstalkのAutoScalingGroupをALBのTargetGroupにAttatch
resource "aws_autoscaling_attachment" "asg_attachment" {
  # リストで返ってくるので先頭のものを指定している。countを使っても良い。
  autoscaling_group_name = "${element(aws_elastic_beanstalk_environment.api.autoscaling_groups, 0)}"
  alb_target_group_arn   = "${aws_alb_target_group.public.arn}"
}

注意

まだデプロイや環境のアップグレードまわりで問題ないことが確認できていません。自己責任で適用お願いします。

例えば、マネジメントコンソール上でAutoScalingGroupが置き換わるような変更をElasticBeanstalkに対して行うと、ElasitcBeanstalkが古いAutoScalingGroupを削除しようとして、TargetGroupに紐付いているため削除できずにゾンビ化する可能性があります。 これから実際に運用してみて問題がないか確認していきます。

2018/03/16 追記 フィジビリティ取れました。
ElasticBeanstalkのImmutableデプロイでAutoScalingGroupが削除/新規作成されても新しい方のインスタンスをターゲットグループに自動追加してくれました。