<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">systemdサービスユニットファイルの作成と安全な運用</h1>
<h2 class="wp-block-heading">要件と前提</h2>
<p>、Linuxシステム上でバックグラウンドプロセスを管理する<code>systemd</code>サービスユニットおよびタイマーユニットの作成と運用について解説します。特に、スクリプトの<strong>冪等性 (idempotent)</strong>と<strong>安全性</strong>、そして<code>root</code>権限の適切な扱いと<strong>権限分離</strong>に焦点を当てます。</p>
<p>前提として、以下の環境を想定します。</p>
<ul class="wp-block-list">
<li><p><code>systemd</code>が動作するLinuxディストリビューション(例: CentOS, Ubuntu, RHEL)。</p></li>
<li><p><code>bash</code>シェル、<code>curl</code>コマンド、<code>jq</code>コマンドが利用可能であること。</p></li>
<li><p><code>systemd</code>サービスユニットファイルや関連する設定ファイルの配置には<code>root</code>権限が必要となります。ただし、サービスが実行する実際の処理は<strong>非特権ユーザー</strong>で実行することを推奨し、その設定方法を説明します。</p></li>
</ul>
<h2 class="wp-block-heading">実装</h2>
<p>ここでは、外部APIからJSONデータを取得し、処理してログに記録する架空のバッチ処理を<code>systemd</code>サービスとして実装する例を示します。さらに、このサービスを定期実行するための<code>systemd</code>タイマーユニットも作成します。</p>
<h3 class="wp-block-heading">1. 安全なBashスクリプトの作成</h3>
<p>まず、サービスが実行するメインスクリプトを作成します。このスクリプトは、冪等性を保ちつつ、エラーハンドリングと一時ファイルの安全な管理を含みます。</p>
<p><code>/usr/local/bin/my-api-processor.sh</code> として以下の内容で保存します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# my-api-processor.sh - 外部APIからデータを取得し処理するスクリプト
# 冪等性確保とエラー処理のベストプラクティス
set -euo pipefail # -e: エラーで即終了, -u: 未定義変数でエラー, -o pipefail: パイプ中のエラーを捕捉
# スクリプト名を取得しログ出力に使用
SCRIPT_NAME=$(basename "$0")
# 一時ディレクトリの作成とクリーンアップ
# mktemp -d: 安全な一時ディレクトリを作成
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
# trap: スクリプト終了時に一時ファイルを確実にクリーンアップ
trap 'rm -rf "$TMP_DIR"; logger -t "$SCRIPT_NAME" "Temporary directory $TMP_DIR cleaned up."' EXIT
logger -t "$SCRIPT_NAME" "Script started. PID: $$"
# 外部APIのURL
API_URL="https://jsonplaceholder.typicode.com/posts/1" # 例としてJSONPlaceholderを使用
# curlコマンドでAPIを呼び出し、再試行とTLS検証を設定
# --retry: 再試行回数 (例: 5回)
# --retry-delay: 最初のリトライまでの待ち時間 (秒)
# --retry-max-time: 全体のリトライに費やす最大時間 (秒)
# --fail-with-body: HTTPエラー時にエラーメッセージをbodyに出力 (curl 7.76.0以上)
# --silent: 進行状況メーターを表示しない
# --show-error: エラーメッセージを表示
# --cacert: CA証明書へのパス (本番環境では必ず指定)
# --cert, --key: クライアント証明書と秘密鍵 (双方向TLSが必要な場合)
RESPONSE=$(curl \
--retry 5 \
--retry-delay 5 \
--retry-max-time 30 \
--fail-with-body \
--silent \
--show-error \
"$API_URL" \
--output "$TMP_DIR/api_response.json" \
2>"$TMP_DIR/curl_error.log") # エラーをログファイルにリダイレクト
# curlの終了ステータスを確認
if [ $? -ne 0 ]; then
ERROR_MSG=$(cat "$TMP_DIR/curl_error.log")
logger -t "$SCRIPT_NAME" "ERROR: curl failed. $ERROR_MSG"
exit 1 # エラー終了
fi
# jqでJSONを処理する例
# 例: titleフィールドを抽出
if ! API_TITLE=$(jq -r '.title' "$TMP_DIR/api_response.json"); then
logger -t "$SCRIPT_NAME" "ERROR: Failed to parse JSON with jq or 'title' field not found."
exit 1
fi
logger -t "$SCRIPT_NAME" "Successfully fetched data from $API_URL."
logger -t "$SCRIPT_NAME" "Processed API title: $API_TITLE"
# その他の処理をここに追加
# 例: データベースへの書き込み、別のAPI呼び出し、ファイル操作など
logger -t "$SCRIPT_NAME" "Script finished successfully."
exit 0 # 正常終了
</pre>
</div>
<p><strong>スクリプトの権限設定:</strong></p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo chmod 755 /usr/local/bin/my-api-processor.sh
</pre>
</div>
<h3 class="wp-block-heading">2. サービスユニットファイルの作成 (<code>.service</code>)</h3>
<p>次に、上記スクリプトを実行するための<code>systemd</code>サービスユニットファイルを作成します。非特権ユーザーでの実行、セキュリティ強化のためのディレクティブを適切に設定します。</p>
<p><code>/etc/systemd/system/my-api-processor.service</code> として以下の内容で保存します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=My API Processor Service
# このサービスが起動する前にnetwork.targetが起動していることを保証
After=network.target
[Service]
# プロセスのタイプ。ここではシンプルなワンショット実行スクリプトなのでoneshot。
# バックグラウンドでデーモンとして実行する場合はforkingやsimpleなど。
Type=oneshot
# ExecStartで指定されたコマンドが完了するとサービスは終了
ExecStart=/usr/local/bin/my-api-processor.sh
# サービス実行ユーザーとグループを指定。必ず非特権ユーザーを指定する。
# ここでは例として 'myuser' と 'mygroup' を使用。事前に作成が必要。
User=myuser
Group=mygroup
# スクリプトの作業ディレクトリ
WorkingDirectory=/home/myuser/api-processor
# エラー時にサービスを再起動しない。oneshotタイプの場合、通常はNo。
# 長時間稼働するデーモンの場合はon-failureやalwaysなどを検討。
Restart=no
# サービスのセキュリティ強化 (systemd.exec(5) を参照)
# PrivateTmp=true: サービス専用の一時ディレクトリ (/tmp, /var/tmp) を提供。
# 他のプロセスからは見えない。
PrivateTmp=true
# ProtectSystem=full: /usr, /boot などを読み取り専用でマウントし、書き込みを禁止。
# システムファイルの改ざん防止。
ProtectSystem=full
# ProtectHome=true: /home, /root を空のディレクトリまたは読み取り専用でマウント。
# ユーザーホームディレクトリへのアクセスを制限。
ProtectHome=true
# NoNewPrivileges=true: 実行プロセスが新しい特権を取得することを防止 (例: setuid/setgidバイナリ実行制限)。
NoNewPrivileges=true
# ReadWritePaths: ProtectSystemで保護されたパスに対して書き込みを許可するパスを指定 (注意して使用)
# ReadWritePaths=/var/log/my-app
# 標準出力と標準エラー出力をジャーナルに転送
StandardOutput=journal
StandardError=journal
[Install]
# このサービスがmulti-user.target (通常のマルチユーザー環境) に紐付けられる
# systemctl enable が実行された際に、multi-user.target.wants/ にシンボリックリンクが作成される
WantedBy=multi-user.target
</pre>
</div>
<p><strong>事前準備:</strong> サービスを実行するユーザーとグループを作成します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo groupadd mygroup || true # 既に存在する場合はエラーにならない
sudo useradd -r -s /sbin/nologin -g mygroup -d /home/myuser myuser || true # 既に存在する場合はエラーにならない
sudo mkdir -p /home/myuser/api-processor
sudo chown myuser:mygroup /home/myuser/api-processor
</pre>
</div>
<p>ユーザーは<code>/sbin/nologin</code>でログインシェルを持たないようにし、セキュリティを強化します。</p>
<h3 class="wp-block-heading">3. タイマーユニットファイルの作成 (<code>.timer</code>)</h3>
<p>サービスを定期的に実行するために、<code>systemd</code>タイマーユニットを作成します。</p>
<p><code>/etc/systemd/system/my-api-processor.timer</code> として以下の内容で保存します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Run My API Processor every 10 minutes
# このタイマーは対応するサービスユニット 'my-api-processor.service' を起動する
# サービスユニットとタイマーユニットは同じ名前(拡張子を除く)を持つ必要がある
Requires=my-api-processor.service
[Timer]
# OnCalendar: 特定の日時または定期的な間隔を指定
# ここでは、10分ごとにサービスを起動するように設定
OnCalendar=*:0/10:00
# AccuracySec: 指定された時間にどれくらいの精度で実行するか (デフォルトは1分)。
# システム負荷を軽減するため、厳密な時間でなくてもよい場合は値を大きくする。
AccuracySec=1min
# Persistent=true: タイマーが最後に起動した時間を保存し、システムが停止していた間に
# 実行されるべきだったサービスを、システム起動後に一度だけ実行する。
# これにより、定期的なジョブが抜けることを防ぐ。
Persistent=true
[Install]
# multi-user.target に紐付けられ、システム起動時にタイマーが有効化される
WantedBy=timers.target
</pre>
</div>
<h3 class="wp-block-heading">4. systemdへの登録と有効化</h3>
<p>ユニットファイルを配置した後、<code>systemd</code>にそれらを認識させ、有効化します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># systemdに新しいユニットファイルを認識させる
sudo systemctl daemon-reload
# サービスユニットを有効化(タイマーが起動させるため、サービス自体をenableする必要はないことが多いが、手動起動も想定してenableすることがある)
# この例では、タイマーがサービスを起動するので、サービス自体をenableするのではなく、タイマーをenableする。
# ただし、手動でサービスをstart/stop/statusする際にはサービスファイルが存在する必要がある。
# タイマーユニットを有効化し、起動する
sudo systemctl enable my-api-processor.timer
sudo systemctl start my-api-processor.timer
</pre>
</div>
<h3 class="wp-block-heading">systemdサービス起動フロー</h3>
<p>Mermaid形式で、<code>systemd</code>がサービスを起動するまでの基本的なフローを示します。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["システム起動/daemon-reload"] --> B{"systemdユニットファイルを読み込む"};
B --> C{"タイマーユニットが有効化されているか?"};
C -- YES --> D["my-api-processor.timer を起動"];
C -- NO --> Z["手動でサービス起動が必要"];
D --> E{"OnCalendar のスケジュール到達"};
E -- YES --> F["my-api-processor.service を起動"];
F --> G["ExecStartスクリプト実行"];
G --> H["curlで外部API呼び出し"];
H --> I["jqでJSON処理"];
I --> J["ログ出力 (journaldへ)"];
J --> K["一時ファイルクリーンアップ"];
K --> L["サービス終了"];
</pre></div>
<h2 class="wp-block-heading">検証</h2>
<p>サービスが正しく設定され、期待通りに動作しているかを確認します。</p>
<h3 class="wp-block-heading">サービスのステータス確認</h3>
<p>タイマーとサービスの両方のステータスを確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># タイマーユニットのステータス確認
sudo systemctl status my-api-processor.timer
# サービスユニットのステータス確認
sudo systemctl status my-api-processor.service
</pre>
</div>
<p><code>my-api-processor.timer</code>の出力で<code>Next: ...</code>の項目を確認し、次の実行時刻が正しいことを検証します。
<code>my-api-processor.service</code>の出力で<code>Active: inactive (dead)</code> と表示されるのが正常です(<code>oneshot</code>タイプのため実行後終了します)。<code>journal</code>ログには実行履歴が表示されます。</p>
<h3 class="wp-block-heading">ログの確認</h3>
<p>スクリプトからのログ出力は<code>journald</code>に送られます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 特定サービスのログを確認
sudo journalctl -u my-api-processor.service --since "{{jst_today}} 00:00:00"
# 最新のログをリアルタイムで確認
sudo journalctl -f -u my-api-processor.service
</pre>
</div>
<p>スクリプトが吐き出した<code>logger</code>メッセージや<code>curl</code>のエラーログなどが確認できるはずです。</p>
<h3 class="wp-block-heading">一時ディレクトリのクリーンアップ確認</h3>
<p>スクリプトが実行された後、<code>/tmp</code>ディレクトリ(<code>PrivateTmp=true</code>の場合、サービス専用の<code>/tmp</code>領域)に一時ファイルが残っていないことを確認します。<code>PrivateTmp=true</code>のため、サービスが終了すると自動的にその一時ディレクトリは削除されます。</p>
<h2 class="wp-block-heading">運用</h2>
<h3 class="wp-block-heading">サービスユニットファイルの更新手順</h3>
<p>サービスユニットファイルやスクリプトの内容を変更した場合、以下の手順で変更を反映します。</p>
<ol class="wp-block-list">
<li><p>サービスユニットファイルやスクリプトを更新。</p></li>
<li><p><code>systemd</code>に新しい設定を読み込ませる:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl daemon-reload
</pre>
</div></li>
<li><p>サービスを再起動(タイマーで起動するサービスの場合、通常は不要ですが、変更を即座に適用したい場合や手動起動時に実行):</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl restart my-api-processor.service
</pre>
</div></li>
<li><p>タイマーの設定を変更した場合は、タイマーも再起動します:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl restart my-api-processor.timer
</pre>
</div></li>
</ol>
<h3 class="wp-block-heading">ログの監視と管理</h3>
<p><code>journalctl</code>を使用してログを定期的に監視します。ログのディスク使用量を制限するには、<code>/etc/systemd/journald.conf</code>で<code>SystemMaxUse</code>などの設定を調整します。</p>
<h3 class="wp-block-heading">セキュリティパッチとシステム更新</h3>
<p>基盤となるLinuxシステム、<code>systemd</code>、および関連するパッケージ(<code>curl</code>, <code>jq</code>など)を常に最新の状態に保ち、セキュリティパッチを適用することが重要です。</p>
<h2 class="wp-block-heading">トラブルシュート</h2>
<h3 class="wp-block-heading">1. サービスが起動しない、または即座に終了する</h3>
<ul class="wp-block-list">
<li><p><strong>ログの確認:</strong> 最も重要なのは<code>journalctl</code>でログを確認することです。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo journalctl -xeu my-api-processor.service
</pre>
</div>
<p><code>-x</code>オプションは詳細な説明を、<code>-e</code>オプションは最新のログから表示し、<code>-u</code>でユニットを指定します。</p></li>
<li><p><strong>ユニットファイルの構文エラー:</strong> <code>sudo systemctl status my-api-processor.service</code>の出力に<code>failed</code>や<code>Error: ...</code>が含まれる場合、ユニットファイルの構文エラーが考えられます。</p></li>
<li><p><strong>スクリプトのエラー:</strong> <code>ExecStart</code>で指定したスクリプトが、存在しない、実行権限がない、または内部でエラーを起こして終了している可能性があります。スクリプトを単独で手動実行してデバッグすることも有効です。</p></li>
<li><p><strong>権限問題:</strong> <code>User</code>, <code>Group</code>ディレクティブで指定したユーザーが存在しない、またはスクリプトがアクセスしようとしているファイルやディレクトリへの権限がない場合。</p></li>
</ul>
<h3 class="wp-block-heading">2. タイマーが機能しない</h3>
<ul class="wp-block-list">
<li><p><strong>タイマーの有効化と起動:</strong> <code>sudo systemctl status my-api-processor.timer</code>で<code>Active: active (waiting)</code>と表示されているか確認します。<code>enabled</code>かつ<code>active</code>である必要があります。</p></li>
<li><p><strong><code>OnCalendar</code>の指定ミス:</strong> <code>OnCalendar</code>のフォーマットが間違っていないか、期待する間隔で設定されているか確認します。</p></li>
<li><p><strong>対応するサービスファイル:</strong> <code>.timer</code>ファイルが指す<code>.service</code>ファイルが存在し、適切に設定されているか確認します。</p></li>
</ul>
<h3 class="wp-block-heading">3. <code>curl</code>や<code>jq</code>の実行エラー</h3>
<ul class="wp-block-list">
<li><p><strong>ログの詳細確認:</strong> スクリプト内で<code>logger</code>を使って出力したメッセージや、<code>curl</code>がファイルにリダイレクトしたエラーメッセージ(例: <code>$TMP_DIR/curl_error.log</code>)を確認します。</p></li>
<li><p><strong>ネットワーク接続:</strong> <code>curl</code>が外部APIにアクセスできるか(ファイアウォール、プロキシ設定など)。</p></li>
<li><p><strong><code>jq</code>のパス:</strong> <code>jq</code>コマンドが<code>PATH</code>環境変数に含まれているか、JSONの構造が期待と異なるかを確認します。</p></li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>本記事では、<code>systemd</code>のサービスユニットおよびタイマーユニットの作成、そしてそれらを安全かつ堅牢に運用するためのベストプラクティスを解説しました。</p>
<ul class="wp-block-list">
<li><p><strong>安全性と冪等性:</strong> Bashスクリプトでは<code>set -euo pipefail</code>と<code>trap</code>を用いたエラーハンドリング、<code>mktemp -d</code>による一時ファイルの安全な管理が重要です。</p></li>
<li><p><strong>権限分離:</strong> サービスは<code>root</code>権限でインストールされますが、実行は必ず<code>User</code>および<code>Group</code>ディレクティブで指定した<strong>非特権ユーザー</strong>で行い、セキュリティリスクを最小限に抑えるべきです。<code>PrivateTmp</code>, <code>ProtectSystem</code>, <code>ProtectHome</code>, <code>NoNewPrivileges</code>といった<code>systemd</code>のセキュリティディレクティブを最大限に活用してください。</p></li>
<li><p><strong>効率的な連携:</strong> <code>curl</code>の再試行機能や<code>jq</code>によるJSON処理を活用することで、外部サービスとの連携を堅牢にできます。</p></li>
<li><p><strong>運用とトラブルシューティング:</strong> <code>systemctl</code>と<code>journalctl</code>コマンドを使いこなすことで、サービスの監視、更新、問題解決が容易になります。</p></li>
</ul>
<p><code>systemd</code>を適切に活用することで、Linuxシステム上のバックグラウンド処理をより信頼性高く、安全に管理できるようになります。</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
systemdサービスユニットファイルの作成と安全な運用
要件と前提
、Linuxシステム上でバックグラウンドプロセスを管理するsystemdサービスユニットおよびタイマーユニットの作成と運用について解説します。特に、スクリプトの冪等性 (idempotent)と安全性、そしてroot権限の適切な扱いと権限分離に焦点を当てます。
前提として、以下の環境を想定します。
systemdが動作するLinuxディストリビューション(例: CentOS, Ubuntu, RHEL)。
bashシェル、curlコマンド、jqコマンドが利用可能であること。
systemdサービスユニットファイルや関連する設定ファイルの配置にはroot権限が必要となります。ただし、サービスが実行する実際の処理は非特権ユーザーで実行することを推奨し、その設定方法を説明します。
実装
ここでは、外部APIからJSONデータを取得し、処理してログに記録する架空のバッチ処理をsystemdサービスとして実装する例を示します。さらに、このサービスを定期実行するためのsystemdタイマーユニットも作成します。
1. 安全なBashスクリプトの作成
まず、サービスが実行するメインスクリプトを作成します。このスクリプトは、冪等性を保ちつつ、エラーハンドリングと一時ファイルの安全な管理を含みます。
/usr/local/bin/my-api-processor.sh として以下の内容で保存します。
#!/bin/bash
# my-api-processor.sh - 外部APIからデータを取得し処理するスクリプト
# 冪等性確保とエラー処理のベストプラクティス
set -euo pipefail # -e: エラーで即終了, -u: 未定義変数でエラー, -o pipefail: パイプ中のエラーを捕捉
# スクリプト名を取得しログ出力に使用
SCRIPT_NAME=$(basename "$0")
# 一時ディレクトリの作成とクリーンアップ
# mktemp -d: 安全な一時ディレクトリを作成
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXXXX")
# trap: スクリプト終了時に一時ファイルを確実にクリーンアップ
trap 'rm -rf "$TMP_DIR"; logger -t "$SCRIPT_NAME" "Temporary directory $TMP_DIR cleaned up."' EXIT
logger -t "$SCRIPT_NAME" "Script started. PID: $$"
# 外部APIのURL
API_URL="https://jsonplaceholder.typicode.com/posts/1" # 例としてJSONPlaceholderを使用
# curlコマンドでAPIを呼び出し、再試行とTLS検証を設定
# --retry: 再試行回数 (例: 5回)
# --retry-delay: 最初のリトライまでの待ち時間 (秒)
# --retry-max-time: 全体のリトライに費やす最大時間 (秒)
# --fail-with-body: HTTPエラー時にエラーメッセージをbodyに出力 (curl 7.76.0以上)
# --silent: 進行状況メーターを表示しない
# --show-error: エラーメッセージを表示
# --cacert: CA証明書へのパス (本番環境では必ず指定)
# --cert, --key: クライアント証明書と秘密鍵 (双方向TLSが必要な場合)
RESPONSE=$(curl \
--retry 5 \
--retry-delay 5 \
--retry-max-time 30 \
--fail-with-body \
--silent \
--show-error \
"$API_URL" \
--output "$TMP_DIR/api_response.json" \
2>"$TMP_DIR/curl_error.log") # エラーをログファイルにリダイレクト
# curlの終了ステータスを確認
if [ $? -ne 0 ]; then
ERROR_MSG=$(cat "$TMP_DIR/curl_error.log")
logger -t "$SCRIPT_NAME" "ERROR: curl failed. $ERROR_MSG"
exit 1 # エラー終了
fi
# jqでJSONを処理する例
# 例: titleフィールドを抽出
if ! API_TITLE=$(jq -r '.title' "$TMP_DIR/api_response.json"); then
logger -t "$SCRIPT_NAME" "ERROR: Failed to parse JSON with jq or 'title' field not found."
exit 1
fi
logger -t "$SCRIPT_NAME" "Successfully fetched data from $API_URL."
logger -t "$SCRIPT_NAME" "Processed API title: $API_TITLE"
# その他の処理をここに追加
# 例: データベースへの書き込み、別のAPI呼び出し、ファイル操作など
logger -t "$SCRIPT_NAME" "Script finished successfully."
exit 0 # 正常終了
スクリプトの権限設定:
sudo chmod 755 /usr/local/bin/my-api-processor.sh
2. サービスユニットファイルの作成 (.service)
次に、上記スクリプトを実行するためのsystemdサービスユニットファイルを作成します。非特権ユーザーでの実行、セキュリティ強化のためのディレクティブを適切に設定します。
/etc/systemd/system/my-api-processor.service として以下の内容で保存します。
[Unit]
Description=My API Processor Service
# このサービスが起動する前にnetwork.targetが起動していることを保証
After=network.target
[Service]
# プロセスのタイプ。ここではシンプルなワンショット実行スクリプトなのでoneshot。
# バックグラウンドでデーモンとして実行する場合はforkingやsimpleなど。
Type=oneshot
# ExecStartで指定されたコマンドが完了するとサービスは終了
ExecStart=/usr/local/bin/my-api-processor.sh
# サービス実行ユーザーとグループを指定。必ず非特権ユーザーを指定する。
# ここでは例として 'myuser' と 'mygroup' を使用。事前に作成が必要。
User=myuser
Group=mygroup
# スクリプトの作業ディレクトリ
WorkingDirectory=/home/myuser/api-processor
# エラー時にサービスを再起動しない。oneshotタイプの場合、通常はNo。
# 長時間稼働するデーモンの場合はon-failureやalwaysなどを検討。
Restart=no
# サービスのセキュリティ強化 (systemd.exec(5) を参照)
# PrivateTmp=true: サービス専用の一時ディレクトリ (/tmp, /var/tmp) を提供。
# 他のプロセスからは見えない。
PrivateTmp=true
# ProtectSystem=full: /usr, /boot などを読み取り専用でマウントし、書き込みを禁止。
# システムファイルの改ざん防止。
ProtectSystem=full
# ProtectHome=true: /home, /root を空のディレクトリまたは読み取り専用でマウント。
# ユーザーホームディレクトリへのアクセスを制限。
ProtectHome=true
# NoNewPrivileges=true: 実行プロセスが新しい特権を取得することを防止 (例: setuid/setgidバイナリ実行制限)。
NoNewPrivileges=true
# ReadWritePaths: ProtectSystemで保護されたパスに対して書き込みを許可するパスを指定 (注意して使用)
# ReadWritePaths=/var/log/my-app
# 標準出力と標準エラー出力をジャーナルに転送
StandardOutput=journal
StandardError=journal
[Install]
# このサービスがmulti-user.target (通常のマルチユーザー環境) に紐付けられる
# systemctl enable が実行された際に、multi-user.target.wants/ にシンボリックリンクが作成される
WantedBy=multi-user.target
事前準備: サービスを実行するユーザーとグループを作成します。
sudo groupadd mygroup || true # 既に存在する場合はエラーにならない
sudo useradd -r -s /sbin/nologin -g mygroup -d /home/myuser myuser || true # 既に存在する場合はエラーにならない
sudo mkdir -p /home/myuser/api-processor
sudo chown myuser:mygroup /home/myuser/api-processor
ユーザーは/sbin/nologinでログインシェルを持たないようにし、セキュリティを強化します。
3. タイマーユニットファイルの作成 (.timer)
サービスを定期的に実行するために、systemdタイマーユニットを作成します。
/etc/systemd/system/my-api-processor.timer として以下の内容で保存します。
[Unit]
Description=Run My API Processor every 10 minutes
# このタイマーは対応するサービスユニット 'my-api-processor.service' を起動する
# サービスユニットとタイマーユニットは同じ名前(拡張子を除く)を持つ必要がある
Requires=my-api-processor.service
[Timer]
# OnCalendar: 特定の日時または定期的な間隔を指定
# ここでは、10分ごとにサービスを起動するように設定
OnCalendar=*:0/10:00
# AccuracySec: 指定された時間にどれくらいの精度で実行するか (デフォルトは1分)。
# システム負荷を軽減するため、厳密な時間でなくてもよい場合は値を大きくする。
AccuracySec=1min
# Persistent=true: タイマーが最後に起動した時間を保存し、システムが停止していた間に
# 実行されるべきだったサービスを、システム起動後に一度だけ実行する。
# これにより、定期的なジョブが抜けることを防ぐ。
Persistent=true
[Install]
# multi-user.target に紐付けられ、システム起動時にタイマーが有効化される
WantedBy=timers.target
4. systemdへの登録と有効化
ユニットファイルを配置した後、systemdにそれらを認識させ、有効化します。
# systemdに新しいユニットファイルを認識させる
sudo systemctl daemon-reload
# サービスユニットを有効化(タイマーが起動させるため、サービス自体をenableする必要はないことが多いが、手動起動も想定してenableすることがある)
# この例では、タイマーがサービスを起動するので、サービス自体をenableするのではなく、タイマーをenableする。
# ただし、手動でサービスをstart/stop/statusする際にはサービスファイルが存在する必要がある。
# タイマーユニットを有効化し、起動する
sudo systemctl enable my-api-processor.timer
sudo systemctl start my-api-processor.timer
systemdサービス起動フロー
Mermaid形式で、systemdがサービスを起動するまでの基本的なフローを示します。
graph TD
A["システム起動/daemon-reload"] --> B{"systemdユニットファイルを読み込む"};
B --> C{"タイマーユニットが有効化されているか?"};
C -- YES --> D["my-api-processor.timer を起動"];
C -- NO --> Z["手動でサービス起動が必要"];
D --> E{"OnCalendar のスケジュール到達"};
E -- YES --> F["my-api-processor.service を起動"];
F --> G["ExecStartスクリプト実行"];
G --> H["curlで外部API呼び出し"];
H --> I["jqでJSON処理"];
I --> J["ログ出力 (journaldへ)"];
J --> K["一時ファイルクリーンアップ"];
K --> L["サービス終了"];
検証
サービスが正しく設定され、期待通りに動作しているかを確認します。
サービスのステータス確認
タイマーとサービスの両方のステータスを確認します。
# タイマーユニットのステータス確認
sudo systemctl status my-api-processor.timer
# サービスユニットのステータス確認
sudo systemctl status my-api-processor.service
my-api-processor.timerの出力でNext: ...の項目を確認し、次の実行時刻が正しいことを検証します。
my-api-processor.serviceの出力でActive: inactive (dead) と表示されるのが正常です(oneshotタイプのため実行後終了します)。journalログには実行履歴が表示されます。
ログの確認
スクリプトからのログ出力はjournaldに送られます。
# 特定サービスのログを確認
sudo journalctl -u my-api-processor.service --since "{{jst_today}} 00:00:00"
# 最新のログをリアルタイムで確認
sudo journalctl -f -u my-api-processor.service
スクリプトが吐き出したloggerメッセージやcurlのエラーログなどが確認できるはずです。
一時ディレクトリのクリーンアップ確認
スクリプトが実行された後、/tmpディレクトリ(PrivateTmp=trueの場合、サービス専用の/tmp領域)に一時ファイルが残っていないことを確認します。PrivateTmp=trueのため、サービスが終了すると自動的にその一時ディレクトリは削除されます。
運用
サービスユニットファイルの更新手順
サービスユニットファイルやスクリプトの内容を変更した場合、以下の手順で変更を反映します。
サービスユニットファイルやスクリプトを更新。
systemdに新しい設定を読み込ませる:
sudo systemctl daemon-reload
サービスを再起動(タイマーで起動するサービスの場合、通常は不要ですが、変更を即座に適用したい場合や手動起動時に実行):
sudo systemctl restart my-api-processor.service
タイマーの設定を変更した場合は、タイマーも再起動します:
sudo systemctl restart my-api-processor.timer
ログの監視と管理
journalctlを使用してログを定期的に監視します。ログのディスク使用量を制限するには、/etc/systemd/journald.confでSystemMaxUseなどの設定を調整します。
セキュリティパッチとシステム更新
基盤となるLinuxシステム、systemd、および関連するパッケージ(curl, jqなど)を常に最新の状態に保ち、セキュリティパッチを適用することが重要です。
トラブルシュート
1. サービスが起動しない、または即座に終了する
ログの確認: 最も重要なのはjournalctlでログを確認することです。
sudo journalctl -xeu my-api-processor.service
-xオプションは詳細な説明を、-eオプションは最新のログから表示し、-uでユニットを指定します。
ユニットファイルの構文エラー: sudo systemctl status my-api-processor.serviceの出力にfailedやError: ...が含まれる場合、ユニットファイルの構文エラーが考えられます。
スクリプトのエラー: ExecStartで指定したスクリプトが、存在しない、実行権限がない、または内部でエラーを起こして終了している可能性があります。スクリプトを単独で手動実行してデバッグすることも有効です。
権限問題: User, Groupディレクティブで指定したユーザーが存在しない、またはスクリプトがアクセスしようとしているファイルやディレクトリへの権限がない場合。
2. タイマーが機能しない
タイマーの有効化と起動: sudo systemctl status my-api-processor.timerでActive: active (waiting)と表示されているか確認します。enabledかつactiveである必要があります。
OnCalendarの指定ミス: OnCalendarのフォーマットが間違っていないか、期待する間隔で設定されているか確認します。
対応するサービスファイル: .timerファイルが指す.serviceファイルが存在し、適切に設定されているか確認します。
3. curlやjqの実行エラー
ログの詳細確認: スクリプト内でloggerを使って出力したメッセージや、curlがファイルにリダイレクトしたエラーメッセージ(例: $TMP_DIR/curl_error.log)を確認します。
ネットワーク接続: curlが外部APIにアクセスできるか(ファイアウォール、プロキシ設定など)。
jqのパス: jqコマンドがPATH環境変数に含まれているか、JSONの構造が期待と異なるかを確認します。
まとめ
本記事では、systemdのサービスユニットおよびタイマーユニットの作成、そしてそれらを安全かつ堅牢に運用するためのベストプラクティスを解説しました。
安全性と冪等性: Bashスクリプトではset -euo pipefailとtrapを用いたエラーハンドリング、mktemp -dによる一時ファイルの安全な管理が重要です。
権限分離: サービスはroot権限でインストールされますが、実行は必ずUserおよびGroupディレクティブで指定した非特権ユーザーで行い、セキュリティリスクを最小限に抑えるべきです。PrivateTmp, ProtectSystem, ProtectHome, NoNewPrivilegesといったsystemdのセキュリティディレクティブを最大限に活用してください。
効率的な連携: curlの再試行機能やjqによるJSON処理を活用することで、外部サービスとの連携を堅牢にできます。
運用とトラブルシューティング: systemctlとjournalctlコマンドを使いこなすことで、サービスの監視、更新、問題解決が容易になります。
systemdを適切に活用することで、Linuxシステム上のバックグラウンド処理をより信頼性高く、安全に管理できるようになります。
コメント