AnsibleSpecを複数環境プロビジョニングに対応させてOSSに初Contributeした

この度、AnsibleSpecにコントリビュートして「複数環境プロビジョニング」のユースケースで便利にテストを実行できるようにしました。

github.com

私がやろうとした時に目の前にあった前提

最初に、私がこの対応をするにあたって目の前にあった前提をお伝えしておきます。

プロビジョン完了時に合わせてインフラの状態をテストしておきたかった理由としては、こんな感じです。

  • 本番・検証など多くの環境ができてくるとインフラテストがやりきれない
  • インフラテストを自動化してインクリメンタルに作業を進めたい
  • よりよいプラクティスを追求したいという想い

その他、下記が決めてあったことです。

Ansibleのディレクトリ構成ベストプラクティスを採用

TerraformやCloudFormationなどで環境群自動構築のアプローチを採用

  • 環境がProduciton、Staging、develop、demo、などセットをたくさん作る事を想定
    • Immutable Infrastructureを意識し環境を使い捨て・切り替えしていく
  • プライベートIPアドレスは任意のアドレスが割り当てられている
    • Ansibleのプロビジョンコードをコミットする時点ではhostsは予測不能

各環境には基本的にSSHしない

  • 運用的なセキュリティ強度を高める為にSSHをしなくても済むようにしたい
    • 必要なログ情報は全てログコレクタを使って転送
      • 障害やアラートの検索はElasticsearchやCloudWatch logsなどで実施

AnsibleとAnsiblespecの組み合わせを選択

「Ansibleのテスト事情」も参考にさせて頂き、結果的にAnsible標準テスト方法ではなく、Serverspecを使う事にしました。左記のエントリーでは好みの問題でAnsibleSpecは避けられていましたが、最近「Dynamic Inventory」に対応された事もあり、統合的に使いたかったことも相まって、私はAnsibleSpecを選びました。

話しが少しそれますが、「Ansibleのテスト事情」エントリーで下記が言及されていました。

また、serverspecをrole単位で実行できるansiblespecという拡張モジュールもあります。 こちらも使ってみましたが、テスト単位がrole単位というのが好みに合いませんでした。 構成管理ツールは上から下まで流してナンボだと思うので、playbook単位に行うべきだと思います。 プログラムでいえばユニットテスト用で扱い難く、severspecでインテグレーションテストをすべきという考え方です。

これが示す意味としてはtest all的なものがないということかなと。私もplaybook単位で実行できる方が便利だと感じたので、デフォルトのRakefileでallというrakeタスクを追加し、"rake all"または"rake serverspec:all"でプロビジョニング対象になったタスク全部を一括で流せる形にしました。

この修正は、まだ現時点ではRubygemsとしてはまだリリースされていませんが、一緒に取り込んでもらえました。きっと、次のバージョンに含まれる事になると思います。

パッチが必要になった機能

AnsibleSpecの幾つかの機能がまだ未実装で自前でパッチを当てる必要がありました。

  • AnsibleSpecではHostsに対して複数のホストを指定されたPlaybookに対応していない
  • AnsibleではgroupのChildren指定で依存関係を書けばgroup_varsも依存を辿って参照してくれるがAnsibleSpecでは未対応

この2点は複数環境プロビジョニングにとって重要な要素でしたのでパッチをあてることにしました。パッチもAnsibleSpecを追従していくのはきついし、後からわかった事なのですが、GitHubを見るとアイディア・要望としてchildrenの依存でgroup_varsを参照したいというのを幾つかみたので、思い切ってプルリクすることに決めました。

プルリクするために自分以外のInventoryやhostsファイルの書き方でも正常に動く必要があるので、AnsibleSpecライブラリ自体のテストコードを修正したり、新しくテストを追記したりしました。

実例

実際にはAWS CLIなどを使って命名規則や条件に従ってhostsのアドレスを取得し埋め込み、JSON文字列を生成する必要があります。また、実行時に環境変数に環境名を置き換えれるようなスクリプトにすれば、環境名は容易に変更する事ができますね!

dynamic_inventory.sh

#!/bin/bash
cat << EOS
{
  "database-servers": {
    "hosts": [
      "10.0.0.4"
    ]
  },
  "application-servers": {
    "hosts": [
      "10.0.0.2",
      "10.0.0.3"
    ]
  },
  "web-servers": {
    "hosts": [
      "10.0.0.1"
    ]
  },
  "staging-database-servers": {
    "children": [
      "database-servers"
    ]
  },
  "staging-application-servers": {
    "children": [
      "application-servers"
    ]
  },
  "staging-web-servers": {
    "children": [
      "web-servers"
    ]
  },
  "staging": {
    "children": [
      "staging-web-servers",
      "staging-application-servers",
      "staging-database-servers"
    ]
  }
}
EOS

