2-4. systemd - サービスの起動・常駐・自動復旧

所要時間: 50-70分(がっつりなら2-3セッション分) ゴール: systemd unit ファイルを自分で書き、Go サーバーを常駐化して自動再起動させる コミット内容: 自作 unit ファイルと検証ログを ~/learn/linux/day204/ に保存

この章が終わるとできること

  • systemctl status / start / stop / restart / reload / enable使い分け できる
  • .service ユニットの [Unit] [Service] [Install] セクションを自分で書ける
  • Type=simple / forking / oneshot の違いを説明できる
  • journalctl -u <unit> -f でリアルタイムにログを追える
  • 専用ユーザー + Restart=on-failure自分のサーバーを本番品質で常駐化 できる

Day 6 / Day 9 とのつながり

  • Day 6 で見た nohup ./myapp &本番版 が今日扱う systemd
  • Day 9 で書いた set -euo pipefail のスクリプトを ExecStart= から呼ぶ のが今日のパターン
  • Day 8 で出てきた「cron に環境変数が無い」と同じ罠が systemd にもある(EnvironmentFile= で解決)

これができると何が嬉しいか

  • SSH 切ったら止まる」状態から卒業 ─ サーバーが本物の「サービス」として動く
  • 落ちても自動復旧、OS再起動でも復活、ログは journald に集中
  • Day 21(デプロイ)の最終形 ─ systemd で常駐 + Nginx で公開 ─ が見えてくる

大前提: なぜ systemd を学ぶか

「Go や Node でサーバープログラムを書いた。ローカルでは動く。本番のサーバーで ./myapp & で起動した。SSHを切ったらプロセスが死んだ。」

新人バックエンドエンジニアが必ず通る道です。これが意味するのは:

  • 長期稼働させるプロセスは、ユーザーのログインセッションから切り離さねばならない
  • クラッシュしたら自動で再起動してほしい
  • OS 起動時に自動で立ち上がってほしい
  • ログを適切な場所に出力し、ローテーションさせたい
  • root で動かさず、専用ユーザーで動かしたい

これらを 自前で頑張ろうとすると死ぬほど面倒nohup で切り離し、PID ファイルで管理、シェルスクリプトでヘルスチェック、cron で再起動… 全部やったとしても本番品質には届かない。

そこに登場したのが systemd です。Linux サーバーで動くほぼ全てのサービスは systemd で管理されている。Docker も Kubernetes ノードも nginx も PostgreSQL も。バックエンドエンジニアが systemd を読み書きできないと、本番デプロイで詰む


セッション①: systemd の世界観(25-30分)

0. 録画スタート(Linux サーバー想定。macOS には systemd は無いので Lima や VPS を使う)

mkdir -p ~/log ~/learn/linux/day204
cd ~/learn/linux/day204
script ~/log/linux_day204.log
 
# 動作環境の確認
systemctl --version  # macOS では入っていない

macOS には systemd は無い

macOS は launchd を使う。本レッスンは Linux サーバー(Ubuntu/Debian/CentOS)が前提。

学習用には以下を推奨:

  • Lima (brew install lima): macOS 上に軽量 Linux VM
  • Multipass: Canonical 製、Ubuntu VM が一発
  • EC2 / Vultr / Hetzner: 月数百円の VPS(実本番に近い)
  • WSL2(Windows): systemd 公式サポートあり

以降のコマンドは Ubuntu 22.04 を想定。

1. init.d から systemd への移行:なぜ起きたか

systemd 登場の歴史的背景

Linux のサービス起動は長らく SysV init/etc/init.d/ の起動スクリプト方式)で行われていた。問題:

  1. 完全に逐次起動: スクリプトを上から順に実行するだけ。10秒以上の起動待ち
  2. 依存解決が貧弱: 「DB が起動してからアプリ起動」のような依存を書きづらい
  3. シェルスクリプトの集合体: 何百行ものシェルで脆い、デバッグ困難
  4. プロセス監視が無い: 起動はできるが、死んだら検知できない
  5. ログがバラバラ: 各サービスが自前でログを書く → 場所も形式もバラバラ

systemd は2010年に登場し、これら全てを解決:

  • 並列起動: 依存関係から逆算してDAGで起動 → 起動時間が劇的短縮
  • 宣言的な unit ファイル: [Service]\nExecStart=... のような ini 形式
  • プロセス監視と自動再起動: Restart=always
  • journald による統合ログ: 全サービスのログを構造化して一元管理
  • cgroups でリソース制限: メモリ・CPU 上限を unit で指定可能

一方で批判もある:「機能を抱え込みすぎ(init/ログ/ネットワーク/DNS)」「Unix 哲学に反する」など。が、現状の Linux サーバーで避けて通れない。

2. systemctl の基本操作

systemctlsystemd を操作するための司令塔コマンド。サービスの起動・停止・状態確認・自動起動の有効化など、Linux サーバーの日常運用で最も叩くコマンドのひとつ。本番調査では「まず systemctl status で動いてるか確認」が定石。

