亚马逊AWS官方博客

一文看懂 Amazon EKS 中的网络规划

Amazon Elastic Kubernetes Service (EKS)是一项云上的托管 Kubernetes (K8S) 服务,使用该服务您可以轻松地在亚马逊云上部署、管理和扩展容器化的应用程序。Amazon EKS 会在一个亚马逊区域中的不同可用区内托管 Kubernetes 的管理节点,并提供一定的 SLA 保证,从而消除单点故障和管理节点的运维管理工作。用户只需要管理自己的工作节点,以及底层的 EC2 实例或者是无服务器化的 Fargate 即可。

在我们的某些业务场景中,我们需要对整个 K8S 的集群网络配置进行一定的定制化,比如我们在多个区域进行 K8S 集群的部署,并且已经规划好每个区域所使用的 IP 地址网段,比如 Node 节点网段,Service 服务网段,Pod 容器网段,那么在创建和管理 K8S 集群的时候,我们就需要对这些网段进行定制化的配置。

另外,如果我们随着云上的负载越来越多,可能会遇到前期规划的 VPC 网络地址池太小的问题,我们也可以使用自定义 Pod 网段的方法来扩展 Pod 能使用的网络地址池。

本文包含的主要内容有:

  • 如何自定义 Node 节点网段和 Service 服务网段
  • 如何自定义 Pod 容器网段
  • 一个 Node 节点如何创建无数量限制的 Pod
  • 每一个 Pod 绑定单独的公网地址
  • 一个 Pod 如何绑定多张网卡

子网设计

我们先在美东1区按照下面的网络规划创建环境。

  • VPC 网络:172. 16.0.0/16
  • Pod 网段:172.16.10.0/24 和 172.16.11.0/24
  • Node 网段:172.16.1.0/24 和 172.16.2.0/24
  • 公有网段:172.16.100.0/24和172.16.101.0/24
  • 服务网段:172.17.0.0/24,这是一个虚拟的网段,不需要在VPC子网里面

最佳实践:所有的网段最好在至少两个可用区内进行构造,这样方便后期做任何高可用的设置。以及万一一个可用区挂了,不影响另外一个可用区的工作负载,也不影响整个 EKS 集群(EKS 集群的管理节点默认是跨域3个可用区的)。

我们首先需要根据这个设置手动创建相应的 VPC,子网,Internet 网关,NAT 网关和相应的路由表,具体就不在这里展开描述了。需要特别注意的是,NAT 网关创建的时候需要选择公有子网,公有子网的路由表需要有一条默认路由去往 Internet 网关。私有子网创建一条默认路由去往 NAT 网关。

整个设计的网络规划图如下所示:


子网创建如下:

准备工作

EKS集群创建涉及非常多权限,安全组,IAM 角色和策略的创建,强烈建议使用 EKSCTL 来创建,而不是控制台来创建。另外,如果使用控制台来创建集群,目前还不支持自定义 Service 服务网络,所以对 Service 服务网络有 CIDR 定制需求的话,必须用 EKSCTL 来创建集群。

如何定义 Node 节点网段和 Service 服务网段

首先创建文件cluster-demo.yaml,其中serviceIPv4CIDR这个参数定义了我们的 Service 网段的地址,vpc下的参数设置了我们 Node 的网段,并且定义了它是跨域2个可用区的。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: cluster-demo
  region: us-east-1

kubernetesNetworkConfig:
  serviceIPv4CIDR: 172.17.0.0/24

vpc:
  id: "vpc-0b051aa1e4d39f9c3"
  cidr: "172.16.0.0/16" 
  subnets:
    private:
      us-east-1a:
        id: "subnet-07c2254d23e8c3607"
        cidr: "172.16.1.0/24"
      us-east-1b:
        id: "subnet-08957bf56343de13a"
        cidr: "172.16.2.0/24" 

managedNodeGroups:
  - name: ng-1-workers
    labels: { role: workers }
    instanceType: m5.xlarge
    desiredCapacity: 2
    volumeSize: 80
    privateNetworking: true

