Kubernetes 用 CNI プラグイン Coil v2 の紹介

Neco プロジェクトの ymmt です。

サイボウズでは 2018 年から Kubernetes 用のネットワークプラグイン Coil を開発しています。 当時は Kubernetes の知識がチームに蓄積されておらず、いささか使い勝手が悪い仕様でした。

そこで 3 か月ほど前から設計を全面的に見直した Coil v2 の開発を開始し、先日リリースしました。 Coil v2 は多くの方にご利用いただけると思いますので、本記事にて機能と使い方を紹介します。

特徴

Coil v2 は以下のような特徴を備えた CNI プラグインです。 CNI とは、Kubernetes で使われているネットワークプラグインの規格名です。

  • 高速な Pod 間通信
  • Namespace 毎に指定できる複数の IP アドレスプール
  • IPv4/v6 シングルスタックおよびデュアルスタック
  • 任意のルーティングソフトウェアと連携可能
  • オプトイン方式で外部ネットワークへの NAT 接続を提供
  • 設定操作が kubectl で可能

以下、順を追って解説していきます。

高速な Pod 間通信

Coil は後述する 外部ネットワークへの NAT 接続機能を除き、オーバーレイネットワークを使いません。 Pod 間のパケットは加工されずに物理ネットワークを流れるため余分なオーバーヘッドはなく高速です。

Namespace 毎に指定できる複数の IP アドレスプール

大規模なデータセンターは、複数のインターネットを含む外部ネットワークへの接続を持っていることがあります。 IPv4 でネットワークを構成する場合に顕著なのですが、外部ネットワークに接続可能(ルータブル)な IP アドレスは少数しかなく、通常の Pod が持つ IP アドレスでは直接外部ネットワークにアクセスできないことがあります。

このような場合、ルータブルなアドレスを特定のノードや Pod に割り当て、ゲートウェイサーバーとして使うことで Pod に外部ネットワークへのアクセスを提供することが可能です。

Coil ではこのようなゲートウェイとなる Pod を特定の namespace で作れるようにする機能があります。 以下はインターネットに接続可能な HTTP proxy の Pod を作成する例です。

1: 外部ネットワークに接続可能なアドレスを、AddressPool というカスタムリソースで定義

apiVersion: coil.cybozu.com/v2
kind: AddressPool
metadata:
  name: internet
spec:
  blockSizeBits: 0
  subnets:
  - ipv4: 103.79.??.???/28

2: Namespace にアノテーションでどの AdressPool を使うか指定

$ kubectl create ns internet-egress
$ kubectl annotate ns internet-egress coil.cybozu.com/pool=internet

3: 当該 Namespace に proxy 用の Deployment と Service を作成

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: internet-egress
  name: squid
spec:
  ...
---
apiVersion: v1
kind: Service
metadata:
  namespace: internet-egress
  name: proxy
spec:
  ...

これで proxy.internet-egress.svc という DNS 名でインターネットに接続できるプロキシが Kubernetes クラスタ内で利用できるようになります。

IPv4/v6 シングルスタックおよびデュアルスタック

IPv4, IPv6 のシングルスタック構成に加え、デュアルスタック構成もサポートしています。 設定は、AddressPool のアドレスに IPv4, IPv6 を片方ないし両方指定するだけです。

IPv4 と IPv6 で使える機能に差は基本的にありません。注意点として、Kubernetes のデュアルスタック対応はまだアルファなのでそれに起因する機能制限はあります。

任意のルーティングソフトウェアと連携可能

Coil は他のネットワークソフトウェアと連携することを前提に設計されました。 例えば MetalLB で LoadBalancer を実装し、CalicoCilium で NetworkPolicy を実装するといった具合です。

これらのソフトウェアを組み合わせる際に障害になりがちなのが、BGP 機能です。BGP については以前記事を書いたので詳しくない方はそちらをご覧ください。

blog.cybozu.io

MetalLB や Calico は自力で BGP を話すのですが、同一のノードから複数のソフトウェアが BGP でルーターとピアすることはできないので複雑な回避策が必要になってきます。Neco ではノード上ですでに BIRD が動作して BGP でルーターと接続しているため、さらに構成に無理が生じます。

