c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

DynamoDBで楽観的lockを行なふ

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のunittestを書かう。其れを自動実行しやう (rake test + guardを置き換へやう)

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

Lint

追記 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 ドキュメント

定番っぽいのと云へばpytesttoxなのだらうが、附属のものを使ってみる。後で 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 で謂ふ CapistranoMina) にも採用されてゐる。

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()

大体わからう。

ハイ。

Pythonアプリケーション毎に環境を分ける (pyenv + virtualenv (or venv) + pip)

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) に任せやう。

ハイ。

AWS EC2インスタンスの一覧を.ssh/config形式で吐く (Crystalで)

aws ec2 describe-instancesJSON として取得する。

JSON.mapping で Crystal の class に対応させる。

よしなに。

#!/usr/bin/env crystal run

require "json"

module Instances
  class Tag
    JSON.mapping(
      value: {type: String, key: "Value"},
      key: {type: String, key: "Key"},
    )
  end

  class Instance
    JSON.mapping(
      private_ip_address: {type: String, key: "PrivateIpAddress", nilable: true},
      tags: {type: Array(Tag), key: "Tags", nilable: true},
    )
  end

  class Reservation
    JSON.mapping(
      instances: {type: Array(Instance), key: "Instances"},
    )
  end

  class Instances
    JSON.mapping(
      reservations: {type: Array(Reservation), key: "Reservations"},
    )
  end
end

instances = Instances::Instances.from_json(`aws ec2 describe-instances --profile example-production --output json`)
hosts = instances
  .reservations
  .flat_map(&.instances)
  .map { |instance| {instance.private_ip_address, instance.tags.try(&.find { |tag| tag.key == "Name" }).try(&.value)} }
  .select { |private_ip_address, name| private_ip_address && name }
  .map { |private_ip_address, name| "Host #{name}\n  HostName #{private_ip_address}" }
  .join("\n")
puts hosts
config = File.read("#{ENV["HOME"]}/.ssh/config", "UTF-8")
config += "\n### EXAMPLE BEGIN ###\n### EXAMPLE END ###" unless config =~ %r(^### EXAMPLE BEGIN ###\n.+^### EXAMPLE END ###$)m
config = config.sub(%r(^### EXAMPLE BEGIN ###\n.+^### EXAMPLE END ###$)m, "\n### EXAMPLE BEGIN ###\n#{hosts}\n### EXAMPLE END ###")
File.write("#{ENV["HOME"]}/.ssh/config", config, File::DEFAULT_CREATE_MODE, "UTF-8")

# vim:set ft=crystal:

cf. 殺したサーバーを一覧する

API GatewayとLambdaの組み合はせでリリースするバージョンを制御する

追記 20161018 Serverless Framework をお勧めします。1.0 が出ました。 Serverless Framework (1.0.0-beta2) が在ります - c4se 記:さっちゃんですよ ☆

サーバーレスって奴です。

API Gateway から Lambda を呼ぶ

此の節は前説なので、図だけ見つつ次の節へ読み飛ばすことも出來ます。

version を制御しない所からはじめてみます。

を実装してゆきます。

f:id:Kureduki_Maari:20160726121749p:plain

事前に以下のものを作成します。

上では「sample.example」と成ってゐるドメインを取得しておいてください。例へば私であれば「c4se.jp」です。其れを Route53 に Host zone として登録しておきます。

resource "aws_route53_zone" "primary" {
  name = "sample.example"
}

resource "aws_route53_record" "primary_root_a_record" {
  name = "sample.example"
  records = ["xxx.xxx.xxx.xxx"]
  ttl = "300"
  type = "A"
  zone_id = "${aws_route53_zone.primary.zone_id}"
}

記法は Terraform です。

HTTPS が使へるやうに、*.sample.example 等の証明書を取得しておいてください。

DynamoDB のテーブルを設計し作成します。DynamoDB でなくとも RDS でも何でも好ろしいです。御好きな奴でやるので、此所では DynamoDB でやります。

resource "aws_dynamodb_table" "example_user" {
  name = "example_user"
  attribute {
    name = "id"
    type = "S"
  }
  hash_key = "id"
  read_capacity = 1
  write_capacity = 1
}

IAM ロールを作ります。Lamnda 函數に割り当てるロールです。Lambda に対し、Lambda 実行権限 (arn:aws:iam::aws:policy/AWSLambdaExecute) をアタッチし、DynamoDB の讀み書き権限 (dynamodb:GetItem, dynamodb:PutItem) を與へます。「lambda_example_exec」の名で IAM ロールを作ってみます。

resource "aws_iam_policy_attachment" "AWSLambdaExecute" {
  name = "AWSLambdaExecute"
  policy_arn = "arn:aws:iam::aws:policy/AWSLambdaExecute"
  roles = [
    "${aws_iam_role.lambda_example_exec.name}"
  ]
}

resource "aws_iam_role" "lambda_example_exec" {
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": ["lambda.amazonaws.com"]
      }
    }
  ]
}
EOF
  name = "lambda_crud_random_exec"
}

