🔰

Kubernetesパターン (読書感想文)

に公開

これ読みました。
自分が初めて知ったこと書いたり知識の整理をする。
https://www.oreilly.co.jp/books/9784814400881/

第Ⅰ部 基本パターン

2章 Predictable Demand

2.2.2 リソースプロファイル

Podの.spec.resources.limits.cpuを設定しないことを推奨していた。
QoSのGuaranteed (requests, limitsが同じ)がいいと思っていたが、For the Love of God, Stop Using CPU Limits on Kubernetes のとおりアンチパターンだった。

2.2.3 Podの優先度

kubeletはPodの強制停止にQoSを考えてからPriorityClassを考えることを初めて知った。
一方、スケジューラが強制停止する時はQoSを完全に無視するっぽい。

priorityの低いPodを強制停止させるときはterminationGracePeriodSecondsは尊重されるが、PDBは保証されない。

3章 Declarative Deployment

Deploymentの戦略で色々あるよーって話。
Flagger, Argo Rollouts, Knativeも紹介されてた。

4章 Health Probe

アプリケーション側はLiveness, Readiness用のエンドポイントを用意してねって話。

4.2.1 プロセスヘルスチェック

kubeletがコンテナプロセスに対して行う単純なヘルスチェック

4.2.2 Liveness Probe

http, tcp, exec, grpcがある異常なコンテナを再起動するやつ。

4.2.3 Readiness Probe

コンテナがDBとかの依存関係を待っているケースとか。
チェック方法はLivenessと同じだけど、復旧方法は再起動ではなくサービスのエンドポイントから外されてリクエストを受けなくなる。

4.2.4 Startup Probe

初期化に長い時間が必要なアプリケーションでは、起動する頃にはLivenessが失敗して再起動し続ける。これのためにLivenessの間隔やリトライを増やすと通常のチェックでエラーを拾うのが遅くなる。

Startup Probeの成功後にLiveness, Readinessが開始される。

5章 Managed Lifecycle

5.2.1 SIGTERM

ノードシャットダウンやLiveness Probeの失敗とかでSIGTERMを受け取ったコンテナは猶予時間内で安全に停止してね.
デフォルトでは30秒待つよ。

5.2.2 SIGKILL

SIGTERMでもプロセスが停止しないと強制的にシャットダウンするSIGKILLが送られる。
だからアプリケーションは短命になるように設計してね。

6章 Automated Placement

6.2.1 利用可能なノードリソース

ノードのキャパシティはこんな感じで計算されてるよ。

Allocatable =
    Node Capacity
      - Kube-Reserved (e.g. kubelet)
      - System-Reserved (sshdなどのOS daemon)
      - Eviction Thresholds

6.2.4 スケジューリングのプロセス

デフォルトで使われるkube-schedulerはscheduling, bindingというプロセスでPodを配置するノードを決めてるよ。

参考

第Ⅱ部 振る舞いパターン

7章 Batch Job

single Pod Jobs

ワンショットのjob

fixed completion count Jobs

.spec.completionの数完了するまで、.spec.parallelism並列で実行する

work queue Jobs

.spec.completion指定しない.spec.parallelism並列で実行する

indexed

.spec.completionMode: Indexedに設定する、0から.spec.completion - 1がPodにつく。

10章 Singleton Service

使用例は、ある時点でインスタンスが1つしか実行が許されないサービス。
同じPodのレプリカを実行するとアクティブ・アクティブトポロジが形成される。
シングルトンサービスでは、アクティブ・パッシブトポロジになる。

これは原則的に、アプリ外のロックとアプリ内のロックで実現される。

10.2.1 アプリケーション外のロック

なんか色々書いてあるけど最低1台あればいいというケースはReplicaSetの定義で十分で、最大1台という厳格なケースではStatefulSetのreplicaを1にしてる。

10.2.2 アプリケーション内のロック

