YasuBlog

中年インフラエンジニアの備忘録です。

Kubernetes Node の cordon/drain

Node の cordon/drain について整理し挙動を確認しました。

1. 説明

1.1. cordon/uncordon

Node は以下のどちらかのステータスを持ちます。

ステータス 説明
SchedulingEnabled Node がスケジューリング対象になっている状態(Pod を新たに起動できる状態)
SchedulingDisabled Node がスケジューリング対象から外れている状態(Pod を新たに起動できない状態)

このステータスを変更する際に cordon/uncordon コマンドを使用します。

kubectl cordon <Node> で Node のステータスを SchedulingDisabled に変更し、kubectl uncordon <Node> で SchedulingEnabled に戻します。

なお、cordon で SchedulingDisabled に変更しても、元々その Node で起動していた Pod に影響はありません。

1.2. drain

cordon を実行しても既存の Pod には影響がありません。新しい Pod を起動できないだけです。

既存の Pod も退避させる場合は drain を使用します。kubectl drain <Node> を実行すると Node を SchedulingDisabled に変更してから各 Pod に SIGTERM シグナルを送信して Pod を退避します。drain 処理には cordon 処理が含まれるため drain の前に cordon を実行する必要はありません。

なお、drain 時は Pod に SIGTERM を送るため、Pod 上のアプリケーションが SIGTERM/SIGKILL に対応している必要があります。(アプリケーションが SIGTERM を受け取った際に処置が途中のものは処理が完全に完了してからアプリを落とす作りにする必要がある)

また、特定の Pod が起動している Node では drain 時にエラーが発生します。その場合はオプションを与える事で drain できます。

エラーとなる Pod エラーとなる理由 drain 時に必要なオプション
DaemonSet が管理している Pod DaemonSet のため、Pod を退避して他の Node で起動できない ignore-daemonsets
emptyDir を使用している Pod Pod を削除すると emptyDir のデータも消える(emptyDir のデータは Pod のローカルのため Pod の削除と共に消える) delete-emptydir-data
ReplicationController, ReplicaSet, Job, DaemonSet, StatefulSet が管理していない Pod 管理されていないため Pod 退避後に他の Node で起動できない force

drain 時に Pod の退避数を制限できる PodDisruptionBudget(PDB) というリソースについては別記事で整理します。

2. 検証

2.1. 検証環境構築

eksctl コマンドで EKS Cluster を作成する - YasuBlog で作成した EKS Cluster を使用します。

適当な Deployment も起動しておきます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"
$ kubectl apply -f test-deployment.yaml
deployment.apps/deployment created

2.2. cordon

まずは cordon 前の Node,Pod の状態です。3 Node に Pod が一つずつ起動している状態です。

$ kubectl get node
NAME                                              STATUS   ROLES    AGE   VERSION
ip-10-0-101-239.ap-northeast-1.compute.internal   Ready    <none>   61m   v1.21.5-eks-9017834
ip-10-0-102-44.ap-northeast-1.compute.internal    Ready    <none>   61m   v1.21.5-eks-9017834
ip-10-0-103-228.ap-northeast-1.compute.internal   Ready    <none>   60m   v1.21.5-eks-9017834
$ kubectl get pod -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-g8qhq   1/1     Running   0          44s   10.0.103.252   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-g9tx2   1/1     Running   0          44s   10.0.101.212   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-wp7zv   1/1     Running   0          44s   10.0.102.88    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>

この状態で cordon を実行します。

$ kubectl cordon ip-10-0-103-228.ap-northeast-1.compute.internal
node/ip-10-0-103-228.ap-northeast-1.compute.internal cordoned
$ kubectl get node
NAME                                              STATUS                     ROLES    AGE   VERSION
ip-10-0-101-239.ap-northeast-1.compute.internal   Ready                      <none>   65m   v1.21.5-eks-9017834
ip-10-0-102-44.ap-northeast-1.compute.internal    Ready                      <none>   65m   v1.21.5-eks-9017834
ip-10-0-103-228.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   65m   v1.21.5-eks-9017834
$ kubectl  get pod -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-g8qhq   1/1     Running   0          56s   10.0.103.252   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-g9tx2   1/1     Running   0          56s   10.0.101.212   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-wp7zv   1/1     Running   0          56s   10.0.102.88    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>

cordon を実行した Node の STATUS が SchedulingDisabled になりました。既存の Pod には変化はありませんでした。

describe node を実行すると Taint に node.kubernetes.io/unschedulable:NoSchedule が設定されていました。

% kubectl describe node ip-10-0-103-228.ap-northeast-1.compute.internal
~省略~

Taints:             node.kubernetes.io/unschedulable:NoSchedule
Unschedulable:      true

~省略~

この状態で Deployment の replicas の値を 3 から 10 に変更します。

$ kubectl scale deployment/test-deployment --replicas=10
deployment.apps/test-deployment scaled
$ kubectl get pod -o wide
NAME                               READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-4p7qw   1/1     Running   0          5s    10.0.102.14    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-g8qhq   1/1     Running   0          74s   10.0.103.252   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-g9tx2   1/1     Running   0          74s   10.0.101.212   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-hwsmw   1/1     Running   0          5s    10.0.101.177   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-ldr7h   1/1     Running   0          5s    10.0.101.65    ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-nx649   1/1     Running   0          5s    10.0.102.184   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-rgslw   1/1     Running   0          5s    10.0.102.42    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-vd8bn   1/1     Running   0          5s    10.0.102.208   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-wm77s   1/1     Running   0          5s    10.0.101.186   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-wp7zv   1/1     Running   0          74s   10.0.102.88    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>

新しい Pod が 7 個起動しましたが、cordon した Node(SchedulingDisabled になっている Node)には新しい Pod が起動していないことが確認できました。

2.3. uncordon

uncordon を実行して SchedulingDisabled から SchedulingEnabled に戻します。

まずは uncordon 前の状態です。一つの Node が SchedulingDisabled の状態です。

$ kubectl get node
NAME                                              STATUS                     ROLES    AGE   VERSION
ip-10-0-101-239.ap-northeast-1.compute.internal   Ready                      <none>   84m   v1.21.5-eks-9017834
ip-10-0-102-44.ap-northeast-1.compute.internal    Ready                      <none>   84m   v1.21.5-eks-9017834
ip-10-0-103-228.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   83m   v1.21.5-eks-9017834
$ kubectl get pod -o wide
NAME                               READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-8l8rp   1/1     Running   0          40s   10.0.101.84    ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-9s7lm   1/1     Running   0          40s   10.0.102.176   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-t8bzh   1/1     Running   0          40s   10.0.101.113   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>

uncordon を実行します。

$ kubectl uncordon ip-10-0-103-228.ap-northeast-1.compute.internal
node/ip-10-0-103-228.ap-northeast-1.compute.internal uncordoned
$ kubectl get node
NAME                                              STATUS   ROLES    AGE   VERSION
ip-10-0-101-239.ap-northeast-1.compute.internal   Ready    <none>   86m   v1.21.5-eks-9017834
ip-10-0-102-44.ap-northeast-1.compute.internal    Ready    <none>   86m   v1.21.5-eks-9017834
ip-10-0-103-228.ap-northeast-1.compute.internal   Ready    <none>   85m   v1.21.5-eks-9017834

Node の STATUS から SchedulingDisabled が消えました。

describe node を実行すると node.kubernetes.io/unschedulable:NoSchedule の Taint が消えていました。

% kubectl describe node ip-10-0-103-228.ap-northeast-1.compute.internal
~省略~

Taints:             <none>
Unschedulable:      false

~省略~

この状態で Deployment の replicas の値を 3 から 10 に変更します。

$ kubectl scale deployment/test-deployment --replicas=10
deployment.apps/test-deployment scaled
$ kubectl get pod -o wide
NAME                               READY   STATUS    RESTARTS   AGE     IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-2nt8q   1/1     Running   0          37s     10.0.102.208   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-8l8rp   1/1     Running   0          3m18s   10.0.101.84    ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-9s7lm   1/1     Running   0          3m18s   10.0.102.176   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-fc444   1/1     Running   0          37s     10.0.103.215   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-lgmpg   1/1     Running   0          37s     10.0.103.153   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-m8mqk   1/1     Running   0          37s     10.0.101.212   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-qnnr5   1/1     Running   0          37s     10.0.102.184   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-t8bzh   1/1     Running   0          3m18s   10.0.101.113   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-thbf5   1/1     Running   0          37s     10.0.102.42    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-zpjmx   1/1     Running   0          37s     10.0.103.104   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>

uncordon した Node に Pod が起動されている事を確認できました。

2.4. drain

2.4.1. DaemonSet がある場合

まずは drain 前の Node,Pod の状態です。3 Node に Pod が一つずつ起動している状態です。

$ kubectl get node
NAME                                              STATUS   ROLES    AGE   VERSION
ip-10-0-101-239.ap-northeast-1.compute.internal   Ready    <none>   98m   v1.21.5-eks-9017834
ip-10-0-102-44.ap-northeast-1.compute.internal    Ready    <none>   98m   v1.21.5-eks-9017834
ip-10-0-103-228.ap-northeast-1.compute.internal   Ready    <none>   98m   v1.21.5-eks-9017834
$ kubectl get pod -o wide
NAME                               READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-kgndl   1/1     Running   0          8s    10.0.103.153   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-t7wqf   1/1     Running   0          8s    10.0.102.42    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-zscgl   1/1     Running   0          8s    10.0.101.177   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>

drain を実行します。

$ kubectl drain ip-10-0-103-228.ap-northeast-1.compute.internal
node/ip-10-0-103-228.ap-northeast-1.compute.internal cordoned
error: unable to drain node "ip-10-0-103-228.ap-northeast-1.compute.internal" due to error:cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/aws-node-jls99, kube-system/kube-proxy-x95dw, continuing command...
There are pending nodes to be drained:
 ip-10-0-103-228.ap-northeast-1.compute.internal
cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/aws-node-jls99, kube-system/kube-proxy-x95dw

エラーになりました。これは EKS の場合だと VPC CNI プラグインaws-node や kube-proxy が DaemonSet で動いているためです。エラー文言にも書いてありますが最初の説明に記載した通り --ignore-daemonsets オプションを付与する事で drain できます。

$ kubectl drain ip-10-0-103-228.ap-northeast-1.compute.internal --ignore-daemonsets
node/ip-10-0-103-228.ap-northeast-1.compute.internal already cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/aws-node-jls99, kube-system/kube-proxy-x95dw
evicting pod default/test-deployment-6bb985c8c9-kgndl
pod/test-deployment-6bb985c8c9-kgndl evicted
node/ip-10-0-103-228.ap-northeast-1.compute.internal drained
$ kubectl get node
NAME                                              STATUS                     ROLES    AGE    VERSION
ip-10-0-101-239.ap-northeast-1.compute.internal   Ready                      <none>   104m   v1.21.5-eks-9017834
ip-10-0-102-44.ap-northeast-1.compute.internal    Ready                      <none>   104m   v1.21.5-eks-9017834
ip-10-0-103-228.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   103m   v1.21.5-eks-9017834

Node が drain されました。cordon を実行しなくても SchedulingDisabled になりました。

Taint には node.kubernetes.io/unschedulable:NoSchedule が設定されています。

% kubectl describe node ip-10-0-103-228.ap-northeast-1.compute.internal
~省略~

Taints:             node.kubernetes.io/unschedulable:NoSchedule
Unschedulable:      true

~省略~

drain 中の Pod の状態遷移です。

$ kubectl get pod -o wide -w
NAME                               READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-kgndl   1/1     Running   0          60s   10.0.103.153   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-t7wqf   1/1     Running   0          60s   10.0.102.42    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-zscgl   1/1     Running   0          60s   10.0.101.177   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-kgndl   1/1     Terminating   0          3m32s   10.0.103.153   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-t2xpr   0/1     Pending       0          0s      <none>         <none>                                            <none>           <none>
test-deployment-6bb985c8c9-t2xpr   0/1     Pending       0          0s      <none>         ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-t2xpr   0/1     ContainerCreating   0          0s      <none>         ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-t2xpr   1/1     Running             0          3s      10.0.102.184   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-kgndl   0/1     Terminating         0          4m2s    10.0.103.153   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-kgndl   0/1     Terminating         0          4m6s    10.0.103.153   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
test-deployment-6bb985c8c9-kgndl   0/1     Terminating         0          4m6s    10.0.103.153   ip-10-0-103-228.ap-northeast-1.compute.internal   <none>           <none>
$ kubectl get pod -o wide
NAME                               READY   STATUS    RESTARTS   AGE    IP             NODE                                              NOMINATED NODE   READINESS GATES
test-deployment-6bb985c8c9-t2xpr   1/1     Running   0          93s    10.0.102.184   ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-t7wqf   1/1     Running   0          5m5s   10.0.102.42    ip-10-0-102-44.ap-northeast-1.compute.internal    <none>           <none>
test-deployment-6bb985c8c9-zscgl   1/1     Running   0          5m5s   10.0.101.177   ip-10-0-101-239.ap-northeast-1.compute.internal   <none>           <none>

drain 対象の Node で起動していた Pod が終了し別 Node で起動した事が確認できました。

2.4.2. emptyDir を使用する Pod がある場合

以下 manifest の Deployment を起動して検証します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-emptydir-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: app-emptydir
  template:
    metadata:
      labels:
        app: app-emptydir
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"
        volumeMounts:
        - mountPath: /hoge
          name: hoge-volume
      volumes:
      - name: hoge-volume
        emptyDir:
          sizeLimit: 1Gi

drain すると以下エラーが出ました。

$ kubectl drain ip-10-0-101-69.ap-northeast-1.compute.internal --ignore-daemonsets
node/ip-10-0-101-69.ap-northeast-1.compute.internal cordoned
error: unable to drain node "ip-10-0-101-69.ap-northeast-1.compute.internal" due to error:cannot delete Pods with local storage (use --delete-emptydir-data to override): default/test-emptydir-deployment-79859745dd-l6fbh, continuing command...
There are pending nodes to be drained:
 ip-10-0-101-69.ap-northeast-1.compute.internal
cannot delete Pods with local storage (use --delete-emptydir-data to override): default/test-emptydir-deployment-79859745dd-l6fbh

delete-emptydir-data オプションを付与する事で drain できます。

$ kubectl drain ip-10-0-101-69.ap-northeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data
node/ip-10-0-101-69.ap-northeast-1.compute.internal already cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/aws-node-8597j, kube-system/kube-proxy-pw5st
evicting pod default/test-emptydir-deployment-79859745dd-l6fbh
pod/test-emptydir-deployment-79859745dd-l6fbh evicted
node/ip-10-0-101-69.ap-northeast-1.compute.internal drained

2.4.3. ReplicationController, ReplicaSet, Job, DaemonSet, StatefulSet が管理していない Pod がある場合

以下 manifest の Pod を起動して検証します。

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
    - name: amazonlinux
      image: public.ecr.aws/amazonlinux/amazonlinux:latest
      command:
        - "bin/bash"
        - "-c"
        - "sleep 3600"

drain すると以下エラーが出ました。

$ kubectl drain ip-10-0-103-89.ap-northeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data
node/ip-10-0-103-89.ap-northeast-1.compute.internal already cordoned
error: unable to drain node "ip-10-0-103-89.ap-northeast-1.compute.internal" due to error:cannot delete Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet (use --force to override): default/test-pod, continuing command...
There are pending nodes to be drained:
 ip-10-0-103-89.ap-northeast-1.compute.internal
cannot delete Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet (use --force to override): default/test-pod

force オプションを付与する事で drain できます。

$ kubectl drain ip-10-0-103-89.ap-northeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data --force
node/ip-10-0-103-89.ap-northeast-1.compute.internal already cordoned
WARNING: deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: default/test-pod; ignoring DaemonSet-managed Pods: kube-system/aws-node-vnmfx, kube-system/kube-proxy-6x767
evicting pod kube-system/coredns-76f4967988-7slnc
evicting pod default/test-pod
pod/coredns-76f4967988-7slnc evicted
pod/test-pod evicted
node/ip-10-0-103-89.ap-northeast-1.compute.internal drained

3. まとめ

cordon/uncordon/drain は Kubernetes クラスタのメンテナンス時によく使うコマンドなので整理して挙動を確認しました。

4. 参考

ノード | Kubernetes

Safely Drain a Node | Kubernetes

Kubectl Reference Docs

EKS Cluster の消費 IP 数を減らす

EKS Cluster を構築すると VPC 内の IP を大量に使うので、IP 数の削減方法について整理し検証しました。

1. EKS Cluster の ENI と IP アドレスの挙動

EKS のデフォルト CNI である Amazon VPC CNI の ENI と IP アドレスの挙動は以下になります。

  • コントロールプレーン(Master)
    • ENI が 2 個付く
    • ENI の 説明 項目は Amazon EKS <クラスタ名> となる
    • この ENI を使用してデータプレーンと通信する
  • データプレーン(Node)
    • Node 作成時に一つの ENI(プライマリ ENI)がアタッチされる
    • 後述する環境変数に基づき、追加の ENI(セカンダリ ENI)がアタッチされる
    • ENI にアサインされるプライマリプライベート IP は Node 自体が使用し、セカンダリプライベート IP は Pod が使用する
    • ENI にアサインされる IP の数は Node のインスタンスサイズ(の制限)に依存する
      • 例えば、t3.medium の制限は 2 個の ENI と ENI ごとに 6 個の IP のため、ENI には一つのプライマリプライベート IP と 5 個のセカンダリプライベート IP がアサインされる
    • 空きセカンダリプライベート IP の数が閾値を下回ると、ENI が最大数までアタッチされていない限り、新たな ENI が Node にアタッチされる
      • デフォルト設定では、空きセカンダリ IP 数が ENI の最大 IP 数 - 1 を下回った際に新たな ENI がアタッチされる(つまり、一つ目の Pod が起動したらすぐに新たな ENI がアタッチされる)

Amazon VPC CNI プラグインaws-node という名前の daemonset としてデプロイされます。上記の ENI, IP の挙動をデフォルトから変更したい場合はこの aws-node の設定を変更する必要があります。

2. デフォルト設定の Cluster を構築した際の消費 IP 数

EKS Cluster を構築する VPC は以下構成になっています。

f:id:dunkshoot:20220307230818p:plain

特に何のリソースも作成していない状態でマネジメントコンソールから確認した各 Subnet の空き IP 数は 753(251 + 251 + 251)でした。

f:id:dunkshoot:20220315002533p:plain

この状態で各 AZ に Node を 1 台配置するシンプルな構成の EKS Cluster を構築します。

f:id:dunkshoot:20220307231348p:plain

yaml は以下です。

    ---
    apiVersion: eksctl.io/v1alpha5
    kind: ClusterConfig
    metadata:
      name: ekstest
      region: ap-northeast-1
      version: "1.21"
    vpc:
      id: "vpc-063b58ff16344acfd"
      subnets:
        public:
          ap-northeast-1a:
              id: "subnet-06324dcadf5706acb"
          ap-northeast-1c:
              id: "subnet-048cad38a6c49de67"
          ap-northeast-1d:
              id: "subnet-0bb85370bb4b4528d"
    managedNodeGroups:
      - name: managed-ng
        instanceType: t3.medium
        desiredCapacity: 3
        volumeSize: 30
        availabilityZones: ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
        ssh:
          allow: true
          publicKeyName: ekstest

EKS Cluster 作成後の各 Subnet の空き IP 数は以下の通り 721(239 + 238 + 244) でした。Cluster を作成しただけで VPC の IP を 32 個使用していることがわかります。

f:id:dunkshoot:20220315002917p:plain

Node の ENI, IP の構成を図に表すと以下のようになります。未使用の IP はグレーにしています。

f:id:dunkshoot:20220315001259p:plain

Node(データプレーン) が 30 IP, Master(コントロールプレーン)が 2 IP を使用しています。

なお、coredns が一つの Node に二つ起動する場合は ENI の数が一つ減ります。可用性のためには分散した方が良いので今回は二つの Node に分散したケースを前提として検証します。

3. 消費 IP の削減方法

(ユーザ管理の)Pod を起動していない状態でもクラスタが多くの IP を使用する(t3.medimu x3 構成で 32 IP)ことがわかりました。 複数チームやサービスで共有している VPC 環境などでは IP を節約する必要があるため、消費 IP の削減方法を整理します。

Amazon VPC CNI プラグイン aws-node環境変数を変更することで ENI,IP の数を変更することが可能です。

GitHub - aws/amazon-vpc-cni-k8s: Networking plugin repository for pod networking in Kubernetes using Elastic Network Interfaces on AWS

以下が ENI,IP の数に関連する環境変数です。

環境変数 デフォルト値 説明
WARM_ENI_TARGET 1 ・Node にアタッチする余剰 ENI 数
・Node への IP の付け替えが大量に発生すると EC2 API がスロットリングされ、ENI,IP がアタッチできなくなる可能性がある。デフォルト値が 1 になっている理由は、未使用の IP を大量に持つ状態と、EC2 API がスロットリングされるリスクとの間のバランスを取っている
・ENI のアタッチには最大 10 秒かかる
WARM_IP_TARGET が設定されている場合、この環境変数は無視される
MAX_ENI None ・Node にアタッチする最大 ENI 数
・未設定か 0 以下の場合は、インスタンスサイズの ENI 制限値が MAX_ENI の値となる
 Elastic Network Interface - Amazon Elastic Compute Cloud
WARM_IP_TARGET None ・Node が Pod 用に確保しておく余剰 IP 数
・0 未満の場合は無効
・例えば 5 に設定した場合、常に 5 個の IP を使用可能な状態にしておく。もし ENI の IP 数上限に達している場合は新たに ENI をアタッチして 5 個の空き IP を確保する
・小規模なクラスタや Pod の作成/削除が少ない環境に向いている
・大規模なクラスタや Pod の作成/削除が高頻度で行われる環境では、EC2 API がスロットリングされる可能性があるため向いていない
MINIMUM_IP_TARGET と同時に設定する場合、両方の制約を満たそうとする
WARM_ENI_TARGET より WARM_IP_TARGET が優先される
MINIMUM_IP_TARGET None ・Node が Pod 用に確保しておく最小 IP 数
・0 未満の場合は無効
・例えば Node あたり 30 Pod 起動するクラスタの場合
 ・WARM_IP_TARGET=30 にすると、30 Pod 起動後に新たに 30 IP を追加する
 ・MINIMUM_IP_TARGET=30, WARM_IP_TARGET=2 にすると、30 Pod 起動後に新たに 2 IP を追加する
