Rails on Docker での PostgreSQL & unicorn が動く開発環境のメモ

自己学習のためにざっくり作ってみた

個人的には、本番はUnicornでも開発環境ではPumaでいいんじゃないかな...って思っている

SQLは合わせたほうが良いと思うけど

前準備

experimentalな機能を使うので環境変数を予め設定しておく

.envrc

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

Dockerfile

# syntax = docker/dockerfile:1.0-experimental
################################################################
# node Image
################################################################
# 先にマルチステージビルドでNode.jsとYarnを用意
# そうすることで、rails側のDockerfileの記述がシンプルになる
FROM node:12.18-alpine as node

# zipダウンロードの流派もあるけど、記述がシンプルなので apk add を使用
RUN apk --update add --no-cache yarn

################################################################
# rails Image
################################################################
FROM ruby:2.7.1-alpine

# node image から、Node.jsとYarnをコピー
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /opt/yarn-* /opt/yarn
RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn

ENV RAILS_ENV development
ENV NODE_ENV development

ENV ROOT_PATH /app/
ENV LANG C.UTF-8
ENV PORT 80

WORKDIR $ROOT_PATH

# 必要なパッケージのインストール
RUN apk add --update --no-cache \
      postgresql-client \
      xz-dev \
      tzdata

# ビルドでしか使用しないパッケージは
# 後で削除するため virtual tag をつけている
RUN apk add --update --no-cache --virtual=build-dependencies \
      build-base \
      curl-dev \
      linux-headers \
      libxml2-dev \
      libxslt-dev \
      postgresql-dev \
      ruby-dev \
      yaml-dev \
      zlib-dev

# Gemfile.lock とおなじバージョンの bundler をインストール
RUN gem install bundler -v '2.1.4'

# 全部コピーするとRailsアプリの変更の度にここからビルドし直しになるので
# 必要なGemfileやyarnのファイルを先にコピーする
COPY Gemfile Gemfile.lock package.json yarn.lock $ROOT_PATH

# mount cacheを利用して、gemをキャッシュする
RUN bundle config set path .cache/bundle
RUN --mount=type=cache,target=/app/.cache/bundle \
    bundle install && \
    mkdir -p vendor && \
    cp -ar .cache/bundle vendor/bundle

# ビルドでのみ使用するパッケージの削除
RUN apk del --purge build-dependencies

COPY . /myapp

# 起動
CMD rm -f tmp/pids/unicorn.pid && \
      bundle exec unicorn_rails -E $RAILS_ENV -c config/unicorn.rb -p ${PORT}

Dockerfileの方針

ビルド時間を短くして、無駄な時間を減らす

そのために、

  • 上の方は変更がなるべく無いようにして、イメージレイヤーのビルド数をへらす
  • alpineを利用したり、不要なパッケージを削除して、なるべくimageサイズを小さくする
  • mount cacheを利用して、gemをキャッシュする(2分半ほど処理が短くなった!)

docker-compose.yml

version: '3'
services:
  db:
    image: postgres:12-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: 'postgres'
    # 開発用。直接DBにアクセスしたい時のために
    ports:
      - "5432:5432"
  web:
    build: .
    ports:
      - "3000:80"
    volumes:
      - .:/app
    environment:
      DATABASE_USER: postgres
      DATABASE_PASSWORD: postgres
      DATABASE_HOST: db
    depends_on:
      - db

# databaseのデータはvolumeとして保存
volumes:
  db_data:

docker-compose.yml の方針

あんまり変わったことはしていない

Dockerfile側は、なるべく変数で制御して、環境毎の変数を docker-compose 側に持たせる流派もあるけど、まぁそこまではやらなくても良いのかな...? と思っている

  • 個人的には、Dockerfileはそこまで汎化せずに、環境毎にDockerfile作ってもいいんじゃないかなと思っている
  • だいたい事情で汎用化は破綻するので

以下に、Docker関係以外で初期状態から設定を追加/変更した部分を記す

config/unicorn.rb

# frozen_string_literal: true

APP_DIR = `pwd`.delete("\n")

working_directory APP_DIR

pid "#{APP_DIR}/tmp/pids/unicorn.pid"

worker_processes 2
# backlog は、workerが作業中でも受け取るTCPコネクションの数
# デフォルトの1024だと大量アクセスの時に待ちが大量発生するので減らしている
listen '/tmp/unicorn.sock', backlog: 64
timeout 60

config/unicorn.rb の方針

あんまり特筆すべきことはない。 backlog の数をデフォルトから減らしているところくらいかな...

