Classic VSIのプロビジョニング時に静的ルーティングを自動追加する

IBM Cloudは現在、以前からのSoftLayerの流れをくむClassic Infrastructureと、急ピッチで整備中のVPCが併存しています。
両者間で通信ができる[Classic Access] (https://cloud.ibm.com/docs/vpc-on-classic?topic=vpc-on-classic-setting-up-access-to-your-classic-infrastructure-from-vpc)というオプションもあるのですが、現在『Classic側のVSI（仮想サーバ）がVPCと通信するためには、ユーザーがVPCのCIDR宛の静的ルーティングをVSIに追加しなければならない』という制約があります。
このような手間はなるべく省きたいところですが、ClassicのVSIはプロビジョニング時に任意のスクリプトを自動実行させるオプションがあるので、この仕掛けを利用して静的ルーティングの追加処理の自動化にトライしてみました。
作業の流れは次のとおりです。VSIのOSはUbuntu 18.04を前提としています。

1. 静的ルーティングのゴールイメージ
2. Netplanによる静的ルーティングの追加
3. Netplanの設定ファイルを修正するプログラムの作成
4. VSIプロビジョニング時に実行するシェルスクリプトの作成
5. VSIプロビジョニング時の指定方法
6. 結果

---
## 1. 静的ルーティングのゴールイメージ
試しにVSIをひとつプロビジョニングして、ルーティングテーブルを見てみます。


```console:netstat
# netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         169.62.94.97    0.0.0.0         UG        0 0          0 eth1
10.0.0.0        10.87.22.1      255.0.0.0       UG        0 0          0 eth0
10.87.22.0      0.0.0.0         255.255.255.192 U         0 0          0 eth0
161.26.0.0      10.87.22.1      255.255.0.0     UG        0 0          0 eth0
166.8.0.0       10.87.22.1      255.252.0.0     UG        0 0          0 eth0
169.62.94.96    0.0.0.0         255.255.255.224 U         0 0          0 eth1
```

VPC(Gen1)のCIDRを172.18.0.0/18とします。このCIDRを宛先とするパケットは169.62.94.97（eth1）に向かいますが、そうするとインターネットに出ていこうとして結局行方不明になってしまいます。VPCに届けるためには、10.87.22.1（eth0）の方に向けてやるためのルーティングを追加してやる必要があります。

ゴールイメージはこんな感じです。最後の行がVPC用に追加したルーティングです。

```console:netstat
# netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         169.62.94.97    0.0.0.0         UG        0 0          0 eth1
10.0.0.0        10.87.22.1      255.0.0.0       UG        0 0          0 eth0
10.87.22.0      0.0.0.0         255.255.255.192 U         0 0          0 eth0
161.26.0.0      10.87.22.1      255.255.0.0     UG        0 0          0 eth0
166.8.0.0       10.87.22.1      255.252.0.0     UG        0 0          0 eth0
169.62.94.96    0.0.0.0         255.255.255.224 U         0 0          0 eth1
172.18.0.0      10.87.22.1      255.255.192.0   UG        0 0          0 eth0
```

## 2. Netplanによる静的ルーティングの追加
ubuntu 18.04ではネットワークの設定にNetplanを使用します。設定情報は/etc/netplanの下にyamlファイルとして配置し、netplan applyコマンドで反映します。
上記のVSIでは、/etc/netplanの下に00-networking.yamlというファイルがありました。内容は以下のとおりです。

```yaml:00-networking.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      addresses:
        - 10.87.22.3/26
      routes:
        - to: 10.0.0.0/8
          via: 10.87.22.1
        - to: 161.26.0.0/16
          via: 10.87.22.1
        - to: 166.8.0.0/14
          via: 10.87.22.1
      nameservers:
        addresses:
          - 10.0.80.12
    eth1:
      addresses:
        - 169.62.94.108/27
      gateway4: 169.62.94.97
      nameservers:
        addresses:
          - 10.0.80.12
```

真ん中あたりに10.87.22.1（eth0）を経路とする静的ルーティングの定義があります。ここにVPC用の静的ルーティングを追加してやればよさそうです。ゴールイメージは以下のような感じになります。

```yaml:00-networking.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      addresses:
        - 10.87.22.3/26
      routes:
        - to: 10.0.0.0/8
          via: 10.87.22.1
        - to: 161.26.0.0/16
          via: 10.87.22.1
        - to: 166.8.0.0/14
          via: 10.87.22.1
        - to: 172.18.0.0/18
          via: 10.87.22.1
      nameservers:
        addresses:
          - 10.0.80.12
    eth1:
      addresses:
        - 169.62.94.108/27
      gateway4: 169.62.94.97
      nameservers:
        addresses:
          - 10.0.80.12
```

静的ルーティングはip addコマンドで追加することもできますが、VSIの再起動でVPCへの静的ルーティングがリセットされないためには、このNetplan方式で定義を設定してやる必要があります。したがって今回は、設定ファイルに静的ルーティングの定義を追加してnetplan applyコマンドを発行する仕組みを作成し、VSIプロビジョニング時に呼び出されるスクリプティングとして組み込むことにします。

## 3. Netplanの設定ファイルを修正するプログラムの作成
既存のNetplanの設定ファイルを編集して、静的ルーティングの追加情報をインデントを崩さないように挿入するプログラムを作成します。幸い設定ファイルはYAML形式で構造化されているので、YAMLを扱える言語であれば編集処理は容易にプログラミングできそうです。VSIにはpython3が組み込まれており、yamlパッケージも組み込み済みだったので、今回はpythonでプログラミングすることにしました。出来上がりは次のとおりです。

```python:add_route.py
import yaml
import json
import sys

args = sys.argv

routes = ""
gateway = ""
netp_yaml = args[1]
netp_bkup = f'{netp_yaml}.backup'
dest_cidr = args[2]

with open(netp_bkup) as file:
    netplan = yaml.load(file)
    routes = netplan['network']['ethernets']['eth0']['routes']

for route in routes:
    if route['to'] == '10.0.0.0/8':
        gateway = route['via']      # 10.0.0.0/8向けのネクストホップをVPC CIDR向けのネクストホップにする

newroute = f'{{ \"to\" : \"{dest_cidr}\" , \"via\" : \"{gateway}\" }}'
netplan['network']['ethernets']['eth0']['routes'].append(json.loads(newroute))

with open(netp_yaml, 'w') as file:
    file.write(yaml.dump(netplan, default_flow_style=False))
```

netplanの設定ファイルでは、"routes"というキー以下にプライベートネットワーク向けの静的ルーティング情報が配列で記述されています。ここにVPC向けの静的ルーティング情報を追加して、YAML形式で出力してやるのがこのプログラムのポイントです。
netplanの設定ファイル名は、事前に"ファイル名.backup"にリネームされている前提になっています。

## 4. VSIプロビジョニング時に実行するシェルスクリプトの作成
VSIプロビジョニング時に呼び出されて実行するシェルスクリプトを作成します。先のpythonプログラムをGitHubに置いておくことにして、これをgit cloneで入手して実行します。pythonプログラムを実行する前にnetplan設定ファイルをリネームしておき、実行後にはnetplan applyコマンドで修正された設定（静的ルーティングの追加）を反映します。

```bash:new_netplan.sh
#!/bin/bash
cd /tmp
cidr="172.18.0.0/18"
git clone https://github.com/takeyan/add_static_route.git
cd add_static_route
files="/etc/netplan/*.yaml"
for file in $files; do
    mv $file ${file}.backup
    python3 add_route.py $file $cidr
    break
done
netplan apply
```

netplanの設定ファイル名を取得するのにforループを使っていますが、ファイルは1個しかないはずなので本当はループにする必要はありません。他に手頃なサンプルが見当たらなかったので、ここではよくある方法を流用した次第です。無条件にbreakするのでループすることはありません。
シェルスクリプトはpythonプログラムと一緒にGitHubのリポジトリにpushしました。


## 5. VSIプロビジョニング時のスクリプト指定方法
Classic VSIの注文画面の、OSの選択肢のすぐ下の"Add-ons"の中に、"Provision script"という入力フィールドがあります。GitHubにpushしたシェルスクリプトのrawコンテンツのURLをここに指定します。
![ClassicVSI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32083/4f12495a-b944-52bc-684a-760baf1a73ad.png)

[terraform](https://ibm-cloud.github.io/tf-ibm-docs/v0.20.0/r/compute_vm_instance.html) でプロビジョニングする場合には、"post_install_script_uri"という引数でこのURLを指定します。以下に例を示しておきます。

```ts:terraform
resource "ibm_compute_vm_instance" "staticrt6" {
    transient = true
    hostname = "staticrt6"
    domain = "swallowlab.work"
    os_reference_code = "UBUNTU_18_64"
    flavor_key_name = "B1_1X2X25"
    datacenter = "sjc04"
    network_speed = 100
    hourly_billing = true
    private_network_only = false
    local_disk = false
    private_security_group_ids = []
    public_security_group_ids = [1287613, 1236927]
    ssh_key_ids = [1424657]
    post_install_script_uri = "https://raw.githubusercontent.com/takeyan/add_static_route/master/new_netplan.sh"
    notes = "${var.softlayer_username}"
}
```


## 6. 結果

"Provision script"を指定して作成したVSIにログインして、静的ルーティングが追加されていることを確認できました。

```console:netstat
# netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         169.62.94.97    0.0.0.0         UG        0 0          0 eth1
10.0.0.0        10.87.22.1      255.0.0.0       UG        0 0          0 eth0
10.87.22.0      0.0.0.0         255.255.255.192 U         0 0          0 eth0
161.26.0.0      10.87.22.1      255.255.0.0     UG        0 0          0 eth0
166.8.0.0       10.87.22.1      255.252.0.0     UG        0 0          0 eth0
169.62.94.96    0.0.0.0         255.255.255.224 U         0 0          0 eth1
172.18.0.0      10.87.22.1      255.255.192.0   UG        0 0          0 eth0
```

VPC内に作成したVSIに対してpingが通ることも確認できました。

```console:ping
# ping 172.18.0.60
PING 172.18.0.60 (172.18.0.60) 56(84) byte of data.
64 bytes from 172.18.0.60: icmp_seq=1 ttl=52 time=137 ms
64 bytes from 172.18.0.60: icmp_seq=2 ttl=52 time=137 ms
64 bytes from 172.18.0.60: icmp_seq=3 ttl=52 time=137 ms
64 bytes from 172.18.0.60: icmp_seq=4 ttl=52 time=137 ms
^C
--- 172.18.0.60 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3001ms
rtt min/avg/max/mdev = 137.031/137.318/137.688/0.537 ms
```

/etc/netplanにはファイルが２つできています。

```console:ls
# ls /etc/netplan
00-networking.yaml  00-networking.yaml.backup
```

00-networking.yamlにはVPC向けの静的ルーティングが追加されていました。

```yaml:00-networking.yaml
network:
  ethernets:
    eth0:
      addresses:
      - 10.87.22.3/26
      nameservers:
        addresses:
        - 10.0.80.12
      routes:
      - to: 10.0.0.0/8
        via: 10.87.22.1
      - to: 161.26.0.0/16
        via: 10.87.22.1
      - to: 166.8.0.0/14
        via: 10.87.22.1
      - to: 172.18.0.0/18
        via: 10.87.22.1
    eth1:
      addresses:
      - 169.62.94.108/27
      gateway4: 169.62.94.97
      nameservers:
        addresses:
        - 10.0.80.12
  renderer: networkd
  version: 2
```

以上です。