32
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-08-22

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

やりたいこと

バックエンド構成.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

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
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
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

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

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

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

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;
  }
}
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. ソケットファイル

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. タイムアウト

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

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

mysql

docker/mysql/charset.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
[client]
default-character-set=utf8mb4
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のようなモジュールの位置付けです。
単体でもサーバとして使えるらしいです。

src/Gemfile
# 追記
gem "unicorn"
bundle install

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

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

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'] %>

Dockerfilepassword.ymlで設定してきた環境変数がここで読み込まれていきます。
developmentのhostで指定しているdbというのはdocker-compose.ymlでデータベースに設定しているサービス名です。
別の名前に設定している場合はそちらの名前にしてください。

ビルドと起動

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

ビルド
docker-compose -f docker-compose.development.yml -p development build
データベース作成(初回のみ)
docker-compose -f docker-compose.development.yml -p development run --rm web rake db:create
マイグレート(変更があった場合のみ)
docker-compose -f docker-compose.development.yml -p development run --rm web rake db:migrate
起動
docker-compose -f docker-compose.development.yml -p development up -d

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

停止
docker-compose -f docker-compose.development.yml -p development stop

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

.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

次記事:ECS FargateでRails動かそうとして2ヶ月かかって手元に残ったもの【2.Build編】

32
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?