Help us understand the problem. What is going on with this article?

Vagrant + Ansibleで環境構築をコード化する(2)Playbookを使う

More than 3 years have passed since last update.

「Vagrant + Ansibleで環境構築をコード化する」 の第2弾です。

本シリーズで作成するファイルは GitHub に置いてあります。
※パートごとにブランチがあります

シリーズ記事一覧

今回のゴール

Playbookを書いてリモートVMに基本的なツールの導入や各種設定を行う。
それを通じてPlaybookに慣れる。

実践

Playbook化

前回までの作業で、コントロールVMからリモートVMをAnsibleでいじれる状態になっています。前回は ansible ホスト -m モジュール名 で直接実行しましたが、今回からは Playbook という構成定義ファイルを作っていきます。

まずはPlaybook本体と関連ファイルを置くディレクトリを作ります。
インベントリファイルもここに移動しておきます。

ホストOS
foo@localhost:~$ cd ~/ansible-sandbox
foo@localhost:~/ansible-sandbox$ mkdir playbook
foo@localhost:~/ansible-sandbox$ mv hosts_sandbox playbook/

Playbookを作ります。

ホストOS
foo@localhost:~/ansible-sandbox$ touch playbook/site.yml

site.yml という名前は慣習です。
最初に見るべきファイルが伝わりやすいので、これに倣っておきます。

Playbookは YAML で記述します。
前回の導通確認で行った ping をPlaybookになおしてみましょう。

site.yml
---
- hosts: remote-vms
  tasks:
    - name: ping remote-VM
      action: ping

ここまでの構成はこんな感じ。

ansible-sandbox
|-- playbook
|   |-- hosts_sandbox
|   `-- site.yml
|-- sendkey.expect
`-- Vagrantfile

実行してみます。

コントロールVM
vagrant@ansible-control:~$ ansible-playbook -i /vagrant/playbook/hosts_sandbox /vagrant/playbook/site.yml

PLAY [remote-vms] *************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.50.102]

TASK: [ping to remote-VM] *****************************************************
ok: [192.168.50.102]

PLAY RECAP ********************************************************************
192.168.50.102             : ok=2    changed=0    unreachable=0    failed=0

前回と異なるのは、

  • 使うコマンドが ansible ではなく ansible-playbook
  • ホストの指定がない(Playbookに書かれているから)

…の2点です。これ以降はPlaybookの編集に集中していくことになります。

Playbook ベストプラクティス

Playbookを書くときのファイル構成は自由ですが、まずは 公式のベストプラクティス に従うのが無難です。

ここまでのファイルをベストプラクティス方式に置き換えてみます。