# 起動しているサービスを見る
systemctl list-units --type=service --state=running
 
# 全てのサービスunit
systemctl list-unit-files --type=service
 
# 状態確認
systemctl status nginx
systemctl status sshd
 
# 起動・停止・再起動・リロード
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx   # 設定だけ再読込(プロセス維持)
 
# OS 起動時の自動起動
sudo systemctl enable nginx
sudo systemctl disable nginx
 
# 有効化と起動を同時に
sudo systemctl enable --now nginx

ざっくり言うと(enable vs start)

start = 今すぐ起動する(再起動したら消える) enable = 次の OS 起動時に自動で立ち上がるよう登録する(今は起動しない)

両方やりたいので、実務では enable --now をセットで使う。

まず普通のパターン

サーバーに nginx をインストールした直後:

nginx パッケージがインストールされた
  ↓
プロセスは起動していない
  ↓
OS 起動時の自動起動も無効

start だけ打った時

sudo systemctl start nginx
今すぐ nginx が起動 → サービス開始
  ↓
(その日は問題なく動く)
  ↓
夜にサーバー再起動
  ↓
OS 起動時に nginx は自動起動しない
  ↓
朝、サービスダウン状態で目覚める ← 事故

enable だけ打った時

sudo systemctl enable nginx
OS 起動時の自動起動が有効化された
  ↓
(でも今は起動していない)
  ↓
「あれ、サービス動いてない?」 ← 事故

enable --now を打った時(実務の正解)

sudo systemctl enable --now nginx
今すぐ起動 + OS 起動時の自動起動を登録
  ↓
今動いてる、再起動後も動く ← 完璧

対比表

操作今すぐ動くか再起動後に動くか
start のみ✕(消える)
enable のみ✕(今は動かない)
enable --now
stop止める設定は維持
disable止めない自動起動を解除

「サーバー再起動したらサービス上がらない」の正体

ほぼ100% enable し忘れ

# 確認方法
systemctl is-enabled nginx   # enabled / disabled / static
systemctl is-active nginx    # active / inactive

一番覚えやすい説明

  • start = 今動かす
  • enable = 次回 OS 起動時も動かす
  • 実務は両方やりたいので enable --now が定型
  • 「サービス止めて自動起動も解除」なら disable --now

ざっくり言うと

restartプロセスを殺してから新しく起動reloadプロセスを生かしたまま設定だけ読み直させる。 nginx で restart を打つと現在の接続が全切断されてエラー画面、reload なら無傷で反映される。

まず普通のパターン: nginx を運用中

nginx プロセス (PID 1234)
  ├─ ワーカー (1235) ← ユーザーA がダウンロード中
  ├─ ワーカー (1236) ← ユーザーB が動画視聴中
  └─ ワーカー (1237) ← ユーザーC が API リクエスト中

設定ファイル /etc/nginx/nginx.conf を変更したい。

restart を打った時のフロー

systemctl restart nginx
systemctl が nginx に SIGTERM を送る
  ↓
nginx (1234) と全ワーカーが終了
  ↓
ユーザー A,B,C の接続が全部切れる → エラー画面
  ↓
systemd が新しい nginx を起動
  ↓
新しい PID で待ち受け開始

イメージ: restart = 店を一旦閉めて開け直す。中の客は全員追い出される。

reload を打った時のフロー

systemctl reload nginx
systemctl が nginx に SIGHUP を送る
  ↓
nginx (1234) はそのまま生きている
  ↓
nginx が「設定ファイルを読み直せ」と理解
  ↓
新しい設定で新ワーカーを起動
  ↓
既存ワーカー (1235,1236,1237) は接続が終わるまで生かす
  ↓
新規接続は新ワーカーへ
  ↓
ユーザー A,B,C は何も気づかない → 無停止

イメージ: reload = 店を開けたまま、新メニューを配る。既存の客は古いメニューのまま、新規客は新メニュー。

対比表

restartreload
プロセス殺して再起動生かしたまま
シグナルSIGTERM → 起動SIGHUP(多くの場合)
既存接続全切断保護
ダウンタイムあり(数秒)なし
反映できる範囲設定もバイナリも全部設定のみ(バイナリ更新は不可)
使う時バイナリアップデート後、状態リセット設定変更だけ

「nginx の設定変えただけなのに障害」の正体

# NG: 本番でこれをやって接続全断
sudo systemctl restart nginx
 
# OK: 検証 → reload で無停止反映
sudo nginx -t                  # 設定の文法チェック
sudo systemctl reload nginx    # 既存接続を守りながら反映

reload サポートの確認

すべてのサービスが reload に対応しているわけではない。確認方法:

systemctl show nginx -p CanReload
# CanReload=yes  ← 対応してる

unit ファイルに ExecReload= が書かれているかで決まる。

