Chaliceで簡単にサーバレスREST APIを作る
どんな人に向いてる?
- REST APIを作りたいけどサーバとか運用したくない
- APIゲートウェイとかLambdaを手動で管理するのだるい
- AWSでなんとかしたい
- 運用費用安く済ませたい
Chaliceって何? 何ができるもの?
Amazon AWSが開発しているオープンソースのWebフレームワーク。
簡単な記述でREST APIを定義し、ワンタッチでAWS API Gataeway + AWS Lambda上にデプロイすることができる
Chaliceを選ぶときの留意点
-
フルスタックなWebフレームワークではない
いわゆるMVCを完備したフレームワークではなく、Vの部分に特化している
MやCは適宜ORマッパやAWSサービスを使う必要がある。(SQLAlchemyとかboto3で連携) -
Lambdaで導入できるライブラリに限定?
Chaliceでは未確認だが、Lambdaの場合Importできるライブラリに制限があったはず。 -
永続化について
エフェメラルなコンテナで実行する都合上、メモリ上のデータの永続性が担保されない
セッションの維持やデータの保管が必要な場合、適宜ElasticCacheやS3等を使って永続化すること
環境構築の前提
- Python 3の使える環境(pyenv, python venv等でのバージョン管理推奨)
.aws設定ファイルの作成
- クレデンシャルやコンフィグを設定する
MacBook-Air:codes $ cat ~/.aws/credentials
[default]
aws_access_key_id=XXXX
aws_secret_access_key=XXXXX
MacBook-Air:codes $ cat ~/.aws/config
[default]
region=us-west-2
output=json
Chalice プロジェクトの作成
chalice new-project helloworld
基本的な書き方例
- リクエストに対してjsonレスポンスを返す例
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/')
def index():
return {'hello': 'world'}
デプロイ/アンデプロイ
AWSへのデプロイ
chalice deploy
ログ
$ chalice deploy
Creating deployment package.
Creating IAM role: helloworld-dev
Creating lambda function: helloworld-dev
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:99999944444:function:helloworld-dev
- Rest API URL: https://jjjsssiii0.execute-api.us-west-2.amazonaws.com/api/
AWSコンソールで見ると、configで指定したリージョンにLambdaとAPIGWでアプリケーションが構築されているのが分かる
localへのdeploy
chalice local
ローカルにサーバが立ち上がり、ブラウザ等でアクセスできる
環境をわけてdeployする
以下の様に --stage 引数で任意のステージ名を指定して環境を分けることができる
(指定しないと自動的にdevが指定される)
MacBook-Air:helloworld $ chalice deploy --stage staging
Creating deployment package.
Reusing existing deployment package.
Creating IAM role: helloworld-staging
Creating lambda function: helloworld-staging
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:999999444444:function:helloworld-staging
- Rest API URL: https://egmfhnogug.execute-api.us-west-2.amazonaws.com/api/
MacBook-Air:helloworld $ chalice deploy --stage production
Creating deployment package.
Reusing existing deployment package.
Creating IAM role: helloworld-production
Creating lambda function: helloworld-production
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:999999444444:function:helloworld-production
- Rest API URL: https://ozkhx5xhwf.execute-api.us-west-2.amazonaws.com/api/
deployしたサーバへのURLを取得する
$ chalice url
https://y6zfasiii0.execute-api.us-west-2.amazonaws.com/api/
アプリケーションの削除
$chalice delete
ステージを複数定義している場合は
chalice delete --stage dev
という感じで指定できる
色々な書き方
URLパラメータの取得方法
@app.route('/cities/{city}')
def state_of_city(city):
return {'state': CITIES_TO_STATE[city]}
というように、routeの宣言でパラメータ位置を指定し、メソッド側の引数でそのパラメータ名を指定すると
その場所に入っていたパラメータを拾うことができる
エラーハンドリングの追加
例:400 Bad Requestを返す
from chalice import Chalice
from chalice import BadRequestError
app = Chalice(app_name='helloworld')
CITIES_TO_STATE = {
'seattle': 'WA',
'portland': 'OR',
}
..(同上)..
@app.route('/cities/{city}')
def state_of_city(city):
try:
return {'state': CITIES_TO_STATE[city]}
except KeyError:
raise BadRequestError("Unknown city '%s', valid choices are: %s" % (city, ', '.join(CITIES_TO_STATE.keys())))
これで存在しないcityをリクエストすると400 BadRequestErrorが返却される
POSTやPUTのAPI定義
PUTメソッドの例。methodsで宣言
@app.route('/resource/{value}', methods=['PUT'])
def put_test(value):
return {"value": value}
あまり使わないかもしれないが、複数メソッドも定義できる
@app.route('/myview', methods=['POST', 'PUT'])
def myview():
pass
メソッドで挙動を分けたい場合は
@app.route('/myview', methods=['POST'])
def myview_post():
pass
@app.route('/myview', methods=['PUT'])
def myview_put():
pass
リクエストBodyの取得
...
from chalice import NotFoundError
OBJECTS = {
}
@app.route('/objects/{key}', methods=['GET', 'PUT'])
def myobject(key):
request = app.current_request
if request.method == 'PUT':
OBJECTS[key] = request.json_body
elif request.method == 'GET':
try:
return {key: OBJECTS[key]}
except KeyError:
raise NotFoundError(key)
・app.current_requestがリクエストオブジェクト
・request.methodでメソッドを取得できる
・request.json_bodyでリクエストのjsonボディを取得できる
その他、app.currenct_requestから取れるもの
current_request.query_params
クエリパラメータ (辞書型) (URLの?以降のパラメータ)
current_request.headers
ヘッダー (辞書型) ※全て小文字にキャストされるので注意
current_request.uri_params
URIパラメータ (辞書型) (URLのパス部分のパラメータ)
current_request.method
リクエストのメソッド (文字列)
current_request.json_body
ボディ (json)
current_request.raw_body
ボディ (生のままのボディ (bytes型なのでキャスト要))
current_request.context
コンテキスト情報 (※説明は後述、中身要確認)
ソースIPやuserAgentはここで取得可能
current_request.stage_vars
APIGW のステージの設定に関する情報 (※説明は後述、中身要確認)
current_request自体をdict化することも可能
app.current_request.to_dict()
current_requestの中身の例:
{
"context": {
"apiId": "apiId",
"httpMethod": "GET",
"identity": {
"accessKey": null,
"accountId": null,
"apiKey": null,
"caller": null,
"cognitoAuthenticationProvider": null,
"cognitoAuthenticationType": null,
"cognitoIdentityId": null,
"cognitoIdentityPoolId": null,
"sourceIp": "1.1.1.1",
"userAgent": "HTTPie/0.9.3",
"userArn": null
},
"requestId": "request-id",
"resourceId": "resourceId",
"resourcePath": "/introspect",
"stage": "dev"
},
"headers": {
"accept": "*/*",
...
"x-testheader": "Foo"
},
"method": "GET",
"query_params": {
"query1": "value1",
"query2": "value2"
},
"raw_body": null,
"stage_vars": null,
"uri_params": null
}
コンテンツタイプの選択(json以外のコンテンツタイプ受け付け)
import sys
from chalice import Chalice
from urllib.parse import urlparse, parse_qs
app = Chalice(app_name='helloworld')
@app.route('/', methods=['POST'],
content_types=['application/x-www-form-urlencoded'])
def index():
parsed = parse_qs(app.current_request.raw_body.decode())
return {
'states': parsed.get('states', [])
}
上記の様に、content_typesで指定できる(リスト型なので複数指定可)
・指定外の形式を送られると415 Unsuported media
・app.current_request.json_bodyはapplication/jsonタイプのリクエストでのみ生成される
curl -i -X POST -H"Content-Type: application/json" https://XXXXXX.execute-api.us-west-2.amazonaws.com/api/ -d'{"chalice":"helloapi"}'
レスポンスBodyのカスタマイズ
・Chaliceでは普通にオブジェクトをreturnすると、シリアライズしてJSONに変換にしてレスポンスを作る
・JSON以外でレスポンスしたい場合は以下の様にする
・プレーンテキストを返却する例
@app.route('/')
def index():
return Response(body='hello world!',
status_code=200,
headers={'Content-Type': 'text/plain'})
JSONをgzip圧縮して返却する
import json
import gzip
from chalice import Chalice, Response
app = Chalice(app_name='compress-response')
app.api.binary_types.append('application/json')
@app.route('/')
def index():
blob = json.dumps({'hello': 'world'}).encode('utf-8')
payload = gzip.compress(blob)
custom_headers = {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip'
}
return Response(body=payload,
status_code=200,
headers=custom_headers)
ポイントは以下
・gzipをインポートする
・app.api.binary_types.appendでapplication/jsonを追加することを宣言する
・jsonレスポンスをencodeしてblobを作り、gzip.compressで圧縮してpayloadを作る
・Responseオブジェクトにbody引数にpayloadを渡す
このように、Responseオブジェクトを使うことで、柔軟なレスポンス作成ができる
ファイルの分割
app.pyと並列にchalicelibフォルダを作り、配下にpyファイルを置くことで、ロジックの切り出しができる。
※それ以外の命名だとLambdaに反映されないっぽい。
環境変数のコード管理
環境変数はコンソールで設定してもよいが、以下のように.chalice/config.json に記載しておくこともできる
{
"version": "2.0",
"app_name": "xxx",
"environment_variables": {
"env_var_common":"is_common"
},
"stages": {
"dev": {
"api_gateway_stage": "api",
"environment_variables": {
"env_var_stage":"is_dev"
}
},
"local": {
"environment_variables": {
"env_var_stage":"is_local"
}
}
}
}
・app_nameはchaliceのプロジェクト名
・environment_variables: 全てのステージに共通に適用される環境変数
・stages以下: それぞれdev, localなど名前に合致するステージで利用される環境変数
CORSサポート
@app.route('/supports-cors', methods=['PUT'], cors=True)
def supports_cors():
return {}
・上記のようにcors=Trueを指定することで、CORSに対応させられる
さらに細かく設定したい場合は
from chalice import CORSConfig
cors_config = CORSConfig(
allow_origin='https://foo.example.com',
allow_headers=['X-Special-Header'],
max_age=600,
expose_headers=['X-Special-Header'],
allow_credentials=True
)
@app.route('/custom-cors', methods=['GET'], cors=cors_config)
def supports_custom_cors():
return {'cors': True}
複数のORIGINを許可したい場合の例:
from chalice import Chalice, Response
app = Chalice(app_name='multipleorigincors')
_ALLOWED_ORIGINS = set([
'http://allowed1.example.com',
'http://allowed2.example.com',
])
@app.route('/cors_multiple_origins', methods=['GET', 'OPTIONS'])
def supports_cors_multiple_origins():
method = app.current_request.method
if method == 'OPTIONS':
headers = {
'Access-Control-Allow-Method': 'GET,OPTIONS',
'Access-Control-Allow-Origin': ','.join(_ALLOWED_ORIGINS),
'Access-Control-Allow-Headers': 'X-Some-Header',
}
origin = app.current_request.headers.get('origin', '')
if origin in _ALLOWED_ORIGINS:
headers.update({'Access-Control-Allow-Origin': origin})
return Response(
body=None,
headers=headers,
)
elif method == 'GET':
return 'Foo'
S3を使った情報の永続化
OBJECTS = {
}
@app.route('/objects/{key}', methods=['GET', 'PUT'])
def myobject(key):
request = app.current_request
if request.method == 'PUT':
OBJECTS[key] = request.json_body
elif request.method == 'GET':
try:
return {key: OBJECTS[key]}
except KeyError:
raise NotFoundError(key)
上記のコードで、指定したKeyに対してオブジェクトをPUTメソッドで保存し、同じkeyでGETできるようになるが、
所詮Lambdaで動いているため永続性は保証されない(Lambdaを実行しているコンテナが消滅すると揮発してしまう)
以下ではS3を使って永続化を行う。
$ pip install boto3
$ pip freeze | grep boto3 >> requirements.txt
import json
import boto3
from botocore.exceptions import ClientError
from chalice import NotFoundError
S3 = boto3.client('s3', region_name='us-west-2')
BUCKET = 'your-bucket-name'
@app.route('/objects/{key}', methods=['GET', 'PUT'])
def s3objects(key):
request = app.current_request
if request.method == 'PUT':
S3.put_object(Bucket=BUCKET, Key=key,
Body=json.dumps(request.json_body))
elif request.method == 'GET':
try:
response = S3.get_object(Bucket=BUCKET, Key=key)
return json.loads(response['Body'].read())
except ClientError as e:
raise NotFoundError(key)
とすることで、S3のバケット上にオブジェクトが保存され、永続化することができる
この際、必要なIAMポリシーは自動的に設定される(※すごい)
生成されるポリシーは以下で確認できる
$ chalice gen-policy
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:ListAllMyBuckets"
],
"Resource": [
"*"
],
"Effect": "Allow",
"Sid": "9155de6ad1d74e4c8b1448255770e60c"
}
]
}
- 手動でポリシーを設定する
もし自動設定ポリシーを使用せず、手動で設定したい場合は、
"manage_iam_role":false
"iam_role_arn":"arn:aws:iam::<account-id>:role/<role-name>"
- 認証のカスタマイズ
以下の様な方法で認証を行うことができる - API Key
- AWS IAM
- Cognito User Pools
- Custom Auth Handler
例: API Keyによる認証
@app.route('/authenticated', methods=['GET'], api_key_required=True)
def authenticated():
return {"secure": True}
ここでいうAPI KeyはAWSアクセスキーではなく、
Amazon API GatewayのAPIキーのことを指す
作り方↓
-
APIGWのAPIを選択後、左側ペインから"API Keys"を選択
-
Createを実施
-
Usage Plansを作成
ここで、秒間リクエスト許容数等の制限を定義する
throttling: rate: 一秒あたりにリクエストできる回数
throttling: burst: Tokenブランケットに蓄積できるToken数
quota: このUsage planが適用されたユーザが日、週、月ごとにAPI発行できる上限
4.UsageをAPIステージ(helloworldとか)と紐づける
5. 作成した鍵をUsager Planに紐づける
つまり、鍵 -> Usage Plan -> API Stageというモデルになっている(それぞれ1:n)
ここまで実施した上でリクエストすると、
$ curl -H"X-Api-Key:rAjMYhVQkCXXXXXXXXXXXXXXXX" https://y6zfasiii0.execute-api.us-west-2.amazonaws.com/api/authenticated
{"secure":true}
という形でリクエストを認証できる
主に参考したドキュメント
ーーー以下はAWS APIGWと連携させる使い方。event sourceで
Discussion