注目の記事

Ansible実践入門

2017.01.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

渡辺です。

最近、Ansibleに関する書籍が増えてきていますね。 とはいえ、ほとんどは入門的な位置付けで、それはそれで需要があるんですが、実践レベルで使いこなすノウハウは少ないというのが現実かと思います。 この辺り、まだ試行錯誤を繰り返しているところも多いでしょう。 そこで、ノウハウをガンガン流出させるクラスメソッドなので、ベストプラクティスみたいなものをまとめちゃいました。

Ansibleとは?

雑な言い方をすれば、SSH接続したリモートホストでミドルウェアのインストールや設定ファイルの更新を行うツールです。 カテゴリとしては構成管理ツールに分類されます。 SSH接続が可能であれば、リモートホスト側にエージェントのインストールが不要である点は大きな特徴です。

Playbookにサーバの状態を定義する

AnsibleのPlaybookは、リモートホストの状態を定義したファイルです。 構成管理ツールでは、定義ファイルを作成してツールを実行するのが共通的な流れです。 ツールの実行が成功した時、定義ファイルの内容がリモートホストに反映されています。 Ansibleでは、この定義ファイルをPlaybookと呼びます。

PlaybookはYAMLフォーマットで定義します。 YAMLとかキモイのでRubyでロジックを書かせろという方は、chefを使いましょう:p ロジックが書きにくく、宣言的にリモートホストの定義を行えることが大切です。

Ansibleと冪等性

Ansibleの特徴のひとつは、冪等性(べきとうせい)です。 冪等性はリモートホストの状態を一定に保つためのポリシーです。

原則として、Playbookは繰り返し実行してもリモートホストの状態が変更されないように記述します。 当然、初回実行時はリモートホストに変更が行われます。 しかし、Playbookを変更しない限り、繰り返して実行してもリモートホストに変更が行われないことが重要です。 これは、もしリモートホストに意図しない変更が行われた場合、Playbookの実行によりその変更を検知できるとも言えます。 冪等性を保ったPlaybookを維持することで、継続的なリモートホストの状態管理を行えるようになります。 なお、ワンショットでサーバを構築するような場合、冪等性を無視するのもひとつの戦略です。

冪等性については、Ansibleの冪等性とPlaybookで解説しています。

テストファーストとAnsible

Ansibleによる構成管理は、テストファーストと考え方が同じです。 TDD(テスト駆動開発)などで実践するテストファーストとは、対象のプログラムが「こういう振る舞いをすべきだ」という宣言を、プログラムを書く前にテストコードとして宣言する手法です。 TDDでは、テストコードを先に作成し、テストが成功するまでプログラムを作り込むことになります。 Ansibleでは、対象のリモートホストが「こういう状態であるべきだ」という宣言をPlaybookとして記述します。 テストファーストと異なるのは、Playbookを書きさえすれば、後はAnsibleでリモートホストの変更を行うため、実装(サーバの設定)は行わなくて良い点でしょう。

リモートホストの定義

AnsibleではリモートホストにSSH接続を行います。

インベントリでリモートホストの一覧を定義する

Ansibleでは対象のリモートホストをインベントリ(Inventory)と呼ばれるファイルに定義します。 インベントリファイルは、多くの場合、過去の経緯からhostsというファイル名となっています。 実際は、ファイル名に規則はありません。 自分はinventoryという名前にします。

インベントリファイルの構造は、SSH接続時に名前解決可能なホスト名の一覧です。

foo.example.com
bar.example.com

勿論、IPアドレスの一覧でも良いのですが、「お前どのサーバよ」となるのは明白です。

50.xx.0.1
50.xx.0.2

また、インベントリではグループを作成できます。

[web]
foo.example.com
bar.example.com

[db]
one.example.com
two.example.com
three.example.com

[web][db]がグループです。 リモートホストをグループ化できるので、後述のPlaybookをまとめて実行出来ます。

ssh_configを作成しAnsibleで参照する

インベントリでは名前解決可能なリモートホスト名を記述します。 一方、ホスト名は名前解決はssh_configに定義することがほとんどです。 ところが、多くのリモートホストを管理していくとなると、ssh_configが肥大化します。 また、異なるプロジェクトのリモートホストの管理を単一のssh_configで行うと、事故にも繋がるので避けたいところです。 そこで、プロジェクト毎にssh_configを作成することをオススメします。 詳細は、ansible.cnfでssh_configを設定するを参照してください。

