Yokohama.rb Monthly Meetup #81に参加した

2017-06-10(土)のYokohama.rb Monthly Meetup #81参加メモです。

yokohamarb.doorkeeper.jp

3月の#78も参加していたのにメモを書いていなかったので、久しぶり感あります。

Rubyレシピブック

レシピ254から256まで。HTTP, SMTP, FTPというプロトコル3連発でした。

254: HTTPクライアントをつくる

HTTPクライアントを一からつくるというわけではなく、open-uri, net/http, httpclientを使ってみるというレシピでした。rest-clientというクライアントライブラリがあるという話も聞けました。

github.com

あとはlibcurlラッパのtyphoeusとかがありますね。

github.com

255: メールを送信する

net/stmpを使って、素のRubyスクリプトでメールを送るというレシピでした。ふだんはAction Mailerばかりなので新鮮。

今回はメールの受信テストにMailCatcherを使っていましたが、Railsのときはletter_openerを使ったり、Action Mailerにもプレビューがあるという話をしていました。

256: FTPでファイルを送受信する

Ruby, net/ftpというライブラリも標準添付されています。@igrepさんにPure-FTPdというFTPサーバを立ててデモしてもらいました。

バイナリファイル、テキストファイルの送受信がプロトコルとして存在していて、そのまま Net::FTP#getbinaryfile のように対応するメソッドが存在しています。get という名前ですが、返り値はなくローカルファイルを保存するという仕様。

その他

Rubocopを導入するとき、とっかかりのルールはどうしたらよさそうかという話をしたところ、onkcopの設定を参考にしてみるとよさそうというお話を聞いたので、それを眺めたりしていました。

github.com

あとはこのエントリをもくもく書いたり。

blog.kymmt.com



次回は2017-07-08(土)です。

yokohamarb.doorkeeper.jp

Springが動くRails+MySQLなAPIサーバの開発環境をDocker Composeで作る

このあいだ、Rails+MySQLという構成のアプリケーション開発環境をDocker Composeで構築できるようにしました。

blog.kymmt.com

いろいろと理解が深まるにつれて、何点かクリアしたい問題が見えてきました。

  • Dockerfile で使うイメージ
    • ruby:2.4.1-onbuildRUN bundle config frozen 1 しているので、あとから Gemfile を更新して bundle install できない
  • bundle install に時間がかかる
    • Gemfile にgemを追加するたびに、すべてのgemのインストールが走る
  • rails, rake コマンドの立ち上がりが遅い
    • アプリケーションプリローダSpringのサーバが立ち上がっていないため

今回はこれらの問題をクリアして、さらにいい感じの開発環境を作ります。

Dockerfile で使うイメージ

前回作った Dockerfile では ruby:2.4.1-onbuild というイメージを使っていました。このイメージは ONBUILD 命令を使っており、このイメージを使った Dockerfile をビルドすることで ONBUILD に指定されたコマンドが自動で実行されるようになっています。Rubyの場合だと、Gemfile のイメージ内へのコピーや bundle install の実行のような決まりきったコマンドが ONBUILD として指定されているので、Dockerfile の作成を省力化できます。ruby:2.4.1-onbuildDockerfile は次のものです。

しかし、この ruby:2.4.1-onbuild では、RUN bundle config --global frozen 1 というコマンドを実行するようになっています。これは、イメージ内の Gemfile を変更して bundle install できないようにするものです*1

デプロイするような用途であればこれでもよいですが、今回は開発環境がほしいので、Gemfile は必要なときに都度変更してgemをインストールできるようにしたいです。また、onbuild タグがついたイメージは非推奨となっているようでした*2

そこで、ruby:2.4.1-onbuild イメージを使うのはやめて、ruby:2.4.1 イメージをベースに自分でもろもろの作業をやる Dockerfile になるように書き直しました。

FROM ruby:2.4.1

ENV APP_HOME /usr/src/app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

COPY Gemfile \
     Gemfile.lock \
     $APP_HOME/

ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
    BUNDLE_JOBS=4
RUN bundle install

(※後述の[追記]も参照してください)

これで、コンテナ内の Gemfile をあとから編集しても、bundle install が実行できます。

bundle install に時間がかかる

現在、bundle install で入るgemの保存先はコンテナ内になっています。この場合、Gemfile にgemを追加してインストールしたいときはイメージを作り直す必要がありますが、これだとすべてのgemのインストールが走ってしまいます。

これを回避するために、gemの保存先をDocker Volumeに変更します。docker-compose.yml でgem保存用のVolumeを作り、APIサーバを動かすコンテナにマウントします。

version: '3'
services:
  # dbの設定...

  app:
    build: .
    command: bin/rails s -p 3000 -b "0.0.0.0"
    depends_on:
      - db
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    volumes:
      - .:/usr/src/app
      - bundle_cache:/usr/local/bundle
volumes:
  bundle_cache:

bundle_cache がgem保存用のVolumeです。マウント先が /usr/local/bundle になっているのは、ベースイメージとしている ruby:2.4.1GEM_HOME/usr/local/bundle としているからです。

