YasuBlog

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

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