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

<del>この記事はフィジビリティが完全には取れていないので注意。</del>
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を使えば似たようなことを簡単に実現できますが、全てのパスに対して毎回チェックが入ってオーバーヘッドが気になるのと、コスト面で選択しませんでした。

要は、これを

<img width="768" alt="通常のElasticBeanstalkとALB" src="https://qiita-image-store.s3.amazonaws.com/0/17735/921e86a6-a31e-9e2a-3712-0b0beb7ee6bd.png">

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

<img width="775" alt="別のALBを追加した図" src="https://qiita-image-store.s3.amazonaws.com/0/17735/c756d35a-6f86-8f3b-b75e-403f5e82c4ce.png">

# 概要手順

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

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

```bash
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番ポートにプロキシしています。

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

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

```nginx:.ebextensions/nginx/nginx.conf
http {
    server {
        listen        80 default_server;
        access_log    /var/log/nginx/access.log main;
        include conf.d/elasticbeanstalk/*.conf;
    }
    # もう１台のALB用LISTENポート
    server {
        listen        8080;
        access_log    /var/log/nginx/access-public.log main;
        include conf.d/elasticbeanstalk/*.conf;
    }
}
```
ただ、この方法だと **特定のパスのみ** という要件にはマッチしないので、`location`ディレクティブを追加してあげます。

```nginx:.ebextensions/nginx/nginx.conf
    # もう１台の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`を作成します。

<img width="594" alt="TargetGroupの作成" src="https://qiita-image-store.s3.amazonaws.com/0/17735/6c1bf407-fd49-454d-6653-d712dac76474.png">

CLIだとこんな感じ。

```bash
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が自動生成した`AutoScalingGroup`の`Name`を取得する必要があります。

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

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

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

```bash
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です。

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

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

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

# Tips

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

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

```nginx:.ebextensions/nginx/nginx.conf
    # もう１台の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`ディレクティブを追加します。

```nginx:.ebextensions/nginx/nginx.conf
    # もう１台の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を利用した設定手順

```tf
# 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}"
}
```

# 注意

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

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

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