https://github.com/docker-library/ruby/blob/752c5f7cf44870ceae77134b346d20093053c370/2.4/Dockerfile#L63

これで、gemをVolumeへ保存できるようになりました。コンテナが終了したあともVolumeにgemが残ります。Gemfile にgemを追加して bundle install したいときは、次のようにコンテナを起動すればOKです。

$ docker-compose run app --rm bundle install

[2017-06-11 追記]bundle install する場所について

bundle installDockerfile に書くと、イメージのビルド時にインストールされたgemがイメージに入ります。この時点ではgemを保存するVolumeが設定されていないのですが、docker-compose コマンド経由でコンテナを起動すると、Volumeがgem保存先にマウントされます。このVolumeが上述の説明で使っている名前付きボリュームだと、マウント先に存在するファイルはVolumeへコピーされます*3

しかし、コンテナ起動前後でgemの保存の仕組みが変わるのはわかりにくいです。Dockerfile 内に bundle install を書くのではなく docker-compose run --rm app bundle install のように明示的にVolumeへgemをインストールする、という方法のほうがわかりやすそうです。

rails, rake コマンドの立ち上がりが遅い

RailsにはデフォルトでSpringというアプリケーションプリローダが入っています。Springを起動させておくと、rails, rake のようなコマンドの実行を高速化できます。現在、Springのことを考慮していないので、APIサーバとは別にSpring用のコンテナを立ち上げるようにします。

実際の作業内容では、この記事が参考になりました。ほとんどこの記事のとおりやっています。

tech.degica.com

version: '3'
services:
  # dbの設定...

  app: &app_base
    build: .
    command: bin/rails s -p 3000 -b "0.0.0.0"
    depends_on:
      - db
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    volumes:
      - .:/usr/src/app
      - bundle_cache:/usr/local/bundle
  spring:
    <<: *app_base
    command: bin/spring server
    ports: []
volumes:
  bundle_cache:

bin 配下にある railsrake のようなbinstubにSpringの処理を差し込み、叩くコマンドを省略できるようにする機能があるので、それを実行しておきます。

$ docker-compose exec spring bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted

docker-compose upappspring のコンテナを立ち上げたあと、実行中の spring コンテナ内でSpringを通じたコマンドの実行ができます。

$ docker-compose exec spring bin/rails console

[参考]spring コマンド実行時にエラーが出る場合

もし .bundle/config 内で BUNDLE_DISABLE_SHARED_GEMS: 1 という設定がある場合は削除する必要があります。この設定があると、Springを通じたコマンド実行時にうまくgemの場所を解決ができず次のエラーが出てしまいます。

Could not find rake-12.0.0 in any of the sources
Run `bundle install` to install missing gems.

あんまりないと思いますが、以前に bundle install --path vendor/bundle したときに自動で BUNDLE_DISABLE_SHARED_GEMS が設定に入っていてハマりました…

結果

Dockerfile は次のようになりました。

FROM ruby:2.4.1

ENV APP_HOME /usr/src/app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

COPY Gemfile \
     Gemfile.lock \
     $APP_HOME/

ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
    BUNDLE_JOBS=4
RUN bundle install

docker-compose.yml は次のとおりです。

version: '3'
services:
  db:
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
    image: mysql:5.7
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
  app: &app_base
    build: .
    command: bin/rails s -p 3000 -b "0.0.0.0"
    depends_on:
      - db
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    volumes:
      - .:/usr/src/app
      - bundle_cache:/usr/local/bundle
  spring:
    <<: *app_base
    command: bin/spring server
    ports: []
volumes:
  bundle_cache:
  mysql_data:

Rails+MySQLなAPIサーバの開発環境をDocker Composeで作る

先日、"Quickstart: Compose and Rails"の手順にしたがいながら、Docker ComposeでRails+MySQLがとりあえず動くような環境を作りました。

blog.kymmt.com

今回は、RailsのAPIサーバの開発環境をもうちょっといい感じにDocker Composeで作ってみます。DBにはMySQL 5.7を使います。

ディレクトリ構成

コマンド rails new sample_app --api --db=mysql でRailsのプロジェクトツリーを作り、ルートに Dockerfiledocker-compose.yml を置きます。

.
├── Gemfile
├── Gemfile.lock
├── app
│   └── ...
├── config
│   ├── database.yml
│   └── ...
├── db
│   ├── migrate
│   │   └── ...
│   ├── seeds.rb
│   └── ...
├── Dockerfile
├── docker-compose.yml
└── ...

イメージの作成

最終的に立ち上げたいDockerコンテナは次のふたつです。

  • app
    • APIモードのRailsの実行環境
  • db
    • MySQL 5.7の実行環境

このうち、db は既存のMySQL公式イメージを使ってコンテナを立ち上げます。app は次の Dockerfile を書いてイメージを作ります。

FROM ruby:2.4.1-onbuild

ここでは、Ruby 2.4.1の公式イメージ*1、特に ruby:2.4.1-onbuild というイメージを使います。