resource "aws_iam_role_policy" "lambda_example_exec" {
  name = "lambda_example_exec"
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem"
      ],
      "Effect": "Allow",
      "Resource": [
        "${aws_dynamodb_table.example.arn}"
      ]
    }
  ]
}
EOF
  role = "${aws_iam_role.lambda_example_exec.id}"
}

aws_iam_policy_attachment は一つのポリシーに一回しか使へないので Terraform のコードを分割する時に aws_iam_policy_attachment もバラさないやう注意してください。

此所迄が事前準備です。やっと Lambda 函數を作ります。「example」といふ Lambda 函數を作りませう。node.js でも Python でも Java でも node.js にぶら下げた任意のバイナリ (多くの Lambda 用ツールは此の方法で Go を実行可能にしてゐます。Crystal でも Haskell でも何でも) でも何でもよいので、此所では Python でコードを書く事にします。Python で書いた理由はAnsible モジュールを書いて Python に慣れたからです。

加へて API Gateway からの呼び出しを許可しておきます。

resource "aws_lambda_function" "example" {
  description = "Example."
  filename = "example.zip"
  function_name = "example"
  handler = "main.lambda_handler"
  memory_size = 128
  role = "${aws_iam_role.lambda_example_exec.arn}"
  runtime = "python2.7"
  source_code_hash = "${base64sha256(file("example.zip"))}"
  timeout = "${var.timeout}"
}

resource "aws_lambda_permission" "example_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.example"]
  function_name = "${aws_lambda_function.example.function_name}"
  principal = "apigateway.amazonaws.com"
  statement_id = "example_apigateway"
}

先に作った IAM ロールを Lambda 函數に當ててゐます。

Lamnbda のコードは此んな感じです。

import boto3
import logging
import re
import traceback

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class EventValidationException(Exception):
    def __str__(self):
        return "400: %s" % self.message

def get_user(event):
    for key in ["id"]:
      if key not in event:
          raise EventValidationException("`id` should be required.")
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def post_user(event):
    for key in ["id", "name"]:
      if key not in event:
          raise EventValidationException("`%s` should be required." % key)
    if not re.match(r"\A[0-9A-Za-z]{8,64}\Z", event["id"]):
        raise EventValidationException("`id` should match [0-9A-Za-z]{8,64}")
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def lambda_handler(event, context):
    try:
        logger.info(event)
        if event["_method"] == "GET":
            get_user(event)
        elif event["_method"] == "POST":
            post_user(event)
        else:
            raise NotImplementedError()
    except Exception as e:
        logger.error("%s\n%s" % (e, traceback.format_exc()))
        m = re.match(r"\A\d{3}: ", e.__str__())
        if (not m) or (m and m.group(0)[0:3] not in ["400", "500"]):
            e = Exception("500: %s" % e)
        raise e

DynamoDB とごにょごにょやるのは boto3 を呼び出して色々やるだけです。「色々やる」の所で色々やってください。

此の Python コードを「main.py」として保存し、zip に格納します。

example.tf
example.zip
├ main.py
└ other.py等

先に Lambda 函數のハンドラを handler = "main.lambda_handler" と定義しました。此れはおそらく Lambda 側で from main import lambda_hander として呼ばれるので、さうできるやうにしませう。

正常実行時には dict を返します。異常終了には Exception を raise します。node.js だと異常終了時には context.fail("Message.") します。後で API Gateway でエラーのレスポンスを設定する時に関ります。

さて API Gateway をやります。先は長い。以下のものを作る必要があります。

aws_api_gateway_rest_api (example)
├ aws_api_gateway_deployment (prod)
└ aws_api_gateway_resource (/user)
    └ aws_api_gateway_resource (/user/{id})
        ├ aws_api_gateway_method (GET)
        │  ├ aws_api_gateway_integration
        │  ├ aws_api_gateway_method_response
        │  └ aws_api_gateway_integration_response
        └ aws_api_gateway_method (POST)
            ├ aws_api_gateway_integration
            ├ aws_api_gateway_method_response
            └ aws_api_gateway_integration_response

