kikukawa's diary

都内で活動するシステムエンジニアが書いてます。 興味を持った技術やハマったポイント、自分用メモをつけてます。 最近はweb中心

AnsibleのPlaybookを作る前に考えておくこと

この記事はAnsible Advent Calendar 2016 - Qiitaの24日目の記事になります。
Ansibleで個人開発環境を構築して、一通り使ってみて感じたことを残しておきます。
基本的に自分のメモ程度なので、何かあったらコメントお願い致します。

ベスト・プラクティス

取り敢えず読んでおきます。
更新されているので、たまに読み返すと新しい情報が追加されていることがあります。

Best Practices — Ansible Documentation

ディレクトリ構成

ベスト・プラクティスで下記2つが紹介されています。

qiitaでも別解として話題に上がっています。

どれがいいかはプロジェクト次第です。
作り始める前にどれがいいか検討すべきですが、決められなければ、
とりあえず、ベスト・プラクティスのDirectory Layoutで始めてみてあとで構成変えるでもいいと思います。

ちゃんと設計する

最初からロールを分割しすぎない

「早すぎる最適化」はよくないです。 ロール名を変数名の接頭辞にするのが慣例なので、
タスクを別のロールに移動するとか考えるとリファクタリングコストがそこそこかかります。

同じミドルウェアのタスクでも分割したほうがいいタスクがある

例えば、MySQLのロールの中で

  • MySQLを起動するLinuxユーザー作成
  • インストール
  • プロジェクト用のデータベースの作成

をすべて一つのロールでやらないほうがいいかもしれないです。
Playbookにした時に、ユーザーの作成とインストールまで行って、
何か別のタスクを実行した後に、データベースの作成という流れにしたいときに困ります。

共通で使う変数

各ロールやPlaybookで共通で使用したい変数名の命名規則は最初に決めておいたほうがいいです
後から変更するとそこそこリファクタリングコストがかかります。

部分実行しやすいようにする

Playbookの開発中に毎回毎回すべてのタスクを実行していては効率が悪いです。
--start-at-task--tags を使えるようにしておくと再実行しやすいので時短につながります。
なので、タスクやロールにtagはつけておいたほうがいいです。
上記オプションをつけて実行するとき、pre_taskは実行されないっぽいので注意が必要です。

冪等性をどこまで担保するか

なんでもかんでも冪等性を担保しようとすると辛い場面が出てきます。

ドライラン対策

確認のために -C をつけて実行することがあると思います。
しかし、一部のモジュール(commandやshell)はこのドライランに対応していないです。
冪等性を担保するなら自分でどうにかする必要があります。

不要になったタスクをどこまで面倒みるか

例えば、Vagrantで各自の開発環境を用意するとして、そこに流すPlaybookの場合を想定します。
前のバージョンでは作っていたけど、いらなくなったディレクトリが出てきたとき、次のバージョンで削除するタスクを書いておくのか?
という問題が出てきます。
前のバージョンを実行していたメンバーが次のバージョンを実行したときに
そのいらなくなったディレクトリを削除するためだけに タスクを残しておくと本来必要のないコードが残っている状態になります。
コードの見通しが悪くなるので、タスク自体消したほうがいいですが、いつ消すかはちゃんと考えておかないとメンバーに迷惑がかかることもあります。

モジュール一覧を一通り眺めてみる

意外と細かいことをやってくれるモジュールもあるのでcommandモジュール使う前に、
Ansibleのドキュメントのモジュール一覧を見てみるとよいです。
apt_key とか apt_repository とかcommandでやりがちなモジュールもあったりします。

Vagrant環境での注意

ディレクトリのオーナー、パーミッション

Ansibleの問題ではなくVagrant(VirtualBox)の使い方の問題です。
ホストOSのパーミッションはゲストOSでは変えられないことを念頭に置きましょう。

ディレクトリのオーナーやパーミッションを変えるようなタスクを作るとき
ホストOSとの共有ディレクトリは、Vagrant側から流したPlaybookでは変更できません。
Ansibleの実行結果はchengedとなりますが、実際には変わりません。

個人開発環境を作るときなんかは、ソースは共有ディレクトリに置くことが多いと思うので
その辺意識しておかないと、なんで変わってないんだとなってはまってしまうので注意です。

