新年あけましておめで…え、もうすぐ8月!?
前回は、Rubyのビルドを行うAnsibleのプレイブックとしてyamlファイルを1つ作りました。内容が単純な場合はファイル1つで問題ありませんが、複雑な構築手順をプレイブックとして記述するときには、いくつかの段階ごとにファイルを分割した方が見通しが良くなりそうです。
Ansibleの公式ドキュメントでは、そのような場合におすすめの Best Practices を提示しています。
今回はこのBest Practicesにしたがって前回作ったRubyビルドのプレイブックを複数のファイルに分割してみます。
ターゲットとするホストは前回同様の2つですが、今回はそれに入れるRubyのバージョンを分けてみます。
作業場所のディレクトリ構成は、次のようになります。
. ├── book.yml ├── host_vars/ ├── hosts └── roles/
book.ymlが今回のプレイブック、hostsがインベントリです。
インベントリはホスト名を並べてグループ名をつけただけのものになります。
[remote] trusty.l.syngram.co.jp wily.l.syngram.co.jp
プレイブックは前回と大きく変わります。
--- - hosts: remote roles: - ruby-source serial: 1 - hosts: remote roles: - build-dep-ruby - build-ruby
ホストのグループに対して、適用するべきロールを列挙しています。
「ロール」というのは分割したタスクの集まりに名前をつけたもので、ここでは以下の3つのロールを用意しています。
今回は全体のタスクがRubyをビルドする準備をしてビルドするという単純なものなので、ちょっと細かめに手順をロールに分割してしまいましたが、本来は、ホストに持たせたい役割を実現するタスクの集まりにするのが良いようです(だからroleという名前なのでしょうか。演劇の比喩で用語を揃えていると言うのもあるようですが)。
例えば、WordPressを稼働させるサーバーを作る時のロールとして
とか作ったりすると良いようです。
今回はバージョンの異なる2つのRubyをそれぞれ別のホストにインストールします。ダウンロードするRubyのソースファイルのURLやホストのアドレスは異なりますが、手順は共通しています。こういう場合は異なる部分を変数として記述し、手順は同じものを使うようにしたいものです。
Ansibleでは以下のような場所に変数を書いて使うことができます。
よく使うと思われるものを挙げましたが、他にもどんな場所に書けるかはこちらで確認できます。
このように変数はいくつかの場所で書く事ができますが、同じ名前の変数は優先度の高い方の値で上書きされます。上に挙げたリストでは、下の方ほど優先度が高く、例えばroleのデフォルト値はgroup_varsやhost_varsで上書きできますが、roles/XXX/vars/main.yml に書いた変数は上書きできません。
上に挙げた以外の変数も含めた完全な優先順位はこちらで確認できます(Ansibleのバージョン1と2とで少し異なるようです)。
今回はホストごとに別のバージョンのRubyをインストールするので、 host_vars/
にRubyのバージョンに依存した情報を書いてみます。他には例えば同じバージョンを複数のホストに入れる場合だと、inventoryでバージョンごとのホストをグループ分けして group_vars/
に書いたりする事になるはずです。
Rubyのバージョンに依存する情報として、今回は以下のように書いてみました。
trusty.l.syngram.co.jp
用
ruby_ver: 2.2.5 ruby_src_url: https://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.5.tar.gz ruby_src_sha256: 30c4b31697a4ca4ea0c8db8ad30cf45e6690a0f09687e5d483c933c03ca335e3
xenial.l.syngram.co.jp
用
ruby_ver: 2.3.1 ruby_src_url: https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz ruby_src_sha256: b87c738cb2032bf4920fef8e3864dc5cf8eae9d89d8d523ce0236945c5797dcd
操作対象のホスト上でパッケージのインストールなどのroot権限が必要な操作を実行するためには、 sudo
コマンドを使うためのパスワードをAnsibleに教える必要があります。操作対象のホスト全てでパスワードが共通なら、 ansible-playbook
を起動するときに --ask-become-pass
を使ってパスワードを入力できます。
$ ansible-playbook --ask-become-pass -i hosts playbook.yml SUDO password:
しかし、この方法では複数の操作対象に対してパスワードは共通のもの1つしか入力できません。ホスト単位またはグループ単位でパスワードが異なる場合は、専用の変数で ansible_become_pass
というものがあって、 host_vars/
や group_vars/
などで記述できます。
ただし、そもそもパスワードをそのままファイルに書くわけにはいきません。多分、そのプレイブックはgitか何かのリポジトリで管理して共有したりするでしょう?
こういう場合のために、Ansibleには暗号化したyamlファイルを扱うためのvaultという機能があります。
早速、試してみましょう。適当なディレクトリで以下のようにすることで、暗号化したYAMLファイルを作ることができます
kuroda@charlie:~/tmp$ ansible-vault create foo.yml New Vault password: (パスワードを入れる) Confirm New Vault password: (確認用にもう一度入れる)
こうするとエディタが起動して、コマンドの引数に指定した foo.yml
の内容を好きに書くことができます(実際はYAML以外のファイルでも構いません)。
これで foo.yml
ができたので、まずはそのまま表示してみます
kuroda@charlie:~/tmp$ cat foo.yml $ANSIBLE_VAULT;1.1;AES256 32303935373465653132633166633765393762626337646261383937343966383533306434636432 6664613966353938376261393339376239636531336339610a376565623830326634333564643638 38313830373736626637326238336439343566393731613661356130656431376432623836346135 3038366535656663340a383339383032306664386436343032653361613030393231613038616133 3431
ばっちり暗号化されています。
復号して表示するには、 ansible-vault view
を使います
kuroda@charlie:~/tmp$ ansible-vault view foo.yml Vault password: (さっき決めたパスワードを入れる) foo: bar
(実際はページャーが起動して表示されます。環境にも依るかもしれません。)
hostsgroup_vars
等の下の、通常の変数を書いたYAMLファイルと同じ場所に置いておくと、 ansible-playbook
が必要に応じて復号して使ってくれます。
例えば、今回は hosts_vars/trusty.l.syngram.co.jp/vault.yml
に ansible_become_pass
を書いて暗号化したものを置いてみます。
kuroda@charlie:~/work/ansible/ruby-build$ ansible-vault view host_vars/trusty.l.syngram.co.jp/vault.yml Vault password: ansible_become_pass: XXXXX
このまま ansible-playbook
すると
kuroda@charlie:~/work/ansible/ruby-build$ ansible-playbook -i hosts book.yml ERROR! Decryption failed
おっと。今度は --ask-vault-pass
で解除用のパスワードを教える必要があります。
kuroda@charlie:~/work/ansible/ruby-build$ ansible-playbook --ask-vault-pass -i hosts book.yml Vault password: PLAY *************************************************************************** (以下略)
さて、YAMLファイルを暗号化できるようになったので、パスワードをそのままファイルに書いて保存する事への不安はひとまずなくなりました。とはいえ、暗号化したファイルをそのままリポジトリに入れて管理するのは、自分専用ならともかく共有する事を考えると適切ではないでしょう。また、YAMLファイル全体を暗号化してしまうため、「パスワードはどこに書いたっけ」と忘れたときにgrepなどで検索して探し出すのも難しくなります。
こういう場合のオススメが公式のドキュメントで紹介されていますが、以下のような方法になります。
まず、暗号化するYAMLにパスワードを変数として記述しますが、この時、変数名の頭に vault_
を付けます。例えばsudoのパスワードなら vault_ansible_become_pass
という名前にします。
次に、暗号化しないYAMLで実際の変数を定義しますが、変数の値には先ほどの vault_
付きの変数が展開されるように記述します。
そして、暗号化しないYAMLファイルはリポジトリに追加するなどして管理し、暗号化する方のYAMLファイルは誰にも見られないようにパーミッションや .gitignore
等をうまく設定しておきます。
具体的な例をあげると、暗号化しないYAMLファイルは host_vars/trusty.l.syngram.co.jp/main.yml
として以下のように書いて、
ansible_become_pass: "{{ vault_ansible_become_passs }}"
暗号化するYAMLファイルは host_vars/trusty.l.syngram.co.jp/vault.yml
として以下のように書きます。
vault_ansible_become_passs: XXXXX
そしてこの vault.yml
だけを ansible_vault
で暗号化するわけです。
こうすることで、
ansible_become_pass
という文字列で検索すると、どのファイルに書いているかが分かるvault_ansible_become_pass
という変数を展開しているので、同じディレクトリのvault.yml
に暗号化して書いてあると見当がつけられると言うわけです。
下準備が長くなりましたが、実際のファイルの中身を見ていきましょう。
今回のプレイブック一式はgithubに置いてあるので、必要に応じてお手元にcloneしてください。
まずは各ホスト用の変数ファイルです。先ほどほとんどの部分をお見せしましたが、もう一度全体を見返してみます。今回は2つ用意して使いますが、似たような内容なので片方だけ。
ansible_become_pass: "{{ vault_ansible_become_pass }}" ruby_ver: 2.2.5 ruby_src_url: https://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.5.tar.gz ruby_src_sha256: 30c4b31697a4ca4ea0c8db8ad30cf45e6690a0f09687e5d483c933c03ca335e3
1行目はsudoのパスワードで、実際の内容は ansible-vault
で暗号化したファイルに vault_ansible_become_pass
という名前の変数で記述しています。
2行目以降はホスト毎にインストールするRubyのバージョン番号、ソースコードのtar-ballのURL、そのハッシュ値ですが、この辺は構造化して次のように書くこともできます。
ruby: ver: 2.2.5 src: url: https://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.5.tar.gz sha256: 30c4b31697a4ca4ea0c8db8ad30cf45e6690a0f09687e5d483c933c03ca335e3
ただしこの書き方には注意があって、優先度の高い場所で変数を上書きする場合に、構造化した変数はその全体がまるごと置き換えられてしまいます。
例えば、上記のRubyソースに関する情報を仮にroles/XXX/defaults/ に書いていたとして、ローカルに置いたミラーからダウンロードする為にURLだけgroup_varsで書き換えたいとします。
この時、変数の ruby.src.url
だけを書き換えるつもりで以下のように書くと、
ruby: src: url: http://mirror.intra.example.net/ruby/2.2/2.2.5.tar.gz
変数 ruby
全体がこの内容で置き換えられてしまい、 ruby.ver
や ruby.src.sha256
は未定義になってしまいます。
この点に付いてはこちらのnoteに書いてありますが、Ansibleの設定ファイル等で hash_behavior=replace
というデフォルト値を hash_behavior=merge
に変更することで期待した動作にする事はできます。
ただ、デフォルト動作の変更は混乱の原因になりがちなので、変数は構造化しない書き方をする方が良さそうです。
次はロールを見ていきます。1つ目はRubyのソースコードをローカルにダウンロードするロールです。
ruby-source
ロールには、タスクを書いたymlを1つだけ置いています。
--- - name: create tmp directory local_action: module: file path: tmp/ state: directory - name: fetch ruby source local_action: module: get_url url: "{{ ruby_src_url }}" sha256sum: "{{ ruby_src_sha256 }}" dest: tmp/ruby-{{ ruby_ver }}.tar.gz
内容としては、Ansibleを実行するローカルホストにRubyのソースをダウンロードする処理を書いています。
今回は1つのバージョンを1つのホストにインストールするだけなので、操作対象のホストに直接ダウンロードさせた方が早いのですが、同じバージョンのRubyを複数のホストにインストールする場合はこんな感じになるだろうかというのを試しています。
ここでポイントになるのは localaction:
項で、通常の場合、タスクは操作対象のホストで実行されますが、これをつけたタスクは操作対象のホストではなくAnsibleを実行するローカルホストで実行されます。
ここではダウンロードしたファイルを置くための tmp
ディレクトリの作成と、実際のRubyソースのダウンロード処理を記述していますが、どちらも目的のディレクトリやファイルが既に存在する場合は実行をスキップします。
また、操作対象のホスト毎にタスクを平行に実行するので、今回の場合は2つのバージョンのRubyソースが同時にダウンロードされます。
ところで、例えば仮に100台の操作対象ホストに同じバージョンのRubyをインストールする場合を考えてみると、このプレイブックの初回実行時にはまだ目的のソースファイルは存在しないため、100台分のダウンロード処理が同時に動き始めて、同じファイルを同時に100回ダウンロードしようとしてしまいます。
この問題を回避する為に、Ansibleでは同時に実行するタスクの数を制限することができます。
その方法は、serial値を指定する事で、今回のようなroleを使うプレイブックの場合は大元のYAMLファイル book.yml
に以下のように記述します。
--- - hosts: remote roles: - ruby-source serial: 1
これで、ruby-sourceロールは同時に1つずつしか実行されないため、100台に同じRubyをインストールする場合でも、ローカルホストでのダウンロード処理は以下のように1回しか実行されません。
2つ目のロールは、Rubyのビルドに必要なパッケージを apt-get コマンドでインストールするロールです。
--- - name: apt-get update apt: update_cache: yes upgrade: dist cache_valid_time: 3600 become: true - name: build-dep ruby-full apt: name: ruby-full state: build-dep become: true - name: install libXX-dev apt: name: "{{ item }}" state: installed with_items: - zlib1g - libreadline-dev - libssl-dev become: true
最初にパッケージ情報とインストール済みパッケージを最新版に更新しています(1行目から7行目)。cache_valid_time: 3600
で apt-get update
を前回実行してから3600秒の間は省略するようにしています。
次に apt-get build-dep ruby-full
でRubyのビルドに必要なパッケージをインストールします(8行目から12行目)。
最後に、build-depではなぜか入らないライブラリ3つをインストールして、このロールは終わりです(13行目以降)。
ここで使うaptモジュールの詳細は apt – Manages apt-packages — Ansible Documentationで解説されています。
最後のロールはRubyのビルドして、 /opt/ruby/X-Y-Z
にインストールします。
まずは、ロールの中で使う変数定義から。
--- ruby_prefix: /opt/ruby/{{ruby_ver}} ruby_basename: ruby-{{ruby_ver}} ruby_file: "{{ruby_basename}}.tar.gz"
実際のタスクは次のようになっています。
--- - stat: path={{ ruby_prefix }}/bin/ruby register: stat_result - name: build and install ruby include: build.yml when: not stat_result.stat.exists
このタスクでは、最初にインストール先にRubyが既に入っているか確認しています(2行目から3行目)。
確認した結果は変数 stat_result
に格納するよう指定しています。
ビルド処理自体は main.yml
とは別に build.yml
で書いていて、4行目からのタスクでは、
stat_result.stat.exists
が偽の場合は build.yml
を実行するように書いています。
ビルド処理はtar.gzの展開、configure、make、make installといった複数のステップが必要なのですが、それぞれのステップで別々にrubyのインストール済みを判定する処理を書くのは手間がかかるので、別のYAMLに分離した上で判定を1回で済ませているわけです。
そのbuild.ymlは以下のようになります。
--- - command: mktemp -d register: temp_dir - unarchive: src: tmp/{{ruby_file}} dest: "{{ temp_dir.stdout }}/" - command: ./configure --prefix={{ruby_prefix}} args: chdir: "{{ temp_dir.stdout }}/{{ ruby_basename }}/" - command: make args: chdir: "{{ temp_dir.stdout }}/{{ ruby_basename }}/" - command: make install become: true args: chdir: "{{ temp_dir.stdout }}/{{ ruby_basename }}/" - name: remove temp directory file: path: "{{ temp_dir.stdout }}" state: absent
ここに書いたタスクはすべて操作対象のホストで実行されます。
まずビルド用の一時ディレクトリを作り、そのディレクトリ名を変数 temp_dir
に格納します(2行目から3行目)。
次にunarchive モジュールでソースをそのディレクトリに展開します。 src
項で展開する tar.gz
ファイルを指定しますが、デフォルトではこの tar.gz
ファイルは操作対象ホストではなく、Ansibleを実行するローカルホストから探して自動的に転送してくれるので、ローカルから操作対象にtar.gzを転送する処理をプレイブックに書く必要はありません。
それに対して dest
で指定するのは操作対象ホストのディレクトリになります。プレイブックに自前で tar.gz
を転送するように処理を書いた場合は、 copy: no
を指定することで、 src
で指定したファイルを操作対象のホストで探すようになります。
また、新しいバージョン(2.0以降)のAnsbileでは src
にURLを指定することでダウンロードもここで同時に行うことができます。
後は見たままですがcommand モジュールを使って configure
から make install
までを実行します(7行目から16行目)。
最後に作業用の一時ディレクトリを削除して終わりです(17行目)。
今回はAnsibleのプレイブックを複数のファイルに分割するBest Practicesと呼ばれる方法に関連して、次のような点を紹介しました
次回はRubyのビルドでは使わなかった、設定ファイルの書き換えやサービスの再起動について紹介します。