systemdの.timerと.pathユニットによるイベント駆動型自動化

Tech

systemdの.timerと.pathユニットによるイベント駆動型自動化

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

1. 要件と前提

1.1. 要件

  • systemdの.timer.pathユニットを用いたイベント駆動型自動化の実装。

  • 安全なBashスクリプトの採用(set -euo pipefail, trap, mktemp -d)。

  • curljqを活用した外部API連携。

  • 冪等(idempotent)な設計。

  • Mermaidによるフロー図の提示。

  • root権限の扱いと権限分離に関する注意喚起。

1.2. 前提

  • Linuxシステム(systemdが稼働していること)。

  • Bashシェル、curljqコマンドがインストールされていること。

  • systemdユニットファイルに関する基本的な知識。

  • 本記事の手順はroot権限またはsudoを必要とします。

2. 実装

2.1. イベント駆動型自動化の概要

systemdは、従来のcronやinotifyに代わる強力なイベント駆動型自動化メカニズムを提供します。.timerユニットは時間ベースのイベントを、.pathユニットはファイルシステムイベントをトリガーとして、.serviceユニットを起動します。これにより、システムのリソース消費を抑えつつ、柔軟な自動化が可能です。

graph TD
    A["システム起動"] --> B(systemd);

    subgraph timer_event["定期実行イベント"]
        C1[my-api-poller.timer] -- OnCalendar/OnUnitActiveSecでトリトリガー --> D1[my-api-poller.service];
    end

    subgraph path_event["ファイルシステムイベント"]
        C2[my-config-monitor.path] -- PathModified/PathExistsでトリガー --> D2[my-config-monitor.service];
    end

    B --> timer_event;
    B --> path_event;

    D1 -- APIコール/JSON処理を実行 --> E1["定期タスク実行"];
    D2 -- ファイル内容解析を実行 --> E2["ファイル変更対応タスク実行"];

2.2. 定期実行タスクの実装(.timerユニット)

外部APIを定期的にポーリングし、その応答を処理するシナリオを考えます。

2.2.1. サービススクリプトの作成 (/usr/local/bin/api-poller.sh)

冪等性と安全性を考慮したBashスクリプトの例です。curljqを用いて外部APIからJSONデータを取得・処理します。

#!/bin/bash


# file: /usr/local/bin/api-poller.sh

# 1. 安全なBashスクリプトの設定

set -euo pipefail # -e: エラーで即終了, -u: 未定義変数でエラー, -o pipefail: パイプ中のエラーも捕捉
trap 'rm -rf "$TMP_DIR"; echo "$(date +'%Y-%m-%d %H:%M:%S') - Cleanup complete." >&2' EXIT # 終了時に一時ディレクトリを削除

# 2. 一時ディレクトリの作成 (安全かつ冪等)


# mktemp -d はユニークなディレクトリを作成するため、複数回実行しても問題ない

readonly TMP_DIR=$(mktemp -d)
echo "$(date +'%Y-%m-%d %H:%M:%S') - Working directory: $TMP_DIR" >&2

# 3. APIエンドポイントの設定 (環境変数や設定ファイルから取得するべきだが、例として直書き)

readonly API_URL="https://jsonplaceholder.typicode.com/todos/1"
readonly OUTPUT_FILE="$TMP_DIR/api_response.json"
readonly LOG_FILE="/var/log/api-poller.log" # ログ出力先

# 4. curlコマンドでAPIを呼び出し (TLS検証、再試行、バックオフ)


# -f: HTTPエラー(4xx,5xx)時に失敗


# --retry: 指定回数リトライ (ここでは5回)


# --retry-delay: リトライ間隔(秒) (ここでは3秒)


# --retry-max-time: リトライの合計時間(秒) (ここでは30秒)


# -s: サイレントモード


# --show-error: エラー表示


# --cacert: TLS証明書のパスを指定し、信頼されたCAによる検証を行う


# -o: 出力ファイルを指定

echo "$(date +'%Y-%m-%d %H:%M:%S') - Attempting to fetch API: $API_URL" >> "$LOG_FILE"
if ! curl -f --retry 5 --retry-delay 3 --retry-max-time 30 \
          -s --show-error \
          --cacert /etc/ssl/certs/ca-certificates.crt \
          -o "$OUTPUT_FILE" "$API_URL"; then
    echo "$(date +'%Y-%m-%d %H:%M:%S') - ERROR: Failed to fetch API after multiple retries." >> "$LOG_FILE"
    exit 1
fi
echo "$(date +'%Y-%m-%d %H:%M:%S') - API response fetched successfully." >> "$LOG_FILE"

# 5. jqコマンドでJSONを処理