例えば、ソースを置くディレクトリは、 wwwをオーナーにしたい場合を考えます。 Vagrantfileでディレクトリ共有の設定でwwwを指定すれば、ゲストOS側でそのオーナーになります。 しかし、そこに指定するユーザーは、Playbook流す前は存在しない場合、
1つのPlaybookだけで解決しようとするとなかなか難しい状況に遭遇します。
ユーザーやグループの追加はプレイブック分けたほうがいいかもしれません。 vagrant up だけで環境整うと嬉しいけど、そこまでいくにはいろいろ考えないといけないことが多いです。

saharaを使う

Playbookの開発を行うときにVagrantを使うなら sahara というプラグインを使うとよいです。
Playbookを流しては、失敗して、修正し、もう一度流す。みたいなことを繰り返します。
ある程度動作が固まってきたときに、ゲストOSをまっさらな状態にしてから流し直すことをやると思います。 そんなときは sahara が便利です。
他にも同じようなことができるプラグインはありますが、
ゲストOSの状態をバージョン管理したいとかでなければsahara で十分です。

Ansibleの流し方を考える

Vagrantでは、ゲストOSに対してAnsibleを実行する方法が幾つかあります。
Vagrantのプロビジョナを使う場合は、Shell, Ansible, Ansible Local のどれかを使うことになるでしょう。
どれがいいかはプロジェクト次第だと思うので、一概に言えません。

私は、 Shell を使用して、Ansibleのインストールのみを行い、
Playbookの実行はVagrantに入って、手動でやる方法を取りました。
Playbookの開発中は何度も実行するし、どう変わったかの確認もするため、結局sshするからです。
そもそも開発メンバーの中に、Windowsの人がいたので、Ansibleは選択できませんでした。

テストコード

Ansibleに限った話ではないですが、考えておいたほうがいいです。
Ansibleのタスクに対してテストコードを書く場合、今ならServerSpecを選択することが多いと思います。
そこまで大きなプロジェクトでないなら、書かないほうがコストやすいことのほうが多いかもしれないと感じました。
書くなら書き始める時期もちゃんと検討しましょう。

実行ユーザー

  • remote_user
  • become_user

はちゃんと運用時のことを想定して考えておきましょう。
Vagrant環境だとvagrantだけでいろいろいけちゃいますが、Vagrant以外の環境のこともちゃんと考えないと後が大変です。

他のサーバーにsshするとき(git cloneなど)にどの秘密鍵を使うのか、
その秘密鍵はどうやって管理するのか
はたまた、エージェント転送で解決していくのかも先にある程度考えておくといいと思います。

becomeの単位

Playbook単位でbecome: Trueとするかタスク単位で切り替えて行くのかは
上記実行ユーザーと一緒に考えておきましょう。

変数の分割

apache_document_root:
  name: www
  owner: www
  group: www
  mode: 0755

みたいなハッシュが defaults にあったときに apache_document_root.name だけ上書きしたい時に困ります。 Ansibleはデフォルトでハッシュを置換するので、別のところで

apache_document_root:
  name: foo

とかできないです。 ansible.cfg で置換ではなくマージに変えることもできますが、
全体的な影響(自分が中身をよく知らないロールとか)を考えたときに、他は大丈夫なのかという問題が出てきます。
あと、Ansibleをよく知らないメンバーが、マージがデフォルトだと思ってしまうこともあるかもしれません。

ansible-galaxyで管理するロールの配置場所

これは正解が分かりませんでした。
前提として下記の条件があったとします。

  • プロジェクトごとにPlaybookを管理するリポジトリを持つ
  • プロジェクト特有のロールは、このリポジトリに入れる
  • Ansibleを実行するサーバーは、共有で複数のプロジェクトがそこから実行する

デフォルトだと、システムグローバルに置かれますが、それはしたくないです。
それぞれのプロジェクトで、同じロールの違うバージョンを使う可能性があるからです。
require.yml で、個別の名前をつければ対応できると思いますが、しっくりこないです。

結局、下記のような運用にしました。

  • ansible.cfg の中でインストール場所に ./roles を指定する
  • require.ymlの中で、インストールするの名前を role-xxx というような命名規則にする
  • .gitignoreroles/role-* 追加して、galaxyで管理しているロールは除外する

Ansibleのソースを読む

