昨今、さまざまな場面で自動化が進められていますが、IT業界の仕事の中でも作業の効率化や人的ミスを削減することを目的として自動化を取り入れていく動きが活発になっています。

環境の規模が大きいプロジェクトなどでは、同じ作業や目視確認作業が続くとどうしてもミスが出てきてしまったり時間がかかったりしてしまいますが、そのような状況を改善できるのが構成管理ツールです。この構成管理ツールとして人気が高いものが「Ansible」です。

私が携わったプロジェクトでもAnsibleを積極的に取り入れていこうという取り組みを行っており、今後も需要は高まっていくと思います。
その需要の高まりに対応して、社内でAnsible学習を兼ねたマニュアル作りを行ったのでその内容を記載します。

Ansibleとは?                         

Ansibleとは、オープンソース(無償のソフトウェア)の構成管理ツールで、サーバーの作成やOS、MWの設定を自動化できます。

RedHat株式会社が提供しているもので、開発言語はPythonです。
RedHat株式会社が提供しているということもありLinux OSのサーバーで使えるモジュールの方が多いですが、Windows OS用モジュールも用意されており、さらにエージェントレスのため制限のある環境でも取り入れやすいです。

Ansibleを利用することで特に恩恵を受けるのは大規模プロジェクトです。また似たような環境構築の場合Playbookを使いまわすことも可能であることから、効率化のためにAnsibleを積極的に使っていこうとする企業もあります。

Ansibleのメリット・デメリット

Ansibleを導入すると、人が手順書に従って手動で設定する作業を自動化できるため、作業時間の短縮や、人的ミスの削減信頼性の向上が見込めます。Playbookを作成するという点では人の手が加わりますが、サーバー数台で作業を手動で行うのと、事前に検証して準備をしておいたPlaybookを流すだけにしておくのとでは後者の方が明らかにミスの生じにくい状態になります。

逆にデメリットとしては、実行するPlaybookにミスがあった場合、全ての対象クライアントに影響を及ぼしてしまうという点です。実行する前に検証環境で検証したり、仮実行コマンドで想定通りの動きをするか確認をしたりして事前に準備をしておく必要があります。

Ansibleの利用イメージ                         

Ansibleはインベントリファイル(hosts.ini)で実行したいクライアントを指定して、yamlファイル(main.yml)で実行したい処理を記載するのが基本の構成です。

【利用イメージ図】

Ansible実行イメージ図

【ディレクトリ】

├── inventories
│     └──hosts.ini  ##インベントリファイル。クライアントのIPアドレスやホスト名を記載する。
└── main.yml  ##クライアントに実行したい内容(=タスク)などを記載する。

手動の場合はクライアントサーバー10台に対してそれぞれディレクトリ作成のコマンドを実行していく必要がありますが、Ansibleを利用するとホストとディレクトリ作成の処理を記載したPlaybookを一度流すだけで「/scripts」を作成できます。

また、コマンドよりも視覚的に見やすいモジュール(後述)というものが用意されているので、コードも比較的容易に記述できます。

Ansible環境構築

Ansibleの環境構築は、Ansibleインストールや鍵の配置などで手軽に済みます。
今回はAWS上にAnsibleサーバー1台、クライアントサーバー2台の計3台のEC2を作成して検証を行いました。
弊社のBFT道場 チョイトレを参考に環境構築を行ったのですが本当に簡単に環境を整えることができました。
詳しい手順は割愛しますが、以下コマンドでAmazon Linux2 OSにAnsibleをインストールし、後はAnsibleサーバーからクライアントにSSH接続するための設定を行うだけです。

【Ansibleインストールコマンド(Amazon Linux2 用)】

① EPELリポジトリ有効化

amazon-linux-extras install -y epel

② Ansibleインストール

yum install ansible


Playbookの書き方 ~基礎編~                            

先述の利用イメージのインベントリファイルとyamlファイルを例にして基本的な書き方について解説します。

インベントリファイル

インベントリファイルのコード

① ホストグループ
[]内に任意のグループ名を記載。クライアントをグループに分けて実行できます。後述のyamlファイルで「hosts: web_servers」と指定して利用しています。

② クライアント情報
IPアドレスかホスト名を記載します。ここではエイリアス名として「web1」などと記載し、クライアントのIPアドレスを接続変数「ansible_host」で記載しています。

yamlファイル

yamlファイルのコード

① yamlファイルの始め
yamlファイルの始めに記載します。

② シーケンス(配列、リスト)
文法⇒ -[半角スペース]値。項目の区切り。
どこにつければ良いか疑問に思うかと思いますが、最初は深く考えずに調査したコードを使用して慣れていくのが良いと思います。

