Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

serverless frameworkでAWSにサーバレスなAPIサーバーを作る

f:id:nipe880324:20180701190810j:plain:w420

フルマネージドなAPIサーバーを簡易にたてるときの選択肢の一つとして、API Gateway, Lambda (Flask), DynamoDBを使ってAPIサーバーを作成しました。
各サービスの構築には、serverless frameworkを使いました。

ソースコードはこちらにあります。
https://github.com/nipe0324/serverless-flask-sample

使用ツール&動作確認バージョン

1. システム構成

f:id:nipe880324:20180701191817j:plain:w420

下記のような構成になっています。

  • API Gatewayでリクエストを受け付け、
  • LambdaでFlaskを動かして処理をし、
  • DyanamoDBでデータを保存する


2. 前提条件

下記の環境を構築する必要があります。

  • AWSアカウント
  • Node.js v4以上
  • awscli

3. 構築方法

serverlessをグローバルにインストールする
npm install serverless -g

ボイラープレートを作成

.gitignore, handler.py, serverless.ymlが作成されます。
デプロイ時にserverless.ymlの内容に応じてサーバーが構成されます。
handler.pyの内容はlambdaにデプロイされますが今回はflaskでAPIサーバーの処理を書くので削除します。

serverless create --template aws-python3 --path serverless-flask-sample
cd serverless-flask-sample
tree -a
> .
> ├── .gitignore
> ├── handler.py
> └── serverless.yml

rm handler.py

package.jsonの作成

npm init -f

serverless-wsgiとserverless-python-requirementsをインストール

serverless-wsgiAPI GatewayからLambdaへのリクエストをwsgiアプリ用に形式に変換してくれるパッケージです。flaskはwsgiアプリなのでwsgiに変換してあげる必要があります。
わざわざwsgiに変換する目的として、Lambda依存しないソースコードを書くことで、必要に応じてEC2にお引越しができるようになるというメリットがあります。
serverless-python-requirementsは、自動でrequirements.txtに記載されたpythonのライブラリをインストールし、パスを読み込んでくれます。

npm install --save-dev serverless-wsgi serverless-python-requirements

requirements.txtを記載する

virtualenvで作ったのですが、簡略化のため、下記のように追記します。

./requirements.txt
boto3==1.7.48
botocore==1.10.48
click==6.7
docutils==0.14
Flask==1.0.2
itsdangerous==0.24
Jinja2==2.10
jmespath==0.9.3
MarkupSafe==1.0
python-dateutil==2.7.3
s3transfer==0.1.13
six==1.11.0
Werkzeug==0.14.1

Todo管理のFlaskアプリを書く

詳細は省きますが、下記の通りのTodoをCRUDができるFlaskアプリを書きます。

# ./main.py
import os
from flask import Flask, request, jsonify, abort
import boto3

# flaskを作成する
# __name__はファイル名(__main__)
app = Flask(__name__)

# serverless.ymlから変数を受け取る
TODOS_TABLE = os.environ['TODOS_TABLE']

# boto3はawsにアクセスするライブラリ
# dynamodbのテーブルを取得
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(TODOS_TABLE)

# todoを取得する
def get_todo(id):
    res = table.get_item(Key={ 'id': id })
    if 'Item' not in res:
        abort(404)
    return res['Item']

# Todoのidを取得する
# 採番テーブルを使うかuuidを使うことなどの必要があるが、
# 簡略化のため、最後のTodo+1している。
def new_id():
    res = table.scan()
    ary = res['Items']
    if len(ary) == 0:
        return '1'
    else:
        ary = sorted(ary, key=lambda x: x['id'], reverse=True)
        return str(int(ary[0]['id']) + 1)

# Todoの一覧を返す
@app.route("/todos")
def index():
    res = table.scan()
    return jsonify(res['Items'])

# Todoを返す
@app.route("/todos/<id>")
def show(id):
    todo = get_todo(id)
    return jsonify(todo)

