Calicoを使ってK8s PodネットワークからIP Masquarading無しで外部へ接続する

# Calicoを使ってK8s PodネットワークからIP Masquarading無しで外部へ接続する

オンプレミスでIPアドレスを台帳管理しているようなレガシーなネットワーク運用をされている方から（最近は少なくなくなっていますが）既存ネットワークへKubernetesを繋ぐ際の懸念として言及されることが多い、ClusterからのOutbound通信について書きたいと思います。

## Podからの通信のSource IPアドレスを固定したい

標準的なK8s Clusterの場合、Cluster内のPodから外部宛ての通信はすべてNodeのIPアドレスにSNATされるため、Podに振られているIPアドレスが外部に晒されることはありません。

> 大概のネットワークではルーティング可能な（経路広告やスタティックな経路設定する必要がある）IPアドレスの数は限定的です。このため、K8s ClusterはPodのスケーラビリティに対してその影響を受けないよう、外部のネットワークとは異なるCluster内部通信用のネットワークを構成してトポロジーハイディングを図っています。
こうすることで、Cluster内部のPodをいくつどう構成しようが外部のルーターやホストがそれを意識する必要がなくなるため、Cluster内のネットワーク・リソース管理の自由が確保されるというわけです。

一方で、通信のセキュリティを確保するために、ネットワークの境界にFirewallを配備したり、SIEM（Security information and event management）やDLP（Data Loss Prevention）などで通信の監視ソリューションを導入したい場合、やはりLayer3でアプリケーションを特定したい要件が出てきます。要するにパケットのSource IPアドレスによるアプリケーションの特定やパケット・フィルタリングが求められるわけです。

## Source IPアドレスでアプリケーションを特定するには

では、上述したK8sの特性がありつつも、あるアプリケーションのPodからの外部宛てのパケットにを特定のSource IPアドレスを付与するためにはどういった方式があるでしょうか。（あくまでもK8s Nativeな方式として。）

1. 外部通信を行う特定のアプリケーションPod（Egress Pod）を専用のNodeにAffinityしてやる
2. Podに外部へルーティング可能なIPアドレスをアサインする

前者は柔軟かつ潤沢なリソースを備えたIaaS上に乗っかってるClusterであれば許容出来るでしょうが、スケーラビリティへのリソース関与が大きくなるため、あまり気が進みません。
後者については[以前（だいぶ前…）紹介したMultus](https://qiita.com/tomotake-koike/items/aa147a601713cd8efefd)も実現可能な方式の一つではありますが、IPAM管理が面倒だったりします。
そこで今回はCalicoを使った実現をご紹介したいと思います。BGPルーターと連携する必要がありますが、CNIでCalicoを採用しているClusterであれば簡単に試すことが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110787/ffd89fdf-d7e0-7a82-9c5c-0c0246502cbb.png)

## それ以外の方法

### Calico Egress Gateway

細かい機能は調べ切れていませんが、Calico Enterprise版のEgress Gatewayを使えば、この辺の管理を簡単に行うことができそうです。
プロダクション環境では導入を検討したいところですが、今回はK8s CNIのSNATをさせない方法を試します。
https://www.tigera.io/blog/calico-egress-gateway-universal-firewall-integration-for-kubernetes/

### Cilium Egress IP Gateway

Ciliumでもつい最近リリースされたVersion 1.10で同じような機能が提供されています。ただEgress Gateway用のNodeを立てる仕組みのようです。（実際この構成が現実的かも。）
https://cilium.io/blog/2021/05/20/cilium-110#egress-ip-gateway
Ciliumは実績も増えて来てますし、今回のリリースでは今まで欲しかった機能も充実してきて本命になりつつあります。


# 試してみる

以下、公式ドキュメントを参考に自宅のLab環境で試してみます。K8s ClusterとBGPサーバーのマシンは事前に確保してください。（自分はミニマルなKVMにUbuntuを入れました。）
https://docs.projectcalico.org/about/about-kubernetes-egress#nat-outgoing

