構成管理ツールのAnsibleについて丁寧に解説してみた

## Ansibleとは？

対象のサーバーへ、アプリケーションを動かすために必要なモジュールやミドルウェアなどをインストールしたり、ネットワーク構築などを自動化してくれる構成管理ツールの一つ。

似たような構成管理ツールとしては、

- Chef(シェフ)
- Puppet(パペット)
- Itamae(イタマエ)

などが挙げられます。

構成管理ツールを使うことで、

- 今まで手作業やっていた部分が自動化される
- ヒューマンエラーが減る
- 似たような環境を構築する際にもスピーディーに対応可能

といった恩恵を受けることができるので、モダンな技術への感度が高い企業ではよく採用されていますね。

## Ansibleを使うメリット

先に挙げた構成管理ツールに比べると、

- YAML形式で処理を書いていくため、ノンプログラマーでも入門しやすい
- ディレクトリ構成にベストプラクティスがあるため、そのレールに乗ることでプロジェクトが変わっても内容を把握しやすい

### Ansibleのデメリット

- YAML形式であるため、条件分岐や繰り返し処理ができず、似たような構成を作る際にはコピペになりがち

## もっとAnsibleの概念について知りたい方は

下記のスライドがよくまとまっているので、読んでみるといいです。

https://www.slideshare.net/dcubeio/ansible-72056386 

## ansibleのインストール

ansibleはHomebrewを使ってインストールすることができます。

```
$ brew install ansible
```

インストールができたら、ansibleコマンドが使えるか確認しましょう。

ターミナル上で`ansible --version`を実行した際、次のような結果が表示される時は、ansibleがインストールされている証拠です。

```console
$ ansible --version
ansible 2.8.2
  config file = /Users/yuta_ushizima/yuta-ushijima-git/workspace/Ansible/qiita_clone_2019_ansible/ansible/ansible.cfg
  configured module search path = [u'/Users/yuta_ushizima/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.15 (default, Jan 12 2019, 21:07:57) [GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)]
```

## ansibleを動かすための基本的なコマンド

ここでは、ansibleを動かすときによく利用する代表的なコマンドを紹介します。

###  ansibleコマンド

指定したホストに対して、オプションで指定した処理を実行する時に使います。

【ansible-Working with Command Line Tools-】
https://docs.ansible.com/ansible/latest/cli/ansible.html

公式ではいくつかオプションが紹介されていますが、モジュールを指定して使うことが多いです。

```console
$ ansible -i hosts <IPアドレス> -m <モジュール名>
```

### ansible-playbookコマンド
作成したplaybookを実行する際のコマンドです。

【ansible-playbook-Working with Command Line Tools-】
https://docs.ansible.com/ansible/latest/cli/ansible-playbook.html

playbook名は、公式サイトだとplaybook.ymlとなっていますが、拡張子がYAMLであれば好きな名前をつけられます。

staging.ymlやproduction.ymlといったように、環境ごとの名前で作成することが業務だと多いですね。

```console
$ ansible-playbook -i <インベントリファイル> <playbook名>.yml
```

### そのほかのコマンドについて

そのほかのコマンドについては、下記の公式ドキュメントにまとめられていますので、気になる方は参照してみてください。

【Working with Command Line Tools】
https://docs.ansible.com/ansible/latest/user_guide/command_line_tools.html

## サンプルリポジトリ

Take off Railsの課題であるQiitaのクローンアプリを作成した際に、Ansibleを使ってEC2にモジュールのインストールなどを自動化しました。

https://github.com/yuta-ushijima/qiita_clone_2019_ansible

### サンプルリポジトリのディレクトリ構成

```
ansinle/
  - roles/
    - common/
      - tasks/
        - main.yml
    - git/
        - tasks/
          - main.yml
    - mysql/
      - tasks/
        - main.yml
      - templates/
        - my.conf.j2
    - nginx/
      - tasks/
        - main.yml
      - templates/
        - nginx.conf.j2
    - nodejs/
      - tasks/
        - main.yml
    - ruby/
      - files/
        - .gemrc
      - tasks/
        - main.yml
      - templates/
        - rbenv_system.sh.j2
    - vim/
      - tasks/
        - main.yml
  - vars/
    - common.yml
  - ansible.cfg
  - hosts
  - production.yml
.gitignore
requirements.txt
```

構成については、公式サイトのベストプラクティスを参考にしながら、必要な部分に絞り込んだものにしています。

https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html?highlight=handler#directory-layout

#### requirements.txtについて

