LoginSignup
57

More than 5 years have passed since last update.

Amazon API GatewayとAWS Lambda Python版

Last updated at Posted at 2016-01-17

Amazon API GatewayAWS Lambdaを組み合わせて、サーバーレスアーキテクチャを実現する件について書きます。Pythonで。

API Gateway

API Gatewayとは

API GatewayはRestful WebAPIを提供するために、ラッパーになってくれるサービス。
自身もCloudFrontと結合されているというか、そのラッパーとして実装されているようです。CloudFrontといえばCDNというイメージですが、AWS WAFもCloudFrontと結合されているあたり、CDNに限らずLayer7のネットワークサービスという位置づけになったのかもしれません。CDNだけだと競合も多く、ある意味コモディティ化していきそうですしね。

API Gatewayの構成要素

  • API
    • 作成するAPI(の器)。
  • Resource
    • APIのパス。いわゆるルーティングの定義です。
    • example.com/hoge/fugaのhogeとかfugaの部分。
    • (当たり前だが)example.com/hoge/fugaのfugaとexample.com/fugaのfugaは別物として定義します。
    • "var"ではなく"{var}"のように定義すると、パス変数になります。
  • Method
    • HTTP Method(GET/POST/PUT, etc.)を定義します。
    • 一つのResourceに対して複数定義できます。
  • Integration
    • API Gatewayから見たBackendの定義。CloudFrontのOriginに対応しているのかと。
    • Methodと1対1
    • HTTPでアクセス可能なエンドポイントのほか、本題のLambdaも起動可能です。
    • Methodから渡されるRequest Bodyを変換することが可能。
  • Integration Response
    • IntegrationからのResponse(Status Code、Response Body)と作成するAPIのResponseを対応づけます。
    • 一つにMethodに対して複数指定可能(クライアントに返したいステータスコードの数だけ)。
    • Status Codeのマッピングと、Response Bodyのフォーマット変換が可能。
  • Method Response
    • Integration ResponseのStatus CodeとResponse Model(Json Schema)を紐づける
    • 初期状態で二つのModelが定義されています。
      • Empty(パススルー用)
      • Error(エラーメッセージ用)

サンプル

API Gateway

せっかくなのでPythonからAPIを作成。冒頭のregion, function, roleを適宜変更してください。

createapi.py
# -*- coding: utf-8 -*-

import boto3
client = boto3.client('apigateway')
region = 'ap-northeast-1'
function = 'arn:aws:lambda:ap-northeast-1:AWS_ACCOUNT_ID:function:YOUR_LAMBDA_FUNCTION'
role = 'arn:aws:iam::AWS_ACCOUNT_ID:role/YOUR_IAM_ROLE_FOR_INVOCATION'


def create_api():
    rest_api = client.create_rest_api(
        name='sample01',
        description='sample api',
    )
    return rest_api['id']


def create_resource(rest_api_id):
    for resource in client.get_resources(
        restApiId=rest_api_id
    )['items']:
        if resource['path'] == '/':
            path_root_id = resource['id']
    new_resource = client.create_resource(
        restApiId=rest_api_id,
        parentId=path_root_id,
        pathPart='{hoge}',
    )
    return new_resource['id']


def setup_method(rest_api_id, resource_id):
    client.put_method(
        restApiId=rest_api_id,
        resourceId=resource_id,
        httpMethod='GET',
        authorizationType='NONE',
    )
    uri = 'arn:aws:apigateway:' + region + ':lambda:path/2015-03-31/functions/' + function + '/invocations'
    client.put_integration(
        restApiId=rest_api_id,
        resourceId=resource_id,
        httpMethod='GET',
        type='AWS',
        integrationHttpMethod='POST',
        uri=uri,
        credentials=role,
        requestTemplates={
            'application/json': get_request_template()
        },
    )
    client.put_integration_response(
        restApiId=rest_api_id,
        resourceId=resource_id,
        httpMethod='GET',
        statusCode='200',
        responseTemplates={
            'application/json': '',
        },
    )
    client.put_method_response(
        restApiId=rest_api_id,
        resourceId=resource_id,
        httpMethod='GET',
        statusCode='200',
        responseModels={
            'application/json': 'Empty',
        },
    )
    client.put_integration_response(
        restApiId=rest_api_id,
        resourceId=resource_id,
        httpMethod='GET',
        statusCode='400',
        selectionPattern='^\[400:.*',
        responseTemplates={
            'application/json': get_response_template(),
        },
    )
    client.put_method_response(
        restApiId=rest_api_id,
        resourceId=resource_id,
        httpMethod='GET',
        statusCode='400',
        responseModels={
            'application/json': 'Error',
        },
    )


def get_request_template():
    """
    ref. http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
    """
    return """
{
  "pathParams": {
#foreach ($key in $input.params().path.keySet())
    "$key": "$util.escapeJavaScript($input.params().path.get($key))"#if ($foreach.hasNext),#end
#end
  }
}
"""


def get_response_template():
    """
    ref. http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
    """
    return """
#set($data = $input.path('$'))
{
  "message" : "${data.errorMessage}"
}
"""


def deploy(rest_api_id):
    client.create_deployment(
        restApiId=rest_api_id,
        stageName='snapshot',
        stageDescription='snapshot stage',
    )
    client.update_stage(
        restApiId=rest_api_id,
        stageName='snapshot',
        patchOperations=[
            {
                'op': 'replace',
                'path': '/*/*/logging/loglevel',
                'value': 'INFO',
            },
        ],
    )

