読者です 読者をやめる 読者になる 読者になる

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

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

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

音樂はSoundCloud等バラバラの場所に公開中です。申し訳ないがlinkをたどるなどして探してください。

考察は現在は主に此のblogで公表中です。

programmingは、ひろくみせるものはGitHubで、個人的なものはBitBucketで開発中です。

c4se

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

Programming Python Terraform AWS AWS_APIGateway AWS_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をデプロイ。

ハイ。