一番覚えやすい説明

  • restart = 店ごと閉めて開け直す(客は追い出す)
  • reload = 店を開けたまま設定を差し替える(客は気づかない)
  • nginx/apache/postgres の設定変更は基本 reload
  • バイナリ更新やプロセス状態を綺麗にしたい時だけ restart

3. unit ファイルの種類

systemd は「サービス」だけでなく タイマー・ソケット・マウント・パス監視 など複数の概念を統一的な「unit」ファイル形式で扱います。種類を理解しないと「やりたいこと」と「使うべき unit」が結びつきません。

  • 何のファイルか: systemd が管理する対象を宣言的に記述する設定ファイル(.service, .timer, .socket, .mount, .path, .target, .slice など)
  • いつ意識するか: 新しいサービスを常駐化する時(.service)、cron 代替で定期実行する時(.timer)、複数 unit のグループ化(.target)など、用途に応じて拡張子を選ぶ
  • 解決する具体的な問題: 「init.d スクリプトの自前実装」「cron と systemd timer の使い分け」「複数サービスの起動順序制御」など、Linux のサービス管理を統一的なメンタルモデルで扱えるようになります
# unit ファイルの場所
ls /lib/systemd/system/    # ディストリ標準
ls /etc/systemd/system/    # ユーザー定義(こっちを編集する)
 
# 既存 unit を見る
cat /lib/systemd/system/ssh.service

unit の種類と用途

拡張子種類用途
.serviceservice unit常駐プロセスを管理(最頻出)
.socketsocket unitポートやソケットを監視し、接続時にサービス起動(systemd の socket activation)
.timertimer unitcron の代替。定期実行
.mountmount unitファイルシステムのマウント
.pathpath unitファイルやディレクトリの変更を監視
.targettarget unit複数 unit のグループ化(multi-user.target など)
.sliceslice unitcgroups リソース制限

覚えるべき本質:systemd は「init システム」ではなく 「ホスト上の状態を宣言的に管理するシステム」。だからマウントやタイマーまで unit で表現できる。

4. unit ファイル探検

unit ファイルを 「ゼロから書く」前に「既存の良い実例を読む」 のが学習の近道です。nginx や postgresql の unit はディストリビューションメンテナが磨いた教材レベル。

  • 何のコマンドか: 既存の unit ファイルを cat で表示、systemctl edit で安全に上書き(drop-in)
  • いつ使うか: 自作 unit を書く前のテンプレート探し、既存サービスの設定を一部だけ変えたい時(メモリ上限追加、環境変数追加など)
  • 解決する具体的な問題: 「/lib/systemd/system/ を直接編集するとパッケージ更新で上書きされる」事故を、drop-in(/etc/systemd/system/<unit>.d/override.conf)で防ぎつつ、必要な差分だけを管理できるようになります
# nginx の unit ファイルを読む(よく書けている教材)
cat /lib/systemd/system/nginx.service
 
# 上書きや拡張は drop-in で行う
sudo systemctl edit nginx
# ↑ /etc/systemd/system/nginx.service.d/override.conf を作って差分を書ける

unit ファイルは2箇所にある(優先順位)

  1. /etc/systemd/system/ユーザー定義 が最優先
  2. /run/systemd/system/ ← ランタイム生成
  3. /lib/systemd/system/ ← パッケージ提供のデフォルト

絶対に守る: /lib/systemd/system/ のファイルは直接編集しない。パッケージアップデートで上書きされる。変更したいなら:

  • 完全に上書きしたい → /etc/systemd/system/ にコピーして編集
  • 一部だけ変えたい → systemctl edit <unit> で drop-in を作る

セッション②: service unit を自分で書く(25-30分)

5. 最小の service unit

実際に手を動かして「自作 Go サーバーを systemd で常駐化」を体験します。バックエンドエンジニアが本番で必ず通る道 を、最小構成で一気通貫に体験するのが目的です。

  • 何を作るか: 小さな HTTP サーバー(Go)を /opt/myapp/ に配置し、専用ユーザー myapp で動かす準備
  • いつやるか: 新しいアプリを本番サーバーに乗せる初回構築、Docker を使わない素朴なデプロイ、VPS への直接配置
  • 解決する具体的な問題: 「./myapp & で動かして SSH 切ったらプロセス死亡」「root でアプリを動かしていて脆弱性で乗っ取られる」を、専用ユーザー + systemd 管理で予防します
# まず Go の小さなサーバーを用意(システムへ /opt/myapp に配置すると想定)
cat > /tmp/hello-server.go <<'EOF'
package main
 
import (
    "fmt"
    "log"
    "net/http"
)
 
func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello from myapp\n")
        log.Printf("request: %s %s", r.Method, r.URL.Path)
    })
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
EOF
 
# ビルド
cd /tmp && go build -o myapp hello-server.go
 
# 配置場所と専用ユーザー
sudo mkdir -p /opt/myapp
sudo mv /tmp/myapp /opt/myapp/myapp
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /opt/myapp