通过以下命令来创建 EKS 的集群以及和一个节点组。eksctl 后台会使用 AWS CloudFormation 来创建底层的依赖资源,我们也可以随时通过控制台找到这个服务来查看部署状态或者部署细节。

eksctl create cluster --config-file=cluster-demo.yaml

等待20分钟左右,集群创建好后,可以看到 service 和 node 的地址已经是在自己定义的地址范围内了。


从上图可以看出来,Service 网段已经分配到我们定义的172.17.0.0/24这个网段了,Node 节点的网络也已经是在172.16.1.0/24 和 172.16.2.0/24这两个网段了。

在这个基础上,我们通过命令kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1直接创建一个 Pod,这个 Pod 的地址会自动在 Node 所在的子网地址中进行分配。


我们可以看到新部署的 Pod 也是在 172.16.1.0/24 这个网段,默认情况下 Pod 获取的地址是和 Node 在同一个地址池,如果需要自定义 Pod 网段,我们需要接着往下看。

如何自定义 Pod 容器网段

如前面所说的,我们可以在一个 VPC 中定义我们 Node 所在的网段,我们可以通过 VPC 的路由表来控制所有 Node 节点之间的通信(不管是 VPC 内部的通信,还是 VPC 与 VPN 或者本地 IDC 的通信)。但是对于 K8S 集群来说,它有自己的网络,比如通过 kubenet 我们可以在 Linux 中创建一个 Linux Bridge,通过这个来解决 Pod 之间通信的问题。但是如果我们要跨不同的节点进行 Pod 之间的通信,我们还需要用到 Container Network Plugins (CNI) 插件,比如常见的 Calico, Flannel, Weave Net (EKS 使用的插件), Canal, kube-router, romana等。

以 EKS 中的 CNI 为例子,每一个 Pod 都会和所在的 Node 节点上的 ENI 网卡上的 Secondary IP 地址做一个1对1的映射,所有的 Pod 在 VPC 层级的网络都会以这个地址作为呈现。也就是说,通过 CNI,我们可以将 Pod 的网络和其他 VPC 内的资源一样,打成一个扁平的大三层网络。


EKS VPC CNI 架构图

默认情况下,如前面所说,Pod 所获取的 IP 地址是和 Node 处在同一个子网和可用区的,如果我们需要对 Pod 进行自定义的化,需要用到 CNI Custom Networking 的方法。

首先执行以下简要操作:

kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2

接着创建文件 us-east-1a.yaml, 需要修改以下的securityGroups的 ID(这个是集群的安全组,如果是通过 eksctl 创建的集群的化,可以到安全组内寻找名字后缀是ClusterSharedNodeSecurityGroup的安全组,记录其 ID 填到下面),subnet(找到给 Pod 分配的子网,记录下其ID到下面)和 name(改为可用区的名字)。

apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata: 
  name: us-east-1a
spec: 
  securityGroups: 
    - sg-0cea1cf2ed938xxxx
  subnet: subnet-0295e90ae9985xxxx

同样在另一个可用区也创建一个类似的文件us-east-1b.yaml如下

apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata: 
  name: us-east-1b
spec: 
  securityGroups: 
    - sg-0cea1cf2ed938xxxx
  subnet: subnet-0566721793f91xxxx

注意以上的文件名需要和 YAML 文件内的 name 保持一致。如果实际情况还涉及到多个可用区,则需要在每一个可用区都创建一个相应的文件。

然后我们应用这两个配置文件。这样操作之后,Pod 在启动的时候会自动根据其可用区来分配相应的网段地址。

kubectl apply -f us-east-1a.yaml
kubectl apply -f us-east-1b.yaml
kubectl set env daemonset aws-node -n kube-system ENI_CONFIG_LABEL_DEF=failure-domain.beta.kubernetes.io/zone

使用控制台或者 EKSCTL 删除原来的 NodeGroup,使用命令行的话可参照如下:

eksctl delete nodegroup --cluster cluster-demo --name ng-1-workers

创建一个新的 Node Group 让我们自定义网段的配置生效,首先创建新 NodeGroup 的配置文件new-nodegroup.yaml。这里定义了一个名字为ng-2-workers的节点组,以及它的规格和数量。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: cluster-demo
  region: us-east-1

