ECS FargateでRails動かそうとして2ヶ月かかって手元に残ったもの【1.docker編】

弊社の基幹システムをVPSからAWSに移行するにあたって、まずはrailsを動かしてみる、という段階で無事に死にました☆
今後のための備忘録です。

# やりたいこと
![バックエンド構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131949/0252749a-67e6-6e53-02c7-0fe56e9b1c33.png)
こんな感じ。

# 今回やったこと
## ファイル構成

```
プロジェクトルート
  ├ docker
  │  ├ mysql
  │  │  ├ volumes
  │  │  │  └ ※mysqlの永続化に使う。後述
  │  │  ├ charset.cnf
  │  │  ├ password.yml
  │  │  └ Dockerfile
  │  ├ nginx
  │  │  ├ development
  │  │  │  ├ default.conf
  │  │  │  └ Dockerfile
  │  │  ├ production
  │  │  │  ├ default.conf
  │  │  │  └ Dockerfile
  │  │  └ nginx.conf
  │  └ rails
  │     └ Dockerfile
  ├ src
  │  └ ※railsアプリのソースをここに置きます。後述
  ├ .gitignore
  ├ buildspec.yml
  ├ docker-compose.common.yml
  ├ docker-compose.development.yml
  ├ docker-compose.production.yml
  └ README.md
```
開発環境と本番環境で`docker-compose`の内容にかなり差があるので、ファイルを分けています。
共通する内容は`docker-compose.common.yml`に記述して、なるべく助長化を防ぎます。
ただし、nginxの`Dockerfile`に関しては環境ごとにほぼ同じ内容でファイルを分けて作っています。

これも後述しますが、最後までハマり続けたポイントを解決するための苦肉の策です。

## docker-compose

```yaml:docker-compose.common.yml
version: '2'
services:
  server:
    environment:
      TZ: Asia/Tokyo
    ports:
      - '80:80'
  web:
    build:
      context: .
      dockerfile: ./docker/rails/Dockerfile
    ports:
      - '3000:3000'
    volumes:
      - ./src:/app
    extends:
      file: ./docker/mysql/password.yml
      service: password
  db:
    build:
      context: .
      dockerfile: ./docker/mysql/Dockerfile
    ports:
      - '3306:3306'
    extends:
      file: ./docker/mysql/password.yml
      service: password
```

```yaml:docker-compose.development.yml
version: '2'
services:
  datastore:
    image: busybox
    volumes:
      - /share
      - ./docker/mysql/volumes:/var/lib/mysql
  server:
    build:
      context: .
      dockerfile: ./docker/nginx/development/Dockerfile
    extends:
      file: docker-compose.common.yml
      service: server
    volumes_from:
      - datastore
    depends_on:
      - datastore
  web:
    extends:
      file: docker-compose.common.yml
      service: web
    command: bundle exec unicorn -p 3000 -c /app/config/unicorn.rb
    volumes_from:
      - datastore
    depends_on:
      - db
  db:
    extends:
      file: docker-compose.common.yml
      service: db
    volumes_from:
      - datastore
    depends_on:
      - datastore
```

```yaml:docker-compose.production.yml
version: '2'
services:
  server:
    build:
      context: .
      dockerfile: ./docker/nginx/production/Dockerfile
    extends:
      file: docker-compose.common.yml
      service: server
  web:
    extends:
      file: docker-compose.common.yml
      service: web
    depends_on:
      - server
    environment:
      RAILS_ENV: production
      DB_HOST: ◯◯.△△.ap-northeast-1.rds.amazonaws.com
      DB_USERNAME: ユーザー名
      DB_PASSWORD: パスワード
      DB_DATABASE: データベース名
```

serviceで言えば、

- development
    - datastore → busybox
    - server → nginx
    - web → ruby（rails)
    - db → mysql

- production
    - server → nginx
    - web → ruby(rails)

といった構成になっています。
developmentのdatastoreでvolumesとしてローカルディレクトリの`docker/mysql/volumes`を`/var/lib/mysql`にマウントしています。
dockerはイメージを停止するとデータが消えてしまいますが、このようにすることで、開発環境のデータベースを永続化できます。
`docker/mysql/volumes`の中身は`.gitignore`に設定して、git管理はしないようにしましょう。