ベストプラクティス方式
ansible-sandbox
|-- playbook
|   |-- hosts_sandbox
|   |-- roles
|   |   `-- ping
|   |       `-- tasks
|   |           `-- main.yml
|   `-- site.yml
|-- sendkey.expect
`-- Vagrantfile

新たに roles 以下のディレクトリを作り、 pingロール として分離しました。
(pingごときでロール作る必要なんてないというのは置いといて)

ロールの起点は tasks/main.yml と定められています。

playbook/roles/ping/tasks/main.yml
---
- name: ping to remote-VM
  action: ping

中身は前回まで site.yml に書いていたタスクほぼそのままです。
site.yml の方はどうなったかというと

playbook/site.yml
---
- hosts: remote-vms
  roles:
    - ping

このplaybookで呼び出すロールを指定するだけ に変更しています。
とりあえず実行してみます。

コントロールVM
vagrant@ansible-control:~$ ansible-playbook -i /vagrant/playbook/hosts_sandbox /vagrant/playbook/site.yml

PLAY [remote-vms] ************************************************************* 

GATHERING FACTS *************************************************************** 
ok: [192.168.50.102]

TASK: [ping | ping to remote-VM] ********************************************** 
ok: [192.168.50.102]

PLAY RECAP ******************************************************************** 
192.168.50.102             : ok=2    changed=0    unreachable=0    failed=0

よく見ると TASK: の結果表示が [ロール名: タスクのname] に変わっていますが、動作自体は同じです。

このようにAnsibleでは、構成処理をロールという単位で分離することができます。
例えば「Nginxを入れるロール」「Gitを入れるロール」と言った具合です。

個々の処理はロールとして作り、playbookから「このマシンにはNginxとMariaDBが必要だから、該当するロールを呼び出して…」と言った具合でロールを拾い集めていくことで、最終的にマシンを作り上げます。

なお、お察しの通りロールはオンラインで共有することができ、 Ansible Galaxy というホスティングサービスに蓄積されています。そのまま取り込んで使うこともできますし、あるいは学習のために覗いてみるのも良いかと思います。

sudo

ロールを増やしていく前に、Playbookを実行するとき sudo されるようにしておきます。

site.yml
---
- hosts: remote-vms
  sudo: yes
  roles:
    - ping

おおもとの site.yml に sudo: yes を追記しただけです。分かりやすいですね。

通常のサーバーならば、リモートVM側で vagrant ユーザーがパスワード無しで sudo できるように設定が必要ですが、Vagrantの場合はもともと vagrant ユーザーがその状態になっているのでこれだけで済みます。

ロールの作成

それでは、リモートVMを構成するロールを作っていきます。
pingロールは要らないので消しちゃいます。

ホストOS
foo@localhost:~/ansible-sandbox$ rm -rf playbook/roles/ping

今回は次のロールを作ります。

  • apt まわりやロケール設定、言語パックのインストールなどを行う common ロール
  • Git をインストールするロール
  • OracleJDK をインストールするロール

とりあえずディレクトリと空ファイルを一気に。

ホストOS
$ mkdir -p \
    playbook/roles/common/tasks \
    playbook/roles/git/tasks \
    playbook/roles/java/tasks

$ touch \
    playbook/roles/common/tasks/main.yml \
    playbook/roles/git/tasks/main.yml \
    playbook/roles/java/tasks/main.yml

こんな状態

playbook/roles/
|-- common
|   `-- tasks
|       `-- main.yml
|-- git
|   `-- tasks
|       `-- main.yml
`-- java
    `-- tasks
        `-- main.yml

common ロール

いきなり長いですが、下で解説します。

各タスクにつけている name は(YAML文法の許す範囲で)自由に付けられます。
コンソールログにも出るので、何をしているか分かる簡潔な名前をつけましょう。

main.yml
---
- name: disable ipv6
  lineinfile: >
    dest="/etc/sysctl.conf"
    line="{{ item }}"
  with_items:
    - "net.ipv6.conf.all.disable_ipv6 = 1"
    - "net.ipv6.conf.default.disable_ipv6 = 1"
    - "net.ipv6.conf.eth0.disable_ipv6 = 1"
    - "net.ipv6.conf.lo.disable_ipv6 = 1"
  register: sysctl_ipv6

- name: sysctl load
  command: sysctl --load
  when: sysctl_ipv6|changed

- name: change mirror
  replace: >
    dest="/etc/apt/sources.list"
    regexp="http://(archive|security)\.ubuntu\.com/ubuntu(.+)"
    replace="http://ftp.jaist.ac.jp/pub/Linux/ubuntu\2"
  register: apt_mirror

- name: apt update
  apt: update_cache=yes
  when: apt_mirror|changed

- name: aptitude upgrade
  apt: upgrade=full state=latest
  when: apt_mirror|changed

- name: install language-pack
  apt: name="{{ item }}"
  environment:
    DEBIAN_FRONTEND: "noninteractive"
  with_items:
    - "language-pack-ja"
    - "language-pack-gnome-ja"
    - "thunderbird-locale-ja"
    - "fonts-takao"
    - "ibus-anthy"
    - "ibus-mozc"
  register: install_language_pack

