kitchen-ansiblepushを利用したAnsible roleのテスト環境構築

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

はじめに

こんにちは、中山です。

最近Ansibleのplaybookをテストするための環境作りをしています。いろいろなツールを検証しているのですが、Test Kitchenを利用する機会がありました。Test Kitchenのprovisionerにはさまざまなプラグインがあるのですが、kitchen-ansiblepushというプラグインがとても便利なのに、あまりブログなどで解説されていないようなので今回ご紹介したいと思います。

Test KitchenでAnsibleを利用する従来の方法

kitchen-ansiblepushの紹介の前に、Test KitchenでAnsibleを利用する従来の一般的な方法について説明します。

今まで多くの方はkitchen-ansibleというprovisionerプラグインを利用されている方が多かったのではないでしょうか。「test kitchen ansible」などでググるとたいていこのプラグインを利用した例が表示されます。私も当初このプラグインを利用しようと考えていました。とても便利なプラグインではあるのですが、いくつか私の環境ではあまり向かない挙動をすることが分かりました。

kitchen-ansibleは以下のような挙動をします。

  1. テスト対象のインスタンスにAnsibleをインストール
  2. テスト対象のインスタンスにAnsibleのroleをコピー
  3. コピーしたroleをlocal connectionで実行

以下にそれぞれの処理内容についてその問題点を記述します。

「テスト対象のインスタンスにAnsibleをインストール」

これは対象インスタンス上でyumなどのパッケージマネージャを利用してAnsibleをインストールさせる処理です。インストールの方法はいくつかオプションで変更できるのですが、例えば ansible_omnibus_url を利用した例ではこちらのシェルスクリプトを対象インスタンス上で実行させてAnsibleをインストールしています(もちろん実行するシェルスクリプトを変更させることは可能です)。

この処理には以下の問題点があると考えています。

  1. パッケージのインストールに時間が掛かる
  2. インスタンスの種類によってはAnsibleのインストールがうまくいかない
  3. インスタンス毎の違いを考慮したスクリプトを作成する必要がある

まず「パッケージのインストールに時間が掛かる」について。見ていただくと分かるかと思うのですが、さまざまなパッケージをインストールしています。Test Kitchenで立ち上げるインスタンスは基本一時的に利用するだけなので、パッケージをインストールすること自体は環境によって問題にならない場合もあるかもしれません。しかし、インスタンスを作成する度にこの処理が実行されるとインスタンスの立ち上げに時間が掛かってしまうという問題があります。

続いて「インスタンスの種類によってはAnsibleのインストールがうまくいかない」について。私は今回Test Kitchenのdriverにkitchen-ec2を利用しています。Amazon Linuxの場合はデフォルトでpipがインストールされているので簡単にAnsibleのインストールが可能です。しかし他のディストリビューション(UbuntuやRHELなど)の場合、pipがデフォルトで入ってないので意外と難しいという印象がありました。Ansibleのバージョンやインストール方法を統一した方が無駄なエラーに悩まされることが少ないので、なるべくpipでインストールした方がよいと私は考えています。こういった事情があるのでこの部分は私にとって導入のネックだと思いました。

最後に「インスタンス毎の違いを考慮したスクリプトを作成する必要がある」について。先程のスクリプトはRed Hat系/Debian系に対応させるために分岐の処理が入っていました。Test Kitchenでテストをする場合多くのインスタンス上でテストを実行させたいので、さまざまなディストリビューションを利用すると思います。そういった場合、ディストリビューション毎の違いをスクリプトで抽象化しようとすると、スクリプトの管理が大変になるという問題があります。

Ansibleの利点はplaybook適用ホストに対してほぼ何もインストールする必要がないという点にも関わらず、この恩恵を受けられないのは導入を検討する際のネックになると感じました。

「テスト対象のインスタンスにAnsibleのroleをコピー」

後ほど説明しますが、kitchen-ansibleはAnsibleをテスト対象上で実行させるので、利用するroleを対象インスタンス上にTest Kitchen実行ホストからコピーさせる必要があります。Ansible自体もSSH connectionで実行させる場合、対象インスタンス上にスクリプトをコピーさせる処理がありますが、これはあくまでモジュールをコピーしているだけなので少し挙動が異なります。

この処理には以下の問題点があると考えています。

  1. roleのコピーに時間が掛かる
  2. roleのdependenciesを利用している場合コピーするroleが多くなる

「roleのコピーに時間が掛かる」について。これはそのまま時間が掛るのでテストの実行時間に影響を及ぼす結果になります。

