Amazon EKS のクラスター証明書認証局(CA)ローテーション【後編】検証に基づく移行手順

記事タイトルとURLをコピーする

こんにちは。アプリケーションサービス部の山本です。
いつも読んでいただき、ありがとうございます。😺

はじめに

2026 年 8 月 20 日、Amazon EKS がクラスターの証明書認証局(CA)のローテーションに対応しました。

本記事は前後編の後編です。前編では、証明書認証局の役割、Amazon EKS における扱い、今回提供された仕組みを整理しました。本記事では、検証環境で一連のローテーションを実施した結果を交えながら、実際の移行手順を工程順に説明します。

前編: Amazon EKS のクラスター証明書認証局(CA)ローテーション【前編】仕組みと背景

本記事の手順は、概要とコマンドを別々の章に分けていません。各工程を 実行するコマンド、検証で観測した結果、注意点、完了条件 の順にまとめています。作業時は、上から順番に確認してください。

先に結論

移行で特に重要な点は、次のとおりです。

  • 移行は 7 工程です。後継 CA を追加した後、外部クライアントと通常のワーカーノードを更新してから、後継 CA を有効化します。加えて、当社の検証に基づく推奨として 工程 6-2 を挟みます
  • スケジュールを決めるのは 工程 4 の外部クライアント更新です。ここだけは Amazon EKS の処理時間ではなく、接続元の数と社内の体制で期間が決まります
  • 工程 3 の完了条件は、describe-update の Successful ではなく、describe-certificate-authority の distributionStatus=COMPLETE です
  • 有効化後は kubectl get nodes の表示だけで判断しません。ノードの生存報告である Node Lease、外部クライアント、API サーバーと通信するコンポーネントも確認します。Ready 表示は異常の反映が遅れるためです
  • 問題があれば、ロールバック可能期間中に旧 CA を再度有効化します
  • 旧 CA を削除する前に、工程 6-2 として通常のワーカーノードをもう一度入れ替えます。当社の検証では、これを省くと kubectl exec や kubectl logs が失敗しました(公式ドキュメントには記載がない挙動です)
  • 旧 CA の削除は、十分な動作確認を終えて、ロールバックが不要と判断してから実施します。削除後は元に戻せません

検証では、distributionStatus=COMPLETE を確認した後、外部クライアントと通常のワーカーノードを更新してから後継 CA を有効化しました。有効化後も Node Lease の更新は継続し、4 つの Web サーバー Pod は正常に稼働しました。ノード入れ替え中の応答計測では、4,123 件中 1 件の瞬間的な失敗を観測したものの、継続したサービス停止は確認されませんでした。

適用範囲と検証環境

検証環境

項目 内容
リージョン アジアパシフィック(東京)
Kubernetes バージョン 1.36
CA 切り替え検証 マネージドノードグループ(t3.medium × 2、Amazon Linux 2023)、nginx 4 レプリカ
ノード入れ替え中の応答計測 アプリケーション用 t3.medium × 2、計測用 t3.small × 1、nginx 6 レプリカ
AWS CLI 2.36.31

本記事では、マネージドノードグループを使用して検証しました。EKS Auto Mode、AWS Fargate、Karpenter については、公式仕様を基に対応方針を記載していますが、実際の入れ替え動作までは確認していません。

AWS CLI のバージョンにご注意ください

AWS CLI 2.36.21 では、CA ローテーションのコマンドを実行できず、次のエラーになりました。

Found invalid choice 'list-certificate-authorities'

2.36.31 では正常に実行できました。厳密な最小対応バージョンは確認できていないため、「2.36.31 以上が必須」とは断定できませんが、作業前に使用中の AWS CLI で対象コマンドを利用できることを確認してください。

aws --version
aws eks list-certificate-authorities help

使用する API は、create、describe、list、activate、delete の 5 つの certificate-authority 系コマンドです。ロールバック専用の API はなく、旧 CA に対して activate-certificate-authority を実行することで元に戻します。

実測結果と公式仕様を区別します

本記事の所要時間と挙動は、上記環境での実測結果です。環境によって変わる可能性があるため、作業時間の保証値ではありません。工程 1 から工程 7 まで、および工程 6-2 は、いずれも実際に検証環境で実施しています。

移行の全体像

CA ローテーションでは、最初に新旧両方の CA を信頼できる状態を作ります。その間にお客様が管理する接続元を更新し、準備が整ってから証明書へ署名する CA を切り替えます。

工程 実施内容 主な担当 検証環境での所要時間 完了条件
1 現在の CA と更新対象を確認する お客様 数分 旧 CA の ID、期限、更新対象を記録している
2 後継 CA を追加する お客様 受付は即時 UpdateId と後継 CA の ID を保存している
3 二重信頼の準備完了を待つ Amazon EKS/お客様は確認 約 19 分から 24 分 distributionStatus=COMPLETE
4 外部クライアントを更新する お客様 検証環境は 1 つのみ。実際の所要時間はお客様の環境次第です 全接続元が新旧両方の CA を信頼し、接続できる
5 通常のワーカーノードを入れ替える お客様 約 18 分(2 台) 対象ノードの入れ替えと更新処理が完了している
6 後継 CA を有効化する お客様 5 分から 9 分 後継 CA が IN_USE で、全接続元が正常に動作している
確認 必要に応じて旧 CA へ戻す お客様 環境により異なる(実測は後半の補足検証) 問題が解消し、再切り替えの準備ができている
6-2 通常のワーカーノードを再度入れ替える
(当社検証に基づく推奨)
お客様 約 16 分(2 台) kubelet のサーバー証明書が後継 CA でのみ検証できる
7 旧 CA を削除する お客様 約 9 分 信頼バンドルが後継 CA のみになっている

工程 3、5、6、7 は Amazon EKS 側の処理時間が中心のため、検証環境の実測値がある程度の目安になります。一方、工程 4 だけは性質が異なります。 所要時間を決めるのは API の処理ではなく、接続元がいくつあるか、その設定を誰が管理しているか、変更にどのような承認や試験が必要かです。検証環境の接続元は 1 つだけだったため、参考になる実測値はありません。

工程 4 は、CA ローテーション全体のスケジュールを左右します。まず接続元の洗い出しから始めて、自組織の体制に合わせて期間を見積もってください。

