<p><!--META
{
"title": "jqコマンドでJSONLデータを効率処理",
"primary_category": "DevOps",
"secondary_categories": ["Linux","コマンド","bash","systemd"],
"tags": ["jq", "JSONL", "systemd", "curl", "bash", "DevOps", "Linux", "cron"],
"summary": "jqコマンドを用いたJSONLデータ処理を、安全なbashスクリプトとsystemdで自動化・効率化する手順を解説します。",
"mermaid": true,
"verify_level": "L0",
"tweet_hint": {"text":"jqでJSONLデータを効率処理!安全なbashスクリプトとsystemdによる自動化をDevOps視点で解説。curlでのデータ取得、jqでの加工、systemd timerでの定期実行まで網羅。
#DevOps #jq #JSONL","hashtags":["#DevOps","#jq","#JSONL"]},
"link_hints": [
"https://jqlang.github.io/jq/manual/",
"https://curl.se/docs/manpage.html",
"https://www.freedesktop.org/software/systemd/man/systemd.service.html"
]
}
-->
本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">jqコマンドでJSONLデータを効率処理</h1>
<h2 class="wp-block-heading">要件と前提</h2>
<p>、JSONL (JSON Lines) 形式のデータを <code>jq</code> コマンドで効率的に処理し、<code>curl</code> で外部から取得、<code>systemd</code> で定期実行するDevOpsプラクティスを解説します。処理の安全性、冪等性、権限分離に重点を置き、具体的なコード例と共に手順を示します。</p>
<h3 class="wp-block-heading">JSONL (JSON Lines) とは</h3>
<p>JSONLは、各行が独立したJSONオブジェクトであるテキストベースのデータ形式です。ストリーミングデータやログデータの保存によく利用され、行ごとに処理できるため大規模データに適しています。<code>jq</code> は、デフォルトでJSONL形式の入力を行ごとに処理する特性を持ちます[3]。</p>
<h3 class="wp-block-heading">jqコマンドとは</h3>
<p><code>jq</code> は、軽量かつ柔軟なコマンドラインJSONプロセッサです。JSONデータのフィルタリング、変換、抽出、集計など、幅広い操作を効率的に実行できます[1]。パイプライン処理に対応し、複雑なデータ変換も容易に行えるため、DevOpsやデータ処理の現場で広く活用されています。2024年5月18日にはv1.7.1がリリースされています[2]。</p>
<h3 class="wp-block-heading">安全なBashスクリプトの原則</h3>
<p>本記事で提示するBashスクリプトは、以下の原則に基づいています。</p>
<ul class="wp-block-list">
<li><p><strong><code>set -euo pipefail</code></strong>:</p>
<ul>
<li><p><code>set -e</code>: コマンドが失敗した場合、すぐにスクリプトを終了します。</p></li>
<li><p><code>set -u</code>: 未定義の変数が参照された場合にエラーとします。</p></li>
<li><p><code>set -o pipefail</code>: パイプライン内のいずれかのコマンドが失敗した場合、パイプライン全体を失敗とします。</p></li>
</ul></li>
<li><p><strong><code>trap '...' EXIT</code></strong>: スクリプト終了時に実行されるクリーンアップ処理を定義します。一時ファイルの削除などに利用します。</p></li>
<li><p><strong><code>mktemp -d</code></strong>: 一時ディレクトリを安全に作成し、複数のプロセス間での競合やセキュリティリスクを回避します。</p></li>
<li><p><strong>冪等性 (idempotent)</strong>: 同じ操作を複数回実行しても、システムの状態が同じになることを保証します。例えば、ディレクトリ作成には <code>mkdir -p</code> を使用します。</p></li>
</ul>
<h3 class="wp-block-heading">権限の考慮</h3>
<p><code>systemd</code> サービスは通常 <code>root</code> ユーザーによって設定・インストールされますが、実際の処理実行は最小限の権限を持つ専用ユーザー (例: <code>myuser</code>) で行うべきです。これにより、万が一スクリプトに脆弱性があった場合でも、システム全体への影響を限定できます。<code>systemd</code> の <code>User=</code> および <code>Group=</code> オプションを利用して権限分離を図ります[8]。</p>
<h2 class="wp-block-heading">実装</h2>
<h3 class="wp-block-heading">処理フローの概要</h3>
<p>JSONLデータの取得から加工、保存、そして定期実行までの基本的なフローは以下の通りです。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
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["終了"];
</pre></div>
<h3 class="wp-block-heading">1. JSONLデータ取得・処理スクリプト (<code>fetch_and_process.sh</code>)</h3>
<p>以下のスクリプトは、<code>curl</code> で外部APIからJSONLデータを取得し、<code>jq</code> でフィルタリングと変換を行った後、ローカルに保存します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/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."
</pre>
</div>
<h3 class="wp-block-heading">2. systemdサービスとしての定期実行</h3>
<p>上記のスクリプトを <code>systemd</code> サービスおよびタイマーとして設定し、定期的に実行します。</p>
<h4 class="wp-block-heading">a. サービスファイル (<code>/etc/systemd/system/my-jsonl-processor.service</code>)</h4>
<p>このサービスファイルは、スクリプトの実行方法と環境を定義します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[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
</pre>
</div>
<h4 class="wp-block-heading">b. タイマーファイル (<code>/etc/systemd/system/my-jsonl-processor.timer</code>)</h4>
<p>このタイマーファイルは、サービスがいつ実行されるかを定義します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[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
</pre>
</div>
<h4 class="wp-block-heading">c. ファイルの配置と権限</h4>
<ol class="wp-block-list">
<li><p><strong>スクリプトの配置</strong>:
<code>myuser</code> が実行可能な <code>/opt/my-jsonl-processor/</code> ディレクトリを作成し、スクリプトを配置します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 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
</pre>
</div></li>
<li><p><strong>systemdユニットファイルの配置</strong>:
<code>.service</code> および <code>.timer</code> ファイルを <code>/etc/systemd/system/</code> に配置します。これには <code>root</code> 権限が必要です。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># root権限で実行
sudo cp my-jsonl-processor.service /etc/systemd/system/
sudo cp my-jsonl-processor.timer /etc/systemd/system/
</pre>
</div></li>
</ol>
<h2 class="wp-block-heading">検証</h2>
<ol class="wp-block-list">
<li><p><strong>systemdデーモンのリロード</strong>:
新しいユニットファイルを認識させるために <code>systemd</code> デーモンをリロードします。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl daemon-reload
</pre>
</div></li>
<li><p><strong>サービスの手動実行とステータス確認</strong>:
タイマーを有効にする前に、サービスが正しく動作するか手動で確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl start my-jsonl-processor.service
sudo systemctl status my-jsonl-processor.service
</pre>
</div>
<p><code>Active: inactive (dead)</code> と表示されていれば、<code>Type=oneshot</code> のため正常終了しています。エラーメッセージがないことを確認してください。</p></li>
<li><p><strong>ログの確認</strong>:
<code>journalctl</code> コマンドでサービスが正常に動作したか、エラーがないか確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">journalctl -u my-jsonl-processor.service --since "1 hour ago"
</pre>
</div></li>
<li><p><strong>タイマーの有効化と起動</strong>:
サービスが正常に動作することを確認したら、タイマーを有効化し起動します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl enable my-jsonl-processor.timer
sudo systemctl start my-jsonl-processor.timer
</pre>
</div></li>
<li><p><strong>タイマーのステータス確認</strong>:
タイマーが起動し、次回の実行時刻が設定されていることを確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl status my-jsonl-processor.timer
</pre>
</div>
<p><code>Next: ...</code> の行で次回の実行時刻が確認できます。</p></li>
</ol>
<h2 class="wp-block-heading">運用</h2>
<ul class="wp-block-list">
<li><p><strong>モニタリング</strong>: <code>systemd</code> のログ (<code>journalctl</code>) を集中ログ管理システム (例: Fluentd, Loki, ELK Stack) に転送し、定期的に監視します。スクリプトの実行時間、成功/失敗のメトリクスをPrometheusなどで収集するのも有効です。</p></li>
<li><p><strong>ログローテーション</strong>: <code>/var/log/my_app/</code> ディレクトリに保存されるログファイルが肥大化しないよう、<code>logrotate</code> の設定を行います。</p></li>
<li><p><strong>設定管理</strong>: API URLや出力パスなどの設定値をスクリプト内にハードコードせず、設定ファイル (例: <code>/etc/my-jsonl-processor/config.env</code>) に外部化し、スクリプトから読み込むようにすることで、運用時の柔軟性を高めます。</p></li>
</ul>
<h2 class="wp-block-heading">トラブルシュート</h2>
<h3 class="wp-block-heading">jqコマンドのエラー</h3>
<ul class="wp-block-list">
<li><p><strong>構文エラー</strong>: <code>jq: error: syntax error, unexpected ...</code>。<code>jq</code> フィルターの構文に間違いがないか確認します。複雑なフィルターは少しずつ試すか、<code>jq -c '.'</code> で入力JSONの形式を確認してください。</p></li>
<li><p><strong>入力形式の不一致</strong>: <code>jq</code> に渡される入力が有効なJSONまたはJSONL形式でない場合、エラーが発生します。<code>cat "$RAW_FILE" | head -n 1</code> などで入力ファイルの内容を確認してください。</p></li>
<li><p><strong><code>jq</code> のパスが通っていない</strong>: <code>systemd</code> サービス内で <code>jq</code> コマンドが見つからない場合、<code>ExecStart</code> に <code>PATH=/usr/local/bin:/usr/bin:/bin /opt/my-jsonl-processor/fetch_and_process.sh</code> のようにフルパスを含めるか、<code>Environment="PATH=/usr/local/bin:/usr/bin:/bin"</code> をサービスファイルに追加します。</p></li>
</ul>
<h3 class="wp-block-heading">curlコマンドのエラー</h3>
<ul class="wp-block-list">
<li><p><strong>ネットワーク関連</strong>: <code>curl: (6) Could not resolve host: ...</code> (DNSエラー)、<code>curl: (7) Failed to connect to ...</code> (接続エラー)。ネットワーク設定、ファイアウォール、APIサーバーの稼働状況を確認します。</p></li>
<li><p><strong>HTTPエラー</strong>: <code>--fail</code> オプションにより、4xxや5xxのエラーコードが返された場合にスクリプトが失敗します。APIサーバーの応答コードを確認し、エラーハンドリングを強化します。</p></li>
<li><p><strong>TLS/SSL証明書エラー</strong>: <code>curl: (60) SSL certificate problem: ...</code>。<code>--cacert</code> オプションで指定されたCA証明書が正しくないか、APIサーバーの証明書が有効でない可能性があります。テスト環境では <code>--insecure</code> も一時的に使用できますが、本番環境では絶対に避けるべきです。</p></li>
<li><p><strong>詳細なデバッグ</strong>: <code>curl -v</code> オプションを使用すると、詳細なリクエスト/レスポンスヘッダーやTLSネゴシエーション情報が表示され、問題特定に役立ちます。</p></li>
</ul>
<h3 class="wp-block-heading">systemdサービスのエラー</h3>
<ul class="wp-block-list">
<li><p><strong>サービス起動失敗</strong>: <code>sudo systemctl status my-jsonl-processor.service</code> で <code>Active: failed</code> と表示される場合、<code>journalctl -xeu my-jsonl-processor.service</code> コマンドで詳細なエラーログを確認します。</p></li>
<li><p><strong>権限エラー</strong>: <code>Permission denied</code>。スクリプトファイルや出力ディレクトリの権限 (<code>chmod</code>, <code>chown</code>) が <code>myuser</code> でアクセス可能か確認します。特に <code>ProtectSystem</code>, <code>ProtectHome</code> などのセキュリティオプションが厳しすぎる場合、必要なディレクトリへの書き込みが制限されることがあります。</p></li>
<li><p><strong>パスの問題</strong>: <code>ExecStart</code> に指定したスクリプトのパスが間違っていないか、スクリプト内で使用しているコマンドのパスが <code>PATH</code> 環境変数に含まれているか確認します。</p></li>
<li><p><strong>タイマー未起動</strong>: <code>sudo systemctl status my-jsonl-processor.timer</code> で <code>Active: inactive</code> と表示される場合、<code>sudo systemctl enable --now my-jsonl-processor.timer</code> で有効化・起動します。</p></li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>本記事では、<code>jq</code> コマンドを活用したJSONLデータの効率的な処理、<code>curl</code> による安全なデータ取得、そして <code>systemd</code> を用いた定期自動実行の DevOPs プラクティスを解説しました。安全なBashスクリプトの原則、権限分離、冪等性といった考慮事項を盛り込むことで、堅牢で運用しやすいデータ処理基盤を構築できます。今回紹介した手法は、ログ解析、API連携、データパイプラインの構築など、多岐にわたるシナリオに応用可能です。定期的なモニタリングと適切なトラブルシューティングにより、安定した運用を実現してください。</p>
本記事は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連携、データパイプラインの構築など、多岐にわたるシナリオに応用可能です。定期的なモニタリングと適切なトラブルシューティングにより、安定した運用を実現してください。
コメント