専用ユーザーで動かす理由

なぜ myapp ユーザーを別途作るか:

  • 権限最小化: アプリが乗っ取られても、myapp 権限でできることしかできない
  • 監査ログ: 「誰が」を myapp で記録できる
  • クリーンアップ容易: userdel myapp で関連状態を一掃
  • 複数アプリの分離: アプリAがアプリBのファイルを触れない

--system フラグ:UID < 1000 のシステムユーザーとして作る(通常ユーザーと区別)。 --shell /usr/sbin/nologin:そのユーザーでログイン不可能にする(SSH 経由でも入れない)。

6. service unit ファイル

いよいよ unit ファイル本体を書きます。[Unit] [Service] [Install]3セクションに分けて、宣言的に サービスの動作を記述します。

  • 何を書くか: 依存関係 (After=)、実行コマンド (ExecStart=)、ユーザー (User=)、再起動ポリシー (Restart=)、セキュリティ強化 (NoNewPrivileges= 等)
  • いつ書くか: 新しい常駐サービスをデプロイする時。最初は最小構成で動かしてから、本番品質に段階的に厚くしていく
  • 解決する具体的な問題: 「プロセス監視・自動再起動・ログ統合・起動時間順序・リソース制限」をひとつの宣言的ファイルで実現。シェルスクリプトで自作すると数百行になる init 機能を、ini 形式で完結に書ける
# /etc/systemd/system/myapp.service として配置
[Unit]
Description=My Application HTTP Server
Documentation=https://example.com/myapp/docs
After=network-online.target
Wants=network-online.target
 
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp
EnvironmentFile=-/etc/myapp/env
Restart=on-failure
RestartSec=5s
 
# セキュリティ強化
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
 
# リソース制限
LimitNOFILE=65535
 
[Install]
WantedBy=multi-user.target

[Unit] セクション:依存関係とメタ情報

キー意味
Description=人間向けの説明
Documentation=URL や man ページへの参照
After=この unit より 後に 起動する(順序)
Before=この unit より 前に 起動する
Wants=弱い依存(失敗してもこの unit は起動)
Requires=強い依存(失敗するとこの unit も停止)

After=Requires= の違いAfter=順序のみRequires=依存 を表す。両方書く必要があることが多い。

例: DB に依存するアプリ

Requires=postgresql.service
After=postgresql.service

[Service] セクション:プロセスの起動方法

キー意味
Type=プロセスの種類(後述)
User= / Group=実行ユーザー(root を避ける)
WorkingDirectory=cd される作業ディレクトリ
ExecStart=起動コマンド(絶対パス必須)
ExecStop=停止コマンド(省略時は SIGTERM)
ExecReload=reload コマンド
EnvironmentFile=環境変数を読み込むファイル(- 接頭で「無くてもエラー無視」)
Environment=環境変数を直接指定
Restart=再起動ポリシー(後述)
RestartSec=再起動までの待ち時間

Type= の使い分け

Type挙動使う場面
simpleExecStart で起動したプロセスを そのまま メインプロセスとして扱う。fork しないGo / Node / Python の素直なサーバー(最頻出)
forkingプロセスが fork して親が終了する伝統的デーモン形式古い C 製デーモン(apache2 など)
oneshot起動して即終了するスクリプト。1回限りバックアップスクリプト、初期化処理
notifyプロセスが「準備完了」を systemd に通知(sd_notify)するまで起動完了とみなさない起動に時間がかかる重いサービス(systemd-aware)
dbusD-Bus 経由で起動完了通知D-Bus デーモン

モダンな Go/Node/Python アプリは Type=simple を選ぶ。fork する必要は無い。

ざっくり言うと(Restart=on-failure vs always)

on-failure = 異常終了の時だけ再起動(プロが選ぶ) always = どんな理由でも再起動(怖い)

always を選ぶと「systemctl stop しても勝手に蘇る」事故が起きる。

まず普通のパターン

myapp サービスが動いている。

myapp (PID 1234) 動作中

on-failure の挙動

ケース1: アプリがバグでクラッシュ (exit 1)
  ↓
on-failure → 再起動する ◎

ケース2: kill -9 で強制終了
  ↓
on-failure → 再起動する ◎(シグナルkillも異常扱い)

ケース3: systemctl stop myapp で正常停止
  ↓
on-failure → 再起動しない ◎(正常終了なので)

ケース4: アプリが exit 0 で自然終了
  ↓
on-failure → 再起動しない ◎

= 「異常な時だけ立て直してくれる、人間の意思は尊重する」 賢い守護神。

always の挙動

ケース1: アプリがバグでクラッシュ (exit 1)
  ↓
always → 再起動する ◎

ケース2: kill -9 で強制終了
  ↓
always → 再起動する ◎

ケース3: systemctl stop myapp で正常停止
  ↓
always → 再起動する ← !!!
  ↓
