PackerによるAMIのテスト考 (2016年度編)

2016.04.25

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

はじめに

こんにちは、中山です。

みなさんはPackerを利用していますか。コードによってAMIを作成できるので弊社ではとても重宝しています。ただ、作成されたAMIが本当に意図したとおり作成されたのかテストしたいと考えたことはないでしょうか。この場合の「テスト」という言葉は provisionerで設定した各種ミドルウェアが意図したとおりに動作しているか という意味で使用しています。

私自身いろいろと模索している段階です。なのですが、 2016年度初旬時点 で取りうるテスト方法にはどういったものがあるのか、という内容でまとめを書いてみます。

前提条件

ミドルウェアのインストールにはansible-local provisionerを利用する前提で話を進めます。弊社での利用事例が多いためです。 file provisioner shell provisioner も利用していますが、私はあくまでOSのアップデートやスクリプトをアップロードするためだけに利用しています。一部を除いて特定のprovisionerに依存しないテスト方法を記述しているので、ChefやPuppetのprovisionerでも参考になる点はあるかと思います。

最近のアップデートで追加された ansible provisioner も便利なのですが、今回は割愛します。すこしクセが強く正直まだ使い慣れていないためです。

では早速本題に入りましょう。

コード

GitHubに各種テスト毎のコードを作成しました。コードはこちらを参照してください。

1. -debug オプションを使う

サンプルコードはこちらです。ディレクトリ構造は以下のとおりです。

1-use-debug-option/
├── scripts
│   ├── ansible.sh
│   ├── bootstrap.sh
│   └── playbook
│       ├── ansible.cfg
│       ├── hosts
│       ├── localhost.yml
│       ├── roles
│       │   └── nginx
│       │       ├── files
│       │       │   └── etc
│       │       │       └── nginx
│       │       │           └── nginx.conf
│       │       ├── handlers
│       │       │   └── main.yml
│       │       └── tasks
│       │           ├── main.yml
│       │           └── nginx.yml
│       └── site.yml
└── template.json

一番手っ取り早くAMIのテストを行う方法がこれだと思います。 -debug オプションを付けて packer build するとPackerの各ステップ毎に次のステップへ進んでよいか尋ねてくれます。Enterを押下することにより次のステップに進みます。このオプションを指定するとカレントディレクトリに秘密鍵が作成されます。provisionerの実行後、この秘密鍵を使用して対象EC2インスタンスにログインし、シェルコマンドを叩いて確認するという流れです。

実行方法は以下のとおりです。

$ cd 1-use-debug-option
$ packer build -debug template.json

この方法ははっきり言ってテストというよりデバッグです( -debug なので当たり前ですが )。provisionerが正常に実行されない場合などで詳細に原因を調査したい、という用途には向いていると思います。しかし、テストという観点から見た場合あまり役に立ちません。CI/CDとの連携もできません。繰り返しですがあくまでデバッグ用途です。

実行例を貼り付けると以下のような感じです。 StepProvision の後で次のステップに進んでよいか聞かれます。

<snip>
==> 1-use-debug-option: Provisioning with shell script: /var/folders/9z/0vv0gmd1523_gp07tp_rbxr80000gp/T/packer-shell413209947
==> 1-use-debug-option: Uploading scripts/. => /ops/scripts
==> 1-use-debug-option: Provisioning with shell script: /var/folders/9z/0vv0gmd1523_gp07tp_rbxr80000gp/T/packer-shell707271626
    1-use-debug-option: Loaded plugins: priorities, update-motd,
<snip>
    1-use-debug-option: PLAY RECAP *********************************************************************
    1-use-debug-option: 127.0.0.1                  : ok=6    changed=4    unreachable=0    failed=0
    1-use-debug-option:
==> 1-use-debug-option: Pausing after run of step 'StepProvision'. Press enter to continue. <- Enterを押下するまで止まる

