Multusで遊ぶ - OpenShift v4.3 IPI on AWS編

Red Hatでコンサルタントをしている織です。こちらの記事では、upstreamのKubernetesとMultusを使ってKVM環境上でMultusを検証しました。本記事では、IPI方式でAWS上に構築したOCP (OpenShift Container Platform) v4.3を使ってMultusを動かしてみます。

いきなりIPIと書きましたが、OpenShift v4では、環境構築の方法が大きく2種類あり、IPIはそのうちのひとつです。

  • IPI(Installer Provisioned Infrastructure)方式のインストール - ネットワーク、ロードバランサ、DNSレコード、仮想マシンインスタンス等、OpenShiftのインストールに必要なインフラのコンポーネントをインストーラが自動的に作成します。workerノードのオートスケールができるようになったりします。インフラをAPIで制御できる環境(例えばIaaS)上にOpenShiftクラスターを構築するときはこちらが便利です。

  • UPI(User Provisioned Infrastructure)方式のインストール - ネットワーク、ロードバランサ、DNSレコード、仮想マシンインスタンス等、OpenShiftのインストールに必要なインフラのコンポーネントは、あらかじめユーザーが自身で用意しておく必要があります。インストーラはその上にOpenShiftをインストールします。インフラをAPIで制御できない環境(例えばオンプレのベアメタル環境)や、IPIが作る構成だと要件に合わない場合、workerノードに(Red Hat CoreOSではなく)RHELを使いたい場合等はUPIを使います。

IPIでAWS上にOpenShiftクラスターを構築すると、

  • 作成されるEC2インスタンスはNIC(ENI)を1個しか持たない
  • ノードは異なるAZにまたがって分散配置

という構成になります。AWS上のOpenShiftでMultusを使う場合は、おそらくUPIで環境を作ることになるかと思うのですが、今回はIPIインストールで構築した環境に対して、少しカスタマイズしてMultusを使えるようにしました。特に深い理由はありません。

実際に行ったカスタマイズは以下です。

  • 各ノードに対してNICを追加し、追加NICで通信するためのVPCサブネットを作成する
  • 追加NICを使って異なるAZにいるworkerノード上のPodと通信できるようにstatic routeを設定する

OpenShift v4では、クラスターに対する設定は全てOperator経由で行う作りになっています。例えばネットワーク周りの設定はCluster Network Operator(CNO)が司ります。Multusの設定をする際、前の記事ではNetworkAttachmentDefinitionのCustom Resourceを定義するmanifestを書いてkubectl createしましたが、OpenShiftではCNO経由でNetworkAttachmentDefinitionを作るよう、CNOに対して指示します。この辺りが本記事で一番言いたかったことなのですが、いろいろ余計なことを書いたので長くなってしまいました。ご容赦ください...

OpenShiftクラスターの構築

AWS上にOpenShift v4.3.0[1]の環境を構築します。今回構築に使ったIPIという方式では、VPC、ELB、Route53のゾーン登録等、必要なAWSリソースは全て自動で作成し、その上でRHCOS(Red Hat CoreOS)を使ったEC2インスタンスを起動してOpenShiftクラスターを構成する、という一連の作業をコマンド一撃でやってくれます。

$ openshift-install create cluster

AWS上でIPI方式でOpenShiftをインストールする手順の詳細は下記をご参照ください。

また、本検証ではやりませんでしたが、もしUPIを使ってインストールする場合は下記をご参照ください。

Multusのために環境をカスタマイズ

IPIインストールでAWSにOpenShiftクラスターを構築すると、下図のような構成になります(EC2インスタンスとVPCのプライベートネットワークのみ記載しています)。

f:id:orimanabu:20200124232455p:plain
IPIインストールでAWSにできるOpenShiftクラスター

MultusでもうひとつNICを使うので、これをカスタマイズして、最終的に下図のようにしたいです。

f:id:orimanabu:20200124232641p:plain
Multusを動かしたい構成

そのために、

  • 各AZにサブネットを追加し
  • 追加サブネットごとに接続するENIを作成し
  • 作成したENIを各workerノードにアタッチする

というカスタマイズをします。

サブネットの追加

Multusで追加接続するためのサブネットを各リージョンに作成します。IPv4のアドレスブロックは、VPCのアドレスブロックから適当に切り出します。本環境では

