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. 安全なシェルスクリプトの基礎
適用スクリプトは、冪等性と安全性を確保するために以下のプラクティスに従います。
set -euo pipefail:e: コマンドが失敗した場合、即座にスクリプトを終了します。u: 未定義の変数を参照した場合、エラーとして終了します。o pipefail: パイプライン内のいずれかのコマンドが失敗した場合、パイプライン全体が失敗と判断されます。
trap cleanup EXIT: スクリプトの終了時に必ずcleanup関数が実行されるように設定し、一時ファイルの削除などを行います。mktemp -d: 一時ディレクトリを作成し、作業ファイルを安全に隔離します。
#!/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は、実際にリソースを作成・更新せずに、変更がどのように適用されるかをシミュレーションします。
--client:kubectlクライアント側でのバリデーションのみを実行します。--server: Kubernetes APIサーバーにリクエストを送信し、Admission Controllerなどのサーバー側ロジックを通して検証しますが、リソースは永続化されません。より正確な検証が可能です。
# 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が失敗するが、手動では成功する:systemd環境と手動実行環境で、環境変数(PATH,KUBECONFIGなど)やカレントディレクトリが異なる可能性があります。systemdユニットファイルで明示的に設定します。jqの構文エラーがないか確認します。
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/

コメント