1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

Kubernetes完全ガイド読んだ感想・まとめ

青山さんのKubernetes完全ガイド読みました

インプレスさんより出版されているKubernetes完全ガイドを読みました。今まで行き当たりばったりでKubernetesを学んできた自分にとって、ものすごく学びが多かったですし、体系だった知識に整理できた一冊でした。図がたくさんあって理解もスムーズだったように思えます。 Kubernetesにこれから触る人や私みたいにその場しのぎで学んできた方は絶対に読むべき一冊だと思います。
※ 私はAmazon ECSとGKEを使い始めて半年くらいです

book.impress.co.jp

今回Kubernetes入門を見て改めて整理出来た項目を中心に挙げていきたいと思います。

kubectl

Contextの切り替え

kubectlで操作する際に、操作対象のクラスターや認証情報が必要になります。実態はデフォルトだと、$HOME/.kube/configに記載されるらしいです。EKSとGKEを使っている場合に、複数の異なる環境にアクセスする場合にはContextの登録・切り替えが必要になります。

# クラスタ名の登録
kubectl config --kubeconfig=config-demo set-cluster development --server=https://1.2.3.4 --certificate-authority=fake-ca-file

# 認証情報の登録
kubectl config --kubeconfig=config-demo set-credentials developer --client-certificate=fake-cert-file --client-key=fake-key-seefile

# コンテキスト名 / クラスタ名 / namespace / 認証情報
kubectl config --kubeconfig=config-demo set-context dev-frontend --cluster=development --namespace=frontend --user=developer
kubectl config --kubeconfig=config-demo set-context dev-storage --cluster=development --namespace=storage --user=developer
kubectl config --kubeconfig=config-demo set-context exp-scratch --cluster=scratch --namespace=default --user=experimenter

# 設定情報を見る
kubectl config --kubeconfig=config-demo view

# 切り替えコマンド
kubectl config --kubeconfig=config-demo use-context dev-frontend

Configure Access to Multiple Clusters - Kubernetes

kubectlで適用される差分

今まで意識してこなかったと同時に、なぜ意識できなかったのか後悔の多いところです。 さらに言えば、kubectl applyのマニフェストのマージ方法というところになります。 削除項目の差分適用は、新しくapplyされるマニフェストと、以前applyされたマニフェストとの比較で行われますが、追加・更新の項目の差分適用は以前applyされたマニフェストではなく、そこからKubernetesが変更を加えたものとの比較になります。つまり、kubectl get deploy xxxxx -o yamlした結果との比較です。 applyした結果、podのテンプレートと変更があれば、ローリングアップデートが起こりますね。以前適応したマニフェストにrollingUpdateについて書いてない状態から、rollingUpdateでmaxSurge25%、minUnAvailable25%ととしても、デフォルト値としてKubernetes側がセットした値とかわらないため、rollingUpdateは起こりません。

また、削除項目の差分適用が以前applyされたマニフェストとの比較ということで、単純にcreateされたマニフェストから、applyで更新すると削除できない項目が出てきてしまいます。createコマンドを使用するときはkubectl create --save-configを使用すること。

マニフェストファイルの設計

結合度の高いリソースである場合、例えばDeploymentとService、HPA、PDBは同じマニフェストファイルに書いても良いかもしれない。ConfigMapやSecretなどの疎結合のリソースに関してはマニフェストファイルを分けるべき。1つのファイルに書く場合には---で区切る。上から順に適用されていくため、書く順番は考慮されるべきである。
kubectl apply -f ./ -R再帰的にマニフェストを適応させることが出来るらしいので、ディレクトリ構造は極力わかりやすい形に持っていくことも可能。

