InSpecを使ったAWSリソースの自動テストに入門した

はじめに

こんにちは! GIMLEチームの太田です。

関東では既に梅雨入りしたみたいですね。こちら関西では平年より遅い梅雨入りとなりそうです。 じめじめした季節がやってきますが、心は晴れ晴れとした状態で過ごしたいものですね(*^-^*)


さて、今私が関わっているプロジェクトでは、AWSインフラの自動テストツールを導入・利用しています。
プロジェクトでは、'awspec' や 'InSpec' というツールを利用するのですが、本記事では 'InSpec' の方に入門していきたいと思います。

今回は、実際に手を動かしてやってみるのをメインにしていますので、InSpec自体の説明は他の記事やドキュメントの方にお任せしようと思います。

InSpecのドキュメント
InSpecのGitHubリポジトリ

自動テストツールを触るのは、私も今回が初めて・・・。
ご興味があれば、一緒にやってみていただけると大変ありがたいです。

では、いってみましょう!

検証した環境

  • 端末:MacBook Pro
  • OS:macOS Monterey 12.4
  • CPU:Intel Core i5
  • InSpecのバージョン:5.12.2

前提条件

  • AWSアカウントを所有していること
  • AWS CLIがインストールされていること
  • AWS CLIを実行するIAMユーザー、ロールなどが作成されていること
    • マネージドポリシーの、ReadOnlyAccess がアタッチされていれば、InSpecが実行可能です

InSpecのインストール

Macの場合、brewでインストールできます。

$ brew install chef/chef/inspec

Windowsの場合は、こちら からインストーラをダウンロードして、インストールします。

Linuxの場合(AWS CloudShellでも利用できます!)、以下のコマンドでインストールが可能です。

$ curl https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec

インストールが終わったら、以下のコマンドでバージョンを確認しておきましょう。

$ inspec -v
5.12.2

InSpecのプロジェクトを作成

以下のコマンドを実行すると、新しいプロジェクトを作成してくれます。

$ inspec init profile --platform aws <任意のプロジェクト名>

 ─────────────────────────── InSpec Code Generator ─────────────────────────── 

Creating new profile at /xxxx/xxxx/xxxx/xxxx/testproject
 • Creating file README.md
 • Creating directory /xxxx/xxxx/xxxx/xxxx/testproject/controls
 • Creating file controls/example.rb
 • Creating file inputs.yml
 • Creating file inspec.yml

ディレクトリ構造はこんな感じ。

プロジェクトのディレクトリ
├── README.md
├── controls
│   └── example.rb
├── inputs.yml
└── inspec.yml

controls ディレクトリの配下に、<任意の名前>.rb というファイル名でテストコードを作っていきます。
(その他のファイルの詳細は、今回は触れません)

テストコードを作って、実行してみる

テストコードは基本的に、Chef InSpec Resources を参照しつつ、
Chef InSpec Language というDSLを使用して書いていきます。

テストをするにも、テスト対象のAWSリソースが必要ですね・・・。
今回は、テスト用に以下のようなセキュリティグループを作ってみました。

  • セキュリティグループ名:inspec-test-security-group
  • ルール:以下の表の通り
IPバージョン タイプ プロトコル ポート範囲 ソース /送信先
インバウンドルール1 IPv6 HTTP TCP 80 2001:DB8::/32
インバウンドルール2 - カスタム UDP UDP 80 別のセキュリティグループ
インバウンドルール3 IPv4 HTTP TCP 80 192.0.2.0/24
アウトバウンドルール1 IPv6 HTTPS TCP 443 ::/0
アウトバウンドルール2 - HTTPS TCP 443 pl-61a54008
(東京リージョンにおけるS3のマネージドプレフィックスリストです)
アウトバウンドルール3 IPv4 HTTPS TCP 443 0.0.0.0/0

インバウンドルールを3つ、アウトバウンドルールを3つ作成。
それぞれIPv4、IPv6、UDP、マネージドプレフィックスリストなど、適当にばらけさせてみました。

セキュリティグループについてのInSpecのドキュメントは、こちらです。


余談となりますが、上記インバウンドルール1とインバウンドルール3に設定しているIPアドレスは、ドキュメント用に予約されているものを利用させていただきました。

参考:

今回初めて知って大変驚いたのですが、RFCにはこんなことまで記載されているのですね。


それでは、ちょっとテストコードを書いてみましょう。
controls ディレクトリの配下の example.rb の名前を変更して、security_group.rb というファイル名にします。
元々あったコードは一旦消して、以下のコードを書きます。

