ここしばらく個人でスマホアプリを作ってみたいと思い色々とバックエンド側の構築方法を探っていました。Amplifyを使おうかと思っていたのですが、Flutterではdatastore周りが少し安定していない感がありまして、噂に聞いていたServerlessFrameworkを使ってみることにしました
Flutter的にはバックエンドはFirebaseがメジャーだと思いますが、私のスキルセット的にAWS、LambdaもPythonで構築することにしました
この記事でできること
- ServerlessFramework、AWSを使ったバックエンドの構築
- APIGateway + Lambda + S3 + DynamoDBの構成
- APIKeyを使ったREST APIのアクセス管理
- S3やDynamoDBに対する、アクセスロールの設定
- ルートディレクトリに集まりがちなfunctionを関数ごとにディレクトリ分け
と、バックエンドを構築するときの一通りのことができるのではないかと思います。また、ServerlessFrameworkそのものの導入方法は割愛します。すでに詳しく解説されている記事がありますので
環境
Framework Core: 2.57.0
Plugin: 5.4.4
SDK: 4.3.0
Components: 3.16.0
初期化
% serverless create --template aws-python3 --name hello-world <<< Serverlessサービスを作る
ServerlessFrameworkはサービスという単位で環境を作るそうです。上記のコマンドひとつで、Lambda1つを含む基本的なテンプレートが生成されます
service: hello-world
frameworkVersion: '2'
provider:
name: aws
runtime: python3.8
lambdaHashingVersion: 20201221
functions: << これ以下を消すと、作ったLambdaを削除することもできる
hello:
handler: handler.hello <<< hander.pyとの紐付け
import json
def hello(event, context):
body = {
"message": "Go Serverless v1.0! Your function executed successfully!",
"input": event
}
response = {
"statusCode": 200,
"body": json.dumps(body)
}
return response
このテンプレートをベースにガリガリ触っていきます。ちなみに
- デフォルトだとバージニア北部リージョンにできる
- Lambdaは[サービス名]-[環境変数]-[serverless.ymlで指定した名前]でできる
- 実行ログのCloudWatch吐き出しもデフォルトで設定できるようである
Amplifyと比べて、余計なファイルもできないし、デプロイもほぼ待ち時間はありません。全体的にシンプルで良いですね
APIGateway > Lambda > DynamoDBの流れを作る
service: hello-world
frameworkVersion: '2'
provider:
name: aws
runtime: python3.8
lambdaHashingVersion: 20201221
# ///////// ↓DynamoDBへのアクセス権限を追加↓ ///////////#
iam:
role:
statements:
- Effect: 'Allow'
Action:
- 'dynamodb:*'
Resource:
- "arn:aws:dynamodb:us-east-1:xxxxxxxxxx:table/*"
# xxxxxxxxにはAWSのアカウント番号が入る
# ///////// ↑DynamoDBへのアクセス権限を追加↑ ///////////#
functions:
hello:
handler: handler.hello
# ////////// ↓ここから下でAPIGatewayとDynamoDBを追加↓ ///////// #
events:
- httpApi:
path: /users/create
method: get
resources:
Resources:
DynamoDbTable:
Type: 'AWS::DynamoDB::Table'
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
- AttributeName: name
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
- AttributeName: name
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: hello-dynamodb
この時、Lambdaからはboto3を使ってDynamoDBにアクセスします
def hello(event, context):
dynamodb = boto3.resource('dynamodb')
result = dynamodb.Table('hello-dynamodb').put_item(
Item = {
'id': "hoge_id",
'name': 'hoge_name'
}
)
response = {
"statusCode": 200,
"body": json.dumps('success!')
}
return response
デプロイをして再度実行すると、DynamoDBにも無事レコードが追加されました。Amplifyでは自動でcreated_at, updated_at, versionなどが付加されていましたが、今回は定義したもの以外は全くなく、非常にシンプルでした。
生成されたURLをブラウザで開いてみると、「success!」とだけ表示されました。成功ですね
APIKeyを使ったREST APIのアクセス管理
このままだとAPIは全世界に公開されていて、URLを知ってさえいれば誰でもアクセスできてしまいます。これはこれで良いのですが、コール数を管理したり制限するときにはAPI Key(AWS的には使用量プラン)をつけます
# 関連しないところは省略しています
service: hello-world
frameworkVersion: '2'
provider:
name: aws
runtime: python3.8
lambdaHashingVersion: 20201221
stage: dev # api-keyがstageできりかわるようにするために追加
# ////////// ↓全function共通で使用するAPIKeyを追加↓ ////////// #
apiGateway:
apiKeys: # API Keyの設定
- mobileApp:
- name: ${self:provider.stage}-app-key
value: # お好みのAPI Key
usagePlan: # 使用量プラン
- mobileApp:
quota:
limit: 1000
offset: 0
period: DAY
throttle:
rateLimit: 100
burstLimit: 100
# ////////// ↑全function共通で使用するAPIKeyを追加↑ ////////// #
functions:
hello:
handler: handler.hello
events:
- http: # httpApi -> http
path: /users/create
method: get
private: true # ApiKeyをつけたいものにprivateをつける
これで画像のようにAPI KeyをHeaderにつけてAPIコールしないと動かなくなります。(API Key無しだとForbidden)
S3の作成と、functionごとへにアクセスロールの設定
custom:
backetName: hello-serverless-bucket-${self:provider.stage}
resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.backetName}
S3AccessRole:
Type: AWS::IAM::Role
DependsOn: Bucket
Properties:
Path: /
RoleName: hello-serverless-lambda-role-with-s3
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: hello-serverless-lambda-policy-with-s3
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- 'Fn::Join':
- ':'
-
- 'arn:aws:logs'
- Ref: 'AWS::Region'
- Ref: 'AWS::AccountId'
- 'log-group:/aws/lambda/*:*:*'
- Effect: "Allow"
Action:
- "s3:PutObject"
- "s3:GetObject"
- "s3:ListBucket"
Resource:
- 'Fn::Join':
- ''
-
- "arn:aws:s3:::"
- ${self:custom.backetName}
- 'Fn::Join':
- ''
-
- "arn:aws:s3:::"
- ${self:custom.backetName}
- "/*"
- LambdaごとにAWSの各リソースに対するアクセス権限を付与しています
- Bucket名は定数化して環境ごとに切り替わるようにしています
def hello(event, context):
filepath = '/tmp/data.csv'
with open(filepath, 'w') as f:
writer = csv.writer(f)
writer.writerow(["a", "b", "c"])
s3 = boto3.resource('s3')
s3.meta.client.upload_file(filepath, 'hello-serverless-bucket-dev', 'hgoe.csv')
response = {
"statusCode": 200,
"body": json.dumps('success!')
}
return response
Lambdaのコードは上記のようになります
functionのディレクトリ分けと、外部ライブラリのアップロード
functionは数十個になりうるのですが、このままだとServerlessのルートディレクトリに全て並べないといけなくなります。また、Lambdaがデフォルトでサポートしていない外部ライブラリを使う場合にアップロードする方法に迷いました
これらを解決してくれるのが、ServerlessFrameworkのライブラリserverless-python-requirements
です。導入方法は公式を参照していただくのが良いかと思います。ちなみにDockerが必要です
ディレクトリ構成
/
├── serverless.yml
├── functions/
│ ├── hello/
│ │ ├── handler.py
│ │ └── requirements.txt
こんな感じでfunctionごとにディレクトリを切ることができます。ちなみに、各ディレクトリ中のrequirements.txtには利用したい外部ライブラリを列挙するのですが、外部ライブラリを使わない場合でも必要です
Serverless定義
service: hello-world
frameworkVersion: '2'
provider:
name: aws
runtime: python3.8
lambdaHashingVersion: 20201221
stage: dev
plugins: # 追加
- serverless-python-requirements # 追加
custom:
backetName: hello-serverless-bucket-${self:provider.stage}
pythonRequirements: # 追加
dockerizePip: true # 追加
package: # 追加
individually: true # 追加
functions:
hello:
role: S3AccessRole
module: functions/hello # 追加
handler: handler.hello
events:
- http:
path: /users/create
method: get
private: true
まとめ
ServerlessFrameworkはやや情報が少ないのか、一度ハマると解決するのが結構大変でした(requirement.txtが必須とか)。今回の記事で、ServerlessFrameworkで、AWSを使って、Pythonを使う場合の基本的な内容はカバーできた気がします