・Node 上で動く予定の Pod 数より少し多めの値を設定することを推奨

ざっくり図に表すと以下のようなイメージです。

f:id:dunkshoot:20220309002844p:plain

4. 検証

上記で作成した EKS Cluster で検証します。

4.1. 設定方法

まずは aws-node のデフォルトの環境変数を確認します。aws-nodekube-system namespace で起動しています。

$ kubectl get daemonset -n kube-system
NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
aws-node     3         3         3       3            3           <none>          70m
kube-proxy   3         3         3       3            3           <none>          70m

環境変数は kubectl get daemonset で確認できます。

$ kubectl get daemonset aws-node -n kube-system -o jsonpath='{.spec.template.spec.containers[*].env}' | jq .
[
  {
    "name": "ADDITIONAL_ENI_TAGS",
    "value": "{}"
  },
  {
    "name": "AWS_VPC_CNI_NODE_PORT_SUPPORT",
    "value": "true"
  },
  {
    "name": "AWS_VPC_ENI_MTU",
    "value": "9001"
  },
  {
    "name": "AWS_VPC_K8S_CNI_CONFIGURE_RPFILTER",
    "value": "false"
  },
  {
    "name": "AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG",
    "value": "false"
  },
  {
    "name": "AWS_VPC_K8S_CNI_EXTERNALSNAT",
    "value": "false"
  },
  {
    "name": "AWS_VPC_K8S_CNI_LOGLEVEL",
    "value": "DEBUG"
  },
  {
    "name": "AWS_VPC_K8S_CNI_LOG_FILE",
    "value": "/host/var/log/aws-routed-eni/ipamd.log"
  },
  {
    "name": "AWS_VPC_K8S_CNI_RANDOMIZESNAT",
    "value": "prng"
  },
  {
    "name": "AWS_VPC_K8S_CNI_VETHPREFIX",
    "value": "eni"
  },
  {
    "name": "AWS_VPC_K8S_PLUGIN_LOG_FILE",
    "value": "/var/log/aws-routed-eni/plugin.log"
  },
  {
    "name": "AWS_VPC_K8S_PLUGIN_LOG_LEVEL",
    "value": "DEBUG"
  },
  {
    "name": "DISABLE_INTROSPECTION",
    "value": "false"
  },
  {
    "name": "DISABLE_METRICS",
    "value": "false"
  },
  {
    "name": "DISABLE_NETWORK_RESOURCE_PROVISIONING",
    "value": "false"
  },
  {
    "name": "ENABLE_IPv4",
    "value": "true"
  },
  {
    "name": "ENABLE_IPv6",
    "value": "false"
  },
  {
    "name": "ENABLE_POD_ENI",
    "value": "false"
  },
  {
    "name": "ENABLE_PREFIX_DELEGATION",
    "value": "false"
  },
  {
    "name": "MY_NODE_NAME",
    "valueFrom": {
      "fieldRef": {
        "apiVersion": "v1",
        "fieldPath": "spec.nodeName"
      }
    }
  },
  {
    "name": "WARM_ENI_TARGET",
    "value": "1"
  },
  {
    "name": "WARM_PREFIX_TARGET",
    "value": "1"
  }
]

デフォルトでは WARM_ENI_TARGET=1 が設定されており、MAX_ENI, WARM_IP_TARGET, MINIMUM_IP_TARGET が設定されていないことがわかりました。

環境変数を変更する場合は以下のように kubectl set env コマンドを使用します。

$ kubectl set env daemonset aws-node -n kube-system <環境変数名>=<>
daemonset.apps/aws-node env updated

では、それぞれの変数を変更した際の挙動を確認していきます。

4.2. WARM_ENI_TARGET

WARM_ENI_TARGET を 0 に設定してみます。

$ kubectl set env daemonset aws-node -n kube-system WARM_ENI_TARGET=0
daemonset.apps/aws-node env updated

値を確認します。

$ kubectl get daemonset aws-node -n kube-system -o jsonpath='{.spec.template.spec.containers[*].env}' | jq '.[] | select(.name == "WARM_ENI_TARGET")'
{
  "name": "WARM_ENI_TARGET",
  "value": "0"
}

WARM_ENI_TARGET が 0 に変更されたことが確認できました。5 分ぐらい経つと ENI が二つついていた Node から ENI がデタッチされ以下のような 3 ENI, 18 IP の構成になりました。

f:id:dunkshoot:20220315001343p:plain

この状態で Pod を 100 個起動する deployment を起動してみます。

t3.medium の ENI 上限は 3、ENI あたりの IP 上限は 6 です。プライマリ IP アドレスは Node が使用するため、3 Node の場合は最大 45(5 * 3 * 3)Pod 起動できることになります。デフォルトで coredns の Pod が二つ起動しているため、43 Pod 起動できるはずです。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  replicas: 100
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"

apply します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/deployment created

想定通り 43 Pod 起動できました。残りの 57 Pod は Pending 状態になりました。

$ kubectl get deploy
NAME         READY    UP-TO-DATE   AVAILABLE   AGE
deployment   43/100   100          43          26m
$ kubectl get pod | grep -c Running
43
$ kubectl get pod | grep -c Pending
57

この時の Node の ENI,IP の状態は以下でした。

f:id:dunkshoot:20220315004012p:plain

4.3. MAX_ENI

まずはデフォルト状態に戻すため、WARM_ENI_TARGET を 1 に設定します。

$ kubectl set env daemonset aws-node -n kube-system WARM_ENI_TARGET=1
daemonset.apps/aws-node env updated

これで元の 4 ENI, 24 IP の状態に戻りました。ここから MAX_ENI を 1 に設定してみます。

$ kubectl set env daemonset aws-node -n kube-system MAX_ENI=1
daemonset.apps/aws-node env updated

値を確認します。

$ kubectl get daemonset aws-node -n kube-system -o jsonpath='{.spec.template.spec.containers[*].env}' | jq '.[] | select(.name == "MAX_ENI")'
{
  "name": "MAX_ENI",
  "value": "1"
}

MAX_ENI が 1 に設定されたことが確認できました。しかし1時間待っても ENI の数は減りませんでした。

ドキュメントには記載を見つけられなかったのですが、WARM_ENI_TARGET,MAX_ENI を両方設定する場合は先に設定している方が強いような挙動でした。

設定 Node1 の ENI 数 Node2 の ENI 数 Node3 の ENI 数
WARM_ENI_TARGET=1 + MAX_ENI なし ⇨ WARM_ENI_TARGET=1 + MAX_ENI=1 2 ⇨ 2 1 ⇨ 1 1 ⇨ 1
WARM_ENI_TARGET=0 + MAX_ENI=1 ⇨ WARM_ENI_TARGET=1 + MAX_ENI=1 1 ⇨ 1 1 ⇨ 1 1 ⇨ 1

WARM_ENI_TARGET=0, MAX_ENI=1 に設定すると、3 ENI, 18 IP の構成になったので、この状態で先ほどと同様に Pod を 100 個起動する deployment を起動してみます。MAX_ENI が 1 のため、13(5 * 3 - 2)Pod だけ起動するはずです。

$ kubectl apply -f test-deployment.yaml
deployment.apps/deployment created

想定通り 13 Pod 起動できました。

$ kubectl get deploy
NAME         READY    UP-TO-DATE   AVAILABLE   AGE
deployment   13/100   100          13          101s

この時の Node の ENI,IP の状態は以下でした。

f:id:dunkshoot:20220315001703p:plain

4.4. WARM_IP_TARGET

まずはデフォルト状態に戻すため、WARM_ENI_TARGET を 1 に設定し、MAX_ENI を削除します。

$ kubectl set env daemonset aws-node -n kube-system WARM_ENI_TARGET=1
daemonset.apps/aws-node env updated
$ kubectl set env daemonset aws-node -n kube-system MAX_ENI-
daemonset.apps/aws-node env updated

これで元の 4 ENI, 24 IP の状態に戻りました。ここから WARM_IP_TARGET を 1 に設定してみます。

$ kubectl set env daemonset aws-node -n kube-system WARM_IP_TARGET=1
daemonset.apps/aws-node env updated

値を確認します。

$ kubectl get daemonset aws-node -n kube-system -o jsonpath='{.spec.template.spec.containers[*].env}' | jq '.[] | select(.name == "WARM_ENI_TARGET")'
{
  "name": "WARM_ENI_TARGET",
  "value": "1"
}
$ kubectl get daemonset aws-node -n kube-system -o jsonpath='{.spec.template.spec.containers[*].env}' | jq '.[] | select(.name == "WARM_IP_TARGET")'
{
  "name": "WARM_IP_TARGET",
  "value": "1"
}

WARM_IP_TARGET が 1 に設定されたことが確認できました。少し待つと以下のように余剰 IP が一つになりました。

なお、WARM_IP_TARGET を設定している場合は WARM_ENI_TARGET は無視されるとドキュメントに記載がありましたが、WARM_ENI_TARGET=1 により追加されていた ENI はデタッチされずにアタッチされたままでした。

f:id:dunkshoot:20220315001751p:plain

一度 WARM_ENI_TARGET=0 に設定してから WARM_IP_TARGET=1 を設定すると以下のようになります。

f:id:dunkshoot:20220315001939p:plain

この状態で 3 個の空き IP を全て使ってみます。

3 Pod 起動する deployment を起動すると以下のように3 個の余剰 IP が消費され、新たに 3 IP が追加されました。

f:id:dunkshoot:20220315002033p:plain

先ほどと同様に Pod を 100 個起動する deployment を起動してみます。MAX_ENI は設定していないので上限の 43(5 * 3 * 3 - 2)Pod 起動するはずです。

$ kubectl apply -f test-deployment.yaml
deployment.apps/deployment created

想定通り 43 Pod 起動できました。

$ kubectl get deploy
NAME         READY    UP-TO-DATE   AVAILABLE   AGE
deployment   43/100   100          43          3m58s

4.5. MINIMUM_IP_TARGET

まずはデフォルト状態に戻すため、WARM_ENI_TARGET を 1 に設定し、WARM_IP_TARGET を削除します。

$ kubectl set env daemonset aws-node -n kube-system WARM_ENI_TARGET=1
daemonset.apps/aws-node env updated
$ kubectl set env daemonset aws-node -n kube-system WARM_IP_TARGET-
daemonset.apps/aws-node env updated

これで元の 4 ENI, 24 IP の状態に戻りました。ここから MINIMUM_IP_TARGET を 3 に設定してみます。

$ kubectl set env daemonset aws-node -n kube-system MINIMUM_IP_TARGET=3
daemonset.apps/aws-node env updated

値を確認します。

$ kubectl get daemonset aws-node -n kube-system -o jsonpath='{.spec.template.spec.containers[*].env}' | jq '.[] | select(.name == "WARM_ENI_TARGET")'
{
  "name": "WARM_ENI_TARGET",
  "value": "1"
}
$ kubectl get daemonset aws-node -n kube-system -o jsonpath='{.spec.template.spec.containers[*].env}' | jq '.[] | select(.name == "MINIMUM_IP_TARGET")'
{
  "name": "MINIMUM_IP_TARGET",
  "value": "3"
}

MINIMUM_IP_TARGET が 3 に設定されたことが確認できました。少し待つと以下のように ENI のセカンダリ IP が 3 個になりました。

今回もWARM_ENI_TARGET=1 により追加されていた ENI はデタッチされずにアタッチされたままだったので、一度 WARM_ENI_TARGET=0 に設定してから MINIMUM_IP_TARGET=3 を設定すると以下のようになります。

f:id:dunkshoot:20220315002346p:plain

この状態で 7 個の空き IP を全て使ってみます。

7 Pod 起動する deployment を起動すると以下のように7 個の余剰 IP が消費されただけで、新たな IP は追加されませんでした。(余剰 IP はゼロの状態)

f:id:dunkshoot:20220315002432p:plain

4.6. どの環境変数を使用するか

例えば以下要件の場合を考えます。


  • 3 Node 構成
  • Node のインスタンスサイズは t3.medium
    • ENI 制限は 3、ENI あたりの IP 制限は 6(プライマリ IP 1 + セカンダリ IP 5)
  • 常時起動する Pod は 11
    • coredns x 2(異なる Node に起動)
    • カスタム Pod x 9(各 Node に 3 個ずつ起動)
  • つまり、常時使用するセカンダリ IP は 11
  • カスタム Pod のデプロイはローリングアップデート
    • 使用するセカンダリ IP は最大 20(coredns 用 2 + カスタム Pod 用 18)
    • デプロイはなるべく早く終わらせたい(ENI のアタッチ時間を待ちたくないので常時 Pod 用に 20 IP は確保しておきたい)

まず、セカンダリ IP の最大が 20 ということは ENI が 4 個(セカンダリ IP 5 * 4 = 20)で足ります。そのため、MAX_ENI を 2 に設定(Cluster 合計で 6 個の ENI)します。WARM_ENI_TARGET は 0 に設定するか削除しておきます。オペミスや不具合により Pod が大量に起動したとしても MAX_ENI を 2 に設定しておけば最大 36 IP の消費ですみます。設定しない場合は最大 54 IP 使用してしまいます。

つぎに、常時使用するセカンダリ IP は 11 ということで、MINIMUM_IP_TARGET を 4 に設定します。4 に設定すると最低でも 12 個のセカンダリ IP を確保しておくことになります。

デプロイ時には追加で セカンダリ IP が 9 必要のため、WARM_IP_TARGET を 3 に設定します。3 に設定すると余剰 IP が 9 個になるのでデプロイ時に ENI のアタッチや IP のアサイン処理が発生しなくなります。

結論としては以下のようになります。

WARM_ENI_TARGET MAX_ENI WARM_IP_TARGET MINIMUM_IP_TARGET 通常時 IP 数 最大 IP 数
0 or 未設定 2 3 4 24 36

5. まとめ

デフォルト状態だと EKS Cluster は多くの IP を使用するため IP の削減方法について整理しました。複数チームで VPC を共有している場合など、IP を節約したいケースはあるかと思います。

aws-node の環境変数は手動で設定しますが、再構築時や横展開する際に設定作業を忘れる事もあるので構成管理は必要です。が、良い方法が見つからず悩み中です。

6. 参考

Amazon EKS ネットワーク - Amazon EKS

ポッドネットワーキング (CNI) - Amazon EKS

Elastic Network Interface - Amazon Elastic Compute Cloud

GitHub - aws/amazon-vpc-cni-k8s: Networking plugin repository for pod networking in Kubernetes using Elastic Network Interfaces on AWS

amazon-vpc-cni-k8s/eni-and-ip-target.md at master · aws/amazon-vpc-cni-k8s · GitHub

Kubernetes のガベージコレクション

KubernetesガベージコレクションGC) について整理し、挙動を確認しました。

1. Kubernetesガベージコレクション

