そんな今日この頃の技術ネタ

本家側に書くほどでもない小ネタ用

AnsibleでDockerコンテナをデプロイする

直近ではDocker製ツールを用いた複数サーバへのコンテナの展開を考えていたのだが・・・

blue1st-tech.hateblo.jp

常々書いているようにホスト側のCentOS7とは相性が悪いところもあり、 またトラブル時の対応方法や他のメンバーへの周知に不安があるところ。

それに今回の案件ではマルチホストネットワークやスケーリングは必要ではないわけで、 あえて新しいツールを無理に使うよりも、 サーバのセッティングの際に使用しているAnsibleを用いた方が学習コストも抑えられるしシンプルで良いと判断した。

なにより、インフラ側と開発側が共通のツールに親しんでおくことは実運用においてメリットが大きいように思う。


Vagrantで実験環境を準備

Vagrantfileを作成し、とりあえず二台ほどCentOS7サーバを用意する。

Vagrant.configure(2) do |config|
  config.vm.box = "CentOS7"
  config.vm.box_url = "https://github.com/holms/vagrant-centos7-box/releases/download/7.1.1503.001/CentOS-7.1.1503-x86_64-netboot.box"
  config.vm.define "app1" do |server|
    server.vm.network "private_network", ip: "192.168.33.11"
  end
  config.vm.define "app2" do |server|
    server.vm.network "private_network", ip: "192.168.33.12"
  end
end

あとは立ち上げて、以降のAnsible操作で利用できるようにSSHの設定を書き込んでおく。

$ vagrant up
$ vagrant ssh-config -host >> ~/.ssh/config

blue1st-tech.hateblo.jp


Ansible

さて本題。

構成管理ツールといえば以前にはChefにも挑戦したのだが、 いかんせん覚えなければいけないことが多くてしんどくて、 参考書を一通りなめたっきりになってしまった。

その点においてAnsibleはシンプルで理解しやすいのが良い。

初めてのAnsible

初めてのAnsible

DockerまわりについてもDocker Composeの記述と感覚的に近いものがあって馴染みやすかった。

blue1st.hateblo.jp

導入

ひとまずは公式ドキュメントに従えば良い。

Installation Guide — Ansible Documentation

僕の場合はUbuntu環境なので以下のコマンドでホストに導入。

$ sudo apt-get install software-properties-common
$ sudo apt-add-repository ppa:ansible/ansible
$ sudo apt-get update
$ sudo apt-get install ansible

インベントリファイルを記述する ./hosts

作業ディレクトリにhostsという接続先を定義したファイルを作成する。

[server_a]
app1

[server_b]
app2

[docker_server:children]
server_a
server_b

app1と2とをそれぞれ別の役割を持つサーバとして定義し、 それら全てについてdockerを用いるサーバであると記載する。

個人的にはこの分類こそがAnsibleを効率的に使う上でのセンスの発揮どころなように思う。

ここの分類が不適切だと、後々の記述において無理な場合分けをしないといけなくなり、プレイブックがカオス化してしまう。

Ansibleの設定を記述 ./ansible.conf

今回はひとまず最低限に。

作業ディレクトリ直下にansible.confというファイルを作成する。

[defaults]
hostfile = ./hosts
remote_user = vagrant

Vagrantで立ち上げたホストにはvagrantというユーザでアクセスするの記載しておく。

dockerをインストールするroleを作成する ./roles/docker/tasks/main.yml

Ansibleでは完結した作業をroleという概念で扱う。

今回だと全てのサーバで共通してDockerを扱うため、 そのセッティングを行うroleを作成する。

作業ディレクトリ直下にrolesというディレクトリと、 その下にdockerというディレクトリを用意する。

そしてdockerディレクトリ以下に実作業を記述するtasksディレクトリを作成し、 その中に以下のmain.ymlを記述する。

ファイル配置

└── roles
     └── docker
         └── tasks
           └── main.yml

