awspecでAWSインフラのテストをしてみた
はじめまして。インフラストラクチャー部の山下です。
各種サービスのAWSインフラを担当する傍ら、社内ではRubyやRailsなどを書いてたりしています。
私が参加しているプロジェクトで、AWSの各リソースが正しく構成されているかを確認したいという話が出たため、awspecを導入してみました。
awspecとは?
Serverspecのように、AWSの各リソースをテスト出来ます。
構成
本記事では、Rubyやawspecは以下のバージョンを使用しています。
- Ruby: 2.2.4
- awspec: 0.37.7
対応しているAWSリソース
awspecでテストできるAWSリソースの代表的なものは以下の通りです。
詳しくは公式ページをご参照下さい。
https://github.com/k1LoW/awspec/blob/master/doc/resource_types.md
- EC2
- RDS
- ELB
- Elasticache
- IAM
- VPC
- SecurityGroup
インストール
gemパッケージをインストールします。
本記事では単純にgemでインストールしていますが、Bundlerなどで管理するほうがおすすめです。
$ gem install awspec
次に任意のディレクトリに移動し、以下のコマンドで必要なファイルを生成します。
$ awspec init
initを実行すると、以下の様なディレクトリとファイルが生成されます。
.
├── Rakefile
└── spec
└── spec_helper.rb
構成自体はServerspecとほぼ同じです。 specディレクトリ以下に、各テストを書いていきます。
specファイルを書いてみる
specディレクトリ以下にリソース名_spec.rb
という名前でファイルを作成します。
ここではEC2のspecを書いてみます。
$ vi spec/ec2_spec.rb
require 'spec_helper'
describe ec2('server1') do
it { should exist }
it { should be_running }
its(:instance_id) { should eq 'i-00000000' }
its(:image_id) { should eq 'ami-c8xxxxxx' }
its(:private_dns_name) { should eq 'ip-10-0-0-95.ap-northeast-1.compute.internal' }
its(:public_dns_name) { should eq 'ec2-52-xxx-xx-xxx.ap-northeast-1.compute.amazonaws.com' }
its(:instance_type) { should eq 'c3.large' }
its(:private_ip_address) { should eq '10.0.0.95' }
its(:public_ip_address) { should eq 'xx.xxx.xx.xxx' }
%w{ hoge-access-sg huga-access-sg default-sg }.each do |sg|
it { should have_security_group(sg) }
end
it { should belong_to_vpc('vpc-0') }
it { should belong_to_subnet('front-subnet-a') }
it { should have_eip('xx.xxx.xx.xxx') }
it { should have_ebs('vol-xxxxxxxx') }
it { should have_ebs('Created for server1') }
end
このように、Serverspec(RSpec)ライクにテストが書けます。
Generate
ただ、既存リソースを全て手で起こすのは苦行だと思います。
awspecには既存のリソースをテストに起こしてくれる機能があるので、使ってみましょう。
AWSのCredentialは登録されているものとします。
また、IAMで各リソースのRead権限が付与されている必要があります。
$ export AWS_REGION=ap-northeast-1
$ awspec generate ec2 vpc-xxxxxxxx --profile staging >> spec/ec2_spec.rb
※ vpc-xxxxxxxxは実際に存在するVPC IDを指定して下さい。
実行
上記で書いたテストを実行してみましょう。
Rakefileがある場所まで移動し、以下のコマンドを実行します。
$ export AWS_REGION=ap-northeast-1 AWS_PROFILE=staging
$ rake spec
以下のように結果が出力されます。
ec2 'server1'
should exist
should be running
should have security group "hoge-access-sg"
should have security group "huga-access-sg"
should have security group "default-sg"
should belong to vpc "vpc-0"
should belong to subnet "front-subnet-a"
should have eip "xx.xxx.xx.xxx"
should have ebs "vol-xxxxxxxx"
instance_id
should eq "i-00000000"
image_id
should eq "ami-c8xxxxxx"
private_dns_name
should eq "ip-10-0-0-95.ap-northeast-1.compute.internal"
public_dns_name
should eq "ec2-52-xxx-xx-xxx.ap-northeast-1.compute.amazonaws.com"
instance_type
should eq "c3.large"
private_ip_address
should eq "10.0.0.95"
public_ip_address
should eq "52.xxx.xx.xxx"
Finished in 0.48842 seconds (files took 2.11 seconds to load)
16 examples, 0 failures
なんとも見やすい! ちなみに失敗した時は以下の様な出力になります。
ec2 'server2'
should exist
should be running
should belong to vpc "vpc-0"
should belong to subnet "front-subnet-a"
should have eip "xx.xx.xx.xx"
should have ebs "vol-xxxxxxxx"
instance_id
should eq "i-xxxxx" (FAILED - 1)
image_id
should eq "ami-c8xxxxxx"
private_dns_name
should eq "ip-10-0-10-96.ap-northeast-1.compute.internal"
public_dns_name
should eq "ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com"
instance_type
should eq "t2.large" (FAILED - 2)
private_ip_address
should eq "10.0.10.96"
public_ip_address
should eq "xx.xx.xx.xx"
Failures:
1) ec2 'server2' instance_id should eq "i-xxxxx"
Failure/Error: its(:instance_id) { should eq 'i-xxxxx' }
expected: "i-00000000"
got: "i-12345678"
(compared using ==)
# ./spec/ec2_spec.rb:6:in `block (2 levels) in <top>'
2) ec2 'server2' instance_type should eq "t2.large"
Failure/Error: its(:instance_type) { should eq 't2.large' }
expected: "t2.large"
got: "t2.small"
(compared using ==)
# ./spec/ec2_spec.rb:10:in `block (2 levels) in <top>'
Finished in 0.61169 seconds (files took 2.87 seconds to load)
15 examples, 2 failures
Failed examples:
rspec ./spec/ec2_spec.rb:6 # ec2 'server2' instance_id should eq "i-xxxxx"
rspec ./spec/ec2_spec.rb:10 # ec2 'server2' instance_type should eq "t2.large"
上記では、以下の2項目が失敗しています。
- server2のインスタンスIDが、テストケースと実際の値で差異がある
- インスタンスサイズが、テストケースではt2.largeだが、実際はt2.smallとなっている
このように、テストに失敗しても簡単に追うことが可能です。
YAMLに落としてみた
テスト対象が増えてくるとメンテナンスコストが高くなる可能性があります。
頻繁に修正が行われるのを考慮し、YAMLを読むようにしました。
まず、Rakefileに以下を追加します。
$ vi Rakefile
require 'yaml'
次にYAMLを記述します。
$ vi config.yml
ec2:
- name: "server1"
instance_type: "t2.micro"
security_groups:
- "hoge-access-sg"
- "huga-access-sg"
- "default-access-sg"
eip: "52.xxx.xx.xxx"
private_ip_address: "10.0.0.95"
subnet: "front-subnet-a"
- name: "server2"
instance_type: "t2.micro"
security_groups:
- "hoge-access-sg"
- "huga-access-sg"
- "default-access-sg"
eip: "52.xxx.xx.xxx"
private_ip_address: "10.0.0.96"
subnet: "front-subnet-c"
specヘルパーを修正します。
YAMLで読んだ値を@properties
という変数に格納し、各specファイルから参照できるようにします。
$ vi spec/spec_helper.rb
require 'awspec'
require 'yaml'
Awsecrets.load(secrets_path: File.expand_path('./secrets.yml', File.dirname(__FILE__)))
@properties = YAML.load_file("config.yml")
最後に、各specを修正します。 今回はEC2を例として、以下に記述します。
$ vi spec/ec2_spec.rb
require 'spec_helper'
properties = @properties
properties['ec2'].each do |ec2|
describe ec2(ec2['name']) do
it { should exist }
its(:instance_type) { should eq ec2['instance_type'] }
its(:private_ip_address) { should eq ec2['private_ip_address'] }
ec2['security_groups'].each do |s|
it { should have_security_group(s) }
end
it { should belong_to_subnet(ec2['subnet']) }
it { should have_eip(ec2['eip']) } unless ec2['eip'].nil?
end
end
悩んだポイント
インスタンスロールに対応していない
awspecが使用しているawsecretsというgemが、 インスタンスのIAMロールに対応しておらずcredentialが存在しない環境では使用できませんでした。
こちらはawsecretsにPullRequestを行う予定です。
CloudFrontなど一部のリソースに対応していない
特にCloudFrontはほしかったです…。
こちらも時間があるときに書いてみます。
まとめ
これまで、アプリケーションやOS/ミドルウェアのテストはありますが、AWSリソース自体のテストはあまり一般的ではありませんでした。
awspecの登場により、より簡単に構築したリソースのテストが可能となりました。
やはりテスト大事ですね!