リモート開発メインのソフトウェア開発企業のエンジニアブログです

Puma を systemd のユーザーサービスとして起動する

概要

Ruby on Rails でのデフォルト web サーバーである Puma を起動する方法は何通りかあるのですが、今回は systemd のユーザーサービスとして起動する方法を紹介します。

systemd は、主にサーバープロセスの管理などに使われ、サービスの登録・起動などには通常は root 権限が必要です。一方、systemd では、一般ユーザーがサービスを管理する方法もあり、それがユーザーサービスです。今回はそれを使って Puma を起動します。

また、設定方法だけで無く、その背景や個人的な意見なども後の方に記載しようと思います。

前提・やりたいこと

  • Linux サーバー(Amazon Linux 2)上で Puma を直接動かす
    • Docker 等は使わない
    • Ruby on Rails 6
    • Puma 5
  • Capistrano を使ってデプロイ
    • capistrano-puma を使用

Linux サーバー上の app というユーザーの権限で Puma を動かし、app ユーザー権限で Puma の再起動などをできるようにします。

Linux、Rails、Capistrano の基本的な知識は持っていることを前提とします。

設定方法

まずは設定方法をざっと説明します。

1. systemd のユーザーサービスを有効化する

最初の方で「systemd では、一般ユーザーがサービスを管理する方法もあり、それがユーザーサービスです」と記載しましたが、実はこの機能は Red Hat Enterprise Linux や派生 OS では、デフォルトでは無効とされているようです。以下、参考 URL です。

systemd – Cannot use systemctl --user due to “Failed to get D-bus connection: permission denied” – Server Fault

そのため、まずはこの機能を有効化します。

今回使用する app というユーザーの uid が 1001 だとすると、/etc/systemd/system/user@1001.service というファイルを以下の内容で作成します。

[Unit]
Description=User Manager for UID %i
After=systemd-user-sessions.service
# These are present in the RHEL8 version of this file except that the unit is Requires, not Wants.
# It's listed as Wants here so that if this file is used in a RHEL7 settings, it will not fail.
# If a user upgrades from RHEL7 to RHEL8, this unit file will continue to work until it's
# deleted the next time they upgrade Tableau Server itself.
After=user-runtime-dir@%i.service
Wants=user-runtime-dir@%i.service

[Service]
LimitNOFILE=infinity
LimitNPROC=infinity
User=%i
PAMName=systemd-user
Type=notify
# PermissionsStartOnly is deprecated and will be removed in future versions of systemd
# This is required for all systemd versions prior to version 231
PermissionsStartOnly=true
ExecStartPre=/bin/loginctl enable-linger %i
ExecStart=-/lib/systemd/systemd --user
Slice=user-%i.slice
KillMode=mixed
Delegate=yes
TasksMax=infinity
Restart=always
RestartSec=15

[Install]
WantedBy=default.target

次に、root ユーザーの権限で以下を実行します。

systemctl daemon-reload
systemctl enable user@1001.service
systemctl start user@1001.service
loginctl enable-linger app

root 権限が必要な作業はここで終わりです。

後は、app ユーザーの .bashrc に以下の行を追記します。

[ -z "${XDG_RUNTIME_DIR}" ] && export XDG_RUNTIME_DIR=/run/user/$(id -ru)

2. puma サービスの登録

app ユーザーのホームディレクトリ配下に .config/systemd/user/puma.service というファイルを以下の内容で作成します。

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=simple
# 以下の3行は環境によって適宜変えて下さい
WorkingDirectory=/home/app/current
Environment=RAILS_ENV=production
Environment=PORT=3000

Environment=USING_WEB_SERVER=true
ExecStart=/bin/bash -lc 'bundle exec puma -C config/puma.rb'
Restart=always

[Install]
WantedBy=default.target

次に、app ユーザーの権限で以下のコマンドを実行します。

systemctl --user daemon-reload
systemctl --user start puma.service
systemctl --user enable puma.service

ここまでで、app ユーザーが Puma の管理を出来るようになりました。

3. Capistrano の設定