describe aws_security_group(group_name: 'inspec-test-security-group') do
  it { should exist }
end

とってもシンプルですね ^_^;
セキュリティグループが存在するかを確認しています。
とりあえず、これでテストを実行してみましょう!

テストの実行には、AWS CLIのプロファイルを使用するのがお手軽かと思います。 AWS CLIのプロファイルがまだ無い場合は、以下のコマンドを使ってプロファイルを作成しましょう。

$ aws configure --profile <任意のプロファイル名>

AWS CLIのプロファイルができたら、InSpecのプロジェクトのディレクトリ(inspec.ymlや、README.mdが作成されたディレクトリです)内で以下のコマンドを実行すると、テストが開始されます。

$ inspec exec . -t aws://<AWSリージョン>/<AWS CLIのプロファイル名>

では、実行してみます!

実行結果(一部xxx表記にしています):

Profile:   AWS InSpec Profile (testproject)
Version:   0.1.0
Target:    aws://ap-northeast-1
Target ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

  EC2 Security Group ID: sg-xxxxxxxxxxxxxxxxx Name: inspec-test-security-group VPC ID: vpc-xxxxxxxxxxxxxxxxx 
     ✔  is expected to exist

〜中略〜

Test Summary: 1 successful, 0 failures, 0 skipped

おお、成功していますね!(当たり前か・・・)
一番下の Test Summary で、1件のテストが成功したことが確認できます。

インバウンドルールのテストを追加してみる

ではこの調子で、ドキュメントを見ながらコードを書き加えてみましょう。

describe aws_security_group(group_name: 'inspec-test-security-group') do
  it { should exist }
  it { should allow_in(protocol: 'tcp', port: 80, ipv6_range: '2001:DB8::/32') }
  it { should allow_in(protocol: 'udp', port: 80, security_group: 'sg-xxxxxxxxxxxxxxxxx') }
  it { should allow_in(protocol: 'tcp', port: 80, ipv4_range: '192.0.2.0/24') }
end

インバウンドルールのテストを追加してみました。
IPv6のアドレスレンジや、セキュリティグループを含むルールもテストできるみたいですね。

早速、実行してみましょう!

実行結果:

〜省略〜

  EC2 Security Group ID: sg-xxxxxxxxxxxxxxxxx Name: inspec-test-security-group VPC ID: vpc-xxxxxxxxxxxxxxxxx 
     ✔  is expected to exist
     ✔  is expected to allow in {:ipv6_range=>"2001:DB8::/32", :port=>80, :protocol=>"tcp"}
     ✔  is expected to allow in {:port=>80, :protocol=>"udp", :security_group=>"sg-xxxxxxxxxxxxxxxxx"}
     ✔  is expected to allow in {:ipv4_range=>"192.0.2.0/24", :port=>80, :protocol=>"tcp"}

〜省略〜

Test Summary: 4 successful, 0 failures, 0 skipped

成功です! では、次はアウトバウンドルールを追加・・・の前に、ルールの数が合っているかのテストを追加してみます。
(ひねくれ者ですみません)

ルールの数が合っているかのテストを追加してみる

describe aws_security_group(group_name: 'inspec-test-security-group') do
  it { should exist }
  it { should allow_in(protocol: 'tcp', port: 80, ipv6_range: '2001:DB8::/32') }
  it { should allow_in(protocol: 'udp', port: 80, security_group: 'sg-xxxxxxxxxxxxxxxxx') }
  it { should allow_in(protocol: 'tcp', port: 80, ipv4_range: '192.0.2.0/24') }
  its('inbound_rules_count') { should eq 3 }
  its('outbound_rules_count') { should eq 3 }
end

インバウンドルール、アウトバウンドルールをそれぞれ3つずつ作ったんでしたね。
では、実行!

実行結果:

〜省略〜

  EC2 Security Group ID: sg-xxxxxxxxxxxxxxxxx Name: inspec-test-security-group VPC ID: vpc-xxxxxxxxxxxxxxxxx 
     ✔  is expected to exist
     ✔  is expected to allow in {:ipv6_range=>"2001:DB8::/32", :port=>80, :protocol=>"tcp"}
     ✔  is expected to allow in {:port=>80, :protocol=>"udp", :security_group=>"sg-xxxxxxxxxxxxxxxxx"}
     ✔  is expected to allow in {:ipv4_range=>"192.0.2.0/24", :port=>80, :protocol=>"tcp"}
     ✔  inbound_rules_count is expected to eq 3
     ×  outbound_rules_count is expected to eq 3
     
     expected: 3
          got: 2
     
     (compared using ==)

