LXC で固定IPアドレスを持つコンテナを簡単に作れるようにした

LXC のデフォルトの設定だと、DHCP でコンテナに IP アドレスが割り振られるため、コンテナ同士で Riak クラスタを組んでみる、みたいなことがやりづらい。 IP アドレスを固定しようと思ったら普通に `/etc/network/interfaces` にそれっぽい設定を書けばいいんだけど、コンテナを作るたびに設定するのは面倒臭いし、間違えて同じ IP アドレスを異なるコンテナに割り当ててしまう事故も起こりやすい。また、IP アドレスを固定するのであれば、ついでに DNS にも登録して名前から IP アドレスを引けるようにしたい。

ということで、次のような設定をしたらすごく便利になった：

1. ホストの `/etc/hosts` に全てのコンテナの名前と IP アドレスを書くことにした。
    * 一つのファイルに全てのコンテナの IP アドレスが集約されるので、どの IP アドレスを使ってないのかすぐにわかる。
2. LXC の Ubuntu テンプレートを改造して、`/etc/hosts` から自分の IP アドレスを引いてきて、適切な `$rootfs/etc/network/interfaces` を吐き出すようにした。
3. dnsmasq を立てて、`/etc/hosts` の内容を DNS 経由で引けるようにした。
4. 簡単にコンテナを作れるように、補助シェルスクリプトを書いた。

## 1. /etc/hosts

こんな感じに、コンテナの IP アドレスを書いておく。

```
127.0.0.1	localhost
127.0.1.1	dev-3

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

# Local containers
10.0.3.1        web-1
10.0.3.2        web-2
10.0.3.3        db-1
(以下略)
```

## 2. LXC の Ubuntu テンプレートの改造

LXC はコンテナを作るときにテンプレートと呼ばれるスクリプトを実行する。Ubuntu のコンテナを作成するときは、`/usr/share/lxc/templates/lxc-ubuntu` が実行される。これを開いてみると中身はただの bash スクリプトになっていて、`lxc-create` で与えられた引数がこのスクリプトに渡ってくるようになっている。今回はこのスクリプトを改造して、`/etc/hosts` に与えられたコンテナ名に対応する IP アドレスが登録されている場合は、`$rootfs/etc/network/interfaces` をそれっぽく出力するようにする。

まず、`lxc-ubuntu` をコピーして `lxc-ubuntu-nojima` を作る。（ファイル名から `lxc-` を削った文字列がテンプレート名になる）

```bash
sudo cp /usr/share/lxc/templates/lxc-ubuntu{,-nojima}
```
次にテンプレート内の `configure_ubuntu()` の中身を書き換える。diff はこんな感じ：

```diff
--- /usr/share/lxc/templates/lxc-ubuntu	2014-04-15 00:49:58.000000000 +0900
+++ /usr/share/lxc/templates/lxc-ubuntu-nojima	2014-06-22 18:03:15.209620112 +0900
@@ -53,21 +53,41 @@
     release=$3
     user=$4
     password=$5
 
     # configure the network using the dhcp
-    cat <<EOF > $rootfs/etc/network/interfaces
+    ipaddr=$(sed -e 's/#.*$//' /etc/hosts | awk "\$2~/^${hostname}\$/ { print \$1 }")
+    if [ -z "$ipaddr" ]; then
+        cat <<EOF > $rootfs/etc/network/interfaces
 # This file describes the network interfaces available on your system
 # and how to activate them. For more information, see interfaces(5).
 
 # The loopback network interface
 auto lo
 iface lo inet loopback
 
 auto eth0
 iface eth0 inet dhcp
 EOF
+    else
+        echo "Set the IP address of $hostname to $ipaddr"
+        cat <<EOF > $rootfs/etc/network/interfaces
+# This file describes the network interfaces available on your system
+# and how to activate them. For more information, see interfaces(5).
+
+# The loopback network interface
+auto lo
+iface lo inet loopback
+
+auto eth0
+iface eth0 inet static
+    address $ipaddr
+    netmask 255.255.0.0
+    gateway 10.0.0.1
+    dns-nameservers 10.0.0.6
+EOF
+    fi
 
     # set the hostname
     cat <<EOF > $rootfs/etc/hostname
 $hostname
 EOF
```

一応解説しておくと、

```bash
ipaddr=$(sed -e 's/#.*$//' /etc/hosts | awk "\$2~/^${hostname}\$/ { print \$1 }")
```
の部分で、`/etc/hosts` から IP アドレスを取得している。 `sed -e 's/#.*$//' /etc/hosts` の部分が `/etc/hosts` のコメント以外の部分を主力していて、それを `awk "\$2~/^${hostname}\$/ { print \$1 }"` に突っ込んで、「スペースで区切られた 2 番目の部分が `${hostname}` に一致していたら、1 番目の部分を出力」している。