本番環境ではECS fargate上でストレージの共有ができるので`datastore`は不要です。
また、データベースはRDSを用いるので`db`も不要です。

## Dockerfile
### nginx

```dockerfile:docker/nginx/development/Dockerfile
FROM nginx:1.11
RUN apt-get update && \
    apt-get install -y apt-utils \
                       locales && \
    sed -i -e 's/# ja_JP.UTF-8/ja_JP.UTF-8/g' /etc/locale.gen && \
    locale-gen ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LC_TIME C
ADD ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
# productionでは次の行のファイルパスが変わる
ADD ./docker/nginx/development/default.conf /etc/nginx/conf.d/default.conf
```
### rails

```dockerfile:docker/rails/Dockerfile
FROM ruby:2.2.3
RUN apt-get update -qq && \
    apt-get install -y apt-utils \
                       build-essential \
                       libpq-dev \
                       nodejs \
                       mysql-client

RUN mkdir /app
WORKDIR /app
ADD ./src/Gemfile /app/Gemfile
ADD ./src/Gemfile.lock /app/Gemfile.lock
RUN bundle install -j4
ADD ./src /app

EXPOSE 3000
# 次の行は本番環境でしか実行されない
CMD ["unicorn", "-p", "3000", "-c", "/app/config/unicorn.rb", "-E", "production"]
```

### mysql

```dockerfile:docker/mysql/Dockerfile
FROM mysql:5.7

RUN apt-get update && \
    apt-get install -y apt-utils \
                       locales && \
    rm -rf /var/lib/apt/lists/* && \
    echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8
ADD ./docker/mysql/charset.cnf /etc/mysql/conf.d/charset.cnf
```

それぞれのコンテナのコマンドやファイルのコピー、ライブラリの更新などは`Dockerfile`に記述するようにしています。
ただ、ローカルのdockerではコマンドが動いても本番では動かない、もしくはその逆が度々あったので、railsの`Dockerfile`には`CMD`の記述がされていたりしますが、productionでしか使わないコマンドが`Dockerfile`に書いてあるのは気持ち悪い気がするので、ここは後々修正したいです。

## その他のファイル
### nginx

```conf:docker/nginx/development/default.conf
upstream unicorn {
  # productionではserver以降が 127.0.0.1:3000 になる
  server unix:/share/unicorn.sock;
}
server {
  listen 80 default_server;
  server_name localhost;
  root /app/public;
  try_files $uri/index.html $uri @unicorn;

  proxy_connect_timeout 600;
  proxy_read_timeout    600;
  proxy_send_timeout    600;

  location @unicorn {
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://unicorn;
  }
}
```
```conf:docker/nginx/nginx.conf
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}


http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

  access_log  /var/log/nginx/access.log  main;
  sendfile        on;
  keepalive_timeout  600;
  include /etc/nginx/conf.d/*.conf;
}
```

ここでのポイントは`default.conf`の2行目の記述と、それぞれのファイルのtimeoutの時間設定です。
この2点が今回ハマって、かなりの時間を奪われたポイントでした。

####1. ソケットファイル

```conf:default.conf(抜粋)
upstream unicorn {
  # productionではserver以降が 127.0.0.1:3000 になる
  server unix:/share/unicorn.sock;
}
```
developmentではbusyboxを使ったストレージ共有で、ソケットファイルを準備していました。
nginxとrails(unicorn)の両方から、同じソケットファイルを見ることで繋げるようなイメージですが、本番環境ではストレージの共有ができず、直接ローカルIPとポート`127.0.0.1:3000`を指定したら動きました。

なるべく助長なファイル構成は避けたかったのですが、nginxの設定ファイルに環境変数などを読み込ませることが手間だったので、このファイルは開発環境と本番環境でほとんど同じ内容なのですが、環境ごとにファイルを分けて対応しています。

####2. タイムアウト

```conf:default.conf(抜粋)
proxy_connect_timeout 600;
proxy_read_timeout    600;
proxy_send_timeout    600;
```
```conf:nginx.conf(抜粋)
keepalive_timeout  600;
```

railsサーバーやRDSへの接続には少なからず時間がかかります。
キャッシュが効き始めれば読み込み速度は大きく向上するのですが、初回アクセス時などは`504 Gateway Time-Out`のエラーになることも多くありました。
そのため、タイムアウトまでの時間を伸ばしました。
デフォルトは60秒などで設定されていると思います。