if __name__ == '__main__':
    rest_api_id = create_api()
    resource_id = create_resource(rest_api_id)
    setup_method(rest_api_id, resource_id)
    deploy(rest_api_id)
    api_url = 'https://' + rest_api_id + '.execute-api.' + region + '.amazonaws.com/snapshot/'
    print 'OK : {0}'.format(api_url + 'hoge')
    print 'NG : {0}'.format(api_url + 'fuga')

Lambda

Lambdaのデプロイ方法については本題から外れるので、コードだけ。

lambdemo.py
# -*- coding: utf-8 -*-


def lambda_handler(event, context):
    hoge = event['pathParams']['hoge']
    if hoge == 'hoge':
        return {'message': 'hogehoge'}
    else:
        raise NotHogeError(hoge)


class NotHogeError(Exception):
    def __init__(self, hoge):
        self.hoge = hoge

    def __str__(self):
        return '[400:BadRequest] {0} is not hoge'.format(self.hoge)

動作確認

上のcreateapi.pyを実行すると、(バグってなければ)こんな感じで二つURLが表示されます。

createapi.pyの実行結果
OK : https://xxxxx.execute-api.somewhere.amazonaws.com/snapshot/hoge
NG : https://xxxxx.execute-api.somewhere.amazonaws.com/snapshot/fuga

OKの方にアクセスすると

OK
$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/snapshot/hoge
{"message": "hogehoge"}

NGの方だと、怒られます。

NG
$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/snapshot/fuga  -w '%{http_code}\n'
{
  "message" : "[400:BadRequest] fuga is not hoge"
}
400

説明

Integration

LambdaのURI

バックエンドとなるLambdaのURIを指定する必要があります。
複雑ですが仕様が決まっているようで、リージョンとLambda Function ARNから決まります。
最後の「invocations」を抜かしていると「500 internal server error」を返してくれます。地味なはまりポイント。

URIの作り方
uri = 'arn:aws:apigateway:' + region + ':lambda:path/2015-03-31/functions/' + function + '/invocations'

リクエストフォーマット変換

Velocityテンプレートを使ってLambda用のリクエスト(json)を作ります。
使用可能な変数はリファレンスを参照してください。
サンプルではハードコードしてますが、実運用ではテンプレートファイルとして外出しするのが良いかと思います。

request_template.vm
{
  "pathParams": {
#foreach ($key in $input.params().path.keySet())
    "$key": "$util.escapeJavaScript($input.params().path.get($key))"#if ($foreach.hasNext),#end
#end
  }
}

Lambdaの作り方

リクエスト

上記でフォーマット変換したデータはPythonの場合、eventにdictとして入ってます。

リクエストの取得
hoge = event['pathParams']['hoge']

レスポンス

LambdaからはAPI Gatewayにレスポンスを返す際は、returnかraiseを使います。

正常終了
return {'message': 'hogehoge'}
異常終了
raise NotHogeError(hoge)

次に説明しますがreturnで返してしまうと、クライアントにはステータスコード200で返ります。
(正確にはデフォルトのステータスコードですが。。。)

Integration Response

Lambdaからのレスポンスをクライアントレスポンスに変換します。

  • 正規表現(デフォルトにする場合はなし)
  • ステータスコード
  • content-type
  • テンプレート(VTL) の組で指定します。

テンプレートを指定しない(''にする)場合、lambdaからのレスポンスがそのまま帰ります。
この場合はMethod Responseでresponse modelをEmptyにしておきます。

ステータスコードを200で返すだけなら簡単なのですが、クライアントエラー(400番台)やサーバーエラー(500番台)、あるいはリダイレクトしたい(300番台)等で返すことも必要になります。
そのためには、どのようなレスポンスの時にどのステータスコードにするのか、正規表現で対応づけることになります。

正規表現はどこを見ているのか

ここでサンプルのLambdaを書き換えてみます。

returnしてみると
else:
        #raise NotHogeError(hoge)
        return '[400:BadRequest] {0} is not hoge'.format(hoge)
200で返っちゃう
$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/snapshot/fuga -w '%{http_code}\n'
"[400:BadRequest] fuga is not hoge"200

ステータスコードが200になってしまいました。。。
実はLambda handlerの返り値を見ているわけではないんですね。

これも
return {'message' : '[400:BadRequest] {0} is not hoge'.format(hoge)}
これもだめ
return {'errorMessage' : '[400:BadRequest] {0} is not hoge'.format(hoge)}

色々やってたどり着いたのがこれです。

exception.py
class NotHogeError(Exception):
    def __init__(self, hoge):
        self.hoge = hoge

    def __str__(self):
        """ここ!"""
        return '[400:BadRequest] {0} is not hoge'.format(self.hoge)

ということで、ステータスコードを変えたいときは例外を投げようということらしいです。

レスポンスフォーマット変換

正常系の場合はLambdaのレスポンスをパススルーで返せばよいのですが、jsonではなくXMLやHTMLで返す場合は変換する必要があります。また、異常系の場合、パススルーだとStackTraceも返ってしまい情けないことになるので、エラーメッセージだけ返すようにします。

元々ErrorというResponse Modelが定義されているので、それに合わせてテンプレートを作ります。

response_template.vm
#set($data = $input.path('$'))
{
  "message" : "${data.errorMessage}"
}

まとめ

API Gatewayってそんなに簡単じゃなくねって思いましたが、そもそもちゃんとAPI設計するならこれくらい考えろってことな気もしてきました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57