config/database.yml

default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  username: <%= ENV['DATABASE_USER'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
  port: <%= ENV['DATABASE_PORT'] %>

development:
  <<: *default
  database: development

test:
  <<: *default
  database: test

production:
  <<: *default
  database: production

config/database.yml の方針

こちらも、各設定値を環境変数から取ってくるように変えたくらいかな...

config/environments/development.rb

ログ出力の標準出力化

  # Use an evented file watcher to asynchronously detect changes in source code,
  # routes, locales, etc. This feature depends on the listen gem.
  config.file_watcher = ActiveSupport::EventedFileUpdateChecker

  config.logger = ActiveSupport::Logger.new($stdout)
  # syncを有効にしないと、バッファリングされてログが一定量溜まらないと出力されない
  $stdout.sync=true

参考

👇 buildkit を使ったDocker buildの解説

medium.com

👇 docker-compose で buildkit を使う方法の解説

qiita.com

👇 効率の良いDockerfileの書き方

www.slideshare.net

👇 jokerさんによる、最近のRailsのDockerfileの書き方

speakerdeck.com

👇 効率の良いRailsのDockerfileの書き方

techracho.bpsinc.jp

PumaとUnicornの違い

前提

どちらもアプリケーションサーバ(アプリケーションを動作させるためのサーバ)

アプリケーションサーバーは送られてきたリクエストに対して、rubyやphpなどを実行して、動的な処理をした結果を静的な要素に変換してwebサーバーに返すためのもの。つまり、動的なサイトを動かす上で必要なもののうち、静的ではない部分を作ってくれるもののイメージ。

qiita.com

それぞれ、rackというwebアプリとアプリケーションサーバ間のインターフェイスの仕様&実装に準拠していて、それを使ってRailsと通信を行っている(Railsも、もちろんrack準拠)

Railsアプリの動かし方の違い

  • Unicorn: マルチプロセス
  • Puma: マルチプロセス & マルチスレッド

どちらも、1つのアプリケーションサーバで複数のRailsプロセスを起動して、処理の効率化を図っている

Pumaは更に、プロセスをマルチスレッド化している。これにより、I/O wait が発生した時に、別スレッドの処理を進められるため、マルチプロセスのみより効率が良い

2020/08/07 追記

unicornはマルチプロセスではなく、

thinやmongrelみたいなマルチプロセスによるclusterではなく、forkを使ったmaster-slave

だそうです

master-slave のほうが以下のようなメリットがあるとのこと

  • メモリ効率が良いかも
  • プロセスが死ぬわけではないのでダウンタイムが発生しない
  • 結果的にデプロイ時にダウンタイムが発生しない
  • デプロイが早い

techracho.bpsinc.jp

Unicronの弱点

スロークライアント(リクエストが遅いクライアント) に弱い

スロークライアントが来た場合、workerが待ちのまま止まってしまうという特徴がある。そのためNginxなどリバースプロキシを挟んで、スロークライアントをバッファしてもらう必要がある

これは、Unicornの不具合というより仕様。Unix哲学の「一つのことをうまくやる」に沿って作られているため。スロークライアントのバッファは、リバースプロキシに任せるように作られている

Pumaはスロークライアントが来ても1つのスレッドが埋まるだけなので、スロークライアント対応のためのリバースプロキシは不要

Pumaのほうが優れているの?

そんなこともない

マルチスレッドということは逆に言うとスレッドセーフな実装をする必要があり、当然使用しているgemもスレッドセーフであることを保証しなければならない(たいへん

また、Pumaにすればリバースプロキシがいらないの? というとそんなこともなくて、リクエストのバッファ以外にもいろいろやりたいことが出てきたりするので (healthcheck.htmlでempty_gif返したり)、結局Nginxなどのwebサーバを間に挟むことが多いはず(多分)

あと、Pumaはマルチスレッドだけど、そもそもRubyが純粋なネイティブスレッドではないので、Pumaを本気を出させるには、JRubyとかネイティブスレッドに対応したRuby実装を使ったほうが良いらしいとのこと

t.co

個人的な結論

結局のところ、よっぽど大量のリクエストを高速にさばく必要がなければ、どちらでもいいんじゃないかな...

個人的には、Pumaのマルチスレッドは想定外の所でめんどくさい動きをしそうだから怖いな...

Heroku(Pumaを激推ししている)でRails動かしてる時に、マルチスレッドで困ることあるのかな?

参考

👇 ほぼこの記事の引き写しです

t.co

👇 pumaとunicornとpassengerの比較記事。かなりunicornをディスっている

t.co

Alpine Linux で欲しいファイルを持っているパッケージを探す方法

Alpine Linux で gem install をした時のエラーメッセージから不足しているファイルを見つけて、それを持っているパッケージを探す方法を記します native extension を持っている gem install を失敗する時、こんなエラーメッセージが出ます

例: gem pg インストール時のエラーメッセージ

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /usr/local/bundle/gems/pg-1.2.3/ext
/usr/local/bin/ruby -I /usr/local/lib/ruby/2.7.0 -r
./siteconf20200825-1-1o6uw0p.rb extconf.rb
checking for pg_config... yes
Using config values from /usr/bin/pg_config
checking for libpq-fe.h... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

エラーメッセージをよく読むと...

checking for libpq-fe.h... *** extconf.rb failed ***

libpq-fe.h が無いと言ってます

特定ファイルを持っているパッケージの探し方

pkgs.alpinelinux.org の Contents filter から検索できます

pkgs.alpinelinux.org

f:id:kasei_san:20200825105217p:plain

libpq-fe.h は、postgresql-dev に入っていることがわかりましたね!

Alpine Linuxのパッケージ管理システムapkについて理解を深める

APK is 何?

apk = Alpine Linux package management

Alpine Linuxのパッケージ管理システム

パッケージって何?

Linuxが採用しているアプリケーションの配布形態。アプリケーションによっては、正常に動作させるためには「ライブラリ」と呼ぶ別のプログラムを必要とすることがある。パッケージは、こうした動作に必要な各種プログラムやファイルをまとめたものである

xtech.nikkei.com

Alpine Linux に標準でインストールできるパッケージ

ここで調べられる

pkgs.alpinelinux.org

主なコマンド

apk update

利用可能パッケージのインデックスを更新

  • これを実行しても、インストールされたパッケージは更新されない
  • これから取ってくるパッケージの参照先を最新にする
  • Docker で使う場合、最初の方に書く

apk upgrade

現在インストールされているパッケージをアップグレード

  • 別のパッケージ管理システムの話ではあるが、 Docker ではなるべく使わずに、親 image に upgrade を依頼することを推奨している
親イメージの「必須」パッケージの多くは、権限のないコンテナ内ではアップグレードできないため、apt-get アップグレードや dist-upgrade を実行しないようにしてください。

qiita.com

apk add ${パッケージ名}

パッケージの追加

  • apk add curl gcc のように複数指定も可能

以下で各種オプションの解説をする

--no-cache

パッケージをキャッシュしなくするオプション

通常、ダウンロードされたパッケージは /var/cache/apk にキャッシュされる

DockerでImageサイズを削減するために RUN rm -rf /var/cache/apk とかしなくて良いので便利

--update-cache

apk update を実行するオプション

-U も同義

最初に、apk update を実行してるならば不要

  • 古いweb記事を見ると --update オプションを使用していることがあるが、公式ドキュメントの間違いらしい

qiita.com

--virtual ${名前}

インストールした複数のパッケージと依存関係を一つの仮想パッケージとして 名前 で保存

これを使うことで、--virtual で名前をつけたパッケージをまとめて削除できる

Dockerで bundle install するときだけ使うパッケージ(gccとか)を、これを使ってインストール後にまとめて削除することで、imageサイズを削減できる

apk del

パッケージの削除

--virtual

apk add --virtual で追加した仮想パッケージを削除

--purge

関連ファイルも削除してくれるらしい

参考

👇 公式の解説ドキュメント

wiki.alpinelinux.org

Dockerfileで apt-get upgrade するのは止めたほうが良いとベストプラクティスに書かれていた

Dockerfile best practices によると...

Avoid RUN apt-get upgrade and dist-upgrade, as many of the “essential” packages from the parent images cannot upgrade inside an unprivileged container. If a package contained in the parent image is out-of-date, contact its maintainers. If you know there is a particular package, foo, that needs to be updated, use apt-get install -y foo to update automatically.

docs.docker.com

雑な翻訳

親イメージの「必須」パッケージの多くは、権限のないコンテナ内ではアップグレードできないため、apt-get upgrade や dist-upgrade の実行は避けてください。親イメージに含まれるパッケージが古い場合は、そのメンテナに連絡してください。更新が必要なパッケージ foo があることがわかっている場合は、apt-get install -y foo を使って自動的に更新してください。

親イメージが必須とするパッケージ、foo を更新したい場合、自前で apt-get upgrade せずに、メンテナに連絡した上で apt-get install foo で個別にインストールすべきとのこと

ついでに apt-get upgradeapt-get update の違い

  • apt-get update: パッケージリストの更新
  • apt-get upgrade: インストール済みのパッケージ更新

c5 ユーザはさっさと c5a に乗り換えたほうが良いの?

先に結論

保留で良いと思う

  • 数台しか使ってないなら乗り換えてもたいして得しない (c5.large なら1台につき 95$/年 )
  • 何十台もあるなら乗り換えたほうが良いかもだけど、少しだけCPUパワーが下がることを念頭に置く
  • CPUが AMD系に変わることで、処理速度がおちるかもしれない

2020/08/11 にc5aが東京リージョンに上陸しましたね

aws.amazon.com

c5とc5a 何が違うの?

ざっくり、プロセッサの処理速度が少し落ちて、料金が90%になった

プロセッサの違い

Amazon EC2 C5a インスタンスは 2020 年 6 月にリリースされました。C5a インスタンスは、最大 3.3 GHz の周波数で実行される第 2 世代 AMD EPYC™ 7002 シリーズのプロセッサを搭載し、Amazon EC2 コンピューティング最適化 (C5) ファミリーのインスタンスのバリアントです。

今までのc5が Intel Xeon だったのに対して、AMD EPYC に変わり、CPUパワーが少しだけ下がった

c5.large と c5a.large とを比較すると以下のような感じに

f:id:kasei_san:20200819101831j:plain
c5.large

👆2コアの3.4GHz

f:id:kasei_san:20200819101847j:plain
c5a.large

👆2コアの3.3GHz

AMDとIntelで特性の違いってあるの?

PEXT/PDEP という命令が遅いらしいけど、それが具体的にどんな時に使われるのかはちょっと分からない

AMDの時だけ処理が遅いなーって時は、これを疑うと良いかも

umezawa.dyndns.info

料金の違い

largeのオンデマンド料金で比較した場合、c5aの方が90%ほど安い

  • c5.large が 0.107USD/時間
  • c5a.large が `0.096USD/時間

c5 ユーザはさっさと c5a に乗り換えたほうが良いの?

既存の c5 はそのままで、新しくサーバを立てるときには c5a を使うのが良さそう

理由

  • c5.largeで 95$/年 程度と、大した額得しないので(リサーブドとか、Compute Savings Plans 使ってるともっと低い)
  • 何十台もc5系を使っている会社なら、c5a に更新したほうが良いかもしれない

Linux系のパッケージのバージョン番号が古くても、セキュリティフィックスは入っている場合があるというお話

概要

Linux系のパッケージのバージョン番号が古くても、セキュリティフィックスは入っている場合がある

例えば

Debianのbuster(バージョン10)に入っている apache2 は 2.4.38

本家の apache2 は 2.4.43 で、リリースノートを見ると、色々なセキュリティフィックスが入っている

debianのbusterのapache2は 2.4.38 だから、脆弱性が残っているの?

実はそんなことはなくて、2.4.38 にセキュリティフィックスを入れている

なんでそんなことしてるの?

Debianは安定性を重視しているため

勝手にパッケージのバージョンを上げて互換性を損なわないようにしている

そのため、互換性を維持しながら、現状のバージョンのセキュリティフィックスを入れている(これをバックポートと言う)

参考: 質問: なぜ旧バージョンのパッケージを変更しているのですか?

セキュリティフィックスが入ったかどうかは、どこで確認したら良いの?

security-tracker というBTSのようなもので管理されている

以下のように、https://security-tracker.debian.org/tracker/${CVE-番号} で状況が確認できる

自分の環境のパッケージにセキュリティフィックスが入っているかはどう確認するの?

例えば、上記の CVE-2019-0217 は、 バージョン 2.4.38-3+deb10u3 で修正されたと出ている

deb10u3 は、Debianのbuster(バージョン10)での 3 回目のupdateという意味

dpkg -l コマンドで、パッケージの詳細なバージョンが確認できるので、そこで +deb10u${x} のxが3以上ならば、 CVE-2019-0217 のセキュリティフィックスは適用済と分かる

# dpkg -l apache2
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version          Architecture Description
+++-==============-================-============-=================================
ii  apache2        2.4.38-3+deb10u3 amd64        Apache HTTP Server

自分の環境のバージョンが古い場合どうしたら良いの?

以下のコマンドでパッケージを更新すればOK

apt-get update && apt-get install ${パッケージ名}

参考

Debianで実際にバックポートをしている方の記録

security.sios.com

RHELも同様の仕組みを採用している

access.redhat.com