JAWS-UG 福岡 #4
AWS の荒木さんや盛田さんを迎えて、先月行われた JAWSDAYS の報告会を兼ねた JAWS-UG 福岡勉強会第四回に参加した。
日頃、直接人と会う機会が少ないので、久しぶりに大勢の人が集まる勉強会に緊張したが、お酒の勢いであっという間に緊張は解けて楽しい時間を過ごすことが出来た。
毎回ながら、コアな話が聞けて沢山の刺激を頂いた。
懇親会でのイカがめちゃくちゃ美味しかったのがハイライトだった。
完成度はそこそこにある程度の段階で一般に公開することでのメリットを感じた一日だった。公開することで各ジャンルの識者に色々と指摘頂いて、最終的には完成度が上がるし、自分の勉強にもなる一石二鳥な感じで嬉しかった。
一日だった。
moto という AWS SDK のレスポンスを模倣する Python ライブラリについて引き続きです。
moto には Stand-alone Server Mode という Flask で実装された Mock サーバーが提供されていて、HTTP サーバーなので Python に限らず、他の言語ライブラリからも利用出来るのがスーパーメリットだと思いました。
実際に GitHub リポジトリにも https://github.com/spulec/moto/tree/master/other_langs というディレクトリに Ruby や Java から利用するサンプルが提供されています。
$ cat requirements.txt moto[server] $ pip install -r requirements.txt
$ moto_server --help usage: moto_server [-h] [-H HOST] [-p PORT] [service] positional arguments: service optional arguments: -h, --help show this help message and exit -H HOST, --host HOST Which host to bind -p PORT, --port PORT Port number to use for connection
$ moto_server ec2 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
以下のようにインスタンス一覧を取得するだけのライブラリをサンプルとして利用します。
require 'aws-sdk' class MyEc2 def ec2 @ec2 ||= Aws::EC2::Client.new(profile: 'mock_profile', region: 'us-west-2', endpoint: 'http://127.0.0.1:5000') end def list_ec2_instances instance_ids = [] ec2.describe_instances.reservations.each do |res| res.instances.each do |instance| instance_ids << instance.instance_id end end instance_ids end def main list_ec2_instances.each do |instance| instance end end end # myec2 = MyEc2.new # p myec2.main
moto_server の URL とポートを Aws::EC2::Client
のインスタンスを生成する際の引数として endpoint
を指定するだけで moto_server が利用出来ます。
最後の 2 行のコメントアウトを外して実行すると以下のようなレスポンスが得られます。
$ bundle exec ruby my_ec2.rb []
moto_server を起動したコンソールでは以下のようにログが出力されていることを確認出来るかと思います。
$ moto_server ec2 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 127.0.0.1 - - [21/Apr/2017 00:37:27] "POST / HTTP/1.1" 200 -
上記の Ruby スクリプト(ライブラリ)を moto と Rspec でテストしてみたいと思いますので、以下のようなテストを書きます。
$LOAD_PATH.push('./') require "spec_helper" require "my_ec2" require "pty" describe 'MyEc2' do let(:myec2) { MyEc2.new } before(:all) do @output, @input = PTY.spawn("moto_server ec2") client = Aws::EC2::Client.new(region: 'us-west-2', endpoint: 'http://127.0.0.1:5000', profile: 'mock_profile') res = client.run_instances({ image_id: 'ami-20d1c544', min_count: 3, max_count: 3 }) @instance_ids = res.instances.map { |instance| instance.instance_id } end it "#ec2" do res = myec2.ec2 expect(res).to be_an_instance_of(Aws::EC2::Client) end it '#list_ec2_instances' do res = myec2.list_ec2_instances expect(res).to eq @instance_ids end it '#main' do res = myec2.main expect(res).to eq @instance_ids end end
ポイントは before
句で moto_server にアクセスして EC2 インスタンス 3 台を Run Instance している部分です。
moto_server が実行出来る環境にて、以下のように rspec を実行してみます。
$ bundle exec rspec spec MyEc2 #ec2 #list_ec2_instances #main Finished in 1.47 seconds (files took 0.16527 seconds to load) 3 examples, 0 failures
うまくテストが通ったようです。
Rspec の書き方がだいぶん怪しいですが、moto_server を使うことでリアルな AWS リソースにアクセスすることなく、スクリプトやライブラリのテストやデバッグが言語に関係無く出来るといのは嬉しい限りです。
有難うございました。
moto については 4/22(土)に厳かに執り行われる「JAWS-UG福岡:Reboot#4、荒木さんとAWSの話をしてみたり JAWS DAYS 参加者から話をきいてみたりしよう」にて語らせて頂く予定でござります。
尊敬するモト冬樹さんのブログ、「ツルの一声」というタイトルでグッと身近に感じたかっぱです。
モト春樹と名乗らせて頂きたいと思います。
昨年後半から今年にかけて boto3 や aws-sdk for ruby を使って、ちょっとしたツールやライブラリを書かせてもらう機会が増えてきました。有難いことなのですが、動作確認を行うあたって、AWS リソースを作ったり、消したりしながら書いていたりすると、いつのまにかソースコードがデグレしていたりして、その非効率さに頭を悩ませていました。
そんな悩みもあり、ツールやライブラリを書いたらユニットテストくらいは書けるようになりたいということで、テストを書くにあたって必要そうなモジュールを調べていたところ、moto という boto3(boto や boto-core) の結果をシュミレートしてくれるモジュールに出会いましたので試してみました。
moto なのか boto なのかごっちゃになりそうです。
$ python --version Python 2.7.13 $ pip list --format=columns | egrep 'moto|boto|coverage' boto 2.44.0 boto3 1.4.2 botocore 1.4.85 coverage 4.2 moto 0.4.30
インスタンス ID 一覧を取得する簡単なライブラリです。
import boto3 def get_client(): """ Returns the ec2 boto3 client """ return boto3.client('ec2') def list_ec2_instances(): """ List EC2 InstanceId """ ec2 = get_client() response = ec2.describe_instances() if response: for res in response.get('Reservations', []): for instance in res.get('Instances', []): yield instance['InstanceId'] def main(): """ Main entry """ for instance in list_ec2_instances(): print instance if __name__ == '__main__': main()
コマンドラインで実行すると以下のようにインスタンス ID がヅラヅラ〜と表示されるだけです。
$ python ec2.py i-12345678901234567 i-12345678 i-xxxxxxxxxxxxxxxxx
以下のようにテストを書きました。
ポイントは各テストケースにデコレータとして mock_ec2
を付与している部分。また、__moto_setup
でダミーの EC2 インスタンスを run_instances
している部分だと思います。
import sys import os import StringIO import unittest from moto import mock_ec2 from ec2 import get_client, list_ec2_instances, main class Ec2TestCase(unittest.TestCase): def setUp(self): """ setUp will run before execution of each test case """ pass @mock_ec2 def __moto_setup(self): """ Run Instance """ ec2 = get_client() reservation = ec2.run_instances(ImageId='ami-f00ba4', MinCount=1, MaxCount=1) self.instance_id = reservation['Instances'][0]['InstanceId'] def tearDown(self): """ tearDown will run after execution of each test case """ pass @mock_ec2 def test_get_client(self): """ check that out get_client function has a valid endpoint """ ec2 = get_client() self.assertEqual(ec2._endpoint.host, 'https://ec2.ap-northeast-1.amazonaws.com') @mock_ec2 def test_list_ec2_instances(self): """ check that our bucket shows as expected """ instances = [e for e in list_ec2_instances()] self.assertEqual([], instances) @mock_ec2 def test_main(self): """ verifies the execution of the main function """ # setup ec2 environment self.__moto_setup() # capture stdout for processing sys.stdout = mystdout = StringIO.StringIO() # run main function main() content = mystdout.getvalue() self.assertEqual(self.instance_id, content.strip())
setup.py 自体は初めて書く機会を得ましたが、Ruby で言うところの rake の Rakefile みたいなものなのかなという理解です。今後、もう少し深掘りしていきたいと思います。
この setup.py を利用してテストを実行することになります。
from setuptools import setup, find_packages setup( name='oreno-pj2', version='0.0.1', description="Oreno Sample Project", license='GPLv2', author='inokappa', author_email='xxxxxxxxxxxxxxxxxx', packages=find_packages( exclude=['tests'] ), test_suite='tests', install_requires=[ 'boto3' ], tests_require=[ 'moto' ], entry_points={ 'console_scripts': [ 'ec2 = ec2:main' ] }, )
早速、テストを実行してみたいと思います。
$ python setup.py test
以下のように結果が出力されます。
$ python setup.py test running test running egg_info writing requirements to oreno_pj2.egg-info/requires.txt writing oreno_pj2.egg-info/PKG-INFO writing top-level names to oreno_pj2.egg-info/top_level.txt writing dependency_links to oreno_pj2.egg-info/dependency_links.txt writing entry points to oreno_pj2.egg-info/entry_points.txt writing manifest file 'oreno_pj2.egg-info/SOURCES.txt' running build_ext test_get_client (tests.ec2_test.Ec2TestCase) ... ok test_list_ec2_instances (tests.ec2_test.Ec2TestCase) ... ok test_main (tests.ec2_test.Ec2TestCase) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.357s OK
いい感じです。
その名の通り、Python コードのカバレッジを計測するモジュールです。
coverage を利用することで、テストを実行してカバレッジの計測や計測した結果を HTML 等でも表示することが出来ます。
coverage run
で Python スクリプトを実行させることが出来ます。
$ coverage run setup.py test
以下のように出力されます。
running test running egg_info writing requirements to oreno_pj2.egg-info/requires.txt writing oreno_pj2.egg-info/PKG-INFO writing top-level names to oreno_pj2.egg-info/top_level.txt writing dependency_links to oreno_pj2.egg-info/dependency_links.txt writing entry points to oreno_pj2.egg-info/entry_points.txt writing manifest file 'oreno_pj2.egg-info/SOURCES.txt' running build_ext test_get_client (tests.ec2_test.Ec2TestCase) ... ok test_list_ec2_instances (tests.ec2_test.Ec2TestCase) ... ok test_main (tests.ec2_test.Ec2TestCase) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.718s OK
coverage report
でカバレッジのレポートを確認することが出来ます。
$ coverage report -m -i --omit=${除外したいパスを記載}
以下のように出力されます。
Name Stmts Miss Cover Missing ------------------------------------------------- ec2.py 15 1 93% 31 setup.py 2 0 100% tests/__init__.py 0 0 100% tests/ec2_test.py 27 0 100% ------------------------------------------------- TOTAL 44 1 98%
coverage html
では coverage report
の結果を HTML で出力させることが出来ます。
$ coverage html --omit=${除外したいパスを記載}
実行しても何も出力されませんが、以下のように htmlconv
ディレクトリ以下の index.html
をブラウザで開くとカバレッジの結果が HTML で確認することが出来ます。
$ open htmlcov/index.html
以下のようにブラウザで確認することが出来ます。
Module
を項目をクリックすると、各プログラム毎のカバレッジを確認することが出来ます。
moto には Stand-alone Server Mode というモードが存在していて、Flask で実装された API サーバーを起動することが出来ます。この Stand-alone Server Mode を利用することで、Python 以外の言語ライブラリでも moto が利用出来るようになるとのことです。
インストールは以下のように。
$ cat requirements.txt moto[server] $ pip install -r requirements.txt
起動は以下のように。
$ moto_server ec2 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
以下のような EC2 インスタンスのリストを取得するようなコードを利用します。
import boto3 ec2 = boto3.resource('ec2', region_name='us-west-1', endpoint_url='http://localhost:5000') for instance in ec2.instances.all(): print instance
実行すると以下のような結果が出力されます。
$ python test.py ec2.Instance(id='i-xxxxxxx1') ec2.Instance(id='i-xxxxxxx2') ec2.Instance(id='i-xxxxxxx3') ec2.Instance(id='i-xxxxxxx4') ec2.Instance(id='i-xxxxxxx5')
先に起動した moto_server には以下のようなログが記録されています。
$ moto_server ec2 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 127.0.0.1 - - [18/Apr/2017 08:12:14] "POST / HTTP/1.1" 200 - 127.0.0.1 - - [18/Apr/2017 08:12:28] "POST / HTTP/1.1" 200 - ... 127.0.0.1 - - [18/Apr/2017 08:20:42] "POST / HTTP/1.1" 200 -
モト春樹が moto をチュートリアルしてみました。
Python でテストを書いた事がそもそも無いのでテストの書き方から学ぶ必要がありましたが、思ったよりも簡単にモックを使ったテストを書くことが出来ました。これから少しずつですが、自分が書くツールやライブラリでは moto を使ったテストも合わせて実装するようにしたいと思います。
Eryastic の話し。
所詮は手作業なので、意図した構成であるかの確認については他のツールに委ねたいという思いがあり、ドメイン作成、削除、更新時には awspec でテスト出来るように spec ファイルを吐くようにしてみました。
$ AWS_PROFILE=xxxxxx AWS_REGION=ap-northeast-1 bundle exec eryastic domain --create --config-file=demo-es1.toml I, [2017-04-16T13:24:26.961332 #23293] INFO -- : 以下の構成で Elasticsearch ドメインを作成します. +-------------------------------+------------------------------------------------------------------------------+ | key | value | +-------------------------------+------------------------------------------------------------------------------+ | domain_name | demo-es1 | | elasticsearch_version | 2.3 | ... | volume_size | 10 | | automated_snapshot_start_hour | 1 | +-------------------------------+------------------------------------------------------------------------------+ 処理を続行しますか ? [y|n]: y I, [2017-04-16T13:24:38.136936 #23293] INFO -- : 処理を続行します. I, [2017-04-16T13:24:39.429898 #23293] INFO -- : 処理が成功しました.
作成が開始されると、spec
ディレクトリには deploy_spec.rb
というファイルが生成されています。(事前に spec
ディレクトリを作っておいてください。)
bash-3.2$ tree spec/ spec/ ├── deploy_spec.rb └── spec_helper.rb 0 directories, 2 files
実際に awspec を実行してみると…
bash-3.2$ AWS_PROFILE=xxxxx AWS_REGION=ap-northeast-1 bundle exec rake spec:deploy ... elasticsearch 'demo-es1' should exist should have access policies "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"\",\n \"Effect\"...pAddress\": {\n \"aws:SourceIp\": \"xxx.xxx.xxx.xxx\"\n }\n }\n }\n ]\n}\n" elasticsearch_cluster_config.instance_type should eq "t2.micro.elasticsearch" elasticsearch_cluster_config.instance_count should eq 3 elasticsearch_cluster_config.dedicated_master_enabled should eq false elasticsearch_cluster_config.zone_awareness_enabled should eq false ebs_options.ebs_enabled should eq true ebs_options.volume_type should eq "gp2" ebs_options.volume_size should eq 10 snapshot_options.automated_snapshot_start_hour should eq 1 domain_name should eq "demo-es1" elasticsearch_version should eq "2.3" Finished in 0.56487 seconds (files took 1.96 seconds to load) 12 examples, 0 failures
いい感じです。
次にインスタンス数を減らしてみたいと思います。
AWS_PROFILE=xxxxx AWS_REGION=ap-northeast-1 bundle exec eryastic domain --update --domain-name=demo-es1 --config-file=demo-es1.toml
以下のように出力されます。
更新処理が開始されると、spec
ディレクトリには deploy_spec.rb
というファイルが生成されています。(既存の deploy_spec.rb
は上書きされます。)
bash-3.2$ tree spec/ spec/ ├── deploy_spec.rb └── spec_helper.rb 0 directories, 2 files
テストを流してみます。
bash-3.2$ AWS_PROFILE=xxxxxx AWS_REGION=ap-northeast-1 bundle exec rake spec:deploy ... elasticsearch 'demo-es1' should exist should have access policies "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"\",\n \"Effect\"...pAddress\": {\n \"aws:SourceIp\": \"xxx.xxx.xxx.xxx\"\n }\n }\n }\n ]\n}\n" elasticsearch_cluster_config.instance_type should eq "t2.micro.elasticsearch" elasticsearch_cluster_config.instance_count should eq 1 elasticsearch_cluster_config.dedicated_master_enabled should eq false elasticsearch_cluster_config.zone_awareness_enabled should eq false ebs_options.ebs_enabled should eq true ebs_options.volume_type should eq "gp2" ebs_options.volume_size should eq 10 snapshot_options.automated_snapshot_start_hour should eq 1 domain_name should eq "demo-es1" Finished in 0.47943 seconds (files took 1.93 seconds to load) 11 examples, 0 failures
実際の構成変更については少々時間がかかる為、マネジメントコンソール上で確認するとインスタンス数は変わっていないかもしれませんが、API 上では意図したインスタンス数になっていることは確認することが出来ます。
ドメインを削除してみます。
bash-3.2$ AWS_PROFILE=oreno-profile AWS_REGION=ap-northeast-1 bundle exec eryastic domain --delete --domain-name=demo-es1 I, [2017-04-16T13:36:00.514878 #26010] INFO -- : 以下の Amazon Elasticsearch Service ドメインを削除します. +----------------------------------------+------------------------------------------------------------------------------+ | key | value | +----------------------------------------+------------------------------------------------------------------------------+ | domain_id | 044703681656/demo-es1 | | domain_name | demo-es1 | ... | automated_snapshot_start_hour | 1 | | rest.action.multi.allow_explicit_index | true | +----------------------------------------+------------------------------------------------------------------------------+ 処理を続行しますか ? [y|n]: y I, [2017-04-16T13:36:04.319120 #26010] INFO -- : 処理を続行します. I, [2017-04-16T13:36:05.091255 #26010] INFO -- : 処理が成功しました.
削除処理が開始されると、spec
ディレクトリには delete_spec.rb
というファイルが生成されています。
bash-3.2$ tree spec/ spec/ ├── delete_spec.rb ├── deploy_spec.rb └── spec_helper.rb 0 directories, 3 files
テストを流してみます。
bash-3.2$ AWS_PROFILE=xxxxx AWS_REGION=ap-northeast-1 bundle exec rake spec ... elasticsearch 'demo-es1' should not exist (FAILED - 1) elasticsearch 'demo-es1' should exist should have access policies "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"\",\n \"Effect\"...pAddress\": {\n \"aws:SourceIp\": \"xxx.xxx.xxx.xxx\"\n }\n }\n }\n ]\n}\n" elasticsearch_cluster_config.instance_type should eq "t2.micro.elasticsearch" elasticsearch_cluster_config.instance_count should eq 1 elasticsearch_cluster_config.dedicated_master_enabled should eq false elasticsearch_cluster_config.zone_awareness_enabled should eq false ebs_options.ebs_enabled should eq true ebs_options.volume_type should eq "gp2" ebs_options.volume_size should eq 10 snapshot_options.automated_snapshot_start_hour should eq 1 domain_name should eq "demo-es1" Failures: 1) elasticsearch 'demo-es1' should not exist Failure/Error: it { should_not exist } expected elasticsearch 'demo-es1' not to exist # ./spec/delete_spec.rb:3:in `block (2 levels) in <top (required)>' Finished in 1.13 seconds (files took 1.68 seconds to load) 12 examples, 1 failure Failed examples: rspec ./spec/delete_spec.rb:3 # elasticsearch 'demo-es1' should not exist
まだ、完全に削除が完了していない為でしょうか、delete_spec.rb
のテストは Failure
となってしまいますし、削除したにも関わらず deploy_spec.rb
のテストは Success
となってしまいますので注意が必要です。暫く時間を置いてから delete_spec.rb
だけを実行してみたいと思います。
eryastic 自体は toml で Amazon Elasticsearch Service の構成を管理する事は出来るので、awspec の spec ファイルを書くことを考えるとコードの二重管理問題が発生しそうですが、自動で吐くようにすることで二重管理問題から解放されて、且つ、環境が意図した内容で構築出来ていることを第三者(第三のツール)の視点で確認出来るというメリットはあると考えています。
余談になりますが、先日 awspec のコミット権を頂きましたので、今後も awspec を頑張っていきつつ Ruby 力を高めていければと考えています。