<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">systemdサービスユニットファイルの作成と管理:DevOpsベストプラクティス</h1>
<h2 class="wp-block-heading">はじめに</h2>
<p>Linuxシステムにおけるサービス管理のデファクトスタンダードであるsystemdは、堅牢なアプリケーション運用に不可欠です。本記事では、DevOpsエンジニアとしてsystemdサービスユニットおよびタイマーユニットの作成と管理を、セキュリティ、冪等性、そして効率性を考慮したベストプラクティスに基づいて解説します。安全なシェルスクリプトの書き方、<code>curl</code>と<code>jq</code>を用いた外部連携、そしてroot権限の適切な扱いについても深掘りします。</p>
<h2 class="wp-block-heading">要件と前提</h2>
<h3 class="wp-block-heading">目的</h3>
<ul class="wp-block-list">
<li><p>任意のアプリケーションをsystemdサービスとして登録し、自動起動・監視可能にする。</p></li>
<li><p>定期実行タスクをsystemdタイマーで管理する。</p></li>
<li><p>サービスデプロイプロセスを冪等なシェルスクリプトで自動化する。</p></li>
<li><p>実行時のセキュリティを強化し、権限分離を徹底する。</p></li>
</ul>
<h3 class="wp-block-heading">前提条件</h3>
<ul class="wp-block-list">
<li><p>systemdが動作するLinux環境(例: CentOS, Ubuntu, Fedoraなど)。</p></li>
<li><p>Bashシェルおよび基本的なLinuxコマンドの知識。</p></li>
<li><p><code>curl</code>, <code>jq</code>, <code>systemctl</code>, <code>journalctl</code> コマンドが利用可能であること。</p></li>
<li><p>root権限での操作が必要な箇所があることを理解していること。</p></li>
</ul>
<h3 class="wp-block-heading">セキュリティと権限分離の基本原則</h3>
<p>systemdサービスは多くの場合、システム起動時にroot権限で実行されますが、アプリケーション本体は可能な限り非特権ユーザーで動作させるべきです。これにより、万が一アプリケーションに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます。具体的には、以下の原則に従います。</p>
<ul class="wp-block-list">
<li><p><strong>最小権限の原則</strong>: サービスは必要最低限の権限のみを持つユーザー/グループで実行する。</p></li>
<li><p><strong>ファイルシステム保護</strong>: <code>ProtectSystem</code>, <code>ProtectHome</code>, <code>PrivateTmp</code>などのディレクティブで、サービスからアクセス可能なファイルシステム領域を制限する。</p></li>
<li><p><strong>権限昇格の禁止</strong>: <code>NoNewPrivileges</code>, <code>CapabilityBoundingSet</code> などで、サービスが新たな特権を取得するのを防ぐ。</p></li>
<li><p><strong>環境分離</strong>: サービス固有の実行環境を構築し、システム全体の環境に影響を与えないようにする。</p></li>
</ul>
<h2 class="wp-block-heading">実装</h2>
<p>systemdサービスをデプロイする一般的なフローは以下の通りです。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["サービス定義とスクリプトの作成"] --> B{"冪等なセットアップスクリプト"};
B --> C["アプリバイナリ/設定のダウンロード/配置"];
C --> D["systemdユニットファイルの作成/配置"];
D --> E["systemctl daemon-reload|ユニット設定を再読み込み|"];
E --> F["systemctl enable --now my-app.service|サービスの有効化と起動|"];
F --> G["systemctl status my-app.service|サービスの状態確認|"];
G --> H["ログ確認: journalctl -u my-app.service|標準出力/エラーを確認|"];
H --> I["サービス正常稼働"];
</pre></div>
<h3 class="wp-block-heading">サービス対象アプリケーションの準備</h3>
<p>今回は、<code>my-app</code>という架空のGo言語製HTTPサーバーを例に、これをsystemdで管理します。アプリケーションバイナリは <code>/usr/local/bin/my-app</code> に、設定ファイルは <code>/etc/my-app/config.json</code> に配置すると仮定します。</p>
<h3 class="wp-block-heading">冪等なセットアップスクリプトの作成</h3>
<p>デプロイプロセスは、複数回実行しても同じ結果になる「冪等性」を持つべきです。また、シェルスクリプトはエラー耐性を高めるために安全な書き方を採用します。</p>
<h4 class="wp-block-heading">安全なシェルスクリプトの原則</h4>
<ul class="wp-block-list">
<li><p><code>set -euo pipefail</code>:</p>
<ul>
<li><p><code>-e</code>: エラーが発生したら即座にスクリプトを終了。</p></li>
<li><p><code>-u</code>: 未定義の変数を使用したらエラー。</p></li>
<li><p><code>-o pipefail</code>: パイプライン中のコマンドが一つでも失敗したらパイプライン全体を失敗とする。</p></li>
</ul></li>
<li><p><code>trap</code>: スクリプト終了時にクリーンアップ処理を実行する。</p></li>
<li><p><code>mktemp -d</code>: 一時ファイルを安全な方法で作成する。</p></li>
</ul>
<h4 class="wp-block-heading">一時ディレクトリの利用とクリーンアップ</h4>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# file: setup.sh
# ----------------------------------------------------
# 安全なシェルスクリプトのヘッダ
# ----------------------------------------------------
set -euo pipefail
# 一時ディレクトリの作成
TMP_DIR=$(mktemp -d -t my-app-deploy-XXXXXX)
# スクリプト終了時に一時ディレクトリを自動削除
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT QUIT TERM
# ログファイル名(JST基準の具体日付 2024年05月15日 を使用)
LOG_FILE="/var/log/my-app-setup_$(date +%Y%m%d_%H%M%S).log"
# 標準出力と標準エラー出力をログファイルとコンソールにリダイレクト
exec > >(tee -a "$LOG_FILE") 2>&1
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚀 my-app サービスセットアップ開始..."
echo "一時ディレクトリ: $TMP_DIR"
# ----------------------------------------------------
# root権限の確認と非特権ユーザーの準備
# ----------------------------------------------------
if [[ "$(id -u)" -ne 0 ]]; then
echo "エラー: このスクリプトはroot権限で実行する必要があります。" >&2
exit 1
fi
APP_USER="my-app-user"
APP_GROUP="my-app-group"
# ユーザーとグループが存在しない場合のみ作成 (冪等性)
if ! id "$APP_USER" &>/dev/null; then
echo "ユーザー $APP_USER を作成中..."
useradd --system --no-create-home --shell /sbin/nologin "$APP_USER"
fi
if ! getent group "$APP_GROUP" &>/dev/null; then
echo "グループ $APP_GROUP を作成中..."
groupadd --system "$APP_GROUP"
fi
# ユーザーをグループに追加 (既に存在しても問題なし)
usermod -a -G "$APP_GROUP" "$APP_USER"
# アプリケーションディレクトリの準備 (冪等性)
APP_DIR="/usr/local/bin"
CONFIG_DIR="/etc/my-app"
LOG_DIR="/var/log/my-app"
mkdir -p "$APP_DIR" "$CONFIG_DIR" "$LOG_DIR"
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR" "$LOG_DIR"
chmod 750 "$CONFIG_DIR" "$LOG_DIR"
# ----------------------------------------------------
# curl と jq を用いた外部API連携の例
# ----------------------------------------------------
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ⚙️ 設定ファイルを取得・生成中..."
# 外部APIから設定情報を取得する例 (ダミーURL)
# --retry: 5回までリトライ
# --retry-delay: リトライ間隔 (秒)
# --retry-max-time: 最大リトライ時間 (秒)
# --cacert: CA証明書を指定してTLS検証を強化
# --show-error: エラー時に詳細を表示
# -f, --fail: サーバーエラー(4xx, 5xx)でも失敗とみなす
# -s, --silent: 進捗表示を抑制
# -S, --show-error: エラー時にはエラーメッセージを表示
CONFIG_JSON_URL="https://api.example.com/v1/config/my-app" # ダミーURL
APP_BIN_URL="https://downloads.example.com/my-app-v1.0.0.tar.gz" # ダミーURL
CA_CERT_PATH="/etc/ssl/certs/ca-certificates.crt" # システムのCA証明書パス
# 設定を取得し、一部をjqで加工
if curl -fsSL --retry 5 --retry-delay 5 --retry-max-time 60 \
--cacert "$CA_CERT_PATH" "$CONFIG_JSON_URL" > "$TMP_DIR/raw_config.json"; then
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 取得した設定を加工中..."
# jqを使用して、設定値の一部をデフォルト値に上書きまたは追加する例
jq '. + { "log_level": "info", "max_connections": 100 }' "$TMP_DIR/raw_config.json" > "$CONFIG_DIR/config.json"
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR/config.json"
chmod 640 "$CONFIG_DIR/config.json"
else
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚨 エラー: 設定ファイルの取得または加工に失敗しました。" >&2
# エラー時はデフォルト設定を使用するなどのフォールバック
echo '{"port": 8080, "log_level": "info", "max_connections": 100}' > "$CONFIG_DIR/config.json"
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR/config.json"
chmod 640 "$CONFIG_DIR/config.json"
fi
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 📥 アプリケーションバイナリをダウンロード中..."
if curl -fsSL --retry 5 --retry-delay 5 --retry-max-time 60 \
--cacert "$CA_CERT_PATH" "$APP_BIN_URL" | tar -xz -C "$TMP_DIR"; then
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] バイナリを配置中..."
mv "$TMP_DIR/my-app" "$APP_DIR/my-app"
chown "$APP_USER:$APP_GROUP" "$APP_DIR/my-app"
chmod 755 "$APP_DIR/my-app"
else
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚨 エラー: アプリケーションバイナリのダウンロードに失敗しました。" >&2
exit 1
fi
# ----------------------------------------------------
# systemdユニットファイルの作成と配置
# ----------------------------------------------------
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 📝 systemdサービスユニットファイルを作成中..."
SERVICE_UNIT_PATH="/etc/systemd/system/my-app.service"
TIMER_UNIT_PATH="/etc/systemd/system/my-app-updater.timer"
UPDATER_SERVICE_UNIT_PATH="/etc/systemd/system/my-app-updater.service"
cat <<EOF > "$SERVICE_UNIT_PATH"
[Unit]
Description=My Application Service
After=network.target
[Service]
ExecStart=/usr/local/bin/my-app -config /etc/my-app/config.json
WorkingDirectory=/usr/local/bin
User=$APP_USER
Group=$APP_GROUP
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-app
# --- セキュリティ強化ディレクティブ ---
# 新しい特権の取得を禁止
NoNewPrivileges=true
# chrootやmountなどの危険なシステムコールを制限
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN CAP_SETUID CAP_SETGID
# /root, /home, /srv, /mediaなどのユーザーディレクトリを保護
ProtectHome=true
# /usr, /boot, /etcへの書き込みを禁止
ProtectSystem=full
# サービス専用の一時ディレクトリ (/tmp, /var/tmp) を提供
PrivateTmp=true
# ファイルシステムの名前空間を分離し、サービスのプロセスから見えるファイルシステムを制限
PrivateDevices=true
# ネットワークインターフェース情報を隠蔽
PrivateNetwork=false # ネットワークアクセスが必要なためfalse
# サービスが実行するプロセスのPID名前空間を分離
PIDFile=/run/my-app.pid # アプリケーションがPIDファイルを生成する場合
# サービスプロセスがアクセスできるリソースを制限
MemoryAccounting=true
CPUAccounting=true
IOAccounting=true
EOF
cat <<EOF > "$UPDATER_SERVICE_UNIT_PATH"
[Unit]
Description=My Application Update Task
Requires=my-app.service
After=my-app.service
[Service]
Type=oneshot
ExecStart=/bin/bash -c "echo 'Running update task...'; /usr/local/bin/my-app --update; echo 'Update task finished.'"
User=$APP_USER
Group=$APP_GROUP
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-app-updater
# タイマーサービスもセキュリティ強化
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
EOF
cat <<EOF > "$TIMER_UNIT_PATH"
[Unit]
Description=Run My Application Update Task daily
Requires=my-app-updater.service
[Timer]
# 毎日午前3時に実行
OnCalendar=*-*-* 03:00:00
# 起動時に実行されていない場合は、次回のスケジュールで実行
Persistent=true
# 実行精度を1分に設定 (デフォルトは1分)
AccuracySec=1min
[Install]
WantedBy=timers.target
EOF
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ✅ ユニットファイルの配置が完了しました。"
# ----------------------------------------------------
# systemdデーモンのリロードとサービスの有効化・起動
# ----------------------------------------------------
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🔄 systemdデーモンをリロード中..."
systemctl daemon-reload
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚀 my-app.service を有効化し、起動中..."
systemctl enable --now my-app.service
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ⏰ my-app-updater.timer を有効化し、起動中..."
systemctl enable --now my-app-updater.timer
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ✨ セットアップが完了しました。"
</pre>
</div>
<h4 class="wp-block-heading"><code>systemd</code> サービスユニットファイルの作成</h4>
<p><code>my-app.service</code> は、アプリケーション本体を実行するためのユニットです。
<strong><code>[Unit]</code> セクション</strong>: ユニットの一般的な情報と依存関係を定義します。</p>
<ul class="wp-block-list">
<li><p><code>Description</code>: ユニットの説明。</p></li>
<li><p><code>After</code>: サービスが起動する前に完了しているべきユニットを指定。</p></li>
</ul>
<p><strong><code>[Service]</code> セクション</strong>: サービス固有の設定を定義します。</p>
<ul class="wp-block-list">
<li><p><code>ExecStart</code>: サービス起動時に実行されるコマンド。</p></li>
<li><p><code>WorkingDirectory</code>: コマンドが実行されるディレクトリ。</p></li>
<li><p><code>User</code>, <code>Group</code>: サービスを実行するユーザーとグループ。<strong>root権限を避けるための重要な設定です。</strong></p></li>
<li><p><code>Restart</code>: サービス終了時の再起動ポリシー(例: <code>on-failure</code>, <code>always</code>)。</p></li>
<li><p><code>RestartSec</code>: 再起動までの待機時間。</p></li>
<li><p><code>LimitNOFILE</code>: オープン可能なファイルディスクリプタの最大数。</p></li>
<li><p><code>StandardOutput</code>, <code>StandardError</code>: 標準出力と標準エラーの転送先(<code>journal</code>はsystemdジャーナルへ)。</p></li>
<li><p><strong>セキュリティ強化ディレクティブ</strong>:</p>
<ul>
<li><p><code>NoNewPrivileges=true</code>: サービスが新たな特権を取得することを禁止します。</p></li>
<li><p><code>CapabilityBoundingSet</code>: プロセスが持つ可能性のあるLinuxケーパビリティ(特権)を制限します。例: <code>CAP_NET_BIND_SERVICE</code> は1024番以下のポートにバインドする権限。</p></li>
<li><p><code>ProtectHome=true</code>, <code>ProtectSystem=full</code>: <code>/home</code>, <code>/root</code>、<code>/usr</code>, <code>/etc</code> などへのアクセスを制限し、ファイルシステムを保護します。</p></li>
<li><p><code>PrivateTmp=true</code>: サービス専用の一時ディレクトリ (<code>/tmp</code>, <code>/var/tmp</code>) を提供し、システム全体の一時ファイルへのアクセスを防ぎます。</p></li>
<li><p><code>PrivateDevices=true</code>: サービスがデバイスファイルにアクセスするのを防ぎます。</p></li>
<li><p><code>PIDFile</code>: アプリケーションがPIDファイルを生成する場合に指定します。systemdがPIDを管理します。</p></li>
</ul></li>
</ul>
<p><strong><code>[Install]</code> セクション</strong>: ユニットの有効化(<code>systemctl enable</code>)に関する情報を定義します。</p>
<ul class="wp-block-list">
<li><code>WantedBy</code>: このユニットを有効化した際に、どのターゲットユニットにシンボリックリンクが張られるかを指定します。<code>multi-user.target</code>は通常システム起動時のターゲットです。</li>
</ul>
<h4 class="wp-block-heading"><code>systemd</code> タイマーユニットファイルの作成</h4>
<p><code>my-app-updater.timer</code> は定期的なタスクを実行するためのユニットです。</p>
<ul class="wp-block-list">
<li><code>my-app-updater.service</code> はタイマーによって起動される実際のタスクを定義します。</li>
</ul>
<p><strong><code>[Timer]</code> セクション</strong>: タイマー固有の設定を定義します。</p>
<ul class="wp-block-list">
<li><p><code>OnCalendar</code>: タイマーがアクティブになる日時を指定します。Cron形式に似ています。</p></li>
<li><p><code>Persistent=true</code>: systemdの起動中に実行されなかったイベントがある場合、そのイベントがすぐに実行されます。</p></li>
</ul>
<h2 class="wp-block-heading">検証</h2>
<p>セットアップスクリプト実行後、以下のコマンドでサービスの状態を確認します。</p>
<h3 class="wp-block-heading">サービスユニットの起動と状態確認</h3>
<div class="codehilite">
<pre data-enlighter-language="generic">systemctl status my-app.service
</pre>
</div>
<p>出力例:</p>
<pre data-enlighter-language="generic">● my-app.service - My Application Service
Loaded: loaded (/etc/systemd/system/my-app.service; enabled; vendor preset: disabled)
Active: active (running) since Wed 2024-05-15 10:00:00 JST; 1min 20s ago
Main PID: 1234 (my-app)
Tasks: 6 (limit: 4915)
Memory: 10.5M
CPU: 45ms
CGroup: /system.slice/my-app.service
└─1234 /usr/local/bin/my-app -config /etc/my-app/config.json
</pre>
<p><code>Active: active (running)</code> であれば正常に起動しています。</p>
<h3 class="wp-block-heading">タイマーユニットの起動と状態確認</h3>
<div class="codehilite">
<pre data-enlighter-language="generic">systemctl status my-app-updater.timer
</pre>
</div>
<p>出力例:</p>
<pre data-enlighter-language="generic">● my-app-updater.timer - Run My Application Update Task daily
Loaded: loaded (/etc/systemd/system/my-app-updater.timer; enabled; vendor preset: disabled)
Active: active (waiting) since Wed 2024-05-15 10:00:00 JST; 1min 30s ago
Trigger: Thu 2024-05-16 03:00:00 JST; 16h left
Triggers: ● my-app-updater.service
</pre>
<p><code>Active: active (waiting)</code> で、<code>Trigger</code> に次回の実行日時が表示されていれば正常です。</p>
<h3 class="wp-block-heading">ログの確認</h3>
<p>サービスやタイマーの出力を確認するには <code>journalctl</code> を使用します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">journalctl -u my-app.service
journalctl -u my-app-updater.service
</pre>
</div>
<p><code>-f</code> オプションを付けてリアルタイムでログを追うこともできます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">journalctl -u my-app.service -f
</pre>
</div>
<h2 class="wp-block-heading">運用</h2>
<h3 class="wp-block-heading">サービスの一時停止・再起動</h3>
<div class="codehilite">
<pre data-enlighter-language="generic">systemctl stop my-app.service # 停止
systemctl start my-app.service # 起動
systemctl restart my-app.service # 再起動
</pre>
</div>
<h3 class="wp-block-heading">ユニットファイルの更新とリロード</h3>
<p>ユニットファイルを変更した場合、systemdに設定を再読み込みさせる必要があります。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">systemctl daemon-reload # systemdデーモンに設定ファイルの変更を通知
systemctl restart my-app.service # サービスを再起動して新しい設定を適用
</pre>
</div>
<h3 class="wp-block-heading">自動起動の管理</h3>
<p>システム起動時にサービスが自動的に起動するかどうかを設定します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">systemctl enable my-app.service # 自動起動を有効化
systemctl disable my-app.service # 自動起動を無効化
</pre>
</div>
<h2 class="wp-block-heading">トラブルシューティング</h2>
<h3 class="wp-block-heading">サービスが起動しない場合</h3>
<ol class="wp-block-list">
<li><p><strong><code>systemctl status</code> で確認</strong>: 最も重要なコマンドです。エラーメッセージ、PID、CGroup情報から手がかりを得ます。</p></li>
<li><p><strong><code>journalctl -u</code> でログを確認</strong>: アプリケーションが出力するログや、systemd自体のエラーメッセージを確認します。</p></li>
<li><p><strong>ユニットファイルの構文エラー</strong>: <code>systemctl daemon-reload</code> 時にエラーが出力されることがあります。<code>systemd-analyze verify <unit_file></code> で構文チェックが可能です。</p></li>
</ol>
<h3 class="wp-block-heading">ログからの原因特定</h3>
<p><code>journalctl</code> は非常に強力なツールです。</p>
<ul class="wp-block-list">
<li><p><code>journalctl -u my-app.service --since "1 hour ago"</code>: 過去1時間分のログ。</p></li>
<li><p><code>journalctl -u my-app.service -p err..crit</code>: エラー以上の深刻度を持つログのみ表示。</p></li>
<li><p><code>journalctl -e -u my-app.service</code>: 最新のログを最後まで表示。</p></li>
</ul>
<h3 class="wp-block-heading"><code>systemd-analyze</code> の活用</h3>
<ul class="wp-block-list">
<li><p><code>systemd-analyze verify my-app.service</code>: ユニットファイルの構文や参照関係をチェックします。</p></li>
<li><p><code>systemd-analyze security my-app.service</code>: ユニットファイルのセキュリティ設定を評価し、潜在的な脆弱性を特定します。この機能は、設定したセキュリティ強化ディレクティブが正しく機能しているかを確認するのに役立ちます。</p></li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>、systemdサービスユニットとタイマーユニットの作成・管理におけるDevOpsベストプラクティスを解説しました。冪等なシェルスクリプトを用いた安全なデプロイ、<code>curl</code>と<code>jq</code>を活用した設定管理、そして<code>User</code>ディレクティブやセキュリティ強化ディレクティブによる厳格な権限分離は、堅牢なシステム運用に不可欠です。これらの原則を適用することで、サービスの信頼性とセキュリティを向上させ、トラブルシューティングの効率化にも繋がります。 systemdの機能を最大限に活用し、安定したアプリケーション運用を目指しましょう。</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
systemdサービスユニットファイルの作成と管理:DevOpsベストプラクティス
はじめに
Linuxシステムにおけるサービス管理のデファクトスタンダードであるsystemdは、堅牢なアプリケーション運用に不可欠です。本記事では、DevOpsエンジニアとしてsystemdサービスユニットおよびタイマーユニットの作成と管理を、セキュリティ、冪等性、そして効率性を考慮したベストプラクティスに基づいて解説します。安全なシェルスクリプトの書き方、curlとjqを用いた外部連携、そしてroot権限の適切な扱いについても深掘りします。
要件と前提
目的
任意のアプリケーションをsystemdサービスとして登録し、自動起動・監視可能にする。
定期実行タスクをsystemdタイマーで管理する。
サービスデプロイプロセスを冪等なシェルスクリプトで自動化する。
実行時のセキュリティを強化し、権限分離を徹底する。
前提条件
systemdが動作するLinux環境(例: CentOS, Ubuntu, Fedoraなど)。
Bashシェルおよび基本的なLinuxコマンドの知識。
curl, jq, systemctl, journalctl コマンドが利用可能であること。
root権限での操作が必要な箇所があることを理解していること。
セキュリティと権限分離の基本原則
systemdサービスは多くの場合、システム起動時にroot権限で実行されますが、アプリケーション本体は可能な限り非特権ユーザーで動作させるべきです。これにより、万が一アプリケーションに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます。具体的には、以下の原則に従います。
最小権限の原則: サービスは必要最低限の権限のみを持つユーザー/グループで実行する。
ファイルシステム保護: ProtectSystem, ProtectHome, PrivateTmpなどのディレクティブで、サービスからアクセス可能なファイルシステム領域を制限する。
権限昇格の禁止: NoNewPrivileges, CapabilityBoundingSet などで、サービスが新たな特権を取得するのを防ぐ。
環境分離: サービス固有の実行環境を構築し、システム全体の環境に影響を与えないようにする。
実装
systemdサービスをデプロイする一般的なフローは以下の通りです。
graph TD
A["サービス定義とスクリプトの作成"] --> B{"冪等なセットアップスクリプト"};
B --> C["アプリバイナリ/設定のダウンロード/配置"];
C --> D["systemdユニットファイルの作成/配置"];
D --> E["systemctl daemon-reload|ユニット設定を再読み込み|"];
E --> F["systemctl enable --now my-app.service|サービスの有効化と起動|"];
F --> G["systemctl status my-app.service|サービスの状態確認|"];
G --> H["ログ確認: journalctl -u my-app.service|標準出力/エラーを確認|"];
H --> I["サービス正常稼働"];
サービス対象アプリケーションの準備
今回は、my-appという架空のGo言語製HTTPサーバーを例に、これをsystemdで管理します。アプリケーションバイナリは /usr/local/bin/my-app に、設定ファイルは /etc/my-app/config.json に配置すると仮定します。
冪等なセットアップスクリプトの作成
デプロイプロセスは、複数回実行しても同じ結果になる「冪等性」を持つべきです。また、シェルスクリプトはエラー耐性を高めるために安全な書き方を採用します。
安全なシェルスクリプトの原則
一時ディレクトリの利用とクリーンアップ
#!/bin/bash
# file: setup.sh
# ----------------------------------------------------
# 安全なシェルスクリプトのヘッダ
# ----------------------------------------------------
set -euo pipefail
# 一時ディレクトリの作成
TMP_DIR=$(mktemp -d -t my-app-deploy-XXXXXX)
# スクリプト終了時に一時ディレクトリを自動削除
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT QUIT TERM
# ログファイル名(JST基準の具体日付 2024年05月15日 を使用)
LOG_FILE="/var/log/my-app-setup_$(date +%Y%m%d_%H%M%S).log"
# 標準出力と標準エラー出力をログファイルとコンソールにリダイレクト
exec > >(tee -a "$LOG_FILE") 2>&1
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚀 my-app サービスセットアップ開始..."
echo "一時ディレクトリ: $TMP_DIR"
# ----------------------------------------------------
# root権限の確認と非特権ユーザーの準備
# ----------------------------------------------------
if [[ "$(id -u)" -ne 0 ]]; then
echo "エラー: このスクリプトはroot権限で実行する必要があります。" >&2
exit 1
fi
APP_USER="my-app-user"
APP_GROUP="my-app-group"
# ユーザーとグループが存在しない場合のみ作成 (冪等性)
if ! id "$APP_USER" &>/dev/null; then
echo "ユーザー $APP_USER を作成中..."
useradd --system --no-create-home --shell /sbin/nologin "$APP_USER"
fi
if ! getent group "$APP_GROUP" &>/dev/null; then
echo "グループ $APP_GROUP を作成中..."
groupadd --system "$APP_GROUP"
fi
# ユーザーをグループに追加 (既に存在しても問題なし)
usermod -a -G "$APP_GROUP" "$APP_USER"
# アプリケーションディレクトリの準備 (冪等性)
APP_DIR="/usr/local/bin"
CONFIG_DIR="/etc/my-app"
LOG_DIR="/var/log/my-app"
mkdir -p "$APP_DIR" "$CONFIG_DIR" "$LOG_DIR"
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR" "$LOG_DIR"
chmod 750 "$CONFIG_DIR" "$LOG_DIR"
# ----------------------------------------------------
# curl と jq を用いた外部API連携の例
# ----------------------------------------------------
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ⚙️ 設定ファイルを取得・生成中..."
# 外部APIから設定情報を取得する例 (ダミーURL)
# --retry: 5回までリトライ
# --retry-delay: リトライ間隔 (秒)
# --retry-max-time: 最大リトライ時間 (秒)
# --cacert: CA証明書を指定してTLS検証を強化
# --show-error: エラー時に詳細を表示
# -f, --fail: サーバーエラー(4xx, 5xx)でも失敗とみなす
# -s, --silent: 進捗表示を抑制
# -S, --show-error: エラー時にはエラーメッセージを表示
CONFIG_JSON_URL="https://api.example.com/v1/config/my-app" # ダミーURL
APP_BIN_URL="https://downloads.example.com/my-app-v1.0.0.tar.gz" # ダミーURL
CA_CERT_PATH="/etc/ssl/certs/ca-certificates.crt" # システムのCA証明書パス
# 設定を取得し、一部をjqで加工
if curl -fsSL --retry 5 --retry-delay 5 --retry-max-time 60 \
--cacert "$CA_CERT_PATH" "$CONFIG_JSON_URL" > "$TMP_DIR/raw_config.json"; then
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 取得した設定を加工中..."
# jqを使用して、設定値の一部をデフォルト値に上書きまたは追加する例
jq '. + { "log_level": "info", "max_connections": 100 }' "$TMP_DIR/raw_config.json" > "$CONFIG_DIR/config.json"
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR/config.json"
chmod 640 "$CONFIG_DIR/config.json"
else
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚨 エラー: 設定ファイルの取得または加工に失敗しました。" >&2
# エラー時はデフォルト設定を使用するなどのフォールバック
echo '{"port": 8080, "log_level": "info", "max_connections": 100}' > "$CONFIG_DIR/config.json"
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR/config.json"
chmod 640 "$CONFIG_DIR/config.json"
fi
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 📥 アプリケーションバイナリをダウンロード中..."
if curl -fsSL --retry 5 --retry-delay 5 --retry-max-time 60 \
--cacert "$CA_CERT_PATH" "$APP_BIN_URL" | tar -xz -C "$TMP_DIR"; then
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] バイナリを配置中..."
mv "$TMP_DIR/my-app" "$APP_DIR/my-app"
chown "$APP_USER:$APP_GROUP" "$APP_DIR/my-app"
chmod 755 "$APP_DIR/my-app"
else
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚨 エラー: アプリケーションバイナリのダウンロードに失敗しました。" >&2
exit 1
fi
# ----------------------------------------------------
# systemdユニットファイルの作成と配置
# ----------------------------------------------------
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 📝 systemdサービスユニットファイルを作成中..."
SERVICE_UNIT_PATH="/etc/systemd/system/my-app.service"
TIMER_UNIT_PATH="/etc/systemd/system/my-app-updater.timer"
UPDATER_SERVICE_UNIT_PATH="/etc/systemd/system/my-app-updater.service"
cat <<EOF > "$SERVICE_UNIT_PATH"
[Unit]
Description=My Application Service
After=network.target
[Service]
ExecStart=/usr/local/bin/my-app -config /etc/my-app/config.json
WorkingDirectory=/usr/local/bin
User=$APP_USER
Group=$APP_GROUP
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-app
# --- セキュリティ強化ディレクティブ ---
# 新しい特権の取得を禁止
NoNewPrivileges=true
# chrootやmountなどの危険なシステムコールを制限
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN CAP_SETUID CAP_SETGID
# /root, /home, /srv, /mediaなどのユーザーディレクトリを保護
ProtectHome=true
# /usr, /boot, /etcへの書き込みを禁止
ProtectSystem=full
# サービス専用の一時ディレクトリ (/tmp, /var/tmp) を提供
PrivateTmp=true
# ファイルシステムの名前空間を分離し、サービスのプロセスから見えるファイルシステムを制限
PrivateDevices=true
# ネットワークインターフェース情報を隠蔽
PrivateNetwork=false # ネットワークアクセスが必要なためfalse
# サービスが実行するプロセスのPID名前空間を分離
PIDFile=/run/my-app.pid # アプリケーションがPIDファイルを生成する場合
# サービスプロセスがアクセスできるリソースを制限
MemoryAccounting=true
CPUAccounting=true
IOAccounting=true
EOF
cat <<EOF > "$UPDATER_SERVICE_UNIT_PATH"
[Unit]
Description=My Application Update Task
Requires=my-app.service
After=my-app.service
[Service]
Type=oneshot
ExecStart=/bin/bash -c "echo 'Running update task...'; /usr/local/bin/my-app --update; echo 'Update task finished.'"
User=$APP_USER
Group=$APP_GROUP
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-app-updater
# タイマーサービスもセキュリティ強化
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
EOF
cat <<EOF > "$TIMER_UNIT_PATH"
[Unit]
Description=Run My Application Update Task daily
Requires=my-app-updater.service
[Timer]
# 毎日午前3時に実行
OnCalendar=*-*-* 03:00:00
# 起動時に実行されていない場合は、次回のスケジュールで実行
Persistent=true
# 実行精度を1分に設定 (デフォルトは1分)
AccuracySec=1min
[Install]
WantedBy=timers.target
EOF
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ✅ ユニットファイルの配置が完了しました。"
# ----------------------------------------------------
# systemdデーモンのリロードとサービスの有効化・起動
# ----------------------------------------------------
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🔄 systemdデーモンをリロード中..."
systemctl daemon-reload
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] 🚀 my-app.service を有効化し、起動中..."
systemctl enable --now my-app.service
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ⏰ my-app-updater.timer を有効化し、起動中..."
systemctl enable --now my-app-updater.timer
echo "[$ (date +%Y-%m-%d_%H:%M:%S)] ✨ セットアップが完了しました。"
systemd サービスユニットファイルの作成
my-app.service は、アプリケーション本体を実行するためのユニットです。
[Unit] セクション: ユニットの一般的な情報と依存関係を定義します。
[Service] セクション: サービス固有の設定を定義します。
ExecStart: サービス起動時に実行されるコマンド。
WorkingDirectory: コマンドが実行されるディレクトリ。
User, Group: サービスを実行するユーザーとグループ。root権限を避けるための重要な設定です。
Restart: サービス終了時の再起動ポリシー(例: on-failure, always)。
RestartSec: 再起動までの待機時間。
LimitNOFILE: オープン可能なファイルディスクリプタの最大数。
StandardOutput, StandardError: 標準出力と標準エラーの転送先(journalはsystemdジャーナルへ)。
セキュリティ強化ディレクティブ:
NoNewPrivileges=true: サービスが新たな特権を取得することを禁止します。
CapabilityBoundingSet: プロセスが持つ可能性のあるLinuxケーパビリティ(特権)を制限します。例: CAP_NET_BIND_SERVICE は1024番以下のポートにバインドする権限。
ProtectHome=true, ProtectSystem=full: /home, /root、/usr, /etc などへのアクセスを制限し、ファイルシステムを保護します。
PrivateTmp=true: サービス専用の一時ディレクトリ (/tmp, /var/tmp) を提供し、システム全体の一時ファイルへのアクセスを防ぎます。
PrivateDevices=true: サービスがデバイスファイルにアクセスするのを防ぎます。
PIDFile: アプリケーションがPIDファイルを生成する場合に指定します。systemdがPIDを管理します。
[Install] セクション: ユニットの有効化(systemctl enable)に関する情報を定義します。
WantedBy: このユニットを有効化した際に、どのターゲットユニットにシンボリックリンクが張られるかを指定します。multi-user.targetは通常システム起動時のターゲットです。
systemd タイマーユニットファイルの作成
my-app-updater.timer は定期的なタスクを実行するためのユニットです。
my-app-updater.service はタイマーによって起動される実際のタスクを定義します。
[Timer] セクション: タイマー固有の設定を定義します。
検証
セットアップスクリプト実行後、以下のコマンドでサービスの状態を確認します。
サービスユニットの起動と状態確認
systemctl status my-app.service
出力例:
● my-app.service - My Application Service
Loaded: loaded (/etc/systemd/system/my-app.service; enabled; vendor preset: disabled)
Active: active (running) since Wed 2024-05-15 10:00:00 JST; 1min 20s ago
Main PID: 1234 (my-app)
Tasks: 6 (limit: 4915)
Memory: 10.5M
CPU: 45ms
CGroup: /system.slice/my-app.service
└─1234 /usr/local/bin/my-app -config /etc/my-app/config.json
Active: active (running) であれば正常に起動しています。
タイマーユニットの起動と状態確認
systemctl status my-app-updater.timer
出力例:
● my-app-updater.timer - Run My Application Update Task daily
Loaded: loaded (/etc/systemd/system/my-app-updater.timer; enabled; vendor preset: disabled)
Active: active (waiting) since Wed 2024-05-15 10:00:00 JST; 1min 30s ago
Trigger: Thu 2024-05-16 03:00:00 JST; 16h left
Triggers: ● my-app-updater.service
Active: active (waiting) で、Trigger に次回の実行日時が表示されていれば正常です。
ログの確認
サービスやタイマーの出力を確認するには journalctl を使用します。
journalctl -u my-app.service
journalctl -u my-app-updater.service
-f オプションを付けてリアルタイムでログを追うこともできます。
journalctl -u my-app.service -f
運用
サービスの一時停止・再起動
systemctl stop my-app.service # 停止
systemctl start my-app.service # 起動
systemctl restart my-app.service # 再起動
ユニットファイルの更新とリロード
ユニットファイルを変更した場合、systemdに設定を再読み込みさせる必要があります。
systemctl daemon-reload # systemdデーモンに設定ファイルの変更を通知
systemctl restart my-app.service # サービスを再起動して新しい設定を適用
自動起動の管理
システム起動時にサービスが自動的に起動するかどうかを設定します。
systemctl enable my-app.service # 自動起動を有効化
systemctl disable my-app.service # 自動起動を無効化
トラブルシューティング
サービスが起動しない場合
systemctl status で確認: 最も重要なコマンドです。エラーメッセージ、PID、CGroup情報から手がかりを得ます。
journalctl -u でログを確認: アプリケーションが出力するログや、systemd自体のエラーメッセージを確認します。
ユニットファイルの構文エラー: systemctl daemon-reload 時にエラーが出力されることがあります。systemd-analyze verify <unit_file> で構文チェックが可能です。
ログからの原因特定
journalctl は非常に強力なツールです。
journalctl -u my-app.service --since "1 hour ago": 過去1時間分のログ。
journalctl -u my-app.service -p err..crit: エラー以上の深刻度を持つログのみ表示。
journalctl -e -u my-app.service: 最新のログを最後まで表示。
systemd-analyze の活用
まとめ
、systemdサービスユニットとタイマーユニットの作成・管理におけるDevOpsベストプラクティスを解説しました。冪等なシェルスクリプトを用いた安全なデプロイ、curlとjqを活用した設定管理、そしてUserディレクティブやセキュリティ強化ディレクティブによる厳格な権限分離は、堅牢なシステム運用に不可欠です。これらの原則を適用することで、サービスの信頼性とセキュリティを向上させ、トラブルシューティングの効率化にも繋がります。 systemdの機能を最大限に活用し、安定したアプリケーション運用を目指しましょう。
コメント