MultusによるKubernetesクラスタ外からのパケット転送実験

Multus

最近になってMultusというKubernetes CNIの一つを知った。Kubernetes環境ではPodは通常1つのNICのみを持つが、Multusを使うことでPodに複数のNICを持たせることが可能になる。

上記情報源より簡単にその特徴を挙げる。詳細な説明は各記事を参照のこと。(2つの日本語記事はとても分かりやすくまとめられていて助かります。)

  • intel社のOSSであり、コンテナ環境でNFVを実現するための課題 (複数NIC、通信の分離や高速化など) を解決する一手法
  • Kubernetesで管理されるコンテナがホストサーバーのNICを介して通信高速化機能 (DPDKやSR-IOVなど) を利用することができる
  • Multus自体は「Delegating CNIプラグイン」であり、別途CNIプラグインの存在を前提とし、それらに対してネットワーク設定をdelegateする形でNICを設定する
  • Multusが作成するNICKubernetes側では感知されない
  • Multusが付与する複数NICの設定はNetworkAttachmentDefinitionというCRDに準拠する
  • Multusと同じ「Delegating CNIプラグイン」としてはCNI-Genieがある
  • Contrail CNIでも複数のNIC追加ができるがこちらは非Delegatingプラグインらしい

Podに複数のNICを付与する標準はKubernetes Network Plumbing Working Groupで定義されているらしい。

実験

Multusを用いてルーティングソフトウェア (FRR) が動作するPodにNICを追加し、そのNICを経由してKubernetesクラスタの外からのパケットをルーティング・フォワーディングするという実験を行う。本当は色々なNetwork FunctionをPodとして追加したりするともっと面白そう。

環境と構成

下図の通り、macOS上のminikubeにFRRとMultusをデプロイする。

f:id:nstgt7:20210206183929p:plain

  • macOS 10.15.7
  • VirtualBox 6.1.16
  • minikube v1.17.1
  • Kubernetes v1.20.2
  • CNI 0.3.1
  • CNI plugin v0.8.5
  • Multus v3.6
  • FRRouting (frr) 7.7-dev_git

1.基盤環境構築

macOSに2つのbridgeを用意する。以下のApple公式リンクに従いシステム環境設定からbridgeを作成する。

Macで仮想ネットワークインターフェイスのブリッジを設定する - Apple サポート

f:id:nstgt7:20210206153537p:plain

実際にやってみたところ、bridge作成後、適当なIPアドレスアサインするまでは ipconfig で認識されなかった。また、作成時に指定したブリッジ名は ipconfig 上では適用されていなかった。(既にbridge0が存在していたことから、自動的に連番になるものと推測。)

## macOS
$ ifconfig bridge1
bridge1: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500
    options=63<RXCSUM,TXCSUM,TSO4,TSO6>
    ether f2:18:98:45:d8:01
    Configuration:
        id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
        maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
        root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
        ipfilter disabled flags 0x0
    Address cache:
    nd6 options=201<PERFORMNUD,DAD>
    media: <unknown type>
    status: inactive
$ ifconfig bridge2
bridge2: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500
    options=63<RXCSUM,TXCSUM,TSO4,TSO6>
    ether f2:18:98:45:d8:02
    Configuration:
        id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
        maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
        root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
        ipfilter disabled flags 0x0
    Address cache:
    nd6 options=201<PERFORMNUD,DAD>
    media: <unknown type>
    status: inactive

続いてminikubeを用意する。今回はVirtualBoxVMで扱いたいため --driver='virtualbox' を指定。またCNIプラグインとしてciliumを採用する。

## macOS
$ minikube start -p minikube-vbox --driver='virtualbox'  --cni='cilium' \
    --container-runtime=containerd --kubernetes-version='stable'

kubectlの接続を確認したら一度minikube VMを落とす。minikube VMに作成したbridge 2つをNICとして設定したいが、起動時にそれをするオプションはないためVM完成後にVirtualBoxGUIから実施する。

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.2", GitCommit:"faecb196815e248d3ecfb03c680a4507229c2a56", GitTreeState:"clean", BuildDate:"2021-01-13T13:28:09Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.2", GitCommit:"faecb196815e248d3ecfb03c680a4507229c2a56", GitTreeState:"clean", BuildDate:"2021-01-13T13:20:00Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"}

$ minikube -p minikube-vbox stop

minikube VMsshNICを確認する。NICのプロミスキャスモードを有効化し、NICMACアドレスVirtualBox GUIで表示されていたMACアドレスと一致していることを確認する。

## macOS
$ minikube -p minikube-vbox start
$ minikube -p minikube-vbox ssh
## minikube VM
$ sudo ip link set dev eth2 promisc on
$ sudo ip link set dev eth3 promisc on

$ ip addr show eth2
4: eth2: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:b8:f0:85 brd ff:ff:ff:ff:ff:ff
$ ip addr show eth3
5: eth3: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:13:a1:e9 brd ff:ff:ff:ff:ff:ff

2.Multusのデプロイ

Multusのquickstartに従う。MultusのPodとCRD (NetworkAttachmentDefinition) がデプロイされている。

## macOS
$ git clone https://github.com/intel/multus-cni.git && cd multus-cni
$ cat ./images/multus-daemonset.yml | kubectl apply -f -

$ kubectl get pods -A -l app=multus
NAMESPACE     NAME                         READY   STATUS    RESTARTS   AGE
kube-system   kube-multus-ds-amd64-dpr7z   1/1     Running   0          19s

$ kubectl get crd network-attachment-definitions.k8s.cni.cncf.io
NAME                                             CREATED AT
network-attachment-definitions.k8s.cni.cncf.io   2021-02-06T07:45:31Z

Nodeであるminikube VMにはMultus用の設定が保存されている。

## minikube VM
$ sudo jq . /etc/cni/net.d/00-multus.conf
{
  "cniVersion": "0.3.1",
  "name": "multus-cni-network",
  "type": "multus",
  "kubeconfig": "/etc/cni/net.d/multus.d/multus.kubeconfig",
  "delegates": [
    {
      "cniVersion": "0.3.1",
      "name": "cilium",
      "type": "cilium-cni",
      "enable-debug": false
    }
  ]
}

MultusがデプロイされるとNodeの /opt/cni/bin ディレクトリにプラグインのバイナリが配置される。今回IPアドレスを静的に設定したいと考えたが、使用したい staticより同バージョンのバイナリをダウンロードして直接配置することにした。

## minikube VM
$ ls /opt/cni/bin
bandwidth  bridge  cilium-cni  cnitool  dhcp  firewall  flannel  host-local  ipvlan  loopback  macvlan  multus  portmap  ptp  tuning  vlan
$ ls /opt/cni/bin/static
ls: cannot access '/opt/cni/bin/static': No such file or directory
## macOS
$ mkdir tmp
$ wget -P ./tmp https://github.com/containernetworking/plugins/releases/download/v0.8.5/cni-plugins-linux-amd64-v0.8.5.tgz
$ cd tmp && tar zxvf cni-plugins-linux-amd64-v0.8.5.tgz

# minikube VMにバイナリを転送するためmountする (他にもっと良い方法あるかも)
$ minikube -p minikube-vbox mount ./:/mnt

## minikube VM
$ sudo cp /mnt/static /opt/cni/bin/
$ /opt/cni/bin/static version
CNI static plugin v0.8.5

## macOS
# minikube mountを停止しておく

3. Kubernetesリソースのデプロイ

今回はmacvlanプラグインを使用してFRRのPodにL2のネットワークを提供する。

下記のマニフェストにより2つのNetworkAttachmentDefinitionリソースをデプロイする。

  • ホスト (ここではminikube VM) に接続したNICを指定するために type: macvlanmode: bridge 指定
  • NICの指定は master で行う
  • promiscMode: true を指定
  • IPアドレスアサインはPod側のannotationで任意に指定することにしたため、ipam設定にて type: static を指定
## macOS
$ cat <<EOF | kubectl apply -f -
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: bridge-conf-1
spec:
  config: '{
            "cniVersion": "0.3.1",
            "type": "macvlan",
            "master": "eth2",
            "mode": "bridge",
            "promiscMode": true,
            "ipam": {
                "type": "static"
            }
          }'
---
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: bridge-conf-2
spec:
  config: '{
            "cniVersion": "0.3.1",
            "type": "macvlan",
            "master": "eth3",
            "mode": "bridge",
            "promiscMode": true,
            "ipam": {
                "type": "static"
            }
          }'
EOF

$ kubectl get network-attachment-definition
NAME            AGE
bridge-conf-1   15s
bridge-conf-2   15s

FRRのPodをデプロイする。FRRはZebraを有効化するために securityContext にて privileged: true を指定する必要がある。

(今回は実験用なのでこれでよいが、実際にはNodeを守るための手段を講じる必要がある。特にKubernetesが管理しないNICが追加される今回のようなPodではなおさら必要性が増すと考える。)

Podのannotationでは上記で作成したNetworkAttachmentDefinitionリソース (bridge-conf-1, bridge-conf-2) とそのbridgeで使用するIPアドレスをそれぞれ指定している。

## macOS
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: frr
  annotations:
     k8s.v1.cni.cncf.io/networks: |
      [
        {
          "name": "bridge-conf-1",
          "ips": ["10.0.10.1/24", "2001:db8:10::1/64"]
        },
        {
          "name": "bridge-conf-2",
          "ips": ["10.0.20.1/24", "2001:db8:20::1/64"]
        }
      ]
