Amazon VPC CNI plugin for KubernetesのソースコードリーディングでEKSのネットワーキングについて理解を深める #3 (Last)

## はじめに

この記事は[Amazon EKS #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/amazon-eks-2) 18日目の記事です。

今回は **#3** になります。前回からの続きになりますので、先に以下の記事をご覧ください。

1. [Amazon VPC CNI plugin for KubernetesのソースコードリーディングでEKSのネットワーキングについて理解を深める #1 ](https://qiita.com/jlandowner/items/fec3cc33bc754282db11)
2. [Amazon VPC CNI plugin for KubernetesのソースコードリーディングでEKSのネットワーキングについて理解を深める #2 ](https://qiita.com/jlandowner/items/e911dae7d4374c260501)
3. Amazon VPC CNI plugin for KubernetesのソースコードリーディングでEKSのネットワーキングについて理解を深める #3　（**イマココ**）


長くなりましたが、これで終わりです。
最後にkubernetes本体(kubelet)との架け橋となる、CNIプラグインを見ていきます。

## Go2： aws-cni (CNIプラグイン)

CNIプラグインはコマンドラインモジュールで、kubeletからキックされます。
CNIの仕様に沿ってパラメータでPodの情報を受け取るので、L-IPAMDから割り当て可能なIPアドレスを取得後、ネットワーク設定を行ってIPアドレス情報をkubeletに返却する、といった役割となります。

CNIプラグインのメインルーチンは`cni.go`で、実際にネットワーク設定を行うのは`driver.go`というモジュールです。

### cni.go
ソースはこちら。 -> [amazon-vpc-cni-k8s/plugins/routed-eni/cni.go](https://github.com/aws/amazon-vpc-cni-k8s/blob/master/plugins/routed-eni/cni.go)

`cni.go`は本家CNI([containernetworking](https://github.com/containernetworking))のスケルトンパッケージを元に作られていて、addとdelという２つのファンクションを実装しています。

```go:cni.go#L332
e := skel.PluginMainWithError(cmdAdd, cmdDel, cniSpecVersion.All)
```

>package skel
>https://godoc.org/github.com/containernetworking/cni/pkg/skel

>Package skel provides skeleton code for a CNI plugin. In particular, it implements argument parsing and validation.
~~
>PluginMainWithError is the core "main" for a plugin. It accepts callback functions for add, check, and del CNI commands and returns an error.

>The caller must also specify what CNI spec versions the plugin supports.

大体addとdelはおんなじだと思うので、Pod作成時に呼ばれる`cmdAdd`を見てみます。
流れとしては、「**PodのIPを取得 -> 取得したIPでPodのネットワークを設定**」というシンプルなものです。

まずgRPCクライアントでL-IPAMDに対してAddNetworkRequestでIPアドレスを要求します。

```go:cni.go#L107~
conn, err := grpcClient.Dial(ipamDAddress, grpc.WithInsecure())
defer conn.Close()
c := rpcClient.NewCNIBackendClient(conn)
r, err := c.AddNetwork(context.Background(),
	&pb.AddNetworkRequest{
		Netns:                      args.Netns,
		K8S_POD_NAME:               string(k8sArgs.K8S_POD_NAME),
		K8S_POD_NAMESPACE:          string(k8sArgs.K8S_POD_NAMESPACE),
		K8S_POD_INFRA_CONTAINER_ID: string(k8sArgs.K8S_POD_INFRA_CONTAINER_ID),
		IfName:                     args.IfName})

addr := &net.IPNet{
	IP:   net.ParseIP(r.IPv4Addr),
	Mask: net.IPv4Mask(255, 255, 255, 255),
}
```
`args.XXX`はkubeletから起動時に渡されるPodの情報です。
続いて、取得したIPでネットワーク設定を行います。

```go:cni.go#L193
err = driverClient.SetupNS(hostVethName, args.IfName, args.Netns, addr, int(r.DeviceNumber), r.VPCcidrs, r.UseExternalSNAT, mtu)
```

あとは、設定したPodのIPアドレス情報をkubeletに返すだけです。

### driver.go

`driver.go`は`cni.go`と同じ階層のディレクトリ driver の中にあります。
こちら -> [amazon-vpc-cni-k8s/plugins/routed-eni/driver/driver.go](https://github.com/aws/amazon-vpc-cni-k8s/blob/master/plugins/routed-eni/driver/driver.go)

ネットワーク設定の流れは、前回引用したドキュメントの以下の通りです。

>CNI Plugin Sequence
>https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md#cni-plugin-sequence

>1. create a veth pair and have one veth on host namespace and one veth on Pod's namespace
>2. Get an Secondary IP address assigned to the instance and perform following inside Pod's name space:
>3. Assign this IP address to Pod's eth0
>4. Add default gateway and default route to Pod's route table
>5. Add a static ARP entry for default gateway
>6. On host side, add host route so that incoming Pod's traffic can be routed to Pod.

ソースコードでも上記の流れで作っているのが確認でき、ソースも長いので今回一つ一つのファンクションを追っていくのは行いません。ただ、実際のネットワーク設定はほぼ外部ライブラリを利用しているので、今回はそのライブラリを紹介したいと思います。
import文をみると、ラップしているものの含めて、ネットワーク周りのライブラリは以下の５つが利用されています。

- net
- github.com/vishvananda/netlink
- github.com/coreos/go-iptables/iptables
- github.com/containernetworking/cni/pkg/ns
- github.com/containernetworking/cni/pkg/ip

前回のOperator-sdkのバージョンが古い点を指摘しましたが、CNIについてもかなり古いです。。
特に、github.com/containernetworking/cni/pkg/nsとipはすでに存在していません。

#### `net`
[Goの標準ライブラリ](https://golang.org/pkg/net/#IP)ですが、`net.IP`でAmazon VPC CNIプラグインではIPアドレスを処理する程度しか使っていなそうです。

#### `github.com/vishvananda/netlink`
https://godoc.org/github.com/vishvananda/netlink
https://github.com/vishvananda/netlink

helokuのvishvanandaという方の個人ライブラリっぽいです。スター数も多く、かなり昔にコミットされている物もあるので、Goではメジャーなライブラリなのでしょうか。

Amazon VPC CNIプラグインのveth等のPodのネットワーク設定のほぼ全てをこちらのライブラリで行っています。Linuxのipコマンドに相当する機能です。

ネットワーク設定の一番コアな部分が外部の個人ライブラリというところがOSSらしい感じですね(適当)

#### `github.com/coreos/go-iptables/iptables`
https://godoc.org/github.com/coreos/go-iptables/iptables
https://github.com/coreos/go-iptables

こちらはcoreosのiptablesライブラリです。Godocでiptabesと検索するといくつか出てきますが、何がメジャーなのかはよくわかりません。 -> https://godoc.org/?q=iptables
ただkubernetes本体では、kube-proxyがiptablesを操作するときは、kubenrnetesのpkg/util内の[k8s.io/kubernetes/pkg/util/iptables](https://godoc.org/k8s.io/kubernetes/pkg/util/iptables)というライブラリを利用しています。


#### `github.com/containernetworking/cni/pkg/ns`

本家CNI([containernetworking](https://github.com/containernetworking))のpkgです。

ただし、go.modを見るとcni:v0.5.2を利用していることが分かりますが、これは2017年4月にリリースされたものです。
https://github.com/containernetworking/cni/releases/tag/v0.5.2

現在はすでにcniのリポジトリにはなく、pluginという別リポジトリにあります（結構探しました。笑）

https://godoc.org/github.com/containernetworking/plugins/pkg/ns
https://github.com/containernetworking/plugins/blob/master/pkg/ns

こちらは主にPod内のネットワーク設定をする際に、利用されています。
README.mdにGoにおけるネットワーク名前空間とスレッドについて解説がありました。

>Namespaces, Threads, and Go
>https://github.com/containernetworking/plugins/blob/master/pkg/ns/README.md
>On Linux each OS thread can have a different network namespace. Go's thread scheduling model switches goroutines between OS threads based on OS thread load and whether the goroutine would block other goroutines. This can result in a goroutine switching network namespaces without notice and lead to errors in your code.

goでネットワーク名前空間を切り替えを安全に行ってくれるもののようです。
driver.goでは、Pod内のネットワーク名前空間に設定する部分は`run()`というファンクション`ns.Do()`に渡す形で実行しています。

以下ネットワーク設定部分以外だいぶ端折ってますが、`run()`の中身です。

```go:driver.go#84~
// run defines the closure to execute within the container's namespace to create the veth pair
func (createVethContext *createVethPairContext) run(hostNS ns.NetNS) error {
  
  err := createVethContext.netLink.LinkAdd(veth)

  // Add a connected route to a dummy next hop (169.254.1.1)
  // # ip route show
  // default via 169.254.1.1 dev eth0
  // 169.254.1.1 dev eth0
  gw := net.IPv4(169, 254, 1, 1)
  gwNet := &net.IPNet{IP: gw, Mask: net.CIDRMask(32, 32)}
  err = createVethContext.netLink.RouteReplace(&netlink.Route{
    LinkIndex: contVeth.Attrs().Index,
    Scope:     netlink.SCOPE_LINK,
    Dst:       gwNet});

  // Add a default route via dummy next hop(169.254.1.1). Then all outgoing traffic will be routed by this
  // default route via dummy next hop (169.254.1.1).
  err = createVethContext.ip.AddDefaultRoute(gwNet.IP, contVeth);
  err = createVethContext.netLink.AddrAdd(contVeth, &netlink.Addr{IPNet: createVethContext.addr});

  // add static ARP entry for default gateway
  // we are using routed mode on the host and container need this static ARP entry to resolve its default gateway.
  neigh := &netlink.Neigh{
    LinkIndex:    contVeth.Attrs().Index,
    State:        netlink.NUD_PERMANENT,
    IP:           gwNet.IP,
    HardwareAddr: hostVeth.Attrs().HardwareAddr,
  }
  err = createVethContext.netLink.NeighAdd(neigh); 

  // Now that the everything has been successfully set up in the container, move the "host" end of the
  // veth into the host namespace.
  err = createVethContext.netLink.LinkSetNsFd(hostVeth, int(hostNS.Fd()));
}
```

#### `github.com/containernetworking/cni/pkg/ip`
https://godoc.org/github.com/containernetworking/plugins/pkg/ip
https://github.com/containernetworking/plugins/tree/master/pkg/ip

こちらも現在は本家CNI([containernetworking](https://github.com/containernetworking))のcniではなく、pluginsというリポジトリにありますのでURLが異なります。

Amazon VPC CNIプラグインのリポジトリでは、`AddDefaultRoute()`というファンクションしか利用していなそうです。 -> 
 https://github.com/containernetworking/plugins/blob/master/pkg/ip/route_linux.go

```go:route_linux.go
package ip

import (
	"net"

	"github.com/vishvananda/netlink"
)

// AddDefaultRoute sets the default route on the given gateway.
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
	_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
	return AddRoute(defNet, gw, dev)
}
```

なんとこちらでも`github.com/vishvananda/netlink`を使っていました。笑


## 終わりに

全体を見渡して見ると、Amazon VPC CNI pluginは利用しているライブラリが非常に古くなっており、[前回紹介した通り](https://qiita.com/jlandowner/items/e911dae7d4374c260501#next-generation-aws-vpc-cni-plugin)、アーキテクチャの改善も含めた抜本的な改善に向かっていることを改めて感じました。

ただそれでも、EKSのネットワーキングやKubernetes、Goの理解を深めるには十分な収穫がありました。

普段は利用するだけのものかもしれませんが、折角のOSSなので、実際にソースコードを読んだり、自分でビルドしたりするだけでも、かなり理解が深まり、トラブルシューティング力は大きく向上すると思います。

記事的には全然まとまっていない気がしますが、個人的には記事にできてよかったです。
最後までご覧いただきありがとうございました。