## 実施内容

CalicoのIP PoolとIPAMの設定を行うことにより、以下について試してみます。

* Podに任意のIP PoolおよびIPアドレスを付与
* PodのIPアドレスのままClusterの外部へ疎通（NodeのIPアドレスへのSNAT無し）
  * Serviceリソース無しで外部からPodに通信することも可能（しないと思いますが）
* Cluster内のIP PoolのRoute情報をBGPルーターに広告することが可能

本Hands-onでは構築済みのClusterへ設定を行い、外部のBGPサーバーと連携してみます。

## 前提条件

こちらで紹介している方法はCalicoの機能を試してみる目的としておりますので、Best Practiceではないことにご留意ください。
実際にプロダクション環境に適用する場合は、ご自身による十分なFeasibility Checkのご実施を忘れずに。（念のため）

## 準備：BGPサーバーの配備
BGP DaemonのBIRDを建てます。BGPサーバーに見立てたマシンで以下を実行してBIRDと確認用のnginxをインストールします。

```
sudo apt install bird nginx
```

`/etc/bird/bird.conf`をエディットします。
`protcol kernel`のスタンザ内の`export all;`をコメントアウトし、各Calico NodeをBGP Peer追加します。（各`protcol bgp`の名前は記号以外の英数字で設定して下さい。）
以下例ではMaster Node含めた5台のCalico Nodeを追加しています。（CalicoのディフォルトのASは`64512`、BGP RouterやCalico NodeのIPアドレスは適宜環境に合わせ変更して下さい。）

```
# This is a minimal configuration file, which allows the bird daemon to start
# but will not cause anything else to happen.
#
# Please refer to the documentation in the bird-doc package or BIRD User's
# Guide on http://bird.network.cz/ for more information on configuring BIRD and
# adding routing protocols.

# Change this into your BIRD router ID. It's a world-wide unique identification
# of your router, usually one of router's IPv4 addresses.
# router id 198.51.100.1;
router id 192.168.29.4;

# The Kernel protocol is not a real routing protocol. Instead of communicating
# with other routers in the network, it performs synchronization of BIRD's
# routing tables with the OS kernel.
protocol kernel {
        scan time 60;
        import none;
        export all;   # Actually insert routes into the kernel routing table
}

# The Device protocol is not a real routing protocol. It doesn't generate any
# routes and it only serves as a module for getting information about network
# interfaces from the kernel. 
protocol device {
        scan time 60;
}

protocol bgp ubu0703 {
    import all;
    local as 65000;
    neighbor 192.168.29.73 as 64512;
}

protocol bgp ubu0704 {
    import all;
    local as 65000;
    neighbor 192.168.29.74 as 64512;
}

protocol bgp ubu0705 {
    import all;
    local as 65000;
    neighbor 192.168.29.75 as 64512;
}

protocol bgp ubu0706 {
    import all;
    local as 65000;
    neighbor 192.168.29.76 as 64512;
}

protocol bgp ubu0707 {
    import all;
    local as 65000;
    neighbor 192.168.29.77 as 64512;
}
```
BGP Daemonを再起動します。

```
sudo systemctl restart bird
```

## Calico IP Poolの設定
`calicoctl`をセットアップしたホスト（この環境ではMaster Node）で、Podに割り当てる任意のIPアドレスPool（以下例では`192.168.32.0/24`）とSNATせずに接続する外部のCIDR（ここでは`192.168.29.0/26`）をCalicoに適用します。

```
cat << EOF > calico-ippools.yaml 
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: ipam-ipv4-ippool
spec:
  cidr: 192.168.32.0/24
  natOutgoing: true
---
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: living-nw
spec:
  cidr: 192.168.29.0/26
  disabled: true
EOF
calicoctl apply -f calico-ippools.yaml
```

このIP Poolを割り当てたPodを生成します。（ここではあえてDeploymentにしていませんが、templateで適当なWorkloadでデプロイしてください。）  