Kubernetesガベージコレクションクラスタリソースを掃除する以下仕組みの総称です。

  • Failed 状態の Pod
  • 終了した Job
  • owner reference の無いオブジェクト
  • 未使用のイメージ
  • 未使用のコンテナ
  • reclaimPolicy が Delete の StorageClass から動的にプロビジョニングされた PersistentVolume
  • 失効または期限切れの CertificatesSigningRequest(CSR
  • 次のシナリオで削除された Node
    • クラスタが cloud controller manager を使う場合のクラウド環境
    • クラスタが cloud controller manager と同様のアドオンを使用する場合のオンプレ環境
  • Node Lease オブジェクト

eksctl コマンドで EKS Cluster を作成する - YasuBlog の記事で作成した EKS Cluster を使用して検証してみました。

1.1. Failed 状態の Pod

まずは Pod のライフサイクルについて整理します。Pod のとりうるフェーズは以下の 5 種類です。

フェーズ 説明
Pending Pod がクラスタによって承認されたが、1 つ以上のコンテナが稼働する準備ができていない状態。
これには、スケジュールされるまでの時間やネットワーク経由でイメージをダウンロードするための時間などが含まれる。
Running Pod が Node にバインドされ、全てのコンテナが作成された状態。
少なくとも1つのコンテナが実行されているか、開始または再起動中。
Succeeded Pod 内の全てのコンテナが正常に終了し、再起動しない状態。
Failed Pod 内の全てのコンテナが終了し、少なくとも1つのコンテナが異常終了した状態。
つまり、コンテナが 0 以外のステータスで終了したか、システムによって終了された。
Unknown 何らかの理由により Pod の状態を取得できない。
このフェーズは通常は Node との通信エラーにより発生する。

Failed 状態になった Pod は、人またはコントローラーが明示的に削除するまで存在します。

コントロールプレーンは、終了状態の Pod(Succeeded または Failed フェーズを持つ Pod)の数が設定された閾値(kube-controller-managerterminated-pod-gc-threshold)を超えたとき、それらのPodを削除します。 terminated-pod-gc-threshold のデフォルト値は 12,500 です。つまり Failed 状態の Pod が 12,500 個を超えたらコントロールプレーンによって自動で削除されます。(あまり無いユースケースかと思います)

12,500 個の Failed Pod を作成して検証するのは面倒なので、「Failed 状態になった Pod は、人またはコントローラーが明示的に削除するまで存在します。」の部分のみ確認します。

Failed は コンテナが 0 以外のステータスで終了したか、システムによって終了された という状態なので、終了コードが 1 となり終了後に再起動しない設定の以下 Pod を作成してみます。

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
    - name: amazonlinux
      image: public.ecr.aws/amazonlinux/amazonlinux:latest
      command:
        - "bin/bash"
        - "-c"
        - "exit 1"
  restartPolicy: Never

apply します。

$ kubectl apply -f test-pod.yaml
pod/test-pod created

Pod が作成されました。

$ kubectl get pod
NAME       READY   STATUS   RESTARTS   AGE
test-pod   0/1     Error    0          18s

exit 1 を実行しているため STATUS が Error になっています。restartPolicy: Never のため再起動もしません。この状態のまま変化はありませんでした。

describe で Pod の詳細を確認します。

$ kubectl describe pod test-pod
Name:         test-pod
Namespace:    default
Priority:     0
Node:         ip-10-0-102-214.ap-northeast-1.compute.internal/10.0.102.214
Start Time:   Wed, 23 Feb 2022 00:02:53 +0900
Labels:       <none>
Annotations:  kubernetes.io/psp: eks.privileged
Status:       Failed
IP:           10.0.102.139

~省略~

9 行目の通り、Status は Failed になっています。Failed Pod は人またはコントローラーが明示的に削除するまで存在する事が確認できました。

1.2. 終了した Job

TTL-after-finished controller が、終了した Job(Complete か Failed)を削除します。Job 終了から削除までの時間を .spec.ttlSecondsAfterFinished フィールドで指定します。この機能は 1.23 で stable になりました。

Job の終了後 10 秒経ったら削除される設定の以下 Job を作成してみます。6 行目に ttlSecondsAfterFinished を設定しています。

apiVersion: batch/v1
kind: Job
metadata:
  name: test-job
spec:
  ttlSecondsAfterFinished: 10
  template:
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "exit 0"
      restartPolicy: Never

apply します。

$ kubectl apply -f test-job.yaml
job.batch/test-job created

1 秒おきに get job を実行して 10 秒で削除されることを確認します。

$ while true; do date; kubectl get job; sleep 1; done;
2022223日 水曜日 002803秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         4s
2022223日 水曜日 002805秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         5s
2022223日 水曜日 002806秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         7s
2022223日 水曜日 002808秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         9s
2022223日 水曜日 002809秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         10s
2022223日 水曜日 002811秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         12s
2022223日 水曜日 002813秒 JST
No resources found in default namespace.
2022223日 水曜日 002814秒 JST
No resources found in default namespace.

コンテナ起動後、10 秒後に削除されて表示されなくなりました。

終了した Job が .spec.ttlSecondsAfterFinished フィールドで指定した時間後に削除されることを確認できました。

1.3. owner reference の無いオブジェクト

1.3.1. owner と従属オブジェクト

まずはオブジェクトの親子関係について整理します。

Kubernetes では、いくつかのオブジェクトは他のオブジェクトの owner です。例えば ReplicaSet により起動した Pod の owner はその ReplicaSet です。 owner に所有されているオブジェクトは従属オブジェクトと呼びます。

従属オブジェクトは metadata.ownerReferences フィールドで owner を示します。ownerReferences はオブジェクト名と UID で構成されます。ReplicaSet, DaemonSet, Deployment, Job, CronJob, ReplicationController の従属オブジェクトは Kubernetes が自動で ownerReferences を設定します。自動ではなくユーザがマニュアルで設定する事も可能ですが、通常はマニュアルで設定する必要はありません。

従属オブジェクトは metadata.ownerReferences.blockOwnerDeletion フィールドも持ちます。これは truefalse を値に持つことができ、owner オブジェクトの削除をブロックするかどうかを制御できます。これは後述する foreground カスケード削除を明示的に指定した時のみ作用します。background カスケード削除(デフォルトの削除方法)の場合は意味のないフィールドとなります。なお、Kubernetes は自動で blockOwnerDeletion を true に設定します。こちらも自動ではなくユーザがマニュアルで設定する事が可能です。
例えば Deployment を foreground カスケード削除する場合、Pod, ReplicaSet は blockOwnerDeletion フィールドが true のため Pod は ReplicaSet, ReplicaSet は Deployment の削除をブロックします。つまり、Pod, ReplicaSet が削除された後に Deployment が削除されます。

では、Deployment を起動して Deployment,ReplicaSet,Pod の owner 関連のフィールドを確認します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"

Deployment の uid を確認します。なお、Deployment は owner なので ownerReferences フィールドはありません。

$ kubectl get deploy deployment -o jsonpath='{.metadata.uid}'
1dee8bae-4a55-41c0-8a43-4b35dd4dbb24%

ReplicaSet の ownerReferences フィールドです。owner が Deployment になっていることがわかります。

$ kubectl get rs -o yaml | grep -i owner -A4
    ownerReferences:
    - apiVersion: apps/v1
      blockOwnerDeletion: true
      controller: true
      kind: Deployment
      name: deployment
      uid: 1dee8bae-4a55-41c0-8a43-4b35dd4dbb24
$ kubectl get rs deployment-6bb985c8c9 -o jsonpath='{.metadata.uid}'
450c9fea-f13e-462d-9320-509dc1a90cf9%

Pod の ownerReferences フィールドです。owner が ReplicaSet になっていることがわかります。

$ kubectl get pod -o yaml | grep -i owner -A4
    ownerReferences:
    - apiVersion: apps/v1
      blockOwnerDeletion: true
      controller: true
      kind: ReplicaSet
      name: deployment-6bb985c8c9
      uid: 450c9fea-f13e-462d-9320-509dc1a90cf9

続いて blockOwnerDeletion の挙動を確認します。Deployment を(foreground カスケード削除で)削除すると Pod -> ReplicaSet -> Deployment の順に削除されることを確認します。

時間がわかりやすいようにタイムスタンプを出力します。

$ gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment --cascade=foreground; gdate +"%Y-%m-%d %H:%M:%S.%3N"
2022-02-26 21:25:14.747
deployment.apps "deployment" deleted
2022-02-26 21:25:46.899

get pod/rs/deploy のログは以下のようになりました。※ mac で実行しているため gdate コマンドを使用しています(mac のデフォルトの date コマンドだとミリ秒が出力できないため)

# Pod
$ kubectl  get pod -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 21:24:42.681     NAME                          READY   STATUS    RESTARTS   AGE
2022-02-26 21:24:42.684     deployment-6bb985c8c9-n8rj8   1/1     Running   0          16s
2022-02-26 21:25:14.994     deployment-6bb985c8c9-n8rj8   1/1     Terminating   0          48s
2022-02-26 21:25:45.815     deployment-6bb985c8c9-n8rj8   0/1     Terminating   0          79s
2022-02-26 21:25:46.800     deployment-6bb985c8c9-n8rj8   0/1     Terminating   0          80s
2022-02-26 21:25:46.808     deployment-6bb985c8c9-n8rj8   0/1     Terminating   0          80s
# ReplicaSet
$ kubectl  get rs -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 21:24:44.108     NAME                    DESIRED   CURRENT   READY   AGE
2022-02-26 21:24:44.110     deployment-6bb985c8c9   1         1         1       18s
2022-02-26 21:25:14.969     deployment-6bb985c8c9   1         1         1       48s
2022-02-26 21:25:14.979     deployment-6bb985c8c9   1         1         1       48s
2022-02-26 21:25:15.012     deployment-6bb985c8c9   1         0         0       48s
2022-02-26 21:25:46.863     deployment-6bb985c8c9   1         0         0       80s
# Deployment
$ kubectl get deploy -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 21:24:45.646     NAME         READY   UP-TO-DATE   AVAILABLE   AGE
2022-02-26 21:24:45.649     deployment   1/1     1            1           19s
2022-02-26 21:25:14.908     deployment   1/1     1            1           48s
2022-02-26 21:25:14.933     deployment   1/1     1            1           48s
2022-02-26 21:25:15.020     deployment   0/1     0            0           49s
2022-02-26 21:25:46.874     deployment   0/1     0            0           80s
2022-02-26 21:25:46.895     deployment   0/1     0            0           80s

Pod -> ReplicaSet -> Deployment の順に削除されていることを確認できました。(厳密には削除された時間は不明ですが、最後に出力された時間の順が Pod -> ReplicaSet -> Deployment になっています)

では、ここで Pod, ReplicaSet の blockOwnerDeletion フィールドを false にして同様に検証してみます。 まずは kubectl edit で blockOwnerDeletiontrue から false に変更します。

# Pod
$ kubectl get pod
NAME                          READY   STATUS    RESTARTS   AGE
deployment-6bb985c8c9-8h9hh   1/1     Running   0          9m20s
$ kubectl edit pod deployment-6bb985c8c9-8h9hh
pod/deployment-6bb985c8c9-8h9hh edited
$ kubectl get pod -o yaml | grep -i owner -A4
    ownerReferences:
    - apiVersion: apps/v1
      blockOwnerDeletion: false
      controller: true
      kind: ReplicaSet
      name: deployment-6bb985c8c9
      uid: 14861de6-9073-42ee-ac13-1e87b1d263be
# ReplicaSet
$ kubectl get rs
NAME                    DESIRED   CURRENT   READY   AGE
deployment-6bb985c8c9   1         1         1       8m8s
$ kubectl edit rs deployment-6bb985c8c9
replicaset.apps/deployment-6bb985c8c9 edited
$ kubectl get rs -o yaml | grep -i owner -A4
    ownerReferences:
    - apiVersion: apps/v1
      blockOwnerDeletion: false
      controller: true
      kind: Deployment
      name: deployment
      uid: 9a411b52-54e7-4a8a-bb4e-09be999cdbec

それでは Deployment を削除します。

$ gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment --cascade=foreground; gdate +"%Y-%m-%d %H:%M:%S.%3N"
2022-02-26 21:37:51.581
deployment.apps "deployment" deleted
2022-02-26 21:37:51.950

get pod/rs/deploy のログは以下のようになりました。

# Pod
$ kubectl  get pod -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 21:37:10.680     NAME                          READY   STATUS    RESTARTS   AGE
2022-02-26 21:37:10.683     deployment-6bb985c8c9-8h9hh   1/1     Running   0          9m57s
2022-02-26 21:37:51.955     deployment-6bb985c8c9-8h9hh   1/1     Terminating   0          10m
2022-02-26 21:38:22.868     deployment-6bb985c8c9-8h9hh   0/1     Terminating   0          11m
2022-02-26 21:38:28.068     deployment-6bb985c8c9-8h9hh   0/1     Terminating   0          11m
2022-02-26 21:38:28.075     deployment-6bb985c8c9-8h9hh   0/1     Terminating   0          11m
# ReplicaSet
$ kubectl  get rs -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 21:37:11.632     NAME                    DESIRED   CURRENT   READY   AGE
2022-02-26 21:37:11.635     deployment-6bb985c8c9   1         1         1       9m58s
2022-02-26 21:37:51.927     deployment-6bb985c8c9   1         1         1       10m
2022-02-26 21:37:51.934     deployment-6bb985c8c9   1         1         1       10m
2022-02-26 21:37:51.953     deployment-6bb985c8c9   1         1         1       10m
# Deployment
$ kubectl get deploy -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 21:37:12.989     NAME         READY   UP-TO-DATE   AVAILABLE   AGE
2022-02-26 21:37:12.992     deployment   1/1     1            1           9m59s
2022-02-26 21:37:51.865     deployment   1/1     1            1           10m
2022-02-26 21:37:51.889     deployment   1/1     1            1           10m
2022-02-26 21:37:51.933     deployment   1/1     1            1           10m

blockOwnerDeletion フィールドが false のため owner の削除はブロックされませんでした。そのため Deployment -> ReplicaSet -> Pod の順に削除されました。 true の時は delete deploy コマンドが返ってくるまでに 32 秒かかりましたが、false の時は Pod, ReplicaSet の削除を待たないので 1 秒未満で返ってきました。

1.3.2. finalizer

続いて finalizer について整理します。

finalizer とは、必要なリソースを誤って削除する事を防止するための機能です。Kubernetes にリソースの削除を命令すると、コントローラはリソースの finalizer ルールを処理します。

例えば、Pod が使用中の PersistentVolume(PV) を削除しようとすると、PV は Terminating ステータスとなりますが削除はすぐには行われません。なぜなら PV は kubernetes.io/pv-protection の finalizer を持っているからです。PV がどの Pod にもバウンドされなくなると finalizer がクリアされて PV が削除されます。

では、以下 manifest の PVC と Pod を作成して、PV の finalizer を確認してみます。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  storageClassName: gp2
  resources:
    requests:
      storage: 10G
  accessModes:
  - ReadWriteOnce
apiVersion: v1
kind: Pod
metadata:
  name: test-pvc-pod
spec:
  containers:
  - name: amazonlinux
    image: public.ecr.aws/amazonlinux/amazonlinux:latest
    command:
      - "bin/bash"
      - "-c"
      - "sleep 3600"
    volumeMounts:
    - name: hoge
      mountPath: /hoge
  volumes:
  - name: hoge
    persistentVolumeClaim:
      claimName: test-pvc

それぞれ apply して PV の finalizer を確認します。

$ kubectl apply -f test-pvc.yaml
persistentvolumeclaim/test-pvc created
$ kubectl apply -f test-pvc-pod.yaml
pod/test-pvc-pod created
$ kubectl get pv -o jsonpath='{.items[*].metadata.finalizers}'
["kubernetes.io/pv-protection"]%

kubernetes.io/pv-protection の finalizer があることを確認できました。この状態で PV を削除してみます。

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
pvc-66312388-5700-472a-a75c-9a94c2a472d4   10Gi       RWO            Delete           Bound    default/test-pvc   gp2                     16s
$ kubectl delete pv pvc-66312388-5700-472a-a75c-9a94c2a472d4
persistentvolume "pvc-66312388-5700-472a-a75c-9a94c2a472d4" deleted

deleted と表示されたまま応答は返ってきませんでした。別ターミナルで get pv したら Terminating 状態になっていました。PV は削除されてないので、Pod にログインして PV がマウントされているディレクトリにファイルを作成する事もできました。

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS        CLAIM              STORAGECLASS   REASON   AGE
pvc-66312388-5700-472a-a75c-9a94c2a472d4   10Gi       RWO            Delete           Terminating   default/test-pvc   gp2                     44s
$ kubectl get pod
NAME           READY   STATUS    RESTARTS   AGE
test-pvc-pod   1/1     Running   0          69s
$ kubectl exec -it test-pvc-pod -- /bin/bash
bash-4.2# touch /hoge/fuga
bash-4.2# ls -lh /hoge/fuga
-rw-r--r-- 1 root root 0 Feb 28 15:06 /hoge/fuga

kubernetes.io/pv-protection finalizer により PV が削除できないことが確認できました。

1.3.3. カスケード削除

続いてカスケード削除について整理します。

owner オブジェクトが削除されると、ガベージコレクションにより従属オブジェクトも自動で削除されます。従属オブジェクトの自動削除をカスケード削除と言います。 finalizer を使用して、ガベージコレクションがいつどのように従属オブジェクトを削除するかコントロールできます。

カスケード削除には foreground と background の二種類があります。削除のオプションとして orphan もあるので並べて記載します。

タイプ owner 削除時の挙動
foreground ・owner オブジェクトは deletion in progress ステータスになり、以下の状態となる。
 ・オブジェクトに削除マークが付いた時間が metadata.deletionTimestamp フィールドにセットされる。
 ・metadata.finalizers フィールドに foregroundDeletion がセットされる。
 ・削除処理が完了するまでオブジェクトは API から見える状態となる。
deletion in progress ステータスに変わったら従属オブジェクトを削除する。全ての従属オブジェクトを削除したら owner オブジェクトを削除する。
・カスケード削除の間、ownerReference.blockOwnerDeletion=true フィールドを持つオブジェクトは owner の削除をブロックする。
background ・すぐに owner オブジェクトを削除し、従属オブジェクトをバックグラウンドで削除する。
Kubernetes はデフォルトで background を使う。
orphan ・owner オブジェクトだけ削除する
metadata.finalizers フィールドに orphan がセットされる
・ownerReferences の無いオブジェクトは orphan(みなしご)オブジェクトと呼ばれる

では、foreground カスケード削除の挙動を確認します。

上の blockOwnerDeletion の検証で未確認の部分のみ確認します。Deployment を --cascade=foreground で削除します。

$ kubectl gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment --cascade=foreground; gdate +"%Y-%m-%d %H:%M:%S.%3N"
2022-02-26 22:39:00.299
deployment.apps "deployment" deleted
2022-02-26 22:39:32.592

削除中に別ターミナルで Deployment の metadata を確認します。

$ kubectl get deploy -o jsonpath='{.items[*].metadata.deletionTimestamp}'
2022-02-26T13:39:00Z%
$ kubectl get deploy -o jsonpath='{.items[*].metadata.finalizers}'
["foregroundDeletion"]%

削除命令を実行した時間が metadata.deletionTimestamp フィールドにセットされ、foregroundDeletion finalizer がセットされることを確認できました。

次は background カスケード削除を確認します。デフォルトで background になるため delete コマンドに特にオプションを追加する必要はありません。先ほどと同様にタイムスタンプを表示します。

$ gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment; gdate +"%Y-%m-%d %H:%M:%S.%3N"
2022-02-26 22:47:23.343
deployment.apps "deployment" deleted
2022-02-26 22:47:23.604

get pod/rs/deploy の結果です。

# Pod
$ kubectl get pod -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 22:47:08.734     NAME                          READY   STATUS    RESTARTS   AGE
2022-02-26 22:47:08.739     deployment-6bb985c8c9-lj5rs   1/1     Running   0          8s
2022-02-26 22:47:23.594     deployment-6bb985c8c9-lj5rs   1/1     Terminating   0          23s
2022-02-26 22:47:54.449     deployment-6bb985c8c9-lj5rs   0/1     Terminating   0          54s
2022-02-26 22:47:58.114     deployment-6bb985c8c9-lj5rs   0/1     Terminating   0          58s
2022-02-26 22:47:58.120     deployment-6bb985c8c9-lj5rs   0/1     Terminating   0          58s
# ReplicaSet
$ kubectl get rs -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 22:47:10.540     NAME                    DESIRED   CURRENT   READY   AGE
2022-02-26 22:47:10.543     deployment-6bb985c8c9   1         1         1       11s
2022-02-26 22:47:23.578     deployment-6bb985c8c9   1         1         1       24s
# Deployment
$ kubectl get deploy -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done
2022-02-26 22:47:11.555     NAME         READY   UP-TO-DATE   AVAILABLE   AGE
2022-02-26 22:47:11.558     deployment   1/1     1            1           12s
2022-02-26 22:47:23.519     deployment   1/1     1            1           24s

background の場合はすぐに Deployment が削除されて、その後に ReplicaSet, Pod が削除された事が確認できました。なお、以下のように削除時に明示的に background を指定しても挙動は同じです。

kubectl delete deploy deployment --cascade=background

最後に orphan です。削除時に --cascade=orphan を与える事で owner だけ削除し、従属オブジェクトを削除しないようにできます。

$ kubectl delete deploy deployment --cascade=orphan
deployment.apps "deployment" deleted
# Pod
$ kubectl get pod
NAME                          READY   STATUS    RESTARTS   AGE
deployment-6bb985c8c9-jtbzh   1/1     Running   0          2m7s# ReplicaSet
# ReplicaSet
$ kubectl get rs
NAME                    DESIRED   CURRENT   READY   AGE
deployment-6bb985c8c9   1         1         1       2m8s
# Deployment
$ kubectl get deployment
No resources found in default namespace.

Deployment だけ削除されて、ReplicaSet, Pod は削除されない事が確認できました。

少し経つと ReplicaSet の ownerReferences フィールドが消えました。ReplicaSet はまだ生きているので Pod の ownerReferences はまだありました。

$ kubectl get rs -o jsonpath='{.items[*].metadata.ownerReferences}'
$ kubectl get pod -o jsonpath='{.items[*].metadata.ownerReferences}'
[{"apiVersion":"apps/v1","blockOwnerDeletion":true,"controller":true,"kind":"ReplicaSet","name":"deployment-6bb985c8c9","uid":"2c1258b9-6d78-40f7-9ca6-e19a0a12298c"}]%

1.4. 未使用のイメージ

1.4.1. 説明

kubelet は未使用のイメージを 5 分おきにガベージコレクションで削除しています。

https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet.go#L172 に 5 分と定義されている

ガベージコレクションを実施するかどうかは、以下の変数によって決まります。

変数 kubelet フラグ 説明 デフォルト値
imageMinimumGCAge なし 未使用イメージが削除される前に経過すべき最小時間 2m
imageGCHighThresholdPercent image-gc-high-threshold イメージのガベージコレクションをトリガーするディスク使用量の割合(%) 85
imageGCLowThresholdPercent image-gc-low-threshold イメージのガベージコレクションが解放を試みるディスク使用量の割合(%) 80

つまり、デフォルトでは、イメージが未使用になってから 2 分経ったらガベージコレクションの削除対象となります。 そして、ディスク使用量が 85% を超えたらディスク使用量が 80% になるまでイメージを削除します。最後に使用された時間に基づいて最も古いイメージから削除していきます。

1.4.2. EKS の設定値

EKS の場合の上記変数の値を確認します。Node の情報なので KubernetesAPI サーバから取得できます。

kubectl proxy コマンドでプロキシをローカルに起動すると API サーバにアクセスできます。

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

デフォルトでローカルの 8001 ポートでプロキシが起動するので、別ターミナルで http://localhost:8001/api/v1/nodes/<Node 名>/proxy/configz にアクセスします。json なので jq で整形すると見やすいです。

$ kubectl get node
NAME                                              STATUS   ROLES    AGE    VERSION
ip-10-0-102-42.ap-northeast-1.compute.internal   Ready    <none>   24h   v1.21.5-eks-9017834
$ curl -sSL "http://localhost:8001/api/v1/nodes/ip-10-0-102-42.ap-northeast-1.compute.internal/proxy/configz" | jq .
{
  "kubeletconfig": {
    "enableServer": true,
    "syncFrequency": "1m0s",
    "fileCheckFrequency": "20s",
    "httpCheckFrequency": "20s",
    "address": "0.0.0.0",
    "port": 10250,
    "tlsCipherSuites": [
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_128_GCM_SHA256"
    ],
    "serverTLSBootstrap": true,
    "authentication": {
      "x509": {
        "clientCAFile": "/etc/kubernetes/pki/ca.crt"
      },
      "webhook": {
        "enabled": true,
        "cacheTTL": "2m0s"
      },
      "anonymous": {
        "enabled": false
      }
    },
    "authorization": {
      "mode": "Webhook",
      "webhook": {
        "cacheAuthorizedTTL": "5m0s",
        "cacheUnauthorizedTTL": "30s"
      }
    },
    "registryPullQPS": 5,
    "registryBurst": 10,
    "eventRecordQPS": 5,
    "eventBurst": 10,
    "enableDebuggingHandlers": true,
    "healthzPort": 10248,
    "healthzBindAddress": "127.0.0.1",
    "oomScoreAdj": -999,
    "clusterDomain": "cluster.local",
    "clusterDNS": [
      "172.20.0.10"
    ],
    "streamingConnectionIdleTimeout": "4h0m0s",
    "nodeStatusUpdateFrequency": "10s",
    "nodeStatusReportFrequency": "5m0s",
    "nodeLeaseDurationSeconds": 40,
    "imageMinimumGCAge": "2m0s",
    "imageGCHighThresholdPercent": 85,
    "imageGCLowThresholdPercent": 80,
    "volumeStatsAggPeriod": "1m0s",
    "cgroupRoot": "/",
    "cgroupsPerQOS": true,
    "cgroupDriver": "cgroupfs",
    "cpuManagerPolicy": "none",
    "cpuManagerReconcilePeriod": "10s",
    "memoryManagerPolicy": "None",
    "topologyManagerPolicy": "none",
    "topologyManagerScope": "container",
    "runtimeRequestTimeout": "2m0s",
    "hairpinMode": "hairpin-veth",
    "maxPods": 17,
    "podPidsLimit": -1,
    "resolvConf": "/etc/resolv.conf",
    "cpuCFSQuota": true,
    "cpuCFSQuotaPeriod": "100ms",
    "nodeStatusMaxImages": 50,
    "maxOpenFiles": 1000000,
    "contentType": "application/vnd.kubernetes.protobuf",
    "kubeAPIQPS": 5,
    "kubeAPIBurst": 10,
    "serializeImagePulls": false,
    "evictionHard": {
      "memory.available": "100Mi",
      "nodefs.available": "10%",
      "nodefs.inodesFree": "5%"
    },
    "evictionPressureTransitionPeriod": "5m0s",
    "enableControllerAttachDetach": true,
    "protectKernelDefaults": true,
    "makeIPTablesUtilChains": true,
    "iptablesMasqueradeBit": 14,
    "iptablesDropBit": 15,
    "featureGates": {
      "RotateKubeletServerCertificate": true
    },
    "failSwapOn": true,
    "containerLogMaxSize": "10Mi",
    "containerLogMaxFiles": 5,
    "configMapAndSecretChangeDetectionStrategy": "Watch",
    "kubeReserved": {
      "cpu": "70m",
      "ephemeral-storage": "1Gi",
      "memory": "442Mi"
    },
    "enforceNodeAllocatable": [
      "pods"
    ],
    "volumePluginDir": "/usr/libexec/kubernetes/kubelet-plugins/volume/exec/",
    "logging": {
      "format": "text"
    },
    "enableSystemLogHandler": true,
    "shutdownGracePeriod": "0s",
    "shutdownGracePeriodCriticalPods": "0s",
    "enableProfilingHandler": true,
    "enableDebugFlagsHandler": true
  }
}

59-61 行目にイメージのガベージコレクション関連のパラメータがあります。全てデフォルト値でした。Node のインスタンスタイプを変更しても値は同じでした。

1.4.3. 設定変更方法

デフォルト値だとディスク使用量が 85% に達するとガベージコレクションがトリガーされます。

サーバのディスク監視の閾値として warning は 80%, critical は 90% にしているケースがあったりすると思います。そのケースの場合は 80% を超えてアラートがなってもその時点では未使用イメージが自動で削除されないので手動で不要なデータを消すか、85% を超えてガベージコレクションがトリガーされるまで待つ必要があります。それは微妙なので imageGCHighThresholdPercent を 70%, imageGCLowThresholdPercent を 50% に変更してみます。

Node 上の設定ファイル(/etc/kubernetes/kubelet/kubelet-config.json や /etc/systemd/system/kubelet.service)を直接修正する事で設定変更が可能です。 Node 再作成時にも同様に設定されるようにするには、カスタム起動テンプレートかカスタム AMI を使用する必要があります。

Amazon EKS ワーカーノードを設定して特定のディスク使用率でイメージキャッシュをクリーンアップする

今回は、起動テンプレートのユーザデータに以下を記載することで設定を変更しました。

if ! grep -q imageGCHighThresholdPercent /etc/kubernetes/kubelet/kubelet-config.json; 
then 
    sed -i '/"apiVersion*/a \ \ "imageGCHighThresholdPercent": 70,' /etc/kubernetes/kubelet/kubelet-config.json
fi
if ! grep -q imageGCLowThresholdPercent /etc/kubernetes/kubelet/kubelet-config.json; 
then 
    sed -i '/"imageGCHigh*/a \ \ "imageGCLowThresholdPercent": 50,' /etc/kubernetes/kubelet/kubelet-config.json
fi
systemctl restart kubelet

設定が変更されていることを確認しました。

$ curl -sSL "http://localhost:8001/api/v1/nodes/ip-10-0-102-160.ap-northeast-1.compute.internal/proxy/configz" | jq . | grep imageGC
    "imageGCHighThresholdPercent": 70,
    "imageGCLowThresholdPercent": 50,

1.5. 未使用のコンテナ

kubelet は未使用のコンテナを 1 分おきにガベージコレクションで削除しています。

https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet.go#L170 に 1 分と定義されている

完了したコンテナは以下変数に基づいて最も古いものから順に削除されます。

変数 説明 デフォルト値
MinAge ・完了したコンテナが削除される前に経過すべき最小時間
・0 に設定すると無効
0
MaxPerPodContainer ・Pod が持つことができる dead 状態のコンテナの最大値
・0 未満に設定すると無効
1
MaxContainers クラスタが持つことができる dead 状態のコンテナの最大値
・0 未満に設定すると無効
-1

つまり、デフォルトでは、コンテナが完了するとすぐに削除対象となり、1 分以内にガベージコレクションにより削除が実行されます。

MaxPerPodContainer と MaxContainers はコンフリクトするケースがあります。Pod あたりの dead コンテナ数の合計がクラスタあたりの dead コンテナ数を超えるようなケースです。この場合、コンフリクトを修正するため kubelet は MaxPerPodContainer を調整します。(MaxContainers が優先される)

なお、設定確認方法はわかりませんでした。(API で取得した Node 情報や Node 上のファイルからは確認できなかった)

1.6. reclaimPolicy が Delete の StorageClass から動的にプロビジョニングされた PersistentVolume

まず、StorageClass の reclaimPolicy とは、PersistentVolumeClaim(PVC) が削除された際に PersistentVolume(PV)を削除するか残すかを指定するパラメータです。削除の場合は Delete 、残す場合は Retain を指定します。デフォルトは Delete です。

PV が削除されると、AWS EBS/GCE PD/Azure Disk/Tencent CBS などの外部ボリュームも同様に削除されます。

では、PVC を削除して PV, EBS が自動で削除されることを確認します。

まずは EKS でデフォルトで使用できる StorageClass を確認します。

$ kubectl get storageclass -o yaml
apiVersion: v1
items:
- apiVersion: storage.k8s.io/v1
  kind: StorageClass
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"},"name":"gp2"},"parameters":{"fsType":"ext4","type":"gp2"},"provisioner":"kubernetes.io/aws-ebs","volumeBindingMode":"WaitForFirstConsumer"}
      storageclass.kubernetes.io/is-default-class: "true"
    creationTimestamp: "2022-02-28T08:29:50Z"
    name: gp2
    resourceVersion: "254"
    uid: c97d773f-a438-4d3b-aee4-a9f9d660647f
  parameters:
    fsType: ext4
    type: gp2
  provisioner: kubernetes.io/aws-ebs
  reclaimPolicy: Delete
  volumeBindingMode: WaitForFirstConsumer
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

gp2 の StorageClass がデフォルトで用意されており、reclaimPolicyDelete になっているのでこちらを使用します。

以下 manifest の PVC を作成します。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  storageClassName: gp2
  resources:
    requests:
      storage: 10G
  accessModes:
  - ReadWriteOnce

apply して PVC が作成されることを確認します。

$ kubectl apply -f test-pvc.yaml
persistentvolumeclaim/test-pvc created
$ kubectl get pvc
NAME       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-pvc   Pending                                      gp2            41s¨

PVC を関連付けた以下 manifest の Pod を作成します。

apiVersion: v1
kind: Pod
metadata:
  name: test-pvc-pod
spec:
  containers:
  - name: amazonlinux
    image: public.ecr.aws/amazonlinux/amazonlinux:latest
    command:
      - "bin/bash"
      - "-c"
      - "sleep 3600"
    volumeMounts:
    - name: hoge
      mountPath: /hoge
  volumes:
  - name: hoge
    persistentVolumeClaim:
      claimName: test-pvc

apply して Pod,PV が作成されることを確認します。

$ kubectl apply -f test-pvc-pod.yaml
pod/test-pvc-pod created
$ kubectl get pod
NAME           READY   STATUS    RESTARTS   AGE
test-pvc-pod   1/1     Running   0          24s
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
pvc-8369a57f-a42c-43b8-98a2-d39ed34af8fb   10Gi       RWO            Delete           Bound    default/test-pvc   gp2                     19s

指定したサイズ 10GB の EBS が作成されていることを確認します。

%aws ec2 describe-volumes --filters "Name=size,Values=10"
{
    "Volumes": [
        {
            "Attachments": [],
            "AvailabilityZone": "ap-northeast-1a",
            "CreateTime": "2022-02-28T14:45:40.745000+00:00",
            "Encrypted": false,
            "Size": 10,
            "SnapshotId": "",
            "State": "available",
            "VolumeId": "vol-0bd96ea93c9f4c2b7",
            "Iops": 100,
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "kubernetes-dynamic-pvc-f71826f1-0721-429e-a73d-df4afad32da0"
                },
                {
                    "Key": "kubernetes.io/cluster/ekstest",
                    "Value": "owned"
                },
                {
                    "Key": "kubernetes.io/created-for/pvc/name",
                    "Value": "test-pvc"
                },
                {
                    "Key": "kubernetes.io/created-for/pv/name",
                    "Value": "pvc-f71826f1-0721-429e-a73d-df4afad32da0"
                },
                {
                    "Key": "kubernetes.io/created-for/pvc/namespace",
                    "Value": "default"
                }
            ],
            "VolumeType": "gp2",
            "MultiAttachEnabled": false
        }
    ]
}

