awspecでAWSリソースをテストする
こんにちは、佐伯です。
AWSリソースの設定をマネジメントコンソールから目視確認するのはつらみがありますよね。ということで、今日はAWSリソースをテストするツール、awspecについて紹介したいと思います。
awspecとは
awspecはAWSのリソース構成をServerspecのようにテストできるツールです。GitHubリポジトリは下記になります。
GitHub - k1LoW/awspec: RSpec tests for your AWS resources.
ローカル環境を整える
rbenv, Bundler, direnvの話になるので、普段使ってる方は省略してください。
OSSを利用する際、バージョン管理が重要な課題になってきます。注目度が高いOSSほど頻繁にバージョンアップされ、新たな機能追加やバグ修正が行われます。また、稀にバージョンアップによって意図せぬバグが生まれ、以前のバージョンではできていたことができなくなってしまうこともあります。
そのため、既存のコードが動作するかをバージョンアップのタイミングで確認せずに安易にバージョンアップしてしまうと、ハマってしまうことがありますので、rbenvでRubyのバージョンを、Bundlerでgemのバージョンを管理できるように、ローカル環境を整えましょう。
なお、その後CIを導入する場合でも同じ仕組みを使うことができると思いますので、ちゃんと取り組みたいポイントになります。
rbenvのインストール
といいつつも、過去エントリのご紹介になります。下記エントリを参考にrbenvをインストールして下さい。rbenvは複数のRubyバージョンをディレクトリごとなどの単位で切り替えることができます。
様々な言語の実行環境が必要な方は下記エントリを参考にanyenvをインストールしても良いと思います。
Bundlerのインストール
こちらも過去エントリ(というか↑の続き)の紹介になります。Bundlerでgemのバージョン管理を行うのでインストールします。
(必要に応じて)direnvのインストール
複数のAWSアカウントを使用している場合は、AWSアカウント毎のIAMユーザーのアクセスキー/シークレットキーを使用するのではなく、direnvを使用してAssumeRoleで一時クレデンシャルを環境変数にセットする方法がおすすめです。
セットアップ
インストール手順はGitHub - k1LoW/awspec: RSpec tests for your AWS resources.に記載のとおりですが、上記のツール類を使った場合のセットアップ方法を書きます。
Rubyのインストール
awspecはRuby 2.1以上が必要です。Ruby 2.4.3までテスト済みのようなので、今回はRuby 2.4.3をインストールします。
| 1 2 3 4 5 | $ rbenv install2.4.3$ rbenv local2.4.3$ rbenv versions  system* 2.4.3 (setby /Users/saiki.ko/example/.ruby-version) | 
Bundlerのインストール
私の環境にRuby 2.4.3を初めて入れたので、Bundlerもインストールしておきます。
| 1 | $ gem installbundler | 
awspecのインストール
Gemfileを生成した後にawspecとrake(後ほど実行するawspec initでRakeタスク(Rakefile)を生成してくれるので)を追記し、bundle installを実行してawspecとrakeをインストールします。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | $ bundle initWriting new Gemfile to /Users/saiki.ko/example/Gemfile$ echo'gem "awspec"'>>Gemfile$ echo'gem "rake"'>>Gemfile$ bundle install--path vendor/bundleFetching gem metadata from https://rubygems.org/....................Resolving dependencies...Fetching aws-partitions 1.57.0Installing aws-partitions 1.57.0Fetching aws-sigv4 1.0.2Installing aws-sigv4 1.0.2Fetching jmespath 1.3.1Installing jmespath 1.3.1Fetching aws-sdk-core 3.14.0Installing aws-sdk-core 3.14.0......(省略)...... | 
awspecディレクトリとinitファイルの作成
awspecのコードはawspecディレクトリで管理したい(後ほどTerraformのコードも作成予定)ので、ディレクトリを作成し、awspec initコマンドでinitファイルを生成します。
| 1 2 3 4 5 6 7 8 | $ mkdirawspec$ cdawspec$ bundle execawspec init + spec/ + spec/spec_helper.rb + Rakefile + spec/.gitignore + .rspec | 
認証情報の設定
AWS CLIの認証情報にも対応してますし、spec/secrets.ymlに設定してもよいですし、紹介したdirenvを使って環境変数にセットする形でもよいです。また、IAMの権限は基本的にはRead権限があれば大抵のテストは実行できると思います。
動かしてみる
awspecを使ってAWSリソースをテストしてみましょう。
その前に
そもそもテストするAWSリソースがないので、さくっとTerraformで作っちゃいます。terraformディレクトリを作成し、その中にTerraform Module Refistryのvpc/awsモジュールを使ったvpc.tfを作成し、VPC、サブネット、ルートテーブル、インターネットゲートウェイを作成しました。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | provider "aws" {  region = "ap-northeast-1"}module "vpc" {  source = "terraform-aws-modules/vpc/aws"  name = "my-vpc"  cidr = "10.0.0.0/16"  azs             = ["ap-northeast-1a", "ap-northeast-1c"]  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]  enable_nat_gateway = false  enable_vpn_gateway = false  tags = {    Terraform   = "true"    Environment = "dev"  }} | 
テストコードを書く
現在のディレクトリ構成は下記のとおりです。awspec/spec配下にテストコードを作成します。
| 1 2 3 4 5 6 7 8 9 10 | example├── Gemfile├── Gemfile.lock├── awspec│   ├── Rakefile│   └── spec├── terraform│   └── vpc.tf└── vendor    └── bundle | 
基本的にはドキュメントを読みながらリソースごとにテストする項目を決め、*_spec.rbというファイル名でテストを書きます。特にリソース単位でファイルを分ける必要はないですが、私はなんとなく分けてます。
また、spec/spec_helper.rbでawspecをロードしてるので、各テストコードファイルの先頭にrequire 'spec_helper'を書きましょう。
VPC
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | require 'spec_helper'describe vpc('my-vpc') do  it { should exist }  it { should be_available }  its(:cidr_block) { should eq '10.0.0.0/16'}  its(:dhcp_options_id) { should eq 'dopt-XXXXXXXX'}  it { should have_route_table('my-vpc-private-ap-northeast-1a') }  it { should have_route_table('my-vpc-private-ap-northeast-1c') }  it { should have_route_table('my-vpc-public') }  it { should have_network_acl('acl-XXXXXXXX') }  it { should have_tag('Name').value('my-vpc') }  it { should have_tag('Environment').value('dev') }end | 
Subnet
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | require 'spec_helper'describe subnet('my-vpc-private-ap-northeast-1a') do  it { should exist }  it { should be_available }  it { should belong_to_vpc('my-vpc') }  its(:availability_zone) { should eq 'ap-northeast-1a'}  its(:cidr_block) { should eq '10.0.1.0/24'}  its(:map_public_ip_on_launch) { should eq false}  it { should have_tag('Name').value('my-vpc-private-ap-northeast-1a') }  it { should have_tag('Environment').value('dev') }enddescribe subnet('my-vpc-private-ap-northeast-1c') do  it { should exist }  it { should be_available }  it { should belong_to_vpc('my-vpc') }  its(:availability_zone) { should eq 'ap-northeast-1c'}  its(:cidr_block) { should eq '10.0.2.0/24'}  its(:map_public_ip_on_launch) { should eq false}  it { should have_tag('Name').value('my-vpc-private-ap-northeast-1c') }  it { should have_tag('Environment').value('dev') }enddescribe subnet('my-vpc-public-ap-northeast-1a') do  it { should exist }  it { should be_available }  it { should belong_to_vpc('my-vpc') }  its(:availability_zone) { should eq 'ap-northeast-1a'}  its(:cidr_block) { should eq '10.0.101.0/24'}  its(:map_public_ip_on_launch) { should eq true}  it { should have_tag('Name').value('my-vpc-public-ap-northeast-1a') }  it { should have_tag('Environment').value('dev') }enddescribe subnet('my-vpc-public-ap-northeast-1c') do  it { should exist }  it { should be_available }  it { should belong_to_vpc('my-vpc') }  its(:availability_zone) { should eq 'ap-northeast-1c'}  its(:cidr_block) { should eq '10.0.102.0/24'}  its(:map_public_ip_on_launch) { should eq true}  it { should have_tag('Name').value('my-vpc-public-ap-northeast-1c') }  it { should have_tag('Environment').value('dev') }end | 
Internet Gataway
| 1 2 3 4 5 6 7 8 | require 'spec_helper'describe internet_gateway('my-vpc') do  it { should exist }  it { should be_attached_to('my-vpc') }  it { should have_tag('Name').value('my-vpc') }  it { should have_tag('Environment').value('dev') }end | 
Route Table
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | require 'spec_helper'describe route_table('my-vpc-private-ap-northeast-1a') do  it { should exist }  it { should have_subnet('my-vpc-private-ap-northeast-1a') }  it { should have_route('10.0.0.0/16').target(gateway: 'local') }  it { should have_tag('Name').value('my-vpc-private-ap-northeast-1a') }  it { should have_tag('Environment').value('dev') }enddescribe route_table('my-vpc-private-ap-northeast-1c') do  it { should exist }  it { should have_subnet('my-vpc-private-ap-northeast-1c') }  it { should have_route('10.0.0.0/16').target(gateway: 'local') }  it { should have_tag('Name').value('my-vpc-private-ap-northeast-1c') }  it { should have_tag('Environment').value('dev') }enddescribe route_table('my-vpc-public') do  it { should exist }  it { should have_subnet('my-vpc-public-ap-northeast-1a') }  it { should have_subnet('my-vpc-public-ap-northeast-1c') }  it { should have_route('10.0.0.0/16').target(gateway: 'local') }  it { should have_route('0.0.0.0/0').target(gateway: 'my-vpc') }  it { should have_tag('Name').value('my-vpc-public') }  it { should have_tag('Environment').value('dev') }end | 
テストを実行する
awspec initでRakefileが生成されており、awspecディレクトリ配下でbundle exec rake specでテストが実行できます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | [saiki.ko@~/example/awspec]$ bundle exec rake spec/Users/saiki.ko/.anyenv/envs/rbenv/versions/2.4.3/bin/ruby -I/Users/saiki.ko/example/vendor/bundle/ruby/2.4.0/gems/rspec-core-3.7.1/lib:/Users/saiki.ko/example/vendor/bundle/ruby/2.4.0/gems/rspec-support-3.7.1/lib /Users/saiki.ko/example/vendor/bundle/ruby/2.4.0/gems/rspec-core-3.7.1/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rbinternet_gateway 'my-vpc'  should exist  should be attached to "my-vpc"  should have tag "Name"  should have tag "Environment"route_table 'my-vpc-private-ap-northeast-1a'  should exist  should have subnet "my-vpc-private-ap-northeast-1a"  should have route "10.0.0.0/16"  should have tag "Name"  should have tag "Environment"route_table 'my-vpc-private-ap-northeast-1c'  should exist  should have subnet "my-vpc-private-ap-northeast-1c"  should have route "10.0.0.0/16"  should have tag "Name"  should have tag "Environment"route_table 'my-vpc-public'  should exist  should have subnet "my-vpc-public-ap-northeast-1a"  should have subnet "my-vpc-public-ap-northeast-1c"  should have route "10.0.0.0/16"  should have route "0.0.0.0/0"  should have tag "Name"  should have tag "Environment"subnet 'my-vpc-private-ap-northeast-1a'  should exist  should be available  should belong to vpc "my-vpc"  should have tag "Name"  should have tag "Environment"  availability_zone    should eq "ap-northeast-1a"  cidr_block    should eq "10.0.1.0/24"  map_public_ip_on_launch    should eq falsesubnet 'my-vpc-private-ap-northeast-1c'  should exist  should be available  should belong to vpc "my-vpc"  should have tag "Name"  should have tag "Environment"  availability_zone    should eq "ap-northeast-1c"  cidr_block    should eq "10.0.2.0/24"  map_public_ip_on_launch    should eq falsesubnet 'my-vpc-public-ap-northeast-1a'  should exist  should be available  should belong to vpc "my-vpc"  should have tag "Name"  should have tag "Environment"  availability_zone    should eq "ap-northeast-1a"  cidr_block    should eq "10.0.101.0/24"  map_public_ip_on_launch    should eq truesubnet 'my-vpc-public-ap-northeast-1c'  should exist  should be available  should belong to vpc "my-vpc"  should have tag "Name"  should have tag "Environment"  availability_zone    should eq "ap-northeast-1c"  cidr_block    should eq "10.0.102.0/24"  map_public_ip_on_launch    should eq truevpc 'my-vpc'  should exist  should be available  should have route table "my-vpc-private-ap-northeast-1a"  should have route table "my-vpc-private-ap-northeast-1c"  should have route table "my-vpc-public"  should have network acl "acl-XXXXXXXX"  should have tag "Name"  should have tag "Environment"  cidr_block    should eq "10.0.0.0/16"  dhcp_options_id    should eq "dopt-XXXXXXXX"Finished in 2.05 seconds (files took 1.34 seconds to load)63 examples, 0 failures | 
テキストじゃあんまりパッとしないので、画像で一部分を貼ります。
ちなみに、テストが失敗した時はこんな感じ。(VPCのNameタグのテストを'my-vpc'から'my-vpn'に変更して意図的にテストを失敗させてます)
ファイル単体でテストを実行したい場合は、bundle exec rspec spec/<ファイル名>で実行できます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | [saiki.ko@~/example/awspec]$ bundle execrspec spec/vpc_spec.rbvpc 'my-vpc'  should exist  should be available  should have route table "my-vpc-private-ap-northeast-1a"  should have route table "my-vpc-private-ap-northeast-1c"  should have route table "my-vpc-public"  should have network acl "acl-XXXXXXXX"  should have tag "Name"  should have tag "Environment"  cidr_block    should eq"10.0.0.0/16"  dhcp_options_id    should eq"dopt-XXXXXXXX"Finished in0.61097 seconds (files took 1.37 seconds to load)10 examples, 0 failures | 
テストコードを生成する
awspecにはテストコードを生成するgenerateサブコマンドがあります。awspec v1.3.0でテストコードを生成できるリソースは下記の通りです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | $ bundle execawspec generate --helpCommands:  awspec generate acm                                              # Generate acm spec  awspec generate alb [vpc_id]                                     # Generate alb spec from VPC ID (or VPC "Name" tag)  awspec generate cloudwatch_alarm                                 # Generate cloudwatch_alarm spec  awspec generate cloudwatch_event                                 # Generate cloudwatch_event spec  awspec generate cloudwatch_logs                                  # Generate cloudwatch_logs spec  awspec generate directconnect                                    # Generate directconnect spec  awspec generate ebs                                              # Generate attached ebs spec  awspec generate ec2 [vpc_id]                                     # Generate ec2 spec from VPC ID (or VPC "Name" tag)  awspec generate efs                                              # Generate efs spec  awspec generate eip                                              # Generate eip spec  awspec generate elasticsearch                                    # Generate elasticsearch spec  awspec generate elb [vpc_id]                                     # Generate elb spec from VPC ID (or VPC "Name" tag)  awspec generate help [COMMAND]                                   # Describe subcommands or one specific subcommand  awspec generate iam_group                                        # Generate iam_group spec  awspec generate iam_policy                                       # Generate attached iam_policy spec  awspec generate iam_role                                         # Generate iam_role spec  awspec generate iam_user                                         # Generate iam_user spec  awspec generate internet_gateway [vpc_id]                        # Generate internet_gateway spec from VPC ID (or VPC "Name" tag)  awspec generate kms                                              # Generate kms spec  awspec generate lambda                                           # Generate lambda spec  awspec generate nat_gateway [vpc_id]                             # Generate nat_gateway spec from VPC ID (or VPC "Name" tag)  awspec generate network_acl [vpc_id]                             # Generate network_acl spec from VPC ID (or VPC "Name" tag)  awspec generate network_interface [vpc_id]                       # Generate network_interface spec from VPC ID (or VPC "Name" tag)  awspec generate nlb [vpc_id]                                     # Generate nlb spec from VPC ID (or VPC "Name" tag)  awspec generate rds [vpc_id]                                     # Generate rds spec from VPC ID (or VPC "Name" tag)  awspec generate rds_db_cluster_parameter_group [paramater_name]  # Generate rds_db_cluster_parameter_group spec from paramater name.  awspec generate rds_db_parameter_group [paramater_name]          # Generate rds_db_parameter_group spec from paramater name.  awspec generate route53_hosted_zone [example.com.]               # Generate route53_hosted_zone spec from Domain name  awspec generate route_table [vpc_id]                             # Generate route_table spec from VPC ID (or VPC "Name" tag)  awspec generate s3_bucket [bucket_name]                          # Generate s3_bucket spec from S3 bucket name. if NO args, Generate all.  awspec generate security_group [vpc_id]                          # Generate security_group spec from VPC ID (or VPC "Name" tag)  awspec generate subnet [vpc_id]                                  # Generate subnet spec from VPC ID (or VPC "Name" tag)  awspec generate vpc [vpc_id]                                     # Generate vpc spec from VPC ID (or VPC "Name" tag) | 
例えば、今回作成したVPC(Name:my-vpc)のテストは以下のコマンドで生成できます。生成されたテストコードは標準出力に出力されるので、リダイレクトなりして*_spec.rbファイルに書きます。なお、生成したテストコードを使用する際、require 'spec_helper'を書くのを忘れがちになるので注意しましょー。
| 1 2 3 4 5 6 7 8 9 10 11 12 | $ bundle execawspec generate vpc my-vpcdescribe vpc('my-vpc') do  it { should exist }  it { should be_available }  its(:vpc_id) { should eq'vpc-XXXXXXXX'}  its(:cidr_block) { should eq'10.0.0.0/16'}  it { should have_route_table('rtb-XXXXXXXX') }  it { should have_route_table('my-vpc-private-ap-northeast-1a') }  it { should have_route_table('my-vpc-private-ap-northeast-1c') }  it { should have_route_table('my-vpc-public') }  it { should have_network_acl('acl-XXXXXXXX') }end | 
全てのリソースに対応しているわけではない
AWSは新しいサービスや機能がガンガンリリースされているので、全てのAWSリソースに対応しているわけではありません。「これテストしたいけど対応してないなー」なんて思ったら、コントリビュートチャンスです。
まとめ
簡単にawspecの使い方を紹介させて頂きました。CloudFormationやTerraformなどの構成管理ツールでAWS環境を構築しているのだから、テストは不要といった意見もありますが、運用していく中であるべき姿に保たれているか?をチェックすることは大事だと思います。GitリポジトリとCodeBuildやCircleCIなどのサービスを連動させて自動テストしたり、定期的に自動テストしたりとできるといい感じになるので、以下エントリも参考にして頂ければと思います。