tl;dr

マージされるか解りませんが、awspec で CloudWatch Logs のテストをしたかったので、CloudWatch Logs リソースタイプの追加をプルリクエストした際の作業内容をメモっておきます。用語の使い方や認識に誤りがあり嘘を書いてしまっていることがあるかもしれませんがご容赦ください。また、ご指摘頂ければ幸いです。

awspec とは

awspec は福岡生まれ*1、福岡育ちの AWS 上で構築したリソースを Rspec の DSL でテストするツールです。

awspec - RSpec tests for your AWS resources.

github.com

まさに地産地消(消費されているのは地元だけではなく世界ですが。)

Rspec 基礎知識

自分の Rspec の知識は以下の記事くらいです。

新年はげましておめでとうございます。 はじめに ずっとやらなきゃって思って引きずってきた Rspec をやってみる ちゃんとうんちくを押さえておきたいけど、まずは写経したりサンプルを弄ったりしながら 合わせて Rspec を Travis CI で実行させてみる 以下、色々と...

inokara.hateblo.jp

プルリクエストして学ぶ awspec

テストしたい AWS リソース

  • CloudWatch Logs

テスト

一応、以下のように書くことを想定しています。

require 'spec_helper'

describe cloudwatch_logs('my-cloudwatch-logs-group') do
  it { should exist }
  its(:retention_in_days) { should eq 365 }
  it { should have_log_stream('my-cloudwatch-logs-stream') }
  it { should have_metric_filter('my-cloudwatch-logs-metric-filter') }
  it do
    should have_subscription_filter('my-cloudwatch-logs-subscription-filter')\
      .filter_pattern('[host, ident, authuser, date, request, status, bytes]')
  end
end

toolbox

awspec では以下の資料のように、リソースタイプを追加しやすいツールが同梱されています。

speakerdeck.com

新しいリソースタイプを追加したい場合には、以下のように実行することで雛形が生成されるので、雛形を修正していくだけで新しいリソースタイプを追加することが出来ます。

bundle exec bin/toolbox template cloudwatch_logs

以下のように出力されます。

$ bundle exec bin/toolbox template cloudwatch_logs
 + lib/awspec/stub/cloudwatch_logs.rb
 + lib/awspec/type/cloudwatch_logs.rb
 + spec/type/cloudwatch_logs_spec.rb
 + lib/awspec/generator/doc/type/cloudwatch_logs.rb
 + doc/_resource_types/cloudwatch_logs.md

Generate CloudwatchLogs template files.

* !! AND add 'cloudwatch_logs' to Awspec::Helper::Type::TYPES in lib/awspec/helper/type.rb *
* !! AND add 'cloudwatch_logs' client to lib/awspec/helper/finder.rb *

Stub

Rspec での Stub とは「あるメソッドが呼ばれたら、任意の値を返す」為に利用されるものという理解。もっとシンプルに言うと、ダミーデータを返すものだと思っておけば良いと勝手に思っています。今回のプルリクエストでは以下のように書きました。

Aws.config[:cloudwatchlogs] = {
  stub_responses: {
    describe_log_groups: {
      log_groups: [
        {
          log_group_name: 'my-cloudwatch-logs-group',
          retention_in_days: 365
        }
      ]
    },
    describe_log_streams: {
      log_streams: [
        {
          log_stream_name: 'my-cloudwatch-logs-stream'
        }
      ]
    },
    describe_metric_filters: {
      metric_filters: [
        {
          filter_name: 'my-cloudwatch-logs-metric-filter'
        }
      ]
    },
    describe_subscription_filters: {
      subscription_filters: [
        {
          filter_name: 'my-cloudwatch-logs-subscription-filter',
          filter_pattern: '[host, ident, authuser, date, request, status, bytes]'
        }
      ]
    }
  }
}

ちなみに、AWS SDK for Ruby では以下の Blog 記事のように標準で Stub が呼べるようになっているとのことです。

ほえー。

Spec

ここでの Spec は awspec を利用する際に書くテストと同じ内容を記載する。

require 'spec_helper'
Awspec::Stub.load 'cloudwatch_logs'

describe cloudwatch_logs('my-cloudwatch-logs-group') do
  it { should exist }
  its(:retention_in_days) { should eq 365 }
  it { should have_log_stream('my-cloudwatch-logs-stream') }
  it { should have_metric_filter('my-cloudwatch-logs-metric-filter') }
  it do
    should have_subscription_filter('my-cloudwatch-logs-subscription-filter')\
      .filter_pattern('[host, ident, authuser, date, request, status, bytes]')
  end
end

但し、Awspec::Stub クラスの load メソッドで Stub を読み込んでいる点だと思います。

Type

Type では実装の Spec ファイルに記述された example の結果を返す処理を記述する部分です。

 def has_log_stream?(stream_name)
      ret = find_cloudwatch_logs_stream(@id).log_stream_name
      return true if ret == stream_name
    end

