InSpecでAWSリソースのテストを自動化する

1年以上前のリリースになりますが、AWSではInSpecをSSMドキュメントから使用することが可能になっています。

AWS Systems Manager に InSpec by Chef のサポートを追加
おそらく、当時にInspec2.0がリリースされ、クラウドリソースに対するテスト機能がサポートされ始めたことが背景にありそうです。
Announcing InSpec 2.0

InSpecで何ができるのか、とりあえず動かしたい派の人にも合ってそう、ということでAmazon SSMベースのInspecをご紹介です。
InSpec概要
まずは動作確認
SSMによるInSpec実行の仕組み
AWSリソースに対するInspecテスト

InSpec概要

Chef社がOSSとして開発しているテスト向けフレームワークです。 オーケストレーションツールのChefや、テスト自動化ツールのServerspecとは、互換性があります。

InSpec is a run-time framework and rule language used to specify compliance, security, and policy requirements. It includes a collection of resources that help you write auditing controls quickly and easily. The syntax used by both open source and |chef compliance| auditing is the same. The open source |InSpec resource| framework is compatible with |chef compliance|.

後発のInSpecのメリットは、AWSリソースに対するメソッドがサポートされてる点や文法が短縮されている点など、テストコードの簡略化ができることです。 サポート対象のクラウドとして、AWS、Azure、GCP、がInSpecリファレンスに載っています。
InSpec Resources Reference

 

まずは動作確認

InSpec 動作確認

・テスト対象:SSMエージェントをインストール済みのEC2(CentOS 7)
・SSM RunCommandから「AWS-RunInspecChecks」ドキュメントを実行する
・テストコード:DevSecコミュニティがGitHubで公開しているテストコードを使用。
 今回はそのファイル一式をS3に保存してRunCommand実行時に参照する。

SSM RunCommandからInspecを動かしてみます。
S3バケット内は、以下の構成で始めます。

aws s3 ls s3://$My_Bucket/DevSec_practice_linux/
   PRE controls/    ---テストコード.今回はos_spec.rb, package_spec.rb ,sysctl_spec.rbの3つ。
   PRE libraries/   ---今回は未使用テストコードで使用するユーザ定義のメソッドを定義可能.
   303 inspec.yml   ---テスト概要.テスト環境の定義が重要。あとはcopyright等.

SSM RunCommand実行時、InSpecファイルがS3にある場合は、次のようにソース指定します。

{
"path": "https://s3.amazonaws.com/test-tsakurai/DevSec_practice_linux"
}

そのSSM RunCommandをAWS CLIで書くと、次の通りです。

aws ssm send-command --document-name "AWS-RunInspecChecks" \
--targets "Key=instanceids,Values=i-xxxxxxxxxxxxxxxxx" \
--parameters '{"sourceType":["S3"],"sourceInfo":["{\"path\":\"https://s3.amazonaws.com/$My_Bucket/DevSec_practice_linux\"}\n"]}' \
--output-s3-bucket-name "$My_Bucket" --output-s3-key-prefix "ssm" --region ap-northeast-1

 

SSM RunCommandによって正常にInSpec実行が完了すると、AWSコンソールでは次のような画面になります。

AWSコンソール

 

AWSコンソールでも分かる通り、SSM RunCommand実行時のログは3種類、出力されます。 詳細は後続でふれていきます。

・ステップ1(downloadContent)
Content downloaded to /var/lib/amazon/ssm/i-xxxxxxxxxxxxxxxxx/document/orchestration/(SSM実行時に生成されるコマンドID)/downloads/

・ステップ2(runInSpecLinux)
[stdout]
Using existing Chef Development Kit
Executing InSpec tests
Completed InSpec checks and put 81 compliant (81 critical, 0 high, 0 low) and 44 non-compliant (44 critical, 0 high, 0 low) items

[stderr]
※SSM RunCommand実行時の標準エラー出力.

・ステップ3(runInSpecWindows)
Step execution skipped due to incompatible platform. Step name: runInSpecWindows

Linuxサーバに対して実行しているため、Windows用の処理は自動的にスキップ.

 