spec:
  containers:
  - name: frr
    image: frrouting/frr:latest
    securityContext:
      privileged: true
EOF

$ kubectl get pod frr
NAME   READY   STATUS    RESTARTS   AGE
frr    1/1     Running   0          80s

FRR Podでネットワークを確認すると net1, net2 としてインターフェースが作成されていることが分かる。なお eth0Kubernetesネットワークに接続するための通常のNICである。

## macOS
$ kubectl exec frr -- ip addr show
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: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0
3: net1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 76:97:54:32:af:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.10.1/24 brd 10.0.10.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 2001:db8:10::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::7497:54ff:fe32:af03/64 scope link
       valid_lft forever preferred_lft forever
4: net2@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 7a:e6:74:b5:3f:8f brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.20.1/24 brd 10.0.20.255 scope global net2
       valid_lft forever preferred_lft forever
    inet6 2001:db8:20::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::78e6:74ff:feb5:3f8f/64 scope link
       valid_lft forever preferred_lft forever
43: eth0@if44: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 4a:c4:21:45:63:91 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.24/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::48c4:21ff:fe45:6391/64 scope link
       valid_lft forever preferred_lft forever

4.疎通試験用VMの用意

bridgeに接続された適当なVMを2つ用意する。

(本当はコンテナで実施したかったが、docker for macでは host タイプのネットワークを1つしか作成できないらしく断念)

## macOS
$ cat Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.hostname = "vm1"
  config.vm.box = "ubuntu/xenial64"
  config.vm.network "public_network", bridge: "bridge1", ip: "10.0.10.10"
end

$ vagrant up
$ vagrant ssh

## VM1
$ ip addr show
(snip)
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:da:b2:d8 brd ff:ff:ff:ff:ff:ff
    inet 10.0.10.10/24 brd 10.0.10.255 scope global enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:feda:b2d8/64 scope link
       valid_lft forever preferred_lft forever

# この時点でFRRコンテナと通信が可能
$ ping 10.0.10.1 -c 3
PING 10.0.10.1 (10.0.10.1) 56(84) bytes of data.
64 bytes from 10.0.10.1: icmp_seq=1 ttl=64 time=1.26 ms
64 bytes from 10.0.10.1: icmp_seq=2 ttl=64 time=0.263 ms
64 bytes from 10.0.10.1: icmp_seq=3 ttl=64 time=0.279 ms

--- 10.0.10.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 0.263/0.601/1.262/0.467 ms

# せっかくなのでIPv6アドレスの設定も
$ sudo ip address add 2001:db8:10::10/64 dev enp0s8

# 疎通試験のためにstatic routeを追加
$ sudo ip route add 10.0.20.0/24 via 10.0.10.1
$ sudo ip route add 2001:db8:20::/64 via 2001:db8:10::1

VM2も同様に設定する。

## macOS
$ cat Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.hostname = "vm2"
  config.vm.box = "ubuntu/xenial64"
  config.vm.network "public_network", bridge: "bridge2", ip: "10.0.20.20"
end

$ vagrant up
$ vagrant ssh

## VM2
$ sudo ip address add 2001:db8:20::20/64 dev enp0s8
$ sudo ip route add 10.0.10.0/24 via 10.0.20.1
$ sudo ip route add 2001:db8:10::/64 via 2001:db8:20::1

5.疎通試験

FRRを設定する。

## macOS
$ kubectl exec -it frr bash

## FRR Pod
$ vtysh

frr# conf t
frr(config)# interface net1
frr(config-if)# ip address 10.0.10.1/24
frr(config-if)# ipv6 address 2001:db8:10::1/64
frr(config-if)# no shutdown
frr(config-if)# exit
frr(config)# interface net2
frr(config-if)# ip address 10.0.20.1/24
frr(config-if)# ipv6 address 2001:db8:20::1/64
frr(config-if)# no shutdown
frr(config-if)# exit
frr(config)# ip forwarding
frr(config)# ipv6 forwarding
frr(config)# end

疎通確認を行う。想定通りVM間で通信できていることが分かる。

f:id:nstgt7:20210206184026p:plain

## VM1
$ ping 10.0.20.20 -c 3
PING 10.0.20.20 (10.0.20.20) 56(84) bytes of data.
64 bytes from 10.0.20.20: icmp_seq=1 ttl=63 time=0.536 ms
64 bytes from 10.0.20.20: icmp_seq=2 ttl=63 time=1.34 ms
64 bytes from 10.0.20.20: icmp_seq=3 ttl=63 time=1.09 ms
--- 10.0.20.20 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.536/0.989/1.342/0.337 ms

$ ping6 2001:db8:20::20 -c 3
PING 2001:db8:20::20(2001:db8:20::20) 56 data bytes
64 bytes from 2001:db8:20::20: icmp_seq=1 ttl=63 time=0.531 ms
64 bytes from 2001:db8:20::20: icmp_seq=2 ttl=63 time=0.609 ms
64 bytes from 2001:db8:20::20: icmp_seq=3 ttl=63 time=1.24 ms
--- 2001:db8:20::20 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 0.531/0.794/1.242/0.318 ms

ちなみに仮想環境なのであまり意味はないが、通信帯域としては300Mbps程度であった。

## VM1
$ sudo iperf3 -c 10.0.20.20
(snip)
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth       Retr
[  4]   0.00-10.00  sec   365 MBytes   306 Mbits/sec  258             sender
[  4]   0.00-10.00  sec   363 MBytes   304 Mbits/sec                  receiver

所感

上記の通りKubernetesで管理されているコンテナでクラスタ外からのパケットルーティング・フォワーディングを簡単に行うことができた。Webアプリの動作など通常のKubernetesの使い方からするとかなりイレギュラーなことをやっている気持ちになった。今後コンテナベースのNetwork Function (CNF) が流行るのかどうか分からないが、一つ興味深いOSSだった。

一つのユースケースとしておうちKubernetesでご家庭のネットワーク機能を実現できたら面白そう。

一方でセキュリティについては意識する必要があると思う。Multusによって生えたNICKubernetes側に感知されないため、Kuberneteが提供するセキュリティ機能を利用することができないため、Firewallなどネットワークプレイヤーでのセキュリティを十分に担保する必要があると感じた。その辺りもVirtual Function的にPodで提供して対応するべきか。

kind 製 Kubernetes クラスタ内サービスへのアクセス (for macOS)

はじめに

kind を使うと複数 Node のクラスタが簡単に作成できて便利ですが、ホスト (macOS) 側から kind 上で提供されているサービスにどうアクセスするのか少し悩みました。

kind では docker コンテナ1つを1 Nodeとして Kubernetes クラスタを構築できますが、 Docker Desktop for Mac の制約 によりそもそもホストからコンテナへ IP で到達することができません。そのため kind で作成されたクラスタtype: NodePort の Service をデプロイしたとてホスト側からその Service にアクセスできないのです。

kind の Issue にも関連がありそうな話題が。

github.com

やってみよう

kind で作ったクラスタ内のサービスにアクセスするいくつかの方法を試します。
環境は以下の通り。

まずは kind でクラスタを作成します。

❯ cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
EOF

❯ export KUBECONFIG="$(kind get kubeconfig-path --name="kind")"
❯ kubectl get nodes
NAME                 STATUS   ROLES    AGE   VERSION
kind-control-plane   Ready    master   84s   v1.15.3
kind-worker          Ready    <none>   50s   v1.15.3
kind-worker2         Ready    <none>   50s   v1.15.3
kind-worker3         Ready    <none>   50s   v1.15.3

テスト用のアプリケーションをデプロイ

 ❯ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
EOF

❯ kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP           NODE           NOMINATED NODE   READINESS GATES
nginx-68c7f5464c-8b58k   1/1     Running   0          2m16s   10.244.2.6   kind-worker2   <none>           <none>
nginx-68c7f5464c-m6tz5   1/1     Running   0          2m16s   10.244.1.5   kind-worker3   <none>           <none>
nginx-68c7f5464c-tcnhc   1/1     Running   0          2m16s   10.244.3.7   kind-worker    <none>           <none>
❯ for PODNAME in `kubectl get pods -o jsonpath='{.items[*].metadata.name}'`; do
  kubectl exec -it ${PODNAME} -- cp /etc/hostname /usr/share/nginx/html/index.html;
done

HTTP でアクセスすると Pod 名が表示されます。

ホスト (macOS) から如何にしてクラスタ内で展開されるサービスを見ることができるでしょうか。思いついたものを試してみます。

kubectl port-forward

デバッグでよく使うやつ。ただし Pod に直接接続するため今回のようなケースではロードバランスされません。

❯ kubectl port-forward deploy/nginx 8080:80
## 別ターミナルにて
❯ curl 127.0.0.1:8080
nginx-68c7f5464c-8b58k
❯ curl 127.0.0.1:8080
nginx-68c7f5464c-8b58k
❯ curl 127.0.0.1:8080
nginx-68c7f5464c-8b58k

kind の extraPortMappings

kindの公式ドキュメント にある extraPortMappings を使うと hostPort に割当が可能なようです。

❯ cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
  extraPortMappings:
  - containerPort: 30000
    hostPort: 8080