f:id:Kureduki_Maari:20160726121808p:plain

example と云ふ APIAPI Gateway に作ります。

Terraform でいふ「deployment」は AWS コンソールでいふと「ステージ」です。development と production 等と分ける事が出來ます。今は「prod」だけ作ります。

rest_apiAPI の集合です。rest_api に resource で URL を作り、method をぶら下げて REST API を作ります。

method では method、integration、integration_response、method_response の四つの部品を組みます。method と integration で入力を処理し、method_response と integration_response で出力を処理します。method と method_response で HTTP リクエストからの入出力を始末し認証やパラメータや HTTP ステータスコードの許可を行います。integration と integration_response でバックエンドとの入出力を司りパラメータを JSON に変換したりエラーを HTTP ステータスコードに変換したりします。今回のバックエンドは Lambda です。

  • var.aws_region
  • var.aws_account_id

は適当に設定してあるものとします。

resource "aws_api_gateway_rest_api" "example" {
  description = "Dummy endpoint for example."
  name = "example"
}

resource "aws_api_gateway_deployment" "example_prod" {
  depends_on = [
    "aws_api_gateway_integration.example_user_id_get",
    "aws_api_gateway_integration.example_user_id_post"
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  stage_description = "production"
  stage_name = "prod"
}

# /user
resource "aws_api_gateway_resource" "example_user" {
  parent_id = "${aws_api_gateway_rest_api.example.root_resource_id}"
  path_part = "user"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# /user/{id}
resource "aws_api_gateway_resource" "example_user_id" {
  parent_id = "${aws_api_gateway_resource.example_user.id}"
  path_part = "{id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# {{{ GET /user/{id}

resource "aws_api_gateway_method" "example_user_id_get" {
  authorization = "NONE"
  http_method = "GET"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_get" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "_method": "GET"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.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:${aws_lambda_function.example.function_name}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_get_200" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_get_400" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_get_500" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_500.status_code}"
}

# }}} GET /user/{id}

# {{{ POST /user/{id}

resource "aws_api_gateway_method" "example_user_id_post" {
  authorization = "NONE"
  http_method = "POST"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_post" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "name": "$input.path('$.name')",
  "_method": "POST"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.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:${aws_lambda_function.example.function_name}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_post_200" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_post_400" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_post_500" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_500.status_code}"
}

# }}} POST /user/{id}

かう成ります。

request_template 等の記法は VTL (Velocity Template Language) です。オブジェクトに対しては Java のメソッドが其の儘呼べます。

reourse に {id} として設定してあると、method は URL に書かれたパラメーターを自動で integration 迄渡して呉れます。さうでないパラメーターは、GET のクエリーも POST の body もヘッダーも全て method と integration に明示しなければなりません。其れと API GatewayJSON を解するのですが、JSON しか解さない爲、POST 等の body が application/json の場合は素通しできますが、application/x-www-form-urlencoded で來る場合はすざましい request_template を書かねばなりません。今の所は。

レスポンスも全てを設定しなければいけません。Lambda から正常終了で返された JSON を其の儘返すだけならば、200 番の method_response と 200 番のデフォルト integration_response を設定するだけで濟みます。其の場合エラーはスタックトレース迄含めて 500 番でユーザーに返されます。エラーを返すには、Exception の message に正規表現を掛けて、ステータスコードや body やヘッダーをマップしてやります。正規表現が掛けられる樣に Lambda 側で message を整形しておかねばなりません。頑張る。今の所は。

バックエンドから切り離された APIゲートウェイとしては必要な設定ばかりではあります。

此れで API Gateway の設定は一段落する。rest_api を prod ステージにデプロイしてやると、 https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/user/1 等としてアクセス出來ます。

テスト等のやり方は全てすっ飛ばしてゐますが今回の話題ではないからです。未だ前説です。今回の話題迄到達してゐません。

後はドメインを設定します。Terraform は API Gateway のカスタムドメインに対応してゐず、やらうとすると大掛かりに成るから AWS コンソールからやってもよいかもしれません。今の所は。API Gateway のカスタムドメインの頁から証明書を設定してやり、その後 Route53 から其所に向けたレコードを作成します。

f:id:Kureduki_Maari:20160726121836p:plain

そしてゼーレヴェはついに大洋を渡り丘へと登る。

API Gateway の設定と Lambda のコードを staging と production に分ける

本題です。

先ず Lambda のバージョンを管理しませう。Lambda の alias と publish-version を組み合はせます。

publish-version は Lambda 函數のコードや設定のスナップショットを作りバージョンを附ける機能です。バージョンは publish-version を行う度に連番が自動で振られます。Lambda 函數には$LATESTと云ふ特別なバージョンが初めから在ります。此れは常に、アップロードした最新のコードを指します。バージョンを指定せず呼ぶとLambdaは$LATEST を呼びます。

AWS CLI からは aws lambda publish-version コマンドでやれます。aws lambda publish-version help が man です。

alias は Lambda 函數の或るバージョンに名前を附ける機能です。此れで staging には\$LATEST を當て、prod には publish-version した或るバージョンを當てると、staging で最新の設定やコードを検証した後 prod に適用する事が出來ます。

先に作った Lambda 函數の名は example でした。其々の alias は example:staging と example:prod として呼べます。

Terraform で alias を作成し、alias が指すバージョンを指定します。aws_lambda_permission は其々の alias に対して作ってやる必要が在ります。var.example_prod_function_version と云ふ變數で prod のバージョンを設定出來るやうにしておきます。

variable "example_prod_function_version" {
  default = "1"
}

resource "aws_lambda_function" "example" {
  description = "Example."
  filename = "example.zip"
  function_name = "example"
  handler = "main.lambda_handler"
  memory_size = 128
  role = "${aws_iam_role.lambda_example_exec.arn}"
  runtime = "python2.7"
  source_code_hash = "${base64sha256(file("example.zip"))}"
  timeout = "${var.timeout}"
}

resource "aws_lambda_alias" "example_staging" {
  depends_on = ["aws_lambda_function.example"]
  description = "staging"
  function_name = "${aws_lambda_function.example.function_name}"
  function_version = "$LATEST"
  name = "staging"
}

resource "aws_lambda_alias" "prod" {
  depends_on = ["aws_lambda_function.example"]
  description = "production"
  function_name = "${aws_lambda_function.example.function_name}"
  function_version = "${var.example_prod_function_version}"
  name = "prod"
}

resource "aws_lambda_permission" "example_staging_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.example"]
  function_name = "${aws_lambda_function.example.function_name}"
  principal = "apigateway.amazonaws.com"
  qualifier = "staging"
  statement_id = "example_staging_apigateway"
}

