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/の起動スクリプト方式)で行われていた。問題:
- 完全に逐次起動: スクリプトを上から順に実行するだけ。10秒以上の起動待ち
- 依存解決が貧弱: 「DB が起動してからアプリ起動」のような依存を書きづらい
- シェルスクリプトの集合体: 何百行ものシェルで脆い、デバッグ困難
- プロセス監視が無い: 起動はできるが、死んだら検知できない
- ログがバラバラ: 各サービスが自前でログを書く → 場所も形式もバラバラ
systemd は2010年に登場し、これら全てを解決:
- 並列起動: 依存関係から逆算してDAGで起動 → 起動時間が劇的短縮
- 宣言的な unit ファイル:
[Service]\nExecStart=...のような ini 形式- プロセス監視と自動再起動:
Restart=always- journald による統合ログ: 全サービスのログを構造化して一元管理
- cgroups でリソース制限: メモリ・CPU 上限を unit で指定可能
一方で批判もある:「機能を抱え込みすぎ(init/ログ/ネットワーク/DNS)」「Unix 哲学に反する」など。が、現状の Linux サーバーで避けて通れない。
2. systemctl の基本操作
systemctl は systemd を操作するための司令塔コマンド。サービスの起動・停止・状態確認・自動起動の有効化など、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 nginxOS 起動時の自動起動が有効化された
↓
(でも今は起動していない)
↓
「あれ、サービス動いてない?」 ← 事故
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 nginxsystemctl が nginx に SIGTERM を送る
↓
nginx (1234) と全ワーカーが終了
↓
ユーザー A,B,C の接続が全部切れる → エラー画面
↓
systemd が新しい nginx を起動
↓
新しい PID で待ち受け開始
イメージ: restart = 店を一旦閉めて開け直す。中の客は全員追い出される。
reload を打った時のフロー
systemctl reload nginxsystemctl が nginx に SIGHUP を送る
↓
nginx (1234) はそのまま生きている
↓
nginx が「設定ファイルを読み直せ」と理解
↓
新しい設定で新ワーカーを起動
↓
既存ワーカー (1235,1236,1237) は接続が終わるまで生かす
↓
新規接続は新ワーカーへ
↓
ユーザー A,B,C は何も気づかない → 無停止
イメージ: reload = 店を開けたまま、新メニューを配る。既存の客は古いメニューのまま、新規客は新メニュー。
対比表
restart | reload | |
|---|---|---|
| プロセス | 殺して再起動 | 生かしたまま |
| シグナル | 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.serviceunit の種類と用途
拡張子 種類 用途 .serviceservice unit 常駐プロセスを管理(最頻出) .socketsocket unit ポートやソケットを監視し、接続時にサービス起動(systemd の socket activation) .timertimer unit cron の代替。定期実行 .mountmount unit ファイルシステムのマウント .pathpath unit ファイルやディレクトリの変更を監視 .targettarget unit 複数 unit のグループ化( multi-user.targetなど).sliceslice unit cgroups リソース制限 覚えるべき本質: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箇所にある(優先順位)
/etc/systemd/system/← ユーザー定義 が最優先/run/systemd/system/← ランタイム生成/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 しても蘇るゾンビ
↓
人間が「なんで止まらないんだ!」とパニック
= 「何があっても起動し続ける」 暴走する番犬。
対比表
no | on-failure | always | |
|---|---|---|---|
| 異常終了(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 myappunit を編集したら
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/myappsystemd は
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=hunter2unit ファイルは
world-readable(-rw-r--r--) に置かれることが多く、誰でも読める。正解:
EnvironmentFile=/etc/myapp/env.secretをchmod 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の違いを説明できる -
restartとreloadを本番でどう使い分けるか答えられる -
[Unit][Service][Install]各セクションの役割を説明できる -
Type=simpleとType=forkingの違いを説明できる -
Restart=on-failureとRestart=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/myapp。WorkingDirectory= も別途指定する。
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-reload → restart の順。
自己評価チェックリスト
-
restartとreloadの差を 「プロセスを殺すか、シグナルで設定再読込か」 で説明できる -
Type=simpleとType=forkingの選び方を答えられる -
Restart=on-failureとRestart=alwaysの使い分けを言える -
journalctl -u myapp -fで自分のアプリのログをリアルタイム監視した - 専用ユーザーで動かす理由(最小権限の原則)を説明できる
次のレッスン: Day 12 - cron
明日は cron による定期実行 を学ぶ。
systemd の .timer ユニットとの比較も行い、「いつどちらを使うべきか」を判断できるようになる。今日身につけた「systemd で常駐」と、明日学ぶ「cron で定期実行」の 2つで運用自動化の8割が片付く。
→ Day 12: cron へ