例えば、上記は example の以下の部分の呼び出しに対応します。

 describe cloudwatch_logs('my-cloudwatch-logs-group') do
  it { should have_log_stream('my-cloudwatch-logs-stream') }
end

have_log_stream がマッチャとなり、呼び出されるメソッドはhas_log_stream?となります。

Helper

Helper モジュールには AWS SDK クライアントの定義、各 AWS リソースを取得する為のメソッドを定義したりします。

/lib/awspec/helper/finder.rb

  • finder モジュールにて CloudWatch Logs 用のモジュールを include する
  • CloudWatch Logs Client を定義する

lib/awspec/helper/finder/cloudwatch_logs.rb

  • AWS SDK を利用して各種リソースを取得する為のコードを定義する
  • 例えば、以下のように CloudWatch Logs の Log group を取得するメソッドを書いたりする
 def find_cloudwatch_logs_group(id)
        cloudwatch_logs_client.describe_log_groups({ log_group_name_prefix: id }).log_groups.last
      end

ちょっとマッチャ

lib/awspec/matcher/have_subscription_filter.rb

今回、一番テストしたかったのが、CloudWatch Subscription filter のフィルタパターンだったんですが、フィルタパターンをテストするにはマッチャを独自に定義する必要たありました。独自のマッチャを定義するには、以下のように RSpec::Matchers.define を呼び出す必要がありました。

 RSpec::Matchers.define :have_subscription_filter do |filter_name|
  match do |log_group_name|
    log_group_name.has_subscription_filter?(filter_name, @pattern)
  end

  chain :filter_pattern do |pattern|
    @pattern = pattern
  end
end

また、lib/awspec/matcher.rb で上記の have_subscription_filter.rb を include してあげる必要があります。

# CloudWatch Logs
require 'awspec/matcher/have_subscription_filter'

そして、以下のように example を記述してフィルタパターンをテストすることが出来ました。

describe cloudwatch_logs('my-cloudwatch-logs-group') do
  it do
    should have_subscription_filter('my-cloudwatch-logs-subscription-filter')\
      .filter_pattern('[host, ident, authuser, date, request, status, bytes]')
  end
end

Document

ちゃんとドキュメントも追加しましょう。
今回はリソースタイプの追加になりますので、doc/_resource_types/cloudwatch_logs.md を以下のように追加しました。

合わせて、自動生成されていた lib/awspec/generator/doc/type/cloudwatch_logs.rb もチェックします。

$ cat lib/awspec/generator/doc/type/cloudwatch_logs.rb
module Awspec::Generator
  module Doc
    module Type
      class CloudwatchLogs 

尚、Awspec::Type::CloudwatchLogs.new('my-cloudwatch-logs-group') の引数 my-cloudwatch-logs-group はドキュメントの引数と合わせておく必要があります。
最後に以下のようにドキュメントを書き出します。

$ bundle exec bin/toolbox docgen > doc/resource_types.md

rubocop

ここまで来るとあとはプルリクエストを…と思った貴方、もうひと頑張りが必要です。rubocop という国家権力(国家は余計)と戦う必要があります。
rubocop とはコーディングルールに準拠しているかをチェックしてくれるツールで、この警察権力のチェックを事前に行っておくことで、プルリクエスト後の Travis CI でのテストも乗り切ることが出来るはずです。

bundle exec rake spec:rubocop

以下のように出力されれば無事に釈放です。

bash-3.2$ bundle exec rake spec:rubocop
Running RuboCop...
Inspecting 346 files
..........................................................................................................................................................................................................................................................................................................................................................

346 files inspected, no offenses detected

ということで

ひとまずテストを動かしてみると…

bash-3.2$ bundle exec rake spec:cloudwatch_logs
(略)

cloudwatch_logs 'my-cloudwatch-logs-group'
  should exist
  should have log stream "my-cloudwatch-logs-stream"
  should have metric filter "my-cloudwatch-logs-metric-filter"
  should have subscription filter "my-cloudwatch-logs-subscription-filter"
  retention_in_days
    should eq 365

Finished in 0.0841 seconds (files took 1.45 seconds to load)
5 examples, 0 failures

おお、Stub のレスポンスとなりますが、テストが通りました。
わーい。

拡張し易い awspec

これまで書いてきましたが、awspec は自分のような Ruby 初心者でも比較的簡単に拡張することが出来ました。これは awspec 実装の際に参考にされたとされる Serverspec でも同様のことが言えると思います。本当に作者の方には感謝の言葉しかありません。有難うございます!
ということで、福岡生まれ、世界育ちの awspec を頑張っていこうと思います。

*1:作者の @k1LoW さんが福岡の株式会社 Fusic にお務めです

元記事はこちら

awspec にプルリクエストした時のメモ