ansibleは内部でpythonを使っているのですが、自動化を実行するためにいくつかライブラリのインストールが必要となっています。

個別にインストールしてもいいのですが、大抵は`requirements.txt`というテキストファイルが用意されていると思うので、そちらを使うと便利です。

使う際には、`pip`コマンドを使って、次のように実行します。

```console
$ pip install -r ../requirements.txt
```

こうすることで、`requirements.txt`に書かれたライブラリを順次インストールしてくれるんですね。

ご自身でplaybookを作成する場合には、リモートリポジトリに置いておくと共同開発する際に親切かと思います。

####  rolesディレクトリについて

rolesディレクトリ配下にある個々のディレクトリは、インストールや構築を行うモジュール名・ミドルウェア名で切ります。

なので、すでにAnsibleで運用されているプロジェクトに参画する際、

**rolesディレクトリを見れば、どんなモジュールやミドルウェアが使われているかがわかる**

というわけですね。

#### commonディレクトリについて

commonディレクトリには、対象のサーバーで共通する設定などを扱います。

```common/tasks/main.yml
---
- name: Create app dir
  become: yes
  file:
    path: /var/www
    state: directory
    owner: ec2-user
    group: ec2-user
    mode: 0777
```

この設定例では、`/vars/www`というディレクトリを作成しています。

`become:  yes`をつけることで`sudo`をサーバー上でつけて実行するのと同じ意味を持ちます。

#### tasksディレクトリについて

tasksディレクトリにはそれぞれのrolesに応じて、main.ymlというファイルが置かれています。

このmain.ymlには、実際にインストールやパスの反映などのような作業を自動化するための処理を記述します。

```yaml
---
- name: Install packages
  become: yes
  yum:
    name: "{{ packages }}"
  vars:
    packages:
      - git-core
      - gcc
      - gcc-c++
      - gdbm-devel
      - libffi-devel
      - libselinux-python
      - libyaml-devel
      - mysql
      - mysql-devel
      - ncurses-devel
      - openssl-devel
      - sqlite-devel
      - readline-devel
      - zlib-devel
```

上記はサンプルリポジトリのRubyにおけるmain.ymlです。

ここでは、yumを使ってrubyを動かすために必要なライブラリをインストールしています。

#### templatesディレクトリについて

temlatesディレクトリには、Jinja2というPython製のテンプレートエンジンを使って記述します。

【Jinja2】
https://jinja.palletsprojects.com/en/2.10.x/

そのため、ファイルの拡張子が`.j2`となっているんですね。

```nginx:nginx.conf.j2
# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
  worker_connections 1024;
}

http {

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" "$gzip_ratio"';

  access_log  /var/log/nginx/access.log  main;
(中略)
```

例えば、上記はNginxのnginx.conf.j2なのですが、記述は`nginx.conf`そのままですよね。

この`nginx.conf.j2`は、nginxの`main.yml`で次のように呼び出すことで、対象のサーバーへ配置されます。

```main.yml
(中略)
- name: Put local.conf
  become: yes
  template:
    src: roles/nginx/templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
(中略)
```

templateという親keyに対して、srcとdestという子keyがあります。

```
src...テンプレートが置かれているpathを記述(ansibleディレクトリからみて絶対パスで)
dest...対象のサーバー内でテンプレートを配置するpathを記述
```

templateのsrcとdestに対してpathをvalueとして渡してあげれば、ansibleを動かした際に対象のサーバーへテンプレートに記述された内容で配置されるという仕組みです。

#### filesディレクトリについて

filesディレクトリは、templatesと似ているのですが、Jinja2のテンプレートを使わずに、対象のサーバーへ配置するのがポイントです。

あまり使われないので、基本的にはtemplatesを使えばOKなのですが、サンプルリポジトリ上ではrubyのroleで隠しファイルの`.gemrc`を配置するようにしています。

呼び出すときは、main.ymlで次のように記述します。

```main.yml
(中略)
- name: Copy .gemrc to ~/.gemrc to Avoid to Install rb-docs
  become: yes
  copy: 
    src: .gemrc
    dest: ~/.gemrc
(中略)
```

copyという親keyに対して、srcとdestという子keyがあります。(templateと同じ)

```
src...対象のファイルをそのまま記述(パス指定でもOK)
dest...対象のサーバー内でファイルを配置するpathを記述
```

filesディレクトリにあるファイルを対象のサーバーのpathへコピーすることで、ansibleを動かした際に配置されるという仕組みです。

#### varsディレクトリについて

varsディレクトリでは、ansible内の各roleで共通で使用する変数をcommon.ymlというファイル名でまとめます。

