パイプを使うと最後の処理が成功すると戻り値は0になるので、`pipefail` を使ったほうが事故が少なくなるよというお話

コード例

#!/bin/bash -e

exit 1 | echo "a"
echo "b"

実行結果

a
b

-e オプションで、行単位で失敗したら終了するはずなのに、 echo "b" が実行されてしまっている...!

解説

pipeを使う場合、一番右側の処理の戻り値で行の成功/失敗が判定される

exit 1 | echo "a"
echo $? # <= 0

対策

set -o pipefail を使う

  • pipefail は、パイプの左側の処理で失敗した場合、戻り値を失敗扱いにする処理

コード例

#!/bin/bash -e

set -o pipefail

exit 1 | echo "a"
echo "b"

実行結果

a

パイプの右側も実行されるが、行全体としては失敗扱いとなり、 -e オプションにより処理が終了する

参考

DockerfileのLintツール hadolint でも、以下のようにして RUN 実行時に pipefail が設定されるように推奨している

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

see: https://github.com/hadolint/hadolint/wiki/DL4006

UnicornとNginxの接続方法は、UNIXドメインソケットとリバースプロキシの2つの方法がある

UNIXドメインソケット

単一マシン上の高効率なプロセス間通信に用いられる機能・インターフェースの一種である

UNIXドメインソケット - Wikipedia

  • ファイルシステムを介してプロセス同士の通信を行う仕組み
  • 普通にリバースプロキシするより高速だが、ファイルシステムを介するので単一マシン上でしか動作できない
  • UnicornとNginxが同一マシン上にある場合は、これ一択

Railsの場合の設定方法

config/unicorn-config.rb

# Nginxとはソケット通信
listen "/tmp/unicorn.sock"
  • listen + ソケットのPATH でリスナーを設定する

nginx側の設定

http {
  upstream unicorn {
    server unix:/tmp/unicorn-cms.sock
  }
  location / {
     proxy_pass http://unicorn;
  }
}
  • serverunix:${path} で、unicornで設定したUNIXドメインソケットのpathを設定する
  • proxy_pass でリクエストをUNIXドメインソケットに渡す

リバースプロキシ

普通にTCP通信で、unicornにリクエストを投げる方法

  • UNIXドメインソケットより低速
  • UnicornとNginxが別マシンの場合はこちら

Railsの場合の設定方法

config/unicorn-config.rb

listen 3000
  • listen + ポート番号 でリスナーを設定する

nginx側の設定

http {
  upstream unicorn {
    server exsample.com:3000
  }
  location / {
     proxy_pass http://unicorn;
  }
}
  • serverドメイン:ポート番号 で、Unicornがある転送先を設定
  • proxy_pass でリクエストを転送先に渡す

DockerでUnicornとNginxが別コンテナの場合

別コンテナでもボリューム経由でファイルを共有できるので、UNIXドメインソケットを使える

やっぱりコンテナ間で通信より、早いらしいので、UNIXドメインソケットを使ったほうが良い

qiita.com

参考

qiita.com

www.honeybadger.io

docker-slim で 任意のバージョンの ImageMagickを入れる方法

レガシーなシステムをDocker化するときにたまによくやるのでメモ

多分、Aplineでもおんなじだと思う

方法

コードからmakeする

ARG IMAGE_MAGICK_VERSION=${好きなパージョン}
RUN wget --quiet https://imagemagick.org/download/releases/ImageMagick-$IMAGE_MAGICK_VERSION.tar.xz && \
    tar Jxfv ImageMagick-$IMAGE_MAGICK_VERSION.tar.xz && \
    cd ImageMagick-$IMAGE_MAGICK_VERSION/ && \
    export PKG_CONFIG_PATH="/usr/local/libpng/lib/pkgconfig/:/usr/local/zlib/lib/pkgconfig/" && \
    ./configure --prefix=/usr/local/magick --with-png && \
    make && \
    make install && \
    cd .. && \
    rm -f ImageMagick-$IMAGE_MAGICK_VERSION.tar.xz && \
    rm -rf ImageMagick-$IMAGE_MAGICK_VERSION
ENV PATH /usr/local/magick/bin:$PATH

ポイント

  • configure --prefix でmakeされたバイナリの格納先を指定する
  • make install の後に、アーカイブとディレクトリを削除してimageサイズをへらす
  • PATHは ENV で設定する。 RUN export で設定しても、その RUN の中でしか有効ではない

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