手动搭建 AWS K8S CNI 网络环境
{Back to Index}
Table of Contents
1 什么是 AWS CNI
通常 K8S 使用一组内部 IP 分配给 Pod ,Node 之间使用隧道技术封装内部 IP 报文,实现跨主机的 Pod 通信。
AWS 通过 amazon-vpc-cni-k8s
插件为 Pod 赋予一个 VPC 子网(Subnet)内的地址,让 AWS 负责路由。
看上去,Pod 就好像是 VPC 内的真实主机实例,有自己"真正的 IP " 。
背后的组网原理其实并不复杂,即为主机配置多个弹性网络接口(ENI) ,每个接口可以有多个 Secondary IP ,从而获得来自 VPC 池的多个 IP 地址。 然后,将这些 IP 地址分配给主机上的 Pod ,并为 ENI 与 Pod 上创建的虚拟以太网端口 (veth) 之间建立路由关系,剩下的工作交由 Linux 内核完成。
Figure 1: CNI 网络架构
Figure 2: CNI 组网原理
2 Secondary IP 1
Secondary IP 用于为网络接口配置多个 IP ,AWS 会为接口上的所有 Secondary IP 在子网内建立 ARP 规则, 即网卡的所有 Secondary IP 会被 ARP 解析为该网卡的物理地址。 这样,发往某个 Pod 的数据包(目标地址为 Secondary IP )就可以到达对应主机上对应的 ENI 上了。
表面看来它的功能比较简单,但 Secondary IP 应该是实现 CNI 的关键技术,因为:
- 地址由 AWS 集中 管理,即子网内所有可用于分配的 IP
- Secondary IP 可以不用显式得绑定在网卡上 (这是实现主机内 IP 转发的关键)。
3 实验环境
基于 AWS CNI 插件背后的原理,来搭建一个简易的网络环境,使得 Pod 拥有子网内的 IP ,并可以在别的实例上直接访问。
Figure 3: 实验网络拓扑
4 实验步骤
4.1 创建实例
- Image: amzn2-ami-hvm-2.0.20190618-x86_64-gp2
- Subnet: 172.31.0.0/20
4.1.1 实例1
Instance ID | Primary IP (eth0) | ENI (eth1) |
---|---|---|
i-068b01ca732d81883 | 172.31.15.228/20 | eni-071c075bca738408f |
4.1.2 实例2
Instance ID | Primary IP (eth0) |
---|---|
i-086a5d86d378ea742 | 172.31.3.123/20 |
4.2 分配 Secondary IP
使用下面的命令行为 instance1 添加 Secondary IP:
aws ec2 assign-private-ip-addresses --network-interface-id eni-071c075bca738408f --secondary-private-ip-address-count 1
在 instance1 上观察:
[ec2-user@ip-172-31-15-228 ~]$ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000 link/ether 02:55:73:a0:ca:54 brd ff:ff:ff:ff:ff:ff inet 172.31.15.228/20 brd 172.31.15.255 scope global dynamic eth0 valid_lft 2559sec preferred_lft 2559sec inet6 fe80::55:73ff:fea0:ca54/64 scope link valid_lft forever preferred_lft forever 3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000 ---> 172.31.13.130 没有绑定到该接口 link/ether 02:b1:08:c7:0c:54 brd ff:ff:ff:ff:ff:ff inet 172.31.3.173/20 brd 172.31.15.255 scope global dynamic eth1 valid_lft 2565sec preferred_lft 2565sec inet6 fe80::b1:8ff:fec7:c54/64 scope link valid_lft forever preferred_lft forever
在 instance2 上观察:
[root@ip-172-31-3-123 ec2-user]# ip n flush all [root@ip-172-31-3-123 ec2-user]# ping -c 1 172.31.13.130 PING 172.31.13.130 (172.31.13.130) 56(84) bytes of data. --- 172.31.13.130 ping statistics --- 1 packets transmitted, 0 received, 100% packet loss, time 0ms [root@ip-172-31-3-123 ec2-user]# ip n 172.31.13.130 dev eth0 lladdr 02:b1:08:c7:0c:54 REACHABLE ---> mac of eth1 on instance1 172.31.0.1 dev eth0 lladdr 02:85:e2:5f:9b:aa REACHABLE [root@ip-172-31-3-123 ec2-user]#
4.3 创建 veth pair
ip netns add ns1 ip link add veth-1 type veth peer name veth-1c ip link set veth-1c netns ns1 ip link set veth-1 up ip netns exec ns1 ip link set veth-1c up
4.4 配置 IP ,网关和路由
ip route add 172.31.13.130/32 dev veth-1 # 将目标地址为 Secondary IP 的报文路由至 veth-1 ip netns exec ns1 ip addr add 172.31.13.130/32 dev veth-1c # 配置 IP ip netns exec ns1 ip route add 172.31.0.1 dev veth-1c # 配置 网关 ip netns exec ns1 ip route add default via 172.31.0.1 dev veth-1c ip netns exec ns1 arp -i veth-1c -s 172.31.0.1 e6:cf:a4:fb:0a:4f # 为网关配置静态 ARP (veth-1's mac)
为网关配置静态 ARP 是因为数据会通过 veth pair 发送到 veth-1 上,只有报文的 MAC 与 veth-1 相同, 才会被 veth-1 接收,从而实现在根命名空间中的 IP 转发功能。
4.5 开启 IP Forward
systemctl restart network # 1 ip a del 172.31.13.130/20 dev eth1 # 2 echo 1 > /proc/sys/net/ipv4/ip_forward # 3
这里有个地方要注意下,开启 IP 转发功能理论上只要执行 #3 即可,但是实验发现必须先重启网络服务 (#1) 才能生效。 但是一旦重启网络服务后,Secondary IP 会出现在 eth1 上,这样内核又认为不需要转发,所以需要再额外执行 #2 。
也可以在重启网络前使用下面的命令,避免自动更新网卡上的 IP 地址:2
sed -i -e 's/^EC2SYNC=yes/EC2SYNC=no/' /etc/sysconfig/network-scripts/ifcfg-eth1
4.6 连通性分析
4.6.1 Pod to Pod
Figure 4: Life of a Pod to Pod Ping Packet
4.6.2 Pod to External
为使得在 namespace ns1 中能访问外网,必须借助 iptables 配置 SNAT (使用实例 Primary ENI 上的 Primary IP ):
iptables -A POSTROUTING ! -d 172.31.0.0/20 -m addrtype ! --dst-type LOCAL -j SNAT --to-source 172.31.15.228
注意,必须确保访问外网的数据要从 eth0 出去:
[root@ip-172-31-15-228 ec2-user]# ip rule list 0: from all lookup local 32763: from 172.31.13.130 lookup 10001 ---> 从 172.31.13.130 来的数据由表 10001 决定路由 32764: from 172.31.11.77 lookup 10001 32765: from 172.31.3.173 lookup 10001 32766: from all lookup main 32767: from all lookup default [root@ip-172-31-15-228 ec2-user]# ip r show table 10001 default via 172.31.0.1 dev eth0 ---> 这里必须是 eth0 172.31.0.0/20 dev eth1 proto kernel scope link src 172.31.3.173
Figure 5: Life of a Pod to External Packet