main.yml

---
- block:
  - name: basic packages
    yum: name={{item}} state=latest
    with_items:
      - python
      - docker-python

  - name: yum update
    yum: name=*

- name: service check
  service: name=docker state=started
  register: docker_service
  ignore_errors: True

- block:
  - name: Get installer
    get_url: url=https://get.docker.com/ dest=~/docker-install.sh validate_certs=yes

  - name: Install docker-engine
    shell: sh ~/docker-install.sh

  - name: Reload systemd
    shell: systemctl daemon-reload

  - name: Start Docker
    service: name=docker state=started

  when: docker_service|failed

- name: Join User Group
  user: name=vagrant groups=docker append=yes

必要なパッケージおよびDockerをインストールし、 操作のためにvagrantをdockerユーザグループに参加させる。

shellとかyumとかserviceといった項がAnsibleではModuleとよばれる、 実際にサーバに行わせる動作を記述する部分になる。

All modules — Ansible Documentation

まとまった作業はblockという項目でくくることができ、 when句を用いて状況によって実行するしないを分けることもできる。

Dockerのインストールは重複しないように、事前にサービスの起動状態を判定している。

コンテナをデプロイするためのroleを作成する ./roles/deploy/tasks/main.yml

先ほどのdockerのインストールと同じ要領で、 roles以下にdeployというディレクトリを作成する。

そしてそのtasks/main.ymlとして以下の記述をする。

---
- name: Deploy Container
  docker_container:
    name: "{{docker_name}}"
    image: "{{docker_image}}"
    pull: true
    restart_policy: always
    state: started
    ports: "{{docker_ports}}"

Docker用のモジュールはいくつもあって迷うところだが、 dockerモジュールは非推奨となっているので、 今から使うならばdocker_containerモジュールが良いだろう。

docker_container - manage docker containers — Ansible Documentation

さて、{{}}の部分は変数となっており、 今回であればapp1とapp2でそれぞれ値を変えて、 別々のコンテナを記述したい箇所である。

そのようなサーバの役割ごとの変数は作業ディレクトリ直下にgroup_varsというディレクトリを作り、 その下にサーバの役割ごとのファイルを記述すればよい。

今回は下記のものを作成した。

group_vars/server_a

docker_name: "nginx"
docker_image: nginx:latest
docker_ports:
  - 80:80
  - 433:433

group_vars/server_b

docker_name: "redis"
docker_image: redis:latest
docker_ports:
  - 6379:6379

これにより、hosts内でserver_aとして定義されたapp1にはnginx、 server_bとしたapp2にはredisがデプロイされるはずである。

site.ymlの作成 ./site.yml

最後に、ansibleが直接作業内容を読むためのファイルsite.ymlを作業ディレクトリ直下に作成する。

- hosts: docker_server
  become: true
  roles:
    - docker

- hosts: docker_server
  roles:
    - deploy

become項はsudoで作業させるか否かを示す。

もっと複雑になればinclude構文を使ってファイルを分割していくこともできる。

最終的な作業ディレクトリ内のファイル構成

├── Vagrantfile
├── ansible.cfg
├── group_vars
│   ├── server_a
│   └── server_b
├── hosts
├── roles
│   ├── deploy
│   │   └── tasks
│   │       └── main.yml
│   └── docker
│    └── tasks
│        └── main.yml
└── site.yml

実行してみる

ansible-playbook site.ymlというコマンドで動かしてみる。

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [app2]
ok: [app1]

TASK [docker : basic packages] *************************************************
changed: [app1] => (item=[u'python', u'docker-python'])
changed: [app2] => (item=[u'python', u'docker-python'])

TASK [docker : yum update] *****************************************************
ok: [app2]
ok: [app1]