```common.yml
ruby_version: 2.6.2
bundler_version: 2.0.1
db_pass: password
db_user: root
db_name: local
local_db_user: local
public_ip: 127.0.0.1
application_name: qiita_clone_2019
```

サンプルリポジトリでは、上記のような形で、rubyのバージョンやMySQLのパスワードなどをまとめています。

各roleのmain.ymlで呼び出す際には、次のように指定しましょう。

```roles/ruby/tasks/main.yml
(中略)
- name: Install Ruby
  shell: bash -lc "RUBY_CONFIGURE_OPTS="--disable-install-rdoc" rbenv install -s {{ ruby_version }}"
(中略)
```

ダブルクォーテーションと`{{}}` を使うことで、 文字列展開のような感じでvars/common.ymlに記載された変数を呼び出せます。

#### hostsファイルについて

このhostsファイルでは、対象のサーバーへの接続情報を記述します。

やり方は二つあって、

```
- 自分のマシンの`~/.ssh/cofig`でSSH接続に関する名前解決を行い、その情報を記述する
- 直接IPアドレスを記述し、Ansible側で接続情報を記述する
```

主にこの二つですね。

個人開発や勉強用であれば、前者で問題ないです。

が、業務では開発者の担当フェーズごとによる権限付与などもansibleで自動化するため、後者を選ぶことが多いですね。

今回は、後者で行います。

ansibleのディレクトリ配下に、拡張子なしでhostsというファイルを作成し、そこにIPアドレスを記述します。

```
[production-app]
18.182.21.236
```

[production-app]という記述は、どの環境に対するIPアドレスまたはホスト名(~/.ssh/configで名前解決する場合のHostName)なのかを示しています。

後述するproduction.ymlで利用します。

#### production.ymlについて

アプリケーションを本番環境にデプロイする際には、環境ごとにそれぞれEC2インスタンスなどを立てるのが基本です。

production.ymlのように、

```
<環境名>.yml
```

といったファイルを作成し、自動化する内容を記述していきます。

```yml

---
- name: Playbook for production
  hosts: production-app
  become: yes
  vars_files:
    - vars/common.yml
  roles:
    - git
    - nginx
    - nodejs
    - mysql
    - ruby
    - vim
```

サンプルリポジトリでは、`hosts`ファイルに記述した`[production-app]`をhostsとして指定することで、どのサーバーに接続するかを示しています。

vars_filesのkeyに指定しているのは、利用する環境変数が書かれたファイルですね。

rolesについては、利用するroleをそれぞれリスト形式で記述しています。

##  Ansibleの動かし方

### pingモジュールを使って、接続確認をしてみよう
playbookの準備ができたら、ローカルでansibleコマンドを用いて動かします。

が、まずは対象のサーバーに対してキチンとansibleが接続できるかどうかを確認しましょう。

任意のテキストエディタを使って、hostsファイルを次のように編集してください。

```hosts
[<任意の名前>]
<IPアドレス> ansible_user=ec2-user ansible_ssh_private_key_file=<ec2にsshログインするためのpemファイル>
```

設定例としては、下記のような感じですね。

```hosts
[production-app]
3.113.242.122 ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/yuta-ushijima.pem
```

編集ができたら保存して、ansibleのpingモジュールを使って接続確認します。

```console
$ ansible-playbook -i hosts production.yml
```

うまく接続ができると、ターミナルに次のような結果が表示されるはずです。

```bash
3.113.242.122 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
```

逆に設定項目に不備があったり、SSH接続するIPアドレスが間違っていたりした場合には、次のようなエラーが表示されます。

```bash
3.113.242.122 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ec2-user@3.113.242.122: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).",
    "unreachable": true
}
```

`unreachablke: true` というのが、対象のサーバーへのアクセスができなかったというエラーなので、このような表示がでた場合には、設定項目などを確認するようにしましょう。

### ansible-playbookコマンドでモジュールのインストールを自動化しよう

pingモジュールを使って接続確認ができたら、今度はplaybookコマンドを使って動かします。

pingコマンドを使うときに編集した、hostsファイルを再び次のように編集します。

```
[production-app]
3.113.242.122
```

`[production-app]`と`3.113.242.122`の部分に関しては、各々の環境に応じて変更してくださいね。

保存ができたら、playbookがあるディレクトリにいることを確認して、次のコマンドを実行します。

```console
$ ansible-playbook -i hosts staging.yml
```

エラーが発生した場合は、エラーメッセージをよく読み、都度対応していきましょう！