③ ハッシュ(マッピング、ディクショナリー)
文法⇒ キー:[半角スペース]値。
半角スペース2個でインデントすることで、ハッシュをネスト(入れ子)させることができます。
また本サンプルではシーケンスの中にハッシュをネストさせ、さらにハッシュの中にシーケンスをネストさせた構造になっています。
ネスト構造について深く理解したい方はこちらのサイトがおすすめです。
プログラマーのための YAML 入門 (初級編)

④ ディレクティブ
モジュールを実行するためにAnsible側に与える情報のようなものです。

⑤ モジュール
Playbook のタスクで使用可能なコードです。
公式サイトからモジュールを調査できます。
用意されたモジュールを利用して実行したい処理を記載していきます。

Playbookの書き方 ~応用編~

ディレクトリ構成

単純な処理を行うのであれば基礎編での内容で実装可能ですが、実際には複数の異なる処理を実行したい場合やクライアントによって変数を分けたい場面がでてきます。

そういった場合に分かりやすいディレクトリ構成にして処理や変数ごとにyamlファイルを分けておくことで、修正を容易に行うことができたりエラーの原因を特定しやすくなったりといったメリットがあります。
Ansible公式ではベストプラクティスなディレクトリ構成を提示しており、ディレクトリ名など一定の規則があります。

今回はベストプラクティスな構成を参考にこちらのディレクトリ構成で解説していきます。

├── group_vars  ##グループ変数設定。配下にはインベントリファイルのグループ名かサーバー名を記載。
│   ├── all   ##デフォルトで全てのクライアントはallに属しているため共通設定がある場合はallとする
│   │   └── common.yml
│   └── web
│       └── web.yml
├── inventories ## インベントリファイル
│   └── hosts.ini  ## ファイル名は任意
├── ply_web.yml    ## Main Playbook (WEB用)  
├── ply_db.yml     ## Main Playbook(DB用)    
└── roles   ##roles/[ロール名]/tasks/main.ymlの形で記載する。
    └── LNX_directory    ## OSディレクトリ作成  
        └── tasks  
            └── main.yml

メインPlaybook

・ply_web.yml


# ————————
# ply_web.yml
# ————————

name: Web Server Playbook!
  hosts: web
  become: true
  gather_facts: false
  roles:
    – LNX_directory


・ply_db.yml


# ————————
# ply_db.yml
# ————————

name: db Server Playbook!
  hosts: db
  become: true
  gather_facts: false
  roles:
    – LNX_directory

hostsディレクティブでそれぞれインベントリファイル(後述)のグループ名を記載して対象クライアントを指定しています。
またタスクはメインのyamlファイルに記載するのではなく、rolesディレクティブを指定して、roles/[ロール名]/tasksディレクトリ配下のyamlファイルに記載するのが一般的です。こうすることでタスクが増えた際に簡単にPlaybookごとでタスクを指定することが可能です。

またrolesディレクティブ内にtagsディレクティブなどを利用することも可能で、その場合は以下の形式となります。

例) roles :
      – role: LNX_directory
         tags: <タグ名>

tagsディレクティブを利用することで、ansible-playbookコマンド実行時にtagsオプションを使用して実行したいロールを指定でき便利です。

例) ansible-playbook --tags “<タグ名>” -i hosts.ini ply_web.yml

ロール

・roles/LNX_directory/tasks/main.yml


#ディレクトリ作成(共通)
name: create directory (OS common)
  file:
    path: “{{ item.dirname }}”
    owner: “{{ item.owner }}”
    group: “{{ item.group }}”
    mode: “{{ item.mode }}”
    state: directory
  with_items: ‘{{ os_com_dirlist }}’

#ディレクトリ作成(WEB)
name: create directory (WEB)
  file:
    path: “{{ item.dirname }}”
    owner: “{{ item.owner }}”
    group: “{{ item.group }}”
    mode: “{{ item.mode }}”
    state: directory
  with_items: ‘{{ web_dirlist }}’
  when: web_dirlist is defined

{{ 変数名 }}で簡単に別ファイルの変数も呼び出しが可能です。ただし、変数は別ファイルで重複して利用可能ですが優先順位があるので注意が必要です。

またwith_itemsディレクティブを使用すればループ処理が可能です。itemの中にwith_itemsで呼び出した変数が代入されていきます。詳しくは後述の変数章で解説します。

変数、vars

・group_vars/all/common.yml

os_com_dirlist:
  – {dirname: “/scripts_test”, owner: “root”, group: “root”, mode: “0775”}
  – {dirname: “/scripts_test/bin”, owner: “root”, group: “root”, mode: “0775”}
  – {dirname: “/scripts_test/log”, owner: “root”, group: “root”, mode: “0775”}
  – {dirname: “/scripts_test/conf”, owner: “root”, group: “root”, mode: “0775”}