この時点でPackerを実行したカレントディレクトリには以下のようにテンポラリの秘密鍵が削除されずに残っています。この鍵を利用すれば作成中のEC2インスタンスにログイン可能です。

$ ll 1-use-debug-option/
total 16
-rw-------  1 knakayama  staff  1674 Apr 23 06:37 ec2_1-use-debug-option.pem <- 秘密鍵
drwxr-xr-x  5 knakayama  staff   170 Apr 23 06:09 scripts
-rw-r--r--  1 knakayama  staff  2217 Apr 23 06:09 template.json

私が考えるメリット/デメリットは以下のとおりです。

メリット デメリット
PackerとAnsibleだけで作業完結できる
オプション指定するだけなので追加の作業が必要なく簡単に使える
Enter押すの苦行
そもそもテストじゃない

2. 作成したAMIからEC2インスタンスを起動させる

サンプルコードはこちらです。ディレクトリ構造は以下のとおりです。 terraform ディレクトリ以下にEC2インスタンスを起動させるためのコードがあります。

2-launch-ec2-instance-from-ami/
├── packer
│   ├── scripts
│   │   ├── ansible.sh
│   │   ├── bootstrap.sh
│   │   └── playbook
│   │       ├── ansible.cfg
│   │       ├── hosts
│   │       ├── localhost.yml
│   │       ├── roles
│   │       │   └── nginx
│   │       │       ├── files
│   │       │       │   └── etc
│   │       │       │       └── nginx
│   │       │       │           └── nginx.conf
│   │       │       ├── handlers
│   │       │       │   └── main.yml
│   │       │       └── tasks
│   │       │           ├── main.yml
│   │       │           └── nginx.yml
│   │       └── site.yml
│   └── template.json
└── terraform
    ├── main.tf
    └── terraform.tfvars

多くの方々はこの方法でテストを実施していると思います。Packerで作成したAMIを元にしてEC2インスタンスを起動させ、サーバにSSHでログインし確認するという方法です。確かに「素朴」な方法なので、私自身テンプレートの規模によってはこれだけで済ませる場合もあります。また、作り込み方によっては「Packerの実行 - EC2インスタンス起動 - テスト」までをコード化することもできるでしょう。

今回はTerraformを利用してEC2インスタンスの起動を実施しています。使用方法は以下のとおりです。まずAMIを普通に作成します。

$ cd 2-launch-ec2-instance-from-ami/packer
$ packer build template.json

作成されたAMIのIDをメモっておきましょう。続いてKeyPairを作成します。今回はテンポラリな用途を想定しているので terraform ディレクトリにサクっと作成しましょう。

$ cd ../terraform
$ ssh-keygen -f site_key -N '' && chmod 400 site_key && chmod 600 site_key.pub

準備ができたらTerraformを実行します。<access_key><secret_key> に自分のAWSクレデンシャルを指定してください。 <ami-id> に先程メモしておいたAMI IDを指定します。

$ terraform plan -var access_key=<access_key> -var secret_key=<secret_key> -var ami_id=<ami-id>
$ terraform apply -var access_key=<access_key> -var secret_key=<secret_key> -var ami_id=<ami-id>

正常に実行されればEC2インスタンスがVPC上に起動するので、後は煮るなり焼くなりお好きにできます。後ほど説明しますが、この時点でServerspecを利用したテストも実行可能です。

この方法は Packer上でのテストだけではなく本番環境と近い形でテストを行える という点がメリットです。AMIを本番環境と似た条件で動作確認するのは必要な作業なので、実際にサービスへ投入する前には必須の作業だと思います。しかし、Packerテンプレートの修正毎にいちいちEC2インスタンスを立ち上げるのは苦行ではないでしょうか。いくらコード化したとしてもそれなりに時間が掛かります。 この種のテストは結合テスト時に実施すべきで、AMI上のミドルウェアをテストする際には時間が掛かり過ぎる と感じています。

