🦄

gunicornをsystemdで永続化してみる

2023/07/24に公開

FastAPIをsystemdで制御したい

筆者はFastAPIを業務で使用し
実運用時はgunicornでプロセス管理を行います。

ただ、毎度作業するときにgunicornを直接キルしていて
いつかミスしそうな気がしたのでsystemdで動かしたいなあ…って思っていました。

今回は検証環境をサクッと準備して、systemdによる永続化を試してみます。

検証環境

検証環境は以下の通り

サーバ

dockerを使用。

イメージはredhat/ubi8
Red Hat社提供のコンテナイメージです。

https://access.redhat.com/ja/articles/5632841
https://hub.docker.com/r/redhat/ubi8

これをsystemdで動かせる状態にします。

python

dockerイメージにpython実行環境をインストールする必要があります。
やり方は色々ありますが
個人的推しのryeを使うことにしました。

https://github.com/mitsuhiko/rye

環境準備

ディレクトリ構成

docker+rye+systemdを使った場合の構成例です。

.
├── docker # 環境構築用
│   ├── compose.yaml
│   └── Dockerfile
├── service 
│   └── fastapi-gunicorn.service # systemdサービスファイル
├── main.py # サーバアプリケーション
├── pyproject.toml # rye
└── .python-version # rye

以下この構成で話を進めます。

各ファイル詳細

docker

検証環境用のイメージです。

compose.yaml

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

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コマンドはプロセスの状態を確認するために入れました。

ryeRYE_INSTALL_OPTION="--yes"を指定。
応答を全てyesでインストールします。

エイリアスはお好みでどうぞ。

rye

ryeで必要なファイルです。
検証するだけなので予め準備しておきました。

pyproject.toml

  • fastapi
  • uvicorn[standard]
  • gunicorn

が入っています。
構築時点で無くても、後からrye addで追加すれば特に問題ありません。

pyproject.toml
[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!!"}が返ってきます。

main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/hello")
def get_hello_world() -> dict[str, str]:
    return {"hello": "world!!"}

サービスファイル

今回の主役。
systemdで使用するサービスファイルです。

fastapi-gunicorn.service

[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