リージョン サブネット名 アドレスブロック
ap-northeast-1a ocp43-multus-1a 10.0.201.0/24
ap-northeast-1c ocp43-multus-1c 10.0.202.0/24
ap-northeast-1d ocp43-multus-1d 10.0.203.0/24

を作成しました。AWS CLIでやるなら

$ aws ec2 create-subnet --availability-zone ${az} --cidr-block ${cidr} --vpc-id ${vpc_id}
$ aws ec2 create-tags --resources ${multus_subnet_id} --tags Key=Name,Value=${multus_subnet_name}

みたいな感じです。

ENIの作成

各リージョンのworkerノードを追加したサブネットに接続するため、追加サブネットごとにENIを作成します。 今回は下記のIPアドレスを使うことにします。

サブネット プライマリのIPv4アドレス セカンダリのIPv4アドレス
ocp43-multus-1a 10.0.201.11 10.0.201.21 〜 10.0.201.29
ocp43-multus-1c 10.0.202.11 10.0.202.21 〜 10.0.202.29
ocp43-multus-1d 10.0.203.11 10.0.203.21 〜 10.0.203.29

ipvlanでPodに付与するIPアドレスを、あらかじめENIのセカンダリプライベートIPv4アドレスとして列挙しておきます。

$ aws ec2 create-network-interface --subnet-id ${multus_subnet_id} \
--description "for multus, subnet: ${multu_subnet_name}, az:${az}" \
--private-ip-addresses Primary=true,PrivateIpAddress=${prefix}.11 \
Primary=false,PrivateIpAddress=${prefix}.21 \
Primary=false,PrivateIpAddress=${prefix}.22 \
Primary=false,PrivateIpAddress=${prefix}.23 \
Primary=false,PrivateIpAddress=${prefix}.24 \
Primary=false,PrivateIpAddress=${prefix}.25 \
Primary=false,PrivateIpAddress=${prefix}.26 \
Primary=false,PrivateIpAddress=${prefix}.27 \
Primary=false,PrivateIpAddress=${prefix}.28 \
Primary=false,PrivateIpAddress=${prefix}.29
$ aws ec2 create-tags --resources ${interface_id} --tags Key=Name,Value=eni-${multus_subnet_name}

ENIをworkerノードへアタッチ

作成したENIを、各workerノードにアタッチします。

$ aws ec2 attach-network-interface --device-index 1 --instance-id ${instance_id} --network-interface-id ${interface_id}

workerノードの確認

ENIをworkerノードにアタッチすると、workerノードのRHCOS(カーネル、ユーザーランドのコードはRHEL8と全く同じ[4])のカーネルが動的に追加インターフェースをens4として認識してくれます。workerノードでそのことを確認してみます。

OpenShift v4.xでmaster/workerノードに入ってOSコマンドを実行する方法は2つあります。

  1. sshログインする - インストーラ(openshift-installerコマンド)実行ノードのssh-agentに鍵が登録されていれば、それをRHCOSのユーザーcoreのauthorized_keysに登録してくれます。
  2. oc debug nodeを使用する - oc get nodeでノード名を調べ、oc debug node/NODENAMEを実行します。例: oc debug node/ip-10-0-133-117.ap-northeast-1.compute.internal

どちらの方法で確認しても構いませんが、1.を実施するには、IPIインストールで作成したVPCサブネット上に外部からアクセスするための踏み台サーバが必要になります。

どちらかの方法でworkerノードに入って、インスタンスのインターフェースを確認します。

# ip addr show dev ens3
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    link/ether 06:41:25:41:65:46 brd ff:ff:ff:ff:ff:ff
    inet 10.0.133.117/20 brd 10.0.143.255 scope global dynamic noprefixroute ens3
       valid_lft 3255sec preferred_lft 3255sec
    inet6 fe80::b6d2:f22:84d1:e350/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
# ip addr show dev ens4
31: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    link/ether 06:e5:2e:bf:58:3a brd ff:ff:ff:ff:ff:ff
    inet 10.0.201.11/24 brd 10.0.201.255 scope global dynamic noprefixroute ens4
       valid_lft 2308sec preferred_lft 2308sec
    inet6 fe80::2af9:7855:12d6:f03d/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