Capfile に以下を追記して下さい。

install_plugin Capistrano::Puma::Systemd

set :puma_systemctl_user, :app
set :puma_service_unit_name, :puma

config/puma.rb には以下を追記します。

port        ENV.fetch("PORT") { 3000 }

4. 確認

以下のようなコマンドが正しく動作するか確認して下さい。

cap production deploy
cap production puma:status
cap production puma:restart

背景

設定方法の説明が終わったところで、なぜ、こんな面倒な設定をする必要があるのか、背景について説明します。

Puma 5 から、デーモン化のコードが削除された

以下の issue 及び PR にある通り、Puma 5 でプロセスをデーモン化するコードが削除されました。

以前は、Linux サーバー側では特に設定などはせず、Capfile に以下のように記述しておけば、

# Capfile
install_plugin Capistrano::Puma::Daemon

以下のようなコマンドで Puma の管理が出来ました。

cap production puma:restart

が、Puma 5 からは出来なくなりました。

puma-daemon というのはある

issue のやり取りなどを見る限り、割と唐突に当該機能が削除されたため、その機能を復活させる gem が作られました。

kigster/puma-daemon: Puma v5+ removed daemonization code from the gem itself. This gem will restore this functionality.

ただ、自分の場合(色々焦っていたせいもあるのか) capistrano からうまく使う事が出来なかったので、違う方法でやろうと思いました。”capistrano puma-daemon” などで検索しても情報が少なかったのも1つの理由です。

解説・補足

ここでは、細かい解説・補足をしようと思います。

puma.service で User, Group を指定する方法との違い

/etc/systemd/system/puma.service を作成し、そこで以下のように User, Group を指定することも出来ます。

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/home/app/current
Environment=RAILS_ENV=production
Environment=PORT=3000
ExecStart=bundle exec puma -C /home/app/current/config/puma.rb

Restart=always

[Install]
WantedBy=multi-user.target

この方法と今回説明した方法の違いですが、この方法だと

  • Puma サービスの起動・停止は root が行う
  • Puma のプロセスは app ユーザー、app グループの権限で実行される

のに対し、今回説明した方法だと、起動・停止も app ユーザーが行える点が異なります。

journalctl で、ユーザーサービスのログを見る方法

systemd 関連で問題が発生した場合は、journalctl コマンドでログを見ます。同コマンドは、デフォルトでは(ユーザーサービスではない)システムサービスのログが表示されますが、ユーザーサービスのログを見るためには以下の通り実行します。

journalctl --user --user-unit=puma.service

以下のページが参考になりました。

別の選択肢: sudo

今回の方法は若干面倒なので、もう少し簡単な別の選択肢があります。

app ユーザーをパスワード無しで sudo 出来るようにすれば、Puma を通常の systemd のサービスとして登録して、普通に Capistrano で管理できます。

visudo で以下のような行を追加すれば、app ユーザーはパスワード無しで sudo systemctl が実行できるようになると思います。(試していません。)

app ALL=(ALL) NOPASSWD:ALL /bin/systemctl

この方法の方が簡単ですが、 puma 以外のサービスも触れてしまうのが問題かもしれません。

capistrano-puma の systemd 関連のオプション

今回の方法では、Capfile に以下の2つのオプションを設定しました。

  • :puma_systemctl_user
  • :puma_service_unit_name

どんなオプションがあるのかドキュメントではよく分からなかったので、ソースを見た方が早いと思いました。以下のソースです。

capistrano-puma/systemd.rb at master · seuros/capistrano-puma

トラブルシューティング

Failed at step GROUP spawning

今回紹介したユーザーサービスとして起動する方法の場合、一般ユーザー権限で動作するのですが、 puma.service ファイルに User, Group を指定する必要はありません。というか指定してはいけません。

指定すると、 “Failed at step GROUP spawning” といったエラーが出ます。User, Group を指定すると setuid, setgid という、root 権限が必要なシステムコールが呼ばれるからだと思います。

自分も一度このエラーが出て、以下のページを見て間違いに気づきました。