こっちは分散ロックの話。
ロックを取ったインスタンスはアクティブとなり、取れなかったインスタンスはリリースを待ち続ける(パッシブ)ように構成されてる。(e.g. Apache ActiveMQ)

学び
全てのノードにはLeaseオブジェクトがあり、kubeletはLeaseオブジェクトのrenewTimeを更新することでハートビートを動かしてる。
このLeaseはkube-controller-manager, kube-schedulerなど同時に1つだけアクティブでそれ以外はスタンバイにする的なのに使われてる。

pdbのUnavailable: 0minAvailable: 100%にすると強制削除できないので高可用性を持たないPodの停止を防げる。

11章 Stateless Service

the twelve factor appという言葉を初めて知った。

12章 Stateful Service

Operatorの話もあるが、StatefulSetの色々が書かれてる。

ReplicaSetは置き換え可能な、StatefulSetは置き換え不可能なPodを管理する。
ReplicaSetは最低でもX (At-Least-X)、StatefulSetは最大でも1つ (At-Most-One).

12.2.1 ストレージ

スケールダウンするとPodは削除されるが、PVC(或いはPV)は削除されない。
これはステートフルアプリケーションのストレージは非常に重要であるためデータ損失をしてはならないという推測に基づくもの。データをコピーなど移動したらPVCを手動削除でPVを再利用できる。

12.2.2 ネットワーク

ここら辺理解が浅い…

ヘッドレスサービスはkube-proxyの対象とならず、ClusterIPはNoneに設定する。
PodのAレコードを返すようになる。

14章 Self Awareness

自分自身の情報を必要とするアプリケーション。
downwardAPIで色々取れるよって話.

awsのIDMSもそう。

第Ⅲ部 構造化パターン

15章 Init Container

init containerのリソースrequest, limitの最大値が、sidecar含むメインアプリケーションより大きければこちらが要求されるが、小さく素早くあるべき。

その他の方法

初期化という文脈では、admission controller (admission webhook)も選択肢としてある。

17章 Adapter

アプリケーションが出してるログのフォーマットが、例えばprometheusが期待したものじゃないときにprometheusに理解できるようにするsidecarみたいなことをアダプタと言っている。

複雑性の隠蔽という意味らしい。

18章 Ambassador

外部の複雑性を隠蔽し、Podの外のサービスへアクセスする際のインターフェイスを提供するサイドカー。

例としてキャッシュの別シャードにアクセスするクライアント設定が必要な時とか、レジストリからサービスを見つけるクライアントサイドのサービスディスカバリとか、サーキットブレーカーとかタイムアウト、リトライ実行など。

利用側(アプリケーション)はローカルポートをリッスンしてるアンバサダコンテナにリクエストを投げて外部サービスに接続する。

ambassadorもadapterもメインアプリケーションに機能追加が不要ということが大事そう。

第IV部 設定パターン

20章 Configuration Resource

.immutableをtrueにするとk8sのapi-serverはConfigMap, Secretの変更を監視しな苦なるからパフォーマンス面で良い。(逆に大規模クラスタでは設定しないとパフォーマンス低下する?)

Secretはどのくらいセキュアなのか

まず、base64だからプレーンテキストと同等だよね。
エンコーディングではなく、以下の実装がある

  • そのSecretにアクセスするPodがあるノードにのみ配布される
  • ノード上ではtmpfsのメモリ上に保存され、Podが削除されるとSecretも削除される
  • etcdではSecretが暗号化される

しかし、rootで見たりSecretを参照するPodを作ったりでアクセスはできる。
RBACちゃんとやろうねって言われてる。

21章 Immutable Configuration

複雑な設定データ (例えばconfigmapにyamlを書きたくない)の課題を解決するために、コンテナイメージとして配布可能な何もしないイメージにデータを入れるという方法がある。
バージョン管理できるのがメリットって言ってる。