- name: update locale
  command: update-locale LANG="ja_JP.UTF-8"
  when: install_language_pack|changed

- name: update localtime
  file: >
    dest="/etc/localtime"
    src="/usr/share/zoneinfo/Asia/Tokyo"
    state=link
    force=yes

ちょくちょく出ている > はYAMLの文法の一種で、複数行文字列の書き方です。
この場合は改行が半角スペースとして評価されます。

hoge: >
  foo="foofoo"
  bar="bazbaz"

…とあったら

hoge:   foo="foofoo"   bar="bazbaz"

…と同じような意味で評価されます。詳細は YAMLの仕様書 を参照。

話が逸れましたが、以下各タスク(name 単位)ごとに解説します。

disable ipv6

lineinfile モジュール/etc/sysctl.conf に複数行の文字列を追記しています。特定の1行の置換や、末尾への追記にはこのモジュールを使います。

with_items: はループ処理です。大体想像できるかもしれませんが、 {{ item }}with_items: から値が与えられて繰り返し処理されます。詳細はドキュメントの Loops を参照。

register: でこのタスクの実行結果を sysctl_ipv6 という変数に登録しています。あとで使います。

sysctl load

command モジュールsysctl --load を実行しています。このモジュールはパイプやリダイレクトを必要としない場合のOSコマンド実行に使います。

when: はこのタスクを実行する条件で、値が True なら実行され、 False ならスキップされます。
先のタスクで登録した変数を参照し changed フィルターでステータスを取り出して 「前のタスクがマシンを変更していた時(Changed)のみ」 という条件を実現しています。

先の lineinfile は、 dest で指定したファイルに line で与えた行がすでに存在する場合は Changed にならないので False となり、同じPlaybookを複数回流しても2回目以降はこのタスクがスキップされます。

条件の詳細はドキュメントの Conditionals 、フィルターについては Jinja2 filters を参照。

change mirror

replace モジュール/etc/apt/sources.listregexp にマッチするすべての行を置換しています。

lineinfile モジュールでも1行なら置換できますが、こちらはマッチするすべての行を置換できます。ちょうど sed コマンドで置換するときの g フラグ有無のような感じです。

apt update

apt モジュールapt update を行っています。

通常はパッケージをインストールするようなタスクに update_cache=yes を付けて、インストール前についでに update させるものですが、ここでは分かりやすいように分離しました。

aptitude upgrade

同じく apt モジュールで、こちらは aptitude upgrade を行っています。

install language-pack

apt モジュール で日本語パッケージをインストールしています。
environment: でモジュール実行時の環境変数を与えることで、

sudo DEBIAN_FRONTEND=noninteractive apt-get install -y PACKAGE PACKAGE...

と同じことをやっています。ここで入れているパッケージに関して言えば noninteractive は不要ですが、しばしば必要になるので紹介までに。

update locale

command モジュールupdate-locale を実行しています。

update localtime

file モジュール でタイムゾーン情報のシンボリックリンクを貼っています。
シェルで言うと ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime と同じことをしています。

git ロール

common ロールを読み解いた後ならだいたい読めるのではないでしょうか。
apt_repository モジュール で PPA を登録し、Gitパッケージをインストールしています。

main.yml
---
- name: add repository git-core
  apt_repository: repo="ppa:git-core/ppa"

- name: install git
  apt: >
    name="git"
    update_cache=yes

java ロール

これもほぼ見たまんまのことをやっています。

main.yml
---
- name: debconf auto agree to oracle-license
  debconf: >
    name="oracle-java8-installer"
    question="shared/accepted-oracle-license-v1-1"
    value="true"
    vtype=select

- name: apt add repository webupd8team
  apt_repository: repo="ppa:webupd8team/java"

- name: install oracle-java8-installer
  apt: >
    name="oracle-java8-installer"
    update_cache=yes

debconf モジュール で Oracle ライセンスに自動で同意するよう設定し、 Oracle JDK のパッケージをインストールしています。

