gunicornをsystemdで永続化してみる
FastAPIをsystemdで制御したい
筆者はFastAPIを業務で使用し
実運用時はgunicornでプロセス管理を行います。
ただ、毎度作業するときにgunicornを直接キルしていて
いつかミスしそうな気がしたのでsystemdで動かしたいなあ…って思っていました。
今回は検証環境をサクッと準備して、systemdによる永続化を試してみます。
検証環境
検証環境は以下の通り
サーバ
dockerを使用。
イメージはredhat/ubi8
Red Hat社提供のコンテナイメージです。
これをsystemdで動かせる状態にします。
python
dockerイメージにpython実行環境をインストールする必要があります。
やり方は色々ありますが
個人的推しのrye
を使うことにしました。
環境準備
ディレクトリ構成
docker
+rye
+systemd
を使った場合の構成例です。
.
├── docker # 環境構築用
│ ├── compose.yaml
│ └── Dockerfile
├── service
│ └── fastapi-gunicorn.service # systemdサービスファイル
├── main.py # サーバアプリケーション
├── pyproject.toml # rye
└── .python-version # rye
以下この構成で話を進めます。
各ファイル詳細
docker
検証環境用のイメージです。
compose.yaml
name: gunicorn-sample
services:
app:
build:
context: ../
dockerfile: ./docker/Dockerfile
working_dir: "/usr/local/backend"
ports:
- "8000:8000"
# systemdを使える状態でコンテナを立ち上げる設定
tty: true
privileged: true
command: "/sbin/init"
ports
はFastAPIが起動するポートを任意のポート番号に割り当てます。
tty, privileged, command
がsystemdを使えるようにするために必要な設定です。
Dockerfile
FROM redhat/ubi8:8.8-854
WORKDIR /usr/local/backend
COPY main.py pyproject.toml .python-version ./
COPY service/fastapi-gunicorn.service /etc/systemd/system/
RUN dnf update -y && dnf clean all
# psコマンドインストール
RUN dnf install -y procps && dnf clean all
# ryeをインストール
RUN curl -sSf https://rye-up.com/get | RYE_INSTALL_OPTION="--yes" bash
RUN echo 'source "$HOME/.rye/env"' >> ~/.bashrc
# 操作しやすいようにエイリアス貼っておく
RUN echo 'alias ll="ls -l"' >> ~/.bashrc
RUN echo 'alias la="ls -la"' >> ~/.bashrc
ps
コマンドはプロセスの状態を確認するために入れました。
rye
はRYE_INSTALL_OPTION="--yes"
を指定。
応答を全てyesでインストールします。
エイリアスはお好みでどうぞ。
rye
ryeで必要なファイルです。
検証するだけなので予め準備しておきました。
pyproject.toml
fastapi
uvicorn[standard]
gunicorn
が入っています。
構築時点で無くても、後からrye add
で追加すれば特に問題ありません。
[project]
name = "my_project"
version = "0.1.0"
description = "gunicorn sample"
authors = []
dependencies = [
"fastapi>=0.100.0",
"uvicorn[standard]>=0.23.1",
"gunicorn>=21.2.0",
]
requires-python = ">= 3.8"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.rye]
managed = true
dev-dependencies = []
[tool.hatch.metadata]
allow-direct-references = true
.python-version
今回はデフォルト値を採用
cpython@3.11
サーバアプリケーション
main.py
/hello
にリクエストすると
{"hello": "world!!"}
が返ってきます。
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
def get_hello_world() -> dict[str, str]:
return {"hello": "world!!"}
サービスファイル
今回の主役。
systemdで使用するサービスファイルです。
[Unit]
Description=Gunicorn Sample App
After=network.target
[Service]
User=root
Group=root
WorkingDirectory=/usr/local/backend
ExecStart=/usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
Restart=always
PIDFile=/usr/local/backend/master.pid
[Install]
WantedBy=multi-user.target
User, Group
はdockerのためrootにしています。
ExecStart
にgunicornの起動を指定。
{dir}/.venv/bin/python -m gunicorn
で
仮想環境内のgunicornを起動させます。
-p {pid_file}
でプロセスIDファイルを作成(立ち上げ時にファイルが自動作成される)
そのファイルをPIDFile
に指定しています。
以上で準備完了です。
永続化検証開始
docker起動
dockerを起動します
cd docker
docker compose up -d
起動したらコンテナが立ち上がったか確認
docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# {id} gunicorn-sample-app "/sbin/init" 2 seconds ago Up 1 second 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp gunicorn-sample-app-1
プロジェクト初期化
dockerが起動したらコンテナにアクセス。
docker exec -it gunicorn-sample-app-1 /bin/bash
プロジェクトの初期化を行います
pwd
# /usr/local/backend
ls -la
# -rw-r--r-- 1 root root 13 Jul 21 04:39 .python-version
# -rw-r--r-- 1 root root 136 Jul 21 06:44 main.py
# -rw-r--r-- 1 root root 1530 Jul 21 04:44 pyproject.toml
rye sync
ls -la
# -rw-r--r-- 1 root root 13 Jul 21 04:39 .python-version
# drwxr-xr-x 4 root root 4096 Jul 21 08:52 .venv
# -rw-r--r-- 1 root root 136 Jul 21 06:44 main.py
# -rw-r--r-- 1 root root 1530 Jul 21 04:44 pyproject.toml
# -rw-r--r-- 1 root root 731 Jul 21 08:52 requirements-dev.lock
# -rw-r--r-- 1 root root 520 Jul 21 08:52 requirements.lock
.venv
やロックファイルが出来ていたらOKです。
systemd起動
systemdでサービスを起動させます。
systemctl daemon-reload
# 適用されているか確認する
systemctl status fastapi-gunicorn.service
# ● fastapi-gunicorn.service - Gunicorn Sample App
# Loaded: loaded (/etc/systemd/system/fastapi-gunicorn.service; disabled; vendor preset: disabled)
# Active: inactive (dead)
# 自動起動
systemctl enable fastapi-gunicorn.service
# Created symlink /etc/systemd/system/multi-user.target.wants/fastapi-gunicorn.service → /etc/systemd/system/fastapi-gunicorn.service.
# gunicornのプロセスがまだ起動していないことを確認
ps aux | grep gunicorn | grep -v grep
# サービス起動
systemctl start fastapi-gunicorn.service
# gunicornのプロセスを確認できる
ps aux | grep gunicorn | grep -v grep
# root 141 12.5 0.5 63608 31044 ? Ss 09:00 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 142 169 0.8 1127876 48644 ? R 09:00 0:01 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 143 164 0.7 1127184 47984 ? R 09:00 0:01 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 144 155 0.7 1127184 47992 ? R 09:00 0:01 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 145 146 0.7 1126168 44880 ? R 09:00 0:01 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# リクエストを投げてみる
curl http://localhost:8000/hello
{"hello":"world!!"}
これでOKです。
検証
systemdでgunicornを立ち上げたことで色々出来るようになりました。
起動・停止・再起動
systemdで立ち上げたのでsystemctl
コマンドで一連の動作が可能です。
# サービスを停止
systemctl stop fastapi-gunicorn
# サービスを起動
systemctl start fastapi-gunicorn
# サービスを再起動
systemctl restart fastapi-gunicorn
# 設定更新(gunicornの設定をファイルで管理している場合有効)
systemctl reload fastapi-gunicorn
プロセスキル検知
最もやりたかったことです。
メインプロセスをmaster.pid
ファイルで管理しています。
そのため、プロセスがキルされても、systemd側で検知し自動起動します。
# 今の状態を確認
ps aux | grep gunicorn | grep -v grep
#root 149 0.9 0.3 62240 29768 ? Ss 00:08 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
#root 150 5.5 0.6 1130072 54932 ? S 00:08 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
#root 151 5.2 0.6 1130072 54948 ? S 00:08 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
#root 152 5.7 0.6 1130080 54940 ? S 00:08 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
#root 153 5.8 0.6 1130080 54924 ? S 00:08 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# 現在のメインプロセス
cat master.pid
# 149
# プロセスキル
kill `cat master.pid`
# 再度確認
cat master.pid
# 161
# メインプロセスが変わっている
# 自動で復帰されていることが確認できる
ps aux | grep gunicorn | grep -v grep
# root 161 2.3 0.3 62240 29732 ? Ss 00:10 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 162 10.6 0.6 1130068 55056 ? S 00:10 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 163 11.3 0.6 1130068 55072 ? S 00:10 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 164 13.4 0.6 1130076 55064 ? S 00:10 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
# root 165 13.0 0.6 1130076 55048 ? S 00:10 0:00 /usr/local/backend/.venv/bin/python -m gunicorn main:app -p /usr/local/backend/master.pid -w 4 -k uvicorn.workers.UvicornWorker
これで、gunicornを安全に操作できるようになりました。
Discussion