# Todoを作成する
@app.route("/todos", methods=["POST"])
def create():
    title = request.json.get('title')
    if not title:
        return jsonify({'error': 'Please provider todo title'}), 422
    new_todo = { 'id': new_id(), 'title': title }
    res = table.put_item(Item=new_todo)
    return jsonify(new_todo), 201

# Todoを更新する
@app.route("/todos/<id>", methods=["PUT"])
def update(id):
    edit_todo = get_todo(id)
    title = request.json.get('title')
    if not title:
        return jsonify({'error': 'Please provider todo title'}), 422
    table.update_item(
        Key={ 'id': edit_todo['id'] },
        UpdateExpression="set title = :n",
        ExpressionAttributeValues={ ':n': title }
    )
    return jsonify(get_todo(id)), 201

# Todoを削除する
@app.route("/todos/<id>", methods=["DELETE"])
def destroy(id):
    todo = get_todo(id)
    res = table.delete_item(Key={ 'id': todo['id'] })
    return jsonify({'success': True}), 200

# 404のエラーハンドリング処理
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not found'}), 404

if __name__ == '__main__':
    app.run(debug=True)


参考で、デバッグなどのためにローカルでFlaskを動かしたい場合は下記コマンドで起動できます。
DynamoDBのテーブルがないと思うので、AWSに作る必要があります。下記の場合は"todos-local"

TODOS_TABLE=todos-local FLASK_ENV=development python main.py

serverless.ymlを修正する

ボイラープレートとして自動で作られた内容を削除し、下記の通り修正します。

# ./serverless.yml

service: serverless-flask-sample

# serverless pluginを指定
plugins:
  - serverless-python-requirements
  - serverless-wsgi

# custom変数、他の箇所から利用するため記載
custom:
  tableName: 'todos-${self:provider.stage}'
  wsgi:
    app: main.app
    packRequirements: false

provider:
  name: aws # awsを利用
  runtime: python3.6 # Lambdaのランタイムをpytyon3.6にする
  stage: dev
  region: ap-northeast-1 # 東京リージョンを利用
  iamRoleStatements: # LambdaのiamロールでDynamoDBにアクセスできるようにする
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - { "Fn::GetAtt": ["TodosDynamoDBTable", "Arn" ] }
  environment:
    TODOS_TABLE: ${self:custom.tableName} # Lambdaの環境変数の指定

functions:
  app:
    handler: wsgi.handler # API Gatewayのハンドラーを指定
    events: # API Gatewayですべてのリクエストを受け取る
      - http: ANY /
      - http: 'ANY {proxy+}'

resources:
  Resources:
    TodosDynamoDBTable: # DynamoDBを作成する
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        TableName: ${self:custom.tableName}
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

AWSにデプロイする

"-v"は詳細を表示するオプションです。
serverlessはローカルでファイルを作り、AWSのS3にアップロードし、そこからLambdaにデプロイしているのでそれぽい動きになってます。