止めたつもりが復活
  ↓
何度 stop しても蘇るゾンビ
  ↓
人間が「なんで止まらないんだ!」とパニック

= 「何があっても起動し続ける」 暴走する番犬。

対比表

noon-failurealways
異常終了(exit≠0)
シグナル kill
systemctl stop✕(止まる)○(蘇る)
正常終了 (exit 0)
実務で選ぶか✕(罠)

一番覚えやすい説明

  • on-failure = 異常時のみ復活(人間の停止は尊重)
  • always = どんな時も復活(人間の停止すら無視するゾンビ)
  • 実務はほぼ on-failure + RestartSec=5s
  • always は「絶対に止めてはいけないクリティカルなプロセス」+ StartLimitBurst 必須

セキュリティ強化オプション(必須レベル)

キー効果
NoNewPrivileges=trueサブプロセスが setuid で権限昇格できない
PrivateTmp=trueプロセス専用の /tmp を割り当て(他プロセスから見えない)
ProtectSystem=full/usr /boot /etc を読み取り専用に
ProtectHome=true/home を見えなくする
ReadWritePaths=/var/lib/myapp特定パスのみ書き込み許可
CapabilityBoundingSet=Linux capability を絞る

これらを書かないアプリは「侵入されたらサーバー丸ごと持ってかれる」と考えてよい。

7. unit を登録して動かす

書いた unit ファイルを systemd に認識させて起動する 一連の手順です。daemon-reload を忘れると「設定変えたのに反映されない」事故の原因 No.1 になります。

  • 何のコマンドか: daemon-reload で systemd に新規 unit を読み込ませ、enable --now で起動と自動起動有効化を同時実行
  • いつ実行するか: unit ファイルを新規作成・編集した直後。systemctl edit 経由なら daemon-reload は自動だが、ファイルを直接編集した時は必ず手動実行
  • 解決する具体的な問題: 「unit を編集したのに restart しても古い設定のまま」「環境変数ファイルを書いたのに読まれない」など、systemd の設定キャッシュに起因するハマりを予防
# unit 配置
sudo nano /etc/systemd/system/myapp.service  # 上の内容を保存
 
# 環境変数ファイル(オプション)
sudo mkdir -p /etc/myapp
sudo tee /etc/myapp/env > /dev/null <<'EOF'
LOG_LEVEL=info
PORT=8080
EOF
sudo chmod 640 /etc/myapp/env
sudo chown root:myapp /etc/myapp/env
 
# systemd に新しい unit を認識させる
sudo systemctl daemon-reload
 
# 起動
sudo systemctl start myapp
sudo systemctl status myapp
 
# 動作確認
curl http://localhost:8080/
 
# OS 起動時の自動起動を有効化
sudo systemctl enable myapp

unit を編集したら daemon-reload を忘れずに

/etc/systemd/system/myapp.service を編集しても、systemctl daemon-reload しないと systemd は変更を読み込まない。「設定変えたのに反映されない」事故の原因No.1。

8. journalctl でログを追う

journalctl は systemd が管理する 全サービスのログを横断検索 できる最強のログビューア。本番障害対応の8割はこのコマンドから始まります。

  • 何のコマンドか: journald に蓄積された構造化ログを「unit名・時刻・優先度・任意フィールド」でフィルタして表示
  • いつ使うか: サービスが起動失敗した時の原因調査、リアルタイム監視 (-f)、過去エラーの集計、特定時間帯の障害分析、JSON 出力での jq 集計
  • 解決する具体的な問題: 「各サービスがバラバラの場所にログを吐いていて、横断検索できない」を統合ログで解決。/var/log/myapp.log を自前で管理する手間がゼロになる
# このサービスのログだけ
journalctl -u myapp
 
# リアルタイム追跡
journalctl -u myapp -f
 
# 直近100行
journalctl -u myapp -n 100
 
# 期間指定
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp --since "2026-05-14 10:00" --until "2026-05-14 11:00"
 
# エラーレベルのみ
journalctl -u myapp -p err
 
# JSON 形式(パイプ向け)
journalctl -u myapp -o json | jq
 
# ブート単位
journalctl -u myapp -b      # 今回起動以降
journalctl -u myapp -b -1   # 1つ前の起動

ざっくり言うと(journald が解決した世界)

昔は 各サービスがバラバラの場所にログを書いていた。 journald は 「みんな標準出力に書け、俺が回収する」 という統合ログシステム。 これで journalctl -u <unit> だけで全サービスのログを横断検索できるようになった。

journald 以前の世界

nginx       → /var/log/nginx/access.log, error.log(自分でローテ)
postgres    → /var/log/postgresql/postgres.log(自分でローテ)
myapp       → /var/log/myapp.log(自分でローテ)
cron        → /var/log/cron.log
sshd        → /var/log/auth.log

問題:

障害発生
  ↓
ログの場所はどこ?
  ↓