リソースの準備ができたので PVC を削除したいのですが、上で説明した通り PV には kubernetes.io/pv-protection finalizer があり Pod が Bound されていると PV を削除できないため、先に Pod を削除しておきます。

# Pod 削除
$ kubectl delete pod test-pvc-pod
pod "test-pvc-pod" deleted
# PVC 確認
$ kubectl get pvc
NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-pvc   Bound    pvc-f71826f1-0721-429e-a73d-df4afad32da0   10Gi       RWO            gp2            2m15s
# PV 確認
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
pvc-f71826f1-0721-429e-a73d-df4afad32da0   10Gi       RWO            Delete           Bound    default/test-pvc   gp2                     2m8s

PVC と PV だけになったので、PVC を削除してみます。

$ kubectl delete pvc test-pvc
persistentvolumeclaim "test-pvc" deleted
$ kubectl get pv
No resources found
$ aws ec2 describe-volumes --filters "Name=size,Values=10"
{
    "Volumes": []
}

PV,EBS が共に削除されたことを確認できました。

1.7. 失効または期限切れの CertificatesSigningRequest(CSR

KubernetesAPI サーバは X.509 クライアント証明書による認証をサポートしており、証明書を作成するためには KubernetesCSR オブジェクトが必要となります。 失効または有効期限切れの CSR オブジェクトはガベージコレクションにより削除されます。

CSR を作成するケースはあまり無い気がするので検証は割愛します。

なお、ガベージコレクションのドキュメントに記載されている「Stale or expired CertificateSigningRequests (CSRs)」のリンクは 404 エラーになります。
https://kubernetes.io/reference/access-authn-authz/certificate-signing-requests/#request-signing-process

1.8. 次のシナリオで削除された Node

  • クラスタが cloud controller manager を使う場合のクラウド環境
  • クラスタが cloud controller manager と同様のアドオンを使用する場合のオンプレ環境

cloud controller manager とは、KubernetesAWS/GCP 等のクラウドと連携するためのコンポーネントです。例えば ingress を作成すると AWS ALB を作成するといった処理を行っています。

EKS 等のマネージドサービスを使う場合は気にする必要はないので検証は割愛します。

1.9 Node Lease オブジェクト

Kubernetes ではクラスタが各 Node の可用性を判断し、障害時には何かアクションを起こせるように Node がクラスタにハートビートを送信しています。Lease はそのハートビート用のオブジェクトの一つです。

kube-node-lease namespace に各 Node ごとの Lease オブジェクトが保存されています。

kubelet はデフォルトでは 10 秒間隔で Lease オブジェクトの作成と更新を行います。更新に失敗した場合、200 ミリ秒で開始し上限を 7 秒としたエクスポネンシャルバックオフを使用してリトライします。

実際の Lease オブジェクトを確認してみます。

$ kubectl get node
NAME                                              STATUS   ROLES    AGE     VERSION
ip-10-0-101-200.ap-northeast-1.compute.internal   Ready    <none>   5m38s   v1.21.5-eks-9017834
ip-10-0-102-225.ap-northeast-1.compute.internal   Ready    <none>   5m51s   v1.21.5-eks-9017834
ip-10-0-103-131.ap-northeast-1.compute.internal   Ready    <none>   5m48s   v1.21.5-eks-9017834
$ kubectl get lease -n kube-node-lease
NAME                                              HOLDER                                            AGE
ip-10-0-101-200.ap-northeast-1.compute.internal   ip-10-0-101-200.ap-northeast-1.compute.internal   5m46s
ip-10-0-102-225.ap-northeast-1.compute.internal   ip-10-0-102-225.ap-northeast-1.compute.internal   5m59s
ip-10-0-103-131.ap-northeast-1.compute.internal   ip-10-0-103-131.ap-northeast-1.compute.internal   5m56s
$ kubectl describe lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease
Name:         ip-10-0-101-200.ap-northeast-1.compute.internal
Namespace:    kube-node-lease
Labels:       <none>
Annotations:  <none>
API Version:  coordination.k8s.io/v1
Kind:         Lease
Metadata:
  Creation Timestamp:  2022-02-28T08:42:48Z
  Managed Fields:
    API Version:  coordination.k8s.io/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:ownerReferences:
          .:
          k:{"uid":"16da548f-1128-4c0a-9f00-499a815c7146"}:
            .:
            f:apiVersion:
            f:kind:
            f:name:
            f:uid:
      f:spec:
        f:holderIdentity:
        f:leaseDurationSeconds:
        f:renewTime:
    Manager:    kubelet
    Operation:  Update
    Time:       2022-02-28T08:42:48Z
  Owner References:
    API Version:     v1
    Kind:            Node
    Name:            ip-10-0-101-200.ap-northeast-1.compute.internal
    UID:             16da548f-1128-4c0a-9f00-499a815c7146
  Resource Version:  3197
  UID:               1a81eed3-8df4-43cf-ab32-e753b6e7b65f
Spec:
  Holder Identity:         ip-10-0-101-200.ap-northeast-1.compute.internal
  Lease Duration Seconds:  40
  Renew Time:              2022-02-28T08:49:16.077127Z
Events:                    <none>

Node ごとに Lease オブジェクトがあることが確認できました。

更新頻度を確認します。

$ kubectl get lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease -o yaml -o jsonpath='{.spec.renewTime}'
2022-02-28T08:55:02.308638Z%
$ kubectl get lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease -o yaml -o jsonpath='{.spec.renewTime}'
2022-02-28T08:55:12.608059Z%
$ kubectl get lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease -o yaml -o jsonpath='{.spec.renewTime}'
2022-02-28T08:55:22.722818Z%

10 秒間隔で更新されていることが確認できました。

では、この Lease オブジェクトがどのようにガベージコレクションによって削除されるかというと、
ガベージコレクションのドキュメントには「Node lease オブジェクト」とあるだけで説明がないので何がどのように削除されるのか不明でした。

※以前削除した Node の Lease オブジェクトが 100 日以上残っていたので Node を削除しても Lease オブジェクトは削除されなさそうです

2. まとめ

Kubernetesガベージコレクションについて整理しました。量が多いですね。

EKS 等のマネージドサービスの場合はあまり気にする点はなかったです。が、kubelet の変数(イメージ GC閾値等)はもっと柔軟に変更できるようになると嬉しいですね。

あと、公式ドキュメントのリンクが切れてたり情報が少なかったり古い場合があったのが微妙でした。

3. 参考

Garbage Collection | Kubernetes

Pod Lifecycle | Kubernetes

kube-controller-manager | Kubernetes

Automatic Clean-up for Finished Jobs | Kubernetes

Owners and Dependents | Kubernetes

ObjectMeta | Kubernetes

Use Cascading Deletion in a Cluster | Kubernetes

Kubelet Configuration (v1beta1) | Kubernetes

Persistent Volumes | Kubernetes

Cloud Controller Manager | Kubernetes

Nodes | Kubernetes

Kubernetes の標準出力と標準エラー出力

Kubernetes では、コンテナの標準出力/標準エラー出力ログを kubectl logs コマンドで取得できます。実際にどのファイルに標準出力/標準エラー出力ログが出力されて、ローテートはどういう設定になっているのか整理しました。

1. ログの保存場所

まずは標準出力/標準エラー出力ログの保存場所です。

コンテナが標準出力/標準エラー出力に出力したログは Node に保存されます。実体のファイルとシンボリックリンクの構成は以下です。

f:id:dunkshoot:20220215230154p:plain

整理すると以下になります。

# ログ 説明
/var/lib/docker/containers/< Container ID >/< Container ID >-json.log ・コンテナの stdout/stderr に出力されたログが保存される実体のファイル
・起動中の Pod のログのみ保存
・終了した Pod のログは消える
・コンテナが再起動した場合、再起動前のコンテナのログは消えない
・保存されるのは 2 世代分のコンテナ(起動中のコンテナと再起動前のコンテナのログのみ保存)
/var/log/pods/< Namespace >_< Pod Name >_< Pod UID >/< Container Name >/< Index >.log ・①のファイルへのシンボリックリンク
・0.log が現在起動中のコンテナのログのリンク
・1.log が一つ前に起動していたコンテナのログのリンク
kubectl logs コマンドは API Server 経由で /var/log/pods にアクセスしている
/var/log/containers/< Pod Name >_< Namespace >_< Container Name >-< Container ID >.log ・②のファイルへのシンボリックリンク

実際のログです。

@Node $ ls -lh /var/log/pods/kube-system_coredns-76f4967988-pkkx9_1ab7cf50-3781-4576-8a4d-d918e321ee64/coredns/*
lrwxrwxrwx 1 root root 165  215 14:07 /var/log/pods/kube-system_coredns-76f4967988-pkkx9_1ab7cf50-3781-4576-8a4d-d918e321ee64/coredns/0.log -> /var/lib/docker/containers/52d7283862b0b64d6bdfc7dc4dce964bb3ca284ec3048325dbb7e5d61a1a00e3/52d7283862b0b64d6bdfc7dc4dce964bb3ca284ec3048325dbb7e5d61a1a00e3-json.log
lrwxrwxrwx 1 root root 165  215 15:17 /var/log/pods/kube-system_coredns-76f4967988-pkkx9_1ab7cf50-3781-4576-8a4d-d918e321ee64/coredns/1.log -> /var/lib/docker/containers/a402cbab8d63b90c3d76f87579fe73bca77a2bffae2d5e0b3bd1617a1dbc037b/a402cbab8d63b90c3d76f87579fe73bca77a2bffae2d5e0b3bd1617a1dbc037b-json.log
@Node $ ls -lh /var/log/containers/coredns-76f4967988-pkkx9_kube-system_coredns-*
lrwxrwxrwx 1 root root 101  215 14:07 /var/log/containers/coredns-76f4967988-pkkx9_kube-system_coredns-52d7283862b0b64d6bdfc7dc4dce964bb3ca284ec3048325dbb7e5d61a1a00e3.log -> /var/log/pods/kube-system_coredns-76f4967988-pkkx9_1ab7cf50-3781-4576-8a4d-d918e321ee64/coredns/0.log
lrwxrwxrwx 1 root root 101  215 15:17 /var/log/containers/coredns-76f4967988-pkkx9_kube-system_coredns-a402cbab8d63b90c3d76f87579fe73bca77a2bffae2d5e0b3bd1617a1dbc037b.log -> /var/log/pods/kube-system_coredns-76f4967988-pkkx9_1ab7cf50-3781-4576-8a4d-d918e321ee64/coredns/1.log

※ Pod の UID は kubectl get pods -n <Namespace> <Pod Name> -o jsonpath='{.metadata.uid}' で確認可能

2. コンテナログのローテート、世代

ログが増え続けると Node のディスクが逼迫するので、ローテートと削除の設定が必要です。Kubernetes では /etc/docker/daemon.json でそれらを設定しています。

オプション 説明
log-opts.max-size ・ログをローテートするファイルサイズ
・単位はキロバイト(k)/メガバイト(m)/ギガバイト(g)
log-opts.max-file ・ログの世代数
・max-size が設定されていない場合は無効

例えば、max-size:100m,max-file:10 の場合、一つのコンテナが最大 1 GB のログを保存する状態になります。最初に記載した通り、Pod としては 2 世代のコンテナのログを保存するため最大 2 GB になります。Node のディスクをサイジングする際はこれらを考慮する必要があります。

※ kubelet フラグにも同様の意味の containerLogMaxSizecontainerLogMaxFiles がありますが、検証したところ /etc/docker/daemon.jsonlog-opts に設定されている値でローテート、削除されていました

3. 検証

3.1. 検証環境構築

eksctl コマンドで EKS Cluster を作成する - YasuBlog の記事で作成した EKS Cluster を使用します。

3.2. EKS の設定値

max-size/max-file は Node 上の /etc/docker/daemon.json で確認できます。

@Node $ cat /etc/docker/daemon.json
{
  "bridge": "none",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "10"
  },
  "live-restore": true,
  "max-concurrent-downloads": 10,
  "default-ulimits": {
    "memlock": {
      "Hard": -1,
      "Name": "memlock",
      "Soft": -1
    }
  }
}

max-size10m, max-file10 に設定されています。

インスタンスタイプ毎の設定値を確認してみました。Node のリソース量に関わらず固定値が設定されているようです。

インスタンスタイプ max-size max-file
t3.small 10m 10
t3.medium 10m 10
m5.large 10m 10
m5.xlarge 10m 10

3.3. 挙動確認

つづいて実際の挙動を確認してみます。

3.3.1. ローテートと世代

まずはシンプルな Pod を起動します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"

コンテナ ID を確認します。

$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP            NODE                                              NOMINATED NODE   READINESS GATES
deployment-6bb985c8c9-p7b5w   1/1     Running   0          114s   10.0.102.47   ip-10-0-102-148.ap-northeast-1.compute.internal   <none>           <none>
$ kubectl describe pod deployment-6bb985c8c9-7xmm4 | grep 'Container ID'
    Container ID:  docker://057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7

Node 上のログを確認します。

@Node $ ls -lh /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/
合計 12K
-rw-r----- 1 root root    0  216 14:55 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log
drwx------ 2 root root    6  216 14:55 checkpoints
-rw------- 1 root root 4.8K  216 14:55 config.v2.json
-rw-r--r-- 1 root root 2.1K  216 14:55 hostconfig.json
drwx--x--- 2 root root    6  216 14:55 mounts
@Node $ ls -lh /var/log/pods/default_deployment-6bb985c8c9-p7b5w_ef8acb63-8389-47d6-aad8-c2976eac3665/amazonlinux/
合計 0
lrwxrwxrwx 1 root root 165  216 14:55 0.log -> /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log

標準出力/標準エラー出力に何も出力していないので、上記の通りサイズが 0 のファイルが一つだけあります。この状態で Pod にログインし、標準出力に大量にログを出力してみます。

$ kubectl exec -it deployment-6bb985c8c9-p7b5w -- /bin/bash
@Pod $ while true;do cat /etc/services > /proc/1/fd/1;done

Node 上のログを確認します。

@Node $ ls -lh /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/
合計 90M
-rw-r----- 1 root root 2.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.1
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.2
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.3
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.4
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.5
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.6
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.7
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.8
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.9
drwx------ 2 root root    6  216 14:55 checkpoints
-rw------- 1 root root 4.8K  216 14:55 config.v2.json
-rw-r--r-- 1 root root 2.1K  216 14:55 hostconfig.json
drwx--x--- 2 root root    6  216 14:55 mounts
@Node $ ls -lh /var/log/pods/default_deployment-6bb985c8c9-p7b5w_ef8acb63-8389-47d6-aad8-c2976eac3665/amazonlinux/
合計 0
lrwxrwxrwx 1 root root 165  216 14:55 0.log -> /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log

max-size/max-file の設定通り、ログサイズが 10M でローテートされ、10 世代保存されている事が確認できました。/var/log/pods0.log は常に最新のファイルのシンボリックリンクになっています。

3.3.2. コンテナ再起動

上記の状態で、Node 上で docker stop を実行してコンテナを再起動してみます。

@Node $ docker ps | grep amazonlinux
057fc611f5c4   public.ecr.aws/amazonlinux/amazonlinux                                       "bin/bash -c 'sleep …"   5 minutes ago    Up 5 minutes              k8s_amazonlinux_deployment-6bb985c8c9-p7b5w_default_ef8acb63-8389-47d6-aad8-c2976eac3665_0
@Node $ docker stop 057fc611f5c4
057fc611f5c4
$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE     IP            NODE                                              NOMINATED NODE   READINESS GATES
deployment-6bb985c8c9-p7b5w   1/1     Running   1          6m15s   10.0.102.47   ip-10-0-102-148.ap-northeast-1.compute.internal   <none>           <none>
$ kubectl describe pod deployment-6bb985c8c9-p7b5w | grep 'Container ID'
    Container ID:  docker://89f425cce426a87fad882cfc5f091b3f87dca4942bd5c9f9fbde69fc217ea476

コンテナを再起動したので Pod の RESTARTS が 0 から 1 に変わり、コンテナ ID も変わりました。

Node 上のログを確認します。

# 旧コンテナ
@Node $ ls -lh /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/
合計 89M
-rw-r----- 1 root root 2.6M  216 15:01 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.1
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.2
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.3
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.4
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.5
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.6
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.7
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.8
-rw-r----- 1 root root 9.6M  216 14:59 057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log.9
drwx------ 2 root root    6  216 14:55 checkpoints
-rw------- 1 root root 4.8K  216 15:01 config.v2.json
-rw-r--r-- 1 root root 2.1K  216 15:01 hostconfig.json
drwx--x--- 2 root root    6  216 14:55 mounts
# 新コンテナ
@Node $ ls -lh /var/lib/docker/containers/89f425cce426a87fad882cfc5f091b3f87dca4942bd5c9f9fbde69fc217ea476/
合計 12K
-rw-r----- 1 root root    0  216 15:01 89f425cce426a87fad882cfc5f091b3f87dca4942bd5c9f9fbde69fc217ea476-json.log
drwx------ 2 root root    6  216 15:01 checkpoints
-rw------- 1 root root 4.8K  216 15:01 config.v2.json
-rw-r--r-- 1 root root 2.1K  216 15:01 hostconfig.json
drwx--x--- 2 root root    6  216 15:01 mounts
@Node $ ls -lh /var/log/pods/default_deployment-6bb985c8c9-p7b5w_ef8acb63-8389-47d6-aad8-c2976eac3665/amazonlinux/
合計 0
lrwxrwxrwx 1 root root 165  216 14:55 0.log -> /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7-json.log
lrwxrwxrwx 1 root root 165  216 15:01 1.log -> /var/lib/docker/containers/89f425cce426a87fad882cfc5f091b3f87dca4942bd5c9f9fbde69fc217ea476/89f425cce426a87fad882cfc5f091b3f87dca4942bd5c9f9fbde69fc217ea476-json.log

旧コンテナ、新コンテナともにログが存在している事が確認できました。/var/log/pods の方は 0.log が旧コンテナにリンクし、1.log が新コンテナにリンクしてます。

3.3.3. Pod 削除

上記の状態で Pod を削除してみます。

$ kubectl delete pod deployment-6bb985c8c9-p7b5w
pod "deployment-6bb985c8c9-p7b5w" deleted
@Node $  ls -lh /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/
ls: /var/lib/docker/containers/057fc611f5c416c77a00cfb02e24714b52e75af0c14c42ed11c58d42ca115ea7/ にアクセスできません: No such file or directory
@Node $  ls -lh /var/lib/docker/containers/89f425cce426a87fad882cfc5f091b3f87dca4942bd5c9f9fbde69fc217ea476/
ls: /var/lib/docker/containers/89f425cce426a87fad882cfc5f091b3f87dca4942bd5c9f9fbde69fc217ea476/ にアクセスできません: No such file or directory
@Node $  ls -lh /var/log/pods/default_deployment-6bb985c8c9-p7b5w_ef8acb63-8389-47d6-aad8-c2976eac3665/amazonlinux/
合計 0

Pod を削除するとログも全て消える事が確認できました。

3.4. 設定変更

最後に max-size/max-file の変更方法です。

Node 上の /etc/docker/daemon.json を修正して docker デーモンを再起動する事で設定変更が可能です。 Node 再作成時にも同様に設定されるように、カスタム起動テンプレートかカスタム AMI を使用する必要があります。

例えば、max-size100mmax-file5 にしたい場合は、カスタム起動テンプレートのユーザデータに以下のようなコードを追加する事で設定変更が可能です。

sed -i -e 's/"max-size".*/"max-size": "100m",/g' /etc/docker/daemon.json
sed -i -e 's/"max-file".*/"max-file": "5"/g' /etc/docker/daemon.json
systemctl restart docker

個人的には AWS マネージドという事は AWS が良かれと思って設計した値なのでユーザ側がカスタマイズする必要は無いと思っています。

4. まとめ

コンテナの標準出力/標準エラー出力ログまわりについて整理してみました。EKS 等のマネージドサービスの場合は特別な要件がない限り設定値は変更しなくて良いと思います。

5. 参考

Logging Architecture | Kubernetes

ロギング・ドライバの設定 — Docker-docs-ja 1.9.0b ドキュメント

Amazon EKS ワーカーノードを設定して特定のディスク使用率でイメージキャッシュをクリーンアップする

起動テンプレートのサポート - Amazon EKS

EKS Cluster のバージョンアップ

Kubernetes と EKS のバージョンの整理と、EKS Cluster のバージョンアップ手順について整理しました。

1. Kubernetes のバージョン

まずは Kubernetes のバージョンについて整理します。

  • バージョンは x.y.z 形式で表現され、x はメジャーバージョン、y はマイナーバージョン、z はパッチバージョンを指す
  • 最新 3 つのマイナーバージョンのみサポート
    • 2022/02/08 時点では 1.23,1.22,1.21 のみサポート
  • 4 ヶ月に一回の頻度で新しいマイナーバージョンがリリースされる
  • だいたい月に一回の頻度で新しいパッチバージョンがリリースされる
  • 各マイナーバージョンのサポート期間は一年(1.18 以前は 9 ヶ月)
    • 一年経つと 2 ヶ月間のメンテナンスモードに入る
    • セキュリティや重大なコンポーネントの問題が発生した際はメンテナンスモード中もパッチがリリースされる
    • メンテナンスモードが終了すると EOL となり、パッチはリリースされなくなる
    • メンテナンスモードと EOL は毎月 28 日としている(全ての月にある日のため)
  • 直近のマイナーバージョンリリース
マイナーバージョン リリース日 メンテナンスモードに入る日 EOL
1.24 2022/4 予定
1.23 2021/12/7 2022/12/28 2023/2/28
1.22 2021/8/4 2022/8/28 2022/10/28
1.21 2021/4/8 2022/4/28 2022/6/28
1.20 2020/12/8 2021/12/28 2022/2/28

理想はタイムリーに 4 ヶ月に一回、最低でも年に一回は Kubernetes Cluster のバージョンアップが必要となります。

2. EKS のバージョン

つづいて EKS のバージョンについて整理します。

  • 2022/02/08 時点でサポートされている Kubernetes バージョンは 1.21.2,1.20.7,1.19.8,1.18.16
  • Kubernetes バージョンは、Amazon EKS で最初に利用可能になってから 14 か月間は完全にサポートされる
    • Kubernetes 側でサポートを終了していても EKS にバックポートされる
  • 特定のマイナーバージョンのサポート終了日については、サポート終了日の最低 60 日前に AWS Personal Health Dashboard に通知される
  • サポート終了日の挙動
    • 対象バージョンで新しいクラスターを作成できなくなる
    • 既存のコントロールプレーン(Master)は、サポートされている最も古いバージョンに自動的に更新される(タイミングは指定できない)
    • コントロールプレーンの自動更新後は、クラスターアドオンとデータプレーン(Node)を手動で更新する必要がある
  • 少なくとも 4 つの Kubernetes バージョンをサポートするよう努めている
  • 直近のリリース
Kubernetes バージョン Kubernetes リリース日 EKS リリース日 EKS のサポート終了日
1.23 2021/12/7 未定 未定
1.22 2021/8/4 2022/3 予定 2023/5
1.21 2021/4/8 2021/7/19 2023/2
1.20 2020/12/8 2021/5/18 2022/7
1.19 2020/8/26 2021/2/16 2022/4
1.18 2020/3/23 2020/10/13 2022/2/18
  • 特別な要件がない限り最新バージョンで Cluster を作成する事を推奨
  • 新しいバージョンがリリースされたら速やかに Cluster をバージョンアップする事を推奨
  • 実績としては、Kubernetes の新しいマイナーバージョンがリリースされて EKS がそのバージョンをサポートするまでに半年ぐらいかかっている
  • バージョンアップについて
    • Master のバージョンアップ中にダウンタイムは発生しない
    • Master のバージョンアップ後、ロールバックはできない
    • Master のバージョンアップ後は、EKS アドオン(VPC CNI、CoreDNS、および kube-proxy アドオン)と Node を手動でバージョンアップする必要がある
    • バージョンアップ前の Master と Node の Kubernetes バージョンは同じにする必要がある
    • Cluster 作成時に指定したサブネット内に、2~3 の空き IP アドレスが必要
    • 一回にバージョンアップできるマイナーバージョンは一つのみ(例えば 1.20 から 1.22 に上げたい場合は、最初に 1.21 に更新した後に 1.22 に更新する必要がある)
    • バージョンアップするとカスタム設定が上書きされる場合がある

3. EKS Cluster のバージョンアップ

バージョンアップ方法としては以下 3 パターンがあります。

# 方法 pros cons
Master/NodeGroup 共に In Place ・公式ドキュメントに記載の方法
・作業がシンプルで簡単
・EKS アドオンの更新が必要
・NodeGroup をロールバックできない
・更新中は見守るしかできない(EKS が自動で更新するため Pod の停止/起動タイミングをコントロールできない)
Master は In Place、NodeGroup は Blue/Green ・NodeGroup をロールバックできる
・Pod の停止/起動タイミングをコントロールできる
・EKS アドオンの更新が必要
・①より作業が多い
Cluster を Blue/Green ・Master/NodeGroup 共にロールバックできる
・EKS アドオンの更新が不要
・作業が複雑になる
 ・CI/CD で参照しているクラスタエンドポイントの移行
 ・StatefulSet 等のステートフルなデータの移行
 ・アプリのエンドポイント切り替えの考慮等

①より②の方が安心、③は面倒、という事で今回は②の方法で検証します。バージョンは 1.20 から 1.21 に上げます。

3.1. EKS Cluster 作成

まずは 1.20 バージョンの EKS Cluster を作成します。

eksctl コマンドで EKS Cluster を作成する - YasuBlog の記事で作成した yaml を以下のように一部変更して Cluster を作成し、EKS アドオンをインストールします。

---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: ekstest
  region: ap-northeast-1
  version: "1.20"

vpc:
  id: "vpc-063b58ff16344acfd"
  subnets:
    public:
      ap-northeast-1a:
          id: "subnet-06324dcadf5706acb"
      ap-northeast-1c:
          id: "subnet-048cad38a6c49de67"
      ap-northeast-1d:
          id: "subnet-0bb85370bb4b4528d"

managedNodeGroups:
  - name: managed-ng-20
    instanceType: t3.medium
    desiredCapacity: 3
    volumeSize: 30
    availabilityZones: ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
    maxPodsPerNode: 20
    ssh:
      allow: true
      publicKeyName: ekstest

EKS アドオンインストール後のバージョンは以下です。

$ eksctl get addon --cluster ekstest
NAME        VERSION         STATUS  ISSUES  IAMROLE                                             UPDATE AVAILABLE
coredns     v1.8.3-eksbuild.1 ACTIVE  0
kube-proxy  v1.20.7-eksbuild.1    ACTIVE  0
vpc-cni     v1.10.1-eksbuild.1    ACTIVE  0  arn:aws:iam::544880603668:role/eksctl-ekstest-addon-iamserviceaccount-kube-Role1-A4OVEJ0N035E

EKS アドオンのバージョンは Kubernetes バージョンごとに異なります。Kubernetes をバージョンアップすると手動でアドオンをバージョンアップする必要があり、今回のケースだと CoreDNS と kube-proxy が手動更新対象となります。

EKS アドオン Kubernetes 1.20 Kubernetes 1.21
VPC CNI 1.10.1-eksbuild.1 1.10.1-eksbuild.1
CoreDNS 1.8.3 1.8.4
kube-proxy 1.20.4-eksbuild.2 1.21.2-eksbuild.2

3.2. ワークロード作成

本番環境で何も起動していない Cluster をバージョンアップするケースはないと思うので、検証用に適当な Deployment と StatefulSet を起動します。

deployment 用 yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"

statefulset 用 yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: statefulset
spec:
  serviceName: statefulset-a
  replicas: 3
  selector:
    matchLabels:
      app: stateful-app
  template:
    metadata:
      labels:
        app: stateful-app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"
        volumeMounts:
        - name: hoge
          mountPath: /hoge
  volumeClaimTemplates:
  - metadata:
      name: hoge
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 10G

それぞれ起動します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/deployment created
$ kubectl apply -f test-statefulset.yaml
statefulset.apps/statefulset created
$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
deployment-6bb985c8c9-2nxzb   1/1     Running   0          94s   10.0.102.164   ip-10-0-102-121.ap-northeast-1.compute.internal   <none>           <none>
deployment-6bb985c8c9-m8kvw   1/1     Running   0          94s   10.0.103.40    ip-10-0-103-198.ap-northeast-1.compute.internal   <none>           <none>
deployment-6bb985c8c9-x6nhz   1/1     Running   0          94s   10.0.101.46    ip-10-0-101-193.ap-northeast-1.compute.internal   <none>           <none>
statefulset-0                 1/1     Running   0          56s   10.0.101.77    ip-10-0-101-193.ap-northeast-1.compute.internal   <none>           <none>
statefulset-1                 1/1     Running   0          29s   10.0.102.72    ip-10-0-102-121.ap-northeast-1.compute.internal   <none>           <none>
statefulset-2                 1/1     Running   0          15s   10.0.103.217   ip-10-0-103-198.ap-northeast-1.compute.internal   <none>           <none>

以下のような構成が作成できました。

f:id:dunkshoot:20220213214818p:plain

3.3. バージョンアップ

作成した 1.20 の Cluster を 1.21 にバージョンアップします。

3.3.1. バージョン確認

まずは Master と Node の Kubernetes バージョンが揃っていることを確認します。ズレている場合は Node のバージョンをあげて Master に合わせる必要があります。

# Master のバージョン確認
$ kubectl version --short
Client Version: v1.21.3
Server Version: v1.20.11-eks-f17b81
# Node のバージョン確認
$ kubectl get nodes
NAME                                              STATUS   ROLES    AGE     VERSION
ip-10-0-101-193.ap-northeast-1.compute.internal   Ready    <none>   48m   v1.20.11-eks-f17b81
ip-10-0-102-121.ap-northeast-1.compute.internal   Ready    <none>   48m   v1.20.11-eks-f17b81
ip-10-0-103-198.ap-northeast-1.compute.internal   Ready    <none>   48m   v1.20.11-eks-f17b81

3.3.2. ポッドセキュリティポリシーの確認

つづいて、Pod のセキュリティポリシーを確認します。ここはエラーが出なければ OK のようです。

$ kubectl get psp eks.privileged
NAME             PRIV   CAPS   SELINUX    RUNASUSER   FSGROUP    SUPGROUP   READONLYROOTFS   VOLUMES
eks.privileged   true   *      RunAsAny   RunAsAny    RunAsAny   RunAsAny   false            *

3.3.3. Master のバージョンアップ

EKS は、eksctl, マネジメントコンソール、AWS CLI の 3 パターンの方法でバージョンアップできます。今回は eksctl を使用します。

まずは eksctl のバージョンを確認します。0.77.0 以降が必要です。

$ eksctl version
0.71.0
# 低いためバージョンアップ
$ brew upgrade eksctl && brew link --overwrite eksctl
$ eksctl version
0.82.0

upgrade cluster コマンドを実行します。Master が現在のバージョンより 1 つ後のマイナーバージョンに更新されます。

$ eksctl upgrade cluster --name ekstest --approve
2022-02-13 17:30:35 []  eksctl version 0.82.0
2022-02-13 17:30:35 []  using region ap-northeast-1
2022-02-13 17:30:36 []  will upgrade cluster "ekstest" control plane from current version "1.20" to "1.21"
2022-02-13 17:30:38 []  waiting for requested "VersionUpdate" in cluster "ekstest" to succeed
2022-02-13 17:30:55 []  waiting for requested "VersionUpdate" in cluster "ekstest" to succeed

~省略~

2022-02-13 17:58:24 []  waiting for requested "VersionUpdate" in cluster "ekstest" to succeed
2022-02-13 17:58:43 []  waiting for requested "VersionUpdate" in cluster "ekstest" to succeed
2022-02-13 17:58:44 []  cluster "ekstest" control plane has been upgraded to version "1.21"
2022-02-13 17:58:44 []  you will need to follow the upgrade procedure for all of nodegroups and add-ons
2022-02-13 17:58:45 []  re-building cluster stack "eksctl-ekstest-cluster"
2022-02-13 17:58:45 []  all resources in cluster stack "eksctl-ekstest-cluster" are up-to-date
2022-02-13 17:58:45 []  checking security group configuration for all nodegroups
2022-02-13 17:58:45 []  all nodegroups have up-to-date cloudformation templates
# Master のバージョン確認
$ kubectl version --short
Client Version: v1.21.3
Server Version: v1.21.5-eks-bc4871b

Master を 1.20 から 1.21 に更新するのに 30 分ほどかかりました。

Master をバージョンアップすると EKS アドオンの UPDATE AVAILABLE 列に新しいバージョンが表示されます。今回の場合だと CoreDNS と kube-proxy の新しいバージョンが利用可能なことがわかります。

$ eksctl get addon --cluster ekstest
2022-02-13 18:05:12 []  eksctl version 0.82.0
2022-02-13 18:05:12 []  using region ap-northeast-1
2022-02-13 18:05:12 []  Kubernetes version "1.21" in use by cluster "ekstest"
2022-02-13 18:05:12 []  getting all addons
2022-02-13 18:05:14 []  to see issues for an addon run `eksctl get addon --name <addon-name> --cluster <cluster-name>`
NAME        VERSION         STATUS  ISSUES  IAMROLE                                             UPDATE AVAILABLE
coredns     v1.8.3-eksbuild.1 ACTIVE  0                                                  v1.8.4-eksbuild.1
kube-proxy  v1.20.7-eksbuild.1    ACTIVE  0                                                  v1.21.2-eksbuild.2
vpc-cni     v1.10.1-eksbuild.1    ACTIVE  0  arn:aws:iam::544880603668:role/eksctl-ekstest-addon-iamserviceaccount-kube-Role1-A4OVEJ0N035E

3.3.4. 新 NodeGroup の作成

つづいて Node 側のバージョンアップです。流れとしては新しいバージョンの NodeGroup を作成して Pod を移行した後に 旧 NodeGroup を削除します。

まずは既存の NodeGroup を確認します。

$ eksctl get nodegroup --cluster ekstest
2022-02-13 17:32:47 []  eksctl version 0.82.0
2022-02-13 17:32:47 []  using region ap-northeast-1
CLUSTER NODEGROUP   STATUS  CREATED         MIN SIZE    MAX SIZE    DESIRED CAPACITY    INSTANCE TYPE   IMAGE ID    ASG NAME
ekstest managed-ng-20   ACTIVE  2022-02-13T07:39:38Z    3      3      3          t3.medium   AL2_x86_64  eks-48bf7902-e8cc-5e14-42af-54c664189abe

つづいて新しいバージョン(1.21)の NodeGroup を作成します。設定は既存(1.20)の NodeGroup と同じにします。create nodegroup コマンドにバージョンを指定するオプションはなく、Master と同じバージョンの NodeGroup が作成されるようです。

$ eksctl create nodegroup --cluster ekstest --region ap-northeast-1 --name managed-ng-21 --node-type t3.medium --nodes 3 --nodes-min 3 --nodes-max 3 --ssh-access --ssh-public-key ekstest
2022-02-13 18:10:07 []  eksctl version 0.82.0
2022-02-13 18:10:07 []  using region ap-northeast-1
2022-02-13 18:10:07 []  will use version 1.21 for new nodegroup(s) based on control plane version
2022-02-13 18:10:08 []  nodegroup "managed-ng-21" will use "" [AmazonLinux2/1.21]
2022-02-13 18:10:08 []  using EC2 key pair %!q(*string=<nil>)
2022-02-13 18:10:09 []  1 existing nodegroup(s) (managed-ng-20) will be excluded
2022-02-13 18:10:09 []  1 nodegroup (managed-ng-21) was included (based on the include/exclude rules)
2022-02-13 18:10:09 []  will create a CloudFormation stack for each of 1 managed nodegroups in cluster "ekstest"
2022-02-13 18:10:10 []
2 sequential tasks: { fix cluster compatibility, 1 task: { 1 task: { create managed nodegroup "managed-ng-21" } }
}
2022-02-13 18:10:10 []  checking cluster stack for missing resources
2022-02-13 18:10:10 []  cluster stack has all required resources
2022-02-13 18:10:11 []  building managed nodegroup stack "eksctl-ekstest-nodegroup-managed-ng-21"
2022-02-13 18:10:11 []  deploying stack "eksctl-ekstest-nodegroup-managed-ng-21"
2022-02-13 18:10:11 []  waiting for CloudFormation stack "eksctl-ekstest-nodegroup-managed-ng-21"
2022-02-13 18:10:27 []  waiting for CloudFormation stack "eksctl-ekstest-nodegroup-managed-ng-21"

~省略~

2022-02-13 18:13:46 []  waiting for CloudFormation stack "eksctl-ekstest-nodegroup-managed-ng-21"
2022-02-13 18:14:04 []  waiting for CloudFormation stack "eksctl-ekstest-nodegroup-managed-ng-21"
2022-02-13 18:14:04 []  no tasks
2022-02-13 18:14:04 []  created 0 nodegroup(s) in cluster "ekstest"
2022-02-13 18:14:05 []  nodegroup "managed-ng-21" has 3 node(s)
2022-02-13 18:14:05 []  node "ip-10-0-101-208.ap-northeast-1.compute.internal" is ready
2022-02-13 18:14:05 []  node "ip-10-0-102-86.ap-northeast-1.compute.internal" is ready
2022-02-13 18:14:05 []  node "ip-10-0-103-214.ap-northeast-1.compute.internal" is ready
2022-02-13 18:14:05 []  waiting for at least 3 node(s) to become ready in "managed-ng-21"
2022-02-13 18:14:05 []  nodegroup "managed-ng-21" has 3 node(s)
2022-02-13 18:14:05 []  node "ip-10-0-101-208.ap-northeast-1.compute.internal" is ready
2022-02-13 18:14:05 []  node "ip-10-0-102-86.ap-northeast-1.compute.internal" is ready
2022-02-13 18:14:05 []  node "ip-10-0-103-214.ap-northeast-1.compute.internal" is ready
2022-02-13 18:14:05 []  created 1 managed nodegroup(s) in cluster "ekstest"
2022-02-13 18:14:05 []  checking security group configuration for all nodegroups
2022-02-13 18:14:05 []  all nodegroups have up-to-date cloudformation templates
# 作成されている事を確認
$ eksctl get nodegroup --cluster ekstest
2022-02-13 18:14:20 []  eksctl version 0.82.0
2022-02-13 18:14:20 []  using region ap-northeast-1
CLUSTER NODEGROUP   STATUS  CREATED         MIN SIZE    MAX SIZE    DESIRED CAPACITY    INSTANCE TYPE   IMAGE ID    ASG NAME
ekstest managed-ng-20   ACTIVE  2022-02-13T07:39:38Z    3      3      3          t3.medium   AL2_x86_64  eks-48bf7902-e8cc-5e14-42af-54c664189abe
ekstest managed-ng-21   ACTIVE  2022-02-13T09:11:10Z    3      3      3          t3.medium   AL2_x86_64  eks-managed-ng-21-6cbf792c-cfa3-8215-20ef-7eae8033569f
# Node のバージョンを確認
$ kubectl get nodes
NAME                                              STATUS   ROLES    AGE   VERSION
ip-10-0-101-193.ap-northeast-1.compute.internal   Ready    <none>   93m   v1.20.11-eks-f17b81
ip-10-0-101-208.ap-northeast-1.compute.internal   Ready    <none>   85s   v1.21.5-eks-9017834
ip-10-0-102-121.ap-northeast-1.compute.internal   Ready    <none>   93m   v1.20.11-eks-f17b81
ip-10-0-102-86.ap-northeast-1.compute.internal    Ready    <none>   85s   v1.21.5-eks-9017834
ip-10-0-103-198.ap-northeast-1.compute.internal   Ready    <none>   93m   v1.20.11-eks-f17b81
ip-10-0-103-214.ap-northeast-1.compute.internal   Ready    <none>   87s   v1.21.5-eks-9017834

新しい NodeGroup の作成に 4 分ほどかかりました。以下の構成が作成できました。

f:id:dunkshoot:20220213215121p:plain

3.3.5. Pod の移行

旧 Node を drain して Pod を新 Node で起動させます。NodeGroup をまるごと drain する事も可能ですがちょっと怖いので Node を 1 台ずつ drain します。

※ drain は Pod に SIGTERM を送るため、SIGTERM を受けて正常に終了するように作られているアプリケーションが前提となります

# Node の確認
$ kubectl get nodes
NAME                                              STATUS   ROLES    AGE   VERSION
ip-10-0-101-193.ap-northeast-1.compute.internal   Ready    <none>   93m   v1.20.11-eks-f17b81
ip-10-0-101-208.ap-northeast-1.compute.internal   Ready    <none>   85s   v1.21.5-eks-9017834
ip-10-0-102-121.ap-northeast-1.compute.internal   Ready    <none>   93m   v1.20.11-eks-f17b81
ip-10-0-102-86.ap-northeast-1.compute.internal    Ready    <none>   85s   v1.21.5-eks-9017834
ip-10-0-103-198.ap-northeast-1.compute.internal   Ready    <none>   93m   v1.20.11-eks-f17b81
ip-10-0-103-214.ap-northeast-1.compute.internal   Ready    <none>   87s   v1.21.5-eks-9017834
# Pod の確認(全て 1.20 の Node で起動)
$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
deployment-6bb985c8c9-2nxzb   1/1     Running   0          25m   10.0.102.164   ip-10-0-102-121.ap-northeast-1.compute.internal   <none>           <none>
deployment-6bb985c8c9-m8kvw   1/1     Running   0          25m   10.0.103.40    ip-10-0-103-198.ap-northeast-1.compute.internal   <none>           <none>
deployment-6bb985c8c9-x6nhz   1/1     Running   0          25m   10.0.101.46    ip-10-0-101-193.ap-northeast-1.compute.internal   <none>           <none>
statefulset-0                 1/1     Running   0          25m   10.0.101.77    ip-10-0-101-193.ap-northeast-1.compute.internal   <none>           <none>
statefulset-1                 1/1     Running   0          24m   10.0.102.72    ip-10-0-102-121.ap-northeast-1.compute.internal   <none>           <none>
statefulset-2                 1/1     Running   0          24m   10.0.103.217   ip-10-0-103-198.ap-northeast-1.compute.internal   <none>           <none>
# 1.20 の Node を一台ずつ drain
$ kubectl drain ip-10-0-102-121.ap-northeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data
node/ip-10-0-102-121.ap-northeast-1.compute.internal cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/aws-node-pgfsx, kube-system/kube-proxy-mfxsj
evicting pod default/statefulset-1
evicting pod default/deployment-6bb985c8c9-2nxzb
pod/deployment-6bb985c8c9-2nxzb evicted
pod/statefulset-1 evicted
node/ip-10-0-102-121.ap-northeast-1.compute.internal evicted
$ kubectl drain ip-10-0-101-193.ap-northeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data
$ kubectl drain ip-10-0-103-198.ap-northeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data
# Node の確認
$ kubectl get nodes
NAME                                              STATUS                     ROLES    AGE    VERSION
ip-10-0-101-193.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   105m   v1.20.11-eks-f17b81
ip-10-0-101-208.ap-northeast-1.compute.internal   Ready                      <none>   13m    v1.21.5-eks-9017834
ip-10-0-102-121.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   105m   v1.20.11-eks-f17b81
ip-10-0-102-86.ap-northeast-1.compute.internal    Ready                      <none>   13m    v1.21.5-eks-9017834
ip-10-0-103-198.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   105m   v1.20.11-eks-f17b81
ip-10-0-103-214.ap-northeast-1.compute.internal   Ready                      <none>   14m    v1.21.5-eks-9017834
# Pod の確認
$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE     IP             NODE                                              NOMINATED NODE   READINESS GATES
deployment-6bb985c8c9-9sp8k   1/1     Running   0          66s     10.0.103.177   ip-10-0-103-214.ap-northeast-1.compute.internal   <none>           <none>
deployment-6bb985c8c9-ccbms   1/1     Running   0          2m34s   10.0.102.231   ip-10-0-102-86.ap-northeast-1.compute.internal    <none>           <none>
deployment-6bb985c8c9-d656x   1/1     Running   0          3m46s   10.0.101.182   ip-10-0-101-208.ap-northeast-1.compute.internal   <none>           <none>
statefulset-0                 1/1     Running   0          3m14s   10.0.101.12    ip-10-0-101-208.ap-northeast-1.compute.internal   <none>           <none>
statefulset-1                 1/1     Running   0          118s    10.0.102.206   ip-10-0-102-86.ap-northeast-1.compute.internal    <none>           <none>
statefulset-2                 1/1     Running   0          30s     10.0.103.105   ip-10-0-103-214.ap-northeast-1.compute.internal   <none>           <none>

全ての Pod が新 Node で起動しました。

3.3.6. 旧 NodeGroup の削除

旧 Node 上で動く Pod がなくなったので旧 NodeGroup を削除します。

$ eksctl delete nodegroup --cluster ekstest --region ap-northeast-1 --name managed-ng-20
2022-02-13 18:28:34 []  eksctl version 0.82.0
2022-02-13 18:28:34 []  using region ap-northeast-1
2022-02-13 18:28:34 []  1 nodegroup (managed-ng-20) was included (based on the include/exclude rules)
2022-02-13 18:28:34 []  will drain 1 nodegroup(s) in cluster "ekstest"
2022-02-13 18:28:34 [!]  ignoring DaemonSet-managed Pods: kube-system/aws-node-plgkj, kube-system/kube-proxy-x9b6h
2022-02-13 18:28:34 [!]  ignoring DaemonSet-managed Pods: kube-system/aws-node-pgfsx, kube-system/kube-proxy-mfxsj
2022-02-13 18:28:34 [!]  ignoring DaemonSet-managed Pods: kube-system/aws-node-qswdg, kube-system/kube-proxy-fzk28
2022-02-13 18:28:34 []  drained all nodes: [ip-10-0-101-193.ap-northeast-1.compute.internal ip-10-0-102-121.ap-northeast-1.compute.internal ip-10-0-103-198.ap-northeast-1.compute.internal]
2022-02-13 18:28:34 []  will delete 1 nodegroups from cluster "ekstest"
2022-02-13 18:28:35 []  1 task: { 1 task: { delete nodegroup "managed-ng-20" [async] } }
2022-02-13 18:28:35 []  will delete stack "eksctl-ekstest-nodegroup-managed-ng-20"
2022-02-13 18:28:35 []  will delete 0 nodegroups from auth ConfigMap in cluster "ekstest"
2022-02-13 18:28:35 []  deleted 1 nodegroup(s) from cluster "ekstest"
# 削除されている事を確認
$ eksctl get nodegroup --cluster ekstest
2022-02-13 18:32:50 []  eksctl version 0.82.0
2022-02-13 18:32:50 []  using region ap-northeast-1
CLUSTER NODEGROUP   STATUS  CREATED         MIN SIZE    MAX SIZE    DESIRED CAPACITY    INSTANCE TYPE   IMAGE ID    ASG NAME
ekstest managed-ng-21   ACTIVE  2022-02-13T09:11:10Z    3      3      3          t3.medium   AL2_x86_64  eks-managed-ng-21-6cbf792c-cfa3-8215-20ef-7eae8033569f
# Node のバージョンを確認
$ kubectl get nodes
NAME                                              STATUS   ROLES    AGE   VERSION
ip-10-0-101-208.ap-northeast-1.compute.internal   Ready    <none>   19m   v1.21.5-eks-9017834
ip-10-0-102-86.ap-northeast-1.compute.internal    Ready    <none>   19m   v1.21.5-eks-9017834
ip-10-0-103-214.ap-northeast-1.compute.internal   Ready    <none>   19m   v1.21.5-eks-9017834

Master と Node のバージョンが 1.21 になりました。

f:id:dunkshoot:20220213215332p:plain

3.2.7. EKS アドオンの更新

最後に EKS アドオンをバージョンアップします。

# バージョン確認
$ eksctl get addon --cluster ekstest
2022-02-13 18:34:06 []  eksctl version 0.82.0
2022-02-13 18:34:06 []  using region ap-northeast-1
2022-02-13 18:34:06 []  Kubernetes version "1.21" in use by cluster "ekstest"
2022-02-13 18:34:06 []  getting all addons
2022-02-13 18:34:08 []  to see issues for an addon run `eksctl get addon --name <addon-name> --cluster <cluster-name>`
NAME        VERSION         STATUS  ISSUES  IAMROLE                                             UPDATE AVAILABLE
coredns     v1.8.3-eksbuild.1 ACTIVE  0                                                  v1.8.4-eksbuild.1
kube-proxy  v1.20.7-eksbuild.1    ACTIVE  0                                                  v1.21.2-eksbuild.2
vpc-cni     v1.10.1-eksbuild.1    ACTIVE  0  arn:aws:iam::544880603668:role/eksctl-ekstest-addon-iamserviceaccount-kube-Role1-A4OVEJ0N035E
# CoreDNS バージョンアップ
$ eksctl update addon --name coredns --version v1.8.4-eksbuild.1 --cluster ekstest --force
2022-02-13 18:34:51 []  Kubernetes version "1.21" in use by cluster "ekstest"
2022-02-13 18:34:52 []  new version provided v1.8.4-eksbuild.1
2022-02-13 18:34:52 []  updating addon
# kube-proxy バージョンアップ
$ eksctl update addon --name kube-proxy --version v1.21.2-eksbuild.2 --cluster ekstest --force
2022-02-13 18:35:26 []  Kubernetes version "1.21" in use by cluster "ekstest"
2022-02-13 18:35:27 []  new version provided v1.21.2-eksbuild.2
2022-02-13 18:35:27 []  updating addon
# バージョン確認
$ eksctl get addon --cluster ekstest
2022-02-13 18:36:14 []  eksctl version 0.82.0
2022-02-13 18:36:14 []  using region ap-northeast-1
2022-02-13 18:36:14 []  Kubernetes version "1.21" in use by cluster "ekstest"
2022-02-13 18:36:14 []  getting all addons
2022-02-13 18:36:16 []  to see issues for an addon run `eksctl get addon --name <addon-name> --cluster <cluster-name>`
NAME        VERSION         STATUS  ISSUES  IAMROLE                                             UPDATE AVAILABLE
coredns     v1.8.4-eksbuild.1 ACTIVE  0
kube-proxy  v1.21.2-eksbuild.2    ACTIVE  0
vpc-cni     v1.10.1-eksbuild.1    ACTIVE  0  arn:aws:iam::544880603668:role/eksctl-ekstest-addon-iamserviceaccount-kube-Role1-A4OVEJ0N035E

4. まとめ

本番環境を想定するとバージョンアップ方法は Master は In Place, Node は Blue/Green が安全かと思います。 今回は検証のため単純な Deployment と StatefulSet が起動している Cluster で検証したので簡単でしたが、実際の本番環境で実施する場合は結構怖い作業ですね。何かにハマるケースもあるかと思います。

5. 参考

Kubernetesバージョンとバージョンスキューサポートポリシー | Kubernetes

website/patch-releases.md at main · kubernetes/website · GitHub

Patch Releases | Kubernetes

Releases | Kubernetes

Amazon EKS Kubernetes versions - Amazon EKS

クラスターの更新 - Amazon EKS

マネージド型ノードグループの作成 - Amazon EKS

マネージド型ノードグループの更新 - Amazon EKS

マネージド型ノードグループの削除 - Amazon EKS

eksctl のインストール - Amazon EKS

Kubernetes の Eviction

Kubernetes の Eviction について整理し、実際の挙動を確認しました。

1. Eviction

Eviction とは、Node のリソースを確保するために kubelet が Pod を終了する事です。

kubelet は定期的に Node のリソースを監視しており、空きリソースが閾値(Eviction Threshold)を下回るとPod を Evict します。

f:id:dunkshoot:20220202231210p:plain

具体的には Eviction Signal と Eviction Threshold を比較して Pod を Evict するかどうかを判断します。

Eviction Signal とは、ある特定時点での特定リソースの状態です。

Eviction Signal 説明
memory.available 空きメモリ
nodefs.available (ローカルディスク、emptyDir,ログストレージ等に使われる)メインのファイルシステムの空き容量
nodefs.inodesFree (ローカルディスク、emptyDir,ログストレージ等に使われる)メインのファイルシステムの空き inode 量
imagefs.available (コンテナランタイムがコンテナイメージとコンテナ書き込み可能レイヤーを格納するために使用する)オプションのファイルシステムの空き容量
imagefs.inodesFree (コンテナランタイムがコンテナイメージとコンテナ書き込み可能レイヤーを格納するために使用する)オプションのファイルシステムの空き inode 量
pid.available 空き pid

なお、Eviction Signal の監視間隔は kubelet フラグ --housekeeping-interval で設定可能です。デフォルトでは 10 秒です。

Eviction Threshold には soft と hard があります。

Eviction Threshold Eviction Signal が Eviction Threshold を下回った際の挙動 デフォルト値
eviction-soft ・Pod に SIGTERM が送られる
・SIGTERM 後、Pod に設定された terminationGracePeriodSeconds または kubelete に設定された --eviction-max-pod-grace-period の短い方の時間を待った後に SIGKILL が送られる
・Evict 対象になってから Evict を実行するまでの時間を--eviction-soft-grace-period で設定できる
 例えば eviction-soft-grace-period=memory.available=1m30s を設定した場合、Pod が Evict 対象になってから 1分30秒後に Pod に SIGTERM が送られる
なし
eviction-hard ・Pod に SIGKILL が送られる memory.available<100Mi
nodefs.available<10%
imagefs.available<15%
nodefs.inodesFree<5%

続いて、どのように Evict 対象の Pod を選定しているかという点ですが、以下の基準で決まります。

  1. requests を超えてリソースを使用しているか
  2. Pod に設定されている Pod Priority
  3. requests を超えている量がより多いもの

結果的には、次の順序で Pod をランク付けして Evict します。簡単に言うと requests/limits の両方とも設定していない Pod が最初に Evict されます。

  1. requests を超えてリソースを使用している BestEffort または Burstable の Pod
  2. requests を超えていない Guaranteed または Burstable の Pod

※ requests/BestEffort/Burstable/Guaranteed については Kubernetes のリソース制限とその挙動確認 - YasuBlog に整理しています

2. Eviction 発生後の挙動

Pod が Evict されると Node の Condition が変化し Taint が付与されます。 Taint とは直訳すると「汚れ」の意味で、Taint が付与された Node には Pod をスケジュールさせないといった使い方ができます。

Node Condition と Eviction Signal のマッピングは以下です。Eviction が発生していない状態ではそれぞれの Node Condition は False です。Eviction が発生すると True に変わります。

Node Condition Eviction Signal 説明
MemoryPressure memory.available Node で使用可能なメモリが Eviction Threshold を満たした
DiskPressure nodefs.available
nodefs.inodesFree
imagefs.available
imagefs.inodesFree
Node の root or image ファイルシステム の 使用可能ディスク量 or inode が Eviction Threshold を満たした
PIDPressure pid.available Node で使用可能なプロセス ID が Eviction Threshold を満たした

なお、Node Condition の更新間隔は kubelet フラグ --node-status-update-frequency で設定可能です。デフォルトでは 10 秒です。

Node Condition が True になる事により付与される Taint は以下です。

Node Condition Taint
MemoryPressure node.kubernetes.io/memory-pressure:NoSchedule
DiskPressure node.kubernetes.io/disk-pressure:NoSchedule
PIDPressure node.kubernetes.io/pid-pressure:NoSchedule

例えば、Deployment や StatefulSet の場合は Pod が Evict されると新しい Pod が起動してきます。すぐに Pod が起動するとまたすぐに Evict される事になり、Pod 作成 -> Evict が繰り返し発生する事になります。これを防ぐために Node Condition を変更するまでのタイムラグを --eviction-pressure-transition-period フラグで設定可能です。デフォルトでは 5 分です。

3. 検証

3.1. 検証環境構築

eksctl コマンドで EKS Cluster を作成する - YasuBlog で作成した EKS Cluster を使用します。

3.2. EKS の 設定値

まずは eviction-soft/eviction-hard の確認方法です。Node の情報なので KubernetesAPI サーバから取得できます。

kubectl proxy コマンドでプロキシをローカルに起動すると API サーバにアクセスできます。

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

デフォルトでローカルの 8001 ポートでプロキシが起動するので、別ターミナルで http://localhost:8001/api/v1/nodes/<Node 名>/proxy/configz にアクセスします。json なので jq で整形すると見やすいです。

$ kubectl get node
NAME                                              STATUS   ROLES    AGE    VERSION
ip-10-0-101-27.ap-northeast-1.compute.internal    Ready    <none>   19m   v1.21.5-eks-9017834
ip-10-0-102-128.ap-northeast-1.compute.internal   Ready    <none>   20m   v1.21.5-eks-9017834
ip-10-0-103-229.ap-northeast-1.compute.internal   Ready    <none>   19m   v1.21.5-eks-9017834
$ curl -sSL "http://localhost:8001/api/v1/nodes/ip-10-0-101-27.ap-northeast-1.compute.internal/proxy/configz" | jq .
{
  "kubeletconfig": {
    "enableServer": true,
    "syncFrequency": "1m0s",
    "fileCheckFrequency": "20s",
    "httpCheckFrequency": "20s",
    "address": "0.0.0.0",
    "port": 10250,
    "tlsCipherSuites": [
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_128_GCM_SHA256"
    ],
    "serverTLSBootstrap": true,
    "authentication": {
      "x509": {
        "clientCAFile": "/etc/kubernetes/pki/ca.crt"
      },
      "webhook": {
        "enabled": true,
        "cacheTTL": "2m0s"
      },
      "anonymous": {
        "enabled": false
      }
    },
    "authorization": {
      "mode": "Webhook",
      "webhook": {
        "cacheAuthorizedTTL": "5m0s",
        "cacheUnauthorizedTTL": "30s"
      }
    },
    "registryPullQPS": 5,
    "registryBurst": 10,
    "eventRecordQPS": 5,
    "eventBurst": 10,
    "enableDebuggingHandlers": true,
    "healthzPort": 10248,
    "healthzBindAddress": "127.0.0.1",
    "oomScoreAdj": -999,
    "clusterDomain": "cluster.local",
    "clusterDNS": [
      "172.20.0.10"
    ],
    "streamingConnectionIdleTimeout": "4h0m0s",
    "nodeStatusUpdateFrequency": "10s",
    "nodeStatusReportFrequency": "5m0s",
    "nodeLeaseDurationSeconds": 40,
    "imageMinimumGCAge": "2m0s",
    "imageGCHighThresholdPercent": 85,
    "imageGCLowThresholdPercent": 80,
    "volumeStatsAggPeriod": "1m0s",
    "cgroupRoot": "/",
    "cgroupsPerQOS": true,
    "cgroupDriver": "cgroupfs",
    "cpuManagerPolicy": "none",
    "cpuManagerReconcilePeriod": "10s",
    "memoryManagerPolicy": "None",
    "topologyManagerPolicy": "none",
    "topologyManagerScope": "container",
    "runtimeRequestTimeout": "2m0s",
    "hairpinMode": "hairpin-veth",
    "maxPods": 17,
    "podPidsLimit": -1,
    "resolvConf": "/etc/resolv.conf",
    "cpuCFSQuota": true,
    "cpuCFSQuotaPeriod": "100ms",
    "nodeStatusMaxImages": 50,
    "maxOpenFiles": 1000000,
    "contentType": "application/vnd.kubernetes.protobuf",
    "kubeAPIQPS": 5,
    "kubeAPIBurst": 10,
    "serializeImagePulls": false,
    "evictionHard": {
      "memory.available": "100Mi",
      "nodefs.available": "10%",
      "nodefs.inodesFree": "5%"
    },
    "evictionPressureTransitionPeriod": "5m0s",
    "enableControllerAttachDetach": true,
    "protectKernelDefaults": true,
    "makeIPTablesUtilChains": true,
    "iptablesMasqueradeBit": 14,
    "iptablesDropBit": 15,
    "featureGates": {
      "RotateKubeletServerCertificate": true
    },
    "failSwapOn": true,
    "containerLogMaxSize": "10Mi",
    "containerLogMaxFiles": 5,
    "configMapAndSecretChangeDetectionStrategy": "Watch",
    "kubeReserved": {
      "cpu": "70m",
      "ephemeral-storage": "1Gi",
      "memory": "442Mi"
    },
    "enforceNodeAllocatable": [
      "pods"
    ],
    "volumePluginDir": "/usr/libexec/kubernetes/kubelet-plugins/volume/exec/",
    "logging": {
      "format": "text"
    },
    "enableSystemLogHandler": true,
    "shutdownGracePeriod": "0s",
    "shutdownGracePeriodCriticalPods": "0s",
    "enableProfilingHandler": true,
    "enableDebugFlagsHandler": true
  }
}
 

86 行目から 90 行目に eviction-hard が表示されています。eviction-soft は設定されていません。 91 行目から evictionPressureTransitionPeriod がデフォルトの 5 分に設定されていることもわかります。

なお、API サーバではなく Node にログインして設定ファイルから値を確認する事も可能です。

@node$ cat /etc/kubernetes/kubelet/kubelet-config.json
{
  "kind": "KubeletConfiguration",
  "apiVersion": "kubelet.config.k8s.io/v1beta1",
  "address": "0.0.0.0",
  "authentication": {
    "anonymous": {
      "enabled": false
    },
    "webhook": {
      "cacheTTL": "2m0s",
      "enabled": true
    },
    "x509": {
      "clientCAFile": "/etc/kubernetes/pki/ca.crt"
    }
  },
  "authorization": {
    "mode": "Webhook",
    "webhook": {
      "cacheAuthorizedTTL": "5m0s",
      "cacheUnauthorizedTTL": "30s"
    }
  },
  "clusterDomain": "cluster.local",
  "hairpinMode": "hairpin-veth",
  "readOnlyPort": 0,
  "cgroupDriver": "cgroupfs",
  "cgroupRoot": "/",
  "featureGates": {
    "RotateKubeletServerCertificate": true
  },
  "protectKernelDefaults": true,
  "serializeImagePulls": false,
  "serverTLSBootstrap": true,
  "tlsCipherSuites": [
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_128_GCM_SHA256"
  ],
  "maxPods": 20,
  "clusterDNS": [
    "172.20.0.10"
  ],
  "evictionHard": {
    "memory.available": "100Mi",
    "nodefs.available": "10%",
    "nodefs.inodesFree": "5%"
  },
  "kubeReserved": {
    "cpu": "70m",
    "ephemeral-storage": "1Gi",
    "memory": "442Mi"
  }
}

インスタンスタイプ毎の設定値を確認してみました。Node のリソース量に関わらず固定値が設定されているようです。

インスタンスタイプ eviction-soft eviction-hard
t3.small なし memory.available: 100Mi
nodefs.available: 10%
nodefs.inodesFree: 5%
t3.medium なし memory.available: 100Mi
nodefs.available: 10%
nodefs.inodesFree: 5%
m5.large なし memory.available: 100Mi
nodefs.available: 10%
nodefs.inodesFree: 5%
m5.xlarge なし memory.available: 100Mi
nodefs.available: 10%
nodefs.inodesFree: 5%

3.3. 設定変更

AWS マネージドではないセルフマネージド NodeGroup の場合は以下の通り設定できそうです。

Customizing kubelet configuration - eksctl

AWS マネージドの場合は yaml での設定方法は見つかりませんでした。Node 上の設定ファイル(/etc/kubernetes/kubelet/kubelet-config.json や /etc/systemd/system/kubelet.service)を直接修正する事で設定変更が可能です。 Node 再作成時にも同様に設定されるように、カスタム起動テンプレートかカスタム AMI を使用する必要があります。

起動テンプレートのサポート - Amazon EKS

Amazon EKS ワーカーノードを設定して特定のディスク使用率でイメージキャッシュをクリーンアップする

個人的には AWS マネージドという事は AWS が良かれと思って設計した値なのでユーザ側がカスタマイズする必要は無いと思っています。

3.4. Eviction を発生させる

メモリ制限をかけない Pod を起動して空きメモリが eviction-hard の閾値である 100Mi 以下になった際の挙動を確認します。

以下の manifest で deployment を起動します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-a
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-a
  template:
    metadata:
      labels:
        app: app-a
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"

Pod が起動している事を確認します。

$ kubectl get pod -o wide
NAME                            READY   STATUS    RESTARTS   AGE     IP            NODE                                              NOMINATED NODE   READINESS GATES
deployment-a-75979d4477-ssffj   1/1     Running   0          13s     10.0.103.15   ip-10-0-101-27.ap-northeast-1.compute.internal   <none>           <none>

Eviction が発生する前の Node の状態を確認します。

$ kubectl describe node ip-10-0-101-27.ap-northeast-1.compute.internal
Name:               ip-10-0-101-27.ap-northeast-1.compute.internal
Roles:              <none>
Labels:             alpha.eksctl.io/cluster-name=ekstest
                    alpha.eksctl.io/nodegroup-name=managed-ng
                    beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/instance-type=t3.medium
                    beta.kubernetes.io/os=linux
                    eks.amazonaws.com/capacityType=ON_DEMAND
                    eks.amazonaws.com/nodegroup=managed-ng
                    eks.amazonaws.com/nodegroup-image=ami-0b49509d917c6649b
                    eks.amazonaws.com/sourceLaunchTemplateId=lt-0e28d03ab5eb48995
                    eks.amazonaws.com/sourceLaunchTemplateVersion=1
                    failure-domain.beta.kubernetes.io/region=ap-northeast-1
                    failure-domain.beta.kubernetes.io/zone=ap-northeast-1a
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=ip-10-0-101-27.ap-northeast-1.compute.internal
                    kubernetes.io/os=linux
                    node.kubernetes.io/instance-type=t3.medium
                    topology.kubernetes.io/region=ap-northeast-1
                    topology.kubernetes.io/zone=ap-northeast-1a
Annotations:        node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Wed, 02 Feb 2022 22:56:40 +0900
Taints:             <none>
Unschedulable:      false
Lease:
  HolderIdentity:  ip-10-0-101-27.ap-northeast-1.compute.internal
  AcquireTime:     <unset>
  RenewTime:       Wed, 02 Feb 2022 23:34:49 +0900
Conditions:
  Type             Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----             ------  -----------------                 ------------------                ------                       -------
  MemoryPressure   False   Wed, 02 Feb 2022 23:31:12 +0900   Wed, 02 Feb 2022 22:56:38 +0900   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure     False   Wed, 02 Feb 2022 23:31:12 +0900   Wed, 02 Feb 2022 22:56:38 +0900   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure      False   Wed, 02 Feb 2022 23:31:12 +0900   Wed, 02 Feb 2022 22:56:38 +0900   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready            True    Wed, 02 Feb 2022 23:31:12 +0900   Wed, 02 Feb 2022 22:57:11 +0900   KubeletReady                 kubelet is posting ready status
Addresses:
  InternalIP:   10.0.101.27
  ExternalIP:   54.250.92.70
  Hostname:     ip-10-0-101-27.ap-northeast-1.compute.internal
  InternalDNS:  ip-10-0-101-27.ap-northeast-1.compute.internal
Capacity:
  attachable-volumes-aws-ebs:  25
  cpu:                         2
  ephemeral-storage:           31444972Ki
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3967460Ki
  pods:                        17
Allocatable:
  attachable-volumes-aws-ebs:  25
  cpu:                         1930m
  ephemeral-storage:           27905944324
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3412452Ki
  pods:                        17
System Info:
  Machine ID:                 ec21395734297aab934fe1f5ab059326
  System UUID:                ec213957-3429-7aab-934f-e1f5ab059326
  Boot ID:                    ed9c469c-a158-4b4c-a677-8f6688cc53a0
  Kernel Version:             5.4.172-90.336.amzn2.x86_64
  OS Image:                   Amazon Linux 2
  Operating System:           linux
  Architecture:               amd64
  Container Runtime Version:  docker://20.10.7
  Kubelet Version:            v1.21.5-eks-9017834
  Kube-Proxy Version:         v1.21.5-eks-9017834
ProviderID:                   aws:///ap-northeast-1a/i-07d2e46ad433eeb9d
Non-terminated Pods:          (3 in total)
  Namespace                   Name                             CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                             ------------  ----------  ---------------  -------------  ---
  default                     deployment-a-75979d4477-ssffj    0 (0%)        0 (0%)      0 (0%)           0 (0%)         4m30s
  kube-system                 aws-node-b7xfp                   25m (1%)      0 (0%)      0 (0%)           0 (0%)         38m
  kube-system                 kube-proxy-qqzbs                 100m (5%)     0 (0%)      0 (0%)           0 (0%)         38m
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource                    Requests   Limits
  --------                    --------   ------
  cpu                         125m (6%)  0 (0%)
  memory                      0 (0%)     0 (0%)
  ephemeral-storage           0 (0%)     0 (0%)
  hugepages-1Gi               0 (0%)     0 (0%)
  hugepages-2Mi               0 (0%)     0 (0%)
  attachable-volumes-aws-ebs  0          0
Events:
  Type    Reason                   Age                From        Message
  ----    ------                   ----               ----        -------
  Normal  Starting                 38m                kubelet     Starting kubelet.
  Normal  NodeHasSufficientMemory  38m (x2 over 38m)  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasSufficientMemory
  Normal  NodeHasNoDiskPressure    38m (x2 over 38m)  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasNoDiskPressure
  Normal  NodeHasSufficientPID     38m (x2 over 38m)  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasSufficientPID
  Normal  NodeAllocatableEnforced  38m                kubelet     Updated Node Allocatable limit across pods
  Normal  Starting                 38m                kube-proxy  Starting kube-proxy.
  Normal  NodeReady                37m                kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeReady

34 行目の通り MemoryPressure は False です。また 25 行目から Taint が何も付与されていないことがわかります。

では、Pod にメモリ負荷をかけるため、Pod にログインして yes コマンドを実行します。

$ kubectl exec -it deployment-a-75979d4477-ssffj -- /bin/bash
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &

しばらくすると Pod が Evict されて他の Node で再起動しました。

$ kubectl get pod -o wide
NAME                            READY   STATUS    RESTARTS   AGE     IP            NODE                                              NOMINATED NODE   READINESS GATES
deployment-a-75979d4477-2n8mt   0/1     Evicted   0          112s    <none>        ip-10-0-101-27.ap-northeast-1.compute.internal    <none>           <none>
deployment-a-75979d4477-6ls9v   1/1     Running   0          111s    10.0.103.15   ip-10-0-103-229.ap-northeast-1.compute.internal   <none>           <none>

最初に Pod が起動していた Node の状態を確認します。

$ kubectl describe node ip-10-0-101-27.ap-northeast-1.compute.internal
Name:               ip-10-0-101-27.ap-northeast-1.compute.internal

~省略~

CreationTimestamp:  Wed, 02 Feb 2022 22:56:40 +0900
Taints:             node.kubernetes.io/memory-pressure:NoSchedule
Unschedulable:      false
Lease:
  HolderIdentity:  ip-10-0-101-27.ap-northeast-1.compute.internal
  AcquireTime:     <unset>
  RenewTime:       Wed, 02 Feb 2022 23:35:09 +0900
Conditions:
  Type             Status  LastHeartbeatTime                 LastTransitionTime                Reason                         Message
  ----             ------  -----------------                 ------------------                ------                         -------
  MemoryPressure   True    Wed, 02 Feb 2022 23:35:04 +0900   Wed, 02 Feb 2022 23:35:04 +0900   KubeletHasInsufficientMemory   kubelet has insufficient memory available
  DiskPressure     False   Wed, 02 Feb 2022 23:35:04 +0900   Wed, 02 Feb 2022 22:56:38 +0900   KubeletHasNoDiskPressure       kubelet has no disk pressure
  PIDPressure      False   Wed, 02 Feb 2022 23:35:04 +0900   Wed, 02 Feb 2022 22:56:38 +0900   KubeletHasSufficientPID        kubelet has sufficient PID available
  Ready            True    Wed, 02 Feb 2022 23:35:04 +0900   Wed, 02 Feb 2022 22:57:11 +0900   KubeletReady                   kubelet is posting ready status

~省略~

Events:
  Type     Reason                     Age                From        Message
  ----     ------                     ----               ----        -------
  Normal   Starting                   38m                kubelet     Starting kubelet.
  Normal   NodeHasSufficientMemory    38m (x2 over 38m)  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasSufficientMemory
  Normal   NodeHasNoDiskPressure      38m (x2 over 38m)  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasNoDiskPressure
  Normal   NodeHasSufficientPID       38m (x2 over 38m)  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasSufficientPID
  Normal   NodeAllocatableEnforced    38m                kubelet     Updated Node Allocatable limit across pods
  Normal   Starting                   38m                kube-proxy  Starting kube-proxy.
  Normal   NodeReady                  38m                kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeReady
  Warning  EvictionThresholdMet       17s                kubelet     Attempting to reclaim memory
  Normal   NodeHasInsufficientMemory  14s                kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasInsufficientMemory

MemorryPressure が True になり、Taint に node.kubernetes.io/memory-pressure:NoSchedule が付与されました。NoSchedule が付与されると Pod をスケジュールできなくなります。(Pod に合致する toleration がない場合)

しばらくして再度確認すると Taint がなくなって MemoryPressure が False に戻っていました。Condition の 更新時間は LastTransitionTime の列で確認できますが、evictionPressureTransitionPeriod の設定通り 5 分後に更新されたことがわかります。

yasu eviction %k describe node ip-10-0-101-27.ap-northeast-1.compute.internal
Name:               ip-10-0-101-27.ap-northeast-1.compute.internal

~省略~

CreationTimestamp:  Wed, 02 Feb 2022 22:56:40 +0900
Taints:             <none>
Unschedulable:      false
Lease:
  HolderIdentity:  ip-10-0-101-27.ap-northeast-1.compute.internal
  AcquireTime:     <unset>
  RenewTime:       Wed, 02 Feb 2022 23:47:27 +0900
Conditions:
  Type             Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----             ------  -----------------                 ------------------                ------                       -------
  MemoryPressure   False   Wed, 02 Feb 2022 23:45:08 +0900   Wed, 02 Feb 2022 23:40:05 +0900   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure     False   Wed, 02 Feb 2022 23:45:08 +0900   Wed, 02 Feb 2022 22:56:38 +0900   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure      False   Wed, 02 Feb 2022 23:45:08 +0900   Wed, 02 Feb 2022 22:56:38 +0900   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready            True    Wed, 02 Feb 2022 23:45:08 +0900   Wed, 02 Feb 2022 22:57:11 +0900   KubeletReady                 kubelet is posting ready status

~省略~

Events:
  Type     Reason                     Age                  From        Message
  ----     ------                     ----                 ----        -------
  Normal   Starting                   50m                  kubelet     Starting kubelet.
  Normal   NodeHasNoDiskPressure      50m (x2 over 50m)    kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasNoDiskPressure
  Normal   NodeHasSufficientPID       50m (x2 over 50m)    kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasSufficientPID
  Normal   NodeAllocatableEnforced    50m                  kubelet     Updated Node Allocatable limit across pods
  Normal   Starting                   50m                  kube-proxy  Starting kube-proxy.
  Normal   NodeReady                  50m                  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeReady
  Warning  EvictionThresholdMet       12m                  kubelet     Attempting to reclaim memory
  Normal   NodeHasInsufficientMemory  12m                  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasInsufficientMemory
  Normal   NodeHasSufficientMemory    7m25s (x3 over 50m)  kubelet     Node ip-10-0-101-27.ap-northeast-1.compute.internal status is now: NodeHasSufficientMemory

4. まとめ

Eviction まわりの挙動を確認してみました。EKS 等のマネージドサービスの場合は特別な要件がない限り設定値は変更しなくて良いと思います。

5. 参考

Node-pressure Eviction | Kubernetes

Configure Out of Resource Handling | Kubernetes

TaintとToleration | Kubernetes

Kubernetes の kube-reserved と system-reserved

Kubernetes のシステム用に割り当てられるリソースについて整理し、実際の挙動を確認しました。

1. Pod に割り当て可能なリソース

まずは、Kubernetes 上で Pod が使えるリソースについて整理します。

Node 上では OS, Kubernetes が動いており、その上で Pod が動いているため Pod が Node のリソースを全て使用すると問題(OS 全体に影響が出る、Kubernetes 自体が動作しなくなる等)が発生します。 それを回避するため、OS, Kubernetes 用リソース kube-reserved, system-reserved と、Node の最低空きリソース eviction-threshold があります。

Pod に割り当てられるリソースは Node のリソース量から、kube-reserved,system-reserved,eviction-threshold を引いた分になります。

f:id:dunkshoot:20220202230021p:plain

本記事では kube-reserved,system-reserved についてまとめています。eviction-threshold については別の記事でまとめます。

2. cgroup

kube-reserved,system-reserved の説明に cgroup が出てくるため、cgroup について簡単に整理します。

cgroup とは Control Group の略で、プロセスをグループ化し CPU やメモリ等のリソースをグループ毎に制限する Linux カーネルの機能です。

cgroupfs という仮想ファイルシステム上で cgroup を操作します。cgroupfs は /sys/fs/cgroup にマウントされています。

eksctl コマンドで EKS Cluster を作成する - YasuBlog の記事で作成した EKS Node で確認すると cgroupfs が /sys/fs/cgroup にマウントされている事が確認できます。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
devtmpfs         1.9G     0  1.9G    0% /dev
tmpfs            1.9G     0  1.9G    0% /dev/shm
tmpfs            1.9G  760K  1.9G    1% /run
tmpfs            1.9G     0  1.9G    0% /sys/fs/cgroup
/dev/nvme0n1p1    30G  2.7G   28G    9% /
tmpfs            388M     0  388M    0% /run/user/1000

~省略~

$ mount -l | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)