```
for i in {0..5} ; do  kubectl apply -f - << EOF
apiVersion: v1
kind: Pod
metadata:
  name: ipam-nginx-app${i}
  labels:
    ipam-app: ipam-app${i}
  annotations:
    "cni.projectcalico.org/ipv4pools": "[\"ipam-ipv4-ippool\"]"
spec:
  containers:
    - name: ipam-nginx-app${i}
      image: nginx:latest
      ports:
      - containerPort: 80
EOF
done
```

PodにCalico IP Poolのアドレスが付与されていることを確認します。

```
$ kubectl get po -l ipam-app -o wide
NAME              READY   STATUS    RESTARTS   AGE   IP               NODE       NOMINATED NODE   READINESS GATES
ipam-nginx-app0   1/1     Running   0          20h   192.168.32.0     ubu07-06   <none>           <none>
ipam-nginx-app1   1/1     Running   0          20h   192.168.32.192   ubu07-07   <none>           <none>
ipam-nginx-app2   1/1     Running   0          20h   192.168.32.1     ubu07-06   <none>           <none>
ipam-nginx-app3   1/1     Running   0          20h   192.168.32.193   ubu07-07   <none>           <none>
ipam-nginx-app4   1/1     Running   0          20h   192.168.32.2     ubu07-06   <none>           <none>
ipam-nginx-app5   1/1     Running   0          20h   192.168.32.194   ubu07-07   <none>           <none>
```

> 上記ではPodの`annotations`にIP Pool名を指定していますが個別のIPアドレスの設定やNamespace毎の割り当て、Nodeに限定した付与も可能です。詳細については以下のドキュメントを参照下さい。
> https://docs.projectcalico.org/reference/cni-plugin/configuration#ipam-1


## Calico BGP Peerの設定
Calico Cluster内のルート情報を広告するためのBGP Peerの設定を適用します。BGP Peerは先程のBGP Daemonを設定したサーバーの情報を設定します。

```
cat << EOF > calico-bgppeer.yaml
cat calico-bgppeer.yaml
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgp-bird
spec:
  peerIP: 192.168.29.4
  asNumber: 65000
EOF
calicoctl apply -f calico-bgppeer.yaml
```

Calico側のPeer状態を確認します。

```
$ sudo calicoctl node status
Calico process is running.

IPv4 BGP status
+---------------+-------------------+-------+------------+--------------------------------+
| PEER ADDRESS  |     PEER TYPE     | STATE |   SINCE    |              INFO              |
+---------------+-------------------+-------+------------+--------------------------------+
| 192.168.29.74 | node-to-node mesh | up    | 2021-04-13 | Established                    |
| 192.168.29.75 | node-to-node mesh | up    | 2021-04-13 | Established                    |
| 192.168.29.76 | node-to-node mesh | up    | 2021-04-14 | Established                    |
| 192.168.29.77 | node-to-node mesh | up    | 2021-04-14 | Established                    |
| 192.168.29.4  | global            | up    | 21:52:18   | Established                    |
+---------------+-------------------+-------+------------+--------------------------------+

IPv6 BGP status
No IPv6 peers found.
```

BGPルーター側で正しくルート情報が広告されたか確認します。
以下では`birdc`によるDaemon内の情報とOSのルート情報を確認しています。  