Rubyの公式イメージは buildpack-deps というイメージをもとにしています。この buildpack-deps はRubyやPythonのライブラリのインストールに必要となりやすいライブラリをあらかじめインストールする便利なイメージです。

MySQLをRubyから利用するために、本来は libmysql-dev というライブラリをインストールする必要があります。しかし、このライブラリは buildpack-deps があらかじめインストールしているので、今回の Dockerfile には明示的に書かないでも大丈夫です。

このあと、本来はホストに存在するRailsアプリケーションのプロジェクトツリーを app イメージ内にコピーする必要がありますが、この処理を Dockerfile に書いていません。これは、app のもととなるイメージに ruby:2.4.1-onbuild を使っているからです。このイメージの Dockerfile はおおむね次のようになっており、処理が ONBUILD で記述されています。ruby:2.4.1-onbuild では Gemfile のコピーやBundlerでの依存gemインストール、プロジェクトツリーのコピーといった定型作業を ONBUILD で指定してあります。ONBUILD を持つイメージをもとに作った新たなイメージからコンテナをビルドしたあとに、ONBUILD で指定した処理が実行されるようになっています。

FROM ruby:2.4

RUN bundle config --global frozen 1

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# このイメージをもとにしたイメージからコンテナをビルドしたあとに実行する
ONBUILD COPY Gemfile /usr/src/app/
ONBUILD COPY Gemfile.lock /usr/src/app/
ONBUILD RUN bundle install
ONBUILD COPY . /usr/src/app

ちなみに、もし buildpack-deps で入るもの以外のライブラリをインストールしたい場合は、今回書いたDockerfile に次のように追記する必要があります。

RUN apt-get update -qq && apt-get install -y \
    build-essential \
    nodejs \
 && rm -rf /var/lib/apt/lists/*

ここでは、Debianの公式パッケージのビルドに必要な build-essential をインストールしています。さらに && でつないでインストールしたいパッケージの名前(たとえば nodejs)を指定します。このあと、さらに rm -rf var/lib/apt/lists/* を実行していますが、これはベストプラクティスとされている方法であり、APTのキャッシュを削除することでイメージのサイズを削減しています。

Composeファイルの作成

appdb をコンテナとして起動するためにComposeファイルを作ります。

version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
    ports:
      - "3306:3306"
  app:
    build: .
    command: bin/rails s -b "0.0.0.0"
    volumes:
      - .:/usr/src/app
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql_data:

ファイルの末尾で volumes を指定し、Docker Engineがサポートする名前付きボリュームとして mysql_data を作成しています。ボリュームというのはコンテナ間で共有できるデータを保存する仕組みです。ボリュームはコンテナとは独立して作成するため、たとえそのボリュームを使う db コンテナが破棄されてもデータはホストに残ります。

今回のComposeファイルでは、名前付きボリュームとして mysql_data を作成し、db の設定にある volumesmysql_datadb/var/lib/mysql ディレクトリにマウントする形で利用しています。

app では depends_on という設定項目に db を指定しています。これによって、依存先のサービスをコンテナとして立ち上げてから、本サービスを立ち上げるようになります。

Railsのデータベース設定

Railsの config/database.yml で、開発/テスト環境のDBを次のとおり設定します。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: root
  password:
  host: db

development:
  <<: *default
  database: sample_app_development

test:
  <<: *default
  database: sample_app_test

docker-compose.ymlapp の依存先として db を指定し、host: db と指定することで、コンテナ db へ接続することができます。

コンテナの立ち上げ

次の要領で appdb の各コンテナを立ち上げます。

  1. APIサーバで使うDBをセットアップ(DBの作成、テーブルの作成、初期データの投入など)する
  2. Docker Composeでコンテナ群を立ち上げる

コンテナ群を立ち上げる前にDBをセットアップしておかないと、APIサーバにリクエストがあったときにエラーが発生します。

次のコマンドでDBをセットアップします。

$ docker-compose run --rm app bin/rails db:setup

このコマンドによって、次のように処理が進みます。

  • app が依存する db のコンテナを先にイメージから立ち上げる
  • app のコンテナを Dockerfile から立ち上げる
  • app のなかのワーキングディレクトリ /usr/src/appbin/rails db:setup を実行し、DBのセットアップを実行する

これでAPIサーバを動かせるようになりました。最後に次のコマンドを叩き、appdb のコンテナを立ち上げると、RailsとMySQLが動きだします。

$ docker-compose up

curl http://localhost:3000/users/1 のようなリクエスト送信で疎通が取れます。また、ホストの Dockerfile を置いているディレクトリをコンテナ内の該当ディレクトリにマウントしているので、ホストでコードを編集すると直にコンテナ内に反映されます。

開発中に Gemfile を更新したときは次のコマンドでイメージをビルドし直します。

$ docker-compose build app

RSpecのテストは次の要領で実行できます。

$ docker-compose run --rm app bin/rspec

*1:Railsの公式イメージは存在しますが、今では非推奨となっています