Serverless by combining Amazon API Gateway and AWS Lambda I will write about realizing the architecture. In Python.
API Gateway
API Gateway is a service that acts as a wrapper to provide Restful Web API. It seems that it is also combined with CloudFront, or implemented as a wrapper for it. CloudFront has the image of a CDN, but since AWS WAF is also combined with CloudFront, it is positioned as a Layer 7 network service, not just a CDN. It may have been. There are many competitions with only CDNs, and it seems that they will become commodities in a sense.
API Gateway Since it's a big deal, I created an API from Python. Please change the region, function and role at the beginning as appropriate.
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 How to deploy Lambda is out of the question, so just the code.
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)
If you run createapi.py above, you will see two URLs like this (if there are no bugs).
createapi.Execution result of py
OK : https://xxxxx.execute-api.somewhere.amazonaws.com/snapshot/hoge
NG : https://xxxxx.execute-api.somewhere.amazonaws.com/snapshot/fuga
When you access the OK person
OK
$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/snapshot/hoge
{"message": "hogehoge"}
If you are NG, you will get angry.
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
You need to specify the URI of the backend Lambda. It's complicated, but it seems to have a specific specification, which is determined by the region and the Lambda Function ARN. If you omit the last "invocations", it will return "500 internal server error". A sober fit point.
How to make a URI
uri = 'arn:aws:apigateway:' + region + ':lambda:path/2015-03-31/functions/' + function + '/invocations'
Make a request (json) for Lambda using the Velocity template. See Reference (http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html) for available variables. Although it is hard coded in the sample, I think it is better to go out as a template file in actual operation.
request_template.vm
{
"pathParams": {
#foreach ($key in $input.params().path.keySet())
"$key": "$util.escapeJavaScript($input.params().path.get($key))"#if ($foreach.hasNext),#end
#end
}
}
In the case of Python, the data converted in the above format is included in event as a dict.
Get request
hoge = event['pathParams']['hoge']
When returning a response from Lambda to API Gateway, use return or raise.
Successful completion
return {'message': 'hogehoge'}
Abnormal termination
raise NotHogeError(hoge)
As explained next, if you return it with return, it will be returned to the client with status code 200. (To be exact, it's the default status code ...)
Integration Response Convert the response from Lambda to a client response.
If you do not specify a template (set it to''), the response from lambda will be returned as it is. In this case, set the response model to Empty in Method Response.
It is easy to return the status code with 200, but it is also necessary to return it with a client error (400 series), a server error (500 series), or a redirect (300 series). For that purpose, it is necessary to associate with a regular expression what kind of response and which status code should be used.
Now let's rewrite the sample Lambda.
When I try to return
else:
#raise NotHogeError(hoge)
return '[400:BadRequest] {0} is not hoge'.format(hoge)
Will return at 200
$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/snapshot/fuga -w '%{http_code}\n'
"[400:BadRequest] fuga is not hoge"200
The status code has become 200. .. .. Actually, you are not looking at the return value of the Lambda handler.
This is also
return {'message' : '[400:BadRequest] {0} is not hoge'.format(hoge)}
This is no good
return {'errorMessage' : '[400:BadRequest] {0} is not hoge'.format(hoge)}
This is what I arrived at in various ways.
exception.py
class NotHogeError(Exception):
def __init__(self, hoge):
self.hoge = hoge
def __str__(self):
"""here!"""
return '[400:BadRequest] {0} is not hoge'.format(self.hoge)
So, it seems that if you want to change the status code, you will throw an exception.
In the case of normal system, Lambda response should be returned by pass-through, but if it is returned in XML or HTML instead of json, it needs to be converted. Also, in the case of an abnormal system, if it is pass-through, StackTrace will also be returned and it will be pitiful, so only the error message should be returned.
Since the Response Model called Error is originally defined, create a template according to it.
response_template.vm
#set($data = $input.path('$'))
{
"message" : "${data.errorMessage}"
}
I thought that API Gateway was not so easy, but I felt that I should think about it if I design API properly in the first place.
Recommended Posts