```
# birdc show route
BIRD 1.6.8 ready.
172.16.251.64/26   via 192.168.29.73 on cbr0 [ubu0703 06:52:18] * (100) [AS64512i]
                   via 192.168.29.75 on cbr0 [ubu0705 06:52:18] (100) [AS64512i]
                   via 192.168.29.74 on cbr0 [ubu0704 06:52:18] (100) [AS64512i]
                   via 192.168.29.76 on cbr0 [ubu0706 06:52:18] (100) [AS64512i]
                   via 192.168.29.77 on cbr0 [ubu0707 06:52:18] (100) [AS64512i]
192.168.32.192/26  via 192.168.29.73 on cbr0 [ubu0703 06:52:18] * (100) [AS64512i]
                   via 192.168.29.75 on cbr0 [ubu0705 06:52:18] (100) [AS64512i]
                   via 192.168.29.74 on cbr0 [ubu0704 06:52:18] (100) [AS64512i]
                   via 192.168.29.76 on cbr0 [ubu0706 06:52:18] (100) [AS64512i]
                   via 192.168.29.77 on cbr0 [ubu0707 06:52:18] (100) [AS64512i]
192.168.32.0/26    via 192.168.29.73 on cbr0 [ubu0703 06:52:18] * (100) [AS64512i]
                   via 192.168.29.75 on cbr0 [ubu0705 06:52:18] (100) [AS64512i]
                   via 192.168.29.74 on cbr0 [ubu0704 06:52:18] (100) [AS64512i]
                   via 192.168.29.76 on cbr0 [ubu0706 06:52:18] (100) [AS64512i]
                   via 192.168.29.77 on cbr0 [ubu0707 06:52:18] (100) [AS64512i]
172.16.109.64/26   via 192.168.29.73 on cbr0 [ubu0703 06:52:18] * (100) [AS64512i]
                   via 192.168.29.75 on cbr0 [ubu0705 06:52:18] (100) [AS64512i]
                   via 192.168.29.74 on cbr0 [ubu0704 06:52:18] (100) [AS64512i]
                   via 192.168.29.76 on cbr0 [ubu0706 06:52:18] (100) [AS64512i]
                   via 192.168.29.77 on cbr0 [ubu0707 06:52:18] (100) [AS64512i]

# ip r | grep 192.168.32
192.168.32.0/26 via 192.168.29.73 dev cbr0 proto bird 
192.168.32.192/26 via 192.168.29.73 dev cbr0 proto bird 
```

### Workaround：BGPルーター側に各PodへのDirectなルートテーブルが構成されない
本Hands-onの環境ではBGPルーター内OSのルートテーブルがWorker NodeにDirectに接続されない状態（この例ではMaster Node#0が1Hop目）となってしまいました。（BGP周りの設定がおかしいのかも…）  
WorkaroundとしてCluster内への転送を許容するよう各Nodeに以下のように`iptables`の設定を入れています。

```
# iptables -I KUBE-FORWARD -s 192.168.29.0/26 -j ACCEPT
```

## 確認
以下コマンドでPod内から要求を出します。

```
for i in {0..5} ; do kubectl exec ipam-nginx-app${i} -- curl 192.168.29.4 ; done
```

BGPルーターサーバー側のnginxの`access.log`を確認しPod内のIPアドレスの`192.168.32.0/24`で疎通が通っていることを確認します。

```
# tail  /var/log/nginx/access.log
:
192.168.32.0 - - [15/Apr/2021:13:43:31 +0900] "GET / HTTP/1.1" 200 612 "-" "curl/7.64.0"
192.168.32.192 - - [15/Apr/2021:13:43:33 +0900] "GET / HTTP/1.1" 200 612 "-" "curl/7.64.0"
192.168.32.1 - - [15/Apr/2021:13:43:33 +0900] "GET / HTTP/1.1" 200 612 "-" "curl/7.64.0"
192.168.32.193 - - [15/Apr/2021:13:43:34 +0900] "GET / HTTP/1.1" 200 612 "-" "curl/7.64.0"
192.168.32.2 - - [15/Apr/2021:13:43:34 +0900] "GET / HTTP/1.1" 200 612 "-" "curl/7.64.0"
192.168.32.194 - - [15/Apr/2021:13:43:35 +0900] "GET / HTTP/1.1" 200 612 "-" "curl/7.64.0"
```


# おわりに

Calicoを使ったPodへのルーティング可能な静的IPアドレスの付与について書かせていただきました。
今後、前述したCiliumの新しい機能も確認したいと思いますので、その際にはまたご紹介させていただこうと思います。