init containerでemptyDirを初期化してデカい設定ファイル入れようって話。
ConfigMap, Secretが合わないユースケースの標準的なアプローチって感じ?

22章 Configuration Template

ほとんど重複したでかい設定ファイルを扱う時、gomplateのようなテンプレート処理ツールが入ったイメージを使うパターン。

こんな感じ?

  1. ConfigMapにはテンプレート処理ツールに与える環境ごとに異なるパラメータだけを入れておく
  2. Init containerとして設定をvolume mountされたConfigMapからパラメータを読み、テンプレート処理を行う
  3. 処理結果をemptyDirにおいて Init containerの役割は終了
  4. アプリケーションコンテナがvolumeから設定ファイルを読む

第V部 セキュリティパターン

これに焦点を当てている。

23章 Process Containment

プロセス封じ込めと訳されてる。攻撃対象領域(attack surface)を限定し、防御線を張る。

Podレベルの設定はPod volumeとPod内の全コンテナに、コンテナレベルの設定は単一のコンテナに適用され、同じ設定がある場合はコンテナレベルが優先される。

23.2.1 ルート以外のユーザでコンテナを動かす

runAsNonRootがtrueなら、kubeletがチェックする(コンテナをスタートするタイミング)。
https://github.com/kubernetes/kubernetes/blob/40f222b6201d5c6476e5b20f57a4a7d8b2d71845/pkg/kubelet/kuberuntime/security_context_others.go#L31

23.2.2 コンテナのケーパビリティを制限する

コンテナはノード上のプロセスであり、そのプロセスがカーネルレベルの呼び出しを必要とするならコンテナをルートで実行するか必要なケーパビリティを割り当てるかを行うことになる。

.spec.containers[].securityContext.capabilitiesが空のときは直感に反して多くのケーパビリティが含まれる。
以下のように全ての特権を剥奪して必要なものだけを割り当てる習慣。

spec:
  containers:
  - name: app
    image: docker.io/centos/httpd
    securityContext:
      capabilities:
        drop: ['ALL']
        add: ['NET_BIND_SERVICE']

23.2.3 コンテナファイルシステムの変更を防止する

コンテナのルートファイルシステムを読み出し専用でマウントするために、.spec.containers[].securityContext.readOnlyRootFileをtrueに設定する。
これにより、コンテナのルートファイルシステムへの一切の書き込みができなくなり、イミュータブルインフラが強制される。

23.2.4 セキュリティーポリシーを強制する

Namespaceごとに強制できるセキュリティ標準(labelを使う)。labelを元にPSA(Pod Security Admission)コントローラーが強制する。
https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/pod-security-admission

プロファイル

Privileged
可能な限り広く権限を与えたプロファイル。意図して開放的になっていて、信頼されたユーザやインフラワークロード向け。

Baseline
よくある重要でないアプリケーションワークロード向け。最低限のポリシー。

Restricted
採用コストと引き換えに、最新のセキュリティハードニングのベストプラクティスに従った強いプロファイル。
セキュリティ上重要なアプリケーションや、信頼度の低いユーザ向け。

アクション

warn
ユーザに見える警告を出すが、違反は許容される。
audit
監査ログに記録されつつ、違反は許容。
enforce
あらゆる違反でPodが拒否される。

apiVersion: v1
kind: Namespace
metadata:
  name: baseline-namespace
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: 1.25

    # Restrictedに違反するPodを警告するようにPSAに伝える
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: 1.25

24章 Network Segmentation

kubernetesではデフォルトでnamespaceに関係なくPod間通信が可能。
Podのingress, egressのネットワークアクセスを制限する。(ingressがより重要)

以前はfirewall, iptablesでネットワークトポロジを決めていたが、管理者がアプリケーションのネットワーク要件を理解しなければならないという問題がある。

kubernetesでのマルチテナント

複数のテナント(分離されたユーザのグループ)を持つ。

