コンテンツにスキップ

PodにNICを追加する

Podは既定でeth0を1枚だけ持ちます。juneau.loutres.me/networksアノテーションを書くと、別のSubnetやL2Networkに繋がるNICや、ElasticIPを持つNICを追加することができます。eth0自体もこのアノテーションで書けます。 このガイドでは、アプリ用のSubnetに置いたPodへ管理用のSubnetのNICをもう1枚生やし、管理用Subnetの側からだけ届く経路を作ります。

このガイドで構築するもの

  • 専用Vpc (multi-vpc) とSubnet 2つ
    • app-subnet (10.90.0.0/24): Podのeth0を置くSubnet
    • mgmt-subnet (10.90.1.0/24): 追加NICを置くSubnet
  • eth0がapp-subnet、eth1がmgmt-subnetのPod
  • mgmt-subnetにいる運用ツールのPodから、追加NICのアドレスへ到達できること

前提条件

  • Juneauのcontroller/daemonが動作しているクラスター
  • kubectlが利用可能なこと

手順

1. Vpcと2つのSubnetを作成

apiVersion: juneau.loutres.me/v1alpha1
kind: Vpc
metadata:
  name: multi-vpc
---
apiVersion: juneau.loutres.me/v1alpha1
kind: Subnet
metadata:
  name: app-subnet
spec:
  vpc: multi-vpc
  cidr: 10.90.0.0/24
---
apiVersion: juneau.loutres.me/v1alpha1
kind: Subnet
metadata:
  name: mgmt-subnet
spec:
  vpc: multi-vpc
  cidr: 10.90.1.0/24

2. 2枚のNICを持つPodを作成

juneau.loutres.me/subnetはこれまで通りeth0の指定です。追加のNICはjuneau.loutres.me/networksにJSONの配列で書きます。

apiVersion: v1
kind: Pod
metadata:
  name: app
  annotations:
    juneau.loutres.me/subnet: app-subnet
    juneau.loutres.me/networks: |
      [
        {"interface": "eth1", "subnet": "mgmt-subnet"}
      ]
spec:
  containers:
    - name: app
      image: nginx:1.27

配列の要素は次のフィールドを取ります。subnetl2NetworkelasticIPはNICの接続先で、どれか1つだけを書きます。

フィールド 必須 意味
interface Podの中でのNICの名前。DNS-1123ラベルで8文字以内
subnet 接続先のどれか1つ 接続先のSubnet。eth0と別のVpcでも構いません
l2Network 接続先のどれか1つ 接続先のL2Network
elasticIP 接続先のどれか1つ NICに直接持たせる、Podと同じnamespaceのElasticIP
address 要求するIPv4アドレス。未指定ならSubnetやL2Networkのプールから割り当てます。elasticIPのエントリには書けません
securityGroups このNICに適用するSecurityGroup。2つまで。elasticIPのエントリには書けません

interfaceが8文字までなのは、ホスト側のvethに<interface>+<コンテナID>という名前を付けるからです。Linuxのインターフェース名は15文字までなので、コンテナIDを識別できるだけ残すと8文字が上限になります。

Podが起動したら、NICごとにNetworkInterfaceとNetworkEndpointができていることを確認します。名前は<Pod名>.<interface>です。

$ kubectl get networkinterface
NAME       NODE       SUBNET        L2NETWORK   ELASTICIP   ADDRESS         PHASE
app.eth0   worker-1   app-subnet                            10.90.0.5/24    Ready
app.eth1   worker-1   mgmt-subnet                           10.90.1.4/24    Ready

3. 追加NICへの到達を確認

mgmt-subnetにPodをもう1つ置いて、eth1のアドレスへ通信します。

apiVersion: v1
kind: Pod
metadata:
  name: ops
  annotations:
    juneau.loutres.me/subnet: mgmt-subnet
spec:
  containers:
    - name: ops
      image: nicolaka/netshoot:v0.16
      command: ["sleep", "3600"]
$ kubectl exec ops -- curl -sS http://10.90.1.4 | head -1
<!DOCTYPE html>

注意点

eth0の書き方は3通りです

コンテナランタイムはCNIの結果にeth0という名前のNICとそのアドレスがあることを要求します。ないとRunPodSandboxが失敗するので、どのPodにもeth0は必ずあります。eth0は次のどれか1つで書きます。

書き方 eth0の接続先
networksinterface: eth0のエントリを書く エントリのsubnetl2NetworkelasticIP
juneau.loutres.me/elastic-ipにElasticIPの名前を書く そのElasticIP
どちらも書かない juneau.loutres.me/subnetのSubnet。未指定ならdefaultjuneau.loutres.me/addressjuneau.loutres.me/security-groupsもeth0に効きます

書き方を混ぜるとwebhookが拒否します。networksにeth0のエントリがあるときは、juneau.loutres.me/subnetjuneau.loutres.me/addressjuneau.loutres.me/security-groupsjuneau.loutres.me/elastic-ipのどれも書けません。値が空でも、キーがあるだけで拒否します。juneau.loutres.me/elastic-ipを書いたときは、juneau.loutres.me/subnetjuneau.loutres.me/addressjuneau.loutres.me/security-groupsを書けません。

