概要
RailsアプリをECS Fargateにデプロイしようとしたところ、思っていた以上に苦労したので、自分のアウトプット&備忘録を兼ねて執筆しました。ECSのイメージとして、「開発環境で作成したイメージをそのままデプロイすればOK☆\(^o^)/」だったのですが、そう簡単にはいきませんでした。今回は開発環境編です。
環境
- M1mac
- Visual Studio Code
- Docker(20.10.17)
構成
今回、Dockerfileを開発環境と本番環境で分けるか否かでとても悩みました。環境毎で動きが異なる部分があるため、分けた方が正直楽でした。しかし同時に「環境差分も生まれてしまう」こと、「DRYの原則に乗っ取っていない」ことから、Dockerfileは環境統一を図っています。実務ではどうなのでしょうか。。
.(各Railsファイル)
├── config
| └── datebase.yml
| └── unicorn.rb
├── nginx
| ├── Dockerfile
| └── nginx.conf
├── docker-compose.dev.yml
├── (docker-compose.prd.yml)←次回作成
├── Dockerfile
├── entrypoint.sh
├── Gemfile
└── Gemfile.lock
設定手順
1. Rails用Dockerfile&docker-compose.dev.ymlの作成
2. Railsのセットアップ
3. 各設定ファイルの編集
4. Rails用Dockerfile&docker-compose.dev.ymlの編集
5. RailsのWebアプリ作成
1. Rails用Dockerfile&docker-compose.dev.ymlの作成
まずは、アプリのための新しいフォルダと必要なファイルを作成していきます。
ここではデスクトップ配下にフォルダを作成していきます。
cd ~/Desktop
mkdir myapp-miguchi && cd myapp-miguchi
touch Dockerfile docker-compose.dev.yml Gemfile Gemfile.lock
それぞれのファイルの作成が完了したら、順番にファイルの中身を記述していきます。
Dockerfile
FROM --platform=linux/x86_64 ruby:3.1
#環境変数
ENV APP="/myapp-miguchi" \
CONTAINER_ROOT="./"
RUN apt-get update && apt-get install -y \
nodejs \
mariadb-client \
build-essential \
wget \
yarn
WORKDIR $APP
COPY Gemfile Gemfile.lock $CONTAINER_ROOT
RUN bundle install
FROM
でベースとなるイメージを指定するのですが、ここで--platform=linux/x86_64
を記述し、CPUアーキテクチャを指定しています。M1Macではrubyのイメージをインストールする際、--platform=linux/arm64
で作成されます。ローカルでコンテナを動かす際は問題ないのですが、デプロイ先のECSでは対応していないため、ここで指定しています。詳しくは以下の記事をご参照ください。
参考記事:
M1 Mac の基礎知識
1番下と下から2番目の記述では、Gemfileが新しくなった時だけ、bundle installが実行されるようになっています。
Gemfile
source "https://rubygems.org"
gem "rails", "~> 7.0.3"
docker-compose.dev.yml
version: '3'
services:
web:
build: .
tty: true
stdin_open: true
ports:
- '3000:3000'
volumes:
- .:/myapp-miguchi #任意のアプリ名
開発環境ではローカルにあるファイルをコンテナにマウントして使用したかったので、volumesでマウントさせています。コンテナをマウントさせる利点としては、「コマンドを実行時のみファイルを使用するため、コンテナが大きくならない」こと、「コンテナ内での変更をそのままローカルファイルに反映させることができる」ことだと理解しています。
以上、3つのファイルの編集が完了したら、以下のコマンドでコンテナを立ち上げます。
docker-compose -f docker-compose.dev.yml up -d
2. Railsのセットアップ
先ほどdocker-composeで構築したwebコンテナの中に入っていきます。
docker-compose -f docker-compose.dev.yml exec web bash
次に、このWebコンテナに新規のRailsプロジェクトを生成していきます。
rails new . --force --database=mysql --skip-bundle
データベースにはMySQLを使用するため、--database
で指定しています。
bundle installはコンテナ起動時に行ってもらうため、ここではスキップしています。
各Railsアプリのファイルが追加され、Gemfileも更新されます。
Gemfileが更新された後、後ほど設定するunicornのgemを追加しておきます。
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.1.2"
+ gem "unicorn"
~以下省略~
完了したら、一度コンテナから出ます。
exit
3. 各設定ファイルの編集
ここでは3つの種類のファイルを編集していきます。
database.ymlの編集
Nginx用Dockerfile&設定ファイルの作成
Unicorn用設定ファイルの作成
database.ymlの編集
database.ymlは先ほどのrails newでconfigディレクトリ配下に生成されるため、
そちらのファイルを編集していきます。
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= ENV.fetch("DB_USERNAME", "root") %>
password: <%= ENV.fetch("DB_PASSWORD", "password") %>
host: <%= ENV.fetch("DB_HOST", "db") %>
development:
<<: *default
database: myapp_miguchi_development
test:
<<: *default
database: myapp_miguchi_test
production:
<<: *default
database: <%= ENV["DB_DATABASE"] %>
username: <%= ENV["DB_USERNAME"] %>
password: <%= ENV["DB_PASSWORD"] %>
host: <%= ENV['DB_HOST'] %>
defaultの値にはENV.fetch
を使用しています。環境変数に値が入っていない場合は、第2引数の値(username
の場合はroot
)が返されるようになっています。今回は後ほどdocker-composeを再編集する際に、環境変数を記載します。
productionの環境変数は本番環境であるAWSで設定します。(本番環境編)
参考記事:
ENV[]とENV.fetch()の違い【Rails/Ruby】
Nginx用Dockerfile&設定ファイルの作成
Nginxの設定では、Nginx用のDockerfileとnginx.confを作成していきます。
cd ~/Desktop/myapp-miguchi
mkdir nginx && cd nginx
touch Dockerfile nginx.conf
FROM --platform=linux/x86_64 nginx:stable
#デフォルトのnginxファイルを削除して作成したものコンテナ内にコピー
RUN rm -f /etc/nginx/conf.d/*
#自分のapp名.confを記述
COPY nginx.conf /etc/nginx/conf.d/myapp-miguchi.conf
#-c以降の設定ファイルを指定して起動 daemon offでフォアグラウンドで起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
Nginx用DockerfileもRails用同様にCPUアーキテクチャを指定しています。
upstream unicorn {
#ユニコーンソケットの設定
server unix:/myapp-miguchi/tmp/sockets/.unicorn.sock fail_timeout=0;
}
server {
#IPとポートの指定
listen 80 default;
#サーバーネームの指定
server_name localhost;
#アクセスログとエラーログの出力先
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
#ドキュメントルートの指定
root /myapp-miguchi/public;
client_max_body_size 100m;
error_page 404 /404.html;
error_page 505 502 503 504 /500.html;
try_files $uri/index.html $uri @unicorn;
keepalive_timeout 5;
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;
}
}
NginxとUnicornの接続方法はソケット通信を採用しています。そのため最初のupstream unicorn
において、Unicornと接続するファイルの場所を指定しています。
参考記事:
UnicornとNginxの接続方法は、UNIXドメインソケットとリバースプロキシの2つの方法がある
Unicorn用設定ファイルの作成
Unicornの設定では、config配下に設定ファイルを作成していきます。
cd ~/Desktop/myapp-miguchi/config
touch unicorn.rb
# frozen_string_literal: true
#ワーカーの数。後述
$worker = 2
#何秒経過すればワーカーを削除するのかを決める
$timeout = 30
#自分のアプリケーション名。
$app_dir = '/myapp-miguchi'
#リクエストを受け取るポート番号を指定。後述
$listen = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dir
#PIDの管理ファイルディレクトリ
$pid = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
#エラーログを吐き出すファイルのディレクトリ
#$std_log = File.expand_path 'log/unicorn.log', $app_dir
#上記で設定したものが適応されるよう定義
worker_processes $worker
working_directory $app_dir
stderr_path $std_log
stdout_path $std_log
timeout $timeout
listen $listen
pid $pid
#ホットデプロイをするかしないかを設定
preload_app true
#fork前に行うことを定義。後述
before_fork do |server, _worker|
defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect!
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
Process.kill 'QUIT', File.read(old_pid).to_i
rescue Errno::ENOENT, Errno::ESRCH
end
end
end
after_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
Unicornの設定ファイルにおいては、アプリをデプロイしていく中でlogの出力先に悩みました。上記ファイルではエラーログを吐き出すファイルのディレクトリ
の設定をコメントアウトしています。最初ログの出力先を指定していたのですが、本番環境においてログを見ることができませんでした。どうやら標準出力させる必要があるようで、コメントアウトすることでログを出力させることができたため、今はこの設定に落ち着いています。。
参考記事:
DOCKERでUNICORNを動かすのにログを標準出力する
ECS で Amazon CloudWatch Logs にログ出力が出来るようになったのでチュートリアル
4. Rails用Dockerfile&docker-compose.dev.ymlの編集
ここからは、Railsアプリをlocalhostで表示、本番環境へデプロイしていくために、Dockerfileとdocker-composeに記述を加えていきます。
FROM --platform=linux/x86_64 ruby:3.1
#環境変数
ENV APP="/myapp-miguchi" \
CONTAINER_ROOT="./"
#ライブラリのインストール
RUN apt-get update && apt-get install -y \
nodejs \
mariadb-client \
build-essential \
wget \
yarn
#実行するディレクトリの指定
WORKDIR $APP
COPY Gemfile Gemfile.lock $CONTAINER_ROOT
RUN bundle install
#↓懸念点(開発環境ではCOPYをしたくないが、本番環境でする必要がある)
COPY . .
#DB関連の実行
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
#nginxコンテナからrailsコンテナの以下のファイルをマウントすることでソケット通信を可能にする
VOLUME ["/myapp-miguchi/public"]
VOLUME ["/myapp-miguchi/tmp"]
#railsアプリ起動コマンド
CMD ["unicorn", "-p", "3000", "-c", "/myapp-miguchi/config/unicorn.rb", "-E", "$RAILS_ENV"]
環境毎で動きが異なる、COPY
の部分。開発環境ではローカルからファイルをマウントできるため、この記述は必要ありません。しかし、本番環境ではローカルからファイルをマウントすることができないため、コンテナ内へとファイルをコピーする必要があります。docker-compose.dev.ymlで言っていたvolumeの利点を活かせていませんが、苦肉の策です。。
railsアプリ起動コマンドでは、環境を指定する部分を$RAILS_ENV
と環境変数を置くことで、開発環境はdevlopment
、本番環境はproduction
で起動させられるようにしています。
version: '3'
services:
web:
build:
context: .
tty: true
stdin_open: true
ports:
- '3000:3000'
volumes:
- .:/myapp-miguchi #任意のアプリ名
depends_on:
- db
links:
- db
environment:
RAILS_ENV: development
DB_USER: root
DB_PASSWORD: root
DB_HOST: db
db:
platform: linux/x86_64 #M1チップ対応のため追記
restart: always
image: mysql:8
ports:
- 3306:3306
volumes:
- mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
TZ: Asia/Tokyo
nginx:
build:
context: ./nginx
ports:
- 80:80
restart: always #明示的にstopさせるまでリスタートする。(失敗するたび遅延あり)
depends_on:
- web
volumes:
mysql-data:
docker-composeは、主にDB、Nginxコンテナに関する記載と、環境変数environment
などを追記しています。
最後に、pid削除とDB作成を実行する、entrypoint.shを作成していきます。
cd ~/Desktop/myapp-miguchi
touch entrypoint.sh
#!/bin/bash
set -e
#pidの削除&ディレクトリの作成
rm -f tmp/pids/server.pid
mkdir -p tmp/sockets
mkdir -p tmp/pids
#DBコンテナが起動するまで待機する処理
until mysqladmin ping -h $DB_HOST -P 3306 -u root --silent; do
echo "waiting for mysql..."
sleep 3s
done
echo "success to connect mysql"
#DB作成コマンド
bundle exec rails db:create
bundle exec rails db:migrate
bundle exec rails db:migrate:status
bundle exec rails db:seed
#本番環境のみ実行したいが、現状開発環境でも実行されてしまう。
bundle exec rails assets:precompile RAILS_ENV=production
#Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
ここで1番行いたかったことは、DBコンテナが起動するまで待機する処理
です。アプリ作成当初、RailsとMySQLのコンテナを立ち上げ、db:create
を行う際、MySQLに接続できないという事象が発生しました。その事象の発生原因は「DBコンテナは立ち上がっているが、起動に時間がかかる」ことでした。MySQLが起動中で接続できないのに、Railsコンテナ側がdb:create
しに行こうとしていたんですね。そのため、「MySQLに接続できることが確認できたら、db:create
を行う」処理をしています。
assets:precompile
は本番環境のみ必要なのですが、現状開発環境でも実行されてしまいます。特に影響はありませんが、ここも要改善です。
参考記事:
DockerCompose上のRailsでMySQLの起動を待ってdb:migrateする
【Rails】本番環境におけるアセットプリコンパイルの設定
編集が完了したら、localhostでRailsのトップ画面を表示させてみます。
docker-compose -f docker-compose.dev.yml up --build -d
コンテナの構築状況やログはDockerデスクトップで確認できます。
コンテナ構築後、localhostで表示されれば、成功です!
http://localhost:3000/
5. RailsのWebアプリ作成
ここからは、簡単なサンプルアプリを作成し、localhostで確認してみます。
先ほどdocker-composeで再構築した、webコンテナの中に入ります。
docker-compose -f docker-compose.dev.yml exec web bash
アプリ生成コマンドを実行します。
rails g scaffold product name:string price:integer vender:string
生成が完了したら、コンテナから出ます。
exit
docker-composeでコンテナを立て直します。
docker-compose -f docker-compose.dev.yml up --build -d
コンテナの立て直しが完了したら、localhostにアクセスします。
以下の画面が表示されれば、成功です!
http://localhost:3000/products
CRUD処理も正常に行われています。
今回は以上で終了です。
次回はこのアプリをECSにデプロイし、IPアドレスで表示させます。(記事作成中)
参考記事等
Dockerの学習:
非常に分かりやすく、オススメのDocker講座です。
参考記事:
M1 Mac の基礎知識
ENV[]とENV.fetch()の違い【Rails/Ruby】
UnicornとNginxの接続方法は、UNIXドメインソケットとリバースプロキシの2つの方法がある
DOCKERでUNICORNを動かすのにログを標準出力する
ECS で Amazon CloudWatch Logs にログ出力が出来るようになったのでチュートリアル
DockerCompose上のRailsでMySQLの起動を待ってdb:migrateする
【Rails】本番環境におけるアセットプリコンパイルの設定
DockerCompose上のRailsでMySQLの起動を待ってdb:migrateする
【Rails】本番環境におけるアセットプリコンパイルの設定