DockerのnetworkをCalicoでBGP接続する

この記事は [NTTコミュニケーションズ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/nttcom) の10日目です。

# はじめに
## 自己紹介
はじめまして。NTTコミュニケーションズでNetwork/Software Engineerとして働いている @TAR_O_RIN です。普段はクラウドのネットワークコントローラの開発をしており，ネットワークとソフトウェアを掛け合わせて世の中をもっと便利に出来ないかなぁと日々考えています。主なスキルセットとしてはL2/L3のネットワーク周りから，Python，Goによるネットワーク設定の自動化が主戦場です。

## 今回解決したい問題
システム基盤としてDockerが利用されるケースが非常に多くなり携わる機会が多くなってきた。しかし，触れば触るほど我慢できない部分が見えてきた。それはDockerのネットワークである。詳しいDockerネットワークの足回りについては[本家のサイト](https://docs.docker.com/engine/userguide/networking/)を参照するとして，殆どのユーザはコンテナを外部ネットワークに接続する際にBridgeを利用していることと思う。通常利用する分には不便ないが，私が直面したケースでは下記のようなものがあった。

- 外部ネットワークにDockerホストに設定されていないIPアドレス(複数)を晒すケース
- コンテナがどのDockerホスト上にいても特定のIPアドレスを固定して割り当てたいケース

上記の何れも，ユーザ自身でiptablesを用いてスタティックNATを設定し，ルータ側に当該IPへのルーティング設定をその都度入れれば解決できる問題である。しかし，エンドポイントが多くなると管理が煩雑になる上に肥大化したiptablesを管理するのは避けたい。そこで都度iptablesもルータへの設定もせずに上記を実現することを今回のゴールに据えてみる。


# 解決方法
## 選択肢1 Calico
[Calico](https://www.projectcalico.org/)とはWorkload(コンテナやVMのこと。これ以降はコンテナと表現する。)間をIPルーティングで接続し，セキュリティポリシーも設定することが出来るOSSである。VXLAN等のオーバレイネットワークではなく，純粋なIPでコネクティビティを提供することに特徴があり，インターネット等のオープンネットワークでスケーラビリティに実績のあるBGPを利用することが出来る。多くのプラットフォームがサポートされているが，今回はDockerを用いて課題を解決する。

## 選択肢2 Macvlan
そもそもiptablesもルーティングも避けたい(要件を満たせるのであればこれが一番綺麗だと思う)という人はmacvlanというDockerのnetwork driverを利用することをおすすめする。私はコンテナに求められる性質に応じてCalicoと組み合わせて利用している。

# Calicoを用いた今回のデータプレーン設計
バックボーンにはIP CLOS構成のSpine-Leaf構成を用い，各Hypervisor(以下DockerHost)は各々が所属するLeafSwitchと接続されている。さらにDockerHost１つにAS番号を１つ与えるAS per compute serverとし，LeafSwitchとeBGP接続を実施した。Calicoは[BIRD](http://bird.network.cz/)や[GoBGP](https://osrg.github.io/gobgp/)をBGP Clientとして利用し，コンテナへの経路を広告することが出来る。AS番号とDockerHostのIPアドレスの対応関係が分かりやすいように付与すると経路のOriginASからどのDockerHostが当該のコンテナを所持しているか直ぐ分かる。

![Blank Diagram - Page 1.png](https://qiita-image-store.s3.amazonaws.com/0/216192/6140730e-d3f7-0628-f878-20606739b687.png)


※BGPでDCネットワーク構築に関連した文献は世の中にたくさん存在するが，[Use of BGP for routing in large-scale data centers](https://tools.ietf.org/html/draft-ietf-rtgwg-bgp-routing-large-dc-11) というIETFのドキュメントがよく参照されているようなので時間の有る方はぜひ。

# 環境構築
今回利用したVersion
Ubuntu: Ubuntu 16.04.3 LTS
Calico: v2.6.2
Docker: 17.11.0-ce, build 1caf76c
基本的には[公式サイトのインストール手順](https://docs.projectcalico.org/v2.6/getting-started/docker/installation/manual)を利用する。データストアにEtcdを利用するため先にホスト側にインストールしておく。さらに今回はCalicoをDockerのネットワークプラグインとして利用するため[Docker daemonにもcluster-storeの設定](https://docs.projectcalico.org/v2.6/getting-started/docker/installation/requirements)を実施する必要がある。CalicoとDocker daemonで同じEtcdを共用しても問題ないため共用して動かしてみる。

## 利用した起動時オプション
### BGP speakerに利用するIPを指定したい場合
```text:
calicoctl node run --node-image=quay.io/calico/node:v2.6.2 --ip <IPv4-ADDRESS>
```
```text:
calicoctl node run --node-image=quay.io/calico/node:v2.6.2 --ip6 <IPv6-ADDRESS>
```

### AS番号を起動時に指定したい場合
```text:
calicoctl node run --node-image=quay.io/calico/node:v2.6.2 --as <AS-NUM>
```

# 使い方１: DockerHost内のネットワークとBGPpeer設定
## ip poolを作る
Docker networkで利用するアドレス帯を先に宣言しておく必要がある。

```
cat << EOF | calicoctl apply -f -
apiVersion: v1
kind: ipPool
metadata:
  cidr: 172.16.100.0/24
spec:
  ipip:
    enabled: false
  nat-outgoing: false
  disabled: false
EOF
```

## Docker networkを作る
Driverの指定を忘れずに。

```
docker network create --driver calico --ipam-driver calico-ipam calico-net --subnet 172.16.100.0/24
```

## profileを作る
今回はすべての通信を許可する。

```
cat << EOF | calicoctl apply -f -
- apiVersion: v1
  kind: profile
  metadata:
    name: calico-net
    tags:
    - calico-net
  spec:
    egress:
    - action: allow
      destination: {}
      source: {}
    ingress:
    - action: allow
      destination: {}
      source: {}
EOF
```

## BGP peer先を設定する
nodeネームはcalicoctl get nodeで確認できる。

```
cat << EOF | calicoctl apply -f -
apiVersion: v1
kind: bgpPeer
metadata:
  peerIP: 10.100.0.1
  scope: node
  node: test-node
spec:
  asNumber: 65000
EOF
```

## BGP peer status 確認

```
# calicoctl node status
Calico process is running.

IPv4 BGP status
+--------------+---------------+-------+----------+-------------+
| PEER ADDRESS |   PEER TYPE   | STATE |  SINCE   |    INFO     |
+--------------+---------------+-------+----------+-------------+
| 10.100.0.1   | node specific | up    | 08:36:56 | Established |
+--------------+---------------+-------+----------+-------------+

IPv6 BGP status
No IPv6 peers found.

```

# 使い方２: 実際にコンテナを立てる
一番感動するところです。(笑)

```
docker run --net calico-net --name calico-demo1 --ip 172.16.100.1 -tid busybox
```

Linuxのルーティングテーブルを見てみると，172.16.100.1への経路が見える。

```
# ip route show | grep 172.16.100
blackhole 172.16.100.0/26  proto bird
172.16.100.1 dev calib3daa894ef8  scope link
```

LeafSwitchが受けている経路を見てみると，172.16.100.0/26の経路がNexthopをDockerHostとして確認できる。

```
{master:0}
user@Leaf1> show route receive-protocol bgp 10.100.0.254

inet.0: 73 destinations, 73 routes (73 active, 0 holddown, 0 hidden)
  Prefix                  Nexthop              MED     Lclpref    AS path
* 172.16.100.0/26         10.100.0.254                           65100 I
```


# おわりに
Calicoを利用することでピュアL3接続でコンテナへのリーチャビリティを提供することが出来ました。お題であった課題をどう解決できたかというと

- 外部ネットワークにDockerホストに設定されていないIPアドレス(複数)を晒すケース
 - ホスト内に作成したDocker networkをそのまま外部へ広告出来る。

- コンテナがどのDockerホスト上にいても特定のIPアドレスを固定して割り当てたいケース
 - DockerHostが複数ある状態でもそれぞれは単なるASなので172.16.100.1という経路をどこから広告しても良い。あるホストが高負荷ならば，異なるホストに移し替えてコンテナを立てれば経路が広告されるためIPは変わらない。

今回はDockerとCalicoを組み合わせて純粋なIPによるネットワーク設計を実現できました。確かに大規模なネットワークになるとVXLANやIPIP/GREなどのトンネリングプロトコルを利用するケースもあるかと思います。しかし，カプセリング等のオーバーレイは可能な限り避けるべきです。シンプルに枯れた技術を流用した構成を組むことで信頼性の向上やスケーラビリティ，デバッグの容易性を高めることが出来るでしょう。

# 参考文献

Yahoo japan meetup 8より
https://www.slideshare.net/techblogyahoo/yahoo-japan-meetup-8-71847314
CyberAgent　DevelopersBlogより
https://developers.cyberagent.co.jp/blog/archives/3132/
沖縄オープラボラトリより
https://www.slideshare.net/VirtualTech-JP/project-calico-introduction-openstack-20177