# 例: titleとcompletedフィールドを抽出し、文字列として整形

if ! API_DATA=$(jq -r '. | "Title: " + .title + ", Completed: " + (.completed|tostring)' "$OUTPUT_FILE"); then
    echo "$(date +'%Y-%m-%d %H:%M:%S') - ERROR: Failed to process JSON with jq." >> "$LOG_FILE"
    exit 1
fi
echo "$(date +'%Y-%m-%d %H:%M:%S') - Processed API data: $API_DATA" >> "$LOG_FILE"

# 6. 処理結果を永続化(例としてログに追記)


# ここにデータベースへの書き込み、別のファイルへの出力などの冪等な処理を記述

echo "$(date +'%Y-%m-%d %H:%M:%S') - Task completed successfully." >> "$LOG_FILE"

exit 0

このスクリプトは、実行ごとに一時ディレクトリを生成・削除し、途中でエラーが発生してもシステムを汚染しない設計になっています。また、curlの再試行オプションによりネットワークの一時的な問題にも対応します[6]。

2.2.2. サービスユニットファイルの作成 (/etc/systemd/system/my-api-poller.service)

このサービスユニットは、上記のスクリプトを実行します。セキュリティ強化のため、UserGroupPrivateTmpなどのディレクティブを使用します[3]。

# file: /etc/systemd/system/my-api-poller.service

[Unit]
Description=My API Poller Service
Documentation=https://example.com/api-poller-doc

# ネットワークがオンラインになるまで待機

Wants=network-online.target
After=network-online.target

[Service]

# スクリプトのフルパスを指定

ExecStart=/usr/local/bin/api-poller.sh

# 実行ユーザーとグループを指定(最小権限の原則)


# 例えば 'api-user' というシステムユーザーを事前に作成する: sudo useradd -r -s /sbin/nologin api-user

User=api-user
Group=api-user

# サービス実行中に一時ディレクトリを分離し、終了時に自動削除

PrivateTmp=yes

# rootファイルシステムへの書き込みを禁止し、セキュリティを強化 (リードオンリーにする)

ProtectSystem=full

# ホームディレクトリを保護

ProtectHome=yes

# 新しい権限の取得を禁止

NoNewPrivileges=yes

# サービスがアクセスできるパスを制限する (systemd v232 以降で利用可能)


# ReadWritePaths=/var/log/api-poller.log # このファイルへの書き込みは許可


# ReadOnlyPaths=/usr/local/bin/api-poller.sh # 実行スクリプトは読み取り専用

# スクリプトが失敗した場合の再起動ポリシー

Restart=on-failure
RestartSec=5s # 5秒後に再起動を試みる

# サービスのタイプを simple に設定 (ExecStart のプロセスがメインプロセスになる)

Type=simple

[Install]

# systemd のターゲットにリンクすることで、ユニットが有効化される


# multi-user.target は通常のマルチユーザーシステム状態

WantedBy=multi-user.target

2.2.3. タイマーユニットファイルの作成 (/etc/systemd/system/my-api-poller.timer)

このタイマーユニットは、my-api-poller.serviceを定期的に起動します[1]。

# file: /etc/systemd/system/my-api-poller.timer

[Unit]
Description=Run My API Poller every 15 minutes

# このタイマーがアクティブになる前にサービスユニットが利用可能であることを保証

Requires=my-api-poller.service

[Timer]

# systemdサービスが起動してから15分後に初めて実行し、その後15分ごとに実行

OnBootSec=15min
OnUnitActiveSec=15min

# システム再起動時に前回の実行時間に基づいて次の実行時間を調整(永続化)


# これにより、システム停止中にスキップされた実行は、次回の起動時に直ちに実行される

Persistent=true

# タイマーの実行時間をランダムに分散させる(DDoS対策など)


# AccuracySec=1min # 実行の正確性を1分単位にする

[Install]

# このタイマーをシステム起動時に有効化する

WantedBy=timers.target

2.2.4. ユニットの有効化と起動

  1. systemd設定をリロード:

    sudo systemctl daemon-reload
    
  2. api-userシステムユーザーを作成(サービスユニットのUserディレクティブに対応):

    sudo useradd -r -s /sbin/nologin api-user
    
  3. タイマーユニットを有効化し、起動:

    sudo systemctl enable my-api-poller.timer
    sudo systemctl start my-api-poller.timer
    
  4. ステータスの確認:

    systemctl status my-api-poller.timer
    systemctl status my-api-poller.service
    

    タイマーの次回実行時刻も確認できます。

  5. ログの確認:

    journalctl -u my-api-poller.service -f
    tail -f /var/log/api-poller.log # スクリプトが出力したログ
    