ens3が元々インスタンスが持っていたENIで、ens4が先ほど追加したENIです。

ルーティングテーブルを見てみます。

# ip route show
default via 10.0.128.1 dev ens3 proto dhcp metric 100
default via 10.0.201.1 dev ens4 proto dhcp metric 101
10.0.128.0/20 dev ens3 proto kernel scope link src 10.0.140.137 metric 100
10.0.201.0/24 dev ens4 proto kernel scope link src 10.0.201.11 metric 101
10.128.0.0/14 dev tun0 scope link
172.30.0.0/16 dev tun0

ens4にもDHCPからもらったデフォルトゲートウェイが設定されていて気持ち悪いので、ens4ではDHCPでIPアドレスだけをつける(DHCPによるデフォルトゲートウェイとDNS設定をしない)ようにします。

# nmcli c s
NAME                UUID                                  TYPE      DEVICE
Wired connection 1  e2fe62b6-e972-3d43-8023-320f467fec57  ethernet  ens3
Wired connection 2  76577e5a-43d2-3f3d-b2d9-a93a067cf42c  ethernet  ens4

ens4のConnection nameであるWired connection 2に対して、ipv4.ignore-auto-dnsipv4.ignore-auto-routesを有効化します。

# nmcli c m 'Wired connection 2' ipv4.ignore-auto-dns yes ipv4.ignore-auto-routes yes
# sudo nmcli c d 'Wired connection 2'
Connection 'Wired connection 2' successfully deactivated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/2)
# sudo nmcli c u 'Wired connection 2'
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/3)

これで、余分なデフォルトゲートウェイの設定が消えました。

# ip r
default via 10.0.128.1 dev ens3 proto dhcp metric 100
10.0.128.0/20 dev ens3 proto kernel scope link src 10.0.140.137 metric 100
10.0.201.0/24 dev ens4 proto kernel scope link src 10.0.201.11 metric 101
10.128.0.0/14 dev tun0 scope link
172.30.0.0/16 dev tun0

各workerノードに対してこの操作を実施しておきます。

Multusのセットアップ

次にMultusの設定を行います。基本的にこちらの手順に従います。

まずおさらいですが、upstreamのKubernetes+Multusを使った前の記事では、

---
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: ipvlan-conf
spec:
  config: '{
            "cniVersion": "0.3.1",
            "plugins": [
                {
                    "type": "ipvlan",
                    "capabilities": { "ips": true },
                    "master": "eth1",
                    "ipam": {
                        "type": "static"
                    }
                }, {
                    "capabilities": { "mac": true },
                    "type": "tuning"
                } ]
        }'

のようなmanifestを使ってNewtorkAttachmentDefinitionのCustom Resourceを作成しました。

一方OpenShift v4.xでは、CNIの設定は全てCNO (Cluster Network Operator)[5]が司る仕組みになっています。したがいまして、CNOに対してNetworkAttachmentDefinitionを作成してもらうよう指示する必要があります。具体的には、

$ oc edit networks.operator.openshift.io cluster

を実行し、spec.additionalNetworksの中に、NetworkAttachmentDefinitionのCustom Resourceで設定する内容を記載します。本記事の検証では

spec:
  additionalNetworks:
  - name: network-ipvlan
    type: Raw
    rawCNIConfig: '{
      "cniVersion": "0.3.1",
      "type": "ipvlan",
      "mode": "l2",
      "capabilities": {"ips": true},
      "master": "ens4",
      "ipam": {
        "type": "static",
        "routes": [
          {"dst": "10.0.202.0/24", "gw": "10.0.201.1"},
          {"dst": "10.0.203.0/24", "gw": "10.0.201.1"}
        ]
      }
    }'

のような内容を記載しました[6]。 この内容は、ap-northeast-1aリージョン(追加サブネットのIPv4アドレスブロックが10.0.201.0/24)のworkerノードにPodをデプロイしたい場合の設定です。static routeの設定は、デプロイ先のworkerノードがいるリージョンごとに変える必要がありますのでご注意ください。

上記oc edit networks.operator.openshift.io clusterで編集した内容は、oc patchを使って