EOF

## Deployment 作成とアプリの設定は前述したものと同様のコマンドを実行
❯ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: NodePort
  ports:
  - name: "http-port"
    protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 30000
  selector:
    app: nginx
EOF

❯ curl 0.0.0.0:8080
nginx-68c7f5464c-w4h7v
❯ curl 0.0.0.0:8080
nginx-68c7f5464c-rd6hs
❯ curl 0.0.0.0:8080
nginx-68c7f5464c-vl2r6

kind クラスタの config として3つ目の Node に extraPortMappings を設定し立ち上げました。 ホストのポート (extraPortMappings[*].hostPort: 8080) へのアクセスが Node のコンテナのポート (extraPortMappings[*].containerPort: 30000) に転送されます。そのため nodePort: 30000 で指定した NodePort タイプの Service を用意することでロードバランスも可能です。

❯ docker ps -a
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                                  NAMES
81b82a6df489        kindest/node:v1.15.3   "/usr/local/bin/entr…"   8 minutes ago       Up 8 minutes        53372/tcp, 127.0.0.1:53372->6443/tcp   kind-control-plane
6f9bf2c0662b        kindest/node:v1.15.3   "/usr/local/bin/entr…"   8 minutes ago       Up 8 minutes                                               kind-worker
0d5c32d65b11        kindest/node:v1.15.3   "/usr/local/bin/entr…"   8 minutes ago       Up 8 minutes                                               kind-worker2
8b955ef61737        kindest/node:v1.15.3   "/usr/local/bin/entr…"   8 minutes ago       Up 8 minutes        0.0.0.0:8080->30000/tcp                kind-worker3

実際に docker コンテナの状態を確認すると kind-worker3PORTS の項目にて 0.0.0.0:8080->30000/tcp されていることが分かります。

そのため以下のような記述をしても、1つ目の Node をデプロイした時点でホスト側の 8080 ポートは使用されてしまい、複数の Node に同じ extraPortMappings[*].hostPort を割り当てることはできません。

❯ cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
nodes:
- role: control-plane
- role: worker
  extraPortMappings:
  - containerPort: 30000
    hostPort: 8080
- role: worker
  extraPortMappings:
  - containerPort: 30000
    hostPort: 8080
- role: worker
  extraPortMappings:
  - containerPort: 30000
    hostPort: 8080
EOF

## エラーが発生
ERRO[00:46:47] docker: Error response from daemon: driver failed programming external connectivity on endpoint kind-worker2 (17e1142d52d7f985f716cf49a2fc77a2af235bb8eca3918218b1d900f64831aa): Bind for 0.0.0.0:8080 failed: port is already allocated.

つまりは NodePort と言いつつも実際にユーザからのトラフィックを受け取るのは1つの Node のみということになります。

kubectl-open-svc-plugin

@superbrothers さんが作成された kubectl-open-svc-plugin というプラグインを試します。 kubectl port-forward の ClusterIP 版だと理解しています。

qiita.com

これは、クラスタ外からアクセスできない ClusterIP タイプの Service にクラスタの外から簡単にアクセスするためのプラグインです。

kubectl plugin マネージャである krew を使えば簡単に導入可能でした。

❯ kubectl krew install open-svc
❯ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: nginx
EOF

❯ curl http://127.0.0.1:8080/api/v1/namespaces/default/services/nginx:http-port/proxy/
nginx-68c7f5464c-m6tz5
❯ curl http://127.0.0.1:8080/api/v1/namespaces/default/services/nginx:http-port/proxy/
nginx-68c7f5464c-tcnhc
❯ curl http://127.0.0.1:8080/api/v1/namespaces/default/services/nginx:http-port/proxy/
nginx-68c7f5464c-tcnhc
❯ curl http://127.0.0.1:8080/api/v1/namespaces/default/services/nginx:http-port/proxy/
nginx-68c7f5464c-8b58k

生成された URL にアクセスすると ClusterIP によるロードバランスが確認できます。
なんて便利!色々と使い所がありそうです。

おわり

kind や minikube など手元で試せる Kubernetes にデプロイしたサービスにアクセスする方法はどれもそのツールの実装方法に依存しており、なかなかに直感的でないように感じています。今回の話だと macOS ではなく Linux を使えばシンプルに解決するとも言えますが。 kubectl-open-svc-plugin のような補助ツールがあることでマルチクラスタにおけるロードバランスを手軽に試すことができるのは非常に有り難いと感じました。

Go 力養成のため自分も何かプラグインを作ってみたいと思いました。

CKA 取れた

You scored 78% for this Exam. A score of at least 74% was required to pass.

少し前ですが CKA (Certified Kubernetes Administrator) ギリギリ受かりました。予定では88%取れているはずだったのに。肝が冷えます。

規約で大したことは書けないけれど記録のために書いておきます。

モチベーション

私は物事を体系的に学ぶのが好きな教科書人間です。 Kubernetes は業務でもぼちぼちと触っていたのですが、如何せん付け焼き刃の歯抜け知識でした。またチームメンバに情報を展開するにあたっても普段触らない背景知識の必要性を感じていました。そこで今年のGW10連休、暇なので*1 一から勉強し直そうと思い立ち、せっかく資格があるとのことなので連休明けにそれ取ることを目標にしました。

事前状態

GW初日時点での私の戦闘力はこのような感じでした。

  • バイブル Kubernetes 完全ガイド 全編のうち70%程度を数ヶ月前にさらっと読んでいる。
  • 業務を通して基本的な Resource (Pod, Deployment, 各種 Service 等) について概念や基本的な使い方を把握している。一方で Scheduling, Storage, Security, Installation 等の内容は残念なヨクワカラナイ。
  • Kubernetes The Hard Way を一巡。分からないところを調べながら割と時間をかけて取り組みついでに 記事数を稼ぐ。これにより Kubernetes の各コンポーネントと相互接続方法を大まかに理解している。

公式の試験カリキュラム と照らし合わせてみるとこの段階でのカバー率は15%程度だったと思います。

やったこと

試験の情報収集
公式ハンドブック を流し読み。ググると沢山出てくる合格者体験記で試験の Tips や戦略を先人から学びます。問題の内容は受けてみるまで分かりませんが、試験の雰囲気を掴むことはできました。

Cloud Native Certified Kubernetes Administrator (CKA)
Linux Academy のコースで、 公式の試験カリキュラム に沿った内容で作られているため今回の教科書として採用。Yearly で Subscribe をすると1週間の無料トライアル期間が得られるため、GW期間中にlab含めて一巡して解約しました。試験分野を効率的になぞることができるため Kubernetes にまだ自信のない方にはおすすめ。

Kubernetes The Hard Way
直前にもう一周しました。多くの合格者ブログで三周以上やっておくべしと書いてありましたがそこまでの時間はありませんでした。

Kubernetes 完全ガイド
弱点である Scheduling, Storage あたりを中心に yaml を写経デプロイしながら学習。

問題演習
予想問題 (あくまで個人の予想です) を探して解きました。これとかこれ。時間を図りながらやると緊張感があってよいかもしれない。

実際に受けた雑感

  • 後半の難しい問題2問を取り組む時点で試験時間の半分 (1.5時間) が経過。結局その2問は歯が立たなかったので、それ以外の問題の見直しをしました。捨てる問題を決めればそこまで時間に追われることもなかったです。(点数はギリギリでしたが...)
  • 他の方も多く書かれているように kubectl runkubectl expose を息をするようにタイプできると捗ります。--dry-run -o yaml だいじ。
  • 試験時間の中盤移行、与えられたクラスタの kube-apiserver へのアクセスが徐々に重くなっていきました。試験官に相談したところ試験Webの機能でセッションを一度クリアせよ、と指示を受けましたがあまり効果はなし。是非改善して欲しい。
  • 試験時間の後半で突然 PC (MacBook Pro 13-inch 2018, macOS 10.14.4) が再起動しました。かなり焦りましたが再起動後に試験のWebにアクセスしチャットで試験官に事情を説明したところ問題なく再開することができました。どこかのブログでも似たようなことが書かれていたような気がします。

所感

つい先日、 CKA の有効期限が2年から3年の延びました。CKADは2年のままのようですね。運良く受かりましたが再受験までの期間が延びたことに油断せず継続的に知識をアップデートしていきたいです。

f:id:nstgt7:20190527001608p:plain

*1:GWにやったことはこれと Avengers EndGame を2回観たことくらいです。充実です。

Kubernetes The Hard Way をやって知ったこと

巷ではケーサンショー、もとい k3s が盛り上がっていることとは存じますが、初級修行僧の身として今更ながら Kubernetes The Hard Way をやりました。知らなかったところと調べたことをメモしておきます。

github.com

  • 実施した commit
    • bf2850974e19c118d04fdc0809ce2ae8a0026a27
  • 注記
    • 気になった点や調べたこと、感想はこの色で書いてます

01~03

真顔でコピペ。

