【AWS】Elastic Beanstalkのデプロイ時に docker exec する

## 先に結論

Elastic Beanstalk のオートスケーリング設定が有効になっている環境で複数の EC2 インスタンスに対してデプロイする際に、どれか一つの EC2 インスタンス上で DB マイグレーションのコマンドを実行したいとします。
その場合、以下のような設定ファイルを追加してデプロイすると実現できます。

```yaml:.ebextensions/post_migration.config
container_commands:
  01_migrate: # リーダー判定用の一時ファイルを作成
    command: "touch /tmp/leader_only"
    leader_only: true

files:
  "/opt/elasticbeanstalk/hooks/appdeploy/post/10_post_migrate.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/bin/bash -eu
      if [ -f /tmp/leader_only ] # リーダーインスタンスでのみ実行
      then
        # リーダー判定用の一時ファイルを削除する
        rm /tmp/leader_only
        # 最後に起動した Docker コンテナ上で migrate を実行する (CakePHP3の場合)
        docker exec `docker ps -l -q` bin/cake migrations migrate
      fi
```

## 上記の説明

上記の方法は `container_commands:` で直接 migrate せずに `files:` でシェルスクリプトを作成し、そのスクリプトの中で migrate するという回りくどい手順になっています。その辺りの事情をデプロイの流れと共に説明します。

## Elastic Beanstalk (Docker) のデプロイ時の流れ

Elastic Beanstalk には [デプロイ処理の各タイミングで任意スクリプトをフック実行する為の仕組み](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/custom-platform-hooks.html) があります。予め決められたディレクトリにシェルスクリプトを置いておくと、ファイル名の辞書順に実行してくれる仕組みです。

このフック実行用のディレクトリには、インスタンスを作成した段階で Elastic Beanstalk を構成するいくつかのスクリプト（以下を参照）が入っていますが、ユーザーが任意のスクリプトを追加することもできます。

### 1. pre hook スクリプトの実行:

- 1-1. /opt/elasticbeanstalk/hooks/appdeploy/pre/00clean_dir.sh
    - デプロイの準備。/var/app/current の初期化を行う。
- 1-2. /opt/elasticbeanstalk/hooks/appdeploy/pre/01unzip.sh
    - アプリケーションを unzip して /var/app/current へ展開する。
- 1-3. /opt/elasticbeanstalk/hooks/appdeploy/pre/02loopback-check.sh
- 1-4. /opt/elasticbeanstalk/hooks/appdeploy/pre/03build.sh
    - /var/app/current にある `Dockerfile` OR `Dockerrun.aws.json` を元に Docker Image を作成 (`docker build` OR `docker pull`) する。

### 2. .ebextensions の `container_commands` の実行:

- 2-1. .ebextensions/*.config に container_commands がある場合、順にそのコマンドを実行する。
    - 既に新バージョンのアプリは既に unzip 済みのため、それらに触れることができる。
    - このとき、新バージョン docker コンテナは **まだ起動していない。**

参考: [\.ebextensions の container\_commands の挙動メモ \- Qiita](http://qiita.com/ymm1x/items/8eba92e15420715e9a2f)


### 3. enact hook スクリプトの実行:

- 3-1. /opt/elasticbeanstalk/hooks/appdeploy/enact/00run.sh
    - 03build.sh で作成したイメージを `docker run -d` する。
- 3-2. /opt/elasticbeanstalk/hooks/appdeploy/enact/01flip.sh
    - `staging-app` を `current-app` にする。

### 4. post hook スクリプトの実行:

- 4-1. /opt/elasticbeanstalk/hooks/appdeploy/post/00_clean_imgs.sh
    - 未使用のイメージやコンテナを削除する。
- 4-2. /opt/elasticbeanstalk/hooks/appdeploy/post/01_monitor_pids.sh

---

上記のような流れのため、デプロイ時に `docker exec` したい場合は `00run.sh` 以降で行う必要があります。container_commands のタイミングでは実現できません。

その為 .ebextensions で post hook 用のシェルスクリプトを作成して、その中で `docker exec` するという方法を取っています。

また、オートスケーリングの設定をしている場合、作成したフックスクリプトはデプロイされる全てのコンテナ上で実行されるため、そのままだとマイグレーションが何度も実行されることになります。

それでは困るのでリーダーインスタンスでのみ事前に `/tmp/leader_only` のファイルを作成しておき、そのファイルが存在する場合にのみ post hook スクリプトを実行するようにしています。

container_commands の leader_only オプションについて引用:

> Elastic Beanstalk によって選択された単一のインスタンスでコマンドを実行するのみです。リーダー専用コンテナコマンドは、他のコンテナコマンドより前に実行されます。

https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/customize-containers-ec2.html#linux-container-commands

## 参考

- [AWS ElasticBeanstalk deployment hooks](http://www.eq8.eu/blogs/29-aws-elasticbeanstalk-deployment-hooks)