Nginx + Rails (Puma) on ECS のいくつかの (本番環境にむけた) 構成例

コンテナの分離レベル, サービスの分離レベルに応じた 3 つの構成例を紹介します.

本投稿は以下を前提とします.

- ECS Cluster は構築済み
- VPC / Security Group / Application Load Balancer / RDS の構築ができる

# 要件

以下の要件は全構成例で達成すべきものとします.

- フロントエンドは Nginx, バックエンドは Rails5 (Puma)
- Rails の静的コンテンツ (`public/` 配下) は Nginx が処理する

また, 可能な限り Nginx-Puma 間は socket 通信を利用するものとします.

# 1. 単一コンテナ × 単一サービス

![http://imgur.com/sW7gsZ5.png](http://imgur.com/sW7gsZ5.png)

単一イメージに Nginx, Rails をまとめた, 最もシンプルなパターンです. 詳細の解説は省略します.

# 2. 個別コンテナ (Nginx, Rails) × 単一サービス

Nginx/Rails を個別イメージにしつつ, サービスとしては Application service として 1 つにまとめた形です.

サンプルアプリケーションコード, Dockerfile (Rails/Nginx) は https://github.com/na-o-ys/app1/tree/0.1.0-single-service から, Docker イメージは [naoys/app1-rails](https://hub.docker.com/r/naoys/app1-rails/), [naoys/app1-nginx](https://hub.docker.com/r/naoys/app1-nginx/) から参照できます.

![http://i.imgur.com/DHLeKzQ.png](http://i.imgur.com/DHLeKzQ.png)

- Nginx イメージ / Rails イメージを用意する
- ECS では単一サービス (Task Definition) に 2 つのイメージを含める

### 要点

- Rails の静的コンテンツ (`public/` 配下) を Nginx で処理するため && Rails-Nginx 間でソケット通信を行うために,
    - [`config/puma`](https://github.com/na-o-ys/app1/blob/0.1.0-single-service/config/puma.rb) で `bind "unix://#{app_root}/tmp/sockets/puma.sock"` する
    - [`Dockerfile` (Rails イメージ)](https://github.com/na-o-ys/app1/blob/0.1.0-single-service/Dockerfile) で `VOLUME /app/public`, `VOLUME /app/tmp` を指定する
    - Task Definition の Nginx コンテナパートで `volumesFrom` を指定する
- Task Definition で Rails の動作に必要な環境変数 (データベースホスト, 接続情報, SECRET_KEY_BASE など) を埋め込む
- Task Definition で Nginx コンテナとホストの動的ポートマッピング (0:80) を指定

### Task Definition (Application service)

```json
{
    "family": "app1",
    "networkMode": "bridge",
    "containerDefinitions": [
        {
            "name": "app1-rails",
            "image": "naoys/app1-rails:0.1.0-single-service",
            "memory": 300,
            "environment": [
                {
                    "name": "DATABASE_USERNAME",
                    "value": "XXX"
                },
                {
                    "name": "RAILS_ENV",
                    "value": "production"
                },
                {
                    "name": "DATABASE_HOST",
                    "value": "XXX(RDS DNS Host)"
                },
                {
                    "name": "SECRET_KEY_BASE",
                    "value": "XXX"
                },
                {
                    "name": "DATABASE_PASSWORD",
                    "value": "XXX"
                }
            ]
        },
        {
            "name": "app1-nginx",
            "image": "naoys/app1-nginx:0.1.0-single-service",
            "memory": 300,
            "volumesFrom": [
                {
                    "readOnly": null,
                    "sourceContainer": "app1-rails"
                }
            ],
            "portMappings": [
                {
                    "hostPort": 0,
                    "containerPort": 80,
                    "protocol": "tcp"
                }
            ],
            "environment": []
        }
    ]
}
```

# 3. 個別サービス (Nginx, Rails)

イメージ, サービスともに Nginx/Rails を個別にした形です. ~~公式的にはこちらが推奨されているようです (参考: [Application Architecture - Amazon EC2 Container Service](http://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/application_architecture.html)).~~ (読み違えm(_ _)m. コメント欄でご指摘頂いたように, Nginx / Rails が密結合している今回のような構成では, サービスまで分離するのは不自然なようです).
Rails サービスの各タスクをバランシングするために, internal ALB が必要となります.

サンプルアプリケーションコード, Dockerfile (Rails/Nginx) は https://github.com/na-o-ys/app1/tree/0.1.0-multi-service から, Docker イメージは [naoys/app1-rails](https://hub.docker.com/r/naoys/app1-rails/), [naoys/app1-nginx](https://hub.docker.com/r/naoys/app1-nginx/) から参照できます.

![http://i.imgur.com/8WnhLdL.png](http://i.imgur.com/8WnhLdL.png)

- Nginx イメージ / Rails イメージを用意する
- ECS では Nginx サービス / Rails サービスを用意する
- Rails サービス用の internal ALB を用意する

### 要点

- Nginx コンテナから Rails サービスの ALB ホストを発見するため,
    - Nginx イメージ起動時に `nginx.conf` に対して sed で環境変数を埋め込む
    - 参考: [nginx.conf#L3](https://github.com/na-o-ys/app1/blob/0.1.0-multi-service/containers/nginx/nginx.conf#L3), [Dockerfile#L8](https://github.com/na-o-ys/app1/blob/0.1.0-multi-service/containers/nginx/Dockerfile#L8)
- Rails の静的コンテンツ (`public/` 配下) を Nginx で処理するため, Nginx イメージに直接 `/app/public` を含める
    - 参考: [copy_public.sh](https://github.com/na-o-ys/app1/blob/0.1.0-multi-service/copy_public.sh) (別途, ビルド毎に叩く形にする必要があります), [Dockerfile#L8](https://github.com/na-o-ys/app1/blob/0.1.0-multi-service/containers/nginx/Dockerfile#L8)

#### Task Definition (Nginx service)

```json
{
    "family": "app1-nginx",
    "networkMode": "bridge",
    "containerDefinitions": [
        {
            "name": "app1-nginx",
            "image": "naoys/app1-nginx:0.1.0-multi-service",
            "memory": "300",
            "portMappings": [
                {
                    "hostPort": "0",
                    "containerPort": "80",
                    "protocol": "tcp"
                }
            ],
            "environment": [
                {
                    "name": "RAILS_HOST",
                    "value": "XXX(internal ALB (Rails) Host)"
                }
            ]
        }
    ]
}
```

### Task Definition (Rails service)

```json
{
    "family": "app1-rails",
    "networkMode": "bridge",
    "containerDefinitions": [
        {
            "name": "app1-rails",
            "image": "naoys/app1-rails:0.1.0-multi-service",
            "memory": "300",
            "portMappings": [
                {
                    "hostPort": "0",
                    "containerPort": "3000",
                    "protocol": "tcp"
                }
            ],
            "environment": [
                {
                    "name": "DATABASE_USERNAME",
                    "value": "XXX"
                },
                {
                    "name": "RAILS_ENV",
                    "value": "production"
                },
                {
                    "name": "DATABASE_HOST",
                    "value": "XXX(RDS DNS Host)"
                },
                {
                    "name": "SECRET_KEY_BASE",
                    "value": "XXX"
                },
                {
                    "name": "DATABASE_PASSWORD",
                    "value": "XXX"
                }
            ]
        }
    ]
}
```

### その他

- Nginx / Rails タスクが別れるため, **socket 通信は不可能**
    - TCP 3000 ポートを利用する
- `public/` を Nginx イメージに含めるのが面倒
    - シンボリックリンクは使えない; Docker のビルドコンテキストはシンボリックリンクを追いかけない
    - Nginx イメージのビルドコンテキストをより高階層にするか, まるごとコピーするしかない
    - (今回, 前者はやりたくなかったので後者とした)
- `nginx.conf` に環境変数を埋め込む方法が強引
    - lua_module があれば埋め込めるが, Nginx 公式イメージには lua_module が含まれていない

# 雑感

~~新規開発であればパターン 3. か 2., 既存プロジェクトの載せ替えであれば変更コストなど鑑みながら 1~3 を検討すると良いかと思います.~~

追記: 今回のように Rails / Nginx の結合度が高く, 同じインスタンスで稼働するべき場合は, パターン 2 が推奨されるようです. パターン 3 については, 結合度が低かったりスケーリングのライフサイクルが異なる場合, 例えば Nginx に対して複数種類の Web アプリケーションがぶら下がる構成などの場合に, 検討する価値があるかと思います.

> When you’re considering how to model task definitions and services, it helps to think about what processes need to run together on the same instance and how you will scale each component.
> ([Application Architecture - Amazon EC2 Container Service](http://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/application_architecture.html))

# 参考

- Application Architecture - Amazon EC2 Container Service: http://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/application_architecture.html
- Docker上のNginxのconfに環境変数(env)を渡すたったひとつの全く優れてない方法（修正：+優れている方法）: http://qiita.com/takyam/items/e92e5a6ca1548cbd58db