oc patch networks.operator.openshift.io cluster --type merge -p '{"spec": {"additionalNetworks": [{"name": "network-ipvlan", "type": "Raw", "rawCNIConfig": "{\"cniVersion\": \"0.3.1\", \"type\": \"ipvlan\", \"mode\": \"l2\", \"capabilities\": { \"ips\": true }, \"master\": \"ens4\", \"mode\": \"l2\", \"capabilities\": { \"ips\": true }, \"ipam\": {\"type\": \"static\", \"routes\": [{ \"dst\": \"10.0.202.0/24\", \"gw\": \"10.0.201.1\" }, { \"dst\": \"10.0.203.0/24\", \"gw\": \"10.0.201.1\" }]}}"}]}}'

もしくは

oc patch networks.operator.openshift.io cluster --type json -p '
[{
   "op": "add",
   "path": "/spec/additionalNetworks",
   "value": []
 },{
   "op": "add",
   "path": "/spec/additionalNetworks/0",
   "value": {
     name: "network-ipvlan",
     type: "Raw",
     rawCNIConfig: "{
       \"cniVersion\": \"0.3.1\",
       \"type\": \"ipvlan\",
       \"mode\": \"l2\",
       \"capabilities\": { \"ips\": true },
       \"master\": \"ens4\",
       \"ipam\": {
         \"type\": \"static\",
         \"routes\": [
           { \"dst\": \"10.0.202.0/24\", \"gw\": \"10.0.201.1\" },
           { \"dst\": \"10.0.203.0/24\", \"gw\": \"10.0.201.1\" }
         ]
       }
     }"
   }
 }
]'

のように反映させることもできます。

CNOの設定変更が終わったら、oc get net-attach-defを実行し、作成したNetworkAttachmentDefinitionの内容を確認します。

$ oc get net-attach-def network-ipvlan -o yaml
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  creationTimestamp: "2020-01-24T14:57:36Z"
  generation: 1
  name: network-ipvlan
<snip>
spec:
  config: '{"cniVersion": "0.3.1", "type": "ipvlan", "mode": "l2", "capabilities":
    { "ips": true }, "master": "ens4", "mode": "l2", "capabilities": { "ips": true
    }, "ipam": {"type": "static", "routes": [{ "dst": "10.0.202.0/24", "gw": "10.0.201.1"
    }, { "dst": "10.0.203.0/24", "gw": "10.0.201.1" }]}}'

Podのデプロイ

1つ目のPodのデプロイ

本検証では、下記のような設定のPodをデプロイします。

  • ipvlanでホストのens4経由で追加サブネットに接続する
  • Podのデフォルトゲートウェイは、デフォルトCNIプラグインであるOpenShift-SDN側(ens3で接続している、IPIインストーラが作成したサブネット)にする
  • ipvlanで接続した追加サブネット経由でPod間通信ができるよう、static routeを設定する

まず、ap-northeast-1aのAZにいるworkerノードにデプロイすることを考えます。準備として、各ノードがどのAZにいるかを確認します。

$ aws ec2 describe-instances --filter Name=vpc-id,Values=${vpc_id} | jq -r '.Reservations[] | .Instances[] | [.PrivateDnsName, (.Tags[] | select(.Key == "Name").Value)] | @text'
["ip-10-0-158-197.ap-northeast-1.compute.internal","ocp43-9ld5l-worker-ap-northeast-1c-cphxt"]
["ip-10-0-149-79.ap-northeast-1.compute.internal","ocp43-9ld5l-master-1"]
["ip-10-0-143-118.ap-northeast-1.compute.internal","ocp43-9ld5l-master-0"]
["ip-10-0-133-117.ap-northeast-1.compute.internal","ocp43-9ld5l-worker-ap-northeast-1a-vcnxn"]
["ip-10-0-167-226.ap-northeast-1.compute.internal","ocp43-9ld5l-master-2"]
["ip-10-0-162-156.ap-northeast-1.compute.internal","ocp43-9ld5l-worker-ap-northeast-1d-x62nl"]
["ip-10-0-46-214.ap-northeast-1.compute.internal","bastion-ocp43"]

ap-northeast-1aにいるworkerノードはip-10-0-133-117.ap-northeast-1.compute.internalであることがわかりました。

次にそのノードのラベルを確認します。

