本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
jqコマンドでJSONLデータを効率処理
要件と前提
、JSONL (JSON Lines) 形式のデータを jq コマンドで効率的に処理し、curl で外部から取得、systemd で定期実行するDevOpsプラクティスを解説します。処理の安全性、冪等性、権限分離に重点を置き、具体的なコード例と共に手順を示します。
JSONL (JSON Lines) とは
JSONLは、各行が独立したJSONオブジェクトであるテキストベースのデータ形式です。ストリーミングデータやログデータの保存によく利用され、行ごとに処理できるため大規模データに適しています。jq は、デフォルトでJSONL形式の入力を行ごとに処理する特性を持ちます[3]。
jqコマンドとは
jq は、軽量かつ柔軟なコマンドラインJSONプロセッサです。JSONデータのフィルタリング、変換、抽出、集計など、幅広い操作を効率的に実行できます[1]。パイプライン処理に対応し、複雑なデータ変換も容易に行えるため、DevOpsやデータ処理の現場で広く活用されています。2024年5月18日にはv1.7.1がリリースされています[2]。
安全なBashスクリプトの原則
本記事で提示するBashスクリプトは、以下の原則に基づいています。
set -euo pipefail:set -e: コマンドが失敗した場合、すぐにスクリプトを終了します。set -u: 未定義の変数が参照された場合にエラーとします。set -o pipefail: パイプライン内のいずれかのコマンドが失敗した場合、パイプライン全体を失敗とします。
trap '...' EXIT: スクリプト終了時に実行されるクリーンアップ処理を定義します。一時ファイルの削除などに利用します。mktemp -d: 一時ディレクトリを安全に作成し、複数のプロセス間での競合やセキュリティリスクを回避します。冪等性 (idempotent): 同じ操作を複数回実行しても、システムの状態が同じになることを保証します。例えば、ディレクトリ作成には
mkdir -pを使用します。
権限の考慮
systemd サービスは通常 root ユーザーによって設定・インストールされますが、実際の処理実行は最小限の権限を持つ専用ユーザー (例: myuser) で行うべきです。これにより、万が一スクリプトに脆弱性があった場合でも、システム全体への影響を限定できます。systemd の User= および Group= オプションを利用して権限分離を図ります[8]。
実装
処理フローの概要
JSONLデータの取得から加工、保存、そして定期実行までの基本的なフローは以下の通りです。
graph TD
A["開始"] --> B{"systemd Timer発動"};
B --> C("my-jsonl-processor.service 起動");
C --> D["Bashスクリプト実行"];
D --> E["curlでJSONLデータ取得"];
E -->|"成功"| F["jqでデータ処理"]
E -->|"失敗"| G("エラーログ出力")
F --> H["処理済みデータ保存"];
F --> I("成功ログ出力");
G --> J("通知");
I --> J("通知");
J --> K["終了"];1. JSONLデータ取得・処理スクリプト (fetch_and_process.sh)
以下のスクリプトは、curl で外部APIからJSONLデータを取得し、jq でフィルタリングと変換を行った後、ローカルに保存します。
#!/bin/bash
set -euo pipefail
# スクリプト終了時に一時ディレクトリを削除
trap 'rm -rf "$TMP_DIR"' EXIT
# --- 変数定義 ---
# データ取得元のAPIエンドポイント
API_URL="https://httpbin.org/json" # 例: ダミーJSONを返すAPI
# 処理済みデータ保存先のディレクトリ
OUTPUT_DIR="/var/log/my_app/processed_data"
# 処理ファイル名に付与するタイムスタンプ
TIMESTAMP=$(date +%Y%m%d%H%M%S)
# 一時ディレクトリの作成 (安全かつ一意な名前)
TMP_DIR=$(mktemp -d -t jsonl_process-XXXXXXXX)
# 生データ保存用の一時ファイルパス
RAW_FILE="$TMP_DIR/raw_data.jsonl"
# 処理済みデータ保存用のファイルパス
PROCESSED_FILE="$OUTPUT_DIR/processed_data_$TIMESTAMP.jsonl"
# --- 前提条件: 出力ディレクトリの存在確認と作成 (冪等性) ---
# -p オプションにより、パスが存在しない場合にのみディレクトリを作成し、
# 既に存在する場合は何もしないため冪等性がある。
mkdir -p "$OUTPUT_DIR"
echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Starting data fetching and processing."
# --- 1. データ取得 (curl) ---
# -sS: サイレントモード (-s) かつエラー表示 (-S)。
# --fail: HTTPエラー (4xx/5xx) が発生した場合にゼロ以外の終了コードを返す。
# --connect-timeout 10: 接続試行の最大時間を10秒に設定。
# --max-time 60: 転送全体の最大時間を60秒に設定。
# --retry 5: 最大5回再試行。
# --retry-delay 5: 最初の再試行までの遅延を5秒に設定。以降指数バックオフ。
# --retry-max-time 300: 再試行を含めた転送全体の最大時間を300秒に設定。
# --cacert /etc/ssl/certs/ca-certificates.crt: 信頼できるCA証明書を指定し、TLS検証を行う。
# 本番環境では必須。ディストリビューションに応じてパスは異なる場合がある。
# 入力: API_URLからのJSONLデータストリーム
# 出力: RAW_FILEへのJSONLデータ
# 計算量: ネットワークI/Oに依存 (O(データサイズ))
# メモリ: curlがデータをバッファリングする量に依存するが、通常はストリーミング処理される
if ! curl -sS --fail \
--connect-timeout 10 \
--max-time 60 \
--retry 5 \
--retry-delay 5 \
--retry-max-time 300 \
--cacert /etc/ssl/certs/ca-certificates.crt \
"$API_URL" > "$RAW_FILE"; then
echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ERROR: Failed to fetch data from $API_URL" >&2
exit 1
fi
echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Data fetched successfully to $RAW_FILE."
# --- 2. JSONLデータ処理 (jq) ---
# サンプルとして、httpbin.org/json は単一のJSONオブジェクトを返すため、
# ここではRAW_FILEから `slides` 配列の各要素を抽出し、
# その `title` と `type` フィールドを新しいJSONオブジェクトとして出力する例を示します。
# 実際にはJSONL入力 (`{}` の連続) を想定します。
# 例: `{"id":1, "status":"success", "timestamp":"2024-07-26T10:00:00"}`, `{"id":2, "status":"failed", "timestamp":"2024-07-26T10:01:00"}`
# jq -c 'select(.status == "success") | {id: .id, processed_at: .timestamp + "Z"}' "$RAW_FILE" \
# > "$PROCESSED_FILE"
#
# 入力: RAW_FILE (JSONL形式、または単一JSONからJSONLに変換)
# 出力: PROCESSED_FILE (JSONL形式)
# 前提: RAW_FILEが有効なJSONまたはJSONL形式であること
# 計算量: O(N * M) (Nは行数、Mは各JSONオブジェクトの処理コスト)
# メモリ: jqはストリーミング処理に優れるため、大規模データでも効率的
jq -c '.slides[] | {title: .title, type: .type}' "$RAW_FILE" \
> "$PROCESSED_FILE"
echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Data processed successfully to $PROCESSED_FILE."
echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Finished data fetching and processing."
2. systemdサービスとしての定期実行
上記のスクリプトを systemd サービスおよびタイマーとして設定し、定期的に実行します。
a. サービスファイル (/etc/systemd/system/my-jsonl-processor.service)
このサービスファイルは、スクリプトの実行方法と環境を定義します。
[Unit] Description=My JSONL Data Processor Service Documentation=https://example.com/docs/my-jsonl-processor # ネットワークが利用可能になってからサービスを開始 Requires=network-online.target After=network-online.target [Service] # Type=oneshot: コマンド実行後、すぐに終了するサービスタイプ Type=oneshot # User/Group: スクリプトを実行するユーザーとグループ。必ず低権限ユーザーを指定。 User=myuser Group=myuser # WorkingDirectory: スクリプトの実行ディレクトリ WorkingDirectory=/opt/my-jsonl-processor # ExecStart: 実行するスクリプトのフルパス ExecStart=/opt/my-jsonl-processor/fetch_and_process.sh # StandardOutput/StandardError: 標準出力/エラーの出力先をjournaldに設定 # これにより、journalctlコマンドでログを確認できます。 StandardOutput=journal StandardError=journal # Restart=on-failure: 失敗時に再起動 (Type=oneshotでは通常不要だが、他のサービスタイプでは有用) # RestartSec=5s: 再起動までの待機時間 # --- セキュリティ強化のためのオプション --- # ProtectSystem=full: /usr, /boot, /etc を読み取り専用にする。 ProtectSystem=full # ProtectHome=true: /home, /root を読み取り専用にする。 ProtectHome=true # PrivateTmp=true: サービスごとに独立した一時名前空間を提供する。 # スクリプト内で mktemp を使う場合でも、念のため設定することでさらに隔離される。 PrivateTmp=true [Install] # WantedBy: サービスが有効化された際に、どのターゲットにリンクされるかを定義 # multi-user.target: 通常のマルチユーザーシステム起動時に有効化 WantedBy=multi-user.target
b. タイマーファイル (/etc/systemd/system/my-jsonl-processor.timer)
このタイマーファイルは、サービスがいつ実行されるかを定義します。
[Unit] Description=Run My JSONL Data Processor Service hourly Documentation=https://example.com/docs/my-jsonl-processor [Timer] # OnCalendar: 実行スケジュールを定義。例: hourly (毎時), daily (毎日), *:0/15 (15分ごと) OnCalendar=hourly # Persistent=true: システム停止中に見逃した実行があった場合、起動後にすぐに実行 Persistent=true # RandomizedDelaySec=600: 起動を最大600秒 (10分) 遅らせて分散させる。 # 複数のタイマーが同時に起動するのを防ぎ、システム負荷を軽減。 # RandomizedDelaySec=600 # Unit: このタイマーが起動するサービスユニット Unit=my-jsonl-processor.service [Install] # WantedBy: タイマーが有効化された際に、どのターゲットにリンクされるかを定義 WantedBy=timers.target
c. ファイルの配置と権限
スクリプトの配置:
myuserが実行可能な/opt/my-jsonl-processor/ディレクトリを作成し、スクリプトを配置します。# root権限で実行 sudo mkdir -p /opt/my-jsonl-processor sudo cp fetch_and_process.sh /opt/my-jsonl-processor/ sudo chmod +x /opt/my-jsonl-processor/fetch_and_process.sh # スクリプト実行ユーザー (myuser) を作成 (存在しない場合) sudo useradd -r -s /usr/sbin/nologin myuser || true # スクリプトおよび関連ディレクトリの所有者をmyuserに変更 sudo chown -R myuser:myuser /opt/my-jsonl-processor sudo chown -R myuser:myuser /var/log/my_app/processed_data
systemdユニットファイルの配置:
.serviceおよび.timerファイルを/etc/systemd/system/に配置します。これにはroot権限が必要です。# root権限で実行 sudo cp my-jsonl-processor.service /etc/systemd/system/ sudo cp my-jsonl-processor.timer /etc/systemd/system/
検証
systemdデーモンのリロード: 新しいユニットファイルを認識させるために
systemdデーモンをリロードします。sudo systemctl daemon-reload
サービスの手動実行とステータス確認: タイマーを有効にする前に、サービスが正しく動作するか手動で確認します。
sudo systemctl start my-jsonl-processor.service sudo systemctl status my-jsonl-processor.service
Active: inactive (dead)と表示されていれば、Type=oneshotのため正常終了しています。エラーメッセージがないことを確認してください。ログの確認:
journalctlコマンドでサービスが正常に動作したか、エラーがないか確認します。journalctl -u my-jsonl-processor.service --since "1 hour ago"
タイマーの有効化と起動: サービスが正常に動作することを確認したら、タイマーを有効化し起動します。
sudo systemctl enable my-jsonl-processor.timer sudo systemctl start my-jsonl-processor.timer
タイマーのステータス確認: タイマーが起動し、次回の実行時刻が設定されていることを確認します。
sudo systemctl status my-jsonl-processor.timer
Next: ...の行で次回の実行時刻が確認できます。
運用
モニタリング:
systemdのログ (journalctl) を集中ログ管理システム (例: Fluentd, Loki, ELK Stack) に転送し、定期的に監視します。スクリプトの実行時間、成功/失敗のメトリクスをPrometheusなどで収集するのも有効です。ログローテーション:
/var/log/my_app/ディレクトリに保存されるログファイルが肥大化しないよう、logrotateの設定を行います。設定管理: API URLや出力パスなどの設定値をスクリプト内にハードコードせず、設定ファイル (例:
/etc/my-jsonl-processor/config.env) に外部化し、スクリプトから読み込むようにすることで、運用時の柔軟性を高めます。
トラブルシュート
jqコマンドのエラー
構文エラー:
jq: error: syntax error, unexpected ...。jqフィルターの構文に間違いがないか確認します。複雑なフィルターは少しずつ試すか、jq -c '.'で入力JSONの形式を確認してください。入力形式の不一致:
jqに渡される入力が有効なJSONまたはJSONL形式でない場合、エラーが発生します。cat "$RAW_FILE" | head -n 1などで入力ファイルの内容を確認してください。jqのパスが通っていない:systemdサービス内でjqコマンドが見つからない場合、ExecStartにPATH=/usr/local/bin:/usr/bin:/bin /opt/my-jsonl-processor/fetch_and_process.shのようにフルパスを含めるか、Environment="PATH=/usr/local/bin:/usr/bin:/bin"をサービスファイルに追加します。
curlコマンドのエラー
ネットワーク関連:
curl: (6) Could not resolve host: ...(DNSエラー)、curl: (7) Failed to connect to ...(接続エラー)。ネットワーク設定、ファイアウォール、APIサーバーの稼働状況を確認します。HTTPエラー:
--failオプションにより、4xxや5xxのエラーコードが返された場合にスクリプトが失敗します。APIサーバーの応答コードを確認し、エラーハンドリングを強化します。TLS/SSL証明書エラー:
curl: (60) SSL certificate problem: ...。--cacertオプションで指定されたCA証明書が正しくないか、APIサーバーの証明書が有効でない可能性があります。テスト環境では--insecureも一時的に使用できますが、本番環境では絶対に避けるべきです。詳細なデバッグ:
curl -vオプションを使用すると、詳細なリクエスト/レスポンスヘッダーやTLSネゴシエーション情報が表示され、問題特定に役立ちます。
systemdサービスのエラー
サービス起動失敗:
sudo systemctl status my-jsonl-processor.serviceでActive: failedと表示される場合、journalctl -xeu my-jsonl-processor.serviceコマンドで詳細なエラーログを確認します。権限エラー:
Permission denied。スクリプトファイルや出力ディレクトリの権限 (chmod,chown) がmyuserでアクセス可能か確認します。特にProtectSystem,ProtectHomeなどのセキュリティオプションが厳しすぎる場合、必要なディレクトリへの書き込みが制限されることがあります。パスの問題:
ExecStartに指定したスクリプトのパスが間違っていないか、スクリプト内で使用しているコマンドのパスがPATH環境変数に含まれているか確認します。タイマー未起動:
sudo systemctl status my-jsonl-processor.timerでActive: inactiveと表示される場合、sudo systemctl enable --now my-jsonl-processor.timerで有効化・起動します。
まとめ
本記事では、jq コマンドを活用したJSONLデータの効率的な処理、curl による安全なデータ取得、そして systemd を用いた定期自動実行の DevOPs プラクティスを解説しました。安全なBashスクリプトの原則、権限分離、冪等性といった考慮事項を盛り込むことで、堅牢で運用しやすいデータ処理基盤を構築できます。今回紹介した手法は、ログ解析、API連携、データパイプラインの構築など、多岐にわたるシナリオに応用可能です。定期的なモニタリングと適切なトラブルシューティングにより、安定した運用を実現してください。