resource "aws_lambda_permission" "example_prod_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.example"]
  function_name = "${aws_lambda_function.example.function_name}"
  principal = "apigateway.amazonaws.com"
  qualifier = "prod"
  statement_id = "example_prod_apigateway"
}

次に API Gateway を staging と prod に分け、此れを Lambda の alias と結び附けます。API Gateway で使ふ機能はステージ (deployment) とステージ變數です。

ステージは API Gateway を公開する機能であり、より正しくは API Gateway の設定のスナップショットを公開する機能です。staging と prod の二つのステージを作り、staging にデプロイして検証してから prod にデプロイすればバージョンを管理できます。

ステージ變數はステージ毎に管理される環境變數のやうなものです。此れを使って Lambda 函數の alias を呼び分け、又 Lambda 函數内で処理を分岐します。

deployment を二つ作り、其々に stage と云ふステージ變數を定義します。alias を出し分けるには、integration にて呼ぶ Lambda 函數の uri に alias 名をくっ附けておきます。ステージ變數を lambda 函數に迄渡してやるには、integrarion の request_template でマップしてやります。

resource "aws_api_gateway_rest_api" "example" {
  description = "Dummy endpoint for example."
  name = "example"
}

resource "aws_api_gateway_deployment" "example_staging" {
  depends_on = [
    "aws_api_gateway_integration.example_user_id_get",
    "aws_api_gateway_integration.example_user_id_post"
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  stage_description = "staging"
  stage_name = "staging"
  variables = {
    stage = "staging"
  }
}

resource "aws_api_gateway_deployment" "example_prod" {
  depends_on = [
    "aws_api_gateway_integration.example_user_id_get",
    "aws_api_gateway_integration.example_user_id_post"
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  stage_description = "production"
  stage_name = "prod"
  variables = {
    stage = "prod"
  }
}

# /user
resource "aws_api_gateway_resource" "example_user" {
  parent_id = "${aws_api_gateway_rest_api.example.root_resource_id}"
  path_part = "user"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# /user/{id}
resource "aws_api_gateway_resource" "example_user_id" {
  parent_id = "${aws_api_gateway_resource.example_user.id}"
  path_part = "{id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# {{{ GET /user/{id}

resource "aws_api_gateway_method" "example_user_id_get" {
  authorization = "NONE"
  http_method = "GET"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_get" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "stage": "$stageVariables.stage",
  "_method": "GET"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.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:${aws_lambda_function.example.function_name}:$${stageVariables.stage}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_get_200" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_get_400" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_get_500" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_500.status_code}"
}

# }}} GET /user/{id}

# {{{ POST /user/{id}

resource "aws_api_gateway_method" "example_user_id_post" {
  authorization = "NONE"
  http_method = "POST"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_post" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "name": "$input.path('$.name')",
  "stage": "$stageVariables.stage",
  "_method": "POST"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.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:${aws_lambda_function.example.function_name}:$${stageVariables.stage}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_post_200" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_post_400" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_post_500" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_500.status_code}"
}

# }}} POST /user/{id}

此の内で、

resource "aws_api_gateway_integration" "example_user_id_get" {
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${aws_lambda_function.example.function_name}:$${stageVariables.stage}/invocations"
}

${aws_lambda_function.example.function_name}:$${stageVariables.stage} と云ふ部分が呼び分けてゐる所で、此れは Terraform 実行後の AWS 上で example:${stageVariables.stage} と成ります。

又、

resource "aws_api_gateway_integration" "example_user_id_get" {
  request_templates = {
    "application/json" = <<EOF
{
  "stage": "$stageVariables.stage"
}
EOF
  }
}

と integration でマップしてやったので、Lambda 函數には event に stage と云ふキーで "staging" か "prod" が渡されます。讀み書きする DynamoDB のテーブルを切り替へる等色々出來るでせう。

import boto3
import logging
import re
import traceback

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class EventValidationException(Exception):
    def __str__(self):
        return "400: %s" % self.message

def get_user(event):
    for key in ["id", "stage"]:
      if key not in event:
          raise EventValidationException("`id` should be required.")
    if event["stage"] == "prod":
        table_name = "example_user"
    else:
        table_name = "s-example_user"
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def post_user(event):
    for key in ["id", "name", "stage"]:
      if key not in event:
          raise EventValidationException("`%s` should be required." % key)
    if not re.match(r"\A[0-9A-Za-z]{8,64}\Z", event["id"]):
        raise EventValidationException("`id` should match [0-9A-Za-z]{8,64}")
    if event["stage"] == "prod":
        table_name = "example_user"
    else:
        table_name = "s-example_user"
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def lambda_handler(event, context):
    try:
        logger.info(event)
        if event["_method"] == "GET":
            get_user(event)
        elif event["_method"] == "POST":
            post_user(event)
        else:
            raise NotImplementedError()
    except Exception as e:
        logger.error("%s\n%s" % (e, traceback.format_exc()))
        m = re.match(r"\A\d{3}: ", e.__str__())
        if (not m) or (m and m.group(0)[0:3] not in ["400", "500"]):
            e = Exception("500: %s" % e)
        raise e

構成は此んな感じに成ってゐます。

f:id:Kureduki_Maari:20160726121849p:plain

更新の手順は以下のやうに成る筈です。Terraform と AWS CLI を前提とする手順ではありますが。

最初のデプロイ:

  1. aws_lambda_alias.prod の function_version を "\$LATEST" にしておく。Lambda 函數作成前で、publish_version もしてゐずバージョンが未だ無い爲。
  2. Lambda と API Gateway をデプロイ。
  3. API Gateway を staging ステージにデプロイ。
  4. staging で検証。
  5. Lambda を publish_version して、Version をメモする。
  6. aws_lambda_alias.prod の function_version を 上でメモしたバージョンに書き換へる。
  7. Lambda をデプロイ。
  8. API Gateway を prod ステージにデプロイ。

API Gateway の設定を更新した場合:

  1. API Gateway をデプロイ。
  2. API Gateway を staging ステージにデプロイ。
  3. staging で検証。
  4. API Gateway を prod ステージにデプロイ。

Lambda の設定やコードを変更した場合:

  1. Lambda をデプロイ。
  2. staging で検証。
  3. Lambda を publish_version して、Version をメモする。
  4. aws_lambda_alias.prod の function_version を 上でメモしたバージョンに書き換へる。
  5. Lambda をデプロイ。

ハイ。