$ oc get node ip-10-0-133-117.ap-northeast-1.compute.internal --show-labels
NAME                                              STATUS   ROLES    AGE     VERSION   LABELS
ip-10-0-133-117.ap-northeast-1.compute.internal   Ready    worker   3d12h   v1.16.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=m4.large,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=ap-northeast-1,failure-domain.beta.kubernetes.io/zone=ap-northeast-1a,kubernetes.io/arch=amd64,kubernetes.io/hostname=ip-10-0-133-117,kubernetes.io/os=linux,node-role.kubernetes.io/worker=,node.openshift.io/os_id=rhcos

ノードのセレクタとして、ラベルkubernetes.io/hostname=ip-10-0-133-117を使うことにします。

このラベルをnodeSelectorで指定し、ipvlanの追加インターフェースに対して10.0.201.27/24を付与するPodipvlsan-1のmanifestは次のようになります。

---
apiVersion: v1
kind: Pod
metadata:
  name: ipvlan-1
  labels:
    app: multus-ipvlan
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
    {
      "name": "network-ipvlan",
      "ips": [ "10.0.201.27/24" ]
    }
    ]'
spec:
  containers:
  - name: centos-tools
    image: docker.io/centos/tools:latest
    command:
    - /sbin/init
    securityContext:
      privileged: true
  nodeSelector:
    kubernetes.io/hostname: ip-10-0-133-117

このファイルをipvlan-ocp-27-1.yamlに保存して、下記コマンドでPodをデプロイします。

$ oc create -f ipvlan-ocp-27-1.yaml

Podが起動したら、期待する設定が反映されているか確認します。

$ oc rsh ipvlan-1 ip -d 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 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    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
3: eth0@if35: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue state UP group default
    link/ether 0a:58:0a:80:02:1d brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.128.2.29/23 brd 10.128.3.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::58e8:eeff:fe0a:45bf/64 scope link
       valid_lft forever preferred_lft forever
4: net1@if31: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UNKNOWN group default
    link/ether 06:e5:2e:bf:58:3a brd ff:ff:ff:ff:ff:ff promiscuity 0
    ipvlan  mode l2 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.0.201.27/24 brd 10.0.201.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::6e5:2e00:1bf:583a/64 scope link
       valid_lft forever preferred_lft forever

Multusによるipvlanの追加インターフェースがnet1として、指定したIPアドレスが付与した状態で存在することが確認できました。次にPodのルーティングテーブルを確認します。

$ oc rsh ipvlan-1 ip route show
default via 10.128.2.1 dev eth0
10.0.201.0/24 dev net1 proto kernel scope link src 10.0.201.27
10.0.202.0/24 via 10.0.201.1 dev net1
10.0.203.0/24 via 10.0.201.1 dev net1
10.128.0.0/14 dev eth0
10.128.2.0/23 dev eth0 proto kernel scope link src 10.128.2.29
172.30.0.0/16 via 10.128.2.1 dev eth0
224.0.0.0/4 dev eth0

無事、NetworkAttachmentDefinitionで仕込んでおいた10.0.202.0/24および10.0.203.0/24宛てのstatic routeが設定されています。 このstatic routeにより、ipvlanの追加インターフェースを使った通信をリージョン(workerノード)をまたいで行うことができるようになります。

2つ目のPodのデプロイ

次は、ap-northeast-1cのAZにいるworkerノードにPodをデプロイします。 このAZの追加サブネットは10.0.202.0/24であるため、他リージョンの追加サブネットと通信するためには

宛先 nexthop
10.0.201.0/24 10.0.202.1
10.0.203.0/24 10.0.202.1

のようなstatic routeが必要になります。これを反映するように、NetworkAttachmentDefinitionnetwork-ipvlanに書いたstatic route設定の内容を次のように変更します。

spec:
  additionalNetworks:
  - name: network-ipvlan
    type: Raw
    rawCNIConfig: '{
      "cniVersion": "0.3.1",
      "type": "ipvlan",
      "mode": "l2",
      "capabilities": {"ips": true},
      "master": "ens4",
      "ipam": {
        "type": "static",
        "routes": [
          {"dst": "10.0.201.0/24", "gw": "10.0.202.1"},
          {"dst": "10.0.203.0/24", "gw": "10.0.202.1"}
        ]
      }
    }'

前述のoc edit networks.operator.openshift.io clusterもしくは下記のoc patchで変更を実施します。