続いて「roleのdependenciesを利用している場合コピーするroleが多くなる」について。Ansibleにはdependenciesという機能で、あるroleに依存するroleを指定することができます。この機能を利用したroleをkitchen-ansibleでコピーさせる場合、コピーする必要のあるroleが多くなるのでそれだけコピー処理に時間がかかります。また、roleのディレクトリ構造が複雑になっている場合、roleのパス指定が難しいという印象が私にはありました。

「コピーしたroleをlocal connectionで実行」

Ansibleのインストール/roleのコピーが完了すると、いよいよAnsibleを実行させます。kitchen-ansibleではconnectionの方法を ansible_connection で変更させることができますが、デフォルトの挙動はlocalです。SSHで対象インスタンスにログイン後ansible-playbookコマンドを -c local で実行させています。

Ansibleを利用する場合、多くの方はSSH connectionを利用していると思います。私もそうです。テスト環境はなるべく本番と近い環境で行うことが必要だと考えています。connectionの違いによってplaybookの結果が変わることはあまりないかもしれませんが、本番と異なるconnectionで接続するという挙動はあまりよい印象を持ちませんでした。

ちなみに ansible_connection では ssh を指定することもできます。 ssh にした場合どういった動作をするかというと、対象インスタンスにSSHでログイン後 -c ssh でansible-playbookコマンドを実行させていました。つまり、自分自身にSSHでログイン後Ansibleを実行させています。確かに本番と近い環境でAnsibleを実行できるというメリットはありますが、冗長な処理をしているなという印象でした。

以上kitchen-ansibleの問題点を記述してきましたが、あくまで私の環境に合わなかったというだけです。とても便利なプラグインなので上記内容が特に問題とならない場合は利用する価値があると考えています。念のため。

kitchen-ansiblepushの挙動

READMEにズバリと書かれています。

This kitchen plugin adds ansible as a provisioner in push mode. Ansible will run from your host rather than run from guest machines.

つまり、テスト対象インスタンス上ではなくTest Kitchenを実行するホスト上でAnsibleを実行してくれます。やりましたね。この挙動は以下のメリットを提供してくれます。

  1. テスト対象インスタンス上にAnsibleをインストールしなくて済む
  2. 本番環境と同じ状況でAnsibleのテストが実行できる

要するにテスト実行時間の短縮と、理想的なテスト環境の構築が期待できるという訳です。

kitchen-ansiblepushの使い方

サンプルとなるコードをGitHubに上げておきました

driverにkitchen-ec2を指定してAWS上にAmazon Linuxを起動させます。AnsibleではNginxをインストールさせ、ServerspecでNginxがインストールされているかテストします。

ディレクトリ構造

以下の通りです。

kitchen-ansiblepush-demo
├── .envrc.sample
├── .gitignore
├── .kitchen.yml
├── Gemfile
├── Gemfile.lock
├── README.md
├── all.yml
├── ansible.cfg
├── requirements.txt
├── roles
│   └── nginx
│       └── tasks
│           ├── main.yml
│           └── nginx.yml
├── site.yml
└── spec
    ├── nginx
    │   └── nginx_spec.rb
    └── spec_helper.rb

ツールのインストール

リポジトリのclone後以下のコマンドを実行して各種ツールをインストールしてください。

$ git clone https://github.com/knakayama/kitchen-ansiblepush-demo.git
$ cd kitchen-ansiblepush-demo
$ pip install -r requirements.txt
$ bundle install --path vendor/bundle

環境変数の設定

.kitchen.ymlAWS_SSH_KEY_ID を参照してkeypairの指定をしているのでdirenvなどを利用して環境変数を設定してください。

Test Kitchenの実行

テスト対象インスタンスの作成/Ansibleの実行/Serverspecのテストまで一気に実行する場合は以下のコマンドを実行してください。

$ bundle exec kitchen test
-----> Starting Kitchen (v1.8.0)
-----> Cleaning up any prior instances of <default-AmazonLinux-2016031>
-----> Destroying <default-AmazonLinux-2016031>...
       EC2 instance <i-********> destroyed.
       Finished destroying <default-AmazonLinux-2016031> (0m1.39s).
-----> Testing <default-AmazonLinux-2016031>
-----> Creating <default-AmazonLinux-2016031>...
       If you are not using an account that qualifies under the AWS
