ElasticbeanstalkのMulti-container Dockerを使ったら便利だった話

[DMM.com #1 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/dmm)の11日目を担当させていただきます@sssinsi です。
前日の記事は @yu-kgr さんの[良いモノを創る為にチームビルドを頑張る話](https://kglabo.com/blog/team-bilding/)でした。

それではMulti-container Dockerを使って環境構築したら、早い！簡単！便利！だった話をします。

## 経緯 :thinking:
開発環境で[Docker compose](https://docs.docker.com/compose/)を使っていたためDockerは身近な存在でしたが、言語別のAWS ElasticBeanstalk(以下、EB)を使うとなると専用のミドルウェア設定が必要となり、途端に開発環境との差異に苦しめられました。

EBでは、各言語の環境を構築すると自動でミドルウェアの設定も行ってくれます。例えばPHPでデプロイするとApacheを使った環境、JavascriptではNginxを使った環境を作ってくれます。

EBを使わずにカスタムAMIを使うことも検討しましたが、できるだけ開発環境で揃えた設定やミドルウェアを使いつつ、俊敏にEB環境を用意したいと考えた結果、Multi-container Dockerならば簡単にそれを実現できることがわかりました :raised_hands:

その際行った試行錯誤を含めて共有したいと思います。

## 用語説明 :book: 
#### Elasticbeanstalkとは？

EBとは、コードをデプロイするだけでプログラミング言語毎にアプリケーション環境を用意してくれるAWSの便利なサービスです。

[AWS elasticbeanstalk](https://aws.amazon.com/jp/elasticbeanstalk/)

PHP、Node、Goはもちろん、Docker（単体、複数）を使った構築も構築できます。

#### Multi-container Dockerとは？
Multi-container Docker環境を使うことで1つのEC2内に複数のDockerを立ち上げることができます。コンテナの管理は[Amazon ECS](http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create_deploy_docker_ecs.html)で行われます。

[docker compose](https://docs.docker.com/compose/)のEB版と考えればわかりやすいでしょう。ただしデプロイ中に`Dockerfile`からイメージを作れないため、独自Dockerイメージを使いたい場合は別途[Amazon ECR](https://aws.amazon.com/jp/ecr/)等を使って事前に作成する必要があります。


## 簡単なコードの説明 :pencil: 
Multi-container Dockerを使ったサンプルコードを用意しました -> [GitHub](https://github.com/sssinsi/multi-container-docker-sample)

#### EB環境設定

```bash
./.ebextensions/
├── 00.option.config
└── 01.env.config
```
[.ebextensions](http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/ebextensions.html)とはフォルダ配下にファイル(`*.config`)を作成することによってLBポート設定、オートスケーリング設定、セキュリティ設定、ミドルウェア用の設定ファイル作成、コマンド実行などなど、EBの環境構築を制御できる場所です。



参考: [ElasticBeanstalkで.ebextensionsを利用してnginx+php-fpmの構成にする](https://qiita.com/toshihirock/items/e0617a5dc9033c1ad53a#ebextensions%E3%81%AE%E4%BB%95%E6%A7%98)


#### イメージ登録
独自DockerイメージをAmazon ECRへ登録するためのシェルです。コードを最新にする時に活用してください。非公開のモジュールを使っている場合、EC2からアクセスできない可能性があります。その時はDockerイメージを作成する段階でモジュールのインストールも行う必要があるので注意してください。

```bash:store-docker-image.sh
#!/bin/bash

set -xue
# login docker（プロフィールが必要な場合）
AWS_ECR_GET_LOGIN="aws ecr get-login --no-include-email --region ap-northeast-1 --profile ${AWSCLI_PROFILE}"
DOCKER_LOGIN=`eval ${AWS_ECR_GET_LOGIN}`
eval ${DOCKER_LOGIN}

# build image
docker build -t php-fpm:latest -f php-fpm.Dockerfile .

# store image
docker tag php-fpm:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-fpm:latest
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-fpm:latest
```

#### 環境変数

Dockerコンテナ内部で環境変数を使用したい時、EC2に設定された環境変数が使用できます。また、Laravelのような環境変数ファイル(`.env`)を使用する場合はサービス起動直前に環境変数ファイルを変更します。サンプルではデプロイ時にconfigで作成した`.env`ファイルを`Dockerrun.aws.json`でコンテナに取り込んでいますが、ParameterStore等を使った方がより安全だと思います。

```bash:01.env.config
files:
  "/tmp/.env"
    mode: "000644"
    owner: "ec2-user"
    group: "ec2-user"
    content: |
      # ここでは環境変数を直接記載しているが、
      # commandsオプションを使ってParameter Storeから環境変数を取得して.envファイルを作成すると更に良い。
      DB_CONNECTION=mysql
      DB_DATABASE=staging-sample
      DB_USERNAME=staging-user
      DB_PASSWORD=staging-passowd
      DB_ROOT_PASSWORD=staging-password
      DB_EXTERNAL_PORT=3306
      DB_PORT=3306
```

#### 設定共通化
以下のように、開発環境(docker-compose.yaml)とEB環境(Dockerrun.aws.json)で同じミドルウェアと設定ファイル使うことで手間が省け、環境構築の高速化に繋がりました。

`/nginx/nginx.conf`と`/nginx/site.conf`は同じファイルを使用しており、docker imageも同じです。

```yaml:docker-compose.yaml
...

services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/site.conf:/etc/nginx/conf.d/default.conf
...
```

```bash:Dockerrun.aws.json
{
...
  "volumes": [
    {
      "name": "nginx_conf",
      "host": {
        "sourcePath": "/var/app/current/nginx/nginx.conf"
      }
    },
    {
      "name": "nginx_site_conf",
      "host": {
        "sourcePath": "/var/app/current/nginx/site.conf"
      }
    }
  ],
  "containerDefinitions":[
    {
      "image": "nginx:alpine",
      "mountPoints": [
        {
          "sourceVolume": "nginx_conf",
          "containerPath": "/etc/nginx/nginx.conf",
          "readOnly": true
        },
        {
          "sourceVolume": "nginx_site_conf",
          "containerPath": "/etc/nginx/conf.d/default.conf",
          "readOnly": true
        }
      ],
    }
  ]
...
}
```


## デプロイ方法 :airplane_departure:
### 準備

まずは`eb init`で作りたい環境を指定します。この時プラットフォームを`Multi-container Docker`にします。これによって`.elasticbeanstalk/config.yml`が作成されます。そして、必要に応じてdockerイメージを登録しておきます。


### デプロイ実施
`eb create`を使ってALB環境を作ります。この時デプロイも同時に実施されます。
※EBでALB(Application Load Balancer)を作りたい場合、CLIからしか作れないようです。

```bash
$eb create sample-env \
 --elb-type application \
 --vpc.id vpc-aaaaaaaa \
 --vpc.ec2subnets subnet-bbbbbbbb,subnet-cccccccc \
 --vpc.elbsubnets subnet-dddddddd,subnet-eeeeeeee
```
※VPCの指定は必須ではありません。

ソースコードが変更された場合、再度dockerイメージを登録して、`eb deploy`すれば環境へ反映されます :pray: 

## 気がついたこと:bulb:
今回の環境構築で気がついたことを、メリット/デメリットとしてまとめてみました。

- メリット: 
    - 開発環境とAWS環境が同じように作れる
    - ミドルウェア設定が容易
    - Dockerなので言語バージョンの追従が用意
    - Dockerの方がデプロイが早い場合もある
- デメリット:
    - AWS EB、Amazon ECS、Amazon ECRなどを使用できる権限が必要(制限されていたので、、、)
    - AWS EBの起動スクリプトの順序に悩まされる
    - インフラ運用にAWS ECSを使う必要がある

## 最後に:end:
みなさんガンガンAWSを使って、良いサービスをどんどん作っていきましょう！！！ :muscle: 
<img src="https://qiita-image-store.s3.amazonaws.com/0/18168/38de01fb-86a0-117d-b94a-f4db0c11a93c.png"/>