そこで、Coil は自力ではどのルーティングプロトコルも話さない設計としました。 代わりに経路広告するべき経路を Linux カーネルの使っていないルーティングテーブル(デフォルトでは 119 番)に登録します。

BIRD などは Linux カーネルのルーティングテーブルを読み取ることが可能なので、このように間接的に連携することで BGP の問題を回避することができます。また BIRD 以外のルーティングソフトウェアとも連携できますし、BGP 以外のプロトコルで経路広告することも可能になります。

この設計の代償は、手軽に動かすことができなくなる点です。Coil v1 では必ず他のルーティングソフトウェアと連携しなければ、異なるノード間での Pod 通信が動きませんでした。Coil v2 では、すべてのノードが同一の L2 ネットワーク(サブネット)にある場合に限りますが、付属の coil-router というソフトウェアを併用すれば自力でノード間のルーティングが可能です。

オプトイン方式で外部ネットワークへの NAT 接続を提供

Coil v2 の目玉となる機能です。先ほど HTTP proxy をゲートウェイ Pod として動作させる例を出しましたが、これでは HTTP/HTTPS 以外で外部と通信ができません。例えば外部にある MySQL と接続するとか、DNS サーバーに問い合わせるといった操作ができないわけです。

より汎用的なゲートウェイとしてはやはり NAT サーバーを配置したくなります。Coil では、以下のように Egress というカスタムリソースを定義することで、当該 Namespace に自動的に NAT サーバーを構築してくれます。

apiVersion: coil.cybozu.com/v2
kind: Egress
metadata:
  namespace: internet-egress
  name: nat
spec:
  destinations:
  - 0.0.0.0/0
  replicas: 2

この定義から、NAT 用の Pod を 2 つ作る Deployment と、リクエストを負荷分散する Service が自動的に作られます。

この NAT サーバーを利用する Pod は、以下のようにアノテーションをつける必要があります。 Egress を定義したら自動的にパケットがインターネットに届くようになるわけではないので、必要な Pod にのみ選択的に NAT サーバーを利用させることが可能です。

apiVersion: v1
kind: Pod
metadata:
  namespace: default
  name: nat-client
  annotations:
    egress.coil.cybozu.com/internet-egress: nat
spec:
  ...

Egress はデフォルトゲートウェイとしてではなく、特定の外部ネットワークに対して作成することもできます。 どの Egress を利用するか Pod が使い分けることで、自在に通信を制御できます。

設定操作が kubectl で可能

AddressPoolEgress はカスタムリソースなので kubectl で作成・編集できます。 また、AddressPool からどのようにアドレスブロックがノードに割り当てられているかも簡単に確認できます。

$ kubectl get addressblocks.coil.cybozu.com  | head
NAME                NODE          POOL              IPV4               IPV6
default-0           10.69.0.4     default           10.64.0.0/27
default-1           10.69.0.203   default           10.64.0.32/27
default-10          10.69.0.5     default           10.64.1.64/27
default-11          10.69.1.141   default           10.64.1.96/27
...

使い方

Coil は GitHub で OSS として公開されています。

github.com

kind で試す

kind とは、Docker 上で Kubernetes を動かすことができるツールです。 Coil は kind で動作させることができるので、簡単に試せます。

しっかりインストールして使う

以下のドキュメントを用意してあります。BIRD との連携方法も記載しています。

v1 から v2 へのオンラインマイグレーション

サイボウズの Kubernetes クラスタは Coil v1 で 2 年ほど動作していましたが、すでに v2 への移行が完了しています。 すでにプロダクションワークロードが稼働しているため、移行作業はオンラインで行う必要がありました。

ここではどのようにオンラインマイグレーションを実現したか、解説します。

基本的なアイデア

Coil v1 はデータを etcd で管理し、v2 は kube-apiserver のカスタムリソースで管理しています。 そのためデータのコンバートが必要なのですが、Pod の IP アドレス管理を v2 では各ノードのカーネルに与えた情報で管理するように方式を変更したため、すべてのデータをコンバートすることはできません。

そこで、Coil v1 が利用しているアドレスブロックは、Coil v2 では利用できない予約済みアドレスブロックとしてコンバートすることにしました。こうすることで、v1 の IP アドレス管理情報は無視して v2 で新規にアドレスブロックを払い出し、Pod を動作させられます。