https://kubernetes.io/docs/concepts/security/multi-tenancy/
k8sのドキュメントには、namespace per tenant, noisy neighborを防ぐためのquota, storage / networkのisolation, クラスタ全体のDNSやCRDの扱い方が説明されている。(いつか読みたい)

より厳密な分離には、https://www.vcluster.com/ のようなものが使えるらしい。

24.2 解決策

このパターンの本質は、アプリケーションファイアウォールを作ることで、開発者がネットワークセグメンテーションを定義できること。

方法は2つあり、L3/L4での操作ができるNetworkPolicyを定義してワークロードのingress, egressを定義する。もう一つは、サービスメッシュを使いL7プロトコル(特にHTTP)でのフィルタリング。

CNCFでサービスメッシュはistio, linkerdがguraduated。

24.2.2 認証ポリシー

例としてistio.
認証、mTLSでのトランスポートセキュリティ、証明書のローテーションでのidentity管理、認可などの機能を持っている。

OperatorでCRDを作る。(?)

istioでの認可はAuthorizationPolicyによって設定され、HTTPを元にネットワーク範囲を分割できるようにする。

AuthorizationPolicyはnamespacedリソースで、selectorに一致するPodにruleとactionを設定可能。

deny allから始め、選択的にネットワークトポロジを作っていくと良い。

25章 Secure Configuration

GitOpsの登場でGitHubに保存された暗号化済みのシークレットをいつどこで複合化するか。

25.2.1 クラスタ外での暗号化

クラスタ外から機密データを取得し、Secretリソースに変換する。(External Secret, sops等)

sopsを例にする。
yaml, jsonに暗号化データを入れてリポジトリに保存できる。キーはaws kmsなど外部に保存できる。

25.2.2 集中型のシークレット管理

cluster adminはSecretを見れてしまうので、これも避けたい場合に外部SMS(Secret Management System)などクラスタ外に置き、必要な時に要求する方法。

Secrets Store CSI Driver
SMSへアクセスするためのドライバ。
SecretProviderClassを作ってバックエンドのproviderを指定して、Podで.spec.volumes[].csiから参照する。

k8s内部に機密情報を保存しなくて済むが、コスト大きい。

26章 Access Control

k8sのAPIサーバーへのリクエストは、認証、認可、アドミッションコントロールというステップがある。

26.2.4 サブジェクト(誰?の部分)

kubectl叩くとかのユーザーとPodのサービスアカウントなどのサブジェクトがある。
ユーザーグループと、サービスアカウントグループに分けられる。

ユーザー
デフォルトではsystem:apiserverなどがある。
kubectl config set-credentials <user name> ...の時に作られてるっぽい。
kubectlで見れなく、k9sならapi叩ける。

サービスアカウント
OpenID ConnectハンドシェイクとJWTを常に使用する。
systems:serviceaccount:<namespace>:nameというフォーマットのユーザー名で認証される。

projectedのvolumeによってsaのトークンが直接Podにマウントされるので、攻撃対象領域が狭くなっている。

グループ
デフォルトのはsystems:接頭辞がつく。
kubectlでは見れないがk9sならapi叩ける。

26.2.5 RBAC

RoleBindingでRoleとsubjectを紐づける。
大事そうなのをピックアップ。

  • ワイルドカードで権限付与しない
    • 最低限の原則
    • 禁止ポリシー作っておくのがいい
  • cluster-adminClusterRoleを使わない
    • Secret見れちゃうからPodにつけるな!
  • serviceaccountのトークンを自動マウントしない
    • /var/run/secrets/kubernetes.io/serviceaccount/tokenにマウントされてるので、このPodに侵入されるとAPIサーバーと通信できちゃう。
    • 多くのアプリケーションではビジネス上の操作でこれが不要。saのautomountServiceAccountToken: falseにしよう
# 例えばこんな感じで取れちゃう
curl -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
--cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
https://kubernetes/api/v1/pods

