VagrantとAnsibleでLXCの環境構築

# はじめに

最近はDockerの話題が多いですが、本番環境では使ってないという方も多いと思います。環境構築手順をDockerfileでコンパクトにまとめておけるのが便利な一方、コンテナでは[Railsアプリ開発用のDockerコンテナでunicornを起動するにはsupervisord + unicornherderが良い - Qiita]( http://qiita.com/hnakamur/items/f4b270b535d0b8ec3029 )のように通常のLinuxサーバとは違う構成にする必要があるケースもあります。

その点[LXC]( https://linuxcontainers.org/ja/lxc/introduction/ )はコンテナ内でも通常のLinuxサーバとほぼ同じ感覚で環境構築できるのが魅力です。

この記事では[第5回 コンテナ型仮想化の情報交換会＠大阪 - コンテナ型仮想化の情報交換会]( https://sites.google.com/site/containerstudy/home/5thcontainerstudy )で紹介したAnsibleプレーブックを少し発展させて、LXCホストからLXCコンテナへのnginxプロキシ設定もdnsmasqで名前解決するようにしたので、それも含めて紹介します。

# プレーブックの使い方

プレーブックは[hnakamur/lxc-ansible-playbooks]( https://github.com/hnakamur/lxc-ansible-playbooks )にあります。Vagrantfile も含めてあるので `vagrant up` するだけでセットアップが完了します。なお、実行にはMacBook Air Mid 2013で約11分と結構かかります。

実行が完了したら、ブラウザで
http://c1.192.168.33.2.xip.io/
にアクセスすると `Welcome to nginx at container1!` と表示され、
http://c2.192.168.33.2.xip.io/
にアクセスすると `Welcome to nginx at container2!` と表示されます。

# サーバ構成

今回構築するサーバ構成図を以下に示します。

![ansible-lxc-server-diagram.png](https://qiita-image-store.s3.amazonaws.com/0/3227/f26d2217-b6d6-b226-7748-7a56e812f7d3.png)

Mac OS XまたはWindowsのVagrant上にCentOS6.6またはUbuntu14.04のLXCホストをセットアップし、その上で2つのLXCコンテナを作成します。管理する項目を減らしたいのでコンテナのIPアドレスはDHCPを使います。

そしてLXCホストからコンテナにnginxでプロキシして http://c1.192.168.33.2.xip.io と http://c2.192.168.33.2.xip.io というURLでMac OS XまたはWindowsのマシンからアクセスできるようにします。

VagrantのVMに192.168.33.2というIPアドレスを設定しておいて、http://c1.192.168.33.2.xip.io というURLでアクセスすれば[xip.io]( http://xip.io/ )のDNSサービスによって192.168.33.2に名前解決されるので、リクエストがVagrantのVMに送られます。ちなみにxip.ioは[chef-server - Chef Analytics(ChefServer12)のお試しはxip.io利用がおすすめ - Qiita]( http://qiita.com/sawanoboly/items/8692ceab136b42669498 )で知りました。ありがとうございます！

そこでnginxの設定でバーチャルホストがc1.192.168.33.2.xip.ioならコンテナ1、c2.192.168.33.2.xip.ioならコンテナ2にプロキシするというわけです。

# Ansibleプレーブックの説明

## VagrantのShellプロビジョナーでVagrant VM内にAnsibleをセットアップ

Vagrantには[Ansibleプロビジョナ]( https://docs.vagrantup.com/v2/provisioning/ansible.html )が用意されています。また、Mac OS XにAnsibleをインストールしておいてそこからVagrant VMにつないでプレーブックを実行するという方法もあります。

ですが、今回は[Shellプロビジョナー]( https://docs.vagrantup.com/v2/provisioning/shell.html )を使ってVagrant VM内にAnsibleをセットアップして、VM上でAnsibleを実行するようにしてみました。

Ansibleは操作対象としてはWindowsマシンを扱えますが、Windowsマシンで起動することはできません。今回の方式であれば、Mac OS XでもWindowsでも `vagrant up` するだけで全てのセットアップを自動で行えるのが利点です。

```//github.com/hnakamur/lxc-ansible-playbooks/blob/master/Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "hnakamur/centos6.6-x64"
  #config.vm.box = "hnakamur/ubuntu-14.04-x64"
  config.vm.network :private_network, ip: "192.168.33.2"
  config.vm.boot_timeout = 120

  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id, "--memory", "2048"]
  end

  config.vm.provision "shell", path: "install_ansible.sh"
  config.vm.provision "shell", path: "setup_lxc_host.sh"
  config.vm.provision "shell", path: "setup_containers.sh"
end
```

Ansibleのインストールは[install_ansible.sh]( https://github.com/hnakamur/lxc-ansible-playbooks/blob/master/install_ansible.sh )で行っています。 `yum` や `apt-get` ではなく `pip` を使ってインストールしていますので、Ansibleの最新版が使えます。

## PYTHONUNBUFFERED環境変数

Ansibleをセットアップ後、以下のシェルスクリプトでLXCホストをセットアップしています。

```sh://github.com/hnakamur/lxc-ansible-playbooks/blob/master/setup_lxc_host.sh
#!/bin/sh
export PYTHONUNBUFFERED=true
cd /vagrant/provisioning
ansible-playbook lxc_host.yml
```

ここで ```export PYTHONUNBUFFERED=true``` を設定しておくのがポイントです。これがないとプレーブック実行中のログが表示されないのですが、これを設定すると経過が見られるので快適です。

# LXCコネクションプラグインを使用

Ansibleは操作対象のマシンにsshで接続するのが定番ですが、[Connection Type Plugin]( http://docs.ansible.com/developing_plugins.html#connection-type-plugins )という仕組みが用意されていて、違う方法で接続することも可能です。

今回はサードパーティの[Ansible Connection Plugin for lxc containers]( https://github.com/Mic92/ansible-lxc )を使用してみました。

実はAnsibleは標準で[libvirt経由でLXCに接続するプラグイン]( https://github.com/ansible/ansible/blob/devel/lib/ansible/runner/connection_plugins/libvirt_lxc.py )が同梱されています。

ですが、[\[lxc-devel\] LXD an "hypervisor" for containers (based on liblxc)]( https://lists.linuxcontainers.org/pipermail/lxc-devel/2014-November/010820.html )でも言われているように、コンテナは仮想マシンとはいろいろ違うので、仮想マシンに見せかけてlibvirt経由で扱うのはあまりよいアイデアではないと思います。

[Ansible Connection Plugin for lxc containers]( https://github.com/Mic92/ansible-lxc )を使えばlibvirt無しで直接LXCコンテナに接続できます。このプラグインを使えばコンテナ内にsshサーバを立ててアカウントを用意する必要が無いので便利です。

## ダイナミックインベントリ

Ansibleでは[インベントリファイル]( http://docs.ansible.com/intro_inventory.html )に操作対象のサーバの名前を書いておく必要があります。

今回は[ダイナミックインベントリ]( http://docs.ansible.com/intro_dynamic_inventory.html )の仕組みを使ってこのファイルのメンテナンスを不要にしました。[inventory-lxc.py]( https://github.com/hnakamur/lxc-ansible-playbooks/blob/master/provisioning/inventory-lxc.py )でLXCホストと起動中のLXCコンテナのリストを返すようにしています。

## CentOSとUbuntu両対応にするための仕組み

特定のOSファミリーだったり指定のバージョン以上の場合のみモジュールを実行する例が[Conditionals]( http://docs.ansible.com/playbooks_conditionals.html#the-when-statement )に書かれています。

今回はRedHat用とDebian用のタスクファイルをそれぞれ作ってインクルードするようにしました。

OSファミリーやディストリビューションによって、定義する変数を変えたり値を変えたい場合は[include_vars]( http://docs.ansible.com/include_vars_module.html )の例にあるように、 `include_vars` と `with_first_found` を組み合わせるのが便利です。

実際の例を以下に示します。

```//github.com/hnakamur/lxc-ansible-playbooks/blob/539282879aa978552accd1b631b308b5c4243140/provisioning/lxc/tasks/main.yml
---
- include_vars: "{{ item }}"
  with_first_found:
  - "../vars/{{ ansible_distribution }}.yml"
  - "../vars/{{ ansible_os_family }}.yml"
  - "../vars/default.yml"

- include: Debian.yml
  when: ansible_os_family == "Debian"

- include: RedHat.yml
  when: ansible_os_family == "RedHat"
```

## LXCホストからコンテナをホスト名で参照するようにした

ここが今回少し発展させた部分です。

発表スライドの[コンテナのIPアドレス取得]( http://hnakamur.github.io/lxc-ansible-presentation/#_コンテナのipアドレス取得 )では、`lxc-attach`で`ip`コマンドを実行することでコンテナのIPアドレスを取得してnginxの設定ファイルに書くようにしていました。

CentOSでは `/etc/dhcp/dhclient-eth0.conf` に `prepend domain-name-servers 10.0.3.1;` という行を入れることでLXCホスト上でコンテナ名の名前解決ができていたのですが、[Enable dns for containers by hnakamur · Pull Request #4 · hnakamur/lxc-ansible-playbooks]( https://github.com/hnakamur/lxc-ansible-playbooks/pull/4/files )の変更でUbuntuでもLXCホスト上でコンテナ名の名前解決ができるようになりました。

nginxのプロキシでバックエンドのサーバをホスト名で指定する部分は[Nginxでproxy_passにホスト名を書いた時の名前解決のタイミング - (ひ)メモ]( http://d.hatena.ne.jp/hirose31/20131112/1384251646 )を真似させていただきました。ありがとうございます！今日試したところなので『なぞの「DNS error (32: Unknown error), query id:XXXXX」』現象には遭遇していないのですが、情報をお持ちの方はぜひ元記事にコメントしてください！

# 今後の課題

## iptablesとdnsmasqの設定を理解して最適化したい

Ubuntuでは `apt-get` で `lxc` をインストールすれば `iptables` の設定も `dnsmasq` の設定も全てやってくれるので楽です。

CentOSでは `epel` から `yum` で `lxc` をインストールは出来るのですが、 `iptables` と `dnsmasq` の設定は自前で行う必要があります。

この部分は以前[VirtualBox上のCentOS 6.4でLXCをセットアップするAnsible playbook - Qiita]( http://qiita.com/hnakamur/items/75a5d4ea964b0663719b )に書いた時から進歩しておらず、よくわからないままとりあえず使えているからまあいいかという状態になっています。

## CentOSではLXCホストからコンテナ名でpingするとunknown hostになるのを解消したい

`dig` や `nslookup` はLXCホストで実行しても名前解決できるのですが、なぜか `ping` では `unknown host` になってしまいます。

```
[vagrant@localhost ~]$ dig container1

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.30.rc1.el6 <<>> container1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15885
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;container1.			IN	A

;; ANSWER SECTION:
container1.		0	IN	A	10.0.3.149

;; Query time: 0 msec
;; SERVER: 10.0.3.1#53(10.0.3.1)
;; WHEN: Mon Dec  8 02:10:33 2014
;; MSG SIZE  rcvd: 44

[vagrant@localhost ~]$ nslookup container1
Server:		10.0.3.1
Address:	10.0.3.1#53

Name:	container1
Address: 10.0.3.149

[vagrant@localhost ~]$ ping container1
ping: unknown host container1
```

`tcpdump` を動かしておいて `ping container1` を試したときの出力はこんな感じでした。

```
[vagrant@localhost ~]$ sudo tcpdump -nn 'port 53'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
01:55:59.668963 IP 10.0.2.15.6559 > 10.0.2.3.53: 46809+ A? container1.bai.ne.jp. (38)
01:55:59.787368 IP 10.0.2.3.53 > 10.0.2.15.6559: 46809 NXDomain* 0/0/0 (38)
```

試した時の `/etc/resolv.conf` は以下の通りです。 `search bai.ne.jp` と `nameserver 10.0.2.3` は自宅で利用しているISPのDHCPで追加されたもので、 `nameserver 10.0.3.1` が `dnsmasq` 用に追加した設定です。

```
[vagrant@localhost ~]$ cat /etc/resolv.conf
; generated by /sbin/dhclient-script
search bai.ne.jp
nameserver 10.0.3.1
nameserver 10.0.2.3
```

`tcpdump` の結果を見ると通信が `10.0.2.3.53` に行っていて `10.0.3.1:53` じゃないのと `container1.bai.ne.jp` というホスト名を名前解決しようとしているのが、気になるところです。

ただ、これはUbuntuのときも同じなのですが、こちらは `dig` も `nslookup` も `ping` も正常に使えます。

```
vagrant@ubuntu-1404:~$ dig container1

; <<>> DiG 9.9.5-3-Ubuntu <<>> container1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51152
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;container1.			IN	A

;; ANSWER SECTION:
container1.		0	IN	A	10.0.3.189

;; Query time: 5 msec
;; SERVER: 10.0.3.1#53(10.0.3.1)
;; WHEN: Sun Dec 07 17:16:36 UTC 2014
;; MSG SIZE  rcvd: 44

vagrant@ubuntu-1404:~$ nslookup container1
Server:		10.0.3.1
Address:	10.0.3.1#53

Name:	container1
Address: 10.0.3.189

vagrant@ubuntu-1404:~$ ping container1
PING container1 (10.0.3.189) 56(84) bytes of data.
64 bytes from container1 (10.0.3.189): icmp_seq=1 ttl=64 time=0.039 ms
64 bytes from container1 (10.0.3.189): icmp_seq=2 ttl=64 time=0.073 ms
64 bytes from container1 (10.0.3.189): icmp_seq=3 ttl=64 time=0.067 ms
64 bytes from container1 (10.0.3.189): icmp_seq=4 ttl=64 time=0.108 ms
^C
--- container1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 2999ms
rtt min/avg/max/mdev = 0.039/0.071/0.108/0.026 ms
```

Ubuntuで `ping` を実行した時のtcpdumpの結果と `/etc/resolv.conf` の内容は以下の通りです。

```
vagrant@ubuntu-1404:~$ sudo tcpdump -nn 'port 53'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
17:03:02.101345 IP 10.0.2.15.13842 > 10.0.2.3.53: 29346+ A? container1.bai.ne.jp. (38)
17:03:02.163127 IP 10.0.2.3.53 > 10.0.2.15.13842: 29346 NXDomain* 0/0/0 (38)
```

```
vagrant@ubuntu-1404:~$ cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 10.0.3.1
nameserver 10.0.2.3
search bai.ne.jp
```

こちらも `10.0.2.3:53` に通信が行っています。試しに `tcpdump` の `-i eth0` を `-i eth1` や `-i lxcbr0` にして `ping container1` も実行してみたのですが、 `tcpdump` では何も出力されませんでした。

CentOSでは `dnsmasq` は `0.0.0.0` でリッスンするようにしたのですが、Ubuntuのほうは `ps` で確認すると `10.0.3.1` でリッスンするようになっています。

```
vagrant@ubuntu-1404:~$ ps auxww | grep dnsmasq
lxc-dns+  6557  0.0  0.0  28208  1072 ?        S    12:52   0:00 dnsmasq -u lxc-dnsmasq --strict-order --bind-interfaces --pid-file=/run/lxc/dnsmasq.pid --conf-file= --listen-address 10.0.3.1 --dhcp-range 10.0.3.2,10.0.3.254 --dhcp-lease-max=253 --dhcp-no-override --except-interface=lo --interface=lxcbr0 --dhcp-leasefile=/var/lib/misc/dnsmasq.lxcbr0.leases --dhcp-authoritative
```

`tcpdump` で見ると `10.0.2.3:53` に通信が行っているのに `10.0.3.1` でリッスンしている `dnsmasq` で名前解決できているのが不思議です。どういう仕組みになってるんでしょう。

なお、CentOSもUbuntuも `ping` と `nslookup` では `tcpdump` に同様の結果が出るのですが、 `dig` では何も出ませんでした。`dig` の初回実行時は見逃していたのかもしれませんが、その後はキャッシュされているのでしょうか。

# おわりに

`vagrant up` だけでLXCホストとLXCコンテナの環境を構築できるAnsibleプレーブックを紹介しました。最近Dockerでの環境構築に時間を取られていて普段利用してないのですが、適材適所でLXCも活用していきたいと思います。

iptables、dnsmasq、pingの名前解決といったネットワーク周りのインフラ環境構築の基本的なところがよくわかってないので、モヤモヤした状態になっています。良い書籍とかあったら是非教えてください！