署名する CA と、クラスターが返す信頼バンドルの状態は次のように変化します。図中の「署名」は証明書へ署名する CA、「信頼」はクラスターの信頼バンドルです。

%%{init: {'flowchart': {'wrappingWidth': 500, 'nodeSpacing': 50, 'rankSpacing': 60}}}%%
graph TD
    S0["開始<br/>署名: 旧 CA<br/>信頼: 旧 CA"]
    S1["工程 2〜3 配布中<br/>署名: 旧 CA<br/>信頼: 旧 CA から<br/>旧 + 後継 へ移行中"]
    S2["工程 3 完了〜工程 5<br/>署名: 旧 CA<br/>信頼: 旧 + 後継 CA"]
    S3["工程 6 有効化後<br/>署名: 後継 CA<br/>信頼: 旧 + 後継 CA"]
    S4["工程 7 削除後<br/>署名: 後継 CA<br/>信頼: 後継 CA のみ"]

    S0 -->|"create-certificate-authority"| S1
    S1 -->|"distributionStatus=COMPLETE"| S2
    S2 -->|"activate-certificate-authority"| S3
    S3 -->|"delete-certificate-authority"| S4
    S3 -.->|"ロールバック"| S2

    style S2 fill:#e8f4ea,stroke:#2e7d32
    style S3 fill:#fff4e5,stroke:#ef6c00
    style S4 fill:#e3f2fd,stroke:#1565c0

activate-certificate-authority は、証明書へ署名する CA を切り替える操作です。旧 CA を信頼バンドルから取り除く操作ではありません。旧 CA は工程 7 まで残るため、ロールバック可能期間中であれば元に戻せます。

表のとおり、ノードの入れ替えは有効化の前後で 2 回必要です。 工程 5 はノードが後継 CA を信頼できるようにするため、工程 6-2 は旧 CA を削除しても kubelet のサーバー証明書が有効なままにするためです。工程 6-2 は当社の検証に基づく推奨で、理由は工程 6-2 で説明します。

更新対象の分担

対象 具体例 更新方法
コントロールプレーン、EKS Capabilities API サーバー、AWS 管理のアドオン Amazon EKS が対応します
通常のワーカーノード マネージドノードグループ、セルフマネージドノード お客様が入れ替えます(工程 5)
外部クライアント kubeconfig、CI/CD、監視、バックアップ、Terraform、別環境の GitOps ツール お客様が更新します(工程 4)
対象クラスター内の Pod クラスター内のコントローラー、オペレーター、アプリケーション kube-root-ca.crt を通じて反映されます。ただし、プロセスによっては再起動が必要です

本記事で 外部クライアント と呼ぶのは、製品の種類ではなく、対象の Amazon EKS クラスターの外から Kubernetes API サーバーへ接続する端末やサービスです。別のクラスターで稼働する Argo CD や Flux も、対象クラスターから見れば外部クライアントに含まれます。

実行前チェック

作業を始める前に、次の項目を確認します。

確認する場所 確認する内容
各接続元の設定 特定の CA 証明書だけを信頼する設定(CA ピンニング)がないか
設定ファイル、Secret、Infrastructure as Code、Helm の values CA 証明書を直接記述していないか
運用ドキュメント、アカウント台帳 kubeconfig、CI/CD、監視、バックアップ、Terraform、GitOps、独自スクリプトなど、すべての外部クライアントを把握できているか
対象クラスター API サーバーへ接続するコントローラーやオペレーターを把握できているか
ノードグループの構成 マネージドノードグループ、セルフマネージドノード、Karpenter、ハイブリッドノードの更新方法を決めているか
Deployment、PodDisruptionBudget レプリカ数、readiness probe、退避条件、必要な追加容量を確認したか
起動テンプレート、ノードグループ カスタム AMI やカスタムユーザーデータを使用していないか
aws eks list-updates Kubernetes バージョン更新やノードグループ更新など、競合する更新処理が進行していないか
作業計画 ロールバックの判断基準、実施者、確認項目を決めているか

以降のコマンドでは、対象を環境変数に設定して使用します。

CLUSTER="ca-rotation-lab"
NG="mng-al2023"

CA ローテーションの実行手順

工程 1: 現在の CA と有効期限を記録します

後継 CA を追加する前に、現在署名に使用されている CA の ID を OLD_CA_ID に保存します。有効化後は signingStatus が変わるため、必ずこの時点で記録してください。

OLD_CA_ID=$(aws eks list-certificate-authorities --cluster-name "$CLUSTER" \
  --query 'certificateAuthorities[?signingStatus==`IN_USE`].id | [0]' \
  --output text)

printf '%s\n' "$OLD_CA_ID"

aws eks describe-certificate-authority --cluster-name "$CLUSTER" \
  --certificate-authority-id "$OLD_CA_ID" \
  --query 'certificateAuthority.validity'

OLD_CA_ID は、有効化後の署名確認、ロールバック、旧 CA の削除で使用します。作業記録へ残してください。

今回新しく作成した検証クラスターの CA は、1825 日、約 5 年の有効期間でした。

notBefore: 2026-08-26T22:02:19+09:00
notAfter : 2031-08-25T22:02:19+09:00

公式ドキュメントには「元の CA は 10 年」とありますが、今回の 1 クラスターには当てはまりませんでした。すべての新規クラスターが 5 年であるとは断定できないため、ドキュメント上の年数だけで判断せず、クラスターごとに確認してください。

同じ検証で、API サーバーが提示する証明書の有効期間は 1 年でした。これは CA 証明書とは別のリーフ証明書であり、CA の有効期間やシリアル番号と直接比較するものではありません。

この工程の完了条件: OLD_CA_ID、CA の有効期限、更新が必要な接続元とノードを記録していること。

工程 2: 後継 CA を追加します

後継 CA を追加し、応答に含まれる 2 つの ID を記録します。

aws eks create-certificate-authority --cluster-name "$CLUSTER" \
  --query '{UpdateId:update.id,CertificateAuthorityId:certificateAuthority.id}'
UPDATE_ID="<UpdateId の値>"
NEW_CA_ID="<CertificateAuthorityId の値>"

どちらも、次の工程 3 以降で使用します。

この時点では、証明書へ署名する CA は旧 CA のままです。検証では、応答の update.params に ActiveCertificateAuthority として旧 CA の ID だけが、TrustedCertificateAuthorities として新旧両方の ID が入っていました。後継 CA の追加を受け付けたことと、二重信頼の準備が完了したことは別の状態です。

この工程の完了条件: UPDATE_ID と NEW_CA_ID を保存していること。

工程 3: 後継 CA の配布完了(distributionStatus=COMPLETE)を待ちます

まず、後継 CA を追加する更新処理を確認します。

aws eks describe-update --name "$CLUSTER" \
  --update-id "$UPDATE_ID" \
  --query 'update.status' --output text

続いて、distributionStatus を確認します。これは、Amazon EKS が管理する範囲へ後継 CA が配布されたかを表す値です。対象は次のとおりです。

  • コントロールプレーン
  • describe-cluster が返すクラスターの信頼バンドル
  • 各 Namespace の kube-root-ca.crt ConfigMap
  • そこから Pod へマウントされる CA

このうち後半 3 つは、次の節で実際に枚数を確認できます。

aws eks describe-certificate-authority --cluster-name "$CLUSTER" \
  --certificate-authority-id "$NEW_CA_ID" \
  --query 'certificateAuthority.distributionStatus' --output text

2 つの状態は、次のように意味が異なります。

確認する状態 完了時の値 意味
describe-update update.status=Successful 後継 CA を追加する更新処理が完了した
describe-certificate-authority distributionStatus=COMPLETE AWS 管理コンポーネントの二重信頼の準備が完了した

今回の検証では、次の順序で変化しました。

経過時間 update.status distributionStatus
0 秒 InProgress —
約 17 分 30 秒 Successful IN_PROGRESS
約 18 分 40 秒 Successful IN_PROGRESS
約 18 分 50 秒 Successful COMPLETE

update.status が Successful になった後も、distributionStatus は 1 分以上 IN_PROGRESS のままでした。工程 4 と 5 へ進む判断には、必ず distributionStatus=COMPLETE を使用してください。

なお、同じ構成で計測を繰り返すと、COMPLETE までの時間は約 19 分から約 24 分の範囲で変動しました。固定値として扱わず、余裕を持った作業枠を確保してください。

クラスター、ConfigMap、Pod への反映を確認します

describe-cluster が返す CA バンドルは、次のコマンドで取得できます。

CA_BUNDLE_BASE64=$(aws eks describe-cluster --name "$CLUSTER" \
  --query 'cluster.certificateAuthority.data' --output text)

printf '%s' "$CA_BUNDLE_BASE64" | base64 -d > ca-bundle.pem
grep -c -- '-----BEGIN CERTIFICATE-----' ca-bundle.pem

各 Namespace の kube-root-ca.crt ConfigMap は、対象クラスターへ接続できる端末から確認します。

kubectl get configmap kube-root-ca.crt --namespace default \
  --output jsonpath='{.data.ca\.crt}' > kube-root-ca.pem

grep -c -- '-----BEGIN CERTIFICATE-----' kube-root-ca.pem

稼働中の Pod にマウントされた /var/run/secrets/kubernetes.io/serviceaccount/ca.crt も、同じ方法で枚数を数えられます。

検証では、次の順序で反映されました。

追加からの経過時間 クラスターの CA バンドル kube-root-ca.crt ConfigMap
直後 1 枚 1 枚
約 7 分 2 枚 1 枚
約 10 分 2 枚 2 枚
約 19 分 2 枚 2 枚、distributionStatus=COMPLETE

稼働中の Pod にマウントされた ca.crt も、Pod を再作成することなく 2 枚へ更新されました。クラスターの CA バンドルが 2 枚になってから kube-root-ca.crt が追随するまでは、観測上約 2 分半でした。表の経過時間は分単位に丸めています。

ただし、証明書が 2 枚になったことと、AWS 管理コンポーネントの配布完了は同じ状態ではありません。これらの確認は観測や問題調査に使用し、工程を進める条件は distributionStatus=COMPLETE としてください。

自動有効化の予定を確認します

検証環境では、CA バンドルの PEM が 1,119 バイトから 2,239 バイトへ増えました。この増加分は、工程 5 で説明するユーザーデータの上限に影響します。

後継 CA の詳細には、自動有効化の予定も含まれます。

aws eks describe-certificate-authority --cluster-name "$CLUSTER" \
  --certificate-authority-id "$NEW_CA_ID" \
  --query 'certificateAuthority.scheduledEvents'

この日付は、後継 CA を追加した日ではなく、もともと使用していた旧 CA の有効期限を基準に決まります。検証環境では、旧 CA の期限が 2031 年 8 月 25 日で、firstAutoActivation が 2031 年 2 月 26 日、finalAutoActivation が 2031 年 7 月 11 日でした。旧 CA の期限からそれぞれちょうど 180 日前と 45 日前です。後継 CA を追加した時点から最初の自動有効化までは、約 4 年半の猶予がありました。

AWS による自動有効化は、通常のワーカーノードや外部クライアントを更新するものではありません。お客様側の更新を終えないまま自動有効化を迎えると、クラスターが稼働していても一部の接続元から操作できなくなる可能性があります。

この工程の完了条件: distributionStatus=COMPLETE を確認していること。この値は、外部クライアントや通常のワーカーノードの更新完了を意味しません。

工程 4: すべての外部クライアントを更新します

手元の kubeconfig は、次のコマンドで更新できます。

aws eks update-kubeconfig --name "$CLUSTER"

このコマンドが書き換えるのは、実行した端末の kubeconfig ファイル 1 つだけです。 ほかの接続元は対象になりません。それぞれの仕組みに応じて、次のように更新します。

接続元 更新方法
開発端末、運用端末 各端末で aws eks update-kubeconfig を実行します
CI/CD パイプライン ジョブ内で aws eks update-kubeconfig を実行しているなら、次回実行時に反映されます。kubeconfig をシークレットとして保存している場合は、その値を作り直します
監視、バックアップ、セキュリティ製品 製品ごとの接続設定で CA 証明書を再登録します
Terraform や独自スクリプト certificate_authority を参照している場合は再取得します。ファイルとして持っている場合は差し替えます
別環境で稼働する Argo CD、Flux クラスター登録情報(Secret など)の CA 証明書を更新します
CA 証明書を設定ファイルや Secret へ直接記述している仕組み 記述内容を新旧両方の CA を含む値へ差し替えます

いずれの場合も、更新後にその実行環境から実際に API サーバーへ接続できることを確認します。

検証では、更新後の kubeconfig に新旧両方の CA が含まれ、移行中も API サーバーへ正常に接続できました。ただし検証環境の接続元は 1 つだけです。この工程に必要な期間は、本記事の実測値からは判断できません。 期間を決めるのは、Amazon EKS 側の処理ではなく、次のようなお客様側の要因です。

  • 接続元がいくつあり、そのうち何件を把握できているか
  • 設定を管理しているのが自チームか、別のチームか、委託先か
  • 変更に承認、変更管理、リリース枠が必要か
  • 更新後の接続確認を、どの環境でどこまで実施するか

工程 4 だけは、二重信頼期間の中でいつ着手しても構いません。後継 CA を追加した直後から、有効化する日まで時間をかけられます。逆に言えば、この工程を短縮できない前提で全体のスケジュールを立ててください。

この工程の完了条件: 洗い出したすべての外部クライアントが新旧両方の CA を信頼し、それぞれの実行環境から API サーバーへ接続できること。

工程 5: 通常のワーカーノードを入れ替えます

マネージドノードグループは、次のコマンドで更新します。

NODE_UPDATE_ID=$(aws eks update-nodegroup-version \
  --cluster-name "$CLUSTER" --nodegroup-name "$NG" \
  --query 'update.id' --output text)

aws eks describe-update --name "$CLUSTER" \
  --nodegroup-name "$NG" --update-id "$NODE_UPDATE_ID" \
  --query 'update.status' --output text

検証環境では、2 台の入れ替えに約 18 分かかりました。

入れ替えの効果は、ノード上に保存された CA の枚数で確認できます。検証環境の Amazon Linux 2023 では /etc/kubernetes/pki/ca.crt に保存されていたため、AWS Systems Manager 経由で枚数を数えました。

INSTANCE_ID=$(aws ec2 describe-instances \
  --filters "Name=tag:eks:cluster-name,Values=$CLUSTER" \
            "Name=instance-state-name,Values=running" \
  --query 'Reservations[0].Instances[0].InstanceId' --output text)

aws ssm send-command --instance-ids "$INSTANCE_ID" \
  --document-name AWS-RunShellScript \
  --parameters 'commands=["grep -c -- \"-----BEGIN CERTIFICATE-----\" /etc/kubernetes/pki/ca.crt"]'

このコマンドを使うには、ノードの IAM ロールに AmazonSSMManagedInstanceCore が付与されている必要があります。

入れ替え前のノードは旧 CA 1 枚だけを保持していましたが、入れ替え後のノードは新旧 2 枚を保持していました。後継 CA を追加しても、すでに稼働している通常のワーカーノード上の CA は AWS によって書き換えられません。だからこそ、入れ替えが必要です。

このファイルパスは AMI やブートストラップ方式によって異なる可能性があります。上記は検証環境で確認した位置であり、すべての環境に共通する仕様として扱わないでください。

ノード形態ごとの対応

ワーカーノードの形態 対応
AWS Fargate 公式仕様では対応不要です。本記事では未実測です
EKS Auto Mode 公式仕様では対応不要です。本記事では未実測です
マネージドノードグループ 入れ替えます
セルフマネージドノード 入れ替えます
Karpenter が管理するノード ドリフト検出が有効な場合は入れ替えの完了を確認します。無効な場合や、中断の許容範囲を絞る設定によって入れ替えが進まない場合は、手動で入れ替えます。本記事では未実測です
ハイブリッドノード 入れ替えます

更新が失敗する原因を事前に確認します

ノードの入れ替えは、古いノードから Pod を退避させる作業を伴います。このため、CA ローテーションに固有ではない一般的な要因で、更新が失敗したり可用性が下がったりする場合があります。作業前に次の 2 点を確認してください。

1 つは、Pod の退避が完了しない場合です。退避が 15 分以内に終わらないと、マネージドノードグループの更新は PodEvictionFailure で失敗する場合があります。--force は PodDisruptionBudget を無視するため、通常の解決策として使用せず、まず退避できない原因を解消してください。

もう 1 つは、更新方式です。MINIMAL 方式では古いノードを終了してから新しいノードを起動するため、更新中の処理能力が一時的に低下します。可用性を維持する必要がある場合は、現在の更新方式と追加容量を事前に確認します。

カスタムユーザーデータの 16 KB 上限を確認します

この項目は、起動テンプレートに独自のユーザーデータを記述している場合にのみ関係します。お客様がユーザーデータを編集する必要はありません。Amazon EKS がノードのユーザーデータを生成し、そこへ CA 証明書を埋め込みます。

注意が必要なのは、そのサイズです。Amazon EC2 のユーザーデータには、Base64 エンコード前のデータで 16 KB の上限があります。二重信頼期間中は CA バンドルが約 1.5 KB 増加するため、独自の記述が多い環境では上限に近づく可能性があります。

検証環境で Amazon EKS が生成したユーザーデータは 2,111 バイトで、そのうち CA 証明書の Base64 文字列が 1,492 文字を占めていました。独自の記述がなければ、上限に対して十分な余裕があります。

上限に近い場合、対応は AMI の種類によって変わります。Amazon EKS 最適化 AMI では、Amazon EKS がユーザーデータを MIME マルチパート形式で結合するため、gzip 圧縮したデータを渡すとノードグループの作成が失敗しました。

code    : Ec2LaunchTemplateInvalidConfiguration
message : User data was not in the MIME multipart format.

圧縮しない同一内容では成功しました。したがって Amazon EKS 最適化 AMI では、記述を減らすか、設定を外部から取得する構成を検討します。

なお、カスタム AMI であればユーザーデータを gzip 圧縮し、fileb:// を指定してバイナリとして渡せます。ただし、これは上限に達した場合の回避策です。Amazon EKS 最適化 AMI は Amazon EKS がブートストラップと CA の埋め込みを行うため、CA ローテーションの観点では扱いが簡単です。上限のためだけにカスタム AMI へ移行することはおすすめしません。

ノード入れ替え中の可用性を計測しました

入れ替え対象外のノードへ計測用 Pod を置き、入れ替え中のサービスへ 0.2 秒間隔でリクエストを送信しました。アプリケーションは 6 レプリカで、minAvailable: 4 の PodDisruptionBudget を設定しています。

マネージドノードグループの既定の更新では、新しいノードを Ready にしてから、古いノードを新規配置の対象から外し、Pod を退避して終了します。検証でも、利用可能な Pod は 6 つを維持し、ノードは一時的に増えた後で元の 2 台へ戻りました。

結果は 4,123 件中 4,122 件が成功し、成功率は 99.976% でした。継続したサービス停止はなく、瞬間的な接続失敗を 1 件観測しました。原因は特定できていません。

この結果はクラスター内部からサービスへの 1 回の計測です。外部ロードバランサー経由の結果ではなく、単一レプリカや再試行を行わないクライアントへ一般化することもできません。

この工程の完了条件: 対象となる旧ノードがすべて入れ替わり、更新処理が Successful、新ノードが Ready、Node Lease とサービス通信が継続していること。後継 CA の有効化と競合する更新処理が残っていないこと。

工程 6: 後継 CA を有効化します

外部クライアントと通常のワーカーノードの更新完了を確認してから、有効化します。

aws eks activate-certificate-authority --cluster-name "$CLUSTER" \
  --certificate-authority-id "$NEW_CA_ID"

後継 CA の状態を確認します。

aws eks describe-certificate-authority --cluster-name "$CLUSTER" \
  --certificate-authority-id "$NEW_CA_ID" \
  --query 'certificateAuthority.signingStatus' --output text

有効化すると、API サーバーの証明書も後継 CA の署名で新しく発行されます。これが、更新していない接続元が有効化後に接続できなくなる理由です。

有効化の処理中は、接続先の API サーバーによって新旧どちらの CA が署名した証明書を返すかが異なる場合があります。IN_USE になるまで待ち、複数回の接続で確認します。

API サーバー証明書を新旧 CA で検証します

なお、新しく発行された API サーバー証明書は describe-certificate-authority では確認できません。このコマンドが返す validity は CA 自体の有効期間で、data も CA 証明書です。API サーバー証明書を見るには、実際に接続して取得します。

旧 CA と後継 CA は、どちらも subject と issuer が CN=kubernetes です。issuer の表示や CA 証明書のシリアル番号だけでは、API サーバー証明書の署名元を判定できません。同じ API サーバー証明書を、新旧それぞれの CA の公開鍵で検証します。

まず、API サーバーが提示した証明書を保存します。

ENDPOINT=$(aws eks describe-cluster --name "$CLUSTER" \
  --query 'cluster.endpoint' --output text)