・group_vars/web/web.yml

web_dirlist:
  – {dirname: “/opt_test/cps”, owner: “root”, group: “root”, mode: “0644”}
  – {dirname: “/usr_test/share/nginx/webroot”, owner: “root”, group: “root”, mode: “0644”}

変数はvarsディレクトリに格納しておきます。varsディレクトリに分けることで変数が散在せず、また変数の定義場所が分かりやすくなり効率的にコード修正が行えます。
今回はグループ共通とwebグループとで行いたい処理が分かれているので、group_varsディレクトリの利用が最適です。

また、ロール(roles/LNX_directory/tasks/main.yml)で呼び出した変数{{ os_com_dirlist }}と{{ web_dirlist }}はこちらで定義されたものです。
さらにロールのwhenディレクティブで、group_varsにて定義した変数{{ web_dirlist }}を利用して分岐することで、webグループでのみweb用ディレクトリ作成の処理を実行することが可能です。

ここで、

ループ処理とか難しいことせずに書けないの?
変数もロールに書いた方が分かりやすいのでは?

と思った方もいるかもしれないので、その場合どのようなコードになるかみていきます。

「ループ処理(with_items)」「varsディレクトリ」を利用しない場合

以下のようにグループ共通用とwebグループ用のディレクトリに分ける必要があり、さらに作成したいディレクトリの増加に伴ってコード量が膨大になっていきます。

・roles/LNX_directory_all/tasks/main.yml


#ディレクトリ作成(共通)
name: create directory1 (OS common)
  file:
    path: “/scripts_test”
    owner: “root”
    group: “root”
    mode: “0775”
    state: directory
name: create directory2 (OS common)
  file:
    path: “/scripts_test/bin”
    owner: “root”
    group: “root”
    mode: “0775”
    state: directory    
## …作りたいディレクトリ分上記のコードを増やしていく

・roles/LNX_directory_web/tasks/main.yml


#ディレクトリ作成(WEB)
name: create directory1 (WEB)
  file:
    path: “/opt_test/cps”
    owner: “root”
    group: “root”
    mode: “0644”
    state: directory
name: create directory2 (WEB)
  file:
    path: “/usr_test/share/nginx/webroot”
    owner: “root”
    group: “root”
    mode: “0644”
    state: directory
## …作りたいディレクトリ分上記のコードを増やしていく

「ループ処理」のみ利用する場合

ロール内のyamlファイルで変数を利用するときはvarsディレクティブで記載します。
このように記載することで①のコードよりかなりすっきりして見やすくなったのが分かります。
しかしこの場合でも、ロールディレクトリが余計に増えたり、複数の変数に処理を追加する場合にタスクごとにファイルを開く必要があったり、処理が複雑になるにつれて不便を感じてしまいます。

・roles/LNX_directory_all/tasks/main.yml

#ディレクトリ作成(共通)
name: create directory (OS common)
  file:
    path: “{{ item.dirname }}”
    owner: “{{ item.owner }}”
    group: “{{ item.group }}”
    mode: “{{ item.mode }}”
    state: directory
  with_items: ‘{{ os_com_dirlist }}’
  vars:
    os_com_dirlist:
      – {dirname: “/scripts_test”, owner: “root”, group: “root”, mode: “0775”}
      – {dirname: “/scripts_test/bin”, owner: “root”, group: “root”, mode: “0775”}
      – {dirname: “/scripts_test/log”, owner: “root”, group: “root”, mode: “0775”}
      – {dirname: “/scripts_test/conf”, owner: “root”, group: “root”, mode: “0775”}

・roles/LNX_directory_web/tasks/main.yml

#ディレクトリ作成(WEB)
name: create directory (WEB)
  file:
  path: “{{ item.dirname }}”
    owner: “{{ item.owner }}”
    group: “{{ item.group }}”
    mode: “{{ item.mode }}”
    state: directory
  with_items: ‘{{ web_dirlist }}’
  vars:
    web_dirlist:
      – {dirname: “/opt_test/cps”, owner: “root”, group: “root”, mode: “0644”}
      – {dirname: “/usr_test/share/nginx/webroot”, owner: “root”, group: “root”, mode: “0644”}


「ループ処理(with_items)」「varsディレクトリ」を利用することで上記のようなコードが整理され、より効率的に修正が行えるPlaybookにできます。

インベントリファイル

・inventories/hosts.ini

[web]
web1 ansible_host=192.168.1.101
[db]
db1 ansible_host=192.168.1.201

インベントリファイルに変数を記載したり、yml形式のファイルにしたりといったことも可能です。

実行

実際に実行してみます。

  • 構文チェック

