jqコマンドでJSONLデータを効率処理

Tech

本記事は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) で行うべきです。これにより、万が一スクリプトに脆弱性があった場合でも、システム全体への影響を限定できます。systemdUser= および 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. ファイルの配置と権限

  1. スクリプトの配置: 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
    
  2. 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/
    

検証

  1. systemdデーモンのリロード: 新しいユニットファイルを認識させるために systemd デーモンをリロードします。

    sudo systemctl daemon-reload
    
  2. サービスの手動実行とステータス確認: タイマーを有効にする前に、サービスが正しく動作するか手動で確認します。

    sudo systemctl start my-jsonl-processor.service
    sudo systemctl status my-jsonl-processor.service
    

    Active: inactive (dead) と表示されていれば、Type=oneshot のため正常終了しています。エラーメッセージがないことを確認してください。

  3. ログの確認: journalctl コマンドでサービスが正常に動作したか、エラーがないか確認します。

    journalctl -u my-jsonl-processor.service --since "1 hour ago"
    
  4. タイマーの有効化と起動: サービスが正常に動作することを確認したら、タイマーを有効化し起動します。

    sudo systemctl enable my-jsonl-processor.timer
    sudo systemctl start my-jsonl-processor.timer
    
  5. タイマーのステータス確認: タイマーが起動し、次回の実行時刻が設定されていることを確認します。

    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 コマンドが見つからない場合、ExecStartPATH=/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.serviceActive: failed と表示される場合、journalctl -xeu my-jsonl-processor.service コマンドで詳細なエラーログを確認します。

  • 権限エラー: Permission denied。スクリプトファイルや出力ディレクトリの権限 (chmod, chown) が myuser でアクセス可能か確認します。特に ProtectSystem, ProtectHome などのセキュリティオプションが厳しすぎる場合、必要なディレクトリへの書き込みが制限されることがあります。

  • パスの問題: ExecStart に指定したスクリプトのパスが間違っていないか、スクリプト内で使用しているコマンドのパスが PATH 環境変数に含まれているか確認します。

  • タイマー未起動: sudo systemctl status my-jsonl-processor.timerActive: inactive と表示される場合、sudo systemctl enable --now my-jsonl-processor.timer で有効化・起動します。

まとめ

本記事では、jq コマンドを活用したJSONLデータの効率的な処理、curl による安全なデータ取得、そして systemd を用いた定期自動実行の DevOPs プラクティスを解説しました。安全なBashスクリプトの原則、権限分離、冪等性といった考慮事項を盛り込むことで、堅牢で運用しやすいデータ処理基盤を構築できます。今回紹介した手法は、ログ解析、API連携、データパイプラインの構築など、多岐にわたるシナリオに応用可能です。定期的なモニタリングと適切なトラブルシューティングにより、安定した運用を実現してください。

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

コメント

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