$ oc patch networks.operator.openshift.io cluster --type merge -p '{"spec": {"additionalNetworks": [{"name": "network-ipvlan", "type": "Raw", "rawCNIConfig": "{\"cniVersion\": \"0.3.1\", \"type\": \"ipvlan\", \"mode\": \"l2\", \"capabilities\": { \"ips\": true }, \"master\": \"ens4\", \"mode\": \"l2\", \"capabilities\": { \"ips\": true }, \"ipam\": {\"type\": \"static\", \"routes\": [{ \"dst\": \"10.0.201.0/24\", \"gw\": \"10.0.202.1\" }, { \"dst\": \"10.0.203.0/24\", \"gw\": \"10.0.202.1\" }]}}"}]}}'

network-ipvlanが期待する内容に変わったことが確認できたら、次のようなmanifestでPodを起動します。

---
apiVersion: v1
kind: Pod
metadata:
  name: ipvlan-2
  labels:
    app: multus-ipvlan
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
    {
      "name": "network-ipvlan",
      "ips": [ "10.0.202.27/24" ]
    }
    ]'
spec:
  containers:
  - name: centos-tools
    image: docker.io/centos/tools:latest
    command:
    - /sbin/init
    securityContext:
      privileged: true
  nodeSelector:
    kubernetes.io/hostname: ip-10-0-158-197

nodeSelectorとしてデプロイ先のAZにいるworkerノードについたラベルを指定し、annotationでPodのIPアドレスを指定しています。 Podのデプロイができたら、Podに入ってIPアドレスを確認します。

$ oc rsh ipvlan-2 ip -d 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 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    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
3: eth0@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue state UP group default
    link/ether 0a:58:0a:83:00:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.131.0.14/23 brd 10.131.1.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::9010:bfff:fefb:aad7/64 scope link
       valid_lft forever preferred_lft forever
4: net1@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UNKNOWN group default
    link/ether 0a:cd:aa:50:a0:b8 brd ff:ff:ff:ff:ff:ff promiscuity 0
    ipvlan  mode l2 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 10.0.202.27/24 brd 10.0.202.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::acd:aa00:150:a0b8/64 scope link
       valid_lft forever preferred_lft forever

ルーティングテーブルも確認します。

$ oc rsh ipvlan-2 ip route show
default via 10.131.0.1 dev eth0
10.0.201.0/24 via 10.0.202.1 dev net1
10.0.202.0/24 dev net1 proto kernel scope link src 10.0.202.27
10.0.203.0/24 via 10.0.202.1 dev net1
10.128.0.0/14 dev eth0
10.131.0.0/23 dev eth0 proto kernel scope link src 10.131.0.14
172.30.0.0/16 via 10.131.0.1 dev eth0
224.0.0.0/4 dev eth0

このAZ用のstatic route設定が無事反映されていることが確認できました。さらにPodipvlan-1と疎通できることも確認しておきます。

$ oc rsh ipvlan-2 ping -c 3 10.0.201.27
PING 10.0.201.27 (10.0.201.27) 56(84) bytes of data.
64 bytes from 10.0.201.27: icmp_seq=1 ttl=64 time=2.35 ms
64 bytes from 10.0.201.27: icmp_seq=2 ttl=64 time=2.34 ms
64 bytes from 10.0.201.27: icmp_seq=3 ttl=64 time=2.43 ms

--- 10.0.201.27 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 2.345/2.378/2.432/0.038 ms

コンテナホストでのtcpdumpの実行

実際に追加サブネットを通って通信できているかを、ホスト上でtcpdumpして確認したくなるかもしれません。コンテナホストのRHCOSはコンテナ実行に特化しているため、tcpdump等のツールは入っていないのですが、support-toolsというコンテナを使うとtcpdumpできるようになります。

まず、1つ目のPodをデプロイしたノードに入ります。

$ oc debug node/ip-10-0-133-117.ap-northeast-1.compute.internal
Starting pod/ip-10-0-133-117ap-northeast-1computeinternal-debug ...
To use host binaries, run `chroot /host`
Pod IP: 10.0.133.117
If you don't see a command prompt, try pressing enter.
sh-4.2# chroot /host
sh-4.4#