managedNodeGroups:
  - name: ng-2-workers
    labels: { role: workers }
    instanceType: m5.xlarge
    desiredCapacity: 2
    volumeSize: 80
    privateNetworking: true

创建完文件之后,我们用命令创建该 Node Group。

eksctl create nodegroup --config-file=new-nodegroup.yaml

等待节点组创建成功,然后我们再去查看 Pod 的状态。


这个时候我们看到 Pod 已经从我们自定义的网段 172.16.11.0/24 上创建了。

但注意,有一部分 EKS 的自带的 Pod(aws-node,kube-proxy等)地址是用 host networking,地址是不能变的。

一个 Node 节点如何创建无数量限制的 Pod

在前面的操作中我们会发现,当 Pod 创建出来后,相应的 Node 节点上会出现一个新的 ENI 网卡,网卡上会多出很多 Seconadry IP 地址。


但不同家族和大小的实例,所能添加的网卡数量,以及每个网卡上能支持的 Secondary IP 的数量是有限的,因此在这个节点上能创建的 Pod 数量也是有限的。比如我们 m5.xlarge 实例类型只能最多创建58个 Pod。具体每个实例的数量限制可以查看链接

但是在2021年7月份,亚马逊云科技官方发布了一个新的特性,来解决这个问题,让我们原本只能创建58个 Pod 的 m5.large实例可以创建110个 Pod,甚至更多!

确保我们的 VPC CNI 的版本

要使用这个新的功能,首先我们需要确保我们的 VPC CNI 的版本要高于 1.9.0,首先使用以下命令查看目前的版本。

kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2


我们看到目前集群的 VPC CNI 版本是 1.75,接下来我们先做一下升级。

curl -o aws-k8s-cni.yaml https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/release-1.9/config/v1.9/aws-k8s-cni.yaml

kubectl apply -f aws-k8s-cni.yaml

升级后再查看 VPC CNI 版本,已经是1.90了。

开启 Prefix 这个新功能。

kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true
kubectl set env ds aws-node -n kube-system WARM_PREFIX_TARGET=1

创建一个新的节点组,我们先定义最大 Pod 数量是110个。前面我们用了一个 YAML 文件创建节点组,这次我们直接用 eksctl 命令来创建节点组。

eksctl create nodegroup \
  --cluster cluster-demo \
  --region us-east-1 \
  --name ng-3-workers \
  --node-type m5.xlarge \
  --nodes 1 \
  --managed \
  --node-private-networking \
  --max-pods-per-node 110

创建好新的节点之后,我们通过命令 kubectl describe node <Node_NAME> 可以看到我们的 Pod 限制已经生效了。

这里定义的数量是110个,也是开源社区比较推荐的一个节点运行 Pod 数量的最佳实践,当然,也可以根据自己的需要调大这个数值。

每一个 Pod 绑定单独的公网地址

在一些场景中,我们可能需要让每一个 Pod 绑定单独的公网地址(即一个公网地址不被多个 Pod 共享)或者甚至是每一个 Pod 绑定一个固定的公网地址。比如一些电商的场景,会用到类似功能。

要做到这个效果,首先我们需要了解到每一个 Pod 都是对应一个 ENI 网卡的 Secondary IP 的,但是针对每一个 Secondary IP,我们可以绑定一个 EIP,来达到每一个 Pod 都能分配一个单独的公网 IP 的效果。

首先保证 Pod 需要在一个公有子网(即这个子网有一个默认路由去往 Internet 网关),这样它才能路由出去,如果 Pod 在私有子网,那么它对外的 IP 地址永远是 NAT 网关的 IP 地址。因为前面的测试中,我们的 Pod 是在私有子网的,我们需要先将其变成公有子网。这里为了方便起见,我们重新创建一个公有子网的 EKS 集群,直接把节点启动在原来创建的公有子网就可以了。

创建文件cluster-demo-2.yaml,并且输入如下内容,创建新的集群 cluster-demo-2。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: cluster-demo-2
  region: us-east-1

kubernetesNetworkConfig:
  serviceIPv4CIDR: 172.17.0.0/24