サービスごとにバラバラのパスを覚える
  ↓
形式もバラバラ(時刻フォーマットすら違う)
  ↓
横断検索したい → grep を何箇所も
  ↓
ローテーション設定もサービスごとに自前
  ↓
地獄

journald 以降の世界

nginx       → 標準出力に書く
postgres    → 標準出力に書く
myapp       → 標準出力に書く
            ↓
        journald が回収
            ↓
   バイナリ形式で1箇所に保存
            ↓
   PID, UID, unit名, ブートID などのメタデータも自動付与
            ↓
   journalctl で横断検索

アプリ側のコードの違い

// 昔: 自前でファイルを開いてローテーションも自作
file, _ := os.OpenFile("/var/log/myapp.log", ...)
log.SetOutput(file)
 
// 今: ただ標準出力に書くだけ
log.SetOutput(os.Stdout)

systemd 経由で起動すれば、それだけで自動的に journald に吸われる。

journald のメタデータが凄い

ログ1行に対して自動で付くフィールド:

{
  "MESSAGE": "request processed",
  "_PID": "1234",
  "_UID": "1001",
  "_SYSTEMD_UNIT": "myapp.service",
  "_BOOT_ID": "abc123...",      ← どの起動の時のログか
  "_MACHINE_ID": "def456...",   ← どのマシンか
  "PRIORITY": "6",              ← severity
  "_COMM": "myapp"
}

だから検索が強力:

journalctl _PID=1234                    # 特定 PID
journalctl _SYSTEMD_UNIT=myapp.service  # 特定 unit
journalctl -b -1                        # 1つ前のブート時
journalctl -p err --since "1h ago"      # 直近1時間のエラーのみ

対比表

従来(ファイルログ)journald
ログの場所サービスごと別々統合 (/var/log/journal/)
形式各自バラバラ構造化バイナリ
メタデータテキストに混在自動付与(PID、unit等)
ローテーション自前 (logrotate)自動
横断検索grep を何箇所もjournalctl 1コマンド
アプリのコードファイル open + ローテ対応stdout に書くだけ
プレーンgrepできるできない(jq 経由)

一番覚えやすい説明

  • : 各アプリが自前でログファイル管理(カオス)
  • : 全アプリが stdout に書く → journald が拾う(統合)
  • アプリ側: ファイル open 不要、stdout に書くだけ
  • 検索: journalctl -u <unit> が万能の入り口

セキュリティ:journal にセンシティブ情報を書かない

journal は通常 adm グループまたは systemd-journal グループのユーザーが読める。Web サーバーがリクエストボディに含まれるパスワードをログに書いていたら:

2026-05-14T10:30:00 INFO POST /login body={"user":"alice","password":"hunter2"}

これが journal に永続化される。サーバーに侵入されたら全部見られる。

対策

  • ログにパスワード・トークン・クレカ番号・個人情報を出さない
  • 構造化ログでフィールドを選別: password フィールドは省略
  • 監査ログは別系統に(暗号化ストレージ)
  • SystemCallFilter= で journal アクセスを制限することも可能

9. systemctl-analyze で起動時間を可視化

OS 全体の起動が遅い時、どのサービスが時間を食っているか を数値で可視化するツール。クラウドの自動スケーリングで起動時間が課金やSLAに直結する時代の必須スキルです。

  • 何のコマンドか: systemd-analyze で総起動時間、blame で各 unit の起動時間ランキング、plot で SVG の起動タイムライン
  • いつ使うか: 「再起動後にサービスが立ち上がるまで5分かかる」原因調査、AMI 化前の起動時間最適化、自動スケーリンググループのウォームアップ時間短縮
  • 解決する具体的な問題: 「cloud-init や NetworkManager で時間を食っている」「依存関係の解決待ちで直列実行になっている」を可視化し、Wants=After= に変える、不要 unit を disable する、などの改善判断ができるようになります
# 起動全体の時間
systemd-analyze
 
# 各 unit の起動時間(遅い順)
systemd-analyze blame | head -20
 
# 依存グラフを SVG で出力
systemd-analyze plot > /tmp/boot.svg

パフォーマンス調査の入り口

「サーバー起動が遅い」「再起動後にサービスが立ち上がるまで遅い」場合、systemd-analyze blame で何が時間を食っているか即座に分かる。typically NetworkManager や cloud-init が上位。


セッション③: 障害対応と落とし穴(15-20分)

10. クラッシュ時の自動復活を検証

「設定したから大丈夫」と思い込まず、実際に kill して自動復活を目で確認する のがプロの作法です。本番投入前に必ず通すべきテストシナリオ。

  • 何をするか: systemctl show で現在の MainPID を取得し、kill -9 で強制停止 → 数秒後に自動再起動されているか確認
  • いつやるか: 新しい unit をデプロイした直後、Restart= 設定を変更した後、本番障害シナリオを社内訓練する時
  • 解決する具体的な問題: 「Restart=on-failure を書いたつもりが Restart=no だった」「実は OOM Killer 経由だと再起動しない条件だった」を実機テストで発覚させ、本番事故になる前に潰せる