第VI部 高度なパターン

27章 Controller

ビルトインのk8sリソース(Pod, Deployment)を監視するって意味っぽい。
reconcileプロセスは3ステップで構成されている。

  • observe(観察): イベントの監視
  • analyze(分析): 希望する状態からの差分を出す
  • act(行動): 希望する状態にするための操作

より洗練された次世代のコントローラーとしてOperatorが生まれたらしい。

  • Controller
    • k8sリソースに対して監視・操作するシンプルなreconcile
  • Operator
    • CRDとやりとりする
    • アプリケーションの完全なライフサイクルを管理する

複数のコントローラーが同じリソースに同時に操作を行うのを避けるために、Singleton Serviceパターンを使う。
ほとんどのコントローラーはreplicaが1の単なるDeploymentになる。

ほとんどのコントローラーはGoだが、別になんでもいい。

コントローラのデータを保存すべき場所

  • Label
    • セレクタに近い機能の場合は使うべき
    • 制限はkey, valueに制限付きのalphanumericしか使えないこと
    • labelはバックエンドdbでインデックスされてる
  • Annotation
    • labelの代わりとみなしていい存在
    • labelの文法に合わない値がある時とかに使う
    • annotationはインデックスされない
      • 一意な特定のためではない情報に使用する
  • ConfigMap
    • label, annotationに収まらない情報が必要な場合
    • しかし、状態の定義にはCRDの方が適している
      • CRDの登録にはクラスタレベルの権限が必要
      • ない場合はConfigMap

サンプル実装として学習に使えるシンプルなコントローラたち。

28章 Operator

OperatorとはCRDを使うControllerの一種。CRとそれをを操作するコントローラーのことを指すみたい。
https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
なんか自分で書かずにリーダー選出してくれるっぽい。
https://operatorhub.io/ で探せる。

28.2.1 Custom Resource Definition

CRDによってk8s apiで管理され、CRは最終的にetcdに保存される。

# crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: foos.samplecontroller.k8s.io
spec:
  group: samplecontroller.k8s.io # api group
  names:
    kind: Foo
    plural: foos # 複数形を表す時の命名規則。Listで使われる?
  scope: Namespaced
  version: 
  - name: v1alpha1
    storage: true
    served: true # REST API経由で提供するか
    schema:
      openAPIV3Schema: ...

.spec.subresourcesを使うとPodのstatusみたいなのが生えるっぽい。

28.2.2 コントローラとオペレータの分類

オペレータは、インストールCRD, アプリケーションCRDに分類できる。
違いは曖昧らしい。
インストールCRD
k8s上にアプリケーションをインストールし、操作する意図で作られる。
アプリケーションCRD
特有のドメインコンセプトを表現するためのもの。
アプリケーション特有の振る舞いにk8sを結びつけ、k8sと強く統合することができる。(?)

CRDが表現するのに不十分なときはkind: APIServiceが使えるっぽい。
https://kubernetes.io/ja/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/

多くのロジックを実装することになるので、素のCRDを使うオペレータで十分なことが多い。

28.2.3 オペレータの開発とデプロイ

オペレータの開発に使えるツールキット・フレームワーク。

kubebuilder

https://book.kubebuilder.io/
このドキュメントいいらしい。

kubebuilderはオーバーヘッドをある程度除くためのハイレベルな抽象化層をk8s apiに追加することに焦点を当ててる。
新しいやつ用のscaffolding, 1つのオペレータから複数CRDを監視する仕組みもある。

kubebuilderが最適な第一歩となる

Operator framework

Operator SDK, Operator Lifecycle Managerなどサブコンポーネントがいくつかある。
OperatorSDKの依存関係としてkubebulderが使われているっぽい。

CRDを使うにはクラスタレベルの権限が必要で、OLMはCRDをインストールする権限があるsaを使ってバックグラウンド実行できるもの。
ClusterServiceVersion(CSV)というCRDがOLMと一緒に登録され、オペレータのDeploymentを設定できるようになる。