AWS-RunInspecChecks」を使用するメリットは、実行結果がAWSコンソールから確認可能な状態で保存される点でもあります。

AWS-RunInspecChecks

 

SSMによるInSpec実行の仕組み

検証して分かりましたが、現時点では「AWS-RunInspecChecks」ドキュメントは、テスト対象のサーバOSにおけるテストしかサポートしていません。残念ながら。。 そのため、冒頭でふれたような"AWSリソースに対するInSpecテスト"をSSM経由で実行したい場合、任意のOSスクリプトを準備する必要があります。

"AWSリソースに対するInSpecテスト"に必要なものを確認するため、「AWS-RunInspecChecks」の処理内容をサーバ側から見ていきます。 先述のログにもありますが、「AWS-RunInspecChecks」実行によって、対象サーバには色々とファイルがダウンロードされています。

ll /var/lib/amazon/ssm/i-xxxxxxxxxxxxxxxxx/document/orchestration/{コマンドID}/downloads/
total 20
-rw-r--r-- 1 root root   34 xxx.etag
drw------- 2 root root  228 controls
-rw-r--r-- 1 root root   36 inspec.lock
-rwx------ 1 root root  303 inspec.yml
drw------- 2 root root  158 libraries
-rw-r--r-- 1 root root 3953 report_compliance
-rw-r--r-- 1 root root 2346 run_inspec.sh

RunCommand実行時に参照したS3には無かったファイルがダウンロードされています。

・run_inspec.sh
bashスクリプト。RunCommand実行ログと照合すると、「AWS-RunInspecChecks」は本スクリプトを実行しているだけのようです。「CefDKインストール」、「InSpecテスト実行」、「report_complianceスクリプト起動」、「CefDKアンインストール」、が主な処理内容です。

・report_compliance
rubyスクリプト。run_inspec.shから起動され、InSpecテスト結果をSSMに保存する処理(put_compliance_itemsのAPI)を担当しています。本スクリプト内では、テストコード内のimpact値に基づき、テスト結果を下記3つの重大度に分類しています。

low = 0.0...0.4 high = 0.4...0.7 critical = 0.7..1.0 (その他範囲の値もcritical)

 

run_inspec.sh

run_inspec.shがあることで、テスト対象サーバのChefDK有無を意識せずにInSpecテストを実行できます。なお、ChefDKがインストール済みであればそちらを使用するためインストールはスキップされます。

 

AWSリソースに対するInspecテスト

今回の目的としては、AWSリソースに対するテストファイルを準備するほか、run_inspec.shをSSMドキュメント「AWS-RunShellScript」から使用することで、期待する動作に近づきました。 まず、run_inspec.shを2行だけ修正します。

#
vi run_inspec.sh
 
##修正部分のみ記載
 < export AWS_REGION=ap-northeast-1
 
 > #inspec exec . --reporter json | ruby ./report_compliance
 < inspec exec . -t aws:// --reporter json | ruby ./report_compliance

上記のように、AWSリソースのテストでは、InSpec実行時に「-t」オプションを明示する必要があります。「AWS_REGION」環境変数は、テスト結果をSSMに保存するAWS API(report_compliance)を実行するため必要です。

InSpecテストファイルは任意のディレクトリに保存すればOKです。 次に、テストデータの作成です。

##InSpec初期ファイル作成
#
inspec init profile subnet-test
 
 qqqqqqqqqqqqqqqqqqqqqqqqqqq InSpec Code Generator qqqqqqqqqqqqqqqqqqqqqqqqqqq 
 
Creating new profile at /tools/subnet-test
 ~ Creating file README.md
 ~ Creating directory controls
 ~ Creating file controls/example.rb
 ~ Creating file inspec.yml
 ~ Creating directory libraries
 
# 
cd subnet-test/
# 
ll
total 8
drwxr-xr-x. 2 root root  24 May 27 08:50 controls
-rw-r--r--. 1 root root 217 May 27 08:50 inspec.yml
drwxr-xr-x. 2 root root   6 May 27 08:50 libraries
-rw-r--r--. 1 root root  86 May 27 08:50 README.md
#
vi inspec.yml
 
