systemdサービスユニットファイルの作成と安全な運用

Tech

本記事は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のため、サービスが終了すると自動的にその一時ディレクトリは削除されます。

運用

サービスユニットファイルの更新手順

サービスユニットファイルやスクリプトの内容を変更した場合、以下の手順で変更を反映します。

  1. サービスユニットファイルやスクリプトを更新。

  2. systemdに新しい設定を読み込ませる:

    sudo systemctl daemon-reload
    
  3. サービスを再起動(タイマーで起動するサービスの場合、通常は不要ですが、変更を即座に適用したい場合や手動起動時に実行):

    sudo systemctl restart my-api-processor.service
    
  4. タイマーの設定を変更した場合は、タイマーも再起動します:

    sudo systemctl restart my-api-processor.timer
    

ログの監視と管理

journalctlを使用してログを定期的に監視します。ログのディスク使用量を制限するには、/etc/systemd/journald.confSystemMaxUseなどの設定を調整します。

セキュリティパッチとシステム更新

基盤となるLinuxシステム、systemd、および関連するパッケージ(curl, jqなど)を常に最新の状態に保ち、セキュリティパッチを適用することが重要です。

トラブルシュート

1. サービスが起動しない、または即座に終了する

  • ログの確認: 最も重要なのはjournalctlでログを確認することです。

    sudo journalctl -xeu my-api-processor.service
    

    -xオプションは詳細な説明を、-eオプションは最新のログから表示し、-uでユニットを指定します。

  • ユニットファイルの構文エラー: sudo systemctl status my-api-processor.serviceの出力にfailedError: ...が含まれる場合、ユニットファイルの構文エラーが考えられます。

  • スクリプトのエラー: ExecStartで指定したスクリプトが、存在しない、実行権限がない、または内部でエラーを起こして終了している可能性があります。スクリプトを単独で手動実行してデバッグすることも有効です。

  • 権限問題: User, Groupディレクティブで指定したユーザーが存在しない、またはスクリプトがアクセスしようとしているファイルやディレクトリへの権限がない場合。

2. タイマーが機能しない

  • タイマーの有効化と起動: sudo systemctl status my-api-processor.timerActive: 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 pipefailtrapを用いたエラーハンドリング、mktemp -dによる一時ファイルの安全な管理が重要です。

  • 権限分離: サービスはroot権限でインストールされますが、実行は必ずUserおよびGroupディレクティブで指定した非特権ユーザーで行い、セキュリティリスクを最小限に抑えるべきです。PrivateTmp, ProtectSystem, ProtectHome, NoNewPrivilegesといったsystemdのセキュリティディレクティブを最大限に活用してください。

  • 効率的な連携: curlの再試行機能やjqによるJSON処理を活用することで、外部サービスとの連携を堅牢にできます。

  • 運用とトラブルシューティング: systemctljournalctlコマンドを使いこなすことで、サービスの監視、更新、問題解決が容易になります。

systemdを適切に活用することで、Linuxシステム上のバックグラウンド処理をより信頼性高く、安全に管理できるようになります。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました