TT Lab
はじめる
学ぶ 学習パス コース

CKAD — Kubernetesアプリケーション開発者

トークン、ロール、アドミッション、そして消えていく API バージョン

TT Labで続きを見る

一言でいうと

PodがAPIサーバーに送るリクエストは、認証 → 認可 → アドミッションという3つのゲートを順に通過します。サービスアカウントトークンは署名・有効期限・バインドされたオブジェクト・対象(audience)を検査され、RBACは加算しかできないルールで許可の範囲を決め、アドミッションコントローラーはスペックを書き換えるか(mutating)、拒否します(validating)。そしてAPIバージョンは決まったルールに従って消えていくため、kubectl api-resourcesで現在サーバーが何を提供しているかを確認し、kubectl convertでマニフェストを移行します。

なぜ必要なのか

開発者が出会う症状は、いつも同じ3つです。「401 Unauthorized」、「forbidden」、そして「自分が書いたYAMLと動いているPodが違う」です。1つ目は認証、2つ目は認可、3つ目はアドミッションの結果ですが、この3つを区別できないと、RBACを開放しても401が出続けたり、トークンを入れ替えてもforbiddenが変わらなかったりします。4つ目の症状はアップグレードのあとに来ます。昨日まで動いていたkubectl applyが「no matches for kind」で失敗します。サーバーがそのAPIバージョンをもう提供しなくなったためで、これは事故ではなく、API非推奨ポリシーが予告していたスケジュールです。

どう動くのか

認証: サービスアカウントトークンはどう検査されるのか

サービスアカウントのドキュメントによると、サービスアカウントは署名されたJWTでAPIサーバーに認証します。クライアントはAuthorization: Bearer <token>ヘッダーを付け、APIサーバーは次の順序で検査します。トークンの署名、有効期限が切れていないか、トークンのクレームが指すオブジェクト参照が現在も有効か、トークンが今有効な時点にあるか、そしてaudienceクレームです。TokenRequest APIで発行されたトークンはPodのようなクライアントの寿命にバインドされているため、APIサーバーはそのオブジェクトが固有IDのままでまだ存在するかも確認します。Secretに入った旧方式のトークンは、Secretと照合します。

署名鍵は、サービスアカウント管理のドキュメントが説明しています。kube-controller-managerのトークンコントローラーが--service-account-private-key-fileの秘密鍵でトークンに署名し、kube-apiserverは--service-account-key-fileの公開鍵で検証します。Podにトークンが入る経路は、ServiceAccountアドミッションコントローラーが付けるprojectedボリュームです(1.22から安定版で、無効にできません)。

- name: kube-api-access-<random-suffix>
  projected:
    sources:
    - serviceAccountToken:
        path: token
    - configMap:
        name: kube-root-ca.crt
        items: [{key: ca.crt, path: ca.crt}]
    - downwardAPI:
        items: [{fieldRef: {fieldPath: metadata.namespace}, path: namespace}]

3つのソースのうち、1つ目が核心です。kubeletがTokenRequest APIで有効期限付きのトークンを受け取って入れ(既定の寿命は1時間)、期限が切れる前に更新し、トークンはそのPodにバインドされ、kube-apiserverをaudienceとして持ちます。旧方式は期限のないSecretベースのトークンで、このメカニズムがそれを置き換えました。トークンが不要なPodは、サービスアカウントの設定のドキュメントのとおり、ServiceAccountやPodスペックにautomountServiceAccountToken: falseを指定してマウントを無効にします(両方にある場合はPodスペックが優先されます)。外部システム向けには、同じドキュメントの例のようにaudience: vaultとexpirationSeconds: 7200を指定したprojectedトークンを使います。kubeletはTTLの80%が過ぎるか24時間を超えると交換を要求するため、アプリケーションはファイルを定期的に読み直す必要があります。

認可: RBACは加算しかしない

RBACのドキュメントの核心の一文は、「権限は純粋に加算され、拒否ルールはない」です。Roleはネームスペース内の権限で、ClusterRoleはネームスペースに属さないリソースなので、クラスタースコープのリソースに対する権限や、複数のネームスペースで再利用する権限のセットを定義します。RoleBindingはサブジェクト(ユーザー・グループ・サービスアカウント)にRoleまたはClusterRoleを特定のネームスペース内で付与し、ClusterRoleBindingはクラスター全体に付与します。

RBACのベストプラクティスが言う最小権限は具体的です。可能であればネームスペースレベルで権限を与え、ClusterRoleBindingの代わりにRoleBindingを使って特定のネームスペース内に限定し、ワイルドカードを避け(今あるリソースだけでなく、今後作られるすべてのリソースタイプに権限を与えることになるため)、cluster-adminは本当に必要なときだけ使います。開発者にとってこれは、「自分のPodのサービスアカウントにConfigMapの読み取りだけが必要なら、そのネームスペースにgetとlistだけを持つRoleとRoleBindingを作る」という形になります。

アドミッション: スペックはここで変わるか拒否される

アドミッションコントローラーのドキュメントによると、アドミッションコントローラーはkube-apiserver内のコードで、オブジェクトを作成・削除・変更するリクエストのデータを検査します。読み取りリクエスト(get・list・watch)はアドミッションを通りません。2つの段階で動きます。まずmutatingコントローラーがデータを変更し、次にvalidatingコントローラーが検査します。どちらの段階でも1つでも拒否すれば、リクエスト全体が拒否されます。1.37の既定で有効なリストには、CertificateApproval、DefaultIngressClass、DefaultStorageClass、DefaultTolerationSeconds、LimitRanger、MutatingAdmissionPolicy、MutatingAdmissionWebhook、NamespaceLifecycle、PersistentVolumeClaimResize、PodSecurity、Priority、ResourceQuota、RuntimeClass、ServiceAccount、StorageObjectInUseProtection、TaintNodesByCondition、ValidatingAdmissionPolicy、ValidatingAdmissionWebhookなどがあります。

開発者が毎日出会うものだけを選ぶと、次のとおりです。

コントローラー 種類 自分のスペックに対して行うこと
ServiceAccount mutating + validating サービスアカウントトークンのボリュームを付けます
LimitRanger mutating + validating ネームスペースのLimitRangeに違反すると拒否し、要求量を書いていなければ既定値を入れます
DefaultStorageClass mutating storageClassNameのないPVCに、既定のStorageClassを入れます
NamespaceLifecycle validating 終了中または存在しないネームスペースにオブジェクトを作ろうとすると拒否します
PodSecurity validating ネームスペースのPod Securityラベルに反するPodを拒否します
ResourceQuota validating ネームスペースのクォータを超えると拒否します

4つの拡張ポイントもあります。MutatingAdmissionWebhookとValidatingAdmissionWebhookは、APIで登録した外部webhookを呼び出します。ValidatingAdmissionPolicyは、外部呼び出しなしでCEL(Common Expression Language)により検証ルールをAPI内で宣言する方式で、MutatingAdmissionPolicyは同じ方式での変更です。そのため、kubectl get pod -o yamlで見たPodが自分で出したYAMLと違うなら(トークンボリュームができた、要求量が埋められた、サイドカーが付いた)、誰かが書き換えたのではなく、mutating段階が行ったことです。どのアドミッションが拒否したかは、エラーメッセージにコントローラー名が出ます。

API非推奨: 決まったルールで消える

API非推奨ポリシーは、APIグループごとに独立してバージョンが付き、バージョンはalpha(v1alpha1)・beta(v1beta1)・GA(v1)の3つのトラックに従うと説明しています。ルール1: APIの要素は、APIグループのバージョンを上げる方法でのみ削除でき、いったん特定のバージョンに入った要素は、そのバージョンから外れたり大きく変わったりしません。ルール2: 1つのリリースの中で、オブジェクトはバージョン間を情報の損失なしに往復できなければなりません。ルール3: より安定性の低いバージョンのために非推奨にされることはありません(GAがbetaを置き換えることはあっても、betaがGAを置き換えることはありません)。ルール4a: 寿命は安定性レベルで決まります。GAバージョンは非推奨の表示はされても、Kubernetesのメジャーバージョンの中では削除されません。betaバージョンは、導入から9か月または3 minorのうち長いほうの期間内に非推奨になり、非推奨になってから9か月または3 minorのうち長いほうの期間が過ぎると提供が停止されます。alphaバージョンは、予告なしにどのリリースでも削除されることがあります。

非推奨API移行ガイドは、リリースごとに提供が停止されたバージョンを列挙しています。たとえば、v1.32はflowcontrol.apiserver.k8s.io/v1beta3のFlowSchemaとPriorityLevelConfigurationをもう提供しないので、v1.29からあるv1に移行する必要があります。移行の手順も同じドキュメントにあります。

  1. 探す: 1.19以上では、クライアントの警告、メトリクス、監査情報で、非推奨のAPIの使用箇所を見つけます。現在サーバーが何を提供しているかは、kubectl api-resourcesで確認します。-o wideでサポートされるverbまで、--api-group=<그룹>(プレースホルダーはグループ名です)で特定のグループだけ、--namespaced=falseでクラスタースコープのリソースだけを見られます。
  2. 試す: APIサーバーに--runtime-config=<그룹>/<버전>=false(プレースホルダーはグループとバージョンです)を指定して、まもなく削除されるバージョンをあらかじめ無効にし、何が壊れるかを確認します。
  3. 移行する: コントローラーと統合コードは、非推奨でないAPIを呼び出すように直し、YAMLはkubectl convert -f <파일> --output-version <그룹>/<버전>(プレースホルダーはファイルと、グループ・バージョンです)で変換します。たとえば、旧Deploymentは--output-version apps/v1です。変換は理想的ではない既定値を入れることがあるので、結果をAPIリファレンスと照らし合わせます。kubectl convertは以前はkubectlに内蔵されていましたが、現在は標準でインストールされないプラグインなので、インストールドキュメントの手順で別途入手します。

現場での姿

RBACを開放しても401が出続けます。401は認証の失敗です。認可(RBAC)は認証を通過したあとのゲートなので、Roleをどれだけ広げても401は変わりません。Podがマウントしたトークンの期限が切れているのに、アプリケーションが起動時に1回しか読まず、読み直さない場合が典型的です。ドキュメントが定期的に読み直すよう言っているのは、このためです。

PVCにstorageClassNameを書いていないのに、あるクラスが付いています。DefaultStorageClassアドミッションが入れたものです。既定のStorageClassがなければ何もせず、2つ以上が既定として表示されている場合は、ドキュメントが定めた別のルールに従います。「自分が書いていない値が入っている」のは、たいていmutatingアドミッションの痕跡です。

次のクイズで確認すること

クイズでは、サービスアカウントトークンの検査項目と既定の寿命、署名鍵がある場所、RBACの加算の性質とバインドの範囲、アドミッションの2つの段階と読み取りリクエスト、beta APIの寿命ルール、そしてkubectl convertの使い方を問います。