/sys/fs/cgroup 配下のファイルで cgroup を管理しています。

$ ls /sys/fs/cgroup/
blkio  cpu  cpu,cpuacct  cpuacct  cpuset  devices  freezer  hugetlb  memory  net_cls  net_cls,net_prio  net_prio  perf_event  pids  systemd

例えば、/sys/fs/cgroup/cpu/tasks に記載されたプロセス(PID)の CPU リソース は /sys/fs/cgroup/cpu/cpu.shares に記載された 1024 に制限されます。※ CPU share は CPU の相対値

$ cat /sys/fs/cgroup/cpu/tasks
1
2
3
4
6
8

~省略~

$ cat /sys/fs/cgroup/cpu/cpu.shares
1024

cgroup を追加する際はディレクトリを作成します。test という cgroup を作成してみます。

$ mkdir /sys/fs/cgroup/cpu/test

ディレクトリを作成するとその配下に自動でファイルが作成されます。

$ ls /sys/fs/cgroup/cpu/test
cgroup.clone_children  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  cpuacct.stat  cpuacct.usage  cpuacct.usage_all  cpuacct.usage_percpu  cpuacct.usage_percpu_sys  cpuacct.usage_percpu_user  cpuacct.usage_sys  cpuacct.usage_user  notify_on_release  tasks