TASK [docker : service check] **************************************************
fatal: [app2]: FAILED! => {"changed": false, "failed": true, "msg": "systemd could not find the requested service \"'docker'\": "}
...ignoring
fatal: [app1]: FAILED! => {"changed": false, "failed": true, "msg": "systemd could not find the requested service \"'docker'\": "}
...ignoring

TASK [docker : Get installer] **************************************************
changed: [app1]
changed: [app2]

TASK [docker : Install docker-engine] ******************************************
changed: [app2]
changed: [app1]

TASK [docker : Reload systemd] ***************************************
changed: [app2]
changed: [app1]

TASK [docker : Start Docker] *************************************************
changed: [app1]
changed: [app2]

TASK [docker : Join User Group] ************************************************
changed: [app1]
changed: [app2]

TASK [deploy : Deploy Container] ***********************************************
changed: [app1]
changed: [app2]

PLAY RECAP *********************************************************************
app1               : ok=10   changed=7    unreachable=0    failed=0   
app2               : ok=10   changed=7    unreachable=0    failed=0   

初回なので当然すべての項目をこなす。

実際にvagrant sshなどで入ってみると、 目的のコンテナがデプロイされていることが確認できる。


再度実行してみる

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [app2]
ok: [app1]

TASK [docker : basic packages] *************************************************
ok: [app1] => (item=[u'python', u'docker-python'])
ok: [app2] => (item=[u'python', u'docker-python'])

TASK [docker : yum update] *****************************************************
ok: [app1]
ok: [app2]

TASK [docker : service check] **************************************************
ok: [app1]
ok: [app2]

TASK [docker : Get installer] **************************************************
skipping: [app1]
skipping: [app2]

TASK [docker : Install docker-engine] ******************************************
skipping: [app2]
skipping: [app1]

TASK [docker : Reload systemd] *************************************************
skipping: [app2]
skipping: [app1]

TASK [docker : Start Docker] ***************************************************
skipping: [app2]
skipping: [app1]

TASK [docker : Join User Group] ************************************************
ok: [app2]
ok: [app1]

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [app1]
ok: [app2]

TASK [deploy : Deploy Container] ***********************************************
changed: [app1]
changed: [app2]

PLAY RECAP *********************************************************************
app1               : ok=7    changed=1    unreachable=0    failed=0   
app2               : ok=7    changed=1    unreachable=0    failed=0  

不要な処理がskippingになっていることがみてとれる。


考えるべきこと

そんな感じでひとまず目的を達することはできたが、 実運用を見据えるともっと考えるべきことはある。

Docker Registryを用いたプロキシ

サーバの台数が多くなってくると、 全ての台数分のイメージをグローバルなネットワークから拾ってくるのは現実的ではない。

前にも導入したDocker RegistryをProxyモードで動作させることで一端ネットワーク内部にキャッシングし、 そこから配信するのが良いように思う。

blue1st.hateblo.jp

How to Set Up a Registry Proxy Cache with Docker Open Source Registry - Docker Blog

HandlerとNotifyを用いたサービス再起動の明確化

このへんは僕のAnsible力がまだ足りないところ。

tasksの作業がchangedだったか否かを通知して必要最低限のサービス再起動に抑えることができる、らしい。

あと、次のバージョンからはsystemdモジュールが加わるそうなので、サービス周りのコントロールはもう少し上手くできるようになるかも。

Dockerイメージの掃除

今回は不変のイメージの配布なので問題ないが、 実際に更新されるイメージを配布していくとなると、 不要になったイメージは適時削除しないとサーバを圧迫してしまう。

docker-imageモジュールあたりが使えそうだが、まだ調査できていない。

dns_serversオプションのバグ

docker_containerモジュールで使用するdocker-pyが確認した限りでは最新の1.9.0rc1までdns_serversオプションの指定がバグっている。

consulなんかを使って内部でDNSを立てるような運用を考えている場合には要注意だろう。

一応/etc/sysconfig/dockerにDOCKER_OPTとして記載するなどの回避策はあるが、ちょっとスッキリしないところ。