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

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

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

音樂は SoundCloud に公開中です。

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

Programming は GitHub で開發中です。

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 をデプロイ。

ハイ。