例えば tasks に 2000、cpu.share に 512 を記載すると PID 2000 のプロセスの CPU share を 512 に制限する事になります。

$ cat /sys/fs/cgroup/cpu/test/tasks
2000
$ cat /sys/fs/cgroup/cpu/test/cpu.shares
512

コンテナは OS から見たらただのプロセスなのでこのようにリソースを制限出来ます。

なお、systemd-cgls コマンドで cgroup 階層全体が表示されます。

$ systemd-cgls
├─   1 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
├─2913 bpfilter_umh
├─kubepods
│ └─burstable
│   ├─pod2a7eefa0-6719-4cac-82f4-6b7d58d54635
│   │ ├─510f73e18aa7dac452276e74f7acfd7087f44562eae619e05654bae567f5b460
│   │ │ ├─4276 bash /app/entrypoint.sh
│   │ │ ├─4327 ./aws-k8s-agent
│   │ │ └─4328 tee -i aws-k8s-agent.log
│   │ └─e7101431d6f2cad771a0d30c81101d06475c2b4dad6b033eaf58093e636679a8
│   │   └─3585 /pause
│   ├─pode895364f-3e61-48db-93cc-b0f8daefaa55
│   │ ├─c63c43b9ab660f4e2cab66f576e1a3208b6b97d659c0666c7c2c342daeecef8e
│   │ │ └─4891 /coredns -conf /etc/coredns/Corefile
│   │ └─aa5779e764f7fbfe68948de0ce425e7e03b2c5a2082586f6eb94e9a29b4670a3
│   │   └─4587 /pause
│   ├─poddd65f1eb-593a-4882-bb86-9f453fc1a1d2
│   │ ├─0d51784c7ac2cdc9a013d19d79a5617c27c6d4f86c3dfc28c7517f96eb9c8743
│   │ │ └─3637 /pause
│   │ └─cb2b42771c6e91de40b0cddde1e57677f5c112fa013fd9c601fa960e2e9d785d
│   │   └─3876 kube-proxy --v=2 --config=/var/lib/kube-proxy-config/config
│   └─podab6c28cc-525a-4fd2-9a69-07720eeed5ee
│     ├─73c143c3c6b7706e3ef2e45b57ca0d6484b9679ba1920c095ba3c1b9025892b2
│     │ └─4626 /pause
│     └─8dd43dd500d50180b3d287d96be60c1aa341dff864c9ce1903825dfeae6a7c04
│       └─4925 /coredns -conf /etc/coredns/Corefile
├─user.slice
│ └─user-1000.slice
│   ├─session-4.scope
│   │ ├─28624 sshd: ec2-user [priv]
│   │ ├─28655 sshd: ec2-user@pts/1
│   │ ├─28656 -bash
│   │ ├─28700 sudo -s
│   │ ├─28701 /bin/bash
│   │ └─29383 systemd-cgls
│   └─session-1.scope
│     ├─5724 sshd: ec2-user [priv]
│     ├─5779 sshd: ec2-user@pts/0
│     ├─5794 -bash
│     ├─7556 sudo -s
│     ├─7557 /bin/bash
│     ├─8704 systemd-cgls
│     └─8705 less
└─system.slice
  ├─rngd.service
  │ └─1875 /sbin/rngd -f --fill-watermark=0 --exclude=jitter
  ├─irqbalance.service
  │ └─1853 /usr/sbin/irqbalance --foreground
  ├─amazon-ssm-agent.service
  │ ├─2361 /usr/bin/amazon-ssm-agent
  │ └─2518 /usr/bin/ssm-agent-worker
  ├─containerd.service
  │ ├─2781 /usr/bin/containerd
  │ ├─3545 /usr/bin/containerd-shim-runc-v2 -namespace moby -id e7101431d6f2c...
  │ ├─3546 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 0d51784c7ac2c...
  │ ├─3856 /usr/bin/containerd-shim-runc-v2 -namespace moby -id cb2b42771c6e9...
  │ ├─4257 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 510f73e18aa7d...
  │ ├─4528 /usr/bin/containerd-shim-runc-v2 -namespace moby -id aa5779e764f7f...
  │ ├─4530 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 73c143c3c6b77...
  │ ├─4826 /usr/bin/containerd-shim-runc-v2 -namespace moby -id c63c43b9ab660...
  │ └─4844 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 8dd43dd500d50...
  ├─systemd-udevd.service
  │ └─1305 /usr/lib/systemd/systemd-udevd
  ├─system-serial\x2dgetty.slice
  │ └─serial-getty@ttyS0.service
  │   └─2377 /sbin/agetty --keep-baud 115200,38400,9600 ttyS0 vt220
  ├─docker.service
  │ └─2870 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd....
  ├─chronyd.service
  │ └─1861 /usr/sbin/chronyd
  ├─auditd.service
  │ └─1826 /sbin/auditd
  ├─kubelet.service
  │ └─3054 /usr/bin/kubelet --cloud-provider aws --config /etc/kubernetes/kub...
  ├─systemd-journald.service
  │ └─1284 /usr/lib/systemd/systemd-journald
  ├─sshd.service
  │ └─2423 /usr/sbin/sshd -D
  ├─crond.service
  │ └─2376 /usr/sbin/crond -n
  ├─gssproxy.service
  │ └─1887 /usr/sbin/gssproxy -D
  ├─rsyslog.service
  │ └─2367 /usr/sbin/rsyslogd -n
  ├─rpcbind.service
  │ └─1851 /sbin/rpcbind -w
  ├─network.service
  │ ├─2087 /sbin/dhclient -q -lf /var/lib/dhclient/dhclient--eth0.lease -pf /...
  │ └─2122 /sbin/dhclient -6 -nw -lf /var/lib/dhclient/dhclient6--eth0.lease ...
  ├─lvm2-lvmetad.service
  │ └─1302 /usr/sbin/lvmetad -f
  ├─postfix.service
  │ ├─ 2268 /usr/libexec/postfix/master -w
  │ ├─ 2270 qmgr -l -t unix -u
  │ └─13892 pickup -l -t unix -u
  ├─dbus.service
  │ └─1857 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidf...
  ├─system-getty.slice
  │ └─getty@tty1.service
  │   └─2373 /sbin/agetty --noclear tty1 linux
  └─systemd-logind.service
    └─1864 /usr/lib/systemd/systemd-logind