私が考えるメリット/デメリットは以下のとおりです。

メリット デメリット
本番環境に近い形でテストできる EC2インスタンス起動させるのに時間かかる

3. Ansibleのモジュールを使う

サンプルコードはこちらです。ディレクトリ構造は以下のとおりです。テストコードは test ディレクトリ以下にあります。

3-use-ansible-module/
├── scripts
│   ├── ansible.sh
│   ├── bootstrap.sh
│   └── playbook
│       ├── ansible.cfg
│       ├── hosts
│       ├── localhost.yml
│       ├── roles
│       │   ├── nginx
│       │   │   ├── files
│       │   │   │   └── etc
│       │   │   │       └── nginx
│       │   │   │           └── nginx.conf
│       │   │   ├── handlers
│       │   │   │   └── main.yml
│       │   │   └── tasks
│       │   │       ├── main.yml
│       │   │       └── nginx.yml
│       │   └── test
│       │       └── tasks
│       │           ├── main.yml
│       │           └── nginx.yml
│       └── site.yml
└── template.json

Ansibleには条件によってタスクの成功/失敗を判定させるためのモジュールがあります。assertfail モジュールです。これらのモジュールを使用してAnsible単体でプロビジョニングからテストまでを実行するということも可能です。

すいません。半分うそです。

これらのモジュールはミドルウェアの動作確認を目的にして作られていません。あくまでタスクの成否を確認するためのモジュールです。Ansibleのプラグインを開発する際などに利用します。そのため、とてもじゃないですが「まともな」テストコードは書けません。無理やりテストを書くこともできなくないですが、苦行感の高まりを感じます。やめましょう。

例えばこちらのコードのように「テスト」ができます。

このコードにはいくつか問題がありますが、一番大きな問題点は OSやディストリビューション毎の差異を自分自身で解決しないといけない という点だと思います。例えば、上記コードではRed Hat系のディストリビューションにしか対応していません。Debian系の場合エラーでコケてしまいます。もちろん when ステートメントを利用すればOSやディストリビューション毎に分岐させることは可能です。ですが、そんな苦行はしたくないのでこれはだめですね。

私が考えるメリット/デメリットは以下のとおりです。

メリット デメリット
PackerとAnsibleだけで作業完結できる Ansibleのモジュールでテスト書くの苦行

4. Serverspecを使う(AMIにインストールする編)

サンプルコードはこちらです。ディレクトリ構造は以下のとおりです。

4-run-serverspec-local/
├── scripts
│   ├── ansible.sh
│   ├── bootstrap.sh
│   ├── bundler.sh
│   └── playbook
│       ├── Gemfile
│       ├── Gemfile.lock
│       ├── Rakefile
│       ├── ansible.cfg
│       ├── bin
│       │   └── serverspec.sh
│       ├── hosts
│       ├── localhost.yml
│       ├── roles
│       │   └── nginx
│       │       ├── files
│       │       │   └── etc
│       │       │       └── nginx
│       │       │           └── nginx.conf
│       │       ├── handlers
│       │       │   └── main.yml
│       │       └── tasks
│       │           ├── main.yml
│       │           └── nginx.yml
│       ├── site.yml
│       ├── spec
│       │   ├── localhost
│       │   │   └── nginx_spec.rb
│       │   └── spec_helper.rb
│       └── vendor
│           └── bundle
│               └── <snip>
└── template.json

ミドルウェアのテストといったらはやり本命はServerspecでしょう。このツールを どうやってうまくPackerと連携させるか が本エントリの主題です。前置きが長かったです。

一番簡単な方法はAMI自体にインストールしローカルで実行させるという方法です。Serverspecをローカルで利用する場合はExecバックエンドを利用します。 serverspec-init を実行する際に以下のように指定します。

$ bundle exec serverspec-init
Select OS type:

  1) UN*X
  2) Windows