Ansibleのエラーメッセージはあまり親切ではないので、よく分からないことが多いです。
どうにもならないエラーに出会ったら、Ansibleのソースを読んでみることをおすすめします。
homebrewで入れたなら下記辺りに入っているはずです。

/usr/local/Cellar/ansible/2.1.0.0/libexec/lib/python2.7/site-packages/ansible/modules

ここに coreextras というディレクトリがあり、それぞれの中にカテゴライズされたモジュールがあります。
モジュールが coreextras のどっちに属するかは公式ドキュメントに書いてあります。

例えば、gitモジュールだったら、ここの下の方に This is a Core Module と記載されています。
もしくは、モジュール一覧で (E) がついていれば、 extras 扱い
gitモジュールは Source Control Modules にカテゴライズされているので

/usr/local/Cellar/ansible/2.1.0.0/libexec/lib/python2.7/site-packages/ansible/modules/core/source_control/git.py

がソースになります。

Tips

jinja2のフィルターを活用する

例えば、標準出力の整形には from_jsonfrom_ini が結構便利です。
自分で冪等性を担保したいときなど、一旦設定を情報などを確認すると思うのですが、
標準出力に出た内容をAnsible(jinja2)で使えるハッシュに直すときなどによく使いました。

group_varsはイベントリファイルの書き方で意図しない動きになる

  • group_vars
    • web.yml
    • db.yml
    • batch.yml

というような構成になっているときにそれぞれで、 common_main_user という変数を定義しているとします。
かつ、hosts(イベントリファイル)の中で、下記のようにすべて同じものを指している場合

[web]
localhost

[db]
localhost

[batch]
localhost

ansible-playook -i hosts db.yml と実行しても、 多分、期待通りに動かないでしょう。
common_main_user の中身が db.yml に指定したものとは限らないからです。
group_vars の中を探しに行く際に、 マシン名をベースに、最初に見つかったグループを取得するようです。
この例だと、多分、 batch.yml に指定したものが入ってくると思います。
Vagrantで開発しているときなどは、全部を localhost などにしがちですが、これをやるとハマることがあります。
意図したとおりに動かしたい場合、
開発中なら一旦他のグループのlocalhostコメントアウトしておくとか、xip.ioを使うとかしたほうがいいと思います。

blockinfile便利

tipsでもなんでもないですが、よく存在を忘れるのでメモ。
blockinfile モジュールを使うとファイルへの追記などが楽になります。
使うときは marker を指定しておいたほうが無難です。

LinuxユーザーのIDの固定はそれなりに考える必要がある

データベースサーバーでマスタースレーブ構成を取りたいときは、サーバーを複数台用意すると思います。
その時、それぞれのサーバーでユーザーのIDを固定化しておきたいという要望があったとします。
userモジュールでID指定をすれば固定できますが、よく考えてから固定化するしたほうがいいです。

sudo apt-get install mysql-server などとパッケージ管理からインストールすると
mysqlのユーザー、グループも一緒に作られます。そして、mysqlのサービスも起動された状態になります。
ユーザーのIDを変更するには、そのユーザーでサービスが起動していてはいけないので、一旦サービスを止める必要があります。
なので、例えばこんな感じのタスクになる。

- apt: name=mysql-server state=present
- service: name=mysql-server state=stopped
- user: name=mysql uid=9999

本番環境でこんなのは流せないです。
先にユーザーを作ってしまうという手もありますが、
例えば、redis-serverは、redisというユーザーがすでに存在するとaptによるインストールが失敗します。
なので、ユーザーのIDの固定化はタスクとかロールとかがごちゃごちゃしがちになるので、本当に必要かどうか考えたほうがいいです。

Ansibleはshellでコマンドを流しているわけではない

Ansibleを触り始めた頃に思い込みをしていたので、メモとして残しておきます。
Ansibleはsshで他のサーバーに接続してタスクを実行していきますが、各タスクのshellのコマンドを実行するわけではないです。
例えば、gitのクローンは git clone git@github.com/foo/bar.git をshell上で実行しているのではなく、上記を行うpythonスクリプトを送り込んで、そのスクリプトを実行しています。
エラーが起こったときなど、実際どのようなコマンドが発行されたかをつい知りたくなりますが、
コマンド履歴などに残っているようなshellのコマンドを探そうとしても見つかりません。