3. kube-reserved/system-reserved

公式ドキュメント Reserve Compute Resources for System Daemons | Kubernetes には以下のように記載されています。(意訳)

項目 概要
kube-reserved ・kubelet や container runtime, node problem detector などの Kubernetes system daemon 用に確保されるリソース
・Pod として動く system deamon は対象外
・CPU/メモリ/エフェメラルストレージ/PID 数 を指定可能
・kubelet フラグ例
 --kube-reserved=cpu=100m,memory=100Mi,ephemeral-storage=1Gi,pid=1000
Kubernetes system daemon--kube-reserved を強制するには --enforce-node-allocatable=kube-reserved--kube-reserved-cgroup=<cgroup> が必要
system-reserved sshd や udev などの OS system daemon 用に確保されるリソース
カーネルメモリは Pod に割り当てられないためカーネル用のメモリも確保した方がよい
・ユーザのログインセッション用リソースを確保する事も推奨(systemd の世界では user.slice)
・CPU/メモリ/エフェメラルストレージ/PID 数 を指定可能
・kubelet フラグ例
 --system-reserved=cpu=100m,memory=100Mi,ephemeral-storage=1Gi,pid=1000
・OS system daemon--system-reserved を強制するには --enforce-node-allocatable=system-reserved--system-reserved-cgroup=<cgroup> が必要

