さようならサイドカー、Linux カーネル テクノロジ eBPF 基盤の Cilium を Docker で試した

# はじめに
[Cilium](https://github.com/cilium/cilium) を用いた TLS セッション終端を Dockerコンテナで試したので手順を紹介します。

Cilium ドキュメントに次のようにありますが

> Cilium は、**Docker** や Kubernetes などの Linux コンテナ管理プラットフォームを使用してデプロイされたアプリケーション サービス間のネットワーク接続を透過的に保護するためのオープン ソース ソフトウェアです。
>
> [Cilium ドキュメント（v1.13）のイントロダクション](https://docs.cilium.io/en/v1.13/overview/intro/)より抜粋

ドキュメントを読み進めると、Kubernetes の手順はありますが、Docker の手順は見当たりません。[古いドキュメント（Cilium v1.9）に Docker の手順](https://docs.cilium.io/en/v1.9/gettingstarted/docker/)がありますが、Key-Valueストアの consul が非推奨となったため etcd に変更すると、その手順では動作しませんでした。


# モチベーション
Cilium の基盤となっているのは、eBPF と呼ばれる新しい Linux カーネル テクノロジです。これにより、強力なセキュリティの可視性と制御ロジックを動的に挿入できます。eBPF は Linux カーネル内で実行されるため、アプリケーションコードやコンテナー構成を変更することなくセキュリティ ポリシーを適用および更新できます。

次のブログ記事によると

https://isovalent.com/blog/post/2021-12-08-ebpf-servicemesh/

次のようなネットワーク層の機能はビジネスロジックから切り離したいです。

- パケットやリクエストのフィルタリング
- セキュア通信
- シンプルなネットワーキング（Calico 等の代替）
- 負荷分散、経路制御
- 帯域幅管理
- モニタリングとトラブル要因解析

そのためにこれまでは、共有ライブラリなどでコードを分離したり、プロキシやサイドカーでプロセスを分離するといったアプローチがとられてきましたが、eBPF テクノロジを用いることでこれらの機能をカーネル空間へ移動することで（次図）

![295_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629684/783ab443-4aae-4e13-a9a3-020b50e6f8b8.png)
（上図は元記事からの引用）

これまでプロキシによって実行されていた機能の多くをオフロードすることができ、オーバーヘッドと複雑さを軽減できる可能性があるとのこと。

本稿では、異なるノードに配置された Dockerコンテナ app を Cilium を用いてオーバーレイネットワークで接続し、一方に TLS終端を設定し動作を確認します。


　　　**Node1 のコンテナ `app1` と Node2 のコンテナ `app2` を接続**
![qiita-cilium-env.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629684/bd0b99b2-8f21-1b66-a221-f16a929afae7.png)


# 検証環境
Node1 と Node2 は同一構成としました。
```shell-session
$ uname -a
Linux debian11 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) x86_64 GNU/Linux

$ docker --version
Docker version 23.0.5, build bc4487a

$ docker compose exec -it cilium cilium version
Client: 1.13.2 8cb94c70 2023-04-17T23:19:21+02:00 go version go1.19.8 linux/amd64
Daemon: 1.13.2 8cb94c70 2023-04-17T23:19:21+02:00 go version go1.19.8 linux/amd64
```

Cilium のシステム動作要件は、次を参照ください。

https://docs.cilium.io/en/stable/operations/system_requirements/


# Cilium コンポーネント概観
Docker と Cilium は、[プラグイン`cilium-docker`](https://github.com/cilium/cilium/tree/main/plugins/cilium-docker
)（[Docker ネットワークドライバープラグイン](https://matsuand.github.io/docs.docker.jp.onthefly/engine/extend/plugins_network/)）を介してやり取りします。

docker CLI で Dockerネットワークを作成すると `dockerd` は、`cilium-docker`プラグインを介して `cilium-agent` にネットワークの作成を要求します。ネットワーク作成後、そのネットワークでコンテナを開始すると `dockerd` は、`cilium-docker`プラグインを介して`cilium-agent` に IPアドレスの割り当てを要求します。コンテナ間の通信に何らかのポリシーを適用したいときは、cilium CLI で`cilium-agent` へポリシーを適用します。

![cilium-cilium-docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629684/813933d4-3b79-623d-bc83-1079485a34b9.png)



- **Cilium エージェント**（`cilium-agent`）は、クラスター内の各ノードで実行されるデーモンです。ネットワーキングやサービス負荷分散、ネットワーク ポリシー、および可視性と監視の構成を API を介して受け入れ、ネットワークアクセスを制御するための eBPF プログラムを管理します。
- **Cilium CLI**（`cilium`）は、Cilium エージェントと共にインストールされるコマンドラインツールです。同じノードで実行されている Cilium エージェントの API と対話しネットワークポリシーを構成したりエージェントの状態を調べることができます。
- **Cilium オペレータ**は、クラスター全体に対して論理的に 1 回処理する必要があるタスクを管理します。
- **プラグイン**（`cilium-cni`、`cilium-docker`) は、Kubernetes または Docker によって呼び出されます。ノードの Cilium エージェントの API と対話して必要なデータパス構成をトリガーし、ネットワーク、負荷分散、およびネットワーク ポリシーを構成します。
- **データストア**は、Ciliumエージェント間で状態を伝達するために使用します。


:::note warn
ここでの Cilium CLI は、別途 Kubernetes クラスター上に Cilium をインストール／管理するための CLI ツール `cilium` と同じ呼称ですが別物なので混同しないようにしてください。
:::


# データストア
Cilium では、エージェント間で状態を伝達するためのデータストアが必要です。次のデータストアがサポートされています。

- Kubernetes CRD
- Key-Valueストア（KVストア）

ここでは KVストアを用います。これまで Cilium は、KVストアとして `consul` と `etcd` をサポートしてきましたが、リリース 1.11 で `consul` は非​​推奨となりました。

https://github.com/cilium/cilium/issues/18851

`consul` を用いると Ciliumエージェントは、実行時ログに次の `deprecated` 警告を出力しますが、まだ動作するようです。

> Support for Consul as a kvstore backend has been deprecated due to lack of maintainers. If you are interested in helping to maintain Consul support in Cilium, please reach out on GitHub or the official Cilium slack

他方 `etcd` を用いると Ciliumエージェントは、開始数分後にステータスが `"KVStore: Failure Err: quorum check failed"` となりエラー終了します。これは、Ciliumエージェントが定期的に etcd のハートビート・キー（cilium/.heartbeat）の更新を監視しハートビートキーが時間内に更新されない場合、クォーラムチェックに失敗したとみなされるためです。

https://docs.cilium.io/en/stable/operations/troubleshooting/#recovery-behavior


調べた限りでは、現時点で設定だけでエラー終了を回避することはできず、Cilium オペレータを常駐させハートビート・キーを定期的に更新する必要がありました。




# Cilium 実行
Docker-Compose を用いて Node1 と Node2 のそれぞれで Ciliumコンテナを開始します。
```shell-session
$ export NODE1_ADDR=<your-node1-ip-address>
$ export NODE2_ADDR=<your-node2-ip-address>
$ docker compose up -d
```

**コンテナ配置図**
![qiita-cilium-container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629684/058b7425-2b78-574a-6eaa-2dbb6059f27e.png)

Node2 の `cilium-agent` は、Node1 の KVストア `etcd` を参照します。cilium CLI は、Cilium エージェントに同梱されています。

<details><summary>Node1 の docker-compose.yml の例</summary>

```yml:Node1: docker-compose.yml
version: '3'
services:
  cilium:
    container_name: cilium
    image: quay.io/cilium/cilium:v1.13.3
    command: >
      cilium-agent
      --ipv4-node="${NODE1_ADDR}"
      --enable-ipv6=false
      --kvstore=etcd
      --kvstore-opt=etcd.address=127.0.0.1:2379
      --tunnel=vxlan
      --cluster-name=local
      --cluster-id=0
    volumes:
      - /var/run/cilium:/var/run/cilium
      - /sys/fs/bpf:/sys/fs/bpf
      - /var/run/docker/netns:/var/run/docker/netns:rshared
      - /var/run/netns:/var/run/netns:rshared
      - /boot:/boot/
      - /lib/modules:/lib/modules/
      - /usr/lib/modules:/usr/lib/modules/
    network_mode: "host"
    cap_add:
      - "NET_ADMIN"
    privileged: true
    depends_on:
      - kvstore

  plugin:
    container_name: plugin
    image: quay.io/cilium/docker-plugin:v1.13.3
    command: cilium-docker
    volumes:
      - /var/run/cilium:/var/run/cilium
      - /run/docker/plugins:/run/docker/plugins
      - /var/run/docker.sock:/var/run/docker.sock
    network_mode: "host"
    cap_add:
      - "NET_ADMIN"
    privileged: true
    depends_on:
      - cilium

  kvstore:
    container_name: kvstore
    image: quay.io/coreos/etcd:v3.5.8
    command: etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379
    network_mode: "host"

  operator:
    container_name: operator
    image: quay.io/cilium/operator-generic:v1.13.3
    command: >
      cilium-operator-generic
      --enable-ipv6=false
      --kvstore=etcd
      --kvstore-opt=etcd.address=127.0.0.1:2379
      --enable-k8s=false
      --cluster-pool-ipv4-cidr=10.0.0.0/16
    network_mode: "host"
    depends_on:
      - kvstore
```

</details>

<details><summary>Node2 の docker-compose.yml の例</summary>

```yml:Node2: docker-compose.yml
version: '3'
services:
  cilium:
    container_name: cilium
    image: quay.io/cilium/cilium:v1.13.3
    command: >
      cilium-agent
      --ipv4-node="${NODE2_ADDR}"
      --enable-ipv6=false
      --kvstore=etcd
      --kvstore-opt=etcd.address="${NODE1_ADDR}":2379
      --tunnel=vxlan
      --cluster-name=local
      --cluster-id=0
      --certificates-directory=/opt/cilium/certs
    volumes:
      - /var/run/cilium:/var/run/cilium
      - /sys/fs/bpf:/sys/fs/bpf
      - /var/run/docker/netns:/var/run/docker/netns:rshared
      - /var/run/netns:/var/run/netns:rshared
      - ./tls:/opt/cilium/certs/my-tls/
      - ./policy:/opt/cilium/policy/
    network_mode: "host"
    cap_add:
      - "NET_ADMIN"
    privileged: true

  plugin:
    container_name: plugin
    image: quay.io/cilium/docker-plugin:v1.13.3
    command: cilium-docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/run/cilium:/var/run/cilium
      - /run/docker/plugins:/run/docker/plugins
    network_mode: "host"
    cap_add:
      - "NET_ADMIN"
    privileged: true
    depends_on:
      - cilium
```

</details>

:::note info
Node1 と Node2 を同一クラスターのメンバーとするために `cilium-agent` の起動引数 `--cluster-name` と `--cluster-id` に同じ値を指定します。
:::


コンテナ `cilium` に接続し cilium CLI でステータスを確認
```shell-session
$ docker exec -it cilium cilium status
KVStore:                 Ok         etcd: 1/1 connected, lease-ID=694d886b8f79e705, lock lease-ID=694d886b8f79e707, has-quorum=true: 127.0.0.1:2379 - 3.5.8 (Leader)
Kubernetes:              Disabled
Host firewall:           Disabled
CNI Chaining:            none
CNI Config file:         CNI configuration file management disabled
Cilium:                  Ok   1.13.3 (v1.13.3-36cb0eed)
NodeMonitor:             Listening for events on 1 CPUs with 64x4096 of shared memory
Cilium health daemon:    Ok
IPAM:                    IPv4: 2/65534 allocated from 10.13.0.0/16,
IPv6 BIG TCP:            Disabled
BandwidthManager:        Disabled
Host Routing:            Legacy
Masquerading:            IPTables [IPv4: Enabled, IPv6: Disabled]
Controller Status:       21/21 healthy
Proxy Status:            OK, ip 10.13.50.180, 0 redirects active on ports 10000-20000
Global Identity Range:   min 256, max 65535
Hubble:                  Disabled
Encryption:              Disabled
Cluster health:          2/2 reachable   (2023-05-30T07:31:08Z)
```
Node1 と Node2 が正常に認識されるまで数分ほど待ちます。正常に認識されると `Cluster health` が `2/2 reachable` になります。

クラスターノードに割り当てられた CIDR を確認
```shell-session
$ docker exec -it cilium cilium node ls
Name          IPv4 Address      Endpoint CIDR   IPv6 Address   Endpoint CIDR
local/node1   192.168.250.154   10.13.0.0/16
local/node2   192.168.251.138   10.234.0.0/16
```


# アプリケーション コンテナ app 実行
Node1 と Node2 でそれぞれコンテナ `app1` と `app2` を開始しします。

## Node1
Dockerネットワーク `cilium-net` 作成。ドライバーに `cilium` を指定していることに注意
```shell-session: Node1
$ docker network create --driver cilium --ipam-driver cilium cilium-net
```


コンテナ `app1` を開始しシェル接続
```shell-session: Node1
$ docker run -it --rm --name app1 -l name=app1 --net cilium-net alpine/curl sh
```
ここでオプション`--net` に `cilium-net` を指定していることに注意してください。プラグイン cilium-docker を介して IPアドレスが払い出されます。


コンテナ `app1` の IPv4アドレスを確認
```shell-session: app1
/ # ip a
...
45: cilium0@if46: ...
    inet 10.13.165.153/32 scope global cilium0
...
```

Cilium エンドポイントにコンテナ`app1` が追加されたことを確認
```shell-session: Node1
$ docker exec -it cilium cilium endpoint list
ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value])   IPv6   IPv4            STATUS
           ENFORCEMENT        ENFORCEMENT
31         Disabled           Disabled          2532       container:name=app1                  10.13.165.153   ready
...
```

## Node2
Dockerネットワーク `cilium-net` を作成
```shell-session: Node2
$ docker network create --driver cilium --ipam-driver cilium cilium-net
```

コンテナ `app2` を開始しシェル接続
```shell-session: Node2
$ docker run -it --rm --name app2 -l name=app2 --net cilium-net busybox sh
```

コンテナ `app2` の IPv4アドレスを確認
```shell-session: app2
/ # ip a
...
35: cilium0@if36: ...
    inet 10.234.96.18/32 scope global cilium0
...
```

Cilium エンドポイントにコンテナ`app2` が追加されたことを確認
```shell-session: Node2
$  docker exec -it cilium cilium endpoint list
ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value])   IPv6   IPv4             STATUS
           ENFORCEMENT        ENFORCEMENT
3948       Disabled           Disabled          15836      container:name=app2                  10.234.96.18     ready
...
```


## 疎通確認
Node1 のコンテナ `app1` から Node2 のコンテナ `app2` への疎通を確認します。

**IPアドレス**
![qiita-cilium-ip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629684/5c3e1d03-29c3-7319-51de-9524a09b592c.png)



コンテナ `app2` でポート `9999` をリッスン
```shell-session: app2
/ # nc -l -p 9999
```

コンテナ `app1` から `app2` へメッセージ送信
```shell-session: app1
/ # echo hoge | nc 10.234.96.18:9999
```

コンテナ `app2` でメッセージを受信
```shell-session: app2
/ # nc -l -p 9999
hoge
```


# TLS終端
Cilium ネットワークポリシーを適用し Node2 のコンテナ `app2` に TLS 終端を設定します。

## Node2
サーバ秘密鍵とサーバ証明書、Cilium ネットワークポリシーファイルを準備します。サーバ証明書とサーバ秘密鍵の作成手順は割愛します。


フォルダ構成
```
.
├── docker-compose.yml
├── profile
│   └── profile.json   # Cilium ネットワークポリシーファイル
└── tls
    ├── tls.crt        # サーバ証明書
    └── tls.key        # サーバ秘密鍵
```

Cilium ネットワークポリシーファイルを作成
```json: Node2: profile/profile.json
[{
    "labels": [{"key": "name", "value": "tls-termination"}],
    "endpointSelector": {"matchLabels":{"name":"app2"}},
    "ingress": [{
        "fromEndpoints": [
            {"matchLabels":{"name":"app1"}}
        ],
        "toPorts": [{
            "ports": [{"port": "443", "protocol": "TCP"}],
            "terminatingTLS": {
                "secret": {
                    "name": "my-tls"
                },
                "certificate": "tls.crt",
                "privateKey": "tls.key"
            },
            "rules": {
                "http": [{}]
            }
        }]
    }]
}]
```
ラベル `name=app2` のエンドポイントに適用するポリシーです。ラベル `name=app1` からの　ingress ポート443 をTLS終端します。

:::note info
`terminatingTLS.secret.name` は、サーバ証明書を配置するフォルダパス（コンテナ内）の一部となります。この場合、cilium-agent の起動引数に `--certificates-directory=/opt/cilium/certs` を指定しているので次の場所にサーバ証明書と秘密鍵を配置する必要があります。
- /opt/cilium/certs/my-tls/tls.crt
- /opt/cilium/certs/my-tls/tls.key

参照：https://github.com/cilium/cilium/blob/v1.13.3/pkg/crypto/certificatemanager/certificate_manager.go#L42
:::

:::note warn
CA証明書ファイル `ca.crt`（デフォルト）が存在する場合やオプション `terminatingTLS.trustedCA` を指定すると mTLS になるようです。
:::

Cilium ネットワークポリシーを適用
```shell-session: Node2
$ docker exec -it cilium cilium policy import /opt/cilium/policy/policy.json
```

ポリシーが適用されたことを確認
```shell-session: Node2
$ docker exec -it cilium cilium endpoint list
ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value])   IPv6   IPv4             STATUS
           ENFORCEMENT        ENFORCEMENT
3948       Enabled            Disabled          15836      container:name=app2                  10.234.96.18     ready
...
```
`POLICY (ingress)` が `Enabled` であれば ok.


:::note info
ポリシーの詳細を取得
```shell-session
$ docker exec -it cilium cilium policy get name=tls-termination
```
:::

:::note info
全てのポリシーを削除
```shell-session
$ docker exec -it cilium cilium policy delete --all
```
:::

## 動作確認


コンテナ `app2` でポート `443` をリッスン
```shell-session: app2
/ # nc -l -p 443
```

コンテナ `app1` から `app2` へ HTTPS リクエストを送信
```shell-session: app1
/ # curl --insecure https://10.234.96.18/
```

コンテナ `app2` でリクエストを受信
```shell-session: app2
/ # nc -l -p 443
GET / HTTP/1.1
host: 10.234.96.18
user-agent: curl/7.80.0
accept: */*
x-forwarded-proto: https
x-request-id: 1395028f-32ed-4887-aa8c-0defa89bd236
x-envoy-expected-rq-timeout-ms: 3600000
```

TLS終端され平文で受信できました。