04 Provisioning a CA and Generating TLS Certificates

  • コンポーネントがそれぞれに (主に API Server との) 通信を行う際に使用する認証を生成している。
    • Kubernetes admin user
    • Node (Kubelet)
    • kube-controller-manager
    • kube-proxy
    • kube-scheduler
    • kube-apiserver
    • Service Account
      • GCPの Service Account ではない
  • Kubernetes では Node の Kubelet から生成される API リクエストの認証を行うのに Node Authorizer という認証方式を使う
    • Using Node Authorization - Kubernetes
    • Kubelet が Node Authorizer に認証されるためには system:nodes:NodeName の形式で自身を識別させる認証情報を持つ必要がある
  • コンポーネントの中で中心的にリクエストを受け付ける API Server の Certificate には、そのIPアドレス ( kubernetes-the-hard-way という名前で取得した外部 IP アドレス) を含めることで remote client との間で認証が可能になる
  • Kubernetes Controller Manager は鍵ペアを使用して Service Account tokens を生成/認証する

05 Generating Kubernetes Configuration Files for Authentication

  • 04章で作成した認証情報を使って Kubernetes clients (各コンポーネント) が kube-apiserver を認識し認証されるための kubeconfig を生成する
  • API Server の IP アドレスは冗長性のため通常ロードバランサに付与して負荷分散するのがベター
    • 今回は GCPTCP ロードバランサを使用する
  • Kubelet の kubeconfig を生成する際には、 Kubelet が動作する Node の名前 system:nodes:NodeName にマッチする Certificate を使う必要がある。これにより Node Authorizer による認証が行われる

06 Generating the Data Encryption Config and Key

  • Kubernetes は cluster state, application config, secrets といったデータを暗号化する仕組みを持つ
  • 暗号鍵と Secret を暗号化するための kind: EncryptionConfig の manifest を生成し Controller Node にばら撒く

07 Bootstrapping the etcd Cluster

  • Kubernetesコンポーネントはステートレスであり、 ステートに関する情報は etcd に格納される
  • etcd サーバはインスタンスの内部IPアドレス (03章で作成した kubernetes-the-hard-way VPCkubernetes subnet 範囲内のIPアドレス) を使ってクライアントからのリクエストを受け付けたり、 etcd クラスタ内の他の peer とコミュニケーションを行う
  • etcd は API Version を指定しないと v2 で起動する
    • etcdctl -h で表示される実行可能なサブコマンド一覧も v2 と v3 で異なる
    • 今回のように認証情報を渡して起動すると、それなしではクラスタに関する情報を閲覧することができない
controller-2:~$ sudo ETCDCTL_API=3 etcdctl member list --endpoints=https://127.0.0.1:2379   --cacert=/etc/etcd/ca.pem   --cert=/etc/etcd/kubernetes.pem   --key=/etc/etcd/kubernetes-key.pem
3a57933972cb5131, started, controller-2, https://10.240.0.12:2380, https://10.240.0.12:2379
f98dc20bce6225a0, started, controller-0, https://10.240.0.10:2380, https://10.240.0.10:2379
ffed16798470cab5, started, controller-1, https://10.240.0.11:2380, https://10.240.0.11:2379

controller-2:~$ sudo ETCDCTL_API=3 etcdctl member list
Error: context deadline exceeded

08 Bootstrapping the Kubernetes Control Plane

  • kube-apiserver, kube-controller-manager, kube-scheduler を Systemd で Service 化して起動する
    • ちなみに kubeadm は上記コンポーネントを Pod として動かしているらしい。他には GKE とかはどうだろうか。
  • kube-scheduler には kind: KubeSchedulerConfiguration の manifest を引数に与える
  • オプションより
    • --cluster-cidr は kube-controller-manager で指定
    • --service-cluster-ip-range は kube-apiserver, kube-controller-manager の両方で指定
    • --leader-elect=true は kube-controller-manager でのみ指定
      • Kubernetes完全ガイドの第19章を見ると kube-scheduler も leader 制をとることができるという
  • Google Network Load Balancer (様々なプロトコルに対応したRegional LB) は API Server に対するトラフィック分散と SSL の終端を行う目的で使用するが、 HTTPS の health check には非対応なため nginx をインストールして HTTP 宛の health check を HTTPS にプロキシする
    • /healthz エンドポイントはデフォルトでは認証不要になっている
  • kube-apiserver が Kubelet の API にアクセス (ログやメトリクスを収集したりコマンドを実行したり) するためには RBAC の設定が必要
    • kind: ClusterRole の manifest を作製してよく使う一般的な Kubelet API を定義し、 kind: ClusterRoleBinding manifest で kubernetes ユーザに紐づけを行う

09 Bootstrapping the Kubernetes Worker Nodes

  • Worker に必要なコンポーネントをインストールする
    • runc
      • 低レイヤコンテナランタイム
      • 高レイヤコンテナランタイムである containerd から OCI 経由で命令を受け取りコンテナを作成する
    • gVisor
      • runc と同じく低レイヤコンテナランタイム
      • 比較的セキュアなランタイム
    • container networking plugins
      • CNI プラグイン
      • コンテナのネットワークインターフェースに関する仕様とライブラリから成る
    • containerd
    • kubelet
    • kube-proxy
  • cni の設定では type として bridge を指定し、03章で定義した pod-cidr を記述している

The loopback CNI is automatically used by Kubernetes to do low-level networking (like for the Kubelet to talk to the containers local to the node) and Kubernetes will fail if it's not installed or running,

  • containerd の設定では低レイヤコンテナランタイムである runc 及び runsc (gVisor) を指定
  • kubelet の設定では kind: KubeletConfiguration を作成、その中で clusterDNSpodCIDR 、認証情報などを指定
    • 今回各コンポーネントを systemd で動かしており、 systemd 環境で DNS 設定を担当する systemd-resolved とサービスディスカバリに使用する coreDNS が仲良くするために resolvConf を設定する
  • kubelet の unit ファイルを作成。
    • --container-runtime で指定可能な値は docker, remote, rkt(deprecated) であり、 containerd を使う場合には remote を設定の後、 --container-runtime-endpoint で使用する Unix Socket を指定する
  • kube-proxy の設定では kind: KubeProxyConfiguration を作成、その中で動作モードを iptables と指定。

10 Configuring kubectl for Remote Access

  • 前半で作製した admin ユーザで kubectl を実行できるようにする設定する
  • ロードバランスされている API Server のIPアドレスと admin ユーザの認証情報を使用する
  • これを全て手で設定することを思うとgcloud container clusters get-credentials は偉大である

11 Provisioning Pod Network Routes

  • Pod が Node にスケジューリングされると Pod は Node の Pod Cidr からIPアドレスを受け取って設定するが、09章で見たように Pod Cidr は Node 毎に異なるため、通常であれば別 Node にある Pod とは通信することができない
  • そこで今回はそれを GCP のルートを用いて Pod Cidr を Node の Internal IP アドレスに解決する経路を生成する
  • この lab では敢えて原始的な方法を用いているが Calico や Flannel 等の CNI プラグインを用いる方法が一般的ですよね

12 Deploying the DNS Cluster Add-on

  • サービスディスカバリのための DNS として CoreDNS を Add-on 的にデプロイする
  • 作成されている Service に対して名前解決ができることを確認する
  • が、やけに遅い
$ time kubectl exec -ti $POD_NAME -- nslookup kubernetes
Server:    10.32.0.10
Address 1: 10.32.0.10 kube-dns.kube-system.svc.cluster.local

Name:      kubernetes
Address 1: 10.32.0.1
kubectl exec -ti $POD_NAME -- nslookup kubernetes
0.09s user 0.03s system 0% cpu 45.347 total
  • Service に対して問い合わせる場合と Endpoint に問い合わせる場合で差があるようだ。 Service に問い合わせをする場合、実際の DNS 応答は Endpoint のIPアドレスから返ってくるため、そこらへんが nslookup の機嫌を損ねているのではと推測。
## dns の Service と Endpoint を確認しておく
$ kubectl -n kube-system get svc kube-dns
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   ClusterIP   10.32.0.10   <none>        53/UDP,53/TCP   12d

$ kubectl -n kube-system describe ep kube-dns
(snip)
Subsets:
  Addresses:          10.200.0.9,10.200.2.7
  NotReadyAddresses:  <none>
  Ports:
    Name     Port  Protocol
    ----     ----  --------
    dns      53    UDP
    dns-tcp  53    TCP

$ kubectl exec -it $POD_NAME /bin/sh

## 結果が得られることに変わりはないが Service を介した場合だけやけに遅い
/ # time nslookup kubernetes 10.32.0.10 1> /dev/null
real  0m 10.01s
user  0m 0.00s
sys   0m 0.00s
/ # time nslookup kubernetes 10.200.0.9 1> /dev/null
real  0m 0.00s
user  0m 0.00s
sys   0m 0.00s
/ # time nslookup kubernetes 10.200.2.7 1> /dev/null
real  0m 0.00s
user  0m 0.00s
sys   0m 0.00s

12 Smoke Test

  • crictl コマンドを使用して CRI の Unix Socket 経由で実際に動いているコンテナの一覧を取得可能
root@worker-0:~# sudo crictl -r unix:///var/run/containerd/containerd.sock ps
CONTAINER ID        IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID
c67595b3b33c2       8c811b4aec35f       32 minutes ago      Running             busybox             24                  a58da8259ee26
a15e2d786eb05       367cdc8433a45       About an hour ago   Running             coredns             0                   c3a0d3c772526
66f1bbe5ed396       881bd08c0b082       5 hours ago         Running             nginx               2                   e8a5bbdb3733a

まとめ

Hard とは名前だけで優しさに溢れた素晴らしい教材でした。またやろう。

