Ansible超入門ハンズオン

#はじめに

Ansible超初心者向けハンズオンです。Ansibleを使ってローカルVM構築を自動化してみましょう。

※この記事は[サポーターズCoLab](https://supporterzcolab.com/)様でのイベント用に作成したものですが、Ansibleを始めてみたいという方の参考にしていただけたら幸いです。

#事前準備

ハンズオン実施前にに以下1〜3の準備をお願いします。
なお、お手持ちのPCに以下が全て揃っていれば事前準備は特に必要ありません。

1. Virtualbox
2. Vagrant
3. python2.6または2.7の入ったCentOS6のbox

##1．Virtualboxのインストール

下記URLからVirtualboxの最新版をダウンロードして、インストーラを実行してください。
https://www.virtualbox.org/wiki/Download

インストール後、VirtualBoxを起動できればOKです。
Macの場合は、ターミナルを開き下記コマンドでバージョンが表示されることでも確認できます。

```
VBoxManage -v

# 5.2.8r121009
```

##2．Vagrantのインストール

下記URLからVagrantの最新版をダウンロードして、インストーラを実行してください。
https://www.vagrantup.com/

インストール後、Windowsならコマンドプロンプト、Macならターミナルを開き、下記コマンドでバージョンが表示されればOKです。

```
vagrant -v

# Vagrant 2.0.3
```

##3．Vagrant Boxの追加

Windowsならコマンドプロンプト、Macであればターミナルを開き、下記コマンドを実行してvagrant boxを追加してください。かなり時間かかります。

```
vagrant box add centos6-4 https://github.com/2creatives/vagrant-centos/releases/download/v6.4.2/centos64-x86_64-20140116.box
```

終了後、以下のコマンドでvagrant boxの追加を確認できればOKです。

```
vagrant box list

# centos6-4 (virtualbox, 0)
```

不安な方は実際にVMを作成してsshできるかまで確認しておいてください。

```
vagrant init centos6-4　# Vagrantfile作成

vagrant up              # VMの立ち上げ。多少時間がかかる

vagrant ssh             # ログインできればOK
```

#ハンズオンにあたっての注意

- ここから先は基本的にMacを想定して手順を記述しています。Windowsの方は適宜コマンドプロンプトもしくはGUI操作に読み替えてください。簡単に読み替えられない部分に関しては注釈を入れてあります。
- とにかく動かして使ってみるのが目的なので、細かい説明はしません。「Ansible 入門」で検索すれば偉大な先人たちが書いた素晴らしい記事がたくさん出てきますのでそちらを読みましょう。


#ハンズオン１〜Ansibleを使ってみよう〜

##目標

- Ansibleを動かして、VMにコマンドをインストールしてみる
- Ansibleの最小構成を知る

## 手順

### 作業用VM作成

Ansibleで構成を管理するVMを作成します。
適当に作業用ディレクトリを作り、その中で適当にVMを作ります。

```
mkdir ansible-hands-on

cd ansible-hands-on

vagrant init centos6-4

vagrant up

vagrant status

# Current machine states:
# 
# default                   running (virtualbox)
#
# The VM is running. To stop this VM, you can run `vagrant halt` to
# shut it down forcefully, or you can run `vagrant suspend` to simply
# suspend the virtual machine. In either case, to restart it again,
# simply run `vagrant up`.
```
上記のように、「running」と出力されていればVMが作成されています。

次にVMにログインします。Macの場合は以下のコマンド。

```
vagrant ssh
```

vagrantユーザでログインできているはずですので確かめます。

```
id

# uid=500(vagrant) gid=500(vagrant) groups=500(vagrant),10(wheel)

hostname

# vagrant-centos64.vagrantup.com
```

**※Windowsの場合はvagrant sshではログインできないため注意が必要です！**
teraterm等のsshクライアントを用いて、「vagrant@127.0.0.1」に接続してください。パスワードに「vagrant」を入れればログインできるはずです。

### Ansibleをインストールする

ここからはVMにログインした状態で作業を行います。
今回はあくまでハンズオンなので、アクセス権限等を考えなくてもいいようにrootユーザになっておきます。

```
su - root    # パスワードは"vagrant"
```

さて、まずは肝心のAnsibleをインストールしましょう。

```
yum localinstall -y http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm

yum -y install -y ansible
```

インストールが無事完了したかどうか、Ansibleのバージョンを表示させて確かめましょう。

```
ansible --version

# ansible 2.3.1.0
#   config file = /etc/ansible/ansible.cfg
#   configured module search path = Default w/o overrides
#   python version = 2.6.6 (r266:84292, Jul 10 2013, 22:48:45) [GCC 4.4.7 20120313 (Red Hat 4.4.7-3)]
```


※もし「Error: Cannot retrieve metalink for repository: epel.(略)」のようなエラーメッセージが表示される場合、以下のコマンドを実行してからもう一度インストールしてみてください。

```
sed -i -e 's/mirrorlist=https/mirrorlist=http/g' /etc/yum.repos.d/epel.repo
```

### Ansibleを動かしてみる

ansible --version の出力にもあるように、/etc/ansible/というディレクトリが自動的に作成されています。ここに移動して設定などを行なっていきます。

```
cd /etc/ansible/

ls
# ansible.cfg  hosts  roles
```

Ansibleでプロビジョニングを行うために必須なのは、**inventory(hosts)**と**playbook**です。一つずつ見ていきましょう。

#### inventory
inventoryってなんやねん？と思うかもしれませんが、要はhostsファイルのことです。複数サーバのプロビジョニングを行うときには重要ですが、今回はとりあえず中身を全消しして、下のように書いておけばOKです。

```hosts
[localhost]
127.0.0.1
```

#### playbook
playbookってなんや？おう？と思うかもしれませんが、要はyamlファイルのことです。名前は何でも大丈夫です。処理内容をymlで記述します。

練習用として、「nkfコマンドをyumでインストールする」という記述をしたいと思います。main.ymlという名前のファイルを作成し、以下のように書いてください。

```main.yml
- name: Ansible sample 1
  hosts: localhost
  connection: local
  tasks:
    - name: install nkf
      yum: name=nkf
```

これで準備は完了です。Ansibleでプロビジョニングを実行してみましょう。

まず、nkfコマンドがインストールされていないことを確認します。

```
nkf -g hosts

# -bash: /usr/bin/nkf: No such file or directory
```

コマンドがないのでエラーになりますね。
次にPlaybookを実行します。

```
ansible-playbook main.yml
```

以下のような出力がされると思います。

```
PLAY [Ansible sample 1] **********************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [127.0.0.1]

TASK [install nkf] ***************************************************************************************************
changed: [127.0.0.1]

PLAY RECAP ***********************************************************************************************************
127.0.0.1                  : ok=2    changed=1    unreachable=0    failed=0
```

TASK [install nkf]のところがchangedになっていれば成功です。nkfコマンドがインストールされたかどうかを確認します。

```
nkf -g hosts

# ASCII (LF)
```

Playbookにより、コマンドのインストールができました。やったぜ。ちなみにnkfは文字コードの推測や変換を行える超便利コマンドです。知らない人は今覚えましょう。

## ちょっとだけ解説

### main.ymlについて

main.ymlの中身を上から順番に解説します。

```main.yml(再掲)
- name: Ansible sample 1
  hosts: localhost
  connection: local
  tasks:
    - name: install nkf
      yum: name=nkf
```

- 最初のnameはこのplaybookの名前です。別になくても構いません
- hostsはプロビジョニングの対象サーバを表しています。hostsファイルに[localhost]というグループを作成したので今回はそれを指定しています。直接127.0.0.1と書いても動きます
- connection: localはローカルのプロビジョニングをする際に必要な記述です。これがないとssh接続します
- tasks配下に実際のタスクを書いていきます。タスクには様々なモジュールが利用できます
- ここのnameはタスクごとの名前です。今回はnkfをインストールするタスクなのでこの名前にしました
- yumモジュールを使ってnkfをインストールする、という記述です

### 冪等性について

もう一度同じplaybookを実行してみてください。

```
ansible-playbook main.yml
```

今度は以下のような出力がされるのではないでしょうか。

```
PLAY [Ansible sample 1] **********************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [127.0.0.1]

TASK [install nkf] ***************************************************************************************************
ok: [127.0.0.1]

PLAY RECAP ***********************************************************************************************************
127.0.0.1                  : ok=2    changed=0    unreachable=0    failed=0
```

先ほどとは違い、TASK [install nkf]のところがchangedではなくokになっています。サーバの状態が変更されていないためです。つまり、nkfはすでにインストール済みなので、yumモジュールはnkfをインストールしません。

この後何度同じplaybookを実行しても、サーバの状態は変更されません。このように、ある操作を1回実行しても複数回実行しても結果が同じであるという性質を**冪等性（べきとうせい）**といいます。

Ansibleでは、特定のモジュール以外は冪等性を持っています。Playbookの冪等性を保たせることが、上手にAnsibleを使いこなすために大切なことの一つです。

#ハンズオン２〜色々なモジュールを使ってみよう〜

## 目標

ハンズオン1ではyumでnkfコマンドをインストールしました。これにはyumモジュールを利用しています。Ansibleにはモジュールが色々ありますので、よく使いそうなものを試してみましょう。

なお、モジュールの一覧は[Module Index](http://docs.ansible.com/ansible/modules_by_category.html)にありますので困ったらこちらを参照しましょう。

## 手順

### ファイル/ディレクトリを作成する
ファイルやディレクトリを作成するには**file**モジュールを利用します。

まずはディレクトリを作成します。main.ymlの末尾に以下を追記してください。

```
    - name: directory creation sample
      file: path=/etc/ansible/sampledir
            state=directory
            owner=root
            group=root
            mode=0644
```

見たままですが、pathにはパスを、stateにはfile、ownerやgroupは設定したいユーザ/グループ、modeにはパーミッションを指定してください。
注意点としては、modeには**必ず4桁**の数字を書くようにしてください。（3桁で644とか書くと予期せぬ動作をすることがあるらしいので）

ansible-playbookを実行すると、samplidirというディレクトリが作成されているのが分かると思います。

```
ls -l

# -rw-r--r-- 1 root root 18066 Jun  1 21:49 ansible.cfg
# -rw-r--r-- 1 root root    56 Jun 27 23:47 hosts
# -rw-r--r-- 1 root root  1037 Jun 28 00:03 main.yml
# drwxr-xr-x 2 root root  4096 Jun  1 21:49 roles
# drw-r--r-- 2 root root  4096 Jun 27 23:38 sampledir
```

ファイルの作成も一応できますが、冪等性が失われてしまうため基本的には後述のcopyモジュールやtemplateモジュールを使うべきです。一応やり方だけ確認しておきます。

```main.yml
    - name: file creation sample
      file: path=/etc/ansible/samplefile
            state=touch
            owner=root
            group=root
            mode=0644
```
先ほどとほぼ同じで、stateをtouchに変えればファイルが作成されます。ただしファイルが存在していてもtouchしてしまうので毎回サーバの状態が変更されてしまいます。使うのはやめましょう。


### ファイル/ディレクトリをコピーする
ファイル/ディレクトリのコピーには**copy**モジュールを使います。たとえば、ホストOS上にあるファイルをゲストOSとの共有ディレクトリに置いて、それをAnsibleで任意の場所にコピーしましょう。

まず、ホストOSのゲストOSの共有ディレクトリ（デフォルトではVagrantfileのあるディレクトリ）に、コピー用ファイルcopy.txtとコピー用ディレクトリcopydirを作成してください。copydirの中には適当にファイルを作成しておいてください。

自分の場合（Mac）は以下のようにしました。

```
touch copy.txt
mkdir copydir
mkdir copydir/dir
touch copydir/dir/dir_copy_sample.txt
```

続いて、下記をmain.ymlに追加してください。

```main.yml
    - name: file copy sample
      copy: src=/vagrant/copy.txt
            dest=/etc/ansible/copy.txt
            owner=root
            group=root
            mode=0644

    - name: directory copy sample
      copy: src=/vagrant/copydir/
            dest=/etc/ansible/copydir/
            owner=root
            group=root
            mode=2775
```

ansible-playbookを実行して、ファイルとディレクトリがコピーされていることを確認してください。


### ファイルをコピーして変数を展開する
ファイルをコピーするだけでなく、中身の一部をサーバによって変えたいときがあると思います。たとえばWebサーバとDBサーバで何かしらの接続先を変えるなどです。ローカルVMの場合はあまりないかもしれませんが、変数を別に切り出しておけば変更が容易になるというメリットもありますので紹介しておきます。

変数を展開するためには、**jinja2**というテンプレートを使用します。拡張子は" **.j2** "です。先ほどと同様に、ホストOSの共有ディレクトリにtemplate.j2というファイルを作成し、以下のような内容を記述してください。

```template.j2
this is jinja template.

{{ sample_var }}
```

次に、変数sample_varを定義します。変数を定義する方法はいくつかありますが、inventoryに記載するのが簡単です。

```hosts
[localhost]
127.0.0.1

[localhost:vars]
sample_var=hoge
```

このようにすれば、sample_varにhogeという文字列がセットされます。

```main.yml
    - name: template copy sample
      template: src=/vagrant/template.j2
                dest=/etc/ansible/template.txt
                owner=root
                group=root
                mode=0644
```

ansible-playbookを実行して、コピーされたtemplate.txtの中身を確認してください。

```
cat template.txt

# this is jinja template.
#
# hoge

```
変数sample_varが展開されて、hogeと出力されました。便利っぽい。

もっと高度なことがやりたい場合は公式を参照して下さい。
[http://jinja.pocoo.org/docs/dev/templates/](http://jinja.pocoo.org/docs/dev/templates/)

### コマンドを実行する
**command**モジュールを使えばUnixコマンドを実行することができます。ただし、このモジュールは常にchangedを返すため、**冪等性が失われます。** なるべく使わないようにしましょう。

どうしても使いたい場合は、whenなどを使用して条件をつけ、冪等性が保たれるように工夫した方が良いです。ただし今回はとりあえず試してみましょう。

```main.yml
    - name: command sample
      command: touch command.log
```

実行してみてください。以下のような出力になるはずです。

```
PLAY [ansible hands-on] ********************************************************

TASK [Gathering Facts] *********************************************************
ok: [127.0.0.1]

TASK [install nkf] *************************************************************
ok: [127.0.0.1]

TASK [file creation sample] ****************************************************
ok: [127.0.0.1]

TASK [directory creation sample] ***********************************************
ok: [127.0.0.1]

TASK [file copy sample] ********************************************************
ok: [127.0.0.1]

TASK [directory copy sample] ***************************************************
ok: [127.0.0.1]

TASK [template copy sample] ****************************************************
ok: [127.0.0.1]

TASK [command sample] **********************************************************
 [WARNING]: Consider using file module with state=touch rather than running
touch

changed: [127.0.0.1]

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=8    changed=1    unreachable=0    failed=0

```

WARNINGが出ましたね。「commandモジュールでtouchするのではなくて、fileモジュールでstate=touchにしなさい」とのことです。このようにcommandモジュールは基本的に推奨されないため、まずは他のモジュールでやりたいことを実現できないか探すようにしましょう。

ついでに、もう一度ansible-playbookを実行してみてください。command sampleのところがまたchangedになっていると思います。何回やってもchangedになってしまいます。冪等性が失われている状態です。

まあハンズオンなので一旦気にせず進みます。


### コマンドをシェル経由で実行する
commandモジュールと似たようなものに**shell**モジュールがあります。こちらは、shell経由でコマンドを実行できます。commandモジュールとの大きな違いは、パイプやリダイレクトが使用できることです。main.ymlに以下を追記してみてください。

```main.yml
    - name: shell sample
      shell: echo 'this is shell sample.' > shell.log
```

リダイレクトを利用して、echoで標準出力に出力された結果をshell.logに書き込んでいます。commandモジュールではこういうことはできません。基本的にはshellモジュールの方が便利なので、commandよりこちらを利用した方がいいと思います。ただし、実行結果がリモートサーバのシェル環境に影響されますので注意しましょう。

もちろんshellモジュールにも冪等性はありません。工夫して使いましょう。


### エラーを無視して続行する
ここからはモジュールのオプションを少しだけ紹介します。

あるタスクででエラーが発生した場合、そこでansibleの実行自体が止まってしまいます。main.ymlのcommand sampleとshell sampleの間にエラーになるタスクを追加してみましょう。

```main.yml
    - name: command sample
      command: touch command.log

    - name: command error sample
      command: sl

    - name: shell sample
      shell: echo 'this is shell sample.' > shell.log

```

slという未インストールのコマンドを実行するタスクを追加しました。この状態で実行すると、shell sampleが実行されず、command error sampleのエラーで終了します。

このエラーで終了せずにそのまま次のタスクに進みたい場合は、ignore_errorsオプションをtrueにします。

```main.yml
    - name: command error sample
      command: sl
      ignore_errors: true
```

再度実行して、shell sampleが再度実行されていることを確認してください。


### リストを使って同じ処理を複数回実行する
**with_items**オプションを使えば、同じタスクの一部だけを変更しつつ複数回実行することができます。foreach的な感じだと思ってもらえば大丈夫です。使い方は以下の通りです。

```main.yml
    - name: shell with_items sample 
      shell: echo '{{ item }}' >> shell.log
      with_items:
        - item1
        - item2
        - item3
```

shell.log中身を確認してください。

もちろん複数の変数を使用することもできます。


### 変更があったら他のタスクを実行する（ハンドラ）

今までtasks配下にタスクを記述してきましたが、タスクに**notify**オプションをつけることで**ハンドラ**を呼び出すことができます。notifyオプションをつけたタスクがchangedになったときハンドラが実行されます。たとえば、「apacheの設定ファイルをcopyして変更した場合はapacheを再起動する」というようなことができます。

ハンドラは**handlers**配下に記述します。

```main.yml
 handlers:
    - name: handler sample
      command: touch handler.log
```

次にnotifyの設定をしましょう。copy sampleタスクにnotifyオプションを追加します。

```
    - name: file copy sample
      copy: src=/vagrant/copy.txt
            dest=/etc/ansible/copy.txt
            owner=root
            group=root
            mode=0644
      notify: handler sample
```

notifyの値には実行したいハンドラのnameを記述します。名前って大事ですね。

この状態でansible-playbookを実行してみてください。copy sampleタスクはokなので、ハンドラは実行されません。

では、ホストOSからcopy.txtの中身を適当に編集して、再度ansible-playbookしてみましょう。

```
PLAY [ansible hands-on] ********************************************************

TASK [Gathering Facts] *********************************************************
ok: [127.0.0.1]

TASK [install nkf] *************************************************************
ok: [127.0.0.1]

TASK [directory creation sample] ***********************************************
ok: [127.0.0.1]

TASK [file creation sample] ****************************************************
changed: [127.0.0.1]

TASK [file copy sample] ********************************************************
changed: [127.0.0.1]

TASK [directory copy sample] ***************************************************
ok: [127.0.0.1]

TASK [template copy sample] ****************************************************
ok: [127.0.0.1]

TASK [command sample] **********************************************************
 [WARNING]: Consider using file module with state=touch rather than running
touch

changed: [127.0.0.1]

TASK [command error sample] ****************************************************
fatal: [127.0.0.1]: FAILED! => {"changed": false, "cmd": "sl", "failed": true, "msg": "[Errno 2] No such file or directory", "rc": 2}
...ignoring

TASK [shell sample] ************************************************************
changed: [127.0.0.1]

TASK [shell with items sample] *************************************************
changed: [127.0.0.1] => (item=item1)
changed: [127.0.0.1] => (item=item2)
changed: [127.0.0.1] => (item=item3)

TASK [shell with multiple item sample] *****************************************
changed: [127.0.0.1] => (item={u'owner': u'root', u'path': u'item1', u'group': u'root', u'mode': 420})
changed: [127.0.0.1] => (item={u'owner': u'vagrant', u'path': u'item2', u'group': u'vagrant', u'mode': 511})

RUNNING HANDLER [handler sample] ***********************************************
changed: [127.0.0.1]

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=13   changed=7    unreachable=0    failed=0
```

copy sampleがchangedなので、ハンドラが実行されます。最後にRUNNING HANDLERと出力されますね。handler.logが生成されたかどうか確認してみましょう。

```
ls -l handler.log

# -rw-r--r-- 1 root root 0 Jun 28 08:42 handler.log
```
良い感じですね。このように実行される条件が決まっているのであれば、commandモジュールなどを使っても冪等性は保たれます。


## ちょっとだけ解説
この他にもモジュールはたくさんありますが、既存のモジュールではどうしても対応できない場合があると思います。そういう場合はshellモジュールでゴリゴリのシェルスクリプトを書くのも良いですが（良くない）、**モジュールを自作する**という手もあります。Ansibleのモジュールはpythonである必要はなく、好きな言語で書くことができます（それこそシェルでも良い）。興味のある人は試してみましょう。


#ハンズオン３〜実用的な設定をしよう〜

## 目標
もう少し実用的な形にしてみたいと思います。具体的には以下を目指します。

- httpdとphpenvをインストール、あとはgitとかbashとかその辺も設定する
- ホストOSでvagrant upしたあとvagrant provisionすれば全て設定される状態にする
- 変更があったらplaybookやテンプレートファイルを更新して、vagrant provisionすればVMが最新の状態になる
- rolesを使用してメンテナンス性の高い構成にする

## 手順

### rolesとは
先ほどまでは１つのPlaybookに全ての構成を詰め込んでいました。
Ansibleでは、規約に沿ってディレクトリを構成することで、Playbookや設定ファイルなどをロールごとに分割することができ、これをrolesと呼びます。非常に便利です。
[http://docs.ansible.com/ansible/playbooks_roles.html](http://docs.ansible.com/ansible/playbooks_roles.html)

### やってみよう

https://github.com/natsukinoue/ansible-vagrant-centos73-docker
こちらにVagrantfileとPlaybook、シェルスクリプト等を含むサンプルリポジトリを用意しました。
使い方は非常に簡単で、cloneしてきたらディレクトリ内でvagrant upするだけです。

CentOS 7.3のboxに以下の様なRolesを設定しています。

- common
- docker
- php

まずcommon roleでとりあえず入れておきたいコマンドをインストールしたり、myuserという名前のsudoユーザを作成したり、ディレクトリを作成したりしています。
dockerのroleでは文字通りdockerをインストールし、myuser権限でdockerを実行できる様に設定。あとdocker-composeも入れてます。
phpのroleはまあ、おまけです。例えばrubyを入れたい人は同じような感じでrbenvあたりを入れればいいと思います。nodeとかでもなんでも。

動作確認はphpでもいいですが、せっかくなのでdockerイメージをビルドしてコンテナを動かしてみます。

```
cd /vagrant/docker/
docker build .
```
これで空のCentOS 7.3イメージが構築されると思いますので、docker runしてみてください。何もプロセスがないのでコンテナが落ちます。docker ps -aとかすると残骸が見れるはずです。

というわけで、git cloneとvagrant upだけでdockerが使える状態のCentOS 7.3が手に入りました。楽ちんですね。

なおこのリポジトリは結構急ぎで作ったので、ここが間違ってるとかこういう書き方した方がいいよとかあったらPRもらえると喜びます。

# 終わりに

ここまで読んでいただいた方、お疲れさまでした。

これで、超初心者から初心者くらいにはなれたのではないでしょうか。（僕のことです）

一応簡単な設定だけならできるようになりました。しかし、この先まだまだやることがあります。たとえば

- SELinuxとか色々なサービスの設定
- ssh経由でリモートサーバをプロビジョニングする。それも複数台
- jinja2テンプレートやrolesを駆使してもっと複雑な設定をする
- ちゃんと公式を読む
- etc..

などなどです。幸いなことに、日本語で書かれた入門向け資料がたくさんありますので、「ansible 入門」「ansible tutorial」などでググりまくりましょう。

それでは、良いサーバ構成管理ライフを。