Serverless Framework (1.0.0-beta2) が在ります。
Slideshare 版です: Serverless
.。oO(此のblogは、主に音樂と考察と Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)
音樂は SoundCloud に公開中です。
考察は現在は主に Scrapbox で公表中です。
Programming は GitHub で開發中です。
AWS の設定を Terraform で行ってゐる。マネージメントコンソールからからやると簡單な設定であっても AWS CLI や Terraform からやらうとすると大變なものに成る場合も在る。コンソールでは一つに纏められてゐるものが個々別のオブジェクトとして其々操作しなければならないものや、裏で自動で作られるものも明示して作らねばならないものが在り、設定が膨れる。大變だ。一回だけなら膨れても何でも問題に成らないのだが、似たオブジェクトを沢山作らねばならぬと成れば、大變だ。否、似たものであれば纏められる筈である。Terraform にはモジュール機能が在る。
以前 API Gateway と Lambda を連携させて Terraform から設定た (API Gateway と Lambda の組み合はせでリリースするバージョンを制御する)。
此所で作ったものは、
有り體に此れ丈が API を作る度に必要に成る。死にたく成る。抽象化して呉れ。
書いてゐると判るが、以下の纏まりは重複する。Lambda は aws_lambda_function 毎に、
が。API Gateway では aws_api_gateway_method 毎に、
が繰り返す。此れ等を其々モジュールに纏めてみる。
出來上がったものが以下に在るのでコードを読めば解ると云ふ話も在る。→ https://github.com/ne-sachirou/c4se-infra/tree/31bcdcaf8bc11c210163087c01b4087115d9eccd/terraform/modules
Terraform のモジュールは、Terraform のコードを分離する機能だ。モジュールを呼び出す時變數を與へる事が出來る。即ち、變數を切り替へる丈で重複させられるコードは、同じモジュールで扱へる。引數を與へ Terraform のコードを吐く函數の樣なものだ。
terraform-community-modulesに例が在る。
モジュールのファイルは、入力・本体・出力に分けて以下の樣にされる事が多い。
例として以下の樣に使へる "a" と云ふモジュールを書く。
module "a_of_sample" { source = "./a" var1 = "sample" } resource b { attr = "${module.a_of_sample.out1}" }
./a
ディレクトリに三っつのファイルを作る。
# a/variables.tf variable "var1" {}
# a/main.tf resource some_resource { attr = "${var.var1}" }
# a/outputs.tf output "out1" { value = "${var.var1}" }
ファイルの分け方は通常の Terraform と同じく任意なので、もっと細かく分けてもよい。variables.tf と outputs.tf は此う置いておくのが他人から見易いであらう。
モジュールを使った Terraform を実行する時には、plan を作る前に terraform get
を行なふ。
terraform get
terraform plan --out=terraform.tfplan
terraform apply terraform.tfplan
GitHub 上等に在るモジュールを使ってゐれば .terraform/
に clone され、ローカルにモジュールを作ってあれば .terraform/
下にシンボリックリンクが張られる。.terraform/
は.gitignore する。
モジュールに限らず Terraform では条件分岐は未だ出來ない。ループは少しは出來る。以下で實例を下[もと]に詳細を見てゆく。
を纏めてみる。繰り返すコードをただ抽出するだけで出來る。
引數は、
variable "description" {} variable "filename" {} variable "function_name" {} variable "handler" {} variable "memory_size" {} variable "prod_function_version" { default = "$LATEST" } variable "role" {} variable "runtime" {} variable "timeout" {}
でよいであらう。出力は、他から Lambda を呼び出すのに function_name が要るから、
output "function_name" { value = "${var.function_name}" }
と成らう。するとメインは此う成る。
resource "aws_lambda_function" "function" { description = "${var.description}" filename = "${var.filename}" function_name = "${var.function_name}" handler = "${var.handler}" memory_size = "${var.memory_size}" role = "${var.role}" runtime = "${var.runtime}" source_code_hash = "${base64sha256(file(var.filename))}" timeout = "${var.timeout}" } resource "aws_lambda_alias" "staging" { depends_on = ["aws_lambda_function.function"] description = "staging" function_name = "${var.function_name}" function_version = "$LATEST" name = "staging" } resource "aws_lambda_alias" "prod" { depends_on = ["aws_lambda_function.function"] description = "production" function_name = "${var.function_name}" function_version = "${var.prod_function_version}" name = "prod" } resource "aws_lambda_permission" "staging_apigateway" { action = "lambda:InvokeFunction" depends_on = ["aws_lambda_function.function"] function_name = "${var.function_name}" principal = "apigateway.amazonaws.com" qualifier = "staging" statement_id = "${var.function_name}_staging_apigateway" } resource "aws_lambda_permission" "prod_apigateway" { action = "lambda:InvokeFunction" depends_on = ["aws_lambda_function.function"] function_name = "${var.function_name}" principal = "apigateway.amazonaws.com" qualifier = "prod" statement_id = "${var.function_name}_prod_apigateway" }
此れ等を ./modules/aws_lambda_function_for_apigateway
に保存しやう。呼び出しは此う成る。殆ど aws_lambda_function の内容だけで済む。
module "aws_lambda_function_for_apigateway_sample" { source = "./modules/aws_lambda_function_for_apigateway" description = "Sample." filename = "./sample.zip" function_name = "sample" handler = "handler.handler" memory_size = 128 prod_function_version = "1" role = "${aws_iam_role.sample.arn}" runtime = "python2.7" timeout = 3 }
Lambda コードの ./sample.zip
や Lambda 実行權限の aws_iam_role.sample.arn
は今迄通り用意する必要が在る。本番デプロイして publish version した度に prod_function_version
を上げてゆく。
さて、先の Lambda を呼び出す API Gateway を作ってみる。
は今迄通り作っておく。aws_api_gateway_resource.sample_sample
は /sample
を指すとする。
をモジュールにしやう。
モジュールの前に、aws_api_gateway_method_response と aws_api_gateway_integration_response をエラーのステータスコードの個數分作るのはループするのが有効である。ループは、洞の resource にも在る count 属性に依って行なへる。例へば、
resource a "a_with_count" { count = 2 attr = "${count.index}" }
は、
resource a "a_with_count_0" { attr = 0 } resource a "a_with_count_1" { attr = 1 }
と、resource 名を除き等しい。又モジュールに配列は渡せないから、文字列分割函數 split 函數を使って、
variable "status_codes" { default = "400,500" } resource a "a_with_status_codes" { count = "${length(split(",", var.status_codes))}" status_code = "${element(split(",", var.status_codes), count.index)}" }
とループ出來る。何とかして呉れ。
ループも出來る樣に成ったのでモジュール化する。マッピングも全て與へなければならないので引數はいささか多い。成るべく省略出來るやう default を定義しておく。
variable "authorization" { default = "NONE" } variable "aws_region" {} variable "aws_account_id" {} variable "error_status_codes" {} variable "function_name" {} variable "http_method" {} variable "integration_request_parameters_in_json" { default = "{}" } variable "integration_request_templates" { default = <<EOF { "stage": "$stageVariables.stage" } EOF } variable "integration_response_parameters_in_json" { default = "{}" } variable "integration_response_templates" { default = "$input.json('$')" } variable "method_request_parameters_in_json" { default = "{}" } variable "method_response_parameters_in_json" { default = "{}" } variable "resource_id" {} variable "rest_api_id" {}
此れでもう大体見える。出力 (output) は不要だらう。メインは大体長い。長いからモジュール化したので仕方が無い。
resource "aws_api_gateway_method" "method" { authorization = "${var.authorization}" http_method = "${var.http_method}" request_parameters_in_json = "${var.method_request_parameters_in_json}" resource_id = "${var.resource_id}" rest_api_id = "${var.rest_api_id}" } resource "aws_api_gateway_integration" "integration" { depends_on = ["aws_api_gateway_method.method"] http_method = "${var.http_method}" integration_http_method = "POST" request_parameters_in_json = "${var.integration_request_parameters_in_json}" request_templates = { "application/json" = "${var.integration_request_templates}" } resource_id = "${var.resource_id}" rest_api_id = "${var.rest_api_id}" type = "AWS" uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${var.function_name}:$${stageVariables.stage}/invocations" } resource "aws_api_gateway_method_response" "method_response_200" { depends_on = ["aws_api_gateway_method.method"] http_method = "${var.http_method}" resource_id = "${var.resource_id}" response_parameters_in_json = "${var.method_response_parameters_in_json}" rest_api_id = "${var.rest_api_id}" status_code = "200" } resource "aws_api_gateway_integration_response" "integration_response_200" { depends_on = [ "aws_api_gateway_integration.integration", "aws_api_gateway_method_response.method_response_200" ] http_method = "${var.http_method}" resource_id = "${var.resource_id}" response_parameters_in_json = "${var.integration_response_parameters_in_json}" response_templates = { "application/json" = "${var.integration_response_templates}" } rest_api_id = "${var.rest_api_id}" status_code = "200" } resource "aws_api_gateway_method_response" "method_response_error" { count = "${length(split(",", var.error_status_codes))}" depends_on = ["aws_api_gateway_method.method"] http_method = "${var.http_method}" resource_id = "${var.resource_id}" rest_api_id = "${var.rest_api_id}" status_code = "${element(split(",", var.error_status_codes), count.index)}" } resource "aws_api_gateway_integration_response" "integration_response_error" { count = "${length(split(",", var.error_status_codes))}" depends_on = ["aws_api_gateway_integration.integration"] http_method = "${var.http_method}" resource_id = "${var.resource_id}" response_templates = { "application/json" = <<EOF { "error": "$input.path('$.errorMessage').substring(5)" } EOF } rest_api_id = "${var.rest_api_id}" selection_pattern = "${element(split(",", var.error_status_codes), count.index)}: .+" status_code = "${element(split(",", var.error_status_codes), count.index)}" }
aws_api_gateway_method_response.method_response_error
と aws_api_gateway_integration_response.integration_response_error
が count を使ってゐる。エラーメッセージは、「500: Some error.」等 \d{3}:
が先頭に附くのを前提した。Terraform はモジュールに配列や連想配列を渡せないので此うしないとモジュールに出來ない。
此れ等を ./modules/aws_api_gateway_method_for_lambda_function
に保存する。呼び出し例として GET /sample を作ってみる。
module "aws_api_gateway_method_research_random_id_get" { source = "./modules/aws_api_gateway_method_for_lambda_function" aws_region = "${var.aws_region}" aws_account_id = "${var.aws_account_id}" error_status_codes = "400,500" function_name = "${module.aws_lambda_function_for_apigateway_sample.function_name}" http_method = "GET" resource_id = "${aws_api_gateway_resource.sample_sample.id}" rest_api_id = "${aws_api_gateway_rest_api.smaple.id}" }
マッピングが default で定義したもので濟めば此の樣に短い。
やりましたね!
Lambda と API Gateway はServerless Framworkに載せ替へたので上記モジュールは不要と化しましたがわたしは元氣です。
DynamoDB にはトランザクションは無いしロック等無い。RDS 使へ。好い加減にしろ。
然し DynamoDB には魅力が在るし、少々トランザクション出來なからうが此れを使ひたいといふ欲求の在る場面もある。楽観的ロック位いは出來ないだらうか。
楽観的ロックと云へば私にとっては ActiveRecord の lock_version だ。UPDATE items SET name = "New Name", lock_version = 43 WHERE id = 1 AND lock_version = 42
等のやうに、比較と更新をアトミックに行なへれば此れは實裝出來る。
DynamoDB ではテーブルを跨がなければ、比較と更新がアトミックに出來る。從ってテーブルを跨がない楽観的ロックは實裝出來る。詰りテーブルを跨がないトランザクションは實裝出來る。
以下の DynamoDB テーブルが在るとする。id, name, lock_version を持たせやう。
resource "aws_dynamodb_table" "item" { attribute { name = "id" type = "S" } hash_key = "id" name = "item" read_capacity = 1 write_capacity = 1 }
記法は Terraform。
PutItem 時に condition-expression を附けると、condition-expression の結果が僞である時にエラーを起こし更新せぬやうに出來る。
cf. 条件式を使用した条件付きの書き込みの実行 - Amazon DynamoDB
Python でやると以下の如し。行が無い爲 lock_version 列も無い時か、或いは lock_version 列が變更されてゐなければ、PutItem を實行する。
import boto3 table = boto3.resource("dynamodb").Table("item") class Item(object): def __init__(id, **props): self.id = id self.name = props.get("name", None) self.lock_version = props.get("lock_version", 0) def get_item(id): item = Item(id=id) res = table.get_item( Key={"id": id} ) if "Item" in res: item.name = res["Item"]["name"] item.lock_version = res["Item"]["lock_version"] return item def put_item(item): lock_version_attr = boto3.dynamodb.conditions.Attr("lock_version") table.put_item( Item={ "id": item.id, "name": item.name, "lock_version": item.lock_version + 1 }, ConditionExpression=lock_version_attr.not_exists().__or__(lock_version_attr.eq(item.lock_version)) ) item.lock_version += 1 if __name__ == "__main__": item = get_item("mOmonga") item.name = "New name" put_item(item)
此の ConditionExpression=lock_version_attr.not_exists().__or__(lock_version_attr.eq(item.lock_version))
が条件式を組み立ててゐる。
もっと綺麗な組み立て方をしたい。
Python アプリケーション毎に環境を分ける (pyenv + virtualenv (or venv) + pip)の續きのやうなもの。
Ruby | Python | |
---|---|---|
Lint | Rubocop | flake8 |
テスト | RSpec | unittest |
モック | RSpec | mock (or unittest.mock) |
時間に依存したテスト | Timecop (or ActiveSupport::Testing::TimeHelpers) | freezegun |
テストランナー | Rspec | unittest |
タスクランナー | Rake | Invoke |
ファイル監視 | Guard | watchdog + Invoke |
追記 20161107 flake8-pep257 は flake8-docstrings に置き換へられた。
PEP8 と PEP257 と云ふコーディングスタイル標準が在る。flake8 を使へば此れに從ってゐるかテスト出來る。
flake8 3.0.2 : Python Package Index
インストールには此う。
pip install pep8 flake8 flake8-pep257
其の儘実行するには問題が在る。PEP257 の D203 と D211 と云ふ規則は互いに矛盾してゐる。洞ちらかに従へばよいので、洞ちらかを無視させる。又一行の長さが 80 文字はいささか短いので、160 文字にしてやらう。
D203: 1 blank line required before class docstring
D211: No blank lines allowed before class docstring
.flake8 と云ふファイルをプロジェクト root に作ってやる。
[flake8] ignore = D203 max-line-length = 160
flake8 を実行するには、flake8
でよい。
因みに以下のコードは docstring や空行等で PEP8 に従はない。地の文が在るから。
テスト記述とテストランナーには Python 附属の unittest を使ふ。
2 系 25.3. unittest — ユニットテストフレームワーク — Python 2.7.x ドキュメント
3 系 26.4. unittest — ユニットテストフレームワーク — Python 3.5.1 ドキュメント
定番っぽいのと云へばpytestやtoxなのだらうが、附属のものを使ってみる。後で tox にするかも……。
標準のやり方が見當らなかったので當てずっぽうである。tests ディレクトリを作りテストコードを置く。
app.py lib.py tests ├ test_app.py └ test_lib.py
以下のコードだとしやう。
# app.py def succ(n): return n + 1
テストコードは此う成るだらうか。
# tests/test_app.py from app import succ try: import unittest2 as unittest except ImportError: import unittest class TestSucc(unittest.TestCase): def test_succ(self): self.assertEqual(43, succ(42))
變哲も無い。unittest ライブラリのテストディスカバリを使って実行する。
python -m unittest discover -s tests
此れで tests ディレクトリ下から unittest.TestCase を繼承したテストケースを全て実行出來る。
26.5. unittest.mock — モックオブジェクトライブラリ — Python 3.5.1 ドキュメント
Python3 だと附属の unittest の一部と成ってゐるらしい。Python2 には無いので、pip install mock
する。
patch でモックして、呼び出しを assert してみる。次のコードをテストする。
# app.py import lib class SampleException(Exception): pass class Sample(object): def __init__(base): self.base = base def is_even(n): return n % 2 == 0 def sample(self, n): if self.is_even(n): raise SampleException("%d is even." % n) if lib.is_prime(n): return 1 else: return n - 1
機能紹介を意図とした、無理矢理なテストコードを書かう。
# tests/test_app.py from app import ( Sample, SampleException ) try: import unittest2 as unittest except ImportError: import unittest try: import unittest.mock except ImportError: import mock class TestSample(unittest.TestCase): def setUp(self): self.fourtytwo = Sample(42) def test_init(self): self.assertEqual(Sample(1).base, 1) def test_is_even(self): self.assertTrue(Sample(0).is_even()) self.assertFalse(Sample(1).is_even()) self.assertTrue(Sample(2).is_even()) self.assertFalse(Sample(3).is_even()) def test_sample_is_even(self): with\ mock.patch("lib.is_prime") as is_prime, mock.patch.object(self.fourtytwo, "is_even", return_value=True) as is_even: with self.assertRaises(SampleException): self.fourtytwo.sample(2) is_prime.assert_not_called() is_even.assert_called_once_with(2) def test_sample_is_prime(self): with\ mock.patch("lib.is_prime", return_value=True), mock.patch.object(self.fourtytwo, "is_even", return_value=False): self.assertEqual(self.fourtytwo.sample(2), 1) def test_sample_is_not_prime(self): with\ mock.patch("lib.is_prime", return_value=False), mock.patch.object(self.fourtytwo, "is_even", return_value=False): self.assertEqual(self.fourtytwo.sample(2), 1)
patch, patch.object, return_value, assert_called_once_with, assert_not_called を使ってみた。
時間を計算する式を書いてはテストとして元も子もない時や、閏日等で現在時刻を偽装したい時が在る。又テスト中に時間が経過しては assertEqual に失敗する場合も在るだらう。現在時刻をモックしたいのだ。此れが freezegun である。
spulec/freezegun: Let your Python tests travel through time
from datetime import datetime from freezegun import freeze_time from pytz import timezone try: import unittest2 as unittest except ImportError: import unittest class TestExample(unittest.TestCase): @freeze_time(datetime.now()) def test_freeze(self): self.assertEqual(datetime.now(), datetime.now()) @freeze_time(datetime(2016, 4, 1, 7, tzinfo=timezone("Asia/Tokyo"))) def test_freeze(self): self.assertEqual( datetime.now(timezone("Asia/Tokyo")), datetime(2016, 4, 1, 7, tzinfo=timezone("Asia/Tokyo")) )
Invoke と云ふ便利なツールが在る。未だ正式版ではないが、Fabricと云ふデプロイツール (Ruby で謂ふ Capistrano や Mina) にも採用されてゐる。
Welcome to Invoke! — Invoke documentation
pip install invoke
する。tasks.py
と云ふファイルを作ると Invoke が其れをタスク定義ファイルとして扱ふ。
from invoke import run, task @task def test(context): try: run("flake8 *.py tests") run("python -m unittest discover -s tests") except Exception: pass
此れで inv test
或いは invoke test
で flake8 と unittest を実行出來る。
Node.js 界隈には有りふれてゐる。ファイルを変更したら、其れに應じてテストを実行しビルドする仕組みが欲しい。watchdog と云ふライブラリで簡單に実装出來る。
watchdog 0.8.3 : Python Package Index
pip install watchdog
する。inv watch
で、ファイルを更新する度にテストを実行するやうにしてみる。
from invoke import run, task import time from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer class WatcherHandler(FileSystemEventHandler): def __init__(self, context): super(FileSystemEventHandler, self).__init__() self.context = context def on_modified(self, event): test(self.context) @task def test(context): try: run("flake8 *.py tests") run("python -m unittest discover -s tests") except Exception: pass @task def watch(context): observer = Observer() observer.schedule(WatcherHandler(context), ".", recursive=True) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join()
大体わからう。
ハイ。
Ruby では rbenv + Bundler でやる。Haskell では Stack で。Elixir では exenv + Hex で。Node.js では ndenv + npm で。PHP では phpenv + composer で。
API Gateway と Lambda の組み合はせでリリースするバージョンを制御する の樣に Lambda で色々やってゐるので此の邊りを整へた。
Ruby | Python | |
---|---|---|
実行環境のバージョンを分ける | rbenv | pyenv + virtualenv |
依存ライブラリをインストールする | gem | pip |
依存ライブラリのバージョンを固定する | Bundler | pip + virtualenv |
依存ライブラリを更新する | Bundler | pip |
pip の色んなオプションを駆使する事に成る。
ではやる。
システムには Python2 と Python3 を入れてある。此れは開發には使はないが、システムの色んな何かが依存してゐるので、其の儘にしておく。必ず其の儘にしておく。
pyenv を入れる。anyenv を入れてあるので其れを使ふ。
anyenv install pyenv exec $SHELL -l
Python の 2 系と 3 系を両方入れる。本日の最新版は 3.5.2 と 2.7.12 だ。
pyenv install 2.7.12 pyenv install 3.5.2
pyenv は pip も入れてくれる。
virtualenv を入れる。pyenv のプラグインが在るので此れを使ふ。
git clone https://github.com/yyuu/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.zprofile exec $SHELL -l
example と云ふプロジェクトを開發するとしやう。AWS Lambda なので 2 系である。つらい。
mkdir example cd example pyenv virtualenv 2.7.12 example pyenv local example
此れで example と云ふ環境が virtualenv 側に作られた。virtualenv のオプションに渡した example と云ふ名は、システムの中では一意でなければならない。此の名は他人と共有しないので、自分の事だけを考へて附ければよい。環境の設定を共有しないのは Ruby からの Python の違ひだ。開發環境を閉じ込めるのは Ruby では Bundler だけでやるが、Python では virtualenv と pip に分離されてゐる。環境を閉じ込めるのは virtualenv でやり、環境の設定は pip で行なふ。pip で設定したところは、他人と共有する。
pyenv-virtualenv は virtualenv を pyenv と協働させてくれる。example ディレクトリの .python-version に virtualenv で作った環境の名が書かれてある。此のディレクトリ下に cd してくる度に virtualenv の環境が切り替はる。べんり。また 2 系では virtualenv を、3 系では Python 附属の venv を使って呉れるらしい。べんり。
環境を消すには、
pyenv uninstall example
する。
依存ライブラリを入れてゆく。virtualenv で環境が閉じ込められてゐるので、何も考へず example ディレクトリ下でインストールすればよい。
pip install boto3
pip install invoke
pip install watchdog
pip install unittest2
# 等
AWS Lambda では依存ライブラリを纏めて zip に入れて上載しなければならない。Lambda の実行時に使ふ依存ライブラリは、virtualenv の環境ではなく、プロジェクトのディレクトリにインストールしなければならない。
pip install pytz -t . # 等
boto3 は AWS 環境に既に在るので、開發時の依存関係扱ひである。
import boto3 from pytz import timetone def lambda_handler(event, context): # ゴニョゴニョ
序でに云ふと、
rm -f ~/Desktop/example.zip zip -r ~/Desktop/example.zip .
で AWS Lambda に上載できる zip が作られる。
pip freeze
と云ふコマンドで Gemfile.lock 相当のものが出力される。
pip freeze > requirements.txt
argh==0.26.2 boto3==1.3.1 botocore==1.4.41 docutils==0.12 futures==3.0.5 invoke==0.13.0 jmespath==0.9.0 linecache2==1.0.0 pathtools==0.1.2 python-dateutil==2.5.3 PyYAML==3.11 six==1.10.0 traceback2==1.4.0 unittest2==1.1.0 watchdog==0.8.3
ファイル名は任意である。requirements.txt や requirements-dev.txt, requirements-test.txt, requirements27.txt 等のファイル名にする場合が多いやうだ。
requirements.txt を git に含める。記録したライブラリをインストールするには、
pip install -r requirements.txt
とする。
全て更新する場合は、
pip install --upgrade -r requirements.txt pip freeze > requirements.txt
一つだけ更新する場合は、
pip install --upgrade boto3 pip freeze > requirements.txt
アンインストールするには、
pip uninstall boto3
pip freeze > requirements.txt
安心して virtualenv (or venv) に任せやう。
ハイ。