serverless deploy -v
Serverless: Installing requirements of requirements.txt in .serverless...
Serverless: Packaging Python WSGI handler...
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Injecting required Python packages to package...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - serverless-flask-sample-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::CloudFormation::Stack - serverless-flask-sample-dev
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (1.28 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - serverless-flask-sample-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::RestApi - ApiGatewayRestApi
CloudFormation - CREATE_IN_PROGRESS - AWS::Logs::LogGroup - AppLogGroup
CloudFormation - CREATE_IN_PROGRESS - AWS::DynamoDB::Table - TodosDynamoDBTable
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::RestApi - ApiGatewayRestApi
CloudFormation - CREATE_IN_PROGRESS - AWS::Logs::LogGroup - AppLogGroup
CloudFormation - CREATE_IN_PROGRESS - AWS::DynamoDB::Table - TodosDynamoDBTable
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::RestApi - ApiGatewayRestApi
CloudFormation - CREATE_COMPLETE - AWS::Logs::LogGroup - AppLogGroup
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Resource - ApiGatewayResourceProxyVar
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Resource - ApiGatewayResourceProxyVar
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceProxyVar
CloudFormation - CREATE_COMPLETE - AWS::DynamoDB::Table - TodosDynamoDBTable
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_COMPLETE - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - AppLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - AppLambdaFunction
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Function - AppLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - AppLambdaVersionqP4bMq7neNMNaJeqstxcpsZZBt0QvA2yk0ovSbRTBZo
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Method - ApiGatewayMethodAny
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Permission - AppLambdaPermissionApiGateway
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Method - ApiGatewayMethodAny
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Permission - AppLambdaPermissionApiGateway
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Method - ApiGatewayMethodProxyVarAny
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - AppLambdaVersionqP4bMq7neNMNaJeqstxcpsZZBt0QvA2yk0ovSbRTBZo
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Method - ApiGatewayMethodProxyVarAny
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Method - ApiGatewayMethodAny
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Version - AppLambdaVersionqP4bMq7neNMNaJeqstxcpsZZBt0QvA2yk0ovSbRTBZo
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Method - ApiGatewayMethodProxyVarAny
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530438920204
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530438920204
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530438920204
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Permission - AppLambdaPermissionApiGateway
CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - serverless-flask-sample-dev
CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - serverless-flask-sample-dev
Serverless: Stack update finished...
Service Information
service: serverless-flask-sample
stage: dev
region: ap-northeast-1
stack: serverless-flask-sample-dev
api keys:
  None
endpoints:
  ANY - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
  ANY - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/{proxy+}
functions:
  app: serverless-flask-sample-dev-app

Stack Outputs
AppLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:000000000000:function:serverless-flask-sample-dev-app:2
ServiceEndpoint: https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
ServerlessDeploymentBucketName: serverless-flask-sample-serverlessdeploymentbuck-xxxxxxxxxxx

AWSにデプロイしたリソースを削除する

お金かかってしまうかもしれませんので、片付けるときは下記コマンドでリソースを削除します。

serverless remove

4. 動作確認

AWSリソースを確認

まず、AWSにデプロイされたリソースを確認します。

API Gateway
すべてのリクエストを受け付けるように設定されています
f:id:nipe880324:20180701192305p:plain:w420

Lambda
ソースコードがデプロイされており、API GatewayとDynamoDBと接続されています。
f:id:nipe880324:20180701192042p:plain:w420

IAM Role
DynamoDBとCloudWatchにアクセス件があります。
f:id:nipe880324:20180701192107p:plain:w420

DynamoDB
todos-devテーブルが作成されています。
f:id:nipe880324:20180701192054p:plain:w420

ServiceEndpointを取得

serverless deployの結果のServiceEndpointか、API Gatewayの画面からエンドポイントを取得し設定しておきます。

export SLS_ENDPOINT=https://xxxxxxxxxxexecute-api.ap-northeast-1.amazonaws.com/dev

動作を確認する

# Todo一覧を取得
curl ${SLS_ENDPOINT}/todos
> [ ]

# Todoを取得
curl ${SLS_ENDPOINT}/todos/1
> {"error":"Not found"}

# Todo作成
curl ${SLS_ENDPOINT}/todos -X POST -H "Content-Type: application/json" -d '{"title": "Shopping"}'
> {"id":"1","title":"Shopping"}

# 再度、Todoを取得
curl ${SLS_ENDPOINT}/todos/1
> {"id":"1","title":"Shopping"}

# Todoを更新
curl ${SLS_ENDPOINT}/todos/1 -X PUT -H "Content-Type: application/json" -d '{"title": "Shopping 2"}'
> {"id":"1","title":"Shopping 2"}

# Todo一覧を取得
curl ${SLS_ENDPOINT}/todos
> [{"id":"1","title":"Shopping 2"}]

# Todoを削除
curl ${SLS_ENDPOINT}/todos/1 -X DELETE -H "Content-Type: application/json"
> {"success":true}

# Todo一覧を取得
curl ${SLS_ENDPOINT}/todos
> [ ]

5. まとめ

serverless frameworkで簡単にAPIサーバーを構築することができました。
serverlessでは他にも細かな設定ができるので、他にもやりたいことがありましたらドキュメントを確認してみてください。
terraformと使い分けは

参考文献


以上です。