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の登場により、より簡単に構築したリソースのテストが可能となりました。

やはりテスト大事ですね!