Select number: 1

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 2

 + spec/
 + spec/localhost/
 + spec/localhost/sample_spec.rb
 + spec/spec_helper.rb
 + Rakefile
 + .rspec

provisionerの最後でServerspecを実行させています。該当の箇所はこちらです。実行例を以下に貼り付けます。

<snip>
==> 4-run-serverspec-local: Provisioning with shell script: /var/folders/9z/0vv0gmd1523_gp07tp_rbxr80000gp/T/packer-shell292835186
<snip>
    4-run-serverspec-local: Package "nginx"
    4-run-serverspec-local:   should be installed
    4-run-serverspec-local:
    4-run-serverspec-local: Service "nginx"
    4-run-serverspec-local:   should be enabled
    4-run-serverspec-local:   should be running
    4-run-serverspec-local:
    4-run-serverspec-local: Port "80"
    4-run-serverspec-local:   should be listening
    4-run-serverspec-local:
    4-run-serverspec-local: File "/etc/nginx/nginx.conf"
    4-run-serverspec-local:   should be a file
    4-run-serverspec-local:   should be owned by "root"
    4-run-serverspec-local:   should be grouped into "root"
    4-run-serverspec-local:   should be mode 644
    4-run-serverspec-local: md5sum
    4-run-serverspec-local:     should eq "784504306d64ebb062d48be208c1a81a"
    4-run-serverspec-local:
    4-run-serverspec-local: Finished in 0.11207 seconds (files took 0.23673 seconds to load)
    4-run-serverspec-local: 9 examples, 0 failures
<snip>

今回の例では簡易的にgemをroot権限でインストールさせてしまっていますが、もちろんrbenvなどを利用してなるべくシステム環境に影響を与えいようにインストールさせることは可能です。

私が考えるメリット/デメリットは以下のとおりです。

メリット デメリット
実装が比較的容易 環境によってはAMIにServerspecをインストールできない場合がある

5. Serverspecを使う(リモートから実行する編)

サンプルコードはこちらです。ディレクトリ構造は以下のとおりです。

5-run-serverspec-remote/
├── scripts
│   ├── ansible.sh
│   ├── bootstrap.sh
│   └── playbook
│       ├── ansible.cfg
│       ├── bin
│       │   └── serverspec.sh
│       ├── hosts
│       ├── localhost.yml
│       ├── roles
│       │   └── nginx
│       │       ├── files
│       │       │   └── etc
│       │       │       └── nginx
│       │       │           └── nginx.conf
│       │       ├── handlers
│       │       │   └── main.yml
│       │       └── tasks
│       │           ├── main.yml
│       │           └── nginx.yml
│       └── site.yml
├── serverspec
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── Rakefile
│   ├── bin
│   │   └── run.sh
│   ├── hosts
│   ├── spec
│   │   ├── spec_helper.rb
│   │   └── web
│   │       └── nginx_spec.rb
│   └── vendor
│       └── bundle
│           └── <snip>
└── template.json

はじめに断っておきますが、無理矢理感が半端じゃ無いです。もっとうまい実装方法があったら教えて下さい。

環境によってはAMIに不要なパッケージなどをインストールしたくない、あるいはできないという場合もあると思います。まぁ、そもそもAnsible入れていますが、それは今回目をつぶっておきましょう。そういった制限がある場合に、Packerを実行したホスト上でServerspecを実行し、リモートからテストしたいと考えたことは無いでしょうか。私はあります。

いろいろと実行方法はあるかと思いますが、今回はshell-local provisionerを利用します。Packerによって作成中のAMIのパブリックIPをファイルに保存しておき、それをServerspecから参照してSSH経由でテストを実行させます。

Packerにはログを出力させる機能がありますpacker build の際に PACKER_LOG=1 PACKER_LOG_PATH=<packer-log> を指定することでカレントディレクトリにログを出力できます。このファイルにパブリックIPが出力されるので、それを grep で抜き出し、rakeタスクの引数に渡してあげます。無理矢理感の高まりを感じる。