ssh_configをプロジェクト毎に作成し、ansible.cfgで参照するようになれば、インベントリファイルは次のようにシンプルに書けます。

[web]
stg.web1a
stg.web1c

[db]
stg.db1a
stg.db1c

ssh_configはそのままgit管理できます。 普通にサーバに接続する時は、プロジェクトディレクトリで以下のようにsshコマンドを利用すれば良いので便利です。

$ ssh -F ssh_config stg.web1a

これも面倒になった今、自分はsshcでaliasを切ってます。

alias sshc='ssh -F ssh_config'

インベントリとステージング

多くの場合、ステージング環境(検証環境)とプロダクション環境(本番環境)のように、複数のステージについて構成管理を行います。 Ansibleでは、ステージ毎にインベントリファイルを作成することでステージングを扱いやすくなります。

stg/inventory

[web]
stg.web1a
stg.web1c

[db]
stg.db1a
stg.db1c

prd/inventory

[web]
prd.web1a
prd.web2a
prd.web1c
prd.web2c

[db]
prd.db1a
prd.db1c

ステージ毎にインベントリを作成するには、幾つかの方法があります。 色々と試した結果、ステージ毎のディレクトリを作成する方法に落ち着きました。 ファイル構成としては次のようになります。

.
├── ansible.cfg
├── hosts
│   ├── prd
│   │   ├── group_vars
│   │   │   ├── all.yml
│   │   │   └── web.yml
│   │   └── inventory
│   └── stg
│       ├── group_vars
│       │   ├── all.yml
│       │   └── web.yml
│       └── inventory
├── ssh_config
└── web.yml

インベントリファイルはhosts/prd/inventoryhosts/stg/inventoryです。 group_varsにはステージ毎に異なる、グループ変数を定義します。 例えば、Apacheのconfigファイルに含まれるドメイン名や、接続先のDB名などが含まれるでしょう。

この構成でAnsibleを実行する場合は、次のように-iオプションでステージ(ディレクトリ)を指定します。

$ ansible-playbook -i hosts/stg web.yml
$ ansible-playbook -i hosts/prd web.yml

詳細は、Ansible inventoryパターン(詳細ステージパターン)を参照ください。

なお、これはAnsibleのベストプラクティスで紹介されている方法とは異なります。 こちらの方法の場合、構造はシンプルなのですが、グループ変数をステージ毎に切り替える事ができないのでいけていません。

グループ変数にステージ毎の値を定義する

グループ変数は、インベントリのグループと紐付く変数です。 ウェブサーバ(webグループ)に関連する変数はweb.ymlに、すべてのグループに共通した変数はall.ymlに定義します。 グループ変数はステージ毎に定義できるため、例えば、次のようになります。

hosts/stg/group_vars/web.yml

---
domain_name: www-stg.example.com
db_host: db-stg.example.com

hosts/prd/group_vars/web.yml

---
domain_name: www.example.com
db_host: db.example.com

hosts/stg/group_vars/all.yml

---
stage: stg

hosts/prd/group_vars/all.yml

---
stage: prd

グループ変数はTemplateモジュールなどで設定ファイルの中に埋め込み変数として利用できます。

PlaybookとRole

Playbookにはリモートホストの状態を定義します。 原則としてインベントリのグループ毎にPlaybookを作成します(例: web.yml)。

Playbookの記述方法としては、Roleを利用する方法と、Roleを利用しない方法があります。

Roleを利用しない場合はtaskを定義する

PlaybookでRoleを利用しない場合、Playbookにtaskを列挙します。

---
- hosts: web
  tasks:
  - name: ensure apache is at the latest version
    yum: name=httpd state=latest
  - name: write the apache config file
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    notify:
    - restart apache
  - name: ensure apache is running (and enable it at boot)
    service: name=httpd state=started enabled=yes
  handlers:
    - name: restart apache
      service: name=httpd state=restarted

tasksではAnsibleの各モジュールを利用してサーバの状態を定義します。 例えばyumモジュールでは、yumで必要なミドルウェアをインストールし、templateモジュールでは設定ファイルを指定ディレクトリに配置します。