次にtoolboxというコマンドを実行します[7]。このコマンドを実行すると、support-toolsコンテナを取得し、privilegedかつホストネットワークを掴んだ状態でデプロイし、その中に入ります。これで、ホストのインターフェースに対してtcpdumpが使えるようになります[8]。

sh-4.4# toolbox
Trying to pull registry.redhat.io/rhel8/support-tools...
Getting image source signatures
Copying blob 1457434f891b done
Copying blob cb3c77f9bdd8 done
Copying blob fd8daf2668d1 done
Copying config 517597590f done
Writing manifest to image destination
Storing signatures
517597590ff4236b0e5e3efce75d88b2b238c19a58903f59a018fc4a40cd6cce
Spawning a container 'toolbox-' with image 'registry.redhat.io/rhel8/support-tools'
Detected RUN label in the container image. Using that as the default...
command: podman run -it --name toolbox- --privileged --ipc=host --net=host --pid=host -e HOST=/host -e NAME=toolbox- -e IMAGE=registry.redhat.io/rhel8/support-tools:latest -v /run:/run -v /var/log:/var/log -v /etc/machine-id:/etc/machine-id -v /etc/localtime:/etc/localtime -v /:/host registry.redhat.io/rhel8/support-tools:latest
[root@ip-10-0-133-117 /]#

裏でipvlan-2からipvlan-1に対してpingを打ち続けながら、このsupport-toolsコンテナの中でens4に対してtcpdumpを実行してみます。

[root@ip-10-0-133-117 /]# tcpdump -enni ens4 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens4, link-type EN10MB (Ethernet), capture size 262144 bytes
15:59:21.983883 0a:cd:aa:50:a0:b8 > 0a:65:d7:a5:30:62, ethertype IPv4 (0x0800), length 98: 10.0.202.27 > 10.0.201.27: ICMP echo request, id 16088, seq 4, length 64
15:59:21.986235 0a:65:d7:a5:30:62 > 0a:cd:aa:50:a0:b8, ethertype IPv4 (0x0800), length 98: 10.0.201.27 > 10.0.202.27: ICMP echo reply, id 16088, seq 4, length 64
15:59:22.985390 0a:cd:aa:50:a0:b8 > 0a:65:d7:a5:30:62, ethertype IPv4 (0x0800), length 98: 10.0.202.27 > 10.0.201.27: ICMP echo request, id 16088, seq 5, length 64
15:59:22.987738 0a:65:d7:a5:30:62 > 0a:cd:aa:50:a0:b8, ethertype IPv4 (0x0800), length 98: 10.0.201.27 > 10.0.202.27: ICMP echo reply, id 16088, seq 5, length 64
15:59:23.985892 0a:cd:aa:50:a0:b8 > 0a:65:d7:a5:30:62, ethertype IPv4 (0x0800), length 98: 10.0.202.27 > 10.0.201.27: ICMP echo request, id 16088, seq 6, length 64
15:59:23.988242 0a:65:d7:a5:30:62 > 0a:cd:aa:50:a0:b8, ethertype IPv4 (0x0800), length 98: 10.0.201.27 > 10.0.202.27: ICMP echo reply, id 16088, seq 6, length 64

ipvlan-1ipvlan-2のPod間で、ホストのens4を使ってICMP echo request/replyのやり取りをしていることが確認できました。

問題判別のヒント

NetworkAttachmentDefinitionが期待どおり設定されない場合

NetworkAttachmentDefinitionが期待どおり設定されない場合はCNO Podのログを確認してください。

まずCNOのPod名を確認します。

$ oc -n openshift-network-operator get pod
NAME                                READY   STATUS    RESTARTS   AGE
network-operator-5c7c7dc988-vlw6v   1/1     Running   0          25h

このPodに対して、oc logs を実行します。

$ oc -n openshift-network-operator logs -f network-operator-5c7c7dc988-vlw6v

Podの起動に失敗したり、Pod manifestの設定がうまくPodに反映されない場合

Podの起動に失敗したりPod manifestの設定がうまくPodに反映されない場合は、まずoc describe podの出力を確認してください。それで当たりがつかない場合は、デプロイ先のworkerノードのログを確認します。crioサービスのログにエラーメッセージが出ていないかを最初に見るのがよいと思います。

具体的には、workerノードにsshもしくはoc debug nodeでログインし、journalctlで確認します[9]。

# journalctl -u crio -f