2.3. ファイル監視タスクの実装(.pathユニット)

特定のファイルが変更されたら、それに反応して処理を実行するシナリオを考えます。

2.3.1. サービススクリプトの作成 (/usr/local/bin/config-processor.sh)

設定ファイルの変更を検出したら、その内容を読み込んで処理するスクリプトです[4]。

#!/bin/bash


# file: /usr/local/bin/config-processor.sh

set -euo pipefail
trap 'rm -rf "$TMP_DIR"; echo "$(date +'%Y-%m-%d %H:%M:%S') - Cleanup complete." >&2' EXIT

readonly TMP_DIR=$(mktemp -d)
echo "$(date +'%Y-%m-%d %H:%M:%S') - Working directory: $TMP_DIR" >&2

readonly CONFIG_FILE="/etc/app/config.json"
readonly PROCESSED_LOG="/var/log/config-processor.log"

# 設定ファイルが存在するか確認

if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "$(date +'%Y-%m-%d %H:%M:%S') - ERROR: Config file not found: $CONFIG_FILE" >> "$PROCESSED_LOG"
    exit 1
fi

# jqで設定ファイルをパースし、キーと値を抽出する例


# 例: {"threshold": 100, "feature_enabled": true} のようなJSONを想定

if ! CONFIG_DATA=$(jq -r '. | "Threshold: " + (.threshold|tostring) + ", Feature Enabled: " + (.feature_enabled|tostring)' "$CONFIG_FILE"); then
    echo "$(date +'%Y-%m-%d %H:%M:%S') - ERROR: Failed to parse config file with jq: $CONFIG_FILE" >> "$PROCESSED_LOG"
    exit 1
fi

echo "$(date +'%Y-%m-%d %H:%M:%S') - Config file processed. Data: $CONFIG_DATA" >> "$PROCESSED_LOG"

# ここに設定変更に応じた処理を記述


# 例: サービス再起動、設定の再ロード、通知など


# 冪等性を確保するため、前回の処理結果と比較して変更があった場合のみアクションを実行するなど工夫する

exit 0

このスクリプトは、設定ファイルが変更されるたびに実行され、その内容を安全に解析します[5]。

2.3.2. サービスユニットファイルの作成 (/etc/systemd/system/my-config-monitor.service)

先のmy-api-poller.serviceと同様に、セキュリティを考慮して設定します。

# file: /etc/systemd/system/my-config-monitor.service

[Unit]
Description=My Config File Monitor Service
Documentation=https://example.com/config-monitor-doc

[Service]
ExecStart=/usr/local/bin/config-processor.sh
User=config-user # 'config-user' を作成しておく: sudo useradd -r -s /sbin/nologin config-user
Group=config-user
PrivateTmp=yes
ProtectSystem=full
ProtectHome=yes
NoNewPrivileges=yes

# 監視対象ファイルへの読み取り権限とログへの書き込み権限

ReadOnlyPaths=/etc/app/config.json
ReadWritePaths=/var/log/config-processor.log
Restart=on-failure
RestartSec=5s
Type=simple

[Install]
WantedBy=multi-user.target

2.3.3. パスユニットファイルの作成 (/etc/systemd/system/my-config-monitor.path)

このパスユニットは、/etc/app/config.jsonファイルの変更を監視します[2]。

# file: /etc/systemd/system/my-config-monitor.path

[Unit]
Description=Monitor config.json changes

[Path]

# 監視対象のファイルパスとイベントタイプ


# PathModified: ファイル内容の変更またはinodeメタデータ変更をトリガー

PathModified=/etc/app/config.json

# PathExists=/etc/app/config.json # ファイルが作成された時にもトリガーしたい場合


# デバウンス期間 (複数イベントが短時間に発生した場合、指定期間待ってから一度だけトリガー)


# Unit=my-config-monitor.service # 明示的に指定しない場合、同名のサービスユニットを自動で解決

[Install]
WantedBy=multi-user.target

2.3.4. ユニットの有効化と起動

  1. systemd設定をリロード:

    sudo systemctl daemon-reload
    
  2. config-userシステムユーザーを作成:

    sudo useradd -r -s /sbin/nologin config-user
    
  3. テスト用の設定ファイルディレクトリとファイルを作成:

    sudo mkdir -p /etc/app/
    echo '{"threshold": 100, "feature_enabled": true}' | sudo tee /etc/app/config.json
    sudo chown config-user:config-user /etc/app/config.json # 監視対象ファイルの所有者も設定
    
  4. パスユニットを有効化し、起動:

    sudo systemctl enable my-config-monitor.path
    sudo systemctl start my-config-monitor.path
    
  5. ステータスの確認:

    systemctl status my-config-monitor.path
    systemctl status my-config-monitor.service
    
  6. ログの確認:

    journalctl -u my-config-monitor.service -f
    tail -f /var/log/config-processor.log
    

    /etc/app/config.json を変更(例: echo '{"threshold": 150, "feature_enabled": false}' | sudo tee /etc/app/config.json で保存)すると、サービスがトリガーされることを確認できます。