free-tier, you may be charged to run these suites. The charge
should be minimal, but neither Test Kitchen nor its maintainers
are responsible for your incurred costs.

       Instance <i-********> requested.
       Polling AWS for existence, attempt 0...
       Attempting to tag the instance, 0 retries
       EC2 instance <i-********> created.
       Waited 0/300s for instance <i-********> to become ready.
       Waited 5/300s for instance <i-********> to become ready.
       Waited 10/300s for instance <i-********> to become ready.
       Waited 15/300s for instance <i-********> to become ready.
       Waited 20/300s for instance <i-********> to become ready.
       Waited 25/300s for instance <i-********> to become ready.
       EC2 instance <i-********> ready.
       Waiting for SSH service on **.***.***.***:22, retrying in 3 seconds
       Waiting for SSH service on **.***.***.***:22, retrying in 3 seconds
       Waiting for SSH service on **.***.***.***:22, retrying in 3 seconds
       Waiting for SSH service on **.***.***.***:22, retrying in 3 seconds
       Waiting for SSH service on **.***.***.***:22, retrying in 3 seconds
       [SSH] Established
       Finished creating <default-AmazonLinux-2016031> (1m14.45s).
-----> Converging <default-AmazonLinux-2016031>...
       Preparing files for transfer
       *************** AnsiblePush install_command ***************
       Ansible push config validated
       Transferring files to <default-AmazonLinux-2016031>
       *************** AnsiblePush run ***************

PLAY [kitchen-ansiblepush demo] ************************************************

TASK [nginx : include] *********************************************************
included: /Users/knakayama/.ghq/github.com/knakayama/kitchen-ansiblepush-demo/roles/nginx/tasks/nginx.yml for AmazonLinux-2016031

TASK [nginx : Install nginx] ***************************************************
changed: [AmazonLinux-2016031]

TASK [nginx : Be sure nginx enabled and started] *******************************
changed: [AmazonLinux-2016031]

PLAY RECAP *********************************************************************
AmazonLinux-2016031        : ok=3    changed=2    unreachable=0    failed=0

       *************** AnsiblePush end run *******************
       Finished converging <default-AmazonLinux-2016031> (0m15.33s).
-----> Setting up <default-AmazonLinux-2016031>...
       Finished setting up <default-AmazonLinux-2016031> (0m0.00s).
-----> Verifying <default-AmazonLinux-2016031>...
       [Shell] Verify on instance=#<Kitchen::Instance:0x007fe74c04ff98> with state={:server_id=>"i-********", :hostname=>"**.***.***.***", :last_action=>"setup"}

Package "nginx"
  should be installed

Service "nginx"
  should be enabled
  should be running

Port "80"
  should be listening

Finished in 8.23 seconds (files took 0.40603 seconds to load)
4 examples, 0 failures

       Finished verifying <default-AmazonLinux-2016031> (0m9.30s).
-----> Destroying <default-AmazonLinux-2016031>...
       EC2 instance <i-********> destroyed.
       Finished destroying <default-AmazonLinux-2016031> (0m1.21s).
       Finished testing <default-AmazonLinux-2016031> (1m41.69s).
-----> Kitchen is finished. (1m41.79s)

コードの解説

driverにkitchen-ec2、verifierにshellを利用しています。こちらについては参考リンクを参照してください。以下ではkitchen-ansiblepushの解説を行います。

.kitchen.ymlのprovisionerで以下の設定を行っています。

provisioner:
  name: ansible_push
  playbook: site.yml
  sudo: true
  chef_bootstrap_url: false

見たとおりの設定ではあるのですが、何点かハマリポイントがありました。

まず sudo オプションについて。これはansible-playbookコマンドに --sudo オプションを渡すかどうかの設定です(deprecatedなオプションですが後方互換性のために利用していると思われます)。当初 ansible.cfgbecome = True の設定を指定していたため不要だと思っていたのですが、kitchen-ec2ではohai用のファイルを設置する処理を行っています。この処理の sudo_commandprovisionerのsudoオプションが参照されるため指定する必要があります。また、sudo系オプションとbecome系オプションは排他的な関係のオプションなので ansible.cfg から become = True の設定を削除しました。

続いて chef_bootstrap_url について。これをfalseに設定しないとChefをインストールしてしまうようなので指定する必要があります。

基本的にTest KitchenはChefのテスト環境を構築するためのツールなので、Ansibleを利用する場合はこういったハマリポイントがあるようです。

まとめ

いかがだったでしょうか。

kitchen-ansibleの導入がネックになると判断した際、自分で新規のプラグインを作るべきかいろいろと悩んでいたのですが、見事にkitchen-ansiblepushが解決してくれました。私と同じような悩みを抱えている方もいらっしゃると思うので、そういった場合ぜひ利用してみてください。

本エントリがみなさんの参考になったら幸いに思います。

参考リンク