最後に

IPIインストールで構築したOpenShiftクラスタに対してサブネットとNICを追加し、Multusを使って2つのネットワークに接続するPodをデプロイし、追加サブネット経由でPod間通信ができることを確認しました。

ネットワーク構成上、PodをデプロイするAZごとに異なるstatic routeを入れる必要があり、そのためデプロイ先のAZごとにNetworkAttachmentDefinitionのCustom Resourceを作り直しました。これはとても煩雑で、できればやりたくありません。対応策はいくつか考えられます。

対応策1: コンテナホスト側でstatic routeを設定する

Podにはstatic routeを書かず、コンテナホスト上でstatic routeを設定しても、同じようにパケットを流すことができます。 具体的には、コンテナホストにログインし、追加サブネット側のインターフェースens4に対して

# ip route add to 10.0.202.0/24 via 10.0.201.1

もしくは

# nmcli con mod ens4 +ipv4.routes "10.0.202.0/24 10.0.201.1"

のようにstatic routeを設定します (前者は再起動すると設定が消えますが、後者は再起動してもstatic route設定が反映されます)。

対応策2: Pod のmanifestに書く

例えばstaticにIPアドレスを振るIPAMの設定をPod manifestのannotationに埋め込んだように、Podに注入するstatic routeもPod manifestに埋め込むことができれば、デプロイ先のAZが変わるたびにNetworkAttachmentDefinitionのstatic route設定を変更する必要がなくなります。また、対応策1の場合は、コンテナホストで一律にstatic routeが決まってしまいますので、Podごとにstatic routeの設定を切り替えたい場合はこちらの方法が使えるとうれしそうです。この方法の実現方法についてはupstreamで議論中で[10]、OpenShift v4.3ではまだ使えません。今後のMultusの進化に期待しましょう。

謝辞

今回も、Red HatでNFVPEをしている林さんにいろいろ教えていただきました。ありがとうございました!


  1. つい最近出たばかりの新しいバージョンです!

  2. 日本語版はこちらにあります。GA直後なのに日本語のドキュメントがあるなんて素敵ですね。

  3. 日本語版はこちら

  4. RHCOS(Red Hat CoreOS)のOSイメージは、RHEL8と同じrpmパッケージを使いつつ(つまり入っているバイナリはRHEL8と同じ)、rpm-ostreeを使ってOSイメージを作成しています。rpm-ostreeはProject Atomic由来のツールで、ユーザーデータ(/etcや/var)への影響なしにOSイメージをアトミックにロールバックできる仕組みを提供します。RHCOSは、RHELのrpmパッケージ、rpm-ostree等のProject Atomic由来のツール、CoreOS社のContainer Linuxで使っていたIgnition等の仕組みを悪魔合体させたディストリビューションと言えます。RHCOS自体は(今のところ)、OCP(OpenShiftの製品版)のノードを動かすOSとしての使い方しかサポートされません(つまり汎用OSとして使うユースケースはサポートされません)。一方、Red Hat CoreOSのupstreamであるFedora CoreOSは、コンテナホストに特化した汎用OSとして使われることを想定して開発が進められています。

  5. Cluster Network Operatorについては、こちらもご参照ください。

  6. 前の記事では、NetworkAttachmentDefinitionの内容として、plugins配列の中にtype: ipvlanの設定とtype: tuningの設定を書いていましたが、OpenShift v4.3のMultusだとこの書き方がCNOにはじかれてしまうので、少し古い書き方をしています。

  7. toolboxはシェルスクリプトで、中で (1)podman login registry.redhat.io (2)podman pull registry.redhat.io/rhel8/support-tools (3)podman container runlabel RUN registry.redhat.io/rhel8/support-tools みたいな感じのことをやっています。

  8. Red Hat製品で問題判別時の情報収集に使うsosreportコマンドも、support-toolsコンテナ経由で実行できるようになります。

  9. 余談ですが、export SYSTEMD_LESS=FRXMK を実行しておくと、journalctlコマンドのページャが横スクロールせずにターミナルの右端で折り返してくれるので、ちょっとだけ幸せになれます。

  10. 対応策2を実現する実装として、cni-route-overrideが提案されています。Pod manifestのannotationに書いたstatic routeの設定を、cni-argsから差し込みます。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。