IPv4/IPv6デュアルスタックKubernetesと閉域網接続
2020年12月18日 金曜日
CONTENTS
【IIJ 2020 TECHアドベントカレンダー 12/19(土)の記事です】
SRE推進室の李です。SRE推進室ではIIJのサービスのための基盤としてIKE(IIJ Kubernetes Engine)と呼ばれるKubernetesクラスタを運用しています。IKEにおいて複数のIPv4の閉域網をデュアルスタックKubernetesに引き込めるように整備したので、本記事ではその背景や概要を説明します。
なお、IKEについてはこちらの記事で紹介しています。
背景
IIJの特色は、通信事業者としての企業向けネットワーク接続サービスと、クラウドやIoTプラットフォームなどの接続後に利用する多種多様なサービスを提供していることが挙げられます。特に閉域網接続はセキュリティや性能上優れた接続形態であり、高い品質を要求される企業向けネットワーク接続では重要な要素となります。また閉域網においてはネットワーク設計の自由度がインターネットと比較すると高く、利用者にとっては使いやすさにつながります。一方で多様な閉域網と接続するためには逆に基盤側のネットワーク設計に対する制限が厳しくなります。典型的な例としては拠点を超えて閉域接続することにより、各拠点においてプライベートIPアドレスを利用したまま拠点間通信を行うことが考えられます。このようにすることで複数の拠点のオフィスネットワークを一つのLANとして見せることができます。このような閉域網が一つしかない場合は問題は生じませんが、IIJのサービスとしては複数の閉域網に対してサービスを提供することもあり、IPアドレスが衝突するといった課題は随所に散在します。
IPアドレスが衝突するという問題は、NATをすることでIPアドレスを衝突しないように変換することで解決できます。この際に①サーバ側のIPアドレスを変換するか、②クライアント側のIPアドレスを変換するかという選択肢があります。特にIPv4では一般ユーザでは使わないことになっているISP Shared Addressが定義されており、サーバ側のIPアドレスに一般的にはISP Shared Addressを割り当てることで、顧客のネットワークと衝突することなくサービスを提供することができます。しかし、このようなNATを行っても依然として問題があることに変わりありません。一つ目にはISP Shared Addressの取り決めがあるものの、閉域網においてそれを遵守する必要性はなく、ネットワーク設計時に注意して調整をする必要があるという点です。異なるIPアドレスを割り振る設計の場合、イレギュラー対応になり管理コストが増大します。そして二つ目はサーバ⇔クライアントのセットで衝突を防ぐことができても、異なる閉域網のクライアント同士のIPアドレスの衝突は避けられない点です。特に二つ目の問題により、異なる閉域網にサービスを提供する際にサービスホスト(※1)を共通化して集約することができず、結果的にサービスを提供するコストが上がってしまいます。
ここまで来るとクライアント側のIPアドレスを変換する方がサービス提供という観点では汎用的な解決方法であることがわかります。問題はすでに枯渇しそうなIPv4のIPアドレスを何に変換するかが自明ではないという点です。衝突を避けるという意味ではグローバルIPアドレスを与えることが最善ですが、残念ながらグローバルIPアドレスはそれほど豊富に余っているわけではありません。IPv4のみでネットワークを構築するとNATの変換先の割り当て、そしてNAT変換ルールの管理が課題となり、クライアント側のIPアドレス変換が万能薬として採用しにくい状況になっていました。そこで閉域網のIPv4のIPアドレスをIPv6に変換することを検討し、IKEのネットワークを設計・構築するに至りました。これにより一つの基盤でIPv4に対するサービスを提供しつつ、異なる閉域網を共通基盤内に引き込むことができるようになりました。
全体像
IKEではIKE Gateと称したIPv4⇔IPv6変換用のサーバを顧客の閉域網を終端するために配置しています。クライアントはこのIKE Gateを介して、ロードバランサそしてKubernetesクラスタへと接続を行います。
要素技術
IKEにおいて閉域網を引き込むために最も重要な要素はIPv4⇔IPv6変換を行うためのNAT箱です。
IPv6における/96にIPv4の全空間を埋め込むことができます。このことを利用してシンプルにIPv4からIPv6へのマッピングを行う変換がStateless IP/ICMP Translation (SIIT)です。IPv6では一般的に/64でセグメントを払い出されるため、SIITを採用しても十分な数の顧客を受け入れることができます。
Joolを用いたSIIT
IKEではNAT箱としてJoolを採用しました。iptablesを使ってNAT箱に入ってくる変換したいパケットをJoolへ向け、JoolではIPv4⇔IPv6変換のルールを定義します。
例えば上記の仮想的なIPアドレスに対して以下のような設定を仕掛けます。
jool_siit instance add tenant --iptables --pool6 XXXX:XXXX:XXXX:XXXX:XXXX:YYYY::/96 /sbin/iptables -t mangle -i eth0 -A PREROUTING -j JOOL_SIIT --instance "tenant" /sbin/ip6tables -t mangle -i eth1 -A PREROUTING -j JOOL_SIIT --instance "tenant"
これだけでNAT箱に到達するパケットは変換されるようになります。この設定により、例えばeth0から入ってくるIPv4のパケット src: 192.168.0.1/dst: 172.16.0.1 はIPv6のsrc: XXXX:XXXX:XXXX:XXXX:YYYY:192.168.0.1/dst: XXXX:XXXX:XXXX:XXXX:YYYY:172.16.0.1 に変換されます。あとはeth1に対して適切に経路を設定することによって、IPv4のパケットはIPv6のセグメントへと転送されます。逆にIPv6側からくるパケットは条件が満たされていれば後ろのIPv4を意味する部分だけに変換されて、IPv4のネットワークへ流れます。
ネットワーク設計及び設定
Kubernetes 1.16よりIPv4/IPv6デュアルスタック構成がalphaになりました。設定方法や詳細については公式ドキュメントに譲ります。デュアルスタック構成にすると作成されたPodにはIPv4/IPv6両方のIPアドレスが割り振られるようになります。また、IPv4もしくはIPv6のServiceを作成できるようになります。
通信を成立させるためには通信がクライアント発かサーバ発かを分けて考えます。そして、それぞれのパターンに対して行きと戻りの経路について設計を行います。
クライアント→サーバ通信
まずはIKEにてリクエストを受け付けるサーバを設置することを考えます。クライアントからの行きの通信はIKE Gateを通って、一度ロードバランサで受けてから、KubernetesのNodeを経てKubernetesのPodへと到達します。そして戻りの通信はPod・Nodeから出発し、ロードバランサへ戻されてから、IKE Gateを通ってクライアントへ送られます。
パケットの気持ちになってたどってみます。
a-1.) クライアントではロードバランサに送るつもりでサーバを指定します。例えば172.16.0.1へ送りたいとしましょう。IKE GateではこのIPアドレス宛てのパケットを吸い込むように設定する必要があります。IKE Gateのインターフェース自体にこのIPアドレスを設定することも可能であり、IKE Gateへ転送するように手前のルータで経路を設定することも可能です。ここではひとまずそのような設定がされているものとしてIKE Gateへパケットが到達することを仮定します。
a-2.) クライアントからのパケットはIKE Gateに到達することでiptablesによってJoolで処理されてIPv6に変換されます。このIPアドレスに対する経路をIKE Gateに設定することで、IKE Gateはこのパケットをロードバランサ側のネットワークへと転送します。
a-3.) ロードバランサでは適切にIPアドレスを設定しておくことでネットワーク上のパケットを受け取ることができます。Kubernetesを利用する場合、Cloud Controller Manager (CCM)を使って、Serviceリソースでtype: LoadBalancerを作成することでロードバランサへの設定を自動化できます。ここで.spec.loadBalancerIPをクライアントに見せようとしているものではなく、IPv6版のものを設定する必要があります。
a-4.) これでパケットはロードバランサへたどり着きました。ロードバランサにはバックエンドが設定されているはずであり、Kubernetesをバックエンドに構える場合、NodePortへのアクセスに相当するため、KubernetesのNodeとアクセスするためのPort宛てにパケットが送られます。この際、ソースアドレスをロードバランサのIPアドレスに書き換えるかどうかの選択肢があります。最もシンプルな構成では書き換えを行います。書き換えを行わない場合には、戻りの際にNodeからクライアントのIPアドレス宛にパケットが送信されるため、適切に経路を設定する必要が出てきます。
a-5.) Nodeへ到達したリクエストはPodへ転送されます。ここでもソースアドレスをNodeのIPアドレスに書き換えるかどうかの選択肢があります。PodへクライアントのIPアドレスを知らせたいかどうかで、ひとつ前のステップで書き換えるかどうかとあわせて決めます。この制御はServiceリソースの.spec.externalTrafficPolicyで設定されます。
ここまでが往路です。復路については以下のようになります。
b-1.) 戻りのパケットはPodから出発します。PodからはすぐにNodeへパケットが転送されます。
b-2.) Nodeから出ていく際にソースアドレスを書き換えたかどうかが問題になります。①ロードバランサのIPアドレスに書き変わっていれば、Nodeとロードバランサは同じセグメントに属するため問題なく送信されます。②戻りがクライアントのIPアドレスの場合、ロードバランサが一つしかなければNodeにおいて、default gatewayをロードバランサのIPアドレスに設定することで、経路が不明なパケットはロードバランサへ送られます。③戻りがクライアントIPアドレスにもかかわらず、ロードバランサが複数ある環境ではKubernetesの各NodeにクライアントIPアドレス別に経路を設定しておく必要があります。幸いにして、今回の構成ではXXXX:XXXX:XXXX:XXXX:YYYY:の部分でどのIKE Gateへ返すべきかがわかるので、同じIKE Gateに対して利用するロードバランサが一つだけであれば、設定すべき経路は一意的に決まります。
b-3.) レスポンスを持ってロードバランサまで戻ってきたパケットはIKE Gateへ転送される必要があります。この経路をロードバランサは持っている必要があります。
b-4.) ロードバランサに設定された経路に従ってパケットはIKE Gateへ送られて、今度はip6tablesでJoolで処理が行われて、IPv6からIPv4に変換されます。
b-5.) 最後にIKE GateからクライアントのIPアドレス向けに経路があればパケットはクライアントへ戻され、通信が成立します。
太字部分がネットワーク構成上設定が必要な部分です。
サーバ→クライアント通信
次にIKEにあるサーバからクライアントへリクエストを送ることを考えます。サーバ発の通信でも基本的にクライアント発の通信と同じ経路をたどります。唯一の違いは、クライアントはPodへの戻りの経路を持たないので、PodのIPアドレスを晒すことができないことです。いずれにしてもIKE Gateを通すのでIPv6のPodのIPアドレスを使うことができません。サーバ発の通信は事前にクライアントに見せるIPアドレスを決めておきます。そして、ロードバランサでSNATすることによって一つのIPアドレスから通信が来ているように見せかけます。
a-1.) PodからNodeを経由してロードバランサまでたどり着くところまではクライアント→サーバ通信の復路と同じです。
a-2.) ロードバランサでSNATして、IKE Gateを転送します。
a-3.) IKE GateでIPv4に変換してクライアントへ送られます。
b-1.) クライアントからIKE Gateへ戻りのパケットが送信されます。このパケットが到着するためにはIKE Gateでサーバ発時のサーバのIPアドレスを吸い込むように設定されている必要があります。
b-2.) IKE GateでIPv6に変換されてロードバランサへ送られます。
b-3.) ロードバランサに戻されたパケットはNATテーブルに従ってNode・Podへと転送されます。
ロードバランサ周りの設計
ロードバランサの基本的な動作では一度通信を終端してしまいます。これによりKubernetesのPodまで来るパケットではソースアドレスがロードバランサのものになっている構成が一般的です。HTTPのようなプロトコルではX-Forwarded-ForにクライアントのIPアドレスを書き込むことによってサーバでクライアントを識別することができますが、一般的なプロトコルに対してはアプリケーション側の実装が必要になります。ServiceリソースのexternalTrafficPolicyとロードバランサのIP Transparency機能を組み合わせることによりクライアントのIPアドレスを透過的にサーバへ晒すことができ、サービスの開発負担を削減することができます。
IKEではCCMや独自のオペレータを実装し、Kubernetesのリソースとして制御することでロードバランサ側で動的にIPアドレスの確保ができるように整備したりしています。その結果サービス開発者は使いたいIPアドレスとクライアントIPアドレスを見たいかどうか指定するだけでネットワーク周りの設定ができるようになっています。
まとめ
SRE室ではIIJのサービスのためのKubernetes基盤であるIKEを開発・運用しています。本記事ではIIJの特殊なネットワーク要件に応えるためにIPv4/IPv6デュアルスタックKubernetesクラスタを整備し、閉域網のマルチテナントネットワークをIKEに引き込むための概要について説明しました。
全体のネットワークの概略図を最後に載せて終わりにします。
注釈
- IIJのサービスを提供するためのサーバのこと[↑]