該当のコードはこちらです。このコードをprovisionerの最後でshell-local provisionerを利用して実行させています

また、今までの方法ではテンポラリのKeyPairを利用していましたが、今回の方法はすでに登録済みのものを利用します。いろいろと調べたのですが、Packerが作成したKeyPairのうまい取得方法がなさそうだったからです。今回は「site_key」という名前で事前に登録しておきます。以下のコマンドを実行すると標準出力に秘密鍵の内容が表示されます。

$ cd 5-run-serverspec-remote
$ aws ec2 create-key-pair --key-name site-key --output text

出力された内容を「site_key」という名前で保存してください。ついでに権限も変更しておきましょう。

$ $EDITOR site_key
$ chmod 400 site_key

続いてBundlerで必要なgemをインストールします。

$ cd serverspec
$ bundle install --path vendor/bundle

ディレクトリ移動後、以下のコマンドで実行してください。

$ cd ..
$ PACKER_LOG=1 PACKER_LOG_PATH=packer.log packer build -var keypair_name=site_key template.json

テンプレートが実行されると最後にServerspecのテストが実施されます。実行例を以下に貼り付けます。

<snip>
==> 5-run-serverspec-remote: Executing local command: serverspec/bin/run.sh
    5-run-serverspec-remote: ** Invoke spec (first_time)
    5-run-serverspec-remote: ** Invoke spec:all (first_time)
<snip>
    5-run-serverspec-remote:
    5-run-serverspec-remote: Package "nginx"
    5-run-serverspec-remote: should be installed
    5-run-serverspec-remote:
    5-run-serverspec-remote: Service "nginx"
    5-run-serverspec-remote: should be enabled
    5-run-serverspec-remote: should be running
    5-run-serverspec-remote:
    5-run-serverspec-remote: Port "80"
    5-run-serverspec-remote: should be listening
    5-run-serverspec-remote:
    5-run-serverspec-remote: File "/etc/nginx/nginx.conf"
    5-run-serverspec-remote: should be a file
    5-run-serverspec-remote: should be owned by "root"
    5-run-serverspec-remote: should be grouped into "root"
    5-run-serverspec-remote: should be mode 644
    5-run-serverspec-remote: md5sum
    5-run-serverspec-remote: should eq "784504306d64ebb062d48be208c1a81a"
    5-run-serverspec-remote:
    5-run-serverspec-remote: Finished in 6.94 seconds (files took 0.46388 seconds to load)
    5-run-serverspec-remote: 9 examples, 0 failures
    5-run-serverspec-remote:
    5-run-serverspec-remote: ** Execute spec:all
    5-run-serverspec-remote: ** Execute spec
<snip>

私が考えるメリット/デメリットは以下のとおりです。

メリット デメリット
AMIの環境を変更さずにServerspec実行できる 無理矢理感ある

まとめ

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

本エントリではさまざまな方法でAMIにインストールしたミドルウェアをテストする方法を考えてみました。正直どれも一長一短で 全ての環境で最適な方法がない というのが現状なのではないかと考えています。もしみなさんの中にこういった方法もあるよ、などご提案がありましたら教えていただきたいなと考えています。

理想を言えば Serverspec専用Packerプラグインの作成 だと思います。本エントリの内容は既存の機能を駆使してテスト方法を模索していましたが、Packerの実行中にテストを完結させるという点ではプラグインの作成が一番良いと考えています。いくつかこの目的で作成されたPackerプラグインを見たのですが、そこまで作りこまれているものを見つけられませんでした。自分で書けばよいだけなのですがGo力が低すぎてなかなか手が出せていません。

私は今のところ「作成したAMIからEC2インスタンスを起動させる」方法を主に利用しています。時間が掛かるという問題はありますが、本番に近い環境でテストできるというのは大きいかと考えています。

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

参考リンク