Vagrant, Docker and Ansible

Ansibleなどのプロビジョニングスクリプトを同じホストに何度も適用していると、そのスクリプトが正しく動くのか怪しくなってきます。 新規環境で流すと実は動かなかったりなんて。 この記事ではコンテナを使って高速に環境を再作成することで、いつも正しく動くプロビジョニングスクリプトを記述する方法を紹介します。 Vagrant用が生成するssh_configをAnsibleに渡すので、Ansibleのinventoryファイルを編集せずにデプロイ先をコンテナに切り替えることができます。 VagrantfileおよびAnsible playbooksはGitHubで公開しています。

この記事では、例としてAnsibleでElasticsearchクラスタを構築します。 また高速化のためにaptキャッシュサーバも配置します。 Vagrantfileによって以下のホストを作成します。

  • apt-cacher: aptキャッシュサーバ
  • elasticsearch-client Elasticsearchのクライアントノードで、クラスタのエンドポイントとなる
  • elasticsearch-master-{n} Elasticsearchのマスターノード
  • elasticsearch-data-{n} Elasticsearchのデータノード

apt-cacherの構築はAnsibleの対象ではないので、Dockerfileで環境を作ります。 各Elasticsearchノードは、SSHができる環境をDockerfileで作ります。

コンテナを定義する

ますはじめに、DockerのEmbedded DNS serverを使うために、ネットワークを定義します。

docker network create elasticsearch

このネットワークに接続したコンテナは、コンテナ名で他のコンテナの名前解決できるようになります。

Vagrantfileを記述

Vagrantfileの全貌は以下のとおりです。

# Vagrantfile
def vanilla_container(config, name, &proc)
  config.vm.define name do |node|
    node.vm.provider "docker" do |docker|
      docker.name = name
      docker.create_args = ["--hostname=#{name}", "--network=elasticsearch"]
      docker.build_dir = "vanilla"
      docker.has_ssh = true

      proc.call(docker) if block_given?
    end
  end
end

Vagrant.configure("2") do |config|

  config.ssh.username = "vagrant"
  config.ssh.password = "vagrant"

  config.vm.define "apt-cacher" do |node|
    node.vm.provider "docker" do |docker|
      docker.name = "apt-cacher"
      docker.create_args = ["--hostname=apt-cacher", "--network=elasticsearch"]
      docker.build_dir = "apt-cacher"
    end
  end

  (1..4).each do |i|
    vanilla_container config, "elasticsearch-master-#{i}"
  end
  (1..6).each do |i|
    vanilla_container config, "elasticsearch-data-#{i}"
  end
  vanilla_container config, "elasticsearch-client" do |docker|
    docker.expose = [9200]
  end
end

さきほど作成したネットワークにコンテナを接続するために、docker.create_args--network=elasticsearchを指定します。 そしてコンテナ名をdocker.nameで指定して、ホスト名をdocker.create_args--hostname=apt-cacherを追加することで設定します。 今回はElasticsearchノードのコンテナをヘルパメソッドでガッと定義します。 エンドポイントとなるelasticsearch-clientノードは、ポート9200をexposeします。

vanillaコンテナの定義

vanillaのDockerfileは、実際のAnsibleのターゲットホストに近い状態を作るため、必要最低限の環境を構築します。 sudoやsshサーバの設定については、Vagrant Docker providerでSSHができるまでをどうぞ。 またapt-cacherをaptのキャッシュサーバとしてプロキシに設定します。

# vanilla/Dockerfile
FROM ubuntu:16.04

RUN apt update && apt install -y --no-install-recommends \
      openssh-server \
      sudo \
      ca-certificates \
      apt-transport-https \
      python \
      curl

# vagrantユーザを追加
RUN useradd --create-home --user-group vagrant && \
    echo -n 'vagrant:vagrant' | chpasswd && \
    echo 'vagrant ALL=NOPASSWD: ALL' >/etc/sudoers.d/vagrant

# apt-cacherをプロキシに設定
RUN echo 'Acquire::http::Proxy "http://apt-cacher:3142/";' >/etc/apt/apt.conf.d/02proxy

RUN mkdir -p /var/run/sshd
CMD /usr/sbin/sshd -D

apt-cacherコンテナの定義

apt-cacherコンテナは、aptキャッシュのみを行うので、sshサーバやsudoすら必要ありません。 CMDapt-cacher-ngを起動する、シンプルなコンテナです。

# apt-cacher/Dockerfile
FROM ubuntu:16.04
RUN apt update && apt install -y --no-install-recommends \
      ca-certificates \
      apt-cacher-ng

VOLUME "/var/cache/apt-cacher-ng"

RUN mkdir -p /var/run/apt-cacher-ng
CMD /usr/sbin/apt-cacher-ng -c /etc/apt-cacher-ng foreground=1

コンテナを起動する

この状態でコンテナを立ち上げてみましょう

vagrant up

vagrant sshで各ホストにログインできるので、他のホストの名前が引けるか、apt-cacherが正常に動作しているかを確認してみましょう。

vagrant ssh elasticsearch-client -- getent hosts elasticsearch-master-1
vagrant ssh elasticsearch-client -- sudo apt update

Ansibleを流す

Ansible playbookは平凡なElasticsearchクラスタを構築するのみです。 詳しくはをリポジトリを参照してください。

次にAnsibleを流すために、ssh_configを作ります。 引数なしのvagrant ssh-configだと、apt-cacherの設定も作ろうとして失敗するため、apt-cacherを除いたホストを指定してssh_configを作ります。

vagrant status | \
    awk '/running/ { print $1 }' | \
    grep -v 'apt-cacher' | \
    xargs -n1 vagrant ssh-config >ssh_config

そしてssh_configをAnsibleに渡すために、ansible.cfgを作ります。

cat >ansible.cfg <<EOF
[ssh_connection]
ssh_args = -F ssh_config
EOF

そして流します。

ansible-playbook --inventory-file=ansible/inventories/hosts --sudo ansible/site.yml

出来上がったらクライアントノードからクラスタの状態を見てみましょう。number_of_nodesが11、number_of_data_nodesが6となっていれば正常にクラスタが作成できています。

client_ip=$(docker inspect elasticsearch-client  | \
    jq -r '.[].NetworkSettings.Networks.elasticsearch.IPAddress')
curl http://${client_ip}:9200/_cluster/health | jq '.'