hostsにはグループを指定します。 すべてのリモートホストに実行したい場合は、allを指定します。

PlaybookでRoleを利用しない場合、単一ファイルになることで、管理しにくくなります。 後述のRoleを利用する方が再利用性も高いため、単発でコマンドを流したいようなケースを除き、基本的にはRoleを利用しましょう。

AnsibleのRoleは再利用可能な単位

AnsibleのRoleは適切な範囲で幾つかのtaskを集めたモジュールです。 プログラム的に言えば、Playbookにtaskを列挙する方法はmain関数にすべての処理を記述するようなもので、Roleを利用することで適切なクラスや関数に関連する処理をまとめることが出来ると考えて良いでしょう。 Roleの単位(規模)はプロジェクトや考え方によって異なりますが、独立したミドルウェアなどであればインストールから設定までをまとめ、特定サーバの設定などであれば役割(例: web)などでまとめます。 Roleはrolesディレクトリに配置します。

.
└── roles
    ├── cloudwatchlogs
    │   └── tasks
    │       └── main.yml
    ├── codedeploy
    │   └── tasks
    │       └── main.yml
    └── web
        ├── tasks
        │   └── main.yml
        └── templates
            └── vhost.conf

Roleは、rolesディレクトリ配下のディレクトリ名が対応します(ネストも可能)。 各ディレクトリのtasks/main.ymlがタスクを記述するメインファイルとなります。 その他、templates, files, meta といったディレクトリにRoleで参照するファイルを配置します。

PlaybookでRoleを利用する時は、次のようにtasksの代わりにrolesでRoleの配列を指定します。

---
- hosts: web
  roles:
    - cloudwatchlogs
    - codedeploy
    - web

なお、Playbookにtasksとrolesの両方を同時に指定できません。

共通設定はcommon.ymlで流す

web.yml, db.ymlなどグループ毎に作成するPlaybookでも、Role部分をコピーすれば良いので、共通したミドルウェアの定義を行うことは容易です。 しかし、共通設定はcommon.ymlを作成し、hostsにallを指定することをお勧めします。

---
- hosts: all
  roles:
    - system/lang
    - cloudwatchlogs

これは、Playbookが大きくなってくると1回の実行時間が長くなるためです。 共通のPlaybookを独立させておけば、共通部分の変更時にはcommon.ymlを流すだけで済みますし、web.ymlが小さくなるので実行時間を節約できるのです。

Ansible Galaxyというカオス空間

Ansibleでは有志で作成したRoleが公開されているAnsible Galaxyが利用できます。 しかしながら、業務レベルで利用する場合、Galaxyの利用はあまりお勧めできません。 というのも、メンテナンスされていないRoleも非常に多く、バージョンアップに追従していないケースも多々あります。 結果として、自分で作った方が早い/動かすまで時間がかかるといったことになりかねません。 Roleを作っていくときのサンプルコードとしては有用です。

Ansible Playbookの実行

Ansible Playbookを実行するには、Playbookファイルを指定してansible-playbookコマンドを実行します。 この時、必要なパラメータはオプションで指定してください。

$ ansible-playbook -i hosts/stg web.yml

必要なパラメータはansible.cfgによって異なります。 自分の場合、インベントリ(-i)のみを指定し、他のオプションはansible.cfgとssh_configで定義するため省略できるようにしています。

ansible.cfgの例

[defaults]
retry_files_enabled = False

[privilege_escalation]
become = True

[ssh_connection]
control_path = %(directory)s/%%h-%%r
ssh_args = -o ControlPersist=15m -F ssh_config -q
scp_if_ssh = True

他に覚えておきたいオプションは--checkです。 これはいわゆるDryRunなので、実行してもリモートホストに変更は行われません。 構文チェックやPlaybook変更時に、リモートホストの変更の有無などをチェックするのに利用できます。

まとめ

業務レベルでAnsibleを使うにあたり必要な部分をまとめてみました。 肝となるのはリモートホストの管理をプロジェクト毎のssh_configで定義すること、ステージ毎にインベントリを分割すること、Roleを活用すること、の3点です。

実践的なAnsibleライフを!