debconf に関しては WebUpd8の解説 にある

echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | sudo /usr/bin/debconf-set-selections

…と同じです。

実行

ロールができたら site.yml から呼び出すように修正します。
実行時は roles: に与えたリストの順序に従って呼ばれます。

site.yml
---
- hosts: remote-vms
  sudo: yes
  roles:
    - common
    - git
    - java

実行してみます。
aptitude upgrade と Oracle JDK があるので、初回はそれなりに時間がかかります。

コントロールVM
vagrant@ansible-control:~$ ansible-playbook -i /vagrant/playbook/hosts_sandbox /vagrant/playbook/site.yml

PLAY [remote-vms] *************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.50.102]

(中略)

TASK: [java | install oracle-java8-installer] *********************************
changed: [192.168.50.102]

PLAY RECAP ********************************************************************
192.168.50.102             : ok=14   changed=12   unreachable=0    failed=0

failed=0 (失敗なし)で完了しました!

リモートVMに入って確認してみましょう。

ホストOS
foo@localhost:~/ansible-sandbox$ vagrant ssh remote
リモートVM
# aptミラーはJAISTになっているか
vagrant@ansible-remote:~$ cat /etc/apt/sources.list | grep '^deb'
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty main
deb-src http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty main
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-updates main
deb-src http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-updates main
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty universe
deb-src http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty universe
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-updates universe
deb-src http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-updates universe
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-security main
deb-src http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-security main
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-security universe
deb-src http://ftp.jaist.ac.jp/pub/Linux/ubuntu trusty-security universe

# ipv6は無効か
vagrant@ansible-remote:~$ ifconfig | grep inet
          inetアドレス:10.0.2.15  ブロードキャスト:10.0.2.255  マスク:255.255.255.0
          inetアドレス:192.168.50.102  ブロードキャスト:192.168.50.255  マスク:255.255.255.0
          inetアドレス:127.0.0.1  マスク:255.0.0.0

# タイムゾーンと言語は日本か
vagrant@ansible-remote:~$ date
2015年  8月  3日 月曜日 18:50:04 JST

# git
vagrant@ansible-remote:~$ git --version
git version 2.5.0

# java
vagrant@ansible-remote:~$ java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

ちゃんと構成されています!!

:information_source: 理想を言えば、構成されているかの確認はServerspecなどのテストフレームワークを使うのが良いと思います。


ところで、いくつか「変更があった時のみ」という条件をつけたタスクがありました。
もう1回実行してみます。

コントロールVM
vagrant@ansible-control:~$ ansible-playbook -i /vagrant/playbook/hosts_sandbox /vagrant/playbook/site.yml 

PLAY [remote-vms] ************************************************************* 

GATHERING FACTS *************************************************************** 
ok: [192.168.50.102]

TASK: [common | disable ipv6] ************************************************* 
ok: [192.168.50.102] => (item=net.ipv6.conf.all.disable_ipv6 = 1)
ok: [192.168.50.102] => (item=net.ipv6.conf.default.disable_ipv6 = 1)
ok: [192.168.50.102] => (item=net.ipv6.conf.eth0.disable_ipv6 = 1)
ok: [192.168.50.102] => (item=net.ipv6.conf.lo.disable_ipv6 = 1)

(中略)

TASK: [java | install oracle-java8-installer] ********************************* 
ok: [192.168.50.102]

PLAY RECAP ******************************************************************** 
192.168.50.102             : ok=10   changed=0    unreachable=0    failed=0

1回目とくらべて早く終わり、相当数のタスクがスキップされていることがわかります。
また、なんら変更がもたらされなかったため changed=0 となっています。


以上で、Playbookを使ってリモートVMを構成することが出来ました。
今回の最終的なファイル群は GitHub に置いてあります。

次回はデスクトップ環境とNPTを入れつつ、リモートVMを再起動するタスクや、テンプレート等を紹介していきたいと思います。


シリーズ記事一覧

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away