まずPlaybookのコードにエラーがないか --syntax-checkオプションを使用して構文チェックを行います。
実行したいyamlファイルを指定し、-i オプションでインベントリファイルを指定します。

# ansible-playbook -i inventories/hosts.ini ply_web.yml --syntax-check

playbook: ply_web.yml

エラーなく上記の画面になれば問題ありません。

  • 仮実行

--checkオプションを使用することで対象クライアントに対してPlaybookの仮実行を行うことができます。
処理は実際には実行されません。

# ansible-playbook -i inventories/hosts.ini ply_web.yml --check

PLAY [Web Server Playbook!] ****************************************************

TASK [LNX_directory : create directory (OS common)] ****************************
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test’, u’group’: u’root’, u’mode’: u’0775′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/bin’, u’group’: u’root’, u’mode’: u’0775′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/log’, u’group’: u’root’, u’mode’: u’0775′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/conf’, u’group’: u’root’, u’mode’: u’0775′})

TASK [LNX_directory : create directory (WEB)] **********************************
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/opt_test/cps’, u’group’: u’root’, u’mode’: u’0644′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/usr_test/share/nginx/webroot’, u’group’: u’root’, u’mode’: u’0644′})

PLAY RECAP *********************************************************************
web1                       : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

failedが0になっていることが確認できれば大丈夫です。
またnameディレクティブなどで指定したものが表示されているのが分かります。

ply_db.ymlの場合は以下となります。

# ansible-playbook -i inventories/hosts.ini ply_db.yml --check

PLAY [db Server Playbook!] *****************************************************

TASK [LNX_directory : create directory (OS common)] ****************************
changed: [db1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test’, u’group’: u’root’, u’mode’: u’0775′})
changed: [db1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/bin’, u’group’: u’root’, u’mode’: u’0775′})
changed: [db1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/log’, u’group’: u’root’, u’mode’: u’0775′})
changed: [db1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/conf’, u’group’: u’root’, u’mode’: u’0775′})

TASK [LNX_directory : create directory (WEB)] **********************************
skipping: [db1]

PLAY RECAP *********************************************************************
db1                        : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

skippedが1となっておりTASK [LNX_directory : create directory (WEB)]がスキップされているのが分かります。

  • 本実行

実際に実行を行います。

# ansible-playbook -i inventories/hosts.ini ply_web.yml

PLAY [Web Server Playbook!] ****************************************************

TASK [LNX_directory : create directory (OS common)] ****************************
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test’, u’group’: u’root’, u’mode’: u’0775′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/bin’, u’group’: u’root’, u’mode’: u’0775′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/log’, u’group’: u’root’, u’mode’: u’0775′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/conf’, u’group’: u’root’, u’mode’: u’0775′})

TASK [LNX_directory : create directory (WEB)] **********************************
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/opt_test/cps’, u’group’: u’root’, u’mode’: u’0644′})
changed: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/usr_test/share/nginx/webroot’, u’group’: u’root’, u’mode’: u’0644′})

PLAY RECAP *********************************************************************
web1                       : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

failedが0になっており正常に実行できたことが確認できました。

  • 冪等性(べきとうせい)確認

冪等性とは、同じ操作を繰り返し行っても同じ結果が得られることです。
Ansibleは冪等性が保たれるので、もう一度実行し変更が行われたかを確認します。

# ansible-playbook -i inventories/hosts.ini ply_web.yml

PLAY [Web Server Playbook!] ****************************************************

TASK [LNX_directory : create directory (OS common)] ****************************
ok: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test’, u’group’: u’root’, u’mode’: u’0775′})
ok: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/bin’, u’group’: u’root’, u’mode’: u’0775′})
ok: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/log’, u’group’: u’root’, u’mode’: u’0775′})
ok: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/scripts_test/conf’, u’group’: u’root’, u’mode’: u’0775′})
TASK [LNX_directory : create directory (WEB)] **********************************
ok: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/opt_test/cps’, u’group’: u’root’, u’mode’: u’0644′})
ok: [web1] => (item={u’owner’: u’root’, u’dirname’: u’/usr_test/share/nginx/webroot’, u’group’: u’root’, u’mode’: u’0644′})

PLAY RECAP *********************************************************************
web1                       : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

本実行時と異なってchangedが0になっており、冪等性が確認できました。

おわりに

今回Ansibleを学習するにあたって、Playbookの書き方について概要レベルのまとめ記事があればより学習しやすいのではと感じ、本記事では初学者向けにコードベースで書き方の深掘りを意識して執筆しました。こちらを読んでAnsible学習が取り組みやすくなったら嬉しいなと思います!

またPlaybookは4つのセクションで構成されているので、構造の理解としてこちらの記事も一読しておくと良いと思います。
【Ansible】Playbookの構造:4つのセクション