group_varsは環境やグループ(サーバー群)毎に設定ファイルが配置します。

  • all
    • 環境に左右されない全てのホストに設定したい内容を記述
  • staging(productionやdevelopなど環境名)
    • 環境ごとに切り替えたい内容を記述
    • サーバー群に依存しない共通設定を書く事を想定
    • confファイルを環境毎にansilbe templateとして切り替えたいなど
  • staging-web-servers(環境名+グループ名)
    • サーバー群単位で環境毎に切り替えたい内容を記述
    • 最新バージョンを試す場合などのversion番号など
  • web-servers(グループ名)
    • サーバー群単位で環境に左右されない内容を記述
    • mysqlのport番号など
$ls ./
.ansiblespec dynamic_inventory.sh* group_vars/ Rakefile roles/ site.yml spec/
$ls ./group_vars
all                            production-application-servers
application-servers            production-database-servers
database-servers               production-web-servers
develop                        staging
develop-application-servers    staging-application-servers
develop-database-servers       staging-database-servers
develop-web-servers            staging-web-servers
production                     web-servers

playbookでは下記のような書き方をします。

インベントリファイルで依存関係を指定してあるので、ホスト毎にgroup_varsが参照されます。 例えば、web-serversが参照するgroup_varsは「all, staging, staging-web-servers, web-servers」です。

site.yml

- name: install-common-libs
  hosts:
    - web-servers
    - database-servers
    - application-servers
  roles:
    - common

- name: install-nginx
  hosts:
    - web-servers
  roles:
     - nginx

- name: install-java
  hosts:
    - application-servers
  roles:
    - java

- name: install-mysql
  hosts:
    - database-servers
  roles:
    - mysql

うれしかった・よかったこと

  • そっこー返事くれて・コードをレビューしてくれテストケースの抜けが1つあったことを指摘してくれた
  • あまり自信がなかったが快くマージしてもらえた
  • AnsibleSpecが裏でどんな事をやっているかわかった

気になっていること

他の方はどんな風にやっているのか?

実は当記事が、来月に行われる Ansible Meetup in Tokyo 2016.06のLT発表予定のtakuya_onda_3さんによる「Ansibleとterraformで実現するタグベース複数環境プロビジョニング実例」とすごく内容かぶっている気がします。

残念ながら申し込みをしようとした時にはLTも応募枠いっぱいで、抽選にもはずれてしまったのでお話を聞く事はできませんが、どうやってテストをされているか興味があります!

変数値は共有すべきか?

オライリー出版の「Serverspec」という書籍があり、「5.4 サーバ構成管理ツール」の項で、このようにServerspec作者は述べられています。

 また、Node Attributes を使ってそのままテストすると、もし Node Attributesを間違った値に書き換えてしまっていても、Serverspecではそれに気づくことはできません。Serverspecの目的の1つは、Chef レシピ等のインフラコードが正しく書かれているかテストすることです。したたがって、Node Attributesを利用してテストを行うと、このようなミスにきづくことができず、本来の目的を果たせなくなってしまいます。

TDDでいう先にテストコードを書いて、テスト失敗で気づくというプラクティスに似ているのかもと。変数値を変数値を共用してしまうとその気づきが失われ、意図しないヒューマンエラーで修正に洩れや間違いがあった場合、実際の環境で問題がおきてから気づくことは、この項を読んで色々と思考を巡らせるきっかけになりました。

今回このような仕組みをAnsible+AnsibleSpecで実現させてはみたものの、上記のようなケースもあり、完璧に自動化・不用意なミスを防げたとは言い切れないなと思っています。ペアワークのレビューである程度失敗は防げますが、そもそも完璧というものを求めてしまうのが過ちなのかもしれません。

私自身、なぜImmutable Infrastructureにするのかの深ぼり、担保したい事やどういう状況にもっていきたいことは何か、運用経験も加味しながら考えていく必要があると感じました。

最後に

今回はすごくいい経験になり楽しかった...

考える事は幾つかあるものの、導入してみて開発・テストのサイクルがすごくスムーズになりましたし、導入の敷居も低いこともあって、AnsibleSpecはお勧めだと感じました! Serverspec自体を使ったことない人にもぜひ使ってみて頂ければと思います。