vpc:
  id: "vpc-0b051aa1e4d39f9c3"
  cidr: "172.16.0.0/16" 
  subnets:
    public:
      us-east-1a:
        id: "subnet-0bac7fc918a4a3383"
        cidr: "172.16.100.0/24"
      us-east-1b:
        id: "subnet-025da2132adf0b740"
        cidr: "172.16.101.0/24"

managedNodeGroups:
  - name: ng-1-workers
    labels: { role: workers }
    instanceType: m5.xlarge
    desiredCapacity: 2
    volumeSize: 80

然后执行

eksctl create cluster --config-file=cluster-demo-2.yaml

然后,我们需要将 External SNAT(Source Network Address Translation) 关闭掉,原因是如果 Pod 的流量要离开 VPC 网络(比如去往互联网,其他 VPC,或者本地 IDC),那么 SNAT 会将这个 Pod 的源 IP 地址转换成这个 Pod 所在的 ENI 上的 Primary IP 地址。因此如果不关闭 SNAT 功能,Pod 去访问互联网的话,会默认使用这个 Pod 所在节点的 Public IP 地址或者 EIP 地址。

使用如下命令来关闭 External SNAT:

kubectl set env daemonset -n kube-system aws-node AWS_VPC_K8S_CNI_EXTERNALSNAT=true

External SNAT 关闭后,我们删除原来的节点组,重新创建一个新的节点组,然后启动一些新的 Pod 进行测试。

eksctl delete nodegroup --cluster cluster-demo-2 --name ng-1-workers

eksctl create nodegroup \
  --cluster cluster-demo-2 \
  --region us-east-1 \
  --name ng-2-workers \
  --node-type m5.xlarge \
  --nodes 1 \
  --managed

kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1

可以看到我们的 Pod 获得了 IP 地址 172.16.100.177,接着我们去到 EC2 控制台的 ENI 界面找到这个 IP 地址。


然后我们给这个地址绑定一个弹性 IP(EIP),选中这个网卡,点击 Actions → Associate Address 即可。


关联好后,我们试下进去 Pod 查看这个公有 IP 地址是否生效吧。

通过命令 kubectl get pods -A -o wide 我们可以找到运行的 Pod 的名字,然后通过kubectl exec kubernetes-bootcamp-57978f5f5d-qp9hg — curl cip.cc 来进入到容器查看自己的一个公网 IP 地址。


我们发现,这个容器已经对外是用 34.196.240.56 这个公网 IP 地址了,这个就是我们刚才关联的 EIP。

除了以上的方式之外,我们还可以通过 AWS CLI 的方法来关联 EIP 和我们的 Pod。这样,我们可以做到不同的 Pod 对外的公网地址是不一样的,或者我们可以根据业务的逻辑来对不同的应用绑定不同的 IP 地址。

一个 Pod 如何绑定多张网卡

默认情况下,一个 Pod 会绑定一个 VPC 的 ENI 网卡,并消耗一个 Secondary IP 地址,如果需要一个 Pod 绑定多个地址或者网卡的话,需要用到 Multus 的 CNI 插件,具体可以查看官方文档,在这里就不再详细展开了。通过 Multus 插件结合上面一小节讲解的公网 IP 映射方法,我们也可以做到一个 Pod 绑定多个公网 IP 地址。

总结

通过这篇博客,我们了解到了在多区域,多 VPC 或者是混合云的容器场景中,我们往往需要对 K8S 网络进行提前的规划,以防止未来需要紧急扩容的情况。对不同 K8S 环境,我们可以自定义 Service 服务网络,Node 节点网络,Pod 容器网络,以及每一个 Pod 挂单独的公有 IP 地址甚至是多个公有 IP。另外,通过在 VPC 内附加新的网段,或者利用新的 EKS IP Address Prefix 功能,我们可以在有限的网段或者有限的节点上跑更多数量的 Pod。

参考文献

本篇作者

肖培庆

亚马逊AWS解决方案架构师,负责基于AWS的云计算方案架构的咨询和设计,同时致力于AWS云服务在国内的应用和推广。现主要负责初创企业行业解决方案,曾任职于IBM,联想,Avnet,多年大型企业网络架构和运维经验。