そして

```bash
if [ -z "$ipaddr" ]; then
    cat <<EOF > $rootfs/etc/network/interfaces
...
```

`$ipaddr` が空文字列、つまり IP アドレスが取得できなかった場合は、デフォルトの interfaces を出力し、そうでない場合は、

```bash
    else
        echo "Set the IP address of $hostname to $ipaddr"
        cat <<EOF > $rootfs/etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address $ipaddr
    netmask 255.255.0.0
    gateway 10.0.0.1
    dns-nameservers 10.0.0.6
EOF
    fi
```

として、eth0 に固定 IP アドレスを割り当てている。 `netmask`, `gateway` に何を設定すべきかは環境によって異なるので、このテンプレートを流用する場合は環境に合わせてこの部分を書き換えないといけないことに注意。 `dns-nameservers` に関しては、今回ホストに DNS サーバを立てるので、ホストの IP アドレスにしている。

これで、`ubuntu-nojima` という名前のテンプレートを使って `lxc-create` することができるようになった。

例えば、`web-1` というコンテナを作るときはこんな感じで指定する。いろいろくっついているオプションはお好みで。

```bash
sudo lxc-create -t ubuntu-nojima -n web-1 -- --user=nojima --password=secret --auth-key=/home/nojima/.ssh/id_rsa.pub --mirror="http://ftp.jaist.ac.jp/ubuntu"
```

## 3. dnsmasq

dnsmasq はお手軽 DNS サーバ兼 DHCP サーバで、デフォルトで `/etc/hosts` からホスト名と IP アドレスの対応を読んで勝手に DNS リクエストに答えたり、いい感じに上流の DNS にフォワードしてくれたりする。上流の DNS サーバは `/etc/network/interfaces` の `dns-nameservers` の部分に書いておくと自動的に認識してくれる模様。
なので、何の設定もしなくても apt-get して起動するだけで DNS サーバとしてそれっぽく動いてくれる。

（普通だと、`/etc/network/interfaces` の `dns-nameservers` に書くと、自動的に `/etc/resolv.conf` に書きだされてしまうと思うのだが、dnsmasq が動いている場合は謎のフックにより `/etc/resolv.conf` ではなく `/var/run/dnsmasq/resolv.conf` の方に書きだされ、`/etc/resolv.conf` の方は `nameserver 127.0.0.1` が入るという挙動をしていた。ちゃんと動作を追っていないので詳細は不明。）

今回は
1. ホストでは、デフォルトの設定で dnsmasq を動かし、
2. コンテナでは、`/etc/network/interfaces` の `dns-nameservers` にホストの IP アドレスを指定する
ことで、コンテナからもコンテナ名を DNS で引けるようにした。


## 4. シェルスクリプト化

2 で書いたコンテナ作成コマンドはあまりにもタイプするのが面倒くさいのと、コンテナを作成したらコンテナの起動と dnsmasq のリロードを行わないといけないので、手順を簡単にするためにシェルスクリプトを書いた。

```bash
#!/bin/sh
if [ $# -ne 1 ]; then
    echo "Usage: create-container NAME" 1>&2
    exit 1
fi
name=$1
ipaddr=$(sed -e 's/#.*$//' /etc/hosts | awk "\$2~/^${name}\$/ { print \$1 }")
if [ -z "$ipaddr" ]; then
    echo "ERROR: IP address of ${name} was not found in /etc/hosts." 1>&2
    exit 1
fi
set -xe
lxc-create -t ubuntu-nojima -n $name -- --user=nojima --password=secret --auth-key=/home/nojima/.ssh/id_rsa.pub --mirror="http://ftp.jaist.ac.jp/ubuntu"
service dnsmasq force-reload
lxc-start -d -n $name
```

これで、以下の手順で固定 IP アドレスを持つコンテナを作れるようになった。便利！！

```bash
sudo vim /etc/hosts           # ホスト名と IP アドレスの対応を追加する。
sudo create-container $NAME   # コンテナ作成 + 起動
```

## その他

今回は IP アドレスの固定を行ったが、Ubuntu テンプレートの改造は他にもいろいろできる。とりあえず↓のようなことをやってみた。

- `--user` で指定したユーザに sudo 権限を付与するようにした。
- `--password` に空を指定することで、パスワードを disable したユーザを作成できるようにした。
    - つまりどんなパスワードを入れても認証に失敗するユーザになる。
- `/var/lib/lxc/$container_name/fstab` に
    ```
    /data   data    none    bind,create=dir
    ```
    と書いてホストと `/data` を共有する。

とにかく汎用性は高いので、いじってみると楽しい。

## ToDo

- コンテナを作った後の設定をするのが面倒臭いので、Chef とかで自動化したい。
- ファイルシステムを btrfs にしたい。