### mysql
```conf:docker/mysql/charset.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
[client]
default-character-set=utf8mb4
```

```yaml:docker/mysql/password.yml
version: '2'
services:
  password:
    environment:
      MYSQL_ROOT_PASSWORD: ルートパスワード
      TZ: "Asia/Tokyo"
```

セキュアな情報はできるだけコード上には書きたくなかったのですが、最低限環境変数として管理できるようにしました。
運用段階までにはもう少し改善したいです。

## railsソース側の変更
当方は複数のrailsアプリケーションを管理する必要があったので、docker関連のファイルを容易に使い回せるように、あえてrailsアプリケーションのソースはsrcディレクトリに分けて配置しています。
ここでは`unicorn`というgemの設定と、mysqlとRDSを使い分けるための設定をします。

### unicorn
`unicorn`はrailsのアプリケーションサーバとnginxのwebサーバを繋げるためのpassengerのようなモジュールの位置付けです。
単体でもサーバとして使えるらしいです。

```ruby:src/Gemfile
# 追記
gem "unicorn"
```

```sh
bundle install
```

インストールが完了したら、早速設定ファイルを編集します。
`src/config`ディレクトリに`unicorn.rb`ファイルがない場合は作ります。

```ruby:src/config/unicorn.rb
worker_processes 2
pid "/var/run/unicorn.pid"

# developmentとproductionで場合分け
if ENV['RAILS_ENV'] == 'production'
  listen 3000
else
  listen "/share/unicorn.sock"
end

# タイムアウトまでの時間を伸ばす
timeout 600
```
ここでも先程のnginxと同じように、開発環境と本番環境の場合分け、タイムアウトの設定をしています。
特に`listen`の値は`default.conf`と設定がズレていると起動後に上手く繋がらないので、ご注意ください。

### mysql
```yaml:src/config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  timeout: 5000

development:
  <<: *default
  username: 開発環境のユーザー名
  password: <%= ENV['MYSQL_ROOT_PASSWORD'] %>
  database: 開発環境のデータベース名
  host: db

production:
  <<: *default
  host: <%= ENV['DB_HOST'] %>
  username: <%= ENV['DB_USERNAME'] %>
  password: <%= ENV['DB_PASSWORD'] %>
  database: <%= ENV['DB_DATABASE'] %>
```
`Dockerfile`や`password.yml`で設定してきた環境変数がここで読み込まれていきます。
developmentのhostで指定している`db`というのは`docker-compose.yml`でデータベースに設定しているサービス名です。
別の名前に設定している場合はそちらの名前にしてください。

## ビルドと起動
やっと動かす段階です。
dockerを起動しておいてください。
また、`3000`番ポート、`3306`番ポートを使うので、他で使っていれば、そちらを停止しておきます。
プロジェクトのルートディレクトリでコマンドを打ちます。

```sh:ビルド
docker-compose -f docker-compose.development.yml -p development build
```

```sh:データベース作成（初回のみ）
docker-compose -f docker-compose.development.yml -p development run --rm web rake db:create
```

```sh:マイグレート（変更があった場合のみ）
docker-compose -f docker-compose.development.yml -p development run --rm web rake db:migrate
```

```sh:起動
docker-compose -f docker-compose.development.yml -p development up -d
```

これでブラウザで`localhost`にアクセスすると、railsのホーム画面が見られると思います。
ちなみに、停止コマンドは以下です。

```sh:停止
docker-compose -f docker-compose.development.yml -p development stop
```

`docker-compose -f docker-compose.development.yml -p development`は共通なので、お使いのshellでエイリアスに登録してもいいかと思います。
当方の設定を参考に載せておきます。

```zsh:.zshrc
alias dcdev='docker-compose -f docker-compose.development.yml -p development'
# コンテナ全削除
alias drm='docker rm $(docker ps -q -a) -f'
# イメージ全削除
alias dirm='docker rmi $(docker images -q) -f'
```

# ここまででできたこと
![バックエンド構成 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131949/fcdbc2f3-352d-4be2-d8c0-22917d1796fa.png)


次記事:[ECS FargateでRails動かそうとして2ヶ月かかって手元に残ったもの【2.Build編】](https://qiita.com/ShoutaWATANABE/items/7937cc4dcff174ce0284)