awspecを使って、AWSリソース(セキュリティグループ)のテストを自動化してみる

動機

前回に引き続き、AWS環境周りのテスト。 LocalStackで動作確認、motoでmockテストができるようになったところで、本番環境向けにテストしてみたいなと思ってた矢先、 プロが awspec

github.com

という、AWSリソースのテスト(assertion機能付き)フレームワークがあると教えてくれたので、早速使ってみた。

バージョン情報

Windows10

Vagrant2.0

VirtualBox 5.1.26

CentOS 7.2

ruby 2.4.2

ゴール

こんな感じのセキュリティグループを以下の観点でテストしてみたい。

1.Name タグが sampleであること

2.22番ポートはIPアドレス 10.10.10.10 からのみアクセス可能(のみ、が重要)

3.3389番ポートはどこからでもアクセス可能(!)

f:id:yamatatsu-blog:20180205225333p:plain

事前準備

公式手順に従って、ruby gem から awspecをインストールするだけなので省略。。。したいところだが、ただ実行するだけだと、公式ドキュメントの丸パクリ(というか劣化版)になってしまう。 ので、インフラエンジニアの端くれとして多少の差別化をはかり、(申し訳程度に)Vagrantfileを添付することにする。

Vagrant.configure(2) do |config|
      config.vm.provider "virtualbox" do |vb|
        vb.name = "awspec"
        vb.memory = "2048"
      end
      config.vm.box = "bento/centos-7.2"
      config.vm.hostname = 'awspec'
      config.vm.network "private_network", ip: "192.168.100.201"

      config.vm.provision :shell, inline: <<-SHELL 
      for interface in $(grep 'NM_CONTROLLED=no' /etc/sysconfig/network-scripts/ifcfg-* -l)
      do
        sudo sed -i -e 's/NM_CONTROLLED=no/NM_CONTROLLED=yes/' $interface
        sudo ifdown $interface
        sudo ifup $interface
      done
      SHELL

      config.vm.provision :shell, inline: <<-SHELL
        sudo yum install -y git
        sudo git clone https://github.com/sstephenson/rbenv.git /usr/local/rbenv
        sudo git clone https://github.com/sstephenson/ruby-build.git /usr/local/rbenv/plugins/ruby-build
        sudo touch /etc/profile.d/rbenv.sh
        sudo chmod 777 /etc/profile.d/rbenv.sh
        echo 'export RBENV_ROOT="/usr/local/rbenv"' >> /etc/profile.d/rbenv.sh
        echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"'  >> /etc/profile.d/rbenv.sh
        echo 'eval "$(rbenv init -)"'  >> /etc/profile.d/rbenv.sh
        sudo chmod -R 755 /usr/local/rbenv
        sudo yum install -y gcc openssl-devel readline-devel zlib-devel
        sudo -i rbenv install 2.4.2
        sudo -i rbenv global 2.4.2
        sudo -i gem install awspec
        sudo yum install -y epel-release
        sudo yum install -y python-pip
        sudo pip install pip --upgrade
        sudo pip install awscli
      SHELL
end

、、、上記のVagrantfileで作った環境にSSHでログインしたら、aws configure を実行して、credentialを登録。 そのあと、 awspec init を実行し、spec/seacrets.yml を下記のように記述すれば準備完了。

region: ap-northeast-1
aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

実行するコード

spec/security_group_spec.rb を下記のように記述する。

require 'spec_helper'
describe security_group('セキュリティグループID') do
  it { should exist }
  it { should have_tag('Name').value('sample') }
  its(:inbound) { should be_opened_only(22).for('10.10.10.10/32')}
  its(:inbound) { should be_opened(3389).protocol('tcp').for('0.0.0.0/0')}
end

結果

上記のコードを保存したら、Rakefileのあるディレクトリで、rake spec を実行すればテスト完了。 下記のようにオールグリーンのコンソールが出ればO.K.

f:id:yamatatsu-blog:20180205231808p:plain

やってること

記述や実行方式は、serverspecやrspecと同じ。

1. have_tag を用いて、タグのチェックを行っている。

2. be_opened_only を用いて、22番ポートは、10.10.10.10/32から「のみ」アクセスを許可していることをチェックしている。

3. be_opened を用いて、0.0.0.0/0 に対してアクセスを許可していることをチェックしている。

感想

serverspecに慣れ親しんでいる身としては、この方式(環境も)でテストが書けるのはうれしい。

作成した環境の内部の設定はserverspecで今までチェックできたが、セキュリティグループが「許可されている」ことは確認できても、 「余計なものが許可されていない」ことを確認するのは難しかった。 セキュリティグループに限らず、テストの範囲をEC2インスタンスの内部だけでなく、リソース全体に広げることができるので、テストの質が上がりそう。

また、前回、前々回の記事と併せて考えると、下記のような感じで環境構築作業、確認作業が自動化できそう。

(実装フェーズ)

1.LocalStackでCloudFormation や リソース作成の動作確認

2.motoでboto3スクリプトの自動テスト

(テストフェーズ)

3.awspecでリソース作成確認

4.serverspecでサービスの動作確認

どのツールがどの程度まで品質を担保できるものなのかは、もうちょっと使ってみてからまたブログに書きます。

蛇足 ~ be_opened_only のちょっと意地悪な例 ~

be_opened_onlyの揚げ足を取るわけではないのだが、下記のような設定のセキュリティグループ(22番ポートにたいして、10.10.10.9/32と10.10.10.10/32 のIPアドレスを2つ列挙)に対して、10.10.10.8/30 からのみ許可しているかどうかテストすると、感覚的にはテスト成功しそうだが、実際はテスト失敗になる。(ブロードキャストアドレスと、ネットワークアドレスを入れても同様)

f:id:yamatatsu-blog:20180205234850p:plain

require 'spec_helper'
describe security_group('sg-eb68a092') do
  it { should exist }
  it { should have_tag('Name').value('sample') }
  its(:inbound) { should be_opened_only(22).for('10.10.10.8/30')}
  its(:inbound) { should be_opened(3389).protocol('tcp').for('0.0.0.0/0')}
end

。。。しかし、こんな「ポリシーがあるのかないのかよくわからないセキュリティポリシー(笑)」をテストするようなこともないだろうし、あったとしても、本来NGになるべきものがOKになっているわけではないので、無視していいでしょう。