ちょっと話題の記事

Ansibleのテスト事情

2015.11.17

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

テストしてますか?

渡辺です。「進捗どうですか?」のダメージは計り知れないことはご存じかと思いますが、「テストしてますか?」のダメージも侮れません。 前者はほぼ全てのエンジニアに有効なアタックですが、後者はそれなりの経験を積んだエンジニアにしか有効でない点は異なりますね...。

さて、Ansibleを運用してくるとなれば、どうしても「どうやってテストをすべきか?」という問題にぶち当たります。 そこで、Ansibleを運用する中でのテストの考え方についてまとめておきます。

Ansibleの考え方とテスト

Ansibleのような宣言型の構成管理ツールが登場するまで、サーバ構築の自動化といえばセットアップスクリプトの実行でした(自動化されていない場合は、ひとつひとつコマンドを打ち込んでいたでしょう)。 例えば、CloudFormationのcfn-initのUserDataでは次のようなセットアップスクリプトを実施してサーバの初期状態を設定します(CloudFormationでは宣言型の設定も可能ですが、高機能とは言えません)。

# yum -y install apache24
# chkconfig httpd on
# service httpd start

これはサーバの初回起動時に手順にしたがった処理を行い、結果としてあるべき状態に設定していると言えます。

一方、宣言型の構成管理ツールであるAnsibleのリソースはdesired-stateモデルとして定義されます。 すなわち、次のようにYAMLにリソースの状態を宣言します。

tasks: 
  - yum: name=apache24 state
  - service: name=httpd state=started enabled=yes

この構成ファイルでAnsibleを実行して成功すれば、サーバのリソースが宣言された状態にセットアップされるのです。

重要なのは「成功すれば」という部分です。 Ansibleでは、宣言のサーバの状態を設定できなかった段階で実行が失敗する「fail-fast」設計となっています。 言い換えれば、Ansibleの実行に成功したならば、宣言通りにサーバの状態がセットアップされていることが保証されることになります。

テストとは、「期待される結果(状態)となるか」を検証することです。 Ansible自体が宣言的に状態を定義し、その状態となっていることを保証しているので、あらためてテストを実施しなくても良い、というのがAnsibleの思想となります。

不安をテストする

Ansibleでは宣言的にサーバの状態を定義し、実行に成功すれば宣言した構成でサーバがセットアップされるためテストは必要ない、というのがAnsibleの思想でした。 それでは本当にテストは必要ないのでしょうか?

TDD(テスト駆動開発)の世界では、テストを行う上で重要な金言があります。 それは「不安をテストする」という言葉です。 不安に思わないことはテストする必要が無く、プログラマが不安に思うことをテストするべきという教えです。 これは、Ansibleなど構成管理ツールを利用する場合も同様に当てはまることだと考えます。

納品する環境が期待通りの環境か?

顧客として、エンジニアとして、一番の不安は、構築された環境が期待される構成となっているかということには異論はないでしょう。 納品する環境が期待通りにセットアップされていることをテストすることは重要なことです。

共有Roleが正しく動作するか?

弊社では多くのAWS環境を構築する必要があります。 当然ながら案件によってミドルウェアなどの要件は異なりますが、共通する設定も多くあります。 例えば、システムのタイムゾーンや言語設定・CloudWatch Logsの設定といった部分です。 こういった各環境で共通化できる部分は、ライブラリとして抽出し、再利用することで、品質の向上と作業の効率化が実現できることは言うまでもありません。 しかしながら、共通化すると「本当にそのRoleは期待通りに動作するのか?」という不安が生まれます。 テストが必要です。

複数の環境で動作するか?

案件固有のPlaybookであれば、AmazonLinux専用としても大きな問題はありません。 AmazonLinux以外で動作する予定がないのであれば、対応することに意味がないからです。 しかし、共通のRoleであれば、対応する環境は多い事はメリットです。 一方、複数の環境に適用すればするほどRoleは複雑になります。 不安ですのでテストが必要です。

OSアップデート時に動作するか?

AmazonLinuxの場合、数ヶ月のサイクルで新しいAMIがリリースされ、基本となるバージョンが変わっていきます。 その時、既存のPlaybookやRoleは正しく実行できるか保証が難しいでしょう。 「X月のAMIでは動いた」では不安なのです。 テストを自動化し、新しいOSでも動くことを検証すると効果的です。

複雑な手順の結果、期待通りの状態となるか?

Ansibleでは宣言的な記述でリソースを定義しますが、個々のコマンドを実行することも可能です。 しかしながら、コマンドは手続きであるため、宣言的記述と、非常に相性が悪いです。 例えば、次のようなタスクは、常にchangedとなり、Ansibleとしてはサーバの状態を変更したとみなします。

