<h1 class="wp-block-heading">kubectl dry-runとKustomizeで安全なKubernetes適用を実現する</h1>
<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h2 class="wp-block-heading">1. 要件と前提</h2>
<p>Kubernetes環境へのマニフェスト適用は、意図しない変更やダウンタイムを引き起こすリスクがあります。本記事では、DevOpsエンジニアとして、<code>kubectl dry-run</code>とKustomizeを組み合わせ、安全かつ冪等にKubernetesリソースを適用する手法を解説します。</p>
<h3 class="wp-block-heading">1.1. 要件</h3>
<ul class="wp-block-list">
<li><p><code>kubectl dry-run</code> を利用した変更の事前検証。</p></li>
<li><p>Kustomize を用いた環境ごとのリソースカスタマイズ。</p></li>
<li><p>シェルスクリプトによる自動化と安全性の確保(<code>set -euo pipefail</code>、<code>trap</code>、<code>mktemp -d</code>)。</p></li>
<li><p><code>jq</code> を用いたJSON出力の検証。</p></li>
<li><p><code>curl</code> を用いたAPI連携におけるTLS、再試行、バックオフの考慮。</p></li>
<li><p><code>systemd unit/timer</code> による定期的な適用スクリプトの実行とログ管理。</p></li>
<li><p>root権限の扱いと権限分離に関する注意喚起。</p></li>
</ul>
<h3 class="wp-block-heading">1.2. 前提</h3>
<ul class="wp-block-list">
<li><p>Kubernetesクラスタへのアクセス権限と<code>kubectl</code>コマンドが設定済みであること。</p></li>
<li><p>Kustomize (Kubernetes v1.14以降の<code>kubectl</code>に内蔵) が利用可能であること。</p></li>
<li><p><code>bash</code>、<code>jq</code>、<code>curl</code> コマンドが利用可能であること。</p></li>
<li><p><code>systemd</code> が動作するLinux環境であること。</p></li>
</ul>
<h2 class="wp-block-heading">2. 実装</h2>
<h3 class="wp-block-heading">2.1. 全体フロー</h3>
<p>安全な適用プロセスは、Kustomizeによる構成生成、<code>dry-run</code>による検証、そして実際の適用という段階を踏みます。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["Kustomization定義"] --> B{"kustomize build"};
B --> C["生成されたマニフェスト"];
C --> D{"kubectl dry-run --client | クライアント側検証"};
D --> E{"kubectl dry-run --server | サーバー側検証"};
E --> F["jqによる出力検証"];
F -- 異常なし --> G{"kubectl apply | 最終適用"};
G --> H["適用完了"];
D -- 検証失敗 --> I["エラー通知/中断"];
E -- 検証失敗 --> I;
F -- 検証失敗 --> I;
</pre></div>
<h3 class="wp-block-heading">2.2. Kustomizeベースの構成管理</h3>
<p>Kustomizeは、ベースとなるKubernetesマニフェストにパッチやオーバーレイを適用し、環境に応じたリソースを生成するためのツールです。Kustomizeは<code>2024年6月10日</code>にv5.3.0がリリースされており、活発に開発されています[4]。</p>
<p>例えば、<code>base/deployment.yaml</code>と<code>overlays/production/kustomization.yaml</code>があるとします。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
template:
spec:
containers:
- name: my-app
image: nginx:latest
ports:
- containerPort: 80
---
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patchesStrategicMerge:
- patch-replicas.yaml
# overlays/production/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3 # 本番環境では3レプリカ
</pre>
</div>
<p><code>kustomize build overlays/production</code> コマンドで本番環境向けのマニフェストが生成されます。</p>
<h3 class="wp-block-heading">2.3. 安全なシェルスクリプトの基礎</h3>
<p>適用スクリプトは、冪等性と安全性を確保するために以下のプラクティスに従います。</p>
<ul class="wp-block-list">
<li><p><code>set -euo pipefail</code>:</p>
<ul>
<li><p><code>e</code>: コマンドが失敗した場合、即座にスクリプトを終了します。</p></li>
<li><p><code>u</code>: 未定義の変数を参照した場合、エラーとして終了します。</p></li>
<li><p><code>o pipefail</code>: パイプライン内のいずれかのコマンドが失敗した場合、パイプライン全体が失敗と判断されます。</p></li>
</ul></li>
<li><p><code>trap cleanup EXIT</code>: スクリプトの終了時に必ず<code>cleanup</code>関数が実行されるように設定し、一時ファイルの削除などを行います。</p></li>
<li><p><code>mktemp -d</code>: 一時ディレクトリを作成し、作業ファイルを安全に隔離します。</p></li>
</ul>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
set -euo pipefail
# 一時ディレクトリの作成と終了時のクリーンアップ
# mktemp -dは競合を避け、一意なディレクトリを作成する
TMP_DIR=$(mktemp -d)
echo "一時ディレクトリ: $TMP_DIR"
cleanup() {
echo "一時ディレクトリを削除します: $TMP_DIR"
rm -rf "$TMP_DIR"
echo "スクリプトを終了します。"
}
trap cleanup EXIT # スクリプト終了時にcleanup関数を呼び出す
KUSTOMIZE_OVERLAY="overlays/production"
MANIFEST_FILE="$TMP_DIR/manifest.yaml"
echo "Kustomizeビルドを開始します..."
# Kustomizeビルドの結果を一時ファイルに格納
kubectl kustomize "$KUSTOMIZE_OVERLAY" > "$MANIFEST_FILE"
echo "マニフェストを $MANIFEST_FILE に出力しました。"
# ここに dry-run と jq による検証ステップを追加
# ...
# 最終的な適用 (検証後)
# echo "Kubernetesリソースを適用します..."
# kubectl apply -f "$MANIFEST_FILE"
</pre>
</div>
<h3 class="wp-block-heading">2.4. <code>kubectl dry-run</code> を用いた検証</h3>
<p><code>kubectl dry-run</code>は、実際にリソースを作成・更新せずに、変更がどのように適用されるかをシミュレーションします。</p>
<ul class="wp-block-list">
<li><p><code>--client</code>: <code>kubectl</code>クライアント側でのバリデーションのみを実行します。</p></li>
<li><p><code>--server</code>: Kubernetes APIサーバーにリクエストを送信し、Admission Controllerなどのサーバー側ロジックを通して検証しますが、リソースは永続化されません。より正確な検証が可能です。</p></li>
</ul>
<div class="codehilite">
<pre data-enlighter-language="generic"># dry-run検証の例(上記のシェルスクリプトに追加)
echo "kubectl dry-run --client でクライアント側検証を実行します..."
# クライアント側dry-runが失敗した場合、エラーメッセージを出力して終了
if ! kubectl apply -f "$MANIFEST_FILE" --dry-run=client --output=yaml > /dev/null; then
echo "エラー: クライアント側dry-run検証に失敗しました。" >&2
exit 1
fi
echo "クライアント側dry-run検証が成功しました。"
echo "kubectl dry-run --server でサーバー側検証を実行します..."
# サーバー側dry-runの結果をDRY_RUN_OUTPUT変数に格納
DRY_RUN_OUTPUT=$(kubectl apply -f "$MANIFEST_FILE" --dry-run=server --output=json 2>&1)
DRY_RUN_STATUS=$? # コマンドの終了ステータスを取得
if [ $DRY_RUN_STATUS -ne 0 ]; then
echo "エラー: サーバー側dry-run検証に失敗しました。" >&2
echo "$DRY_RUN_OUTPUT" >&2
exit 1
fi
echo "サーバー側dry-run検証が成功しました。"
# DRY_RUN_OUTPUT を jq でさらに検証
</pre>
</div>
<p><code>kubectl apply</code>の<code>--dry-run=server</code>オプションは、Kubernetes v1.29のドキュメント(<code>2024年1月15日</code>更新)にも記載されており、推奨される検証方法です[1]。</p>
<h3 class="wp-block-heading">2.5. <code>jq</code> による出力検証</h3>
<p><code>kubectl dry-run</code>やその他のAPIコマンドのJSON出力を<code>jq</code>で解析し、期待される値が含まれているか、エラーがないかなどを確認します。<code>jq</code>は<code>2024年4月2日</code>に<code>jq-1.7.1</code>がリリースされており、最新の環境でも利用可能です[2]。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># jqによる出力検証の例(上記のシェルスクリプトに追加)
# DRY_RUN_OUTPUT にサーバー側dry-runのJSON結果が入っていると仮定
echo "jq を用いてdry-run出力を検証します..."
# 例: 出力されたリソースの名前が期待通りか確認
# Deploymentリソースで、metadata.nameが"my-app"であるものを選択し、その.metadata.nameを取得
RESOURCE_NAME=$(echo "$DRY_RUN_OUTPUT" | jq -r 'select(.kind == "Deployment" and .metadata.name == "my-app") | .metadata.name')
if [ "$RESOURCE_NAME" != "my-app" ]; then
echo "エラー: 予期しないリソース名が検出されました: $RESOURCE_NAME" >&2
exit 1
fi
# 例: replicasが3になっているか確認 (パッチが適用されているか)
# Deploymentリソースで、metadata.nameが"my-app"であるものを選択し、その.spec.replicasを取得
REPLICAS_COUNT=$(echo "$DRY_RUN_OUTPUT" | jq -r 'select(.kind == "Deployment" and .metadata.name == "my-app") | .spec.replicas')
if [ "$REPLICAS_COUNT" != "3" ]; then
echo "エラー: replicas数が期待値(3)と異なります: $REPLICAS_COUNT" >&2
exit 1
fi
echo "jqによる出力検証が成功しました。"
# すべての検証が成功したら実際の適用
echo "すべての検証が成功しました。Kubernetesリソースを適用します..."
kubectl apply -f "$MANIFEST_FILE"
echo "Kubernetesリソースの適用が完了しました。"
</pre>
</div>
<h3 class="wp-block-heading">2.6. <code>curl</code> を用いたAPI連携(TLS/再試行/バックオフ)</h3>
<p>Kubernetesの適用スクリプト内で外部システム(CI/CDフック、監視システムなど)と連携する場合、<code>curl</code>を安全に利用することが重要です。<code>curl</code>は<code>2024年5月15日</code>に<code>8.8.0</code>がリリースされており、これらの機能は広くサポートされています[3]。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
set -euo pipefail
API_ENDPOINT="https://api.example.com/status"
# 現在時刻をUTCで取得し、ISO 8601形式にフォーマット
PAYLOAD='{"status": "kubernetes_applied", "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"}'
echo "外部APIに適用ステータスを通知します..."
if ! curl -X POST "$API_ENDPOINT" \
--header "Content-Type: application/json" \
--data "$PAYLOAD" \
--silent --show-error \
--fail-early \
--tlsv1.2 \
--retry 5 \
--retry-delay 5 \
--retry-max-time 60 \
--connect-timeout 10 \
--max-time 300; then
echo "エラー: 外部APIへの通知に失敗しました。" >&2
exit 1
fi
echo "外部APIへの通知が成功しました。"
</pre>
</div>
<ul class="wp-block-list">
<li><p><code>--tlsv1.2</code>: TLS 1.2以上のバージョンを強制し、安全な通信を保証します。</p></li>
<li><p><code>--retry 5</code>: 失敗した場合、最大5回リトライします。</p></li>
<li><p><code>--retry-delay 5</code>: 各リトライの間に5秒間待機します(バックオフ)。</p></li>
<li><p><code>--retry-max-time 60</code>: リトライを含むリクエスト全体の最大時間を60秒に制限します。</p></li>
<li><p><code>--connect-timeout 10</code>: 接続確立の最大時間を10秒に設定します。</p></li>
<li><p><code>--max-time 300</code>: 全操作の最大時間を300秒に設定します。</p></li>
</ul>
<p>これらのオプションは、ネットワークの不安定性や一時的なサービス停止に対する堅牢性を高めます。</p>
<h2 class="wp-block-heading">3. 検証</h2>
<p>上記で示したスクリプトは、<code>dry-run</code>および<code>jq</code>による検証ステップで構成されており、エラーが発生した場合は即座に中断するように設計されています。これにより、実際のKubernetesクラスタに変更が加えられる前に問題を発見し、修正することが可能です。手動でスクリプトを実行し、意図した通りの動作とエラーハンドリングが機能することを確認してください。</p>
<h2 class="wp-block-heading">4. 運用</h2>
<h3 class="wp-block-heading">4.1. <code>systemd unit/timer</code> での定期実行</h3>
<p><code>systemd</code>を使用して、作成したスクリプトを定期的に実行し、Kubernetesクラスタの構成を最新の状態に保つことができます。</p>
<h4 class="wp-block-heading">4.1.1. 適用スクリプト (<code>/usr/local/bin/apply-k8s-config.sh</code>)</h4>
<p>上記のシェルスクリプトを <code>/usr/local/bin/apply-k8s-config.sh</code> として保存し、実行権限を与えます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# apply-k8s-config.sh
set -euo pipefail
# スクリプト実行者として通常ユーザーを指定することを推奨
# 例えば、systemd unitでUser=myuserを設定する
# 一時ディレクトリを/var/tmp以下に作成し、システム全体で共有されにくいようにする
TMP_DIR=$(mktemp -d --tmpdir=/var/tmp dry-run-k8s-XXXXXX)
# ログファイルはシステムログとは別に管理し、journaldと重複して出力
LOG_DIR="/var/log/k8s-apply"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/apply-$(date +%Y%m%d-%H%M%S).log"
cleanup() {
echo "$(date +%Y-%m-%dT%H:%M:%SZ): 一時ディレクトリを削除します: $TMP_DIR"
rm -rf "$TMP_DIR"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): スクリプト終了。"
}
trap cleanup EXIT
# ログをjournaldに加えて、個別ファイルにも出力(systemdのstdout/stderrはjournaldに行く)
# execコマンドで標準出力と標準エラー出力をリダイレクト
exec >> "$LOG_FILE" 2>&1
echo "$(date +%Y-%m-%dT%H:%M:%SZ): Kubernetes構成適用スクリプトを開始します。"
# Kustomizeのベースディレクトリは環境に合わせて変更
KUSTOMIZE_OVERLAY="/opt/kustomize/overlays/production"
MANIFEST_FILE="$TMP_DIR/manifest.yaml"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): Kustomizeビルドを開始します..."
# Kustomizeビルドが失敗した場合、ログに出力して終了
if ! kubectl kustomize "$KUSTOMIZE_OVERLAY" > "$MANIFEST_FILE"; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: Kustomizeビルドに失敗しました。"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): マニフェストを $MANIFEST_FILE に出力しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): kubectl dry-run --client でクライアント側検証を実行します..."
if ! kubectl apply -f "$MANIFEST_FILE" --dry-run=client --output=yaml > /dev/null; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: クライアント側dry-run検証に失敗しました。"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): クライアント側dry-run検証が成功しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): kubectl dry-run --server でサーバー側検証を実行します..."
# サーバー側dry-runの結果をDRY_RUN_OUTPUT変数に格納
DRY_RUN_OUTPUT=$(kubectl apply -f "$MANIFEST_FILE" --dry-run=server --output=json)
DRY_RUN_STATUS=$?
if [ $DRY_RUN_STATUS -ne 0 ]; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: サーバー側dry-run検証に失敗しました。"
echo "$DRY_RUN_OUTPUT"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): サーバー側dry-run検証が成功しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): jq を用いてdry-run出力を検証します..."
# 例: replicasが3になっているか確認
REPLICAS_COUNT=$(echo "$DRY_RUN_OUTPUT" | jq -r 'select(.kind == "Deployment" and .metadata.name == "my-app") | .spec.replicas')
if [ "$REPLICAS_COUNT" != "3" ]; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: replicas数が期待値(3)と異なります: $REPLICAS_COUNT"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): jqによる出力検証が成功しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): すべての検証が成功しました。Kubernetesリソースを適用します..."
kubectl apply -f "$MANIFEST_FILE"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): Kubernetesリソースの適用が完了しました。"
exit 0
</pre>
</div>
<p>(<code>sudo chmod +x /usr/local/bin/apply-k8s-config.sh</code> を実行)</p>
<h4 class="wp-block-heading">4.1.2. systemd Unitファイル (<code>/etc/systemd/system/k8s-config-apply.service</code>)</h4>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Apply Kubernetes Configuration
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/apply-k8s-config.sh
WorkingDirectory=/opt/kustomize # Kustomizeのベースディレクトリの親を設定
User=k8sadmin # 専用のシステムユーザーを指定
Group=k8sadmin # 専用のシステムグループを指定
Environment="KUBECONFIG=/home/k8sadmin/.kube/config" # kubeconfigのパスをユーザーのホームディレクトリに設定
StandardOutput=journal # 標準出力はjournaldに記録
StandardError=journal # 標準エラーもjournaldに記録
# Restart=on-failure # タイマー実行なので基本的に不要だが、手動実行時の堅牢性を高めるなら設定
</pre>
</div>
<h4 class="wp-block-heading">4.1.3. systemd Timerファイル (<code>/etc/systemd/system/k8s-config-apply.timer</code>)</h4>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Run Kubernetes Configuration Apply Script Daily
[Timer]
OnCalendar=daily # 毎日実行
# OnCalendar=*-*-* 03:00:00 # 毎日午前3時00分00秒に実行する場合
Persistent=true # タイマーが非アクティブな間に発生したイベントを実行(システムの起動時など)
Unit=k8s-config-apply.service # 実行するサービスユニット
[Install]
WantedBy=timers.target
</pre>
</div>
<h4 class="wp-block-heading">4.1.4. systemdの有効化と起動</h4>
<div class="codehilite">
<pre data-enlighter-language="generic"># systemd設定をリロード
sudo systemctl daemon-reload
# タイマーを有効化
sudo systemctl enable k8s-config-apply.timer
# タイマーを起動
sudo systemctl start k8s-config-apply.timer
# タイマーのステータスを確認
sudo systemctl status k8s-config-apply.timer
</pre>
</div>
<p>ログは<code>journalctl -u k8s-config-apply.service</code>で確認できます。また、スクリプト内で指定した<code>/var/log/k8s-apply/</code>ディレクトリにも出力されます。</p>
<h3 class="wp-block-heading">4.2. root権限の扱いと権限分離</h3>
<p><strong>root権限でのスクリプト実行は避けるべきです。</strong></p>
<ul class="wp-block-list">
<li><p><strong>専用ユーザーの作成</strong>: <code>k8sadmin</code>のような専用のシステムユーザーを作成し、そのユーザーに<code>kubectl</code>実行に必要な最小限の権限(<code>kubeconfig</code>ファイルへの読み取り権限、Kubernetesクラスタ上でのRBACロール)のみを付与します。</p></li>
<li><p><strong>Principle of Least Privilege (最小権限の原則)</strong>: <code>k8sadmin</code>ユーザーは、<code>systemd</code>サービスファイルで指定された<code>ExecStart</code>コマンドと、そのコマンドがアクセスするファイル(Kustomize定義、<code>kubeconfig</code>など)にのみアクセス権を持つようにします。</p></li>
<li><p><strong><code>kubeconfig</code>の制限</strong>: <code>kubeconfig</code>ファイルには、<code>k8sadmin</code>が操作する名前空間やリソースに限定された権限を持つServiceAccountトークンや証明書を使用します。</p></li>
<li><p><strong>パスワードや秘密鍵の管理</strong>: 環境変数、Secrets管理ツール、あるいは<code>systemd-creds</code>のような機能を使用して、認証情報を安全に渡します。スクリプト内にハードコードしてはいけません。</p></li>
</ul>
<p>これにより、万が一スクリプトに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます。</p>
<h2 class="wp-block-heading">5. トラブルシュート</h2>
<ul class="wp-block-list">
<li><p><strong>スクリプトが実行されない</strong>:</p>
<ul>
<li><p><code>sudo systemctl status k8s-config-apply.timer</code>でタイマーが有効かつアクティブか確認します。</p></li>
<li><p><code>sudo systemctl status k8s-config-apply.service</code>でサービスの状態を確認します。</p></li>
<li><p><code>sudo journalctl -u k8s-config-apply.service</code>でサービスのログを確認し、エラーメッセージがないか確認します。</p></li>
<li><p>スクリプトファイルのパスや実行権限(<code>chmod +x</code>)が正しいか確認します。</p></li>
</ul></li>
<li><p><strong><code>kubectl</code>コマンドが失敗する</strong>:</p>
<ul>
<li><p><code>kubeconfig</code>ファイルのパス(<code>Environment="KUBECONFIG=..."</code>)が正しいか、そのファイルへの読み取り権限が<code>User</code>に指定されたユーザーにあるか確認します。</p></li>
<li><p>Kubernetesクラスタへのネットワーク接続が安定しているか確認します。</p></li>
<li><p><code>kubectl get --raw=/healthz</code>などでAPIサーバーのヘルスチェックを行います。</p></li>
<li><p><code>kubectl auth can-i ...</code>で、<code>k8sadmin</code>ユーザーに十分なRBAC権限があるか確認します。</p></li>
</ul></li>
<li><p><strong><code>dry-run</code>が失敗するが、手動では成功する</strong>:</p>
<ul>
<li><p><code>systemd</code>環境と手動実行環境で、環境変数(<code>PATH</code>, <code>KUBECONFIG</code>など)やカレントディレクトリが異なる可能性があります。<code>systemd</code>ユニットファイルで明示的に設定します。</p></li>
<li><p><code>jq</code>の構文エラーがないか確認します。</p></li>
</ul></li>
<li><p><strong><code>jq</code>による検証が期待通りに動作しない</strong>:</p>
<ul>
<li><code>kubectl dry-run --output=json</code>の実際の出力を確認し、<code>jq</code>フィルターが正しくパスをたどっているかテストします。</li>
</ul></li>
</ul>
<h2 class="wp-block-heading">6. まとめ</h2>
<p>、<code>kubectl dry-run</code>とKustomizeを核とし、シェルスクリプトのベストプラクティス、<code>jq</code>による出力検証、<code>curl</code>による安全なAPI連携、そして<code>systemd</code>による自動化を組み合わせることで、Kubernetesへの安全かつ冪等なリソース適用プロセスをDevOpsエンジニアの視点から解説しました。</p>
<p>重要なのは、変更を適用する前に徹底的に検証を行い、予期せぬ動作を防ぐことです。また、運用においては最小権限の原則を徹底し、セキュリティリスクを最小限に抑えることが不可欠です。これらのプラクティスを導入することで、Kubernetes環境の安定性と信頼性を大幅に向上させることができます。</p>
<hr/>
<p><strong>References:</strong>
[1] Kubernetes Documentation, “API Concepts – Dry Run,” Kubernetes Project, Last updated <code>2024年1月15日</code>. Available: <code>https://kubernetes.io/docs/reference/using-api/api-concepts/#dry-run</code>
[2] jq Releases, “jq 1.7.1,” <code>jqlang.github.io</code>, Released <code>2024年4月2日</code>. Available: <code>https://jqlang.github.io/jq/download/</code>
[3] curl Releases, “curl 8.8.0,” <code>curl.se</code>, Released <code>2024年5月15日</code>. Available: <code>https://curl.se/changes.html#8_8_0</code>
[4] Kustomize, “Official Website,” <code>kustomize.io</code>, Last updated <code>2024年6月10日</code>. Available: <code>https://kustomize.io/</code></p>
kubectl dry-runとKustomizeで安全なKubernetes適用を実現する
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
1. 要件と前提
Kubernetes環境へのマニフェスト適用は、意図しない変更やダウンタイムを引き起こすリスクがあります。本記事では、DevOpsエンジニアとして、kubectl dry-runとKustomizeを組み合わせ、安全かつ冪等にKubernetesリソースを適用する手法を解説します。
1.1. 要件
kubectl dry-run を利用した変更の事前検証。
Kustomize を用いた環境ごとのリソースカスタマイズ。
シェルスクリプトによる自動化と安全性の確保(set -euo pipefail、trap、mktemp -d)。
jq を用いたJSON出力の検証。
curl を用いたAPI連携におけるTLS、再試行、バックオフの考慮。
systemd unit/timer による定期的な適用スクリプトの実行とログ管理。
root権限の扱いと権限分離に関する注意喚起。
1.2. 前提
Kubernetesクラスタへのアクセス権限とkubectlコマンドが設定済みであること。
Kustomize (Kubernetes v1.14以降のkubectlに内蔵) が利用可能であること。
bash、jq、curl コマンドが利用可能であること。
systemd が動作するLinux環境であること。
2. 実装
2.1. 全体フロー
安全な適用プロセスは、Kustomizeによる構成生成、dry-runによる検証、そして実際の適用という段階を踏みます。
graph TD
A["Kustomization定義"] --> B{"kustomize build"};
B --> C["生成されたマニフェスト"];
C --> D{"kubectl dry-run --client | クライアント側検証"};
D --> E{"kubectl dry-run --server | サーバー側検証"};
E --> F["jqによる出力検証"];
F -- 異常なし --> G{"kubectl apply | 最終適用"};
G --> H["適用完了"];
D -- 検証失敗 --> I["エラー通知/中断"];
E -- 検証失敗 --> I;
F -- 検証失敗 --> I;
2.2. Kustomizeベースの構成管理
Kustomizeは、ベースとなるKubernetesマニフェストにパッチやオーバーレイを適用し、環境に応じたリソースを生成するためのツールです。Kustomizeは2024年6月10日にv5.3.0がリリースされており、活発に開発されています[4]。
例えば、base/deployment.yamlとoverlays/production/kustomization.yamlがあるとします。
# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
template:
spec:
containers:
- name: my-app
image: nginx:latest
ports:
- containerPort: 80
---
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patchesStrategicMerge:
- patch-replicas.yaml
# overlays/production/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3 # 本番環境では3レプリカ
kustomize build overlays/production コマンドで本番環境向けのマニフェストが生成されます。
2.3. 安全なシェルスクリプトの基礎
適用スクリプトは、冪等性と安全性を確保するために以下のプラクティスに従います。
#!/bin/bash
set -euo pipefail
# 一時ディレクトリの作成と終了時のクリーンアップ
# mktemp -dは競合を避け、一意なディレクトリを作成する
TMP_DIR=$(mktemp -d)
echo "一時ディレクトリ: $TMP_DIR"
cleanup() {
echo "一時ディレクトリを削除します: $TMP_DIR"
rm -rf "$TMP_DIR"
echo "スクリプトを終了します。"
}
trap cleanup EXIT # スクリプト終了時にcleanup関数を呼び出す
KUSTOMIZE_OVERLAY="overlays/production"
MANIFEST_FILE="$TMP_DIR/manifest.yaml"
echo "Kustomizeビルドを開始します..."
# Kustomizeビルドの結果を一時ファイルに格納
kubectl kustomize "$KUSTOMIZE_OVERLAY" > "$MANIFEST_FILE"
echo "マニフェストを $MANIFEST_FILE に出力しました。"
# ここに dry-run と jq による検証ステップを追加
# ...
# 最終的な適用 (検証後)
# echo "Kubernetesリソースを適用します..."
# kubectl apply -f "$MANIFEST_FILE"
2.4. kubectl dry-run を用いた検証
kubectl dry-runは、実際にリソースを作成・更新せずに、変更がどのように適用されるかをシミュレーションします。
# dry-run検証の例(上記のシェルスクリプトに追加)
echo "kubectl dry-run --client でクライアント側検証を実行します..."
# クライアント側dry-runが失敗した場合、エラーメッセージを出力して終了
if ! kubectl apply -f "$MANIFEST_FILE" --dry-run=client --output=yaml > /dev/null; then
echo "エラー: クライアント側dry-run検証に失敗しました。" >&2
exit 1
fi
echo "クライアント側dry-run検証が成功しました。"
echo "kubectl dry-run --server でサーバー側検証を実行します..."
# サーバー側dry-runの結果をDRY_RUN_OUTPUT変数に格納
DRY_RUN_OUTPUT=$(kubectl apply -f "$MANIFEST_FILE" --dry-run=server --output=json 2>&1)
DRY_RUN_STATUS=$? # コマンドの終了ステータスを取得
if [ $DRY_RUN_STATUS -ne 0 ]; then
echo "エラー: サーバー側dry-run検証に失敗しました。" >&2
echo "$DRY_RUN_OUTPUT" >&2
exit 1
fi
echo "サーバー側dry-run検証が成功しました。"
# DRY_RUN_OUTPUT を jq でさらに検証
kubectl applyの--dry-run=serverオプションは、Kubernetes v1.29のドキュメント(2024年1月15日更新)にも記載されており、推奨される検証方法です[1]。
2.5. jq による出力検証
kubectl dry-runやその他のAPIコマンドのJSON出力をjqで解析し、期待される値が含まれているか、エラーがないかなどを確認します。jqは2024年4月2日にjq-1.7.1がリリースされており、最新の環境でも利用可能です[2]。
# jqによる出力検証の例(上記のシェルスクリプトに追加)
# DRY_RUN_OUTPUT にサーバー側dry-runのJSON結果が入っていると仮定
echo "jq を用いてdry-run出力を検証します..."
# 例: 出力されたリソースの名前が期待通りか確認
# Deploymentリソースで、metadata.nameが"my-app"であるものを選択し、その.metadata.nameを取得
RESOURCE_NAME=$(echo "$DRY_RUN_OUTPUT" | jq -r 'select(.kind == "Deployment" and .metadata.name == "my-app") | .metadata.name')
if [ "$RESOURCE_NAME" != "my-app" ]; then
echo "エラー: 予期しないリソース名が検出されました: $RESOURCE_NAME" >&2
exit 1
fi
# 例: replicasが3になっているか確認 (パッチが適用されているか)
# Deploymentリソースで、metadata.nameが"my-app"であるものを選択し、その.spec.replicasを取得
REPLICAS_COUNT=$(echo "$DRY_RUN_OUTPUT" | jq -r 'select(.kind == "Deployment" and .metadata.name == "my-app") | .spec.replicas')
if [ "$REPLICAS_COUNT" != "3" ]; then
echo "エラー: replicas数が期待値(3)と異なります: $REPLICAS_COUNT" >&2
exit 1
fi
echo "jqによる出力検証が成功しました。"
# すべての検証が成功したら実際の適用
echo "すべての検証が成功しました。Kubernetesリソースを適用します..."
kubectl apply -f "$MANIFEST_FILE"
echo "Kubernetesリソースの適用が完了しました。"
2.6. curl を用いたAPI連携(TLS/再試行/バックオフ)
Kubernetesの適用スクリプト内で外部システム(CI/CDフック、監視システムなど)と連携する場合、curlを安全に利用することが重要です。curlは2024年5月15日に8.8.0がリリースされており、これらの機能は広くサポートされています[3]。
#!/bin/bash
set -euo pipefail
API_ENDPOINT="https://api.example.com/status"
# 現在時刻をUTCで取得し、ISO 8601形式にフォーマット
PAYLOAD='{"status": "kubernetes_applied", "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"}'
echo "外部APIに適用ステータスを通知します..."
if ! curl -X POST "$API_ENDPOINT" \
--header "Content-Type: application/json" \
--data "$PAYLOAD" \
--silent --show-error \
--fail-early \
--tlsv1.2 \
--retry 5 \
--retry-delay 5 \
--retry-max-time 60 \
--connect-timeout 10 \
--max-time 300; then
echo "エラー: 外部APIへの通知に失敗しました。" >&2
exit 1
fi
echo "外部APIへの通知が成功しました。"
--tlsv1.2: TLS 1.2以上のバージョンを強制し、安全な通信を保証します。
--retry 5: 失敗した場合、最大5回リトライします。
--retry-delay 5: 各リトライの間に5秒間待機します(バックオフ)。
--retry-max-time 60: リトライを含むリクエスト全体の最大時間を60秒に制限します。
--connect-timeout 10: 接続確立の最大時間を10秒に設定します。
--max-time 300: 全操作の最大時間を300秒に設定します。
これらのオプションは、ネットワークの不安定性や一時的なサービス停止に対する堅牢性を高めます。
3. 検証
上記で示したスクリプトは、dry-runおよびjqによる検証ステップで構成されており、エラーが発生した場合は即座に中断するように設計されています。これにより、実際のKubernetesクラスタに変更が加えられる前に問題を発見し、修正することが可能です。手動でスクリプトを実行し、意図した通りの動作とエラーハンドリングが機能することを確認してください。
4. 運用
4.1. systemd unit/timer での定期実行
systemdを使用して、作成したスクリプトを定期的に実行し、Kubernetesクラスタの構成を最新の状態に保つことができます。
4.1.1. 適用スクリプト (/usr/local/bin/apply-k8s-config.sh)
上記のシェルスクリプトを /usr/local/bin/apply-k8s-config.sh として保存し、実行権限を与えます。
#!/bin/bash
# apply-k8s-config.sh
set -euo pipefail
# スクリプト実行者として通常ユーザーを指定することを推奨
# 例えば、systemd unitでUser=myuserを設定する
# 一時ディレクトリを/var/tmp以下に作成し、システム全体で共有されにくいようにする
TMP_DIR=$(mktemp -d --tmpdir=/var/tmp dry-run-k8s-XXXXXX)
# ログファイルはシステムログとは別に管理し、journaldと重複して出力
LOG_DIR="/var/log/k8s-apply"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/apply-$(date +%Y%m%d-%H%M%S).log"
cleanup() {
echo "$(date +%Y-%m-%dT%H:%M:%SZ): 一時ディレクトリを削除します: $TMP_DIR"
rm -rf "$TMP_DIR"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): スクリプト終了。"
}
trap cleanup EXIT
# ログをjournaldに加えて、個別ファイルにも出力(systemdのstdout/stderrはjournaldに行く)
# execコマンドで標準出力と標準エラー出力をリダイレクト
exec >> "$LOG_FILE" 2>&1
echo "$(date +%Y-%m-%dT%H:%M:%SZ): Kubernetes構成適用スクリプトを開始します。"
# Kustomizeのベースディレクトリは環境に合わせて変更
KUSTOMIZE_OVERLAY="/opt/kustomize/overlays/production"
MANIFEST_FILE="$TMP_DIR/manifest.yaml"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): Kustomizeビルドを開始します..."
# Kustomizeビルドが失敗した場合、ログに出力して終了
if ! kubectl kustomize "$KUSTOMIZE_OVERLAY" > "$MANIFEST_FILE"; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: Kustomizeビルドに失敗しました。"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): マニフェストを $MANIFEST_FILE に出力しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): kubectl dry-run --client でクライアント側検証を実行します..."
if ! kubectl apply -f "$MANIFEST_FILE" --dry-run=client --output=yaml > /dev/null; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: クライアント側dry-run検証に失敗しました。"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): クライアント側dry-run検証が成功しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): kubectl dry-run --server でサーバー側検証を実行します..."
# サーバー側dry-runの結果をDRY_RUN_OUTPUT変数に格納
DRY_RUN_OUTPUT=$(kubectl apply -f "$MANIFEST_FILE" --dry-run=server --output=json)
DRY_RUN_STATUS=$?
if [ $DRY_RUN_STATUS -ne 0 ]; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: サーバー側dry-run検証に失敗しました。"
echo "$DRY_RUN_OUTPUT"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): サーバー側dry-run検証が成功しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): jq を用いてdry-run出力を検証します..."
# 例: replicasが3になっているか確認
REPLICAS_COUNT=$(echo "$DRY_RUN_OUTPUT" | jq -r 'select(.kind == "Deployment" and .metadata.name == "my-app") | .spec.replicas')
if [ "$REPLICAS_COUNT" != "3" ]; then
echo "$(date +%Y-%m-%dT%H:%M:%SZ): エラー: replicas数が期待値(3)と異なります: $REPLICAS_COUNT"
exit 1
fi
echo "$(date +%Y-%m-%dT%H:%M:%SZ): jqによる出力検証が成功しました。"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): すべての検証が成功しました。Kubernetesリソースを適用します..."
kubectl apply -f "$MANIFEST_FILE"
echo "$(date +%Y-%m-%dT%H:%M:%SZ): Kubernetesリソースの適用が完了しました。"
exit 0
(sudo chmod +x /usr/local/bin/apply-k8s-config.sh を実行)
4.1.2. systemd Unitファイル (/etc/systemd/system/k8s-config-apply.service)
[Unit]
Description=Apply Kubernetes Configuration
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/apply-k8s-config.sh
WorkingDirectory=/opt/kustomize # Kustomizeのベースディレクトリの親を設定
User=k8sadmin # 専用のシステムユーザーを指定
Group=k8sadmin # 専用のシステムグループを指定
Environment="KUBECONFIG=/home/k8sadmin/.kube/config" # kubeconfigのパスをユーザーのホームディレクトリに設定
StandardOutput=journal # 標準出力はjournaldに記録
StandardError=journal # 標準エラーもjournaldに記録
# Restart=on-failure # タイマー実行なので基本的に不要だが、手動実行時の堅牢性を高めるなら設定
4.1.3. systemd Timerファイル (/etc/systemd/system/k8s-config-apply.timer)
[Unit]
Description=Run Kubernetes Configuration Apply Script Daily
[Timer]
OnCalendar=daily # 毎日実行
# OnCalendar=*-*-* 03:00:00 # 毎日午前3時00分00秒に実行する場合
Persistent=true # タイマーが非アクティブな間に発生したイベントを実行(システムの起動時など)
Unit=k8s-config-apply.service # 実行するサービスユニット
[Install]
WantedBy=timers.target
4.1.4. systemdの有効化と起動
# systemd設定をリロード
sudo systemctl daemon-reload
# タイマーを有効化
sudo systemctl enable k8s-config-apply.timer
# タイマーを起動
sudo systemctl start k8s-config-apply.timer
# タイマーのステータスを確認
sudo systemctl status k8s-config-apply.timer
ログはjournalctl -u k8s-config-apply.serviceで確認できます。また、スクリプト内で指定した/var/log/k8s-apply/ディレクトリにも出力されます。
4.2. root権限の扱いと権限分離
root権限でのスクリプト実行は避けるべきです。
専用ユーザーの作成: k8sadminのような専用のシステムユーザーを作成し、そのユーザーにkubectl実行に必要な最小限の権限(kubeconfigファイルへの読み取り権限、Kubernetesクラスタ上でのRBACロール)のみを付与します。
Principle of Least Privilege (最小権限の原則): k8sadminユーザーは、systemdサービスファイルで指定されたExecStartコマンドと、そのコマンドがアクセスするファイル(Kustomize定義、kubeconfigなど)にのみアクセス権を持つようにします。
kubeconfigの制限: kubeconfigファイルには、k8sadminが操作する名前空間やリソースに限定された権限を持つServiceAccountトークンや証明書を使用します。
パスワードや秘密鍵の管理: 環境変数、Secrets管理ツール、あるいはsystemd-credsのような機能を使用して、認証情報を安全に渡します。スクリプト内にハードコードしてはいけません。
これにより、万が一スクリプトに脆弱性があった場合でも、システム全体への影響を最小限に抑えることができます。
5. トラブルシュート
スクリプトが実行されない:
sudo systemctl status k8s-config-apply.timerでタイマーが有効かつアクティブか確認します。
sudo systemctl status k8s-config-apply.serviceでサービスの状態を確認します。
sudo journalctl -u k8s-config-apply.serviceでサービスのログを確認し、エラーメッセージがないか確認します。
スクリプトファイルのパスや実行権限(chmod +x)が正しいか確認します。
kubectlコマンドが失敗する:
kubeconfigファイルのパス(Environment="KUBECONFIG=...")が正しいか、そのファイルへの読み取り権限がUserに指定されたユーザーにあるか確認します。
Kubernetesクラスタへのネットワーク接続が安定しているか確認します。
kubectl get --raw=/healthzなどでAPIサーバーのヘルスチェックを行います。
kubectl auth can-i ...で、k8sadminユーザーに十分なRBAC権限があるか確認します。
dry-runが失敗するが、手動では成功する:
jqによる検証が期待通りに動作しない:
kubectl dry-run --output=jsonの実際の出力を確認し、jqフィルターが正しくパスをたどっているかテストします。
6. まとめ
、kubectl dry-runとKustomizeを核とし、シェルスクリプトのベストプラクティス、jqによる出力検証、curlによる安全なAPI連携、そしてsystemdによる自動化を組み合わせることで、Kubernetesへの安全かつ冪等なリソース適用プロセスをDevOpsエンジニアの視点から解説しました。
重要なのは、変更を適用する前に徹底的に検証を行い、予期せぬ動作を防ぐことです。また、運用においては最小権限の原則を徹底し、セキュリティリスクを最小限に抑えることが不可欠です。これらのプラクティスを導入することで、Kubernetes環境の安定性と信頼性を大幅に向上させることができます。
References:
[1] Kubernetes Documentation, “API Concepts – Dry Run,” Kubernetes Project, Last updated 2024年1月15日. Available: https://kubernetes.io/docs/reference/using-api/api-concepts/#dry-run
[2] jq Releases, “jq 1.7.1,” jqlang.github.io, Released 2024年4月2日. Available: https://jqlang.github.io/jq/download/
[3] curl Releases, “curl 8.8.0,” curl.se, Released 2024年5月15日. Available: https://curl.se/changes.html#8_8_0
[4] Kustomize, “Official Website,” kustomize.io, Last updated 2024年6月10日. Available: https://kustomize.io/
コメント