Kubernetes/OS system daemon--kube-reserved/--system-reserved を強制するには --enforce-node-allocatable=kube-reserved/system-reserved--kube-reserved-cgroup/--systemreserved-cgroup=<cgroup> が必要」がいまいちピンと来なかったです。

--kube-reserved/--system-reserved だけでは Kubernetes/OS system daemon 用にリソースが確保されて強制はされない?

他にも、Kubernetes/OS が設定値以上使おうとするとどうなるのか?推奨値はあるのか?

などなど気になる点が多いため検証してみました。

4. 検証

4.1. 検証環境構築

eksctl コマンドで EKS Cluster を作成する - YasuBlog で作成した EKS Cluster を使用します。

4.2. EKS の kube-reserved/system-reserved 設定値

まずは kube-reserved/system-reserved の確認方法です。Node の情報なので KubernetesAPI サーバから取得できます。

kubectl proxy コマンドでプロキシをローカルに起動すると API サーバにアクセスできます。

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

デフォルトでローカルの 8001 ポートでプロキシが起動するので、別ターミナルで http://localhost:8001/api/v1/nodes/<Node 名>/proxy/configz にアクセスします。json なので jq で整形すると見やすいです。

$ kubectl get node
NAME                                              STATUS   ROLES    AGE    VERSION
ip-10-0-101-249.ap-northeast-1.compute.internal   Ready    <none>   2m36s   v1.21.5-eks-bc4871b
ip-10-0-102-241.ap-northeast-1.compute.internal   Ready    <none>   2m27s   v1.21.5-eks-bc4871b
ip-10-0-103-143.ap-northeast-1.compute.internal   Ready    <none>   2m12s   v1.21.5-eks-bc4871b
$ curl -sSL "http://localhost:8001/api/v1/nodes/ip-10-0-101-249.ap-northeast-1.compute.internal/proxy/configz" | jq .
{
  "kubeletconfig": {
    "enableServer": true,
    "syncFrequency": "1m0s",
    "fileCheckFrequency": "20s",
    "httpCheckFrequency": "20s",
    "address": "0.0.0.0",
    "port": 10250,
    "tlsCipherSuites": [
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_128_GCM_SHA256"
    ],
    "serverTLSBootstrap": true,
    "authentication": {
      "x509": {
        "clientCAFile": "/etc/kubernetes/pki/ca.crt"
      },
      "webhook": {
        "enabled": true,
        "cacheTTL": "2m0s"
      },
      "anonymous": {
        "enabled": false
      }
    },
    "authorization": {
      "mode": "Webhook",
      "webhook": {
        "cacheAuthorizedTTL": "5m0s",
        "cacheUnauthorizedTTL": "30s"
      }
    },
    "registryPullQPS": 5,
    "registryBurst": 10,
    "eventRecordQPS": 5,
    "eventBurst": 10,
    "enableDebuggingHandlers": true,
    "healthzPort": 10248,
    "healthzBindAddress": "127.0.0.1",
    "oomScoreAdj": -999,
    "clusterDomain": "cluster.local",
    "clusterDNS": [
      "172.20.0.10"
    ],
    "streamingConnectionIdleTimeout": "4h0m0s",
    "nodeStatusUpdateFrequency": "10s",
    "nodeStatusReportFrequency": "5m0s",
    "nodeLeaseDurationSeconds": 40,
    "imageMinimumGCAge": "2m0s",
    "imageGCHighThresholdPercent": 85,
    "imageGCLowThresholdPercent": 80,
    "volumeStatsAggPeriod": "1m0s",
    "cgroupRoot": "/",
    "cgroupsPerQOS": true,
    "cgroupDriver": "cgroupfs",
    "cpuManagerPolicy": "none",
    "cpuManagerReconcilePeriod": "10s",
    "memoryManagerPolicy": "None",
    "topologyManagerPolicy": "none",
    "topologyManagerScope": "container",
    "runtimeRequestTimeout": "2m0s",
    "hairpinMode": "hairpin-veth",
    "maxPods": 17,
    "podPidsLimit": -1,
    "resolvConf": "/etc/resolv.conf",
    "cpuCFSQuota": true,
    "cpuCFSQuotaPeriod": "100ms",
    "nodeStatusMaxImages": 50,
    "maxOpenFiles": 1000000,
    "contentType": "application/vnd.kubernetes.protobuf",
    "kubeAPIQPS": 5,
    "kubeAPIBurst": 10,
    "serializeImagePulls": false,
    "evictionHard": {
      "memory.available": "100Mi",
      "nodefs.available": "10%",
      "nodefs.inodesFree": "5%"
    },
    "evictionPressureTransitionPeriod": "5m0s",
    "enableControllerAttachDetach": true,
    "protectKernelDefaults": true,
    "makeIPTablesUtilChains": true,
    "iptablesMasqueradeBit": 14,
    "iptablesDropBit": 15,
    "featureGates": {
      "RotateKubeletServerCertificate": true
    },
    "failSwapOn": true,
    "containerLogMaxSize": "10Mi",
    "containerLogMaxFiles": 5,
    "configMapAndSecretChangeDetectionStrategy": "Watch",
    "kubeReserved": {
      "cpu": "70m",
      "ephemeral-storage": "1Gi",
      "memory": "442Mi"
    },
    "enforceNodeAllocatable": [
      "pods"
    ],
    "volumePluginDir": "/usr/libexec/kubernetes/kubelet-plugins/volume/exec/",
    "logging": {
      "format": "text"
    },
    "enableSystemLogHandler": true,
    "shutdownGracePeriod": "0s",
    "shutdownGracePeriodCriticalPods": "0s",
    "enableProfilingHandler": true,
    "enableDebugFlagsHandler": true
  }
}

104 行目から 108 行目に kube-reserved が表示されています。 kubeReservedCgroup/systemReservedCgroup が無いため、--kube-reserved-cgroup/--system-reserved-cgroup が設定されていない事がわかります。 また、enforceNodeAllocatablepods のみ設定されています。(kube-reserved/system-reserved は設定されていない)

なお、API サーバではなく Node にログインして設定ファイルから値を確認する事も可能です。

@node$ cat /etc/kubernetes/kubelet/kubelet-config.json
{
  "kind": "KubeletConfiguration",
  "apiVersion": "kubelet.config.k8s.io/v1beta1",
  "address": "0.0.0.0",
  "authentication": {
    "anonymous": {
      "enabled": false
    },
    "webhook": {
      "cacheTTL": "2m0s",
      "enabled": true
    },
    "x509": {
      "clientCAFile": "/etc/kubernetes/pki/ca.crt"
    }
  },
  "authorization": {
    "mode": "Webhook",
    "webhook": {
      "cacheAuthorizedTTL": "5m0s",
      "cacheUnauthorizedTTL": "30s"
    }
  },
  "clusterDomain": "cluster.local",
  "hairpinMode": "hairpin-veth",
  "readOnlyPort": 0,
  "cgroupDriver": "cgroupfs",
  "cgroupRoot": "/",
  "featureGates": {
    "RotateKubeletServerCertificate": true
  },
  "protectKernelDefaults": true,
  "serializeImagePulls": false,
  "serverTLSBootstrap": true,
  "tlsCipherSuites": [
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_128_GCM_SHA256"
  ],
  "maxPods": 20,
  "clusterDNS": [
    "172.20.0.10"
  ],
  "evictionHard": {
    "memory.available": "100Mi",
    "nodefs.available": "10%",
    "nodefs.inodesFree": "5%"
  },
  "kubeReserved": {
    "cpu": "70m",
    "ephemeral-storage": "1Gi",
    "memory": "442Mi"
  }

インスタンスタイプ毎の設定値を確認してみました。Node のリソース量から自動で計算されているようです。

インスタンスタイプ kube-reserved system-reserved kube-reserved-cgroup system-reserved-cgroup
t3.small cpu:70m
memory:376Mi
ephemeral-storage:1Gi
なし なし なし
t3.medium cpu:70m
memory:442Mi
ephemeral-storage:1Gi
なし なし なし
m5.large cpu:80m
memory:574Mi
ephemeral-storage:1Gi
なし なし なし
m5.xlarge cpu:80m
memory:893Mi
ephemeral-storage:1Gi
なし なし なし

4.3. 設定値変更

AWS マネージドではないセルフマネージド NodeGroup の場合は以下の通り設定できそうです。

Customizing kubelet configuration - eksctl

AWS マネージドの場合は yaml での設定方法は見つかりませんでした。Node 上の設定ファイル(/etc/kubernetes/kubelet/kubelet-config.json や /etc/systemd/system/kubelet.service)を直接修正する事で設定変更が可能です。 Node 再作成時にも同様に設定されるように、カスタム起動テンプレートかカスタム AMI を使用する必要があります。

起動テンプレートのサポート - Amazon EKS

Amazon EKS ワーカーノードを設定して特定のディスク使用率でイメージキャッシュをクリーンアップする

個人的には AWS マネージドという事は AWS が良かれと思って設計した値なのでユーザ側がカスタマイズする必要は無いと思っています。

4.4. --enforce-node-allocatable/--kube-reserved-cgroup/--system-reserved-cgroup 無しの場合の制限

EKS のデフォルト状態(--enforce-node-allocatable--kube-reserved-cgroup/--system-reserved-cgroup が設定されていない状態)での制限を確認します。

検証用 Node(t3.medium)の設定値は以下です。

cpu memory ephemeral-storage
kube-reserved 70m 442Mi 1Gi
system-reserved なし なし なし

Kubernetes system daemon 用に cpu 70M, memory 442Mi, ephemeral-storage 1Gi が確保されています。最初に記載した通り、Pod に割り当てられるリソース = Node - (kube-reserved + system-reserved + eviction-threshold) のため、「確保されている」と言えると思います。

ただし、--enforce-node-allocatable--kube-reserved-cgroup/--system-reserved-cgroup が設定されていないため強制はされていないはずです。 cpu.shares に 70m 、memory.limit_in_bytes に 442Mi が設定されている cgroup の有無を確認しましたが存在しなかったので確かに「強制されていない(リソースを制限していない)」という事がわかります。同様に --system-reserved を設定して検証しても同じ結果でした。

@node$ find /sys/fs/cgroup/ -type f -name 'cpu.shares' -exec cat {} \; | sort | uniq
102
1024
128
1976
2
25
@node$ find /sys/fs/cgroup/ -type f -name 'memory.limit_in_bytes' -exec cat {} \; | sort | uniq
3135741952
9223372036854771712

つまり、--kube-reserved/--system-reservedKubernetes 用とか OS 用とか関係なく、単純に Pod に割り当てられない領域として確保しているだけという事になります。これは --kube-reserved--system-reserved が同じ意味を表しているという事になるかと思います。※--enforce-node-allocatable--kube-reserved-cgroup/--system-reserved-cgroup が無い場合

マネージドサービスの EKS が --system-reserved を設定していない理由はこれかと思います。--kube-reserved 一つ設定しておけば Kubernetes/OS 用のリソースとして確保できるからです。(--kube-reserved--system-reserved が同じ意味なので --system-reserved を設定する必要がない)

当然ですが、リソースを制限しているわけではないので、Kubernetes/OS が --kube-reserved/--system-reserved に設定されている値より多くのリソースを使用する事が可能です。

4.5. --enforce-node-allocatable/--kube-reserved-cgroup/--system-reserved-cgroup ありの場合の制限

まずは --kube-reserved-cgroup に設定する cgroup を作成します。プロセスは dockerd を設定します。

@node$ # find /sys/fs/cgroup/ -maxdepth 1 ! -type l -exec mkdir {}/K8s \;
mkdir: ディレクトリ `/sys/fs/cgroup//K8s` を作成できません: Read-only file system
@node$ pgrep -fl dockerd
2877 dockerd
@node$ find /sys/fs/cgroup/ -type d -name K8s -exec sh -c 'echo 2877 > {}/tasks' \;
sh: 0 行: echo: 書き込みエラー: No space left on device

systemd-cgls で cgroup を確認します。

@node$ systemd-cgls
├─   1 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
├─2986 bpfilter_umh
├─kubepods
│ └─burstable
│   ├─pode887cc03-8c8a-47fe-94ce-77f75b71b7e2
│   │ ├─5724c33afacf82531d99d78fcea7b5b07b54105d2729b20953eb3e6a3da9dcfb
│   │ │ └─3674 /pause
│   │ └─b263bef8f5b3aff3df5430cc05551a42d72ffbd93a8e56ed10b3b2e854f2f160
│   │   ├─4309 bash /app/entrypoint.sh
│   │   ├─4357 ./aws-k8s-agent
│   │   └─4358 tee -i aws-k8s-agent.log
│   └─pod637abaec-5fc8-4855-9952-ac08730384df
│     ├─1ed3616be7204d70dda58c0ac3a6060fd26b6d595b0a832604cb9b5b777bb91e
│     │ └─3629 /pause
│     └─f703824f768510f7b8dfd989c8f64d8784cb201d7fe81949a000baed59c869c3
│       └─3911 kube-proxy --v=2 --config=/var/lib/kube-proxy-config/config
├─user.slice
│ └─user-1000.slice
│   └─session-1.scope
│     ├─4928 sshd: ec2-user [priv]
│     ├─5012 sshd: ec2-user@pts/0
│     ├─5036 -bash
│     ├─5058 sudo -s
│     ├─5060 /bin/bash
│     ├─5640 systemd-cgls
│     └─5641 less
├─system.slice
│ ├─rngd.service
│ │ └─1866 /sbin/rngd -f --fill-watermark=0 --exclude=jitter
│ ├─irqbalance.service
│ │ └─1853 /usr/sbin/irqbalance --foreground
│ ├─amazon-ssm-agent.service
│ │ ├─2363 /usr/bin/amazon-ssm-agent
│ │ └─2519 /usr/bin/ssm-agent-worker
│ ├─containerd.service
│ │ ├─2800 /usr/bin/containerd
│ │ ├─3577 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 1ed3616be7204d70dda58c0ac3a6060fd26b6d595b0a832604cb9b5b777bb91e -address /run/containerd/containerd.sock
│ │ ├─3578 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 5724c33afacf82531d99d78fcea7b5b07b54105d2729b20953eb3e6a3da9dcfb -address /run/containerd/containerd.sock
│ │ ├─3887 /usr/bin/containerd-shim-runc-v2 -namespace moby -id f703824f768510f7b8dfd989c8f64d8784cb201d7fe81949a000baed59c869c3 -address /run/containerd/containerd.sock
│ │ └─4279 /usr/bin/containerd-shim-runc-v2 -namespace moby -id b263bef8f5b3aff3df5430cc05551a42d72ffbd93a8e56ed10b3b2e854f2f160 -address /run/containerd/containerd.sock
│ ├─systemd-udevd.service
│ │ └─1686 /usr/lib/systemd/systemd-udevd
│ ├─system-serial\x2dgetty.slice
│ │ └─serial-getty@ttyS0.service
│ │   └─2375 /sbin/agetty --keep-baud 115200,38400,9600 ttyS0 vt220
│ ├─docker.service
│ │ └─2877 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
│ ├─chronyd.service
│ │ └─1884 /usr/sbin/chronyd
│ ├─auditd.service
│ │ └─1827 /sbin/auditd
│ ├─kubelet.service
│ │ └─3146 /usr/bin/kubelet --cloud-provider aws --config /etc/kubernetes/kubelet/kubelet-config.json --kubeconfig /var/lib/kubelet/kubeconfig --container-runtime docker --network-plugin cni --node-ip=10.0.102.145 --pod-infra-container-image=602401143452.dkr.ecr.ap-northe
│ ├─systemd-journald.service
│ │ └─1285 /usr/lib/systemd/systemd-journald
│ ├─sshd.service
│ │ └─2416 /usr/sbin/sshd -D
│ ├─crond.service
│ │ └─2373 /usr/sbin/crond -n
│ ├─gssproxy.service
│ │ └─1888 /usr/sbin/gssproxy -D
│ ├─rsyslog.service
│ │ └─2365 /usr/sbin/rsyslogd -n
│ ├─rpcbind.service
│ │ └─1867 /sbin/rpcbind -w
│ ├─network.service
│ │ ├─2087 /sbin/dhclient -q -lf /var/lib/dhclient/dhclient--eth0.lease -pf /var/run/dhclient-eth0.pid eth0
│ │ └─2123 /sbin/dhclient -6 -nw -lf /var/lib/dhclient/dhclient6--eth0.lease -pf /var/run/dhclient6-eth0.pid eth0
│ ├─lvm2-lvmetad.service
│ │ └─1301 /usr/sbin/lvmetad -f
│ ├─postfix.service
│ │ ├─2285 /usr/libexec/postfix/master -w
│ │ ├─2286 pickup -l -t unix -u
│ │ └─2287 qmgr -l -t unix -u
│ ├─dbus.service
│ │ └─1860 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
│ ├─system-getty.slice
│ │ └─getty@tty1.service
│ │   └─2374 /sbin/agetty --noclear tty1 linux
│ └─systemd-logind.service
│   └─1864 /usr/lib/systemd/systemd-logind
└─K8s
  └─2877 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd servcie 用 cgroup の K8s が作成されている事がわかります。cgroup K8s のリソースが --kube-reserved に設定した値で制限されるように etc/systemd/system/kubelet.service を修正して kubelet を再起動します。

@node$ cat /etc/systemd/system/kubelet.service 
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=docker.service iptables-restore.service
Requires=docker.service

[Service]
ExecStartPre=/sbin/iptables -P FORWARD ACCEPT -w 5
ExecStart=/usr/bin/kubelet --cloud-provider aws \
    --config /etc/kubernetes/kubelet/kubelet-config.json \
    --kubeconfig /var/lib/kubelet/kubeconfig \
    --container-runtime docker \
    --network-plugin cni $KUBELET_ARGS $KUBELET_EXTRA_ARGS \
    --enforce-node-allocatable=pods,kube-reserved \
    --kube-reserved-cgroup=/K8s

Restart=always
RestartSec=5
KillMode=process

[Install]
WantedBy=multi-user.target
@node$ systemctl daemon-reload
@node$ systemctl restart kubelet

kubelet 再起動後に cgroup K8s を確認すると cpu が --kube-reserved に設定した 70m で制限されていました。

@node$ cat /sys/fs/cgroup/cpu/K8s/cpu.shares
71

--enforce-node-allocatable=kube-reserved,system-reserved--kube-reserved-cgroup/--system-reserved-cgroup を設定する事で、--kube-reserved/--system-reserved に設定した値で Kubernetes/OS のリソースを制限できる事がわかりました。

マネージドではない手組みの Kubernetes の場合は、Kubernetes/OS 用に cgroup を作成してそれぞれのリソースを制限するという使い方が可能という事かと思います。

なお、cpu.shares は相対値のためリソースに空きがあれば設定された数値より多くのリソースを使用する事が可能です。

5. まとめ

上記の検証結果によるまとめです。

  • --enforce-node-allocatable=kube-reserved,system-reserved なし --kube-reserved-cgroup/--system-reserved-cgroup なしの場合
    • 特定のプロセス用にリソースを確保しているわけではなく、単純に Pod が使えないリソースというだけ
    • --kube-reserved--system-reserved は同じ意味(そのため EKS では --kube-reserved のみ設定)
  • --enforce-node-allocatable=kube-reserved,system-reserved あり --kube-reserved-cgroup/--system-reserved-cgroup ありの場合
    • 指定した cgroup に対してリソースを制限する事が可能
    • どのプロセスをどのようにグループ(cgroup)化し、どれぐらいリソースを割り当てるかという設計が必要
  • (個人的には)EKS 等のマネージドサービスの場合はこれらの値を気にしなくて良い(クラウドベンダの設計にまかせて、何か問題が起きた際に考える)

6. 参考

Reserve Compute Resources for System Daemons | Kubernetes

design-proposals-archive/node-allocatable.md at main · kubernetes/design-proposals-archive · GitHub