- tasks:
  - command: aws s3 cp s3://{{ codedeploy_bucket_name }}/latest/install /tmp --region {{ codedeploy_region_name }}

実際、なんらかのミドルウェアをインストールする場合にインストールスクリプトを実施することはよくあります。 次のタスクは、codedeploy-agentをインストールするRoleの一部です。

- name: install ruby
  yum: name=ruby
- name: dowanload codedeploy-agent installer
  command: aws s3 cp s3://{{ codedeploy_bucket_name }}/latest/install /tmp --region {{ codedeploy_region_name }}
- command: chmod +x /tmp/install
- command: ruby /tmp/install auto
- name: start codedeploy-agent service
  service: name=codedeploy-agent state=started enabled=yes

rubyが必要であったり、comandでインストールをしていたりしますが、最終的にcodedeploy-agentがインストールされてサービスとして動いていることが期待されることでしょう。

このように、ミドルウェアのインストールに必要なライブラリなどをインストールする必要がありますが、それらはインストールの過程(手順)でしかありません。 最終的にはミドルウェアがインストールされていることが検証できれば不安はなくなります。

どのようにテストすべきか?

様々なケースで「不安」を感じるとき、あなたはテストすべきです。 テストが必要ならば、どのようにテストするかを検討し、選択しなければなりません。 現在、Ansible周りのテストとしては、手動テスト・serverspec・Ansibleのassertモジュール・ansiblespecあたりが選択肢として考えられます。 結論からすれば、手動テストとserverspecを利用すると良いと思います。

手動テスト

手動テストとは、セットアップしたサーバにSSHなどでログインし、各種コマンドを実行しながらサーバの状態を検証することです。 なにをいまさらと考えるかもしれませんが、はじめにAnsibleで環境を構築した後はAnsibleの管理から離れる場合には一番有効なテスト方法です。

serverspecなどを利用するテストは自動テストと呼ばれますが、自動テストの有効性は、プログラムとして何度でも繰り返し実行出来る点にあります。 ワンショットで構築し、後は自由にカスタマイズするのであれば、テストを繰り返して実行することはありません。 一般的に、テストを1回だけ実行するよりも、テストコードを書くコストは大きくなります。

手動テストのポイントとしては、それぞれの項目について、どうやって確認するかについて共通認識を持たなければならない点でしょう。

serverspec

serverspecはRuby製のテストツールで、サーバの状態を宣言的に記述し、sshログインによって検証を行います。 Ansibleとよく似たアーキテクチャで、serverspecでは環境の検証のみを行うことになります。

Ansibleで宣言的に書いている上で、さらにserverspecで宣言的に書くことに意味がないようにも思えます。 例えば、Apacheのインストールでは、それぞれ次のような宣言になります。

tasks: 
  - yum: name=apache24 state
  - service: name=httpd state=started enabled=yes
describe service('httpd') do
  it { should be_running }
  it { should be_enabled }
end

基本的に、Ansible(YAML)形式かRuby(RSpec)形式かの違いでしかないでしょう。 言い換えれば、Ansibleはテスト不要とも言えるワケです。

一方、システムの言語設定Roleでの宣言は次のようになります。

---
- name: create synbolic link to /etc/localtime (AmazonLinux)
  file: src=/usr/share/zoneinfo/{{ timezone }}  dest=/etc/localtime state=link force=yes

- name: set ZONE={{ timezone }} in /etc/sysconfig/clock (AmazonLinux)
  lineinfile: dest=/etc/sysconfig/clock regexp=^ZONE= line='ZONE={{ timezone }}' state=presen
describe command('date') do
  its(:stdout) { should match /JST/ }
end

歴史的な経緯からCentOS6系のタイムゾーン設定は面倒な手続きが必要です。 一方、検証としてはdateコマンドでJSTが含まれていれば良いことになります。

Ansibleを実行する場合、どうしても手続き的な処理が含まれることは避けられません。 このため、Ansibleで泥臭い処理をしている部分は、serverspecで定義することに意味があります。

もちろん、Ansibleとは異なるシステムで再チェックするという意味も大きいでしょう。

その他のテスト方法

今回は触れませんが、Ansible自体にもassertの仕組みがあります。 しかしながら、記述が複雑になるだけでなく、最終的にはverify.shのようなスクリプトを実行して結果を見る形に落ち着きます。 そのverify.shスクリプトを書くくらいならseverspecを書きましょう。

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

まとめ

Ansibleは基本的にはテスト不要のアーキテクチャです。 これは、Ansibleが宣言的にリソースを定義し、fail-fast設計となっているからです。 しかしながら、内部に泥臭い手続き的な処理を含めることは避けられません。 不安に思う部分があれば、必ずテストしてください。 また、繰り返し実行して有効なテストであればserverspecなどでテストを自動化しましょう。