# プロセス ID を取得
PID=$(systemctl show myapp -p MainPID --value)
echo "myapp PID = $PID"
 
# 強制 kill
sudo kill -9 $PID
 
# 数秒待ってから状態確認
sleep 6
systemctl status myapp
 
# ログで再起動の経緯を確認
journalctl -u myapp -n 20

期待される出力

May 14 10:30:00 host systemd[1]: myapp.service: Main process exited, code=killed, status=9/KILL
May 14 10:30:00 host systemd[1]: myapp.service: Failed with result 'signal'.
May 14 10:30:05 host systemd[1]: myapp.service: Scheduled restart job, restart counter is at 1.
May 14 10:30:05 host systemd[1]: Started My Application HTTP Server.

RestartSec=5s で5秒後に復活している。

11. 再起動が暴走しないように制限

Restart=on-failure だけでは 無限再起動ループ の罠があります。本番では起動失敗が連続する状況で CPU を100%占有しないよう、必ず上限を設定します。

  • 何の設定か: StartLimitIntervalSec=StartLimitBurst= で「N秒間にM回失敗したら再起動を諦める」を宣言
  • いつ書くか: すべての本番 unit ファイル。学習用 unit でもこの2行を入れる癖を付ける
  • 解決する具体的な問題: 「設定ファイルの不備で起動するたびに即死するサービスが無限再起動して CPU 100%」「ログが秒単位で爆発してディスク満杯」を、5分5回で諦める設定で予防します
[Service]
Restart=on-failure
RestartSec=5s
 
# 5分間で5回失敗したら諦める
StartLimitIntervalSec=300
StartLimitBurst=5

無限再起動ループの恐怖

起動するたびにクラッシュするバグがあると、Restart=always だと無限ループ。CPU をブン回し続けて他のサービスに影響。

StartLimitBurst=5 で「短時間に5回失敗したら停止」できる。本番では必ず設定する。

12. アンチパターン集

アンチパターン: root で動かす

# NG
[Service]
User=root
ExecStart=/opt/myapp/myapp

アプリの脆弱性が即サーバー乗っ取りに直結。必ず専用ユーザーを作る

アンチパターン: 相対パス

# NG
ExecStart=./myapp
WorkingDirectory=opt/myapp

systemd は cwd=/ で起動するため、相対パスは事故る。全て絶対パス

アンチパターン: 自前でログファイルに書く

// NG: アプリが直接 /var/log/myapp.log に書く
log.SetOutput(file)

問題:

  • ローテーションを自前で実装するハメに
  • ファイル権限の管理が面倒(誰が書く?誰が読む?)
  • 構造化と検索が貧弱

正解: 標準出力に書くだけ。journald が拾う。後で journalctl で見る。

アンチパターン: ExecStart で & をつけてバックグラウンド化

# NG
ExecStart=/opt/myapp/myapp &

systemd は & を解釈しない(シェルじゃないから)。引数として & がそのままバイナリに渡って起動失敗。バックグラウンド化したいなら Type=forking を使う(が、ほぼ不要)。

アンチパターン: 環境変数を Environment= に秘密情報で書く

# NG
Environment=DATABASE_PASSWORD=hunter2

unit ファイルは world-readable (-rw-r--r--) に置かれることが多く、誰でも読める。

正解:

  • EnvironmentFile=/etc/myapp/env.secretchmod 600 で配置
  • もっと良いのは LoadCredential= や Vault / AWS Secrets Manager 経由

13. trouble shooting フローチャート

サービスが起動しない時の調査手順

# 1. ステータス確認
systemctl status myapp
 
# 2. 詳細ログ
journalctl -u myapp -n 50
 
# 3. unit の設定を確認(drop-in 含む)
systemctl cat myapp
 
# 4. unit の解析後の状態
systemctl show myapp
 
# 5. 起動コマンドを直接実行してみる
sudo -u myapp /opt/myapp/myapp
 
# 6. 環境変数の確認
systemctl show myapp -p Environment

練習課題

課題1: 自分の Go サーバーを常駐化する

上の myapp.service の手順を最後まで実行し、以下を確認:

  • curl localhost:8080 でレスポンスが返る
  • journalctl -u myapp -f でログが見える
  • プロセスを kill -9 しても5秒後に自動再起動する
  • VM 再起動後も自動で起動する

課題2: drop-in でメモリ制限を追加

sudo systemctl edit myapp

エディタが開いたら以下を追加:

[Service]
MemoryMax=128M
CPUQuota=50%
sudo systemctl daemon-reload
sudo systemctl restart myapp
systemctl status myapp  # cgroup の項目に Memory が表示される

課題3: ヘルスチェック付きスクリプトを oneshot で

