サーバーが立ち上がっていないのにsystemdはactiveになっている話

# はじめに
EC2のスケール時、uvicornが起動していないのにステータスがhealthyになってしまいアクセスが来てエラーが発生しました。
解決までをまとめます。
要はヘルスチェックちゃんとやろうねってことなんですが、調査していて表題部分が腑に落ちなかったのでまとめます。

# 環境
EC2: Amazon Linux 2

構成は超ざっくりこんな感じでした。
![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/855584/febdaf7d-b86e-4158-bfc4-f5e1263b713f.png)

# 状態
・**uvicorn起動時、大きめのキャッシュの読み込みを行っており時間がかかっている**
・EC2は正常に起動していた = healthy
・uvicornの実行はsystemdに登録する形式
・**スケールして起動直後uvicornのsystemdはactiveになっていた(実際には立ち上がっていない)**
```sh
$ sudo systemctl status uvicorn
● uvicorn.service - uvicorn
     Loaded: loaded (/etc/systemd/system/uvicorn.service; enabled; preset: disabled)
     Active: active (running) since Thu 2025-05-22 22:05:38 JST; 1 week 5 days ago
```
・ヘルスチェックで叩くパスを `/` のままにしていたため、FastAPI（uvicorn）がまだ立ち上がっていない時点でも「インスタンスが起動している＝EC2 ステータス OK → ELB 通過」とみなされてしまっている

# 原因
## uvicornのヘルスチェックを行っていなかった
はい。簡単なヘルスチェックAPIをウォームアップ時に叩いて状態管理すれば解決です。
ありがとうございました。

```py
from fastapi import FastAPI, Response, status

app = FastAPI()


@app.get("/health", response_model=dict, status_code=status.HTTP_200_OK)
async def health_check():
    """
    シンプルなヘルスチェック用エンドポイント。
    """
    return {"status": "ok"}
```
...
..
.
いやいや、なんでsystemdがactiveになってるねんということで表題を調査。
（当初ヘルスチェック叩いてないわけないよなと思っていたので時系列的に以下の調査が先）

# 検証
Dockerを用意して環境を再現します。

こんな配置になるようにファイルを準備していきます。
```sh
docker-uvicorn-systemd/
├── Dockerfile
├── app.py
├── uvicorn.service
└── nginx.conf
```

## Docker
ローカルで検証するのでDocker用意。

```dockerfile:Dockerfile
# ベースイメージに Amazon Linux 2 を指定
FROM amazonlinux:2

ENV container docker

RUN yum -y update && \
    yum -y install \
      python3 \
      python3-pip \
      nginx \
      systemd \
      systemd-libs && \
    yum clean all

RUN mkdir -p /opt/myproject
WORKDIR /opt/myproject

COPY app.py /opt/myproject/app.py
RUN pip3 install --no-cache-dir fastapi uvicorn

COPY uvicorn.service /etc/systemd/system/uvicorn.service

COPY nginx.conf /etc/nginx/nginx.conf

RUN systemctl enable nginx
RUN systemctl enable uvicorn

EXPOSE 80 8000

# systemd を PID 1 で起動する
STOPSIGNAL SIGRTMIN+3
CMD ["/usr/sbin/init"]
```

## systemdユニットファイル
ユニットファイルを用意しておきます。
```sh
[Unit]
Description=Uvicorn FastAPI Service
After=network.target

[Service]
# 作業ディレクトリ。ここに app.py がある前提
WorkingDirectory=/opt/myproject

# Uvicorn を systemd から直接起動（バックグラウンド化せず）
ExecStart=/usr/bin/uvicorn app:app --host 0.0.0.0 --port 8000

# 異常終了時に自動再起動させる
Restart=on-failure

[Install]
WantedBy=multi-user.target

```

## nginx
nginxはこんな感じで最低限にします。
```nginx:nginx.conf
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    access_log    /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

    upstream uvicorn_upstream {
        server 127.0.0.1:8000;
    }

    server {
        listen       80;
        server_name  localhost;

        location / {
            proxy_pass         http://uvicorn_upstream;
            proxy_http_version 1.1;
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
            proxy_redirect     off;
        }
    }
}
```

## ヘルスチェックAPI
実行時のキャッシュ読み込みを再現したいのでウェイトを入れておきます。
```py
from fastapi import FastAPI
import time

# 3秒停止
time.sleep(3)
app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello from Uvicorn + FastAPI on systemd + nginx!"}

```

