経緯
副業でちょいとお手伝いをしているところがありまして、フロントだけの開発〜って感じで聞いていたのですが、フロントエンドだけじゃどうにもならなくなってきてしまいまして。
本職ぺちぱーなので普通にLaravelとかで組んでもいいんですけど、先方のやってみたいことやっていいよ〜という温かい言葉に甘えて、こりゃいっちょ新しいことやってみっかな、ってなったのです。
で、やっぱ時代はサーバーレスじゃないですか。
サーバーレスでサーバーサイドは案件でAWS SAM使ったNode.jsのはやったのですけど、せっかくなのでServerless Frameworkでpython使ってやってみようかなーって思い至ったわけです。
やりたい構成
難しい事やる気はサラサラなくて、API GatewayからLambda通ってDynamoDBの読み書きするってやつですね。
んで、ここのLambdaとDynamoDBのところをDockerコンテナとしてそれぞれ置こう!ってしたいわけですね。
docker-compose.ymlはこんな感じになりました。
version: '3'
services:
# フロントエンド開発用
web:
build: .
ports:
- 9080:9080
volumes:
- .:/app
stdin_open: true
tty: true
# serverless開発用
serverless:
build: ./serverless
environment:
- AWS_PROFILE=default
volumes:
- ~/.aws/:/root/.aws:ro
- ./serverless:/app
ports:
- 50000:5000
stdin_open: true
tty: true
dynamodb-local:
image: 'amazon/dynamodb-local'
container_name: dynamodb-local
user: root
ports:
- 8000:8000
volumes:
- ./dynamodb_data:/data
command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/data"]
dynamodb-admin:
image: aaronshaf/dynamodb-admin:latest
container_name: dynamodb-admin
environment:
- DYNAMO_ENDPOINT=dynamodb-local:8000
ports:
- 8001:8001
depends_on:
- dynamodb-local
で、serverlessコンテナのDockerfileはこんな感じです。
FROM python:3.9.13-alpine
ARG AWS_PROFILE
ENV NODE_PATH /usr/lib/node_modules/
# install nodejs
RUN apk update \
&& apk add --no-cache nodejs npm curl
# install aws-cli
RUN pip install --upgrade pip
RUN pip install awscli werkzeug boto3
# install serverless framework
RUN npm install -g serverless serverless-plugin-existing-s3
# change work directory
WORKDIR /app/flask-dynamodb-api
それから、serverless.ymlはこんな感じになりました。
これはserverlessコマンドのflask-dynamodb-apiのテンプレートから自動生成されるやつにちょこっと書き加えたやつです。
service: ando-san-flask-dynamodb-api
frameworkVersion: '3'
custom:
tableName: 'users-table-${self:provider.stage}'
wsgi:
app: app.app
dynamodb:
start:
host: dynamodb-local
port: 8000
noStart: true
migrate: true
stages:
- dev
provider:
name: aws
region: ap-northeast-1
runtime: python3.9
stage: dev
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- Fn::GetAtt: [ UsersTable, Arn ]
environment:
USERS_TABLE: ${self:custom.tableName}
functions:
api:
handler: wsgi_handler.handler
events:
- httpApi: '*'
plugins:
- serverless-wsgi
- serverless-python-requirements
- serverless-dynamodb-local
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: ${self:custom.tableName}
ポイントを解説していきましょう。
開発の起点どうすんべ
serverlessをpythonでチャレンジすると決めたので、Qiita漁って参考になりそうなのを探してきました。
こちらの記事を参考にさせていただきました。
とはいえ、4年前の記事なので、pythonのバージョン新しくしたいぜ!と思いイキってdockerhubにあるほぼ最新の3.10.5にしたら無事にserverless deployで死んだので一つ落として3.9にしました。
aws cli用のプロファイルは普通にホスト側にあるクレデンシャルを流用したかったので~/.aws/をコンテナ側にそのままマウントして、環境変数AWS_PROFILEを設定するようにdocker-compose.ymlに記述しています。
dynamodb-local って何?
dockerhubをウロウロしてたらdynamodb-localというDockerイメージが有るのを見つけました。
Serverless Flameworkではdynamodbはserverless-dynamodb-localというプラグインを使って、serverless dynamodb installとやってserverless dynamodb startするのが常套手段のようですが、以下の記事のとおり、別のプラグインによって道連れになって死ぬということがあるようです。
serverless-dynamodb-localプラグインは使わないとどうにもならないのですが、serverless dynamodb installを省きたいという意図です。
なので、この記事を参考にdynamodb-localも無事Dockerコンテナ化しました……というわけにもいかず、これだとmigrateが走らないのでテーブルが作られません。ふべん!ということで、serverless.ymlに一工夫を入れます。
custom:
tableName: 'users-table-${self:provider.stage}'
wsgi:
app: app.app
dynamodb:
start:
host: dynamodb-local
port: 8000
noStart: true
migrate: true
stages:
- dev
ここのhostとnoStartとmigrateのところですね。
hostはdocker-compose.ymlにあるdynamodb-localのコンテナ名ですね。
こうすることによってserverless dynamodb startのコマンドを叩いたときにmigrateが動いてDockerコンテナのdynamodb-localの方にテーブルが作られます。
あ、もちろんapp.pyも書き換えておかないとDockerコンテナのdynamodb-localを見に行かないので注意が必要です。
dynamodb_client = boto3.client('dynamodb')
if os.environ.get('IS_OFFLINE'):
dynamodb_client = boto3.client(
'dynamodb', region_name='dynamodb-local', endpoint_url='http://dynamodb-local:8000'
)
ローカルでAPIのテストをしたいだけなのに
最後、いちばん大事なポイントです。
README.mdには
serverless wsgi serveってやるとローカルで動きまっせ
みたいなこと書いてあって、鵜呑みにしてやってみるのですが、一向にコンテナの外から叩けません。
docker-compose.ymlでちゃんとportのバインディングもしているのにねえ、と悩んでいたんですよ。
Running "serverless" from node_modules
Using Python specified in "runtime": python3.9
* Running on http://localhost:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 170-470-797
# serverless開発用
serverless:
build: ./serverless
environment:
- AWS_PROFILE=default
volumes:
- ~/.aws/:/root/.aws:ro
- ./serverless:/app
ports:
- 50000:5000
stdin_open: true
tty: true

Connection was forcibly closed by a peerってなんやねん。
コンテナ側でhttp://localhost:5000ならホスト側でhttp://localhost:50000にしたらつながるんと違うんかい!と思うじゃないですか。違うんですよこれが。
Running "serverless" from node_modules
Using Python specified in "runtime": python3.9
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 170-470-797
はい! この通り! http://0.0.0.0:5000じゃないとつながらないのです!
こうするためにはこうしないといけないわけですね。
$ serverless wsgi server --host 0.0.0.0
これで無事、コンテナの外からAPIが叩けるようになって平和が訪れました。
さいごに
いかがでしたでしょうか。(言ってみたかった
これを参考にDockerでたのしいServerless開発ライフを送ってみてください。