eth0をSubnetに置くPodは、networksでも従来のアノテーションでも同じように動きます。

annotations:
  juneau.loutres.me/networks: |
    [
      {"interface": "eth0", "subnet": "app-subnet"},
      {"interface": "eth1", "subnet": "mgmt-subnet"}
    ]

eth0で決まるもの

eth0はPodのアドレスそのものでもあります。次のものはすべてeth0だけを見ます。

  • pod.status.podIPとServiceのバックエンド
  • kubeletのプローブ
  • DNSの設定

eth0をSubnet以外に置くと、これらの扱いが変わります。

eth0がSubnet eth0がL2Network eth0がElasticIP
dnsPolicy Subnetの仮想DNSを注入 Defaultに書き換え Defaultに書き換え
NodeからPodへの経路 これまで通り 作りません <ElasticIP>/32をPodのvethへ
kubeletのプローブ これまで通り --enable-probe-rewriteで書き換え Nodeの経路で届きます
Serviceのバックエンド なれる なれない なれない
ClusterIP Serviceへ これまで通り gatewayを跨いで届く 届かない

SubnetのPodには、Subnetごとの仮想DNSを注入しています。L2NetworkとElasticIPのNICにはその仮想DNSが無いので、Nodeのresolv.confのDNSサーバを使うDefaultにします。そのNICからDNSサーバに届く経路が要ります。

書き換えるのは、作成時のdnsPolicyが未指定、ClusterFirstClusterFirstWithHostNetのときだけです。NoneDefaultはそのまま残します。juneau.loutres.me/dns-inject-skip: "true"を付けたPodは書き換えません。

eth0をL2Networkに置く

eth0に使えるのは、spec.cidrspec.gatewayの両方を持つL2Networkだけです。どちらかが無いとwebhookが拒否します。eth0にはアドレスが要り、Podのデフォルトルートの行き先としてgatewayが要るからです。

annotations:
  juneau.loutres.me/networks: |
    [
      {"interface": "eth0", "l2Network": "lab-net"}
    ]

デフォルトルートはL2Networkのstatus.gatewayに向きます。

dnsPolicyはDefaultになり、kubeletがNodeに渡しているresolv.confのDNSサーバを使います。そのDNSサーバには、eth0のアドレスからgateway経由で届く必要があります。kindのNodeが使うdockerの内蔵DNSのように、Nodeの中でしか使えないDNSサーバだと名前解決ができません。

NodeからPodへの経路は作りません。L2Networkは必ずcustom Vpcに属していて、Vpc同士でアドレスが重なってよいので、Nodeに経路を入れると別のVpcのPodとぶつかります。kubeletのhttpGetやtcpSocketのプローブを使うなら、custom VpcのSubnetのPodと同じく、controllerの--enable-probe-rewriteを有効にしてください。プローブがPodのnetwork namespaceの中から実行されるように書き換わります。

ElasticIPを持つNIC

elasticIPのエントリやjuneau.loutres.me/elastic-ipで、NICにElasticIPのアドレスをそのまま持たせることができます。NICはどのVpcにも属さず、SecurityGroupもNetworkACLも効きません。1つのElasticIPを持てるのは1つのNICだけで、1つのPodの2枚のNICに同じElasticIPを書くとwebhookが拒否します。

詳しい手順と制限はPodのNICにElasticIPを直接持たせるを参照してください。

NIC同士のCIDRは重ならないこと

追加NICのSubnetは別のVpcのものでも構いませんが、Pod内の2枚のNICのCIDRが重なる指定はwebhookが拒否します。重なっているとPodの中にオンリンク経路が2本並び、どちらに出ていくかが不定になります。

デフォルトルートは1本だけです

SubnetとL2Networkの追加NICにはデフォルトルートを入れません。mainテーブルのデフォルトルートはeth0の1本だけで、追加NICから出ていくのは、そのNICのSubnetやL2Networkの中に閉じた通信になります。追加NICのVpcの他のSubnetへ届かせたい場合は、Podの中で経路を足してください。

ElasticIPの追加NICだけは、専用のルートテーブルにデフォルトルートを入れ、送信元がそのElasticIPのパケットをそのテーブルへ向けるルールを足します。mainテーブルには何も足さないので、eth0のデフォルトルートはそのままです。仕組みはNetworkInterfaceを参照してください。

NICを減らすとNetworkInterfaceも消えます

networksから要素を消してPodを更新すると、PodControllerが余ったNetworkInterfaceを削除します。動いているPodからNICが抜けるわけではありませんが、NetworkEndpointが一緒に消えてデータプレーンの設定が外れるので、そのNICの通信は止まります。NICの増減はPodを作り直して反映させてください。

全部のNICが揃うまでPodは起動しません

CNIのADDは、Podが要求したNICのアドレスがすべて確定してからvethを作ります。追加NICのSubnetが存在しない、あるいはアドレスが枯渇しているときは、eth0の分も含めてPodがContainerCreatingのまま止まります。kubectl get networkinterfaceでどのNICがPendingかを見てください。