HOST=${ENDPOINT#https://}

openssl s_client -connect "${HOST}:443" -servername "$HOST" </dev/null 2>/dev/null \
  | openssl x509 -outform PEM > serving.pem

次に、新旧それぞれの CA 証明書を取得し、同じ serving.pem を両方で検証します。

for ID in "$OLD_CA_ID" "$NEW_CA_ID"; do
  aws eks describe-certificate-authority --cluster-name "$CLUSTER" \
    --certificate-authority-id "$ID" \
    --query 'certificateAuthority.data' --output text \
    | base64 -d > "ca-${ID}.pem"
done

openssl verify -CAfile "ca-${OLD_CA_ID}.pem" -partial_chain serving.pem
openssl verify -CAfile "ca-${NEW_CA_ID}.pem" -partial_chain serving.pem
旧 CA 後継 CA 判断
serving.pem: OK 検証エラー その接続では旧 CA が署名した証明書が提示されています
検証エラー serving.pem: OK その接続では後継 CA が署名した証明書が提示されています
検証エラー 検証エラー CA の ID と取得した証明書を確認します

検証では、有効化後に旧 CA でエラー、後継 CA で serving.pem: OK となりました。切り替え中の 1 回の接続だけでは全体の状態を判断できないため、証明書の取得と検証を複数回実施します。

Node Lease とすべての接続元を確認します

ここで確認する Node Lease について、先に用語を整理します。

各ノードでは kubelet(ノード上で Pod を動かす Kubernetes のエージェント)が動作し、自分が生きていることを API サーバーへ定期的に報告します。この報告に使われるのが Lease というリソースで、ノードの死活監視のために kube-node-lease という Namespace に置かれています。ノード 1 台につき、ノードと同じ名前の Lease が 1 つ作られます。

Lease には最終更新時刻が .spec.renewTime として記録されます。kubelet がこの時刻を更新し続けていれば、そのノードは API サーバーと通信できています。逆に更新が止まれば、通信できていません。

この値を見る理由は、kubectl get nodes の Ready 表示よりも早く異常を把握できるためです。Ready から Unknown への変化には数分かかる場合がありますが、Lease の更新間隔は数秒です。今回の推奨手順では、後継 CA の有効化後も約 10 秒間隔の更新が継続しました。

kubectl get lease --namespace kube-node-lease \
  --output custom-columns='NODE:.metadata.name,RENEW_TIME:.spec.renewTime'

kubectl get nodes

有効化後はこのコマンドを繰り返し実行し、各ノードの RENEW_TIME が進み続けることを確認します。

あわせて、次の対象を確認します。

  • すべての外部クライアントからの API 接続
  • Node Lease と各ノードの状態
  • API サーバーへ接続するコントローラーやオペレーター
  • ワークロードのレプリカ数とサービス通信
  • 監視、バックアップ、デプロイなどの定期処理

client-go などを利用する長時間稼働プロセスは、更新された CA バンドルを動的に読み直さず、再起動が必要になる場合があります。

検証環境では、後継 CA の有効化後も Node Lease の更新が続き、4 つの Web サーバー Pod は正常に稼働しました。

他のクラスター更新が進行中の場合、有効化は ResourceInUseException で失敗します。検証でも、ノードグループ更新の直後に有効化しようとして同エラーを確認しました。

この工程の完了条件: 後継 CA が IN_USE、複数回取得した API サーバー証明書を後継 CA で検証でき、全外部クライアント、Node Lease、クラスター内コンポーネント、ワークロードが正常であること。

有効化を確認したら、工程 6-2 へ進みます。

問題がある場合: 旧 CA へ戻します

問題が見つかった場合は、旧 CA に対して同じ有効化 API を実行します。

aws eks activate-certificate-authority --cluster-name "$CLUSTER" \
  --certificate-authority-id "$OLD_CA_ID"

専用のロールバック API はありません。AWS による最終的な自動有効化前で、かつ旧 CA を削除していないロールバック可能期間中に限り実施できます。

切り戻し後は、外部クライアント、Node Lease、クラスター内コンポーネント、ワークロードを複数回確認します。切り戻した後も二重信頼期間は続くため、更新漏れを解消し、工程 4 と 5 の完了条件を再確認してから、改めて工程 6 を実施します。

工程 6-2: 通常のワーカーノードをもう一度入れ替えます

本工程は当社の検証結果に基づく推奨です。 AWS 公式ドキュメントの手順は、後継 CA を有効化する前にノードを入れ替えるところまでを説明しています。有効化後にもう一度入れ替えることや、kubelet のサーバー証明書への影響は、本記事の執筆時点で公式ドキュメントに記載がありません。

工程 5 で入れ替えたノードは、後継 CA を有効化する前に起動しています。そのため、kubelet のサーバー証明書は旧 CA が署名しています。有効化後にもう一度入れ替えることで、後継 CA が署名した証明書を取得させます。

これを省略すると、工程 7 で旧 CA を削除した時点で kubectl exec や kubectl logs が失敗します。理由は本工程の後半で説明します。

コマンドは工程 5 と同じです。

NODE_UPDATE_ID=$(aws eks update-nodegroup-version \
  --cluster-name "$CLUSTER" --nodegroup-name "$NG" \
  --query 'update.id' --output text)

aws eks describe-update --name "$CLUSTER" \
  --nodegroup-name "$NG" --update-id "$NODE_UPDATE_ID" \
  --query 'update.status' --output text

入れ替え後、kubelet のサーバー証明書が後継 CA の署名になったことを確認します。検証環境の Amazon Linux 2023 では、証明書は /var/lib/kubelet/pki/kubelet-server-current.pem にありました。

aws ssm send-command --instance-ids "$INSTANCE_ID" \
  --document-name AWS-RunShellScript \
  --parameters 'commands=["cat /var/lib/kubelet/pki/kubelet-server-current.pem"]' \
  --query 'Command.CommandId' --output text

取得した証明書を、新旧それぞれの CA で検証します。後継 CA でのみ成功する状態が正しい状態です。

openssl verify -CAfile "ca-${OLD_CA_ID}.pem" -partial_chain kubelet-server-current.pem
openssl verify -CAfile "ca-${NEW_CA_ID}.pem" -partial_chain kubelet-server-current.pem

検証環境では、2 台の入れ替えに約 16 分かかりました。入れ替え前のノードは旧 CA でのみ検証でき、入れ替え後のノードは後継 CA でのみ検証できました。

Karpenter、セルフマネージドノード、ハイブリッドノードについては、工程 5 と同じ方法で入れ替えます。AWS Fargate と EKS Auto Mode は、公式仕様ではお客様の対応が不要です。

この工程の完了条件: 対象となるノードがすべて入れ替わり、kubelet のサーバー証明書が後継 CA でのみ検証できること。ノードが Ready、Node Lease とワークロードが正常であること。

なぜもう一度入れ替えるのか

本工程を省略して工程 7 へ進むと何が起きるかを説明します。以下は検証環境で観測した挙動で、独立した 2 回の検証で再現しました。

鍵になるのは kubelet のサーバー証明書です。本工程を省略した検証では、旧 CA の削除中に kubectl exec と kubectl logs が次のエラーで失敗しました。

Error from server: Get "https://172.31.46.133:10250/containerLogs/default/web-.../nginx":
tls: failed to verify certificate: x509: certificate signed by unknown authority

同じ時刻に kubectl get は成功し、ノードは Ready、Node Lease の更新も Pod の稼働も正常でした。削除を発行する直前には kubectl exec と kubectl logs がどちらも成功していました。失敗したのは、API サーバーから各ノードの kubelet(10250 番ポート)へ接続する経路だけです。

%%{init: {'flowchart': {'wrappingWidth': 500, 'nodeSpacing': 50, 'rankSpacing': 60}}}%%
graph TD
    A["工程 3 完了<br/>署名役: 旧 CA"]
    B["工程 5 ノード入れ替え<br/>信頼: 旧 + 後継 CA<br/>kubelet 証明書<br/>→ 旧 CA が署名"]
    C["工程 6 後継 CA を有効化<br/>署名役が後継 CA へ交代<br/>発行済みの証明書は<br/>署名し直されない"]
    D["工程 6-2 ノード入れ替え<br/>kubelet 証明書<br/>→ 後継 CA が署名"]
    E["工程 7 旧 CA を削除<br/>exec / logs は正常"]
    F["工程 7 旧 CA を削除<br/>kubelet 証明書を<br/>検証できず<br/>exec / logs が失敗"]

    A --> B
    B --> C
    C --> D
    D --> E
    C -.->|"工程 6-2 を省略"| F

    style B fill:#e8f4ea,stroke:#2e7d32
    style D fill:#e8f4ea,stroke:#2e7d32
    style E fill:#e3f2fd,stroke:#1565c0
    style F fill:#fdecea,stroke:#c62828

矢印の向きが判断の鍵です。ノードから API サーバーへの通信は、ノードが新旧 2 枚の CA を信頼しているため成功します。一方、API サーバーからノードへの通信では、API サーバーが kubelet の証明書を検証します。この証明書が旧 CA 署名のままなので、旧 CA を削除すると検証できません。

原因は、kubelet のサーバー証明書が発行されたタイミングです。

工程 5 でノードを入れ替えると、新しいノードの kubelet は新しいサーバー証明書を取得します。ただし、証明書に署名するのは、その時点で署名役になっている CA です。工程 5 の時点では、まだ旧 CA が署名役です。したがって、入れ替えたばかりのノードであっても、kubelet のサーバー証明書は 旧 CA が署名 しています。

工程 6 で署名役が後継 CA へ交代しても、すでに発行済みの証明書が遡って署名し直されることはありません。そのため、旧 CA を削除すると検証できなくなります。

%%{init: {'flowchart': {'wrappingWidth': 500, 'nodeSpacing': 50, 'rankSpacing': 60}}}%%
graph LR
    CLIENT["kubectl<br/>外部クライアント"]

    subgraph CP["コントロールプレーン"]
        API["API サーバー<br/>証明書<br/>→ 後継 CA が署名"]
    end

    subgraph NODE["ワーカーノード"]
        KUBELET["kubelet<br/>10250 番ポート<br/>証明書<br/>→ 旧 CA が署名"]
        POD["Pod"]
    end

    CLIENT -->|"kubectl get<br/>成功"| API
    KUBELET -->|"Ready・Node Lease<br/>成功"| API
    API -->|"kubectl exec / logs<br/>失敗"| KUBELET
    KUBELET --- POD

    linkStyle 2 stroke:#c62828,stroke-width:3px
    style KUBELET fill:#fdecea,stroke:#c62828
    style API fill:#e3f2fd,stroke:#1565c0

検証環境のノードから証明書を取り出し、本工程の前半と同じ方法で確認すると、旧 CA では検証でき、後継 CA では検証できませんでした。

旧 CA を信頼バンドルから取り除くと、API サーバーはこの証明書を検証できなくなります。kubectl exec、kubectl logs、kubectl port-forward、metrics-server のように API サーバーから kubelet へ接続する機能は、この時点で失敗します。

この挙動は、公式ドキュメントの説明と矛盾しません。公式ドキュメントは、CA ローテーションが個別の証明書を失効させるものではなく、CA 全体を置き換えるものだと説明しています。そして、旧 CA を信頼バンドルから取り除いた後は、その CA が署名した証明書がすべて無効になるとしています。kubelet のサーバー証明書も、この「旧 CA が署名した証明書」に含まれます。

これが、本工程でもう一度ノードを入れ替える理由です。有効化後に入れ替えたノードの kubelet は、後継 CA が署名したサーバー証明書を取得します。

工程 5 と工程 6-2 は、目的が異なります。どちらか一方では足りません。

入れ替え 目的 対象
工程 5(有効化の前) ノードが後継 CA を信頼できるようにする ノードの信頼バンドル(ca.crt)
工程 6-2(有効化の後) kubelet が後継 CA 署名の証明書を持つようにする kubelet のサーバー証明書

工程 5 を省くと、有効化した時点でノードが API サーバーの証明書を検証できず、通信が途絶します。工程 6-2 を省くと、旧 CA を削除した時点で API サーバーが kubelet の証明書を検証できなくなります。

なお、省略したまま削除してしまった場合も、その後にノードを入れ替えれば復旧します。検証では、新しいノードの kubelet サーバー証明書は後継 CA でのみ検証でき、kubectl exec と kubectl logs は復旧しました。このとき、Pod にマウントされた ca.crt も 1 枚になっていました。

検証環境の kubelet サーバー証明書の有効期間は約 45 日でした。入れ替えを行わない場合でも、証明書のローテーションによって最終的には解消される可能性がありますが、それまでの期間はこれらの機能を使用できません。

工程 7: 十分な確認後に旧 CA を削除します

注意: 旧 CA の削除後はロールバックできません。本工程は検証環境で実施し、所要時間と反映順序を計測しています。

削除する前に、次の 2 つを済ませてください。

  1. 外部クライアント、通常のワーカーノード、API サーバーと通信するクラスター内コンポーネントが安定していることを十分な期間確認し、旧 CA へ戻す必要がないと判断していること
  2. 工程 6-2 を完了していること。 kubelet のサーバー証明書が後継 CA でのみ検証できる状態です。省略した場合に何が起きるかは、工程 6-2 の「なぜもう一度入れ替えるのか」で説明しています

準備ができたら、削除します。

DELETE_UPDATE_ID=$(aws eks delete-certificate-authority \
  --cluster-name "$CLUSTER" \
  --certificate-authority-id "$OLD_CA_ID" \
  --query 'update.id' --output text)

aws eks describe-update --name "$CLUSTER" \
  --update-id "$DELETE_UPDATE_ID" \
  --query 'update.status' --output text

処理が Successful になった後、旧 CA が一覧からなくなり、クラスターの信頼バンドルが後継 CA のみになったことを確認します。

aws eks list-certificate-authorities --cluster-name "$CLUSTER"

aws eks describe-cluster --name "$CLUSTER" \
  --query 'cluster.certificateAuthority.data' --output text \
  | base64 -d > final-ca-bundle.pem

grep -c -- '-----BEGIN CERTIFICATE-----' final-ca-bundle.pem

検証では、次の順序で反映されました。

削除発行からの経過時間 状態
2 秒 delete-certificate-authority が UpdateId を返す
6 分 52 秒 旧 CA が一覧から消え、describe-certificate-authority が失敗する
7 分 2 秒 削除処理が Successful、クラスターの信頼バンドルが 1 枚
9 分 19 秒 kube-root-ca.crt ConfigMap が 1 枚

削除中も 2 台のノードは Ready を維持し、4 つの Web サーバー Pod は再作成されることなく稼働を続けました。Node Lease の更新も継続しました。この所要時間は検証環境での実測値であり、保証値ではありません。

delete-certificate-authority は、お客様が管理する kubeconfig や、すでに起動している通常のワーカーノード上のファイルを直接書き換えません。これらの保存先からも旧 CA を取り除く必要がある場合は、別途更新します。

この工程の完了条件: 削除処理が Successful、旧 CA が一覧に存在せず、クラスターの信頼バンドルが後継 CA 1 枚になっていること。

補足検証: 手順を省略した場合の挙動

ここまでは、推奨手順と、その順序で実施した場合の成功結果を説明しました。ここからは、通常手順の必要性を確認するために実施した比較実験を紹介します。検証環境では、意図的にノードを入れ替えずに後継 CA を有効化しました。本番環境で実施する手順ではありません。

同じクラスターで条件をそろえるため、比較実験は次の順番で実施しました。

  1. ノードを更新せずに後継 CA を有効化
  2. 旧 CA へロールバック
  3. ノードを入れ替えて後継 CA を再度有効化

正しい手順とノード更新を省略した場合の比較

項目 ノードを入れ替えてから有効化 ノードを入れ替えずに有効化
ノード上の CA 新旧 2 枚 旧 CA 1 枚
有効化後の kubelet 影響なし 約 4 分後に通信が途絶
ノード状態 Ready を維持 Ready → Unknown → NotReady
Node Lease 約 10 秒間隔で更新を継続 更新が停止
Pod の表示 正常 Running のまま残る
ノードが通信できなかった時間 0 約 11 分

ノードを更新しない場合、有効化から 4 分 11 秒後と 4 分 40 秒後に、各ノードのハートビートが停止しました。すでに確立していた TLS 接続はしばらく継続し、接続を確立し直す時点で証明書の検証に失敗したため、影響が遅れて現れたと考えられます。

一方、kubectl get nodes は通信が途絶した後もしばらく Ready を表示し、Unknown への変化には 5 分以上かかりました。Pod も Running のまま表示されました。有効化後の判定には、ノードや Pod の表示だけでなく、更新時刻を確認できる Node Lease が必要です。

更新していない外部クライアントは接続できませんでした

有効化の途中は、接続先の API サーバーによって新旧どちらの CA が署名した証明書を返すかが異なりました。旧 CA だけを保持する kubeconfig は、後継 CA の有効化後に次の TLS エラーで接続できなくなりました。

Unable to connect to the server: tls: failed to verify certificate: x509: certificate signed by unknown authority

一方、新旧両方の CA を保持する更新済みの kubeconfig は、移行中も接続できました。特定の CA だけを信頼する設定は、移行中の接続失敗につながります。

旧 CA へ戻すと約 3 分で復旧しました

旧 CA への切り戻しを実行してから 3 分 7 秒後と 3 分 24 秒後に、2 台のノードがそれぞれ Ready へ復帰しました。ノードの再作成や Pod の強制削除は不要でした。ただし、最終ハートビートから復旧までの通信不能時間は約 11 分でした。これらは検証環境での実測値であり、復旧時間を保証するものではありません。

ノードが復旧した時点では、パブリックエンドポイントで後継 CA が署名した証明書も観測されました。パブリックとプライベートの両方を有効にしている構成では、経路によって切り戻しの進行に差が生じる可能性があります。内部の処理順序は確認できていないため、切り戻し時も複数の接続元から確認する必要があります。

ロールバックできるから事前更新を省略してよいという意味ではありません。

運用と Infrastructure as Code

操作履歴を確認できます

CA の詳細には、createdBy、activatedBy、activatedAt が記録されます。検証では、旧 CA をお客様操作で再度有効化すると、activatedBy が EKS から CUSTOMER へ変わりました。複数クラスターを運用する場合は、AWS による自動有効化が先に実行されたクラスターを把握する材料になります。

CloudFormation の注意点

CloudFormation では、AWS::EKS::CertificateAuthority で後継 CA を追加し、AWS::EKS::Cluster の ActiveCertificateAuthorityId で有効化します。

ActiveCertificateAuthorityId は書き込み専用で、読み取り時に返されません。AWS による自動有効化など、スタック外で有効な CA が変わってもドリフトとして検出されないため、CloudFormation とは別に CA の状態を監視する必要があります。

本記事で確認できていない点

今回確認できていない項目は、次のとおりです。

  • EKS Auto Mode と AWS Fargate におけるノード側の自動更新
  • Karpenter のドリフト検出による自動入れ替え
  • 2018 年から 2019 年に作成された、CA の有効期間が 10 年の実クラスターでの挙動
  • 外部ロードバランサーを経由した可用性
  • ノード入れ替え中に観測した 1 件の接続失敗の原因
  • 旧 CA 削除後、稼働中の Pod にマウントされた ca.crt が 1 枚へ変わるまでの時間。kubectl exec が使用できなくなったため計測できていません
  • ノードを入れ替えない場合に、kubelet のサーバー証明書のローテーションで kubectl exec が復旧するまでの時間
  • 旧 CA 削除後に kubelet のサーバー証明書が失効する挙動が、Amazon EKS の意図した仕様なのか、今後ドキュメントや実装が変わるのか。当社の検証で観測した事象であり、公式ドキュメントには記載がありません
  • AWS Health や Amazon EventBridge を経由した通知内容
  • AWS CLI の厳密な最小対応バージョン

まとめ

Amazon EKS の CA ローテーションは、後継 CA を追加してすぐに有効化する作業ではありません。distributionStatus=COMPLETE を確認した後、外部クライアントと通常のワーカーノードを更新し、準備が整ってから後継 CA を有効化します。

検証では、推奨する順序で実施した結果、後継 CA の有効化後も Node Lease の更新は継続し、4 つの Web サーバー Pod は正常に稼働しました。ノード入れ替え中の応答計測では 4,123 件中 1 件の瞬間的な失敗を観測しましたが、継続したサービス停止は確認されませんでした。

有効化後は、kubectl get nodes の表示だけでなく、Node Lease、すべての外部クライアント、クラスター内コンポーネント、ワークロードを確認します。問題があれば、ロールバック可能期間中に旧 CA へ戻します。

旧 CA の削除では、削除処理そのものは約 9 分で完了し、ノードと Pod への影響はありませんでした。ただし当社の検証では、有効化後にノードを入れ替えていない状態で削除すると、kubelet のサーバー証明書が旧 CA 署名のまま残り、kubectl exec や kubectl logs が失敗しました。公式ドキュメントに記載のある手順ではありませんが、削除前に工程 6-2 としてノードをもう一度入れ替えることをおすすめします。

全体のスケジュールを決めるのは、Amazon EKS 側の処理時間ではありません。工程 4 の外部クライアントの洗い出しと更新です。ここだけは接続元の数、管理しているチーム、承認の手順によって期間が変わるため、本記事の実測値は目安になりません。期限に追われる前に、まず各クラスターの CA の有効期限と接続元を確認し、定期的な運用作業として計画することをおすすめします。

前編: Amazon EKS のクラスター証明書認証局(CA)ローテーション【前編】仕組みと背景

参考リンク

余談

山を走っていたらタマゴダケを見つけました!

山本 哲也 ฅ^•ω•^ฅ にゃー(記事一覧)

エンジニアです。データ分析に興味あります。ฅ^•ω•^ฅ にゃー

山を走るのが趣味です。(スカイランナー)ฅ^•ω•^ฅ にゃー