## 実行

1秒起きにヘルスチェックの結果とsystemdの状態を監視するシェルを用意しておいて実行しておきます。

```sh:monitor.sh
HOST="${1:-localhost}"
CONTAINER="${2:-uvicorn_sysd}"

# 無限ループで 1 秒ごとにチェック
while true; do
  TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"

  echo "[$TIMESTAMP] === HTTP チェック (curl http://$HOST/) ==="
  # curl で HTTP ステータスコードだけ取得
  HTTP_CODE="$(curl -s -o /dev/null -w "%{http_code}" http://$HOST/)"
  echo "HTTP status code: $HTTP_CODE"

  echo "[$TIMESTAMP] === Uvicorn サービス状態チェック (container: $CONTAINER) ==="
  # docker exec でコンテナ内の systemctl is-active を実行
  UVICORN_STATUS="$(docker exec "$CONTAINER" systemctl is-active uvicorn.service 2>&1)"
  echo "Uvicorn service status: $UVICORN_STATUS"

  echo "------------------------------------------"
  sleep 1
done
```

```sh
$ chmod +x monitor.sh
$ ./monitor.sh
[2025-06-04 16:04:00] === HTTP チェック (curl http://localhost/) ===
HTTP status code: 500
[2025-06-04 16:04:00] === Uvicorn サービス状態チェック (container: uvicorn_sysd) ===
Uvicorn service status:
------------------------------------------
```


用意したDockerを適当な名前でビルドして起動します。
Dockerでsystemdを使ってアプリを扱う場合は`--privileged`オプションをつけておきましょう。

https://qiita.com/muddydixon/items/d2982ab0846002bf3ea8

```sh
$ cd docker-uvicorn-systemd
$ docker build -t uvicorn-systemd-test .
$ docker run -d --privileged \
    --name uvicorn_sysd \
    -p 80:80 \
    uvicorn-systemd-test
```

## 結果
コンテナを再度実行して監視スクリプトの出力を見てみます。
長いので一部省略。
```tex
$ ./monitor.sh
[2025-06-04 16:04:00] === HTTP チェック (curl http://localhost/) ===
HTTP status code: 000
[2025-06-04 16:04:00] === Uvicorn サービス状態チェック (container: uvicorn_sysd) ===
Uvicorn service status:
Uvicorn service status: Cannot connect to the Docker daemon at unix:///xxxx/xxxxx/.docker/run/docker.sock. Is the docker daemon running?
------------------------------------------
# Docker起動
[2025-06-04 16:20:00] === HTTP チェック (curl http://localhost/) ===
HTTP status code: 502
[2025-06-04 16:20:00] === Uvicorn サービス状態チェック (container: uvicorn_sysd) ===
Uvicorn service status: active
------------------------------------------
[2025-06-04 16:21:00] === HTTP チェック (curl http://localhost/) ===
HTTP status code: 502
[2025-06-04 16:21:00] === Uvicorn サービス状態チェック (container: uvicorn_sysd) ===
Uvicorn service status: active
------------------------------------------
[2025-06-04 16:28:00] === HTTP チェック (curl http://localhost/) ===
HTTP status code: 200
[2025-06-04 16:28:00] === Uvicorn サービス状態チェック (container: uvicorn_sysd) ===
Uvicorn service status: active
```

やはりuvicornが立ち上がる前にactiveになっていますね。

Arch Linuxのドキュメントですが、以下のような記述を見つけました。
> Type=simple (default): systemd considers the service to be started up immediately. The process must not fork. Do not use this type if other services need to be ordered on this service, unless it is socket activated.

https://wiki.archlinux.org/title/Systemd#:~:text=is%20started%20anyway.-,Service%20types,-There%20are%20several

ユニットファイルで設定できる`Type=`オプションはデフォルト`simple`であり、このオプションは即時起動前提のもののため以下が実行（≒完了は問わない）された時点で`active`になると予想できます。
```
ExecStart=/usr/bin/uvicorn app:app --host 0.0.0.0 --port 8000
```

https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#Examples

fastAPIの実行は本来速い方だとは思いますが、今回のような起動に時間がかかるような場合は不適切な確認方法もしくは設定だったのかなと思います。

# まとめ
インスタンスのウォームアップ時はしっかりヘルスチェックをしてからOKとしましょう。