raspberry pi3 – Systemctl – Failed at step Group spawning – Stack Overflow

Failed to get D-Bus connection: Connection refused

上の手順通りにやればこのエラーは起こらないはずです。このエラーが出た場合は、「1. systemd のユーザーサービスを有効化」の手順を正しく行ったか、再度見直してみて下さい。

参考になったのは以下の投稿です。

systemd – Cannot use systemctl --user due to “Failed to get D-bus connection: permission denied” – Server Fault

感想

削除する前にもう少し議論して欲しかった

削除する理由としては以下が挙げられていました。

It was almost 700 lines that no one maintained and is no longer necessary for us to include in Puma because there are better options available. Daemonization has not been a good application-level concern for a while now..

Remove daemonization · Issue #1983 · puma/puma

要約すると以下の通りです。

  • デーモン化のコードはメンテされていない
  • systemd 等のもっと良い選択肢がある

とは言え、あまり議論もされずに PR がマージされてしまったのは、OSS としてどうかなと思いました。少なくとも、同機能を「deprecated」にしてから、次のバージョンくらいで削除するとかにして欲しかったです。

better options についての説明がいまいち

systemd を使えという理屈は分からなくはないのですが、既存の機能(デーモン化)を削除して新しい機能(systemd)に移行してもらいたいのであれば、そのやり方などが分かりやすく説明してあるのが望ましいです。

Puma には systemd 関連の設定などをまとめた以下のドキュメントがあります。割としっかりした内容ではあります。

puma/systemd.md at master · puma/puma

ただ、Puma のデーモン化機能を使っていた人っていうのは、EC2 などの Linux サーバーで普通に Puma を起動して、デプロイには Capistrano を使うという場合が多いと思います。そうした人は、root 以外のユーザーを使うことが一般的なので、非 root ユーザーの場合どうするのか、という情報が無いと困ってしまいます。

レガシーな環境も考慮して欲しい

そもそも、2021年現在、web アプリはコンテナ化されている環境がかなり多いはずで、デーモン化のコードを使っているシステムというのはそれなりにレガシーな環境だと思います。レガシーな環境というのはそれなりに理由があって、例えば以下のようなものがあります。

  • 近いうちにシステムが使われなくなる
  • 新システムが開発中で、近いうちに現システムが置き換えられる
  • 閉じた環境なのでセキュリティはあまり気にする必要が無く、あまり保守・修正はしていない

話は少し戻りますが、上の方で紹介した「削除する理由」からリンクされているページで、デーモン化がいかに良くないかといった説明がされています。

Don’t Daemonize your Daemons! | Mike Perham

我々はそんなことは百も承知なのです。bad practice は承知の上で、デーモン化のコードを使いたいのです。

それをいきなり

「デーモン化は bad practice だしあまりメンテされてないから機能を削除しといたよ。代わりに systemd とか使ってね。」

とか言われると困ってしまいます。

まとめ

Ruby on Rails + Puma のアプリを Capistrano でデプロイする環境の場合、以前は Puma 自体の機能で Puma をデーモン化して使う事が出来ましたが、Puma 5 で同機能は削除されました。

今回は、それの代わりに Puma を systemd のユーザーサービスとして起動する方法を紹介しました。この方法では(初期設定を除き)root 権限は必要無いので、Capistrano 経由で Puma の起動・停止が出来ます。

2021年現在、新規で Rails アプリを作る場合、実行環境では何らかのコンテナ化技術を使う事がかなり多いと思いますが、レガシーな Rails アプリのを Puma をアップグレードする場合などには、本記事の方法は参考になるかもしれません。

その他参考になったページ

systemd でのサービス管理について説明したページは沢山ありますが、今回使ったユーザーサービスについての説明はかなり少なかったです。その中で、以下のページは日本語で唯一参考になったページです。

サーバー起動時に非rootユーザーでsystemdを使ってサービスを立ち上げる – Qiita

← 前の投稿

Scala のテストライブラリのバージョン互換の件で苦労した話

次の投稿 →

How to setup Scala Play framework on Docker?

コメントを残す