BGP Scanner を試す

BGP Scanner という MRT レンダリングツールを教えてもらったので試してみました。

BGP Scanner?

バイナリである MRT のデータをテキストで見せるレンダリングツールとしては bgpdump が有名だと思います。 よくお世話になっていますが、特定の subnet や AS 番号でフィルタしたいなんて時に grep 等で頑張らないといけないところに辛さがあります。

BGP ScannerIsolarioプロジェクト で開発されているC言語製の MRT レンダリングツールで、既存の MRT レンダリングツール群よりイケているとのことです。 情報は少ないですが以下参考にしました。

要約すると次のような利点があるそうです。

  • パフォーマンスを重視した設計で速い、しかも低メモリ消費
  • フィルタリング機能が充実
  • 高水準言語でラップ可能なため開発が活発化する(と期待される)

bgpdump 含め様々な MRT レンダリングツールと比較したパフォーマンス評価の結果もあり、早いことは間違いなさそうです。

インストール

環境

インストール方法

(1) Download bgpscanner-[version].tar.gz
(2) ./configure && make && make install

ITNOGでの発表資料 によると上記コマンドでおっけーって書いてあるけど、私の Mac ではそのまんまだとビルドできませんでした。

% wget https://www.isolario.it/tools/bgpscanner-1.0-1.tar.gz
% tar -zxvf bgpscanner-1.0-1.tar.gz
% cd bgpscanner-1.0-1
% ./configure
% make
/Library/Developer/CommandLineTools/usr/bin/make  all-am
depbase=`echo src/main.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\
    gcc -DHAVE_CONFIG_H -I.    -Isubprojects/isocore/include/ -g -O2 -MT src/main.o -MD -MP -MF $depbase.Tpo -c -o src/main.o src/main.c &&\
    mv -f $depbase.Tpo $depbase.Po
src/main.c:942:13: warning: implicit declaration of function 'posix_fadvise' is invalid in C99
      [-Wimplicit-function-declaration]
            posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
            ^
src/main.c:942:37: error: use of undeclared identifier 'POSIX_FADV_SEQUENTIAL'
            posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
                                    ^
1 warning and 1 error generated.
make[1]: *** [src/main.o] Error 1
make: *** [all] Error 2

Man 曰く、

POSIX_FADV_SEQUENTIAL アプリケーションは指定されたデータがシーケンシャルに (大きなオフセットの前に小さなオフセットのデータを読むように) アクセスされることを期待する。 Man page of POSIX_FADVISE

はて。
すみません、コメントアウトさせてください。(動きはしますが正しさは保障できません。。)

% emacs src/main.c
(snip)
941        // if (fd != STDIN_FILENO)
942        //     posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
(snip)

ビルドできました。-v-h もないみたいですが、とりあえず動くようです。

% make
% make install
% which bgpscanner
/usr/local/bin/bgpscanner
% bgpscanner -v
bgpscanner: illegal option -- v
bgpscanner: The Isolario MRT data reader utility
Usage:
    bgpscanner [-cdlL] [-mM COMMSTRING] [-pP PATHEXPR] [-i ADDR] [-I FILE] [-a AS] [-A FILE] [-e PREFIX] [-E FILE] [-t ATTR_CODE] [-T FILE] [-o FILE] [FILE...]
    bgpscanner [-cdlL] [-mM COMMSTRING] [-pP PATHEXPR] [-i ADDR] [-I FILE] [-a AS] [-A FILE] [-s PREFIX] [-S FILE] [-t ATTR_CODE] [-T FILE] [-o FILE] [FILE...]
    bgpscanner [-cdlL] [-mM COMMSTRING] [-pP PATHEXPR] [-i ADDR] [-I FILE] [-a AS] [-A FILE] [-u PREFIX] [-U FILE] [-t ATTR_CODE] [-T FILE] [-o FILE] [FILE...]
    bgpscanner [-cdlL] [-mM COMMSTRING] [-pP PATHEXPR] [-i ADDR] [-I FILE] [-a AS] [-A FILE] [-r PREFIX] [-R FILE] [-t ATTR_CODE] [-T FILE] [-o FILE] [FILE...]

Available options:
    -a <feeder AS>
        Print only entries coming from the given feeder AS
    -A <file>
        Print only entries coming from the feeder ASes contained in file
(snip)

試してみる

おなじみ オレゴン大学Route Viewsプロジェクト から適当な MRT ファイルをいただいて覗いてみます。

見え方

bgpdump との比較。 個人的に現時点では bgpdump の方が見やすい印象です。
例えば bgpscanner は NEXT_HOP と FROM IP (= feeder IP) をどっちも出してたり。フィルタリングは色々できる (後述) けど、出力の内容をオプションで制御するのは現時点では改善の余地ありだと思います。 せめてUNIX時間は直させておくれよ。

% bgpdump -M rib.20190211.1400.bz2| head -n5
2019-02-12 23:39:33 [info] logging to syslog
TABLE_DUMP2|02/11/19 14:00:00|B|94.156.252.18|34224|0.0.0.0/0|34224 3356|IGP
TABLE_DUMP2|02/11/19 14:00:00|B|216.221.157.162|40191|0.0.0.0/0|40191 3257|IGP
TABLE_DUMP2|02/11/19 14:00:00|B|202.73.40.45|18106|0.0.0.0/0|18106|IGP
TABLE_DUMP2|02/11/19 14:00:00|B|195.22.216.188|6762|1.0.0.0/24|6762 13335|IGP
TABLE_DUMP2|02/11/19 14:00:00|B|94.156.252.18|34224|1.0.0.0/24|34224 13335|IGP

% bgpscanner rib.20190211.1400.bz2 | head -n5
=|0.0.0.0/0|34224 3356|94.156.252.18|i|||34224:333|94.156.252.18 34224|1549527724|1
=|0.0.0.0/0|40191 3257|216.221.157.162|i||||216.221.157.162 40191|1549493033|1
=|0.0.0.0/0|18106|202.73.40.45|i||||202.73.40.45 18106|1549493010|1
=|1.0.0.0/24|6762 13335|195.22.216.188|i||13335 173.245.63.1|6762:31 6762:40|195.22.216.188 6762|1549781054|1
=|1.0.0.0/24|34224 13335|94.156.252.18|i||13335 162.158.84.1|34224:333 34224:334 34224:2040|94.156.252.18 34224|1549873473|1

それにしても bgpdump-M (one-line output) すると COMMUNITIES は出力されないんでしたかね。

早さ

試しに測ると早いです。私の環境では bgpscanner のほうが30%程度高速でした。これは有り難い場面もありそう。

% ls -lh rib.20190211.1400.bz2 | awk '{print $5}'
113M

% /usr/bin/time bgpdump -Hm -t dump rib.20190211.1400.bz2 > /dev/null
      134.11 real       133.18 user         0.42 sys

% /usr/bin/time bgpscanner rib.20190211.1400.bz2 > /dev/null
       91.89 real        82.53 user         9.17 sys

完全に余談ですが Macのbashビルトインコマンドのtimeと/usr/bin/timeは別物 - StupidDog's blog ということを初めて知りました。

フィルタリング

せっかくなのでいくつかフィルタリングをやってみます。

-a/-A: feeder AS number

当該経路の feeder の AS 、つまり AS-PATH の1番左の AS でフィルタリングができます。 -p で AS-PATH の正規表現が使えるのにも関わらずわざわざ別のオプションにしてるのは、Isolario プロジェクトが BGP コレクターをやっているからでしょうか。分かりません。 -A ではファイル指定が可能。便利。

## 単一の feeder AS を渡す
% bgpscanner -a 701 rib.20190211.1400.bz2 | head -n5
=|1.0.0.0/24|701 2914 13335|137.39.3.55|i||13335 4.14.97.106||137.39.3.55 701|1549494007|1
=|1.0.4.0/22|701 174 4826 38803 56203|137.39.3.55|i||||137.39.3.55 701|1549493600|1
=|1.0.4.0/24|701 174 4826 38803 56203|137.39.3.55|i||||137.39.3.55 701|1549493600|1
=|1.0.5.0/24|701 174 4826 38803 56203|137.39.3.55|i||||137.39.3.55 701|1549493600|1
=|1.0.6.0/24|701 174 4826 38803 56203|137.39.3.55|i||||137.39.3.55 701|1549493600|1

## 複数の feeder AS を渡す
% bgpscanner -a 701 -a 6939 rib.20190211.1400.bz2 | head -n5
=|1.0.0.0/24|6939 13335|64.71.137.241|i||13335 172.68.188.1||64.71.137.241 6939|1549494051|1
=|1.0.0.0/24|701 2914 13335|137.39.3.55|i||13335 4.14.97.106||137.39.3.55 701|1549494007|1
=|1.0.4.0/22|6939 4826 38803 56203|64.71.137.241|i||||64.71.137.241 6939|1549494433|1
=|1.0.4.0/22|701 174 4826 38803 56203|137.39.3.55|i||||137.39.3.55 701|1549493600|1
=|1.0.4.0/24|6939 4826 38803 56203|64.71.137.241|i||||64.71.137.241 6939|1549494433|1

## 複数の feeder AS をファイルで渡す
% echo '701\n6939' > feeder_as.txt
% bgpscanner -A feeder_as.txt rib.20190211.1400.bz2 | head -n5
=|1.0.0.0/24|6939 13335|64.71.137.241|i||13335 172.68.188.1||64.71.137.241 6939|1549494051|1
=|1.0.0.0/24|701 2914 13335|137.39.3.55|i||13335 4.14.97.106||137.39.3.55 701|1549494007|1
=|1.0.4.0/22|6939 4826 38803 56203|64.71.137.241|i||||64.71.137.241 6939|1549494433|1
=|1.0.4.0/22|701 174 4826 38803 56203|137.39.3.55|i||||137.39.3.55 701|1549493600|1
=|1.0.4.0/24|6939 4826 38803 56203|64.71.137.241|i||||64.71.137.241 6939|1549494433|1

-f: feeder IP

feeder の IP アドレスを sort | uniq した結果が見られます。

% bgpscanner -f rib.20190211.1400.bz2 | head -n5
12.0.1.63 7018|1
37.139.139.0 57866|1
45.61.0.85 22652|1
64.57.28.241 11537|1
64.71.137.241 6939|1

-l/-L: with(out) AS-PATH loop

-l では AS-PATH がループしているエントリを、 -L ではループしていないエントリを表示。

% bgpscanner -l rib.20190211.1400.bz2 | head -n5
=|5.144.132.0/22|7018 6762 12880 12880 12880 12880 12880 12880 1288 12880 12880 12880 59441|12.0.1.63|i|||7018:5000 7018:37232|12.0.1.63 7018|1549889812|1
=|5.144.132.0/22|6762 12880 12880 12880 12880 12880 12880 1288 12880 12880 12880 59441|195.22.216.188|i|||6762:30 6762:40|195.22.216.188 6762|1549889835|1
=|5.144.132.0/22|286 6762 12880 12880 12880 12880 12880 12880 1288 12880 12880 12880 59441|134.222.87.1|i|||286:18 286:19 286:28 286:29 286:49 286:800 286:888 286:3031 286:4517 286:4990 286:28610 6762:1 6762:92 6762:14900|134.222.87.1 286|1549889812|1
=|5.144.132.0/22|3303 6762 12880 12880 12880 12880 12880 12880 1288 12880 12880 12880 59441|217.192.89.50|i|||3303:1004 3303:1006 3303:3056 3303:8199 6762:1 6762:92 6762:14900|217.192.89.50 3303|1549892534|1
=|5.144.132.0/22|57463 6762 12880 12880 12880 12880 12880 12880 1288 12880 12880 12880 59441|87.121.64.4|i|||0:5394 0:43561 0:60501 1:1089 6762:1 6762:30 6762:40 6762:14900 64700:6762 65400:1 65400:65400|87.121.64.4 57463|1549889838|1

-m/-M: COMMUNITIES

BGP COMMUNITIES によるフィルタリング。複数指定した時に OR じゃなくて AND でフィルタして欲しいケースの方が多いような気がします。。subnet などは複数指定で OR になるのは分かりますが果たして。 -M は反対に指定された BGP COMMUNITIES を含まない経路を表示。

% bgpscanner -m 6762:31 rib.20190211.1400.bz2 | head -n3
=|1.0.0.0/24|6762 13335|195.22.216.188|i||13335 173.245.63.1|6762:31 6762:40|195.22.216.188 6762|1549781054|1
=|1.0.4.0/22|6762 174 4826 38803 56203|195.22.216.188|i|||6762:31|195.22.216.188 6762|1549494497|1
=|1.0.4.0/24|6762 174 4826 38803 56203|195.22.216.188|i|||6762:31|195.22.216.188 6762|1549494497|1

% bgpscanner -m 6762:31 -m 6762:40 rib.20190211.1400.bz2 | head -n3
=|1.0.0.0/24|6762 13335|195.22.216.188|i||13335 173.245.63.1|6762:31 6762:40|195.22.216.188 6762|1549781054|1
=|1.0.4.0/22|6762 174 4826 38803 56203|195.22.216.188|i|||6762:31|195.22.216.188 6762|1549494497|1
=|1.0.4.0/24|6762 174 4826 38803 56203|195.22.216.188|i|||6762:31|195.22.216.188 6762|1549494497|1

-p/-P: AS-PATH Expression

-p "^701"-p "701 15169" ができるようです。
が、割と Segmentation fault が出がちです。今後の改善に期待です。(PR を出そう)

% bgpscanner -p "15169$" rib.20190211.1400.bz2 | head -n3
=|8.8.4.0/24|2905 15169|196.7.106.245|i||||196.7.106.245 2905|1549799014|1
=|8.8.4.0/24|6762 15169|195.22.216.188|i|||6762:31|195.22.216.188 6762|1549893400|1
=|8.8.4.0/24|34224 15169|94.156.252.18|i|||34224:333 34224:334 34224:2090|94.156.252.18 34224|1549527763|1

-e/-E他: subnet

-e subnet で引数で与えた subnet に Exact Match する prefix のみを表示。

% bgpscanner -e 1.4.252.0/22 rib.20190211.1400.bz2 | head -n3
=|1.4.252.0/22|37100 38040 23969|105.16.0.247|?|||NO_EXPORT|105.16.0.247 37100|1549758853|1
=|1.4.252.0/22|34224 38040 23969|94.156.252.18|?|||34224:333 34224:334 34224:2040|94.156.252.18 34224|1549709323|1
=|1.4.252.0/22|2914 38040 23969|129.250.1.71|?|||2914:410 2914:1405 2914:2406 2914:3400|129.250.1.71 2914|1549494890|1

subnet でフィルタリング可能なオプションがいくつか用意されています。大文字はファイル指定。

オプション フィルタリング効果
-e (-E) 引数で与えられた subnet に Exact Match するエントリを表示
-s (-S) 引数で与えられた subnet に Or Longer Match するエントリを表示
-r (-R) 引数で与えられた subnet に Related な(?) エントリを表示
-u (-U) 引数で与えられた subnet の Supernet となるエントリを表示

Related とは何かというところですが、やってみると -e, -s, -u を合わせたような結果になります。 "ような" というのは、試した限りでは完全に各オプションの出力が合算される訳ではなさそうという自信の無さから。 codeを深くまで追ってはいないのです。

所感

フィルタリングはまだ不安定な部分もありますが、高機能でかゆいところに手が届くため様々な場面で活用できそうな有り難いツールです。気になるところがあったら PR しよう。

蛇足

コンテナ化しました。nstgt/bgpscanner
こういった類のツールをコンテナ化する利点はあまり思い付きませんが、インストールの過程で寄り道した副産物なのでいずれ何かで使える日が来ることを切に願います。

JANOG43 セルフフォローアップ

JANOG43に参加しました。2,3日目のみの参加でしたが、新しい技術・事例や各社の運用の工夫など、非常に勉強になるプログラムばかりでした。 その場では理解が追いつかないところが多々あったので、特に気になったプログラムを2件だけピックアップして自分のためにフォローアップしたいと思います。

Note

  • (本来と使い方は違いますが) 引用の部分に私の所感や調べたことを記載しています。
  • 理解の誤りや記述の間違いにお気づきの場合には、お手数ですがコメント等でご指摘いただけると幸いです。
  • 後日アーカイブ動画が配信されたら再度見直して加筆修正する部分もあるかと思います。

Day2: LINEのネットワークをゼロから再設計した話

LINEのネットワークをゼロから再設計した話
LINE株式会社の小林さんによる発表。個人的には今回のJANOG全体を通して一番衝撃と感銘を受けたプログラムでした。

背景と方針

  • それまでのLINEのネットワークの課題は、East-Westトラフィックの増加によるキャパシティ圧迫とトラディショナルな2N構成に起因する運用の複雑化にあった。
  • 過去のLINEのネットワーク構成については JANOG39にて同社の三枝さんによる発表でも語られており、2015年以降から顕在化した様々な課題に対して、アーキテクチャレベルでの見直しを図ることで、スケーラビリティの高いネットワークを最小数のオープンなプロトコルのみで構築することで、根本解決を目指す。

(筆者メモ)
一つのキーワードとして、可能な限り "シンプル" に設計・構築するという思想が全体に散りばめられていたと感じました。

新しいアーキテクチャ

  • Externalを除いて事実上Spineが1番上になる3階層のCLOSアーキテクチャをホワイトボックススイッチを利用して実現。
  • 挙げられていた事前資料も踏まえると、ToRより上位では全て100G Linkを利用しサーバ間通信のボトルネックを排除、また各階層毎にN+1スケールが可能なため、障害に強く拡張性の高いネットワークを構築できる。
  • 物理構築が完了している前提で、ZTPとAnsibleを用いた設定を行い、1,000台以上の ホワイトボックススイッチを2,3時間で構築可能。

(筆者メモ)
CLOSネットワークとはOTTを中心に積極的に採用されるDCネットワーク構成で、サーバ群が接続されるLeafとそれを束ねるSpineから成り、East-Westトラフィックに対し高いスケーラビリティを持ちます。改めて復習しましたが、以下の発表が参考になりました。
資料:JANOG38 :: ヤフーのIP CLOS ネットワーク

(筆者注記: 発表では新アーキテクチャに関する様々な技術要素が説明されていましたが、ポイントを掻い摘んで復習します。全貌は是非発表資料を御覧ください。)

L2-LESS
  • 通常のCLOSネットワークではToRより上がL3、下がL2となるが、サーバから一方のToRに向けたトラフィックをもう一方のToRに振り替える際に、サーバ管理者に作業を依頼する必要があるのみならず、パケットロスが発生してしまう。 それを防ぐためサーバにBGPをしゃべらせることでToR <-> サーバ間をL3で接続。
  • 実装としては (筆者補足:質疑で説明あり。LINE社のPrivate CloudはOpenStackで構築されている。) Compute Node上でFRRを動作させ、ハイパーバイザ上のRouting Tableを監視して、ハイパーバイザに接続されたVMの/32のホスト経路をToRにBGPで広報する。
  • (筆者補足:質疑で説明あり) アプリケーションの要求によってはどうしてもL2が必要なケースがあり、ホワイトボックススイッチでMC-LAGを利用している箇所も存在する。

(筆者メモ)
パケロスの仕組みが分からず推測です。
BondingされたサーバのAct-SbyインターフェースとMC-LAG ToR間の接続で、例えばToR Aを切り離してメンテナンスする際に、サーバのActのLinkを落としてSbyをActive化しようとするとMC-LAGの切り替わりが完了するまでの数百msecの間サーバ宛のトラフィックは既に切れている旧Actインターフェースに流れ込もうとしロスしてしまう、といった感じなのでしょうか。

サーバから見た経路情報
  • ハイパーバイザ上のFRRでBGPの経路を見ると2台のToR AS4208258575とAS4208258576のうち前者から受信した経路がalways-compare-medによりBest Pathに選ばれてる。
  • 選ばれた経路のNext Hopには、IPv4の経路であるがRFC5549によりIPv6のリンクローカルアドレスが表示される。
  • ただしLinux KernelではIPv6のNext Hopを登録することができないため、実際にはIPv4のリンクローカルアドレスによってルーティングされる。

(筆者メモ)
RFC5549IPv4のNLRIにIPv6のNext Hopを設定して広報する技術。日本語だとIRS26で土屋さんが発表された資料が参考になりました。また、FRRのRFC5549の動作については以下の記事が参考になりました。
FRRはRFC5549な経路をLinuxカーネルのルーティングテーブルへどうやってインストールするのか - yunazuno.log

ホワイトボックススイッチの採用理由
  • BGP UnnumberedとHostname Capability for BGPを使いたく、両実装を満たすFRRを動作させるため、LinuxベースNOSで動くCumulusを採用。
  • (補足:質疑で説明あり) 機器選定では調達コストより運用コストの低下を重視。また、実際には各社の製品を比較しショートパケットの転送に優れた製品を選択した。

(筆者メモ)
BGP Unnumbered
RFC5549の拡張で、インターフェースにIPアドレスを設定することなくBGPピアを上げることができる技術。Cumulus社の動画は分かりやすかったです。

Hostname Capability for BGP
Open MessageにFQDNを入れて交換することでBGPピアを張るだけで対向のHostnameが分かるようにする技術。

データセンター間ネットワーク

  • DC内のみならずDC間接続においてもL2オーバーレイは作らないポリシーのもと、シンプルなSR-MPLSを採用。
  • 元々の帯域に余裕をもたせているため、帯域制御も不要。

(筆者メモ)
残念ながらSegment Routingを十分に理解していないため中途半端に書かずに別記事か何かでまとめようと思います。

今後の展望

  • 様々なビジネスニーズに迅速に対応するため、一つのアンダーレイネットワーク上に複数のオーバーレイテナントを構築すること検討中。
  • 実現手法としてSRv6に注目している。

Day3: オンプレミスKubernetesのネットワーク

オンプレミスKubernetesのネットワーク
株式会社Jストリームの城田さんによる発表。私はKubernetes勉強中の身で各CNI実装についてよく知らなかったため、エッセンスやメリット/デメリットがまとまった発表は大変有難かったです。

Kubernetesをオンプレミスで動かす理由

  • コンテナ化されクラウドに配置されたオリジンサーバから、自社ネットワークにある数十のキャッシュサーバへトラフィックを転送するコストが高いから。
  • 自社でASを保有しているという側面もあった。

Node間ネットワーク

  • Kubernetes本体ではネットワーキング機能は提供しておらず、Pod間通信、Serviceと他NodeのPod間通信にはNode同士を接続するネットワークが必要となる。
  • 抽象化されたCNI (Container Network Interface) に従ってネットワークを実装することで初めてClusterとして機能する。
  • 以降代表的なCNI実装を紹介。

Flannel

  • Nodeに渡りユニキャストなVXLANを構築する。VXLAN以外にもIP-IPやIPSec(実験的)にも対応している。最近更新頻度が下がってきたのが懸念。
  • 各Nodeにflanneldが立ち上がり、VXLANインターフェースである flannel.1 とBridgeインターフェースである cni.0 を作製。Node内のPod間通信は cni.0 を経由し、Node間通信ではPodからNodeへ転送後、NodeのRIBに従って到達した flannel.1 でVXLANのカプセリング化を行って宛先のNodeへ転送される。

(筆者メモ)
以下の記事も参考になりました。Node間通信でNodeのRIB, ARPテーブル, FDBに書かれている情報はflanneldが書き込みをしているようです。
Kubernetes Network Deep Dive (NodePort, ClusterIP, Flannel) - Qiita

Project Calico

  • Bridgeやオーバーレイを作らずにBGPを用いたL3で処理を完結する。Network Policyに対応している。
  • Calicoのconfig情報を保有するetcdを除く複数のプロセスが一つのPodにまとめられ、その中の一つであるBIRDが(デフォルトでは)フルメッシュでBGP接続を行う。
  • Bridgeは作製されないのでNode内通信はNodeでルーティングされ、Node間通信はBGPで学習した経路情報によって行われる。

(筆者メモ)
FlannelとCalicoのさらに詳しい動作は JAPAN CONTAINER DAYS V18.12 でも発表があり、こちらも分かりやすいです。
資料:コンテナネットワーキング(CNI)最前線
アーカイブ動画:[1BL] コンテナネットワーキング(CNI)最前線 - YouTube

kube-router

  • FlannelとCalicoをあわせたような動作で、Node内通信はBridge経由、Node間通信はBGPによるルーティングを提供する。
  • BGPの設定はNodeのAnnotationsに記載する。
  • CalicoはRoute Reflectorを設ける時に専用のコンテナを起動する必要があるが、kube-routerではCluster内からRoute Reflectorを選択することができる。

質疑の時間にコメントがあり、日本国内で自前でKubernetesを建てている人に聞くとFlannelとCalicoがほとんどでkube-routerを使っているケースは稀とのこと。

Node外ネットワーク

  • クラウド上でマネージドなKubernetesを使う場合には各社が提供するロードバランサを使い、LoadBalancerタイプのServiceを利用することでCluster外からPodへの通信を通すとができるが、オンプレではそれがない。NodePortタイプやIngressタイプのServiceを利用するにしても、なんらかこれらを束ねるロードバランサーが必要になる。
  • 以降、Cluster外部からアクセスを可能とするロードバランサを紹介。

MetalLB

  • Kubernetes Node上で動くロードバランサ。L2モード/BGPモードがある。
  • L2モードではService毎に代表Nodeを選びProxy ARPすることで当該Service宛の通信を全て一つのNodeで受け内部でロードバランスされる。トラフィックが一つのNodeに集中することが難点。
  • BGPモードではCluster外部のルータとBGPを張りECMPを行う。外部のルータと、Cluster内のServiceで二重にロードバランスするのが難点。設定によってServiceでのロードバランスをしないようにすることができるが、外部のルータはCluster内のPodの配置を認識できないため、トラフィックに偏りが生じてしまう。

F5 Container Connector

  • F5社のBIG-IPという製品をKubernetesから設定することができる。NodePortモード/Clusterモードがある。
  • NodePortモードではMetalLBのBGPモードと同じくBIG-IPとServiceとで二重にロードバランスする。
  • ClusterモードではBIG-IPをKubernetes内に組み込むことで、FlannelまたはCalicoを利用してPodに向けたロードバランスを行う。
  • 商用環境でBIG-IPを利用している実績がある。

自社の構成

  • FlannelとCalicoはBIG-IPとの連携に課題があると感じ、kube-router + F5 Container Connector (Clusterモード) で構築を行っている。

(筆者メモ)
以下の発表ではCNI含めたKubernetesネットワークの"すべて"について語られており、圧巻です。
発表資料: Kubernetes ネットワーキングのすべて
アーカイブ動画:[1B1] Kubernetes ネットワーキングのすべて - YouTube

おまけ

上記の発表に比べると大変お恥ずかしいのですが、私自身もLight Lighting Talk大会で発表させていただきました。 speakerdeck.com

普段の運用で感じている課題を、最近流行りのクラウドネイティブ関連技術で何とかできないかと思い勢いでやってみた、という内容です。 発表の意図としては、やったことそのものを伝えたいというよりネットワークとクラウドネイティブな技術との関わりがますます活発化するであろうことを、JANOGerの皆さんと共有できればと思った次第です。今回のJANOGでは他にもそれ系の発表が多々あり、私自身改めてその重要性を感じました。
試作機の実装は趣味で進行中、課題は山積み前途多難ですがぼちぼちやってます。

おわり

他にも勉強になったプログラムは沢山ありましたが、取り急ぎ勢いで書けるところまでを書きました。 後ほどアーカイブ動画で復習しようと思います。発表者の皆さん、スタッフの皆さんありがとうございました。

EVE-NGスタートアップのメモ

以前はGNS3を使っていたけれど社外の方に教えてもらってこっちのほうが使いやすそうだったのでお試ししました。公式ドキュメント(とビデオ)様様ですが、気になったところをメモしておきます。

はじめに

モチベーション

多数のネットワークベンダーに対応し、かつポータブルな仮想ネットワーク環境が欲しい。

環境

  • macOS Mojave ver10.14.2
    • Memory: 16GB
  • VMware Fusion ver11.0.2
  • EVE-NG Community Edition 2.0.3-92

やったこと

EVE-NGのインストール

公式ページから辿れる動画のまま。 動画ではVMware Workstationを使っていますがほぼ同じ流れをVMware Fusionで踏襲しました。

パラメータ等

  • 割当メモリ: 8GB
    • "この仮想マシンでハイパーバイザアプリケーションを有効にする" にチェック
  • プロセッサ: 4個
  • ネットワークアダプタ: NAT
    • Static IPを付与。私の環境では172.16.216.10/24

このあたりも参考にさせていただきました。

各種OSイメージのインストール

今回はインターネットで入手可能なOSイメージを入れてみました。

Cisco CSR1000V

感謝しつつCisco社のダウンロードサイトから入手。私のCiscoアカウントでダウンロードできた最新のバージョンは 3.15.0S でした。 入手したISOイメージをEVE-NGのVMに転送し、公式のインストールガイド に倣ってコマンドを打つだけ。

# Host macOS
$ scp ~/Downloads/csr1000v-universalk9.03.15.00.S.155-2.S-std.iso root@172.16.216.10:/root/csr1000v
$ ssh root@172.16.216.10

# EVE-NG VM
$ cd csr1000v
$ /opt/qemu/bin/qemu-img create -f qcow2  virtioa.qcow2 8G
$ /opt/qemu-2.2.0/bin/qemu-system-x86_64  -nographic -drive file=virtioa.qcow2,if=virtio,bus=0,unit=0,cache=none -machine type=pc-1.0,accel=kvm -serial mon:stdio -nographic -nodefconfig -nodefaults -rtc base=utc -cdrom /tmp/csr1000v-universalk9.03.15.00.S.155-2.S-std.iso -boot order=dc  -m 3072

# Press any key to continue では Ctl+c を押下 (1回目,2回目共に)
# インストールガイドにもあるように、2回目のGNU GRUBでSerial consoleにカーソルをあわせた後、Enterを押さないように注意

$ mkdir /opt/unetlab/addons/qemu/csr1000v-universalk9.03.15.00.S.155-2.S-std/
$ mv virtioa.qcow2 /opt/unetlab/addons/qemu/csr1000v-universalk9.03.15.00.S.155-2.S-std/
$ /opt/unetlab/wrappers/unl_wrapper -a fixpermissions

実は最初に昔ダウンロードしていたバージョン 3.12.0S を使おうと試みたのですが、2回目の Press any key to continue が表示されずにイメージが起動し、インストールできませんでした。その後、バージョン 3.15.0S で試した際に、既に存在するHDD virtioa.qcow2 を削除しなかったためにインストールプロセスで 3.12.0S が起動してしまいました。インストールプロセスを再実行する際には virtioa.qcow2 を作り直すのが無難かも。

Juniper vSRX

vMXを試したかったところですがダウンロードできなかったので、それでも有り難くJuniper社のダウンロードサイトからvSRXの 17.3R2 をいただきました。 公式インストールガイドも有り難い限りです。

# Host macOS
$ scp ~/Downloads/media-vsrx-vmdisk-17.3R2.10.qcow2 root@172.16.216.10:/root/vsrx
$ ssh root@172.16.216.10

# EVE-NG VM
$ cd vsrx
$ mkdir /opt/unetlab/addons/qemu/vsrxng-17.3R2.10/
$ cp media-vsrx-vmdisk-17.3R2.10.qcow2 /opt/unetlab/addons/qemu/vsrxng-17.3R2.10/
$ cd /opt/unetlab/addons/qemu/vsrxng-17.3R2.10/
$ mv media-vsrx-vmdisk-17.3R2.10.qcow2 virtioa.qcow2
$ ls
virtioa.qcow2
$ /opt/unetlab/wrappers/unl_wrapper -a fixpermissions

Arista vEOS

Arista社のダウンロードサイトからバージョン 4.21.1.1F をいただく。感謝とISOも忘れずに。 公式のインストールガイド に従うのみ。

# Host macOS
$ scp ~/Downloads/
$ scp Aboot-veos-serial-8.0.0.iso vEOS-lab-4.21.1.1F.vmdk root@172.16.216.10:/root/veos
$ ssh root@172.16.216.10

# EVE-NG VM
$ cd veos
$ /opt/qemu/bin/qemu-img convert -f vmdk -O qcow2 vEOS-lab-4.21.1.1F.vmdk hda.qcow2
$ mkdir -p /opt/unetlab/addons/qemu/veos-4.21.1.1F
$ mv hda.qcow2 /opt/unetlab/addons/qemu/veos-4.21.1.1F/
$ mv Aboot-veos-serial-8.0.0.iso /opt/unetlab/addons/qemu/veos-4.21.1.1F/cdrom.iso
$ /opt/unetlab/wrappers/unl_wrapper -a fixpermissions

三位一体で起動&疎通確認

EVE-NGのWebコンソール (私の環境では http://172.16.216.10/#/login ) にアクセスしログイン。適当なlabを開始します。

先程インストールした3種のOSを選択しクソみたいな簡単なトポロジで相互接続確認。各ルータを起動するとMACが鬼の形相でファンをぶん回します。

f:id:nstgt7:20190113172555p:plain
相互接続検証トポロジー

適当にconfigしてping通っておしまい。いろいろ遊べそうです。

csr1000v#show ip interface brief
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.0.1.1        YES manual up                    up
GigabitEthernet2       10.0.2.1        YES manual up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
GigabitEthernet4       unassigned      YES NVRAM  administratively down down
csr1000v#ping 10.0.1.2
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.0.1.2, timeout is 2 seconds:
.!!!!
Success rate is 80 percent (4/5), round-trip min/avg/max = 1/2/3 ms
csr1000v#ping 10.0.2.2
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.0.2.2, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 8/13/30 ms

veos#ping 10.0.3.1 repeat 1
PING 10.0.3.1 (10.0.3.1) 72(100) bytes of data.
80 bytes from 10.0.3.1: icmp_seq=1 ttl=64 time=24.0 ms

--- 10.0.3.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 24.002/24.002/24.002/0.000 ms

ちなみに1: ターミナルでtelnetできない問題

labのトポロジ図でルータのアイコンをクリックするとターミナルが起動してtelnet接続できる訳ですが、macOS 10.13 High Sierra移行telnetコマンドは消滅してしまったため、 /usr/bin/telnet が無くそのままでは接続できませんでした。対処法は こちらを参考

要はbrew等でtelnetのバイナリを入手し、リカバリーモードで起動後にSIP (System Integrity Protection) を無効化した状態で /usr/bin 配下にtelnetのバイナリを移せばOK。 SIPの解除はこちらを参考に。自己責任で、戻し忘れの無きように。

# brewでtelnetをインストール
$ brew install telnet
$ which telnet
/usr/local/bin/telnet
$ csrutil status
System Integrity Protection status: enabled.

# MACをリカバリーモードで起動してターミナルを開く
-bash-3.2# csrutil disable

# 再起動で通常起動してターミナルを起動
$ csrutil status
System Integrity Protection status: disabled.
$ sudo cp /usr/local/bin/telnet /usr/bin
$ /usr/bin/telnet
telnet> ^C

# SIPを戻すために再度リカバリーモードで起動
-bash-3.2# csrutil enable

あと通常ではターミナルを起動すると telnet:// が紐付けられたMACのデフォルトのターミナルアプリが開きます。私はiTerm2を使っているので、この記事Change the default ssh:// handler in iTerm2 itself の項を参考に telnet:// の紐付けをiTerm2に変えました。

ちなみに2: 環境のポータビリティ

EVE-NGのlabは簡単にimport/exportすることが可能なようです。自分の手元で作ったlab環境を、同じOSイメージがインストールされた別のEVE-NG環境に持ち込むことが簡単にできるのは助かります。チーム内での検証の同期や研修等でも使えそう。

ちなみに3: パフォーマンスチューニング

はまだやってません。Nodeに対してCPU Limitを設定できるチェックボックスがありますが、有効にしてみてもあんまり変わってないような。

余談ですが各ルータへの割当メモリをケチって削ってみたところちゃんと動きませんでした。そりゃそうだ。

  • Cisco CSR1000V
    • メモリをデフォルトの3072MBから2048MBに変更 → インターフェースが認識されない
  • Juniper vSRX
    • メモリをデフォルトの4096MBから2048MBに変更 → そもそも起動しない

おわりに

OSイメージさえあれば簡単にインストール可能だし、(あまり触れていませんが)Webコンソールも扱いやすく、個人的にはGNS3より使いやすいと思います。スペック盛り盛りのサーバを用意すれば検証が捗りそうです。