mangum + FastAPI で ASGI な Python アプリケーションを AWS Lambda + API Gateway 上で動かす
mangum を使うと FastAPI や responder といった ASGI アプリケーションを AWS Lambda + API Gateway 上へ簡単にデプロイすることが出来ます。 今回は FastAPI で書いた ASGI アプリケーションを mangum を使って AWS 上でデプロイする手順をメモしておきます。
mangum init
の注意点
mangum
を利用する場合、mangum init
を使ってプロジェクトファイルを生成します。 ただ、mangum init
には注意すべき点があるので先に記載しておきます。
最初に mangum init する
mangum init
すると requirements.txt
が生成されるのですが、その際必ず中身が下記のように mangum
だけで上書きされてしまいます。 ASGI 用の Web フレームワークとして FastAPI 等を利用していても requirements.txt
が上書きされてしまい、結果として後の手順で AWS へアップロードするパッケージに mangum
しか含まれなくなってしまいます。 その為、最初に mangum init
してプロジェクトの設定ファイルを作成し、後から requirements.txt
へ必要なパッケージを追記する必要があります。
ハンドラは app/asgi.py に定義する
mangum-cli
を使って mangum
のプロジェクト設定を行う際、AWS Lambda から呼び出されるハンドラは code_dir: app
の handler: asgi.handler
と決め打ちで出力されます。 生成されたファイルである mangum.yml
を手動で修正さればハンドラを定義しているファイル名を変更することは可能ですが、予めスクリプトは app/asgi.py
にしておけば無用なエラーを避けられて無難だと思われます。
mangum のプロジェクト名は AWS ルールに準拠したものにする
mangum-cli
を使って mangum
のプロジェクト設定う際にプロジェクト名を指定する必要がありますが、プロジェクト名は AWS へアップロードされる各種設定の名称に利用されます。 ここで例えば _
(アンダースコア) のように AWS の設定名に使えない記号を使ってしまうと、最後の手順で mangum deploy
する際に以下のようなエラーになりますので注意が必要です。
| # mangum deploy
Deploying your application! This may take some time...
An error occurred (ValidationError) when calling the DescribeStacks operation: 1 validation error detected: Value 'my_first_mangum' at 'stackName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*|arn:[-a-zA-Z0-9:/._+]*
There was an error...
|
mangum cli
には削除機能が無い
これは mangum init
と関係ありませんが、mangum deploy
で AWS へアプリケーションをデプロイする際、内部的には CloudFormation が実行されます。 CloudFormation テンプレートはあるので「何が作られたのか」は分かりますが、「作成されたもの」を自動的に削除する機能は、現状の mangum-cli
にはありません。 不要となった設定は手動で削除する必要があります。
プロジェクトディレクトリを作成する
まず初めにプロジェクトディレクトリを作成します。
プロジェクトディレクトリへ移動しておきます。
mangum-cli をインストールする
mangum
で作成したプロジェクトを AWS へアップロードするには mangum-cli
を使います。 pip
でインストールします。
以下のライブラリがインストールされました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | # pip list
Package Version
--------------- -------
awscli 1.17.9
boto3 1.11.9
botocore 1.14.9
Click 7.0
colorama 0.4.1
docutils 0.15.2
jmespath 0.9.4
mangum-cli 0.0.1
pip 20.0.2
pyasn1 0.4.8
python-dateutil 2.8.1
PyYAML 5.2
rsa 3.4.2
s3transfer 0.3.2
setuptools 45.1.0
six 1.14.0
urllib3 1.25.8
|
AWS CLI をセットアップする
mangum-cli
は AWS へのアップロードを行ってくれますが、前提として AWS CLI のセットアップを完了させておく必要があります。 aws configure
を実行し、AWS CLI のセットアップを完了しておきます。
| # aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: json
|
AWS S3 上に Bucket を作成する
後の手順で mangum-cli
を使って作成したパッケージをアップロードする先となる S3 Bucket を作成しておきます。 今回は my-first-mangum-bucket
という名前の S3 Bucket を作成したものとします。
| aws s3 mb s3://my-first-mangum-bucket --region ap-northeast-1
|
ちなみに mangum create-bucket
でも同様に、S3 Bucket を作成することが出来ます。
| mangum create-bucket my-first-mangum-bucket ap-northeast-1
|
デプロイ用の設定ファイルを作成する
mangum init [PROJECT] [S3-BUCKET] [REGION]
を実行して mangum
の設定ファイルを作成します。
| mangum init my-first-mangum my-first-mangum-bucket ap-northeast-1
|
mangum.yml
というファイルが以下の内容で作成されました。 timeout
は AWS Lambda へアップロードした後、Lambda のタイムアウト値になるようです。
| name: my-first-mangum
code_dir: app
handler: asgi.handler
bucket_name: my-first-mangum-bucket
region_name: ap-northeast-1
websockets: false
timeout: 300
|
この時点でディレクトリ構成は以下のようになりました。
| my-first-mangum/
├── mangum.yml
└── requirements.txt
|
尚、mangum init
実行時に S3 Bucket 名とリージョン名は省略可能ではあるものの、省略すると設定ファイル上 null
という値になってしまいます。 結局、これでは AWS へのデプロイ時にエラーとなってしまう為、省略せずに入力しておくことをお勧めします。
requirements.txt
を修正する
今回のサンプルは FastAPI も利用するので requirements.txt
へ fastapi
も追記します。 この時点で requirements.txt
の中身は以下になりました。
app/asgi.py を用意する
app
ディレクトリ配下に asgi.py
というファイル名でアプリケーションを用意します。 今回は以下の内容にしました。
| from mangum import Mangum
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World!"}
handler = Mangum(app, enable_lifespan=False)
|
この時点でディレクトリ構成は以下になりました。
| my-first-mangum/
├── app
│ └── asgi.py
├── mangum.yml
└── requirements.txt
|
ローカルビルドを作成する
次に mangum build
を実行してローカルビルドを作成します。
mangum build
を実行すると build
というディレクトリが作成され、その中に関連するファイルが大量に作成されます。 この時点でディレクトリ構成は以下のようになっていました。 FastAPI も含む為、それなりのサイズになっています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270 | my-first-mangum/
├── app
│ └── asgi.py
├── build
│ ├── LICENSE.md
│ ├── __pycache__
│ │ └── typing_extensions.cpython-38.pyc
│ ├── asgi.py
│ ├── fastapi
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-38.pyc
│ │ │ ├── applications.cpython-38.pyc
│ │ │ ├── concurrency.cpython-38.pyc
│ │ │ ├── datastructures.cpython-38.pyc
│ │ │ ├── encoders.cpython-38.pyc
│ │ │ ├── exception_handlers.cpython-38.pyc
│ │ │ ├── exceptions.cpython-38.pyc
│ │ │ ├── logger.cpython-38.pyc
│ │ │ ├── param_functions.cpython-38.pyc
│ │ │ ├── params.cpython-38.pyc
│ │ │ ├── routing.cpython-38.pyc
│ │ │ └── utils.cpython-38.pyc
│ │ ├── applications.py
│ │ ├── concurrency.py
│ │ ├── datastructures.py
│ │ ├── dependencies
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── __init__.cpython-38.pyc
│ │ │ │ ├── models.cpython-38.pyc
│ │ │ │ └── utils.cpython-38.pyc
│ │ │ ├── models.py
│ │ │ └── utils.py
│ │ ├── encoders.py
│ │ ├── exception_handlers.py
│ │ ├── exceptions.py
│ │ ├── logger.py
│ │ ├── openapi
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── __init__.cpython-38.pyc
│ │ │ │ ├── constants.cpython-38.pyc
│ │ │ │ ├── docs.cpython-38.pyc
│ │ │ │ ├── models.cpython-38.pyc
│ │ │ │ └── utils.cpython-38.pyc
│ │ │ ├── constants.py
│ │ │ ├── docs.py
│ │ │ ├── models.py
│ │ │ └── utils.py
│ │ ├── param_functions.py
│ │ ├── params.py
│ │ ├── py.typed
│ │ ├── routing.py
│ │ ├── security
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── __init__.cpython-38.pyc
│ │ │ │ ├── api_key.cpython-38.pyc
│ │ │ │ ├── base.cpython-38.pyc
│ │ │ │ ├── http.cpython-38.pyc
│ │ │ │ ├── oauth2.cpython-38.pyc
│ │ │ │ ├── open_id_connect_url.cpython-38.pyc
│ │ │ │ └── utils.cpython-38.pyc
│ │ │ ├── api_key.py
│ │ │ ├── base.py
│ │ │ ├── http.py
│ │ │ ├── oauth2.py
│ │ │ ├── open_id_connect_url.py
│ │ │ └── utils.py
│ │ └── utils.py
│ ├── fastapi-0.47.1.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE
│ │ ├── METADATA
│ │ ├── RECORD
│ │ └── WHEEL
│ ├── mangum
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-38.pyc
│ │ │ ├── adapter.cpython-38.pyc
│ │ │ ├── connections.cpython-38.pyc
│ │ │ ├── exceptions.cpython-38.pyc
│ │ │ ├── lifespan.cpython-38.pyc
│ │ │ ├── types.cpython-38.pyc
│ │ │ └── utils.cpython-38.pyc
│ │ ├── adapter.py
│ │ ├── connections.py
│ │ ├── exceptions.py
│ │ ├── lifespan.py
│ │ ├── protocols
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── __init__.cpython-38.pyc
│ │ │ │ ├── http.cpython-38.pyc
│ │ │ │ └── websockets.cpython-38.pyc
│ │ │ ├── http.py
│ │ │ └── websockets.py
│ │ ├── py.typed
│ │ ├── types.py
│ │ └── utils.py
│ ├── mangum-0.7.0-py3.8.egg-info
│ │ ├── PKG-INFO
│ │ ├── SOURCES.txt
│ │ ├── dependency_links.txt
│ │ ├── installed-files.txt
│ │ ├── requires.txt
│ │ └── top_level.txt
│ ├── pydantic
│ │ ├── __init__.cpython-38-x86_64-linux-gnu.so
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-38.pyc
│ │ │ ├── class_validators.cpython-38.pyc
│ │ │ ├── color.cpython-38.pyc
│ │ │ ├── dataclasses.cpython-38.pyc
│ │ │ ├── datetime_parse.cpython-38.pyc
│ │ │ ├── env_settings.cpython-38.pyc
│ │ │ ├── error_wrappers.cpython-38.pyc
│ │ │ ├── errors.cpython-38.pyc
│ │ │ ├── fields.cpython-38.pyc
│ │ │ ├── generics.cpython-38.pyc
│ │ │ ├── json.cpython-38.pyc
│ │ │ ├── main.cpython-38.pyc
│ │ │ ├── mypy.cpython-38.pyc
│ │ │ ├── networks.cpython-38.pyc
│ │ │ ├── parse.cpython-38.pyc
│ │ │ ├── schema.cpython-38.pyc
│ │ │ ├── tools.cpython-38.pyc
│ │ │ ├── types.cpython-38.pyc
│ │ │ ├── typing.cpython-38.pyc
│ │ │ ├── utils.cpython-38.pyc
│ │ │ ├── validators.cpython-38.pyc
│ │ │ └── version.cpython-38.pyc
│ │ ├── class_validators.cpython-38-x86_64-linux-gnu.so
│ │ ├── class_validators.py
│ │ ├── color.cpython-38-x86_64-linux-gnu.so
│ │ ├── color.py
│ │ ├── dataclasses.cpython-38-x86_64-linux-gnu.so
│ │ ├── dataclasses.py
│ │ ├── datetime_parse.cpython-38-x86_64-linux-gnu.so
│ │ ├── datetime_parse.py
│ │ ├── env_settings.cpython-38-x86_64-linux-gnu.so
│ │ ├── env_settings.py
│ │ ├── error_wrappers.cpython-38-x86_64-linux-gnu.so
│ │ ├── error_wrappers.py
│ │ ├── errors.cpython-38-x86_64-linux-gnu.so
│ │ ├── errors.py
│ │ ├── fields.cpython-38-x86_64-linux-gnu.so
│ │ ├── fields.py
│ │ ├── generics.py
│ │ ├── json.cpython-38-x86_64-linux-gnu.so
│ │ ├── json.py
│ │ ├── main.cpython-38-x86_64-linux-gnu.so
│ │ ├── main.py
│ │ ├── mypy.cpython-38-x86_64-linux-gnu.so
│ │ ├── mypy.py
│ │ ├── networks.cpython-38-x86_64-linux-gnu.so
│ │ ├── networks.py
│ │ ├── parse.cpython-38-x86_64-linux-gnu.so
│ │ ├── parse.py
│ │ ├── py.typed
│ │ ├── schema.cpython-38-x86_64-linux-gnu.so
│ │ ├── schema.py
│ │ ├── tools.cpython-38-x86_64-linux-gnu.so
│ │ ├── tools.py
│ │ ├── types.cpython-38-x86_64-linux-gnu.so
│ │ ├── types.py
│ │ ├── typing.cpython-38-x86_64-linux-gnu.so
│ │ ├── typing.py
│ │ ├── utils.cpython-38-x86_64-linux-gnu.so
│ │ ├── utils.py
│ │ ├── validators.cpython-38-x86_64-linux-gnu.so
│ │ ├── validators.py
│ │ ├── version.cpython-38-x86_64-linux-gnu.so
│ │ └── version.py
│ ├── pydantic-1.4.dist-info
│ │ ├── INSTALLER
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ └── top_level.txt
│ ├── starlette
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-38.pyc
│ │ │ ├── applications.cpython-38.pyc
│ │ │ ├── authentication.cpython-38.pyc
│ │ │ ├── background.cpython-38.pyc
│ │ │ ├── concurrency.cpython-38.pyc
│ │ │ ├── config.cpython-38.pyc
│ │ │ ├── convertors.cpython-38.pyc
│ │ │ ├── datastructures.cpython-38.pyc
│ │ │ ├── endpoints.cpython-38.pyc
│ │ │ ├── exceptions.cpython-38.pyc
│ │ │ ├── formparsers.cpython-38.pyc
│ │ │ ├── graphql.cpython-38.pyc
│ │ │ ├── requests.cpython-38.pyc
│ │ │ ├── responses.cpython-38.pyc
│ │ │ ├── routing.cpython-38.pyc
│ │ │ ├── schemas.cpython-38.pyc
│ │ │ ├── staticfiles.cpython-38.pyc
│ │ │ ├── status.cpython-38.pyc
│ │ │ ├── templating.cpython-38.pyc
│ │ │ ├── testclient.cpython-38.pyc
│ │ │ ├── types.cpython-38.pyc
│ │ │ └── websockets.cpython-38.pyc
│ │ ├── applications.py
│ │ ├── authentication.py
│ │ ├── background.py
│ │ ├── concurrency.py
│ │ ├── config.py
│ │ ├── convertors.py
│ │ ├── datastructures.py
│ │ ├── endpoints.py
│ │ ├── exceptions.py
│ │ ├── formparsers.py
│ │ ├── graphql.py
│ │ ├── middleware
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── __init__.cpython-38.pyc
│ │ │ │ ├── authentication.cpython-38.pyc
│ │ │ │ ├── base.cpython-38.pyc
│ │ │ │ ├── cors.cpython-38.pyc
│ │ │ │ ├── errors.cpython-38.pyc
│ │ │ │ ├── gzip.cpython-38.pyc
│ │ │ │ ├── httpsredirect.cpython-38.pyc
│ │ │ │ ├── sessions.cpython-38.pyc
│ │ │ │ ├── trustedhost.cpython-38.pyc
│ │ │ │ └── wsgi.cpython-38.pyc
│ │ │ ├── authentication.py
│ │ │ ├── base.py
│ │ │ ├── cors.py
│ │ │ ├── errors.py
│ │ │ ├── gzip.py
│ │ │ ├── httpsredirect.py
│ │ │ ├── sessions.py
│ │ │ ├── trustedhost.py
│ │ │ └── wsgi.py
│ │ ├── py.typed
│ │ ├── requests.py
│ │ ├── responses.py
│ │ ├── routing.py
│ │ ├── schemas.py
│ │ ├── staticfiles.py
│ │ ├── status.py
│ │ ├── templating.py
│ │ ├── testclient.py
│ │ ├── types.py
│ │ └── websockets.py
│ ├── starlette-0.12.9-py3.8.egg-info
│ │ ├── PKG-INFO
│ │ ├── SOURCES.txt
│ │ ├── dependency_links.txt
│ │ ├── installed-files.txt
│ │ ├── not-zip-safe
│ │ ├── requires.txt
│ │ └── top_level.txt
│ ├── typing_extensions-3.7.4.1.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ └── top_level.txt
│ └── typing_extensions.py
├── mangum.yml
└── requirements.txt
|
ローカルビルドをパッケージ化する
前の項目でローカルビルドの作成に成功したら、今度は mangum package
を実行してローカルビルドをパッケージ化します。
正常に終了すると packaged.yml
と template.yml
の、ふたつのファイルが作成されているはずです。 このファイルはほぼ同じ内容で、CodeUri
だけが異なります。 以下に packaged.yml
のサンプルを引用しておきます。
packaged.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 | AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ASGI application updated @ 2020-02-04 02:13:06.968651
Globals:
Function:
Timeout: 300
Parameters: {}
Resources:
HTTPFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://my-first-mangum-bucket/1234e4b5b40235469223c27031801234
Handler: asgi.handler
Runtime: python3.7
Environment:
Variables: {}
Events:
ProxyApiRoot:
Type: Api
Properties:
Path: /
Method: ANY
ProxyApiGreedy:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
HTTPFunctionIAMPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:*
Resource:
- arn:aws:s3:::*
- arn:aws:s3:::*/*
- Effect: Allow
Action: dynamodb:*
Resource:
- arn:aws:dynamodb:::*
- arn:aws:dynamodb:::*/*
- Effect: Allow
Action:
- execute-api:ManageConnections
Resource:
- arn:aws:execute-api:::*
- arn:aws:execute-api:::*/*
Roles:
- Ref: HTTPFunctionRole
Outputs:
HTTPFunctionAPI:
Description: API Gateway endpoint URL for HTTPFunction
Value:
Fn::Sub: https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/
HTTPFunction:
Description: HTTPFunction ARN
Value:
Fn::GetAtt: HTTPFunction.Arn
HTTPFunctionRole:
Description: Implicit IAM Role created for HTTPFunction
Value:
Fn::GetAtt: HTTPFunctionRole.Arn
|
テンプレートの妥当性を評価する
必須ではありませんが、mangum validate
を実行すると CloudFormation 用テンプレートの妥当性を確認することが出来ます。
| # mangum validate
[04-Feb-20 01:38:09] Found credentials in shared credentials file: ~/.aws/credentials
Template is valid!
|
アプリケーションを AWS へデプロイする
ここまでの用意が完了したら、いよいよアプリケーションを AWS へデプロイします。 mangum deploy
を実行します。 内部的には CloudFormation で必要な設定を作成していくのですが、若干時間がかかります。 今回は最小のアプリケーションを作成したつもりですが、私の環境では mangum deploy
が完了するまでに 1 分かかりました。
AWS の設定を確認する
S3 や Lambda、API Gateway を確認すると CloudFormation によってデプロイされた各種設定を確認出来ます。 S3 上にはアプリケーションのパッケージがアップロードされていました。
Lambda 関数も定義されています。
Lambda 関数のトリガーに API Gateway が設定されています。
ブラウザでアクセスしてみる
Lambda 関数のトリガーの API Gateway に設定された URL へブラウザでアクセスすると無事、ASGI アプリケーションが実行されました。