3. 検証

  • ユニットの状態確認: systemctl status <unit-name> で、.timer.pathユニット、そして関連する.serviceユニットがアクティブ(active (running))になっていることを確認します。

  • ログの監視: journalctl -u <unit-name> -f でリアルタイムログを監視し、タスクが期待通りに実行され、エラーが発生していないことを確認します。また、スクリプトが出力する専用ログファイル(例: /var/log/api-poller.log)も確認します。

  • イベントのトリガー:

    • .timerユニットの場合: sudo systemctl start my-api-poller.service を手動で実行し、スクリプトが正しく動作するか確認します。

    • .pathユニットの場合: 監視対象ファイルを変更(例: echo '{"key":"value"}' | sudo tee /etc/app/config.json)し、サービスが起動することを確認します。

4. 運用

4.1. 権限分離とセキュリティ

  • 最小権限の原則: サービスユニットのUser=およびGroup=ディレクティブを常に設定し、専用の非特権ユーザーアカウントでスクリプトを実行させます。これにより、スクリプトに脆弱性があった場合でも、システム全体への影響を最小限に抑えられます。

  • サンドボックス化: PrivateTmp=yes, ProtectSystem=full, ProtectHome=yes, NoNewPrivileges=yes などのsystemdセキュリティディレクティブを積極的に使用し、サービスのリソースアクセス範囲を厳しく制限します[3]。

  • ファイルパーミッション: スクリプトファイル自体(/usr/local/bin/以下)はroot:rootで所有され、実行権限のみが付与されていることを確認します(例: sudo chmod 755 /usr/local/bin/api-poller.sh)。

4.2. 冪等性とエラーハンドリング

  • 冪等なスクリプト: スクリプトは何回実行されても同じ結果になるよう設計します。例えば、ファイルへの追記ではなく上書き、データベースへの挿入前に存在チェックなどを行います。

  • 堅牢なエラー処理: set -euo pipefailに加え、特定の失敗ケースに対するロギング、アラート通知、適切なリトライロジックを実装します[4]。

4.3. 設定のバージョン管理

すべての.service, .timer, .pathユニットファイル、および関連するスクリプトは、Gitなどのバージョン管理システムで管理することを強く推奨します。これにより、変更履歴の追跡、ロールバック、チームでの共同作業が容易になります。

5. トラブルシュート

5.1. ログの確認

最優先でjournalctlコマンドを使用します。

# 特定のサービスユニットのログを過去1時間分表示

journalctl -u my-api-poller.service --since "1 hour ago"

# 特定のタイマーユニットのログ

journalctl -u my-api-poller.timer

# 失敗しているユニットの一覧表示

systemctl --failed

スクリプトが独自のログファイルに出力している場合は、そのファイルも確認します(例: tail -f /var/log/api-poller.log)。

5.2. ユニットの状態確認

  • systemctl status <unit-name>: ユニットの現在の状態、PID、メモリ使用量、直近のログなどを詳細に確認できます。

  • systemctl list-timers: 現在有効なタイマーユニットとその次回実行時刻一覧。

  • systemctl list-paths: 現在有効なパスユニットと監視対象一覧。

5.3. スクリプトの手動実行

サービススクリプト(例: /usr/local/bin/api-poller.sh)をsystemdを通さずに直接実行し、エラーが出ないか、意図した動作をするかを確認します。この際、systemdで設定したUserや環境変数を模倣して実行すると、より正確なデバッグが可能です。

6. まとめ

systemdの.timer.pathユニットは、Linuxシステムにおけるイベント駆動型自動化の強力かつ柔軟な基盤を提供します。.timerは定期実行を、.pathはファイルシステムイベントをトリガーとして、それぞれに対応するサービスユニットを起動します[1][2]。 、安全なBashスクリプトの書き方、curljqを活用したAPI連携、そしてsystemdのセキュリティ強化機能を組み合わせることで、堅牢で冪等な自動化システムを構築する手順を示しました。適切な権限分離、詳細なロギング、およびバージョン管理を実践することで、運用負荷を軽減し、システムの安定性とセキュリティを向上させることができます。これにより、DevOpsエンジニアはより効率的かつ信頼性の高い自動化ワークフローを2024年5月28日に実現できます。

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

コメント

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