##修正部分のみ記載
 > platform: os
 < platform: aws

「report_compliance」と修正済みの「run_inspec.sh」は、このinspec.ymlと同じフォルダに置きます。ちなみにこれら2つのスクリプトのコメントには、Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. との記載はありますが、コピーや変更、配布など、無償で許可する点も併記されていました。 ありがたく流用します。

今回は、VPCサブネットに簡単なテストを実行してみます。
VPC内に指定したCIDRのサブネットが存在するか
・全てのサブネットは/24で作成されているか
・サブネットのAZはaゾーンまたはcゾーンを使用しているか

下記内容を.rbファイルとして、controlsディレクトリ内に保存します。

control 'subnet-01' do
  impact 1.0
  title 'check the subnets'
  desc 'check the subnets'
  LIST=[&quot;172.16.0.0/24&quot;, &quot;172.16.1.0/24&quot;, &quot;172.16.2.0/24&quot;]
  describe aws_subnets.where( vpc_id: 'vpc-xxxxxxxx' ) do
    its('cidr_blocks') { should include '172.16.0.0/24' }
    its('cidr_blocks') { should include '172.16.1.0/24' }
    its('cidr_blocks') { should include '172.16.2.0/24' }
  end 
end

control 'subnet-02' do
  impact 1.0
  title 'check subnets cidr'
  desc 'all the cidr is /24 size'
  aws_subnets.where( vpc_id: 'vpc-xxxxxxxx' ).subnet_ids.each do |subnet_id|
    describe aws_subnet(subnet_id) do
      its('cidr_block') { should match(%r{.*\/24}) }
    end
  end
end

control 'subnet-03' do
  impact 1.0
  title 'check subnets AZ'
  desc 'only AZ-a or AZ-c is permitted'
  aws_subnets.where( vpc_id: 'vpc-xxxxxxxx' ).subnet_ids.each do |subnet_id|
    describe aws_subnet(subnet_id) do
      its('availability_zone') { should match(%r{ap-northeast-1a|ap-northeast-1c}) }
    end
  end
end

 

では、SSM RunCommandから「AWS-RunShellScript」ドキュメントを実行します。 作業ディレクトリにinspec initでテストファイル一式を作成したディレクトリパス、実行コマンドに./run_inspec.sh、をRunCommandのパラメータとして指定します。

そのSSM RunCommandをAWS CLIで書くと、次の通りです。

aws ssm send-command --document-name "AWS-RunShellScript" \
--targets "Key=instanceids,Values=i-xxxxxxxxxxxxxxxxx" \
--parameters '{"workingDirectory":["/tools/subnet-test"],"executionTimeout":["3600"],"commands":["./run_inspec.sh"]}' \
--output-s3-bucket-name "$My_Bucket" --output-s3-key-prefix "ssm" --region ap-northeast-1

 

実行結果をAWSコンソールから確認すると、以下のようになります。 1つだけdゾーンを使用しているサブネットがあったため、Non-Compliantになっていることが分かります。 AWSコンソール

 

最後に

テスト自動化まで記載する予定でしたが、思った以上に準備が長くなったため、持ち越します。
基本的には、CloudWatch EventsトリガーのLambda関数で「send-command」APIからInSpecテストを実行し、テスト結果は「list-compliance-items」で取得する、という流れでAWS環境のテストが定期的に自動実行できます。

InSpecはOSSながらAWSをサポートしていますが、インフラテストをコーディングする際、タグ設定を参照できないのはつらい部分です。
※inSpecにおけるAWSリソースに対するクラス実装はこちら

タグ設定をテスト対象にするというより、タグを条件分岐やループの処理で使えると、コーディングも楽になりつつテスト範囲が拡がりそうです。
DIYの精神で自分がやれ、という話はさておき、読者の皆々様がInSpecコミッタ―になる一助になれば幸いです。

 

 

 

このブログの著者

 

 

アプリケーション開発バナー

 

AWS相談会