# /etc/systemd/system/myapp-healthcheck.service
[Unit]
Description=Healthcheck for myapp
 
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'curl -f http://localhost:8080/ || systemctl restart myapp'

→ 次の 2-5_cron.md.timer と組み合わせて定期実行する手順を扱う。


締め: git で証跡を残す

# unit ファイルをリポジトリに保管(編集時のバージョン管理)
mkdir -p ~/learn/linux/day204/systemd
sudo cp /etc/systemd/system/myapp.service ~/learn/linux/day204/systemd/
 
cd ~/learn/linux/day204
git init 2>/dev/null
git add .
git commit -m "feat(linux): systemd で Go サーバーを常駐化"
 
exit

チェックリスト

  • systemctl status / start / stop / restart / reload の違いを説明できる
  • restartreload を本番でどう使い分けるか答えられる
  • [Unit] [Service] [Install] 各セクションの役割を説明できる
  • Type=simpleType=forking の違いを説明できる
  • Restart=on-failureRestart=always の使い分けを説明できる
  • User=, Group=, WorkingDirectory=, EnvironmentFile= を理解している
  • journalctl でフィルタ・期間指定・unit指定でログを引ける
  • 専用ユーザーを作って動かす理由を説明できる
  • セキュリティ強化オプションを3つ以上挙げられる

詰まった時のチートシート

やりたいことコマンド
状態確認systemctl status <unit>
起動 / 停止sudo systemctl start <unit> / stop
自動起動有効化sudo systemctl enable --now <unit>
unit 編集(drop-in)sudo systemctl edit <unit>
unit 設定確認systemctl cat <unit>
解析後の状態systemctl show <unit>
設定再読込sudo systemctl daemon-reload
ログ表示journalctl -u <unit> -n 100
ログ追跡journalctl -u <unit> -f
エラーログのみjournalctl -u <unit> -p err
起動時間分析systemd-analyze blame
全 unit 一覧systemctl list-unit-files

「実務OK」基準

  • 自分の Go/Node/Python アプリを unit ファイルで常駐化できる: 専用ユーザー + Restart + journald
  • 「nginx の設定変えたのにダウンタイム出した」を防げる: reload を使う
  • 障害発生時に journalctl で5分以内に原因を絞れる: フィルタを駆使
  • systemd の落とし穴を3つ即答できる: 相対パス、root実行、自前ログ、& バックグラウンド

さらに深掘りするなら

  • 公式: https://www.freedesktop.org/software/systemd/man/systemd.service.html (service unit 全リファレンス)
  • man systemd.exec : 実行環境とセキュリティオプションの完全一覧
  • man systemd.resource-control : メモリ・CPU・IO 制限
  • 書籍『systemd エッセンシャル』
  • 実例の宝庫: /lib/systemd/system/ 配下の既存 unit を読む
  • nginx, postgresql, redis, prometheus の unit ファイル比較
  • systemd-nspawn: 軽量コンテナ機能(Docker 以前から存在)

アンチパターン / 初心者やらかし事例

NG 1: ExecStart に相対パスを書いて起動失敗

ExecStart=./myapp   # NG: systemd は WorkingDirectory を意識せず、起動時に「実体不明」

→ 対策: 絶対パスで書く: ExecStart=/usr/local/bin/myappWorkingDirectory= も別途指定する。

NG 2: バックグラウンド化で Type=simple のまま & を使う

ExecStart=/usr/local/bin/myapp &

→ systemd は & で「すぐ終わった」と誤認し、毎回 restart して無限ループ。 → 対策: アプリは フォアグラウンド で起動する(コードを修正)。フォークするデーモンなら Type=forking

NG 3: root 権限のままサービス起動

[Service]
ExecStart=/usr/local/bin/myapp

→ User= を指定しないと root で起動。脆弱性があれば即サーバー全権を取られる。 → 対策: 専用ユーザー appuser を作って User=appuser、最小権限で動かす。

NG 4: 設定変更後に daemon-reload を忘れる

sudo vim /etc/systemd/system/myapp.service
sudo systemctl restart myapp   # 古い設定で起動!

→ 対策: ユニットファイル変更後は 必ず sudo systemctl daemon-reloadrestart の順。


自己評価チェックリスト

  • restartreload の差を 「プロセスを殺すか、シグナルで設定再読込か」 で説明できる
  • Type=simpleType=forking の選び方を答えられる
  • Restart=on-failureRestart=always の使い分けを言える
  • journalctl -u myapp -f で自分のアプリのログをリアルタイム監視した
  • 専用ユーザーで動かす理由(最小権限の原則)を説明できる

次のレッスン: Day 12 - cron

明日は cron による定期実行 を学ぶ。

systemd の .timer ユニットとの比較も行い、「いつどちらを使うべきか」を判断できるようになる。今日身につけた「systemd で常駐」と、明日学ぶ「cron で定期実行」の 2つで運用自動化の8割が片付く

Day 12: cron