ElasticBeanstalk running Docker のデプロイ処理を調べる


ElasticBeanstalk で Docker コンテナアプリケーションをデプロイする際の処理を追ってみました。

# 概要

## 新しいバージョンのデプロイ

EB に Docker をデプロイした際の挙動は下記のようになります。

```
1. アップロードされたソースから、新しいイメージを(必要なら pull して)作成
2. 新しいイメージのコンテナを立ち上げる
3. nginx の conf を更新してリバースプロキシを新しいコンテナイメージの IP アドレスに向ける
4. nginx の設定を再読み込み
5. 古いイメージのコンテナを落とす
```

このように、表に立っているリバースプロキシの切り替えをすることで、ダウンタイムが極力短くなるようになっています。

が、デプロイプロセスが落ちるタイミングが悪かったり、ビルドされた Docker イメージ自体に問題があった場合は、ロールバックされずにアップデートが完了してしまうので、プロダクションにおいて起動中の環境を直接アップデートすることは割と恐ろしいです。
別環境にデプロイして、CNAME スワップでプロダクションと切り替える方が安全そうです。

ちなみにちょっと前(Docker 1.0のころ)までは次のように IP アドレスではなくポートを使って、リバースプロキシの切り替えが行われていました。

```
1. アップロードされたソースから、新しいイメージを(必要なら pull して)作成
2. 新しいイメージのコンテナをホスト側にランダムなポートを割り当てて立ち上げる
3. nginx の conf を吐いてリバースプロキシを localhost の新しいコンテナのポートに向ける
4. nginx の設定を再読み込み
5. 古いイメージのコンテナを落とす
```

変更されたのは万が一のポートかぶりを回避するためでしょうか？
それとも物理 nic との対応表を作ると無駄なオーバヘッドがあるのかな？
(そもそも誰がハンドリングして Docker エンジンにつないでくれているのか知らない)

## デプロイの仕組み

update が叩かれると pre -> enact -> post の順で、指定ディレクトリに配置されたスクリプトが実行されます。

| 処理ステップ | 役割 | ディレクトリ |
| ---- | ---- | ------------ |
| pre | build やコンテナの起動 | `/opt/elasticbeanstalk/hooks/appdeploy/pre` |
| enact | 有効なコンテナの切り替え |  `/opt/elasticbeanstalk/hooks/appdeploy/enact` |
| post | 後処理 | `/opt/elasticbeanstalk/hooks/appdeploy/post` |

これらのディレクトリに .ebextensions などを使って任意のスクリプトを配置しておくと、毎回のデプロイ時に実行されるようになります。
各スクリプトはファイル名順で実行されるので、ファイル名を調整することで任意のタイミングに処理を入れ込むことが可能です。


# 各種起動スクリプト

EB の AMI に標準で含まれている、起動用のスクリプトを紹介します。

## pre ステップ

環境の初期化や、イメージのビルド、ステージング状態でのコンテナの起動が行われます。

### 00clean\_dir.sh

デプロイ環境の準備を行う。

- 前回デプロイ済みのアプリケーションディレクトリ(/var/app/current)をクリア
- eb-docker サービスのログディレクトリが無ければ作成

### 01unzip.sh

アプリケーションの配置を行う。

- ソースを必要なら解凍しつつ /var/app/current に配置する

### 02docker\_db\_check.sh

Docker のイメージ管理用 DB の破損チェック＆修復をする。

- /var/lib/docker/linkgraph.db に破損がないことを確認し、壊れていたら削除して docker を再起動
- チェックしている edge テーブルがよくわからなかったけれど、コンテナ間のリンクを管理しているテーブル？

### 03build.sh

ソースに含まれた Dockerfile または Dockerrun.aws.json から Docker イメージを作成する。
ここで作成された Docker イメージはステージング用のリポジトリ名となる(標準では `aws_beanstalk/staging-app`)。
Dockerfile も Dockerrun.aws.json もない場合はここで落ちる。
Dockerfile が含まれていない場合は Dockerrun.aws.json から自動的に生成される。

- Dockerfile が入っていない場合 Dockerrun.aws.json からイメージ名とポートを抜き出して Dockerfile を作成する
    - ここで生成される Dockerfile は EXPOSE を追加しただけのもの
- プライベートリポジトリの認証情報を S3 に置いてある場合はダウンロードしておく
- Dockerrun.aws.json に不要と記述されていなければ FROM イメージの pull を行う
- Dockerfile から Docker イメージをビルドする

### 04run.sh

03build.sh で作成したイメージから Docker コンテナを起動する。
Dockerfile に EXPOSE が含まれていない場合はここで落ちる。
EntryPoint などコンテナ起動時に設定できる情報は、Dockerfile より Dockerrun.aws.json が優先される。

- Docker コンテナを起動してコンテナ ID を控えておく
    - ホスト側ポートは Dockerfile の EXPOSE で指定されたポートを使う
    - Dockerrun.aws.json を見てコンテナにマウントするディレクトリを決める
    - 環境に設定されている環境変数をコンテナに渡す
    - Dockerrun.aws.json に定義されていたら EntryPoint を追加する
- 5秒後の時点でコンテナが起動していなかったら失敗
    - 逆に言えばこの後にコンテナが終了しても気づかれないで先に進む

## enact ステップ

現在起動中のコンテナから pre で起動されたステージング状態のコンテナに接続先を切り替えます。

### 00flip.sh

nginx のリバースプロキシを、現在起動中のコンテナから pre/04run.sh で起動されたステージング状態のコンテナに切り替える。

- コンテナ ID からコンテナの IP アドレスを取得する
- nginx の docker upstream に関して IP アドレスを変更した conf を生成し、nginx を restart
    - conf はここ : `/etc/nginx/conf.d/elasticbeanstalk-nginx-docker-upstream.conf`
- 過去のコンテナは停止&削除する
- 過去のイメージも削除する

## post ステップ

このステップに標準で配置されているスクリプトはありません。