作ったオペレータはOperator Hubで公開できる。

オペレータは銀の弾丸ではないし、カスタムAPIサーバーを金のハンマーと考えてはいけない。

29章 Elastic Scale

HPA, VPAの話。

HPAはPod数を0にできないという制限を初めて知った。
これを可能にしてるのが、Knative, KEDA.

HPA

k8sメトリクスの種類

  • 標準メトリクス
    • cpu, memory
    • limitsではなくrequestsの方
    • metrics-serverで取れる
  • カスタムメトリクス
    • custom.metrics.k8s.ioというAPIパス
    • Prometheus, Datadogとかから提供されるもの(k8sクラスタ内部にあるコンポーネントって意味かな?)
  • 外部メトリクス
    • 例えばクラウドのQueueを使うPodがいる場合など
    • Queueのdepthとか見てスケールするとかがある

HPA設定時の考慮事項

  • メトリクスの選択
    • Pod数とメトリクスに直接的な相関関係があることが大事
  • スラッシングの防止
    • 負荷が安定していないとき、高速でスケールアップ・ダウンするのを避けるために例えば、スケールアップの時にHPAはCPU使用率を無視する
    • スケールダウンでは時間枠でのスケール推奨事項から最も大きい値を採用する
  • 反応の遅延
    • HPAはスラッシング防止のために反応を遅延させる
    • これが重なるとスケールの原因と反応に遅延を発生させる
    • 遅延を減らすとプラットフォームの負荷が増え、スラッシングも増える(バランスよくやれ)

Knative

  • Knative serving
    • 全Podの停止を含む、オートスケールとトラフィック分割のデプロイモデル
  • Knative Eventing
    • CloudEventを生成するものと、イベントを使うシンクに接続するEvent Meshを作成する仕組みを提供してる(シンク=Knative Serving)
  • Knative Function
    • ソースコードからKnative Servingのサービスをscaffoldingする(aws lambda的な)

並列リクエスト数をもとにオートスケールの判断をすることで、CPU, メモリ使用量よりも処理されるHTTPリクエストのレイテンシにより強い相関関係が得られる。
KPA(Knative Pod Autoscaler)を使うことに移っているっぽい。

KEDA

Kubernetes Event-Driven Autoscaling(KEDA)という略称らしい。
KnativeはHTTPベースのオートスケールだが、外部メトリクスを元にスケールする(プルベース)。

KEDAのコンポーネント

  • KEDA Operator
    • スケーラという仕組みで外部システムにオートスケールトリガを紐付ける
    • それをスケール対象と接続するScaledObjectというCRを扱う
  • KEDAのメトリクスサービス
    • k8sのAPI aggregationレイヤーでAPIServiceリソースとして登録されて、それをHPAが外部メトリクスとして使う

KEDAのオートスケールでは2つのシナリオがある

  • レプリカ数を0 -> 1にスケールして起動
    • これはKEDA Operatorがやる
  • 実行中のスケールアップ・ダウン
    • ワークロードが存在するならHPAに任せる

ScaledObjectが大事。
KEDAで利用可能な外部メトリクス

HPAの外部メトリクスとの違いはPodを0にスケールできることくらい?

VPA

memory requestが小さいとOOM killerに殺されたり、cpu limitsが小さすぎるとcpu枯渇・パフォーマンス不足の可能性がある。
逆にrequestsが高すぎると余計なキャパシティを取って無駄遣いになる。

VPAで解決!
.spec.updatePolicy.updateModeOffにすると更新をせず提案のみになる。

requestsの更新にPodの削除と再作成が必要になるので、ワークロードによってはサービス停止を発生させる可能性がある。
また、HPAと互いに関知しないので水平・垂直同時になると二重にスケールしてしまう場合もある。

Discussion