その他

  • Stern(https://github.com/wercker/stern)導入で複数のpodをログを同時に見ることが可能
  • 環境変数KUBE_EDITORで使用するエディタを指定できる。
  • kubectl copyコマンド
  • kubectl port-forward deployment/xxxxxx 8080:3306でkubectlを実行したインスタンス(もしくはローカルマシン)のポート8080にアクセスすると対象Podの3306番のpodに接続される。
  • デバッグ-v=6-v=8を使う。

Workloadsリソース

Podのデザインパターン

  • サイドカー:メインコンテナに機能を追加する。個人的にはkinesisアクセスログを流すflunetdコンテナとかがこれに当たると思いました。
  • アンバサダー:外部システムとのやり取りの代理を行う。HAProxyコンテナのイメージです。AWSでいうならRDSの死活監視を行ってトラフィックを流すようなもの。
  • アダプタ:外部からのアクセスのインターフェース。JavaアプリケーションとかだったらPrometheus Tomcat Exporterのようなイメージでしょうか。

Rolling Updateの処理

マニフェストをapply -> .spec.template以下の構造体のハッシュ値を計算 -> 同じハッシュ値のReplicaSetが既存していなければ作成 -> ローリングアップデート戦略にもとづいて、現在のReplicaSetのPod数を減らし新しいReplicaSetのPod数を増やしていく。ロールバックは、戻すリビジョンを指定しそれに対応するReplicaSetのPod数を増やし、現在のReplicaSetのPod数を減らしていく。kubectl rollout pause deployment sampleでローリングアップデートを止めることも可能。

Daemon Setのアップデート戦略

OndeleteRolling Updateがある。OndeleteはDaemosetのtemplateが変更されてもPodの更新は行われない。更新する場合には手動でPodをdeleteしオートヒーリングでPodを新たに作成する際に新しい定義となる。apiVersionがapps/v1はデフォルトがRolling Updateですが、それより前のバージョンでのデフォルト値はOndeleteなので注意が必要。
参考: Default apps/v1beta2 StatefulSet and DaemonSet strategy to RollingUpdate · Issue #49604 · kubernetes/kubernetes · GitHub

Stateful Setの特徴

業務では使用したこと無いワークロードであるところのStateful Setですが他のワークロードとの違いが顕著であるように見受けられます。以下に列挙していきます。

  • 作成されるPod名のSuffixは数字のインデックスが付与されたのものになる。
  • scaleすると、今あるpodのIndexに+1された名前のPodが作成される。デフォルトでは、Ready状態になってから次のPodを作成し始める。なので増えるときは1つずつ。ただ、podManagementPolicyをPararellにするとdeploymentと同じように複数同時に起動する。
  • Rolling UpdateはpodManagementPolicyにかかわらず1つずつ行われる。partitionを設定することで一部だけを更新することが可能
  • Podの再起動時にも同じPersistentDiskを使用する

その他

  • Podテンプレートのコンテナ実行時のコマンド指定
kubernetes Dockerfile
command ENTRYPOINT
args CMD
  • Pod全コンテナの/etc/hostsを書き換える機能があり、.spec.hostAliasに指定する
  • kubectl applyしたときに--recordオプション必須つけることでアノテーションにchange-causeが記録される

Discovery&LBリソース

Service

kind type description
ClusterIP ClusterIP クラスタ内のみから疎通性があるInternal Networkに作り出される仮想IP。spec.ports.portがClusterIPで待ち受けるIPアドレス。spec.ports.targetPortが転送先コンテナのPort番号。手動でIPを固定する場合には、spec.ClusterIPを指定する。
ExternalIP ClusterIP typeはClusterIP。特定のKubernetesノードで受信したトラフィックをコンテナに転送する。spec.ExternalIPsで受信するノードのIPを列挙する
NodePort NordPort ExternalIPと似ている。ただしこれは、全ノードからトラフィックを受け付けるイメージ。NodePortで利用できるポート範囲は30000~32767
LoadBalancer LoadBalancer Production環境でクラスタ外からトラフィックを受けるときに使用される。Kubernetesクラスタ外に疎通性のある仮想IPを払い出す。上の外部疎通ServiceはノードがSPOFになるが、これは外部のロードバランサを利用するためにノードの障害に強い。GCPAWSでは、spec.loadBalancerSourceRangesに接続を許可する送信元IPアドレス範囲を指定できる。
Headless Service ClusterIP DNSラウンドロビンを使ったエンドポイントの提供。type:ClusterIPかつspec.ClusterIPがNoneのもの。負荷分散に向かない。
ExternalName Service ExternalName クラスタ内からService名で名前解決するとCNAMEが返ってくる。外部サービスと疎結合にするために使用する。
None-Selector Service * 自分で指定したメンバに負荷分散することが出来る。EndPointとServiceを同じ名前で作ることで紐付けられる。typeは自由だがメリットを考えるとClusterIP一択。Selectorには何も指定しない

Ingress

Ingressリソースは一旦作成すると、その後あんまりにも触る機会が無く忘れてしまいがちです。
L7のロードバランシングを行うリソース。L4のServiceと区別するため別のリソースとして扱われている。大別してクラスタ外のロードバランサを利用するものとクラスタ内のIngress用Podをデプロイするものがある。Ingressは事前に作成されたServiceをバックエンドとして転送を行う仕組みになっています。なのでNodePortのリソースをまず用意します。その後に、IngressリソースのserviceNameの項目で結びつけます。L7なので当然パスベースルーティングもマニフェストで指定することが出来ます。https通信を行う場合には別途でSecretを作成する必要性がある。

その他

  • 正式なFQDNを指定せずに、Service名だけでも解決できるのは、/resolve/confsearch [namespace].svc.cluster.localという文言があるため。

Config & Storageリソース

Secret

1つのSecretの中に複数のKey-Value値。 大体は、マニフェストから作成することが考えられる。base64エンコードした値をマニフェストに埋め込みますが、その状態で管理してもセキュリティなど無いに等しいため、kubesec(https://github.com/shyiko/kubesec)と暗号鍵のマネージドサービスを使って暗号化した状態でリポジトリに保存しておくのが良きかと思う。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm

利用方法も複数ある。環境変数として渡す手段とVolumeとしてマウントする手段があります。Volumeとしてマウントした場合には動的なSecretの変更を行うことができます。また、それぞれ特定のKeyを渡す手段(spec.containers[].env[].valueFrom.secretKeyRef)とすべてのKeyを渡す手段(spec.containers[].envFrom.secretRef)がある。マニフェストに一覧性を持たせたい場合には前者、マニフェストを冗長にしたくない場合には後者を用いると良さげ。

kind type description
Generic Opaque マニフェストから作成するケースが多そう。DBなどの資格情報はこれで管理する。
TLS kubernetes.io/tls kubectl create secret tls custom-tls-cert --key /path/to/tls.key --cert /path/to/tls.crtで作成。keyが秘密鍵でcertが証明書ファイル。
Dockerレジストリ kubernetes.io/dockerconfigjson Registoryサーバと認証情報を引数で指定して作成する。podテンプレートのspec.containers[].imagePullSecretsで名前を指定する。
Service Account kubernetes.io/service-account-token 自動で作成されるもの。利用者が意識することはほとんどない

ConfigMap

ConfigMapも殆どの場合でマニフェストから作成されるケースが多そう。Secretと違いBase64エンコードしない点に注意。数値はダブルクォートで囲む。渡し方はSecretとほぼ変わらず、spec.containers[].env[].valueFrom.secretKeyRef -> spec.containers[].env[].valueFrom.configMapKeyRef / spec.containers[].envFrom.secretRef -> spec.containers[].envFrom.configMapRefとフィールドの名前が変わったくらい。

Persistent Volume Claim

ややこしい名前のリソースたちの違い

  • Volume
    • 予め用意された利用可能なボリューム
      • EmptyDir : ホスト上の領域を一時的に確保して利用できる。podがterminateされると削除される
      • hostPath : ホスト上の領域をコンテナにマッピングする。セキュリティに注意。
      • gcePersistentDisk : 同プロジェクト同ゾーンの複数のGCE VMから読み取り専用でマウントされる。大体は予めデータを置いている想定かと思われる。
  • Persistent Volume
    • 実はConfig & StorageリソースではなくClusterリソース
    • 予め作成しておく必要性がある
    • Dynamic Provisioningを使用しない場合にはラベルを付与すること
    • アクセスモードが複数あるが、大体はReadWriteOnceかReadOnlyManyが許可されていて、ReadWriteManyは許可されていない。
    • Reclaim PolicyでPersistentVolumeClaimが削除された際のPersistent Volumeの挙動を決められる。
  • PersistentVolumeClaim
    • その名の通りPersistent Volumeを要求するリソース。マニフェストに指定した条件にあうPVの割当をおこなう。
    • podテンプレートの定義のspec.volumes[].persistentVolumeClaim.ClaimNameにPVCの名前を指定する。
    • 予めPVを使用しなくても、動的にPVを作成して割り当てるのがDynamic Provisioning。StorageClassを作成してPVCのマニフェストで指定する必要あり。
    • StatefulSetはPVCの定義がマニフェストに含まれている

その他

  • SecretとConfigMap以外からPodテンプレートで環境変数を渡すには
    • 静的設定
env:
     - name: TZ
        value: Asia/Tokyo
  • podの情報を参照 。env[].valueFrom.fieldRef.fieldPathで指定する。
  • コンテナの情報を参照。env[].valueFrom.resourceFieldRef.containerNameでコンテナの名前、env[].valueFrom.resourceFieldRef.resouceで取得するフィールドを指定する。
  • SubPathマウントで特定のディレクトリをルートとしてマウントすることができる。mountPathがコンテナ側・subPathがボリューム側のディレクトリパスの指定となる。

リソース管理とオートスケーリング

requestsとlimit

requests/limitともに各コンテナに定義する。
この値が適正かどうか判断する際に役に立つのが、Nodeごとにリソース状況。これはkubectl describe node hogehogeで見ることが可能。
Requestsを大きくしすぎず、RequestsとLimitsの間に顕著な差を作らないことで適正なスケールアウトができるようになる。

  • requests
    • 使用するリソースの下限を指定するもの。
    • この値をもとにNodeにPodがスケジューリングされる。
  • Limits
    • 使用するリソースの上限を指定するもの。
    • この値を超えないようにPodの使用するリソース量がコントロールされる。
    • メモリの場合はLimitsを超えるとOOMでコンテナプロセスが殺される。

HPA(水平スケーリング)

30秒に1回の頻度でオートスケーリングすべきかのチェックが走る。必要なレプリカ数の計算は以下で計算される。
マニフェストの中でDeploymentを指定する。

desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

スケールアウトの式 (最大で3分に1回)

avg(currentMetricValue) / desiredMetricValue > 1.1

スケールインの式(最大で5分に1回)

avg(currentMetricValue) / desiredMetricValue < 0.9

その他

  • LimitRangeでリソースの最小・最大値・デフォルト値を設定可能。新規でPodを作成するときに適用される。
  • OOM KillerによってPodを停止される場合には、QoS Class「Best Effort」「Burstable」「Guaranteed」の順で停止される。RequestsとLimitsが指定されていてなおかつ値が同じ場合には「Guaranteed」となり削除されにくくなる。逆にどちらも指定していない場合には、停止される対象になりやすい。
  • ResouceQuotaによってNameSpaceごとのリソース量・数に制限をかけることができる。

コンテナのライフサイクル

ヘルスチェック

Amazon ECSでもELBを使用したヘルスチェックがありますが、それよりもだいぶ柔軟です。ヘルスチェック方式は3種類あり、コマンドベースのチェック(exec)とHTTPベースのチェック(httpGet)、TCPベースのチェック(tcpSocket)がある。HTTPベースのチェックではホスト名やHeaderを設定できたりもする。
Amazon ECSにおける「healthCheckGracePeriodSeconds(ヘルスチェックまでの猶予期間)」にあたるのがinitialDelaySecondsとなります。

  • Liveness Probe
    • 失敗時にはPodを再起動する。メモリリークなどでPodの再起動なしには状態が回復しずらい場合に使用。
    • 成功と判断するまでのチェック回数であるSuccessThresholdは必ず1
  • Readiness Probe

コンテナライフサイクル

restartPolicyはPodに定義するものでPodが停止した場合にどのような挙動を取るか指定したものです。基本的には、終了コードにかかわらず起動するAlwaysなのではと思うのですが、Jobなどは終了コード0以外で再起動するOnFailureになるのではと思います。Neverは再起動を行わないものです。

Init Containers

初期化処理を行うためのコンテナ。InitContainersの処理が終わらないと、spec.containersで指定したコンテナが起動しない。GCSからファイルをGETしてくる必要性があるときなどに使用している。この機能がAmazon ECSにも欲しい…。

postStart/preStop

コンテナの起動・終了時に行われる処理。postStartは、コンテナのENTRYPOINTとほぼ同じタイミングで実行されるため、どうしてもコンテナ内部で処理をしなくてはいけない場合以外はinitContainersに処理をしてもらったほうが良さそう。
preStopの処理の流れは、この本を読む前にこの記事(Kubernetes: 詳解 Pods の終了 - Qiita)で知っていましたが、改めて別の図も交えてだとよりわかりやすいなと思う。spec.terminationGracePeriodSecondsをpreStop処理にかかる時間を考慮した時間にしないと、デフォルトの30秒がたった時点でSIGKILLコマンド、preStop処理の途中だった場合はSIGTERMコマンドが送られ、その2秒後にSIGKILLが送られる。nginxやapacheなどのWEBサーバでGraceful Shatdownを行う場合に、コマンドだと非同期での実行となる場合があるのでsleepコマンドを入れておくことが望ましい。

その他

  • 親リソース(ReplicaSet)が削除された場合には子リソースのPodを削除するという処理がはいる。この親子関係は、マニフェストmetadata.ownerReferenceから読み取ることが可能。ex) Deployment削除 -> ReplicaSet削除 -> pod削除
  • podを即座に削除したい場合
kubectl delete pod hogehoge --grace-period 0 --force

高度なスケジューリング

Node Affinity

Podが特定のノード上でしか実行できないような条件付ができる。

  • requiredDuringSchedulingIgnoredDuringExecution
    • 必須条件のポリシー
    • この条件を満たさないとスケジューリングされない
    • 検証環境でプリエンプティブインスタンスに起動されると困るものは、priemptive:falseのものを指定したりとか
  • preferredDuringSchedulingIgnoredDuringExecution
    • 優先条件のポリシー
    • この条件を満たさない場合でもスケジューリングされる
    • 優先度の重み(weight)を設定することができる

Node AntiAffinity

これは、matchExpressionで指定できるオペレータでNotがついているものを使えば実現できるもの。

Inter-Pod Affinity

基本的にはNode Affinityと同じだが、topologyKeyというのを指定する必要がある。topologyKeyはスケジューリング対象の範囲を示す。これはホストやゾーンなどを指定でき、matchExpressionのじょうけんを満たすPodのホストもしくはゾーンに配置するという意味になる。

TaintsとTolerations

Podが条件を提示してNode側が許可する形のスケジューリング。node affinityと違う点は他にもあり、考慮されるのがスケジューリングのときだけではないという点がある。

まとめ

本当にめっちゃめちゃ良い本だったので買いましょう。