Coil v1 で動いていた Pod がすべてなくなったら、予約済みアドレスブロックを削除すれば v1 の痕跡を残さず v2 に移行できたということになります。

移行ステップとツール

  1. Coil v1 のプログラムを停止

    DaemonSet と Deployment を削除することで、新規に Pod が作られなくなります。 また、etcd のデータが変更されなくなりコンバートを開始できるようになります。 既存の Pod の通信が止まったりはしません。

  2. etcd のデータをカスタムリソースにコンバート

    この際、v1 が使っているアドレスブロックは予約済みアドレスブロックにします。

  3. Coil v2 のカスタムリソースを定義

  4. 2 のデータを読み込み
  5. Coil v2 のプログラムを動作開始

    この時点から、Pod の作成が再開されます。新しく作られた Pod は v2 のアドレスブロックで管理されます。

  6. すべての hostNetwork=false な Pod を削除

    この操作で Coil v1 の Pod はクラスタから消えます。 Deployment や DaemonSet コントローラーが作り直した Pod は v2 で動作しています。

  7. 予約済みアドレスブロックを削除

  8. coild を再起動

    予約済みアドレスブロックは特殊な存在で、それが削除されたことに気づかせるため coild を再起動します。

この移行ステップの大半は、coil-migrator というツールで自動化しました。 具体的に 1, 2, 6, 7, 8 が自動化されています。

トラブル

実は、当初 8 のステップの必要性に気づかずにいました。結果、v1 時代のアドレスブロックが間違ったノードから経路広報され続けるという障害を起こしてしまいました。

コンバートツールは後付けで作ったので、既存のプログラムの想定外の状況を作り出してしまったわけです。最後に再起動するのが安全でした。

Kubernetes Meetup Tokyo #35 の資料

2020年10月28日に開催された Kubernetes Meetup Tokyo #35 で Coil v2 を紹介しました。その際に使った資料が以下です。Kubernetes のネットワーク機能をどのように実現するかを丁寧に解説していますので、よろしければこちらもご覧ください。

余談: v1 と v2 は何が違うのか

Coil v2 は v1 のコードを再利用せず、全部新規に書き起こしたのですが、そうなった背景を説明します。 Coil v1 は以下の仕様で 2018 年に作成されました。

  • データは etcd に保存

    インストール時に etcd にユーザーを作ったり認証用の証明書を発行する必要がありました。 カスタムリソースなら kube-apiserver にデータを保存できるのですが、当時はまだ作り方を知らなかったため。

  • 管理操作は専用コマンドラインツール

    etcd にデータを保存していたため。kubectl で管理ができないのは単純に不便でした。

  • 効率の悪い IP 管理機能

    これは実装が悪いのですが、Pod のアドレスをすべて etcd で集中管理していて etcd への負荷が高くなりがちでした。 各ノード内でローカルに管理することもできたわけで、Coil v2 ではそうしています。

  • 完全に機能させるために外部ルーターが必須

    Coil だけでは自力でルーティングできず、BIRD 等を別途用意しないと試験環境が作れませんでした。 お手軽とは言えませんし、CI の実行に時間もかかるしで大変でした。

  • Prometheus でメトリクスがとれない

    Coil v1 を作り始めたころは Kubernetes だけでなく、Prometheus 用メトリクスの出し方も詳しくなかったため。

  • IPv6 未対応

    そもそも IPv6 に対応可能なデータモデルになっていませんでした。

それからあれこれ開発するうちに、Kubernetes のカスタムリソースやコントローラーの作り方、Prometheus メトリクスの出し方などを学習し、「今作ればこうするのに」というアイデアが溜まっていきました。 そこで Coil に外部ネットワークに NAT 接続できる機能を追加することになった際、これらの不満点もすべて解消することにしました。

データモデルから全部見直したので、結果 Coil v2 は v1 のコードを一行も引き継いでいない完全に新規の実装となった訳です。 作り直す過程でモジュラーかつテストしやすい構造にできたので、テストのカバレッジを大幅に高められたことは良い副産物でした。

まとめ

サイボウズでは Coil v2 を使うことで Kubernetes 向けの高度なネットワーク機能を実現しています。 便利に生まれ変わった Coil v2 をご利用いただければ幸いです。