Serverless FrameworkでLambdaをコンテナデプロイ
はじめに
terraformでAWS Lambdaのコンテナデプロイしてるのを、Serverless Frameworkを使うともっといい感じにできそうなので試してみたメモ
Serverless Frameworkとは
Develop, deploy, troubleshoot and secure your serverless applications with radically less overhead and cost by using the Serverless Framework. The Serverless Framework consists of an open source CLI and a hosted dashboard. Together, they provide you with full serverless application lifecycle management.
Serverless Frameworkを使うとオーバーヘッドとコストを大幅に削減して、サーバーレスアプリケーションの開発、デプロイ、トラブルシューティング、セキュリティを実現できるとのこと。
要するにLambda関数をいい感じに開発・デプロイができるらしい。
コンセプト
Serverless Framework自体はAWS, Azure, GCPなど様々なプラットフォームに対応している
今回はAWS Lambdaを開発したいのでProvider CLI ReferencesのAWSの部分を眺めてみる
どうやら大きくFunctions, Events, Resources, Servicesという4つのコンセプトがあるらしい
Functions
AWS Lambda Functionsのこと。マイクロサービスのような独立したデプロイメントの単位。
Events
Lambda Functionsが実行されるトリガーとなるイベント。
例えばAPI GatewayのHTTPエンドポイントリクエストやS3バケットのアップロード、CloudWatchのタイマーなど。
Serverless FrameworkでAWS Lambda関数のイベントを定義すると、そのイベントのリソースを自動的に作ってくれる。
Resources
Functionsが使用するAWSリソースのこと。
例えば関数内でアクセスするDynamoDBテーブルやS3 Bucketなど。
トリガーとなるイベントだけでなく、Functionsが依存するAWSリソースのデプロイもできるらしい
Services
フレームワークを構成する単位。Service内に上述のFunctionやEvent,Resourceを定義する
インストール
筆者はmacを使っているので以下のような手順になりました
- node.jsのインストール
asdfを利用
asdf plugin add nodejs
asdf install nodejs latest
asdf global nodejs latest
- serverless frameworkのインストール
npm install -g serverless
早速試してみる
sls createで雛形を作れるらしいので利用してみます
sls create --helpでtemplateの一覧が出てくるので、今回はpython+dockerのtemplateを利用します
mkdir sls_sample
cd sls_sample
sls create --template "aws-python-docker"
実行するとこんな感じの雛形ができます
.
├── Dockerfile
├── README.md
├── app.py
└── serverless.yml
デフォだとregionがus-east-1らしいので, serverless.yml内のregionの指定だけ修正しました。
serverless.ymlの内容は以下となります
service: sls-sample
frameworkVersion: '2'
provider:
name: aws
lambdaHashingVersion: 20201221
ecr:
images:
appimage:
path: ./
region: ap-northeast-1
functions:
hello:
image:
name: appimage
デプロイ。
この時のAWSのクレデンシャル情報は~/.aws/credentialsのdefaultプロファイルが利用されます。
(他のプロファイルを利用したい場合はserverless.yml内で指定する)
sls deploy --stage dev
stageオプションはデフォルトはdev.
dev以外を指定すると別環境で作成されます
これを実行するとまずdocker imageをbuildしECRレポジトリの作成とイメージのpushが行われます。
そしてローカルの.serverless/にcloudFormationのテンプレートが作成され、実際にcloudFormation経由でデフォルトで以下のリソースが作成されます
- Lambda関数
- Lambda関数用のロググループ
- Lambda関数用の実行ロール
- CloudFormation用のS3バケット
ローカルからデプロイしたLambda関数の動作確認(invoke)
sls invoke --function hello
{
"statusCode": 200,
"body": "{\"message\": \"Hello, world! Your function executed successfully!\"}"
}
Lambda + API GatewayでCRUDアプリを作ってみる
簡単なLambda関数を作成することができたので、次にServerless FrameworkでLambda + API GatewayでちょっとしたCRUDアプリを作ってみます.
プロジェクトの作成
pythonは3.8
pyenv local 3.8.0
依存管理はpoetryを利用する
まずプロジェクトの雛形を作成
poetry new todo_app
今回はアプリケーションがDynamoDBを利用するのでAWS SDKのboto3をインストールする
poetry add boto3
アプリケーションの実装
TodoItemモデルを実装する
todo_app/item.py
rom __future__ import annotations
import dataclasses
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict
class Status(Enum):
OPEN = 1
DOING = 2
DONE = 3
@classmethod
def from_str(cls, s: str) -> Status:
return cls[s.strip().upper()]
@dataclass(frozen=True)
class TodoItem:
name: str
id: uuid.UUID = field(default_factory=uuid.uuid4)
status: Status = Status.OPEN
def start(self) -> TodoItem:
item = dataclasses.replace(self, status=Status.DOING)
return item
def done(self) -> TodoItem:
item = dataclasses.replace(self, status=Status.DONE)
return item
@classmethod
def from_dict(cls, dict: Dict[str, str]) -> TodoItem:
return TodoItem(
id=uuid.UUID(dict["id"]),
name=dict["name"],
status=Status.from_str(dict["status"])
)
def to_dict(self) -> Dict[str, str]:
return {"id": str(self.id), "name": self.name, "status": self.status.name}
repositoryを実装する
todo_item/repository.py
import os
from typing import Any, Dict, List
import boto3
from todo_app.item import TodoItem
class ItemTable:
def __init__(self) -> None:
dynamodb = boto3.resource("dynamodb")
self.table = dynamodb.Table(os.environ["DYNAMO_TABLE"])
def get_item(self, id: str) -> Dict[str, Any]:
res = self.table.get_item(Key={"id": id})
return res["Item"]
def list_item(self) -> List[Dict[str, Any]]:
res = self.table.scan()
return res["Items"]
# upsert
def save(self, item: TodoItem) -> None:
self.table.put_item(Item=item.to_dict())
return None
def delete(self, id: str) -> None:
self.table.delete_item(Key={"id": id})
self.table
return None
handlerを実装する
todo_item/handler.py
import json
from todo_app.item import TodoItem
from todo_app.repository import ItemTable
table = ItemTable()
def ping(event, context):
return {
"statusCode": 200,
"body": json.dumps({"message": "OK"}, indent=2, ensure_ascii=False),
}
def list(event, context):
items = table.list_item()
return {
"statusCode": 200,
"body": json.dumps({"results": items}, indent=2, ensure_ascii=False),
}
def get(event, context):
id = event["pathParameters"]["id"]
item = table.get_item(id)
return {
"statusCode": 200,
"body": json.dumps({"results": item}, indent=2, ensure_ascii=False),
}
def create(event, context):
body = json.loads(event["body"])
name = body["name"]
item = TodoItem(name=name)
table.save(item)
return {
"statusCode": 200,
"body": json.dumps({"results": "success"}, indent=2, ensure_ascii=False),
}
def delete(event, context):
id = event["pathParameters"]["id"]
table.delete(id)
return {
"statusCode": 200,
"body": json.dumps({"results": "suceess"}, indent=2, ensure_ascii=False),
}
def start(event, context):
id = event["pathParameters"]["id"]
item = TodoItem.from_dict(table.get_item(id))
item = item.start()
table.save(item)
return {
"statusCode": 200,
"body": json.dumps({"results": "suceess"}, indent=2, ensure_ascii=False),
}
def done(event, context):
id = event["pathParameters"]["id"]
item = TodoItem.from_dict(table.get_item(id))
item = item.done()
table.save(item)
return {
"statusCode": 200,
"body": json.dumps({"results": "suceess"}, indent=2, ensure_ascii=False),
}
Dockerfileの実装
base imageはawsが提供しているlambda用のpythonイメージを利用することにします
awsが提供しているイメージには以下が含まれています
ランタイムインターフェイスクライアントはruntimeがLambda Serviceと通信するのに必要な機能です. オフィシャルのPythonイメージなどをベースイメージとして利用する場合はパッケージをインストールする必要があります
ランタイムインターフェイスエミュレータは、ローカル環境でLambdaのプログラムを動かすツールです
Dockerfileは以下のようになります
Dockerfile
FROM public.ecr.aws/lambda/python:3.8
COPY todo_app ./todo_app
COPY poetry.lock pyproject.toml ./
RUN pip install --upgrade pip poetry && \
poetry config virtualenvs.create false && \
poetry install --no-root
CMD [ "todo_app.handler.ping" ]
これを元にdocker buildしローカルでdocker runするとエミュレーターが起動します
docker run --rm -it -p 9000:8080 -e AWS_DEFAULT_REGION=ap-northeast-1 -e AWS_ACCESS_KEY_ID=XXX -e AWS_SECRET_ACCESS_KEY=XXX -e AWS_SECURITY_TOKEN -e DYNAMO_TABE=todo-app-item-dev <IMAGE:TAG>
curlで動作確認できます
❯ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d {}
{"statusCode": 200, "body": "{\n \"message\": \"OK\"\n}"}%
serverless.yml
severless.ymlを実装します
ポイントとしては以下です
- DynamoDBにアクセスできるiamロールの作成
- これがが全ての関数の実行ロールとして利用される
- 各Lambda関数のeventsにHTTP APIのpathとメソッドを設定
- resourceとしてDynamoDBを作成、CloudFormationのテンプレートで記述する
serverless.yml
service: todo-app
provider:
name: aws
region: ap-northeast-1
environment:
DYNAMO_TABLE: ${self:service}-item-${opt:stage}
#profile: private # profile in ~/.aws/credentials
ecr:
images:
todoapp:
path: ./
file: Dockerfile
iamRoleStatements:
- Effect: "Allow"
Action:
- "dynamodb:*"
Resource:
- "*"
functions:
ping:
image:
name: todoapp
command: todo_app.handler.ping
events:
- httpApi:
path: /ping
method: GET
list:
image:
name: todoapp
command: todo_app.handler.list
events:
- httpApi:
path: /items
method: GET
get:
image:
name: todoapp
command: todo_app.handler.get
events:
- httpApi:
path: /items/{id}
method: GET
create:
image:
name: todoapp
command: todo_app.handler.create
events:
- httpApi:
path: /items
method: POST
delete:
image:
name: todoapp
command: todo_app.handler.delete
events:
- httpApi:
path: /items/{id}
method: DELETE
start:
image:
name: todoapp
command: todo_app.handler.start
events:
- httpApi:
path: /items/{id}/start
method: PATCH
done:
image:
name: todoapp
command: todo_app.handler.done
events:
- httpApi:
path: /items/{id}/done
method: PATCH
resources:
Resources:
testTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-item-${opt:stage}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
デプロイ
sls deploy --stage dev --verbose
Discussion