〜省略〜

Test Summary: 5 successful, 1 failure, 0 skipped

あれれ? アウトバウンドルールの数のテストで失敗しちゃってます (´;︵;`)
確かに3つ設定したはずなのですが・・・。

う〜ん、どのルールがカウントされていないのかわからないので、ひとまず、アウトバウンドルールのテストを書き進めていくことにします。

アウトバウンドルールのテストを追加してみる

describe aws_security_group(group_name: 'inspec-test-security-group') do
  it { should exist }
  it { should allow_in(protocol: 'tcp', port: 80, ipv6_range: '2001:DB8::/32') }
  it { should allow_in(protocol: 'udp', port: 80, security_group: 'sg-xxxxxxxxxxxxxxxxx') }
  it { should allow_in(protocol: 'tcp', port: 80, ipv4_range: '192.0.2.0/24') }
  it { should allow_out(protocol: 'tcp', port: 443, ipv6_range: '::/0') }
  it { should allow_out(protocol: 'tcp', port: 443, ???) } # ⇦ マネージドプレフィックスリストを設定するところがない・・・
  it { should allow_out(protocol: 'tcp', port: 443, ipv4_range: '0.0.0.0/0') }
  its('inbound_rules_count') { should eq 3 }
  its('outbound_rules_count') { should eq 3 }
end

マネージドプレフィックスリストを設定するところはどこだろう、と悩んでしまいました・・・。
セキュリティグループのドキュメントをよく見てみると、Limitations のところに以下の記載が。

While this resource provides facilities for searching inbound and outbound rules on a variety of criteria, there is currently no support for performing matches based on:

References to VPC peers or other AWS services (that is, no support for searches based on ‘prefix lists’ ).

プレフィックスリストについては、対応していないみたいですね・・・残念。
今のところは目視で確認するしかなさそうです。

ということで、アウトバウンドルールを追加したテストコードは以下のようになりました。

describe aws_security_group(group_name: 'inspec-test-security-group') do
  it { should exist }
  it { should allow_in(protocol: 'tcp', port: 80, ipv6_range: '2001:DB8::/32') }
  it { should allow_in(protocol: 'udp', port: 80, security_group: 'sg-xxxxxxxxxxxxxxxxx') }
  it { should allow_in(protocol: 'tcp', port: 80, ipv4_range: '192.0.2.0/24') }
  it { should allow_out(protocol: 'tcp', port: 443, ipv6_range: '::/0') }
  it { should allow_out(protocol: 'tcp', port: 443, ipv4_range: '0.0.0.0/0') }
  its('inbound_rules_count') { should eq 3 }
  its('outbound_rules_count') { should eq 2 }
end

気を取り直して、実行してみましょう。

実行結果:

〜省略〜

  EC2 Security Group ID: sg-xxxxxxxxxxxxxxxxx Name: inspec-test-security-group VPC ID: vpc-xxxxxxxxxxxxxxxxx 
     ✔  is expected to exist
     ✔  is expected to allow in {:ipv6_range=>"2001:DB8::/32", :port=>80, :protocol=>"tcp"}
     ✔  is expected to allow in {:port=>80, :protocol=>"udp", :security_group=>"sg-xxxxxxxxxxxxxxxxx"}
     ✔  is expected to allow in {:ipv4_range=>"192.0.2.0/24", :port=>80, :protocol=>"tcp"}
     ✔  is expected to allow out {:ipv6_range=>"::/0", :port=>443, :protocol=>"tcp"}
     ✔  is expected to allow out {:ipv4_range=>"0.0.0.0/0", :port=>443, :protocol=>"tcp"}
     ✔  inbound_rules_count is expected to eq 3
     ✔  outbound_rules_count is expected to eq 2

〜省略〜

Test Summary: 8 successful, 0 failures, 0 skipped

少し消化不良感はありますが・・・成功です!

おわりに

今回の実践はここまでとなります。
実際にテストが動くところを見ると、テンションが上がりますね!

やってみれば意外と簡単にできるものだなぁと思う反面、実際にプロジェクトで使っていくのはさらなる精進が必要だと感じています。

  • 自動テストでプロジェクトをどう良くしたいのか?
  • 自動テストはあくまで手段。手段が目的とならないように
  • 開発をしている時の自動テストと、運用し始めてからの自動テストでは、目的が異なる

・・・などなど、考慮すべき点がたくさんあって、「まだまだ修行していかないと!」 と思った次第です!

参考情報リンクまとめ