AWS SAM で Lambda を作って API としてデプロイしてみる
前回の続き。
前回は、マネジメントコンソールから Lambda をひとつずつぽちぽち作る体験をした。
今回はいよいよ AWS SAM (Serverless Application Model) の力を借りて、コマンドラインベースで Lambda を試食してみよう。
お品書き
演習環境について
今回は、開発環境の構築を諸々チートできる Cloud9 というツールを使うことにする。
AWS Cloud9Σ(・Д・ノ)ノ https://t.co/eIfPXPWh5a
— たーせる'21 (@tercel_s) December 12, 2017
ご存知ない方のためにざっくり説明すると、これは Web ブラウザで利用できる開発エディタである(下図参照)。
Git やら Python やら Docker やら AWS SAM CLI やら、とにかく開発に必要なもろもろが初めから入っており、今回に関していえば Cloud9 ひとつ立ち上げるだけで開発環境がワンタッチで調達できてしまう優れものだ。
実を言うと、手元の M1 Mac にもローカル開発環境があるといえばあるのだが、そのへんの構築方法を一から書こうとするとそれだけで丸ごとひとつの記事分の分量になりかねないし、少々退屈な話になってしまうので、最初からいろいろ面倒を省ける道具に頼る方がよい。
【練習①】プロジェクトの作成とローカル動作確認
SAM CLI による新規プロジェクトの作成
Cloud9 の右下(下図の赤枠部分)のペインがターミナルである。 前回とは打って変わって、コマンドラインインタフェースを通してプロジェクトの作成・テスト・デプロイを行うことになるため、是非とも仲良くなっておきたいツールである。
ここに以下のコマンドを打ち込んで Enter キーを叩いてみよう。
$ sam init
Which template source would you like to use? と訊かれるので、1
(AWS Quick Start Templates) と入力し、Enter を押す。
さらに What package type would you like to use? と訊かれるので、1
(Zip) と入力し、Enter を押す。
Which runtime would you like to use? と訊かれるので、2
(Python 3.8) と入力し、Enter を押す。
続いて、Project name を訊かれる。 これは、何も入力しないとデフォルトで sam-app
という名前が付けられる。
特に拘りもないので、このまま何も入力せず Enter を押す。
AWS quick start application template を選ぶように促されるので、ここでは一番ベーシックな 1
(Hello World Example) を入力し、Enter を押す。
これでプロジェクトの作成が完了した。 ./sam-app/README.md
を読めと書いてあるので、左側のペイン*1から当該ファイルをダブルクリックで開いてみよう。
プロジェクトのフォルダ構成や、SAM CLI の基本的なコマンドなどが解説されている。
ここからは、特に断りがない限り、カレントディレクトリが sam-app
であるものとする。
Lambda 関数の確認
Lambda 関数の中身を確認するため、./hello_world/app.py
を開こう。
紙面の都合でコメントを除去しているが、以下のようなサンプルの Lambda 関数が実装されている。
前回マネジメントコンソールで見たコードとほぼ同じものであるため、特に抵抗感はない。
import json def lambda_handler(event, context): return { "statusCode": 200, "body": json.dumps({ "message": "hello world", }), }
ローカルでの動作確認
この Lambda 関数を、ローカルで動作確認してみよう。
まずはお手軽編として、以下のコマンドを実行してみよう。 オプションなどの意味は、いまは深く考えなくてよい。
$ sam local invoke HelloWorldFunction --event events/event.json
うまくいくと、実行ログと戻り値が表示されるはずだ。
ローカルでの REST API としてのテスト
続いて、これを REST API としてローカルで動かしてみよう。 Cloud9 のターミナルに、以下のコマンドを入力する。
$ sam local start-api
すると、http://127.0.0.1:3000/hello
に GET
メソッドでアクセスできるようになる。 Ctrl
+c
で解除するまで待ち受け状態が続くので注意。
このターミナルは start-api
によって占領されてしまったので、もうひとつ別のターミナルを起動しよう。+
ボタンを押すとポップアップメニューが表示されるので、「New Terminal」をクリックする。
新しいターミナルに、curl
コマンドを入力する。
$ curl http://127.0.0.1:3000/hello
うまくいくと、Lambda 関数が実行され、HTTP の Response Body の内容がターミナルに表示される。
【練習②】パラメータの受け渡し
入力パラメータと events/event.json
Lambda 関数を実行する際、外部から何らかのパラメータを受け取りたいとしよう。
たとえば、整数値 x
と y
という 2つの変数を受け取り、その値を合計して返す Lambda 関数を考えてみる。
前回書いたとおり、Lambda の実行に必要なパラメータは第一引数にまとめられるので、もし呼び出し側でパラメータをセットしていれば、event.get('x')
もしくはブラケットを利用した event['x']
で取得できる。
import json def lambda_handler(event, context): x = event.get('x', 0) y = event.get('y', 0) return { "statusCode": 200, "body": json.dumps({ "result": str(x + y) }), }
さて、正しく動くかどうかを試すため、実際に x
と y
にパラメータを受け渡した上でローカル実行してみたい。
ここで登場するのが、events/event.json
である。
ここには、実際に AWS 上で実行される Lambda に引き渡される想定のダミーデータが前もって定義されている。
ローカルでテストをする際に x
や y
の値を設定したい場合は、下図の赤枠のように x
、y
それぞれを JSON に追加すればよい。
あとは、先ほどと同じように sam local invoke
コマンドを打つだけだ。
$ sam local invoke HelloWorldFunction --event events/event.json
戻り値には、確かに x
と y
の合計が格納されていることが確認できる。
REST API のクエリパラメータ
続いて、この Lambda が REST API として実行されたときのクエリパラメータの取得方法を見てみよう。
ちなみにクエリパラメータとは、URL 末尾にありがちな、「?」以降のキーバリューのペアである(伝われ)。
これは、queryStringParameters
経由で取得できる。
import json def lambda_handler(event, context): params = event.get('queryStringParameters') x = int(params.get('x', 0)) y = int(params.get('y', 0)) return { "statusCode": 200, "body": json.dumps({ "result": str(x + y) }), }
そして、ターミナルに以下のコマンドを入力し ──
$ sam local start-api
先ほどと同じ要領で新たなターミナルを起動して、curl
を叩く。
$ curl http://127.0.0.1:3000/hello?x=100\&y=20
すると、クエリパラメータに引き渡した値がきちんと Lambda の中で足し算され、戻り値にセットされたことが分かる。
【練習③】AWS へのデプロイ
ここまでの成果を、AWS にデプロイしてみよう。
AWS SAM CLI によるデプロイ
ターミナルに、以下のコマンドを打ち込む。
$ sam deploy --guided
ここから、いくつかの質問を受けるが、ほぼほぼデフォルト(何も入力せず Enter)でよい。
ただし、HelloWorldFunction may not have authorization defined, Is this okay? という問いに対してだけは、この質問文の意味をよく理解した上で y
を応答する。
念の為に書き添えておくと、ここで「y
」を応答するということは、URL さえ知っていれば全世界どこからでもアクセス可能な API としてデプロイされてしまう悪魔の契りを交わしたことになるので本当に自己責任でお願いします。
個人の私有アカウントではなく、会社のアカウントを使って勝手にこういう実験をした場合、(内部統制に厳しい会社の場合は)譴責されたり始末書を書かされたりするので十分注意されたし。
残りの項目に対してはデフォルト応答でよい。
すると、しばらくデプロイ処理が走る。 2〜3分待って、Successfully と表示されたらデプロイ成功である。 さすがに見えてはいけない文字列がいくつかあったのでモザイク処理を施した。
ちなみに、AWS マネジメントコンソールでデプロイされた Lambda 関数を見ると、ご丁寧に API Gateway までセットでデプロイされていることが分かる。
なぜそうなるかというと、template.yaml
がそのような構成になっているからなのだが、今回は時間の都合でこのファイルの中身には触れないため、一旦「そういうふうにできている」と思うことにして先へ進もう。
【練習④】 CORS 対応
ここまでの練習で、Lambda を REST API として AWS にデプロイするステップまで完了した。
しかしながら、今の状態の API を JavaScript から(XHRで)呼ぼうとするとエラーになってしまう。 実験してみよう。
テスト用 HTML の作成
即席で以下のような HTML を用意した*2。
index.html
<html> <head> <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <script> (function() { $.get(`https://████████.execute-api.ap-northeast-1.amazonaws.com/Prod/hello?x=100&y=20`) .then(res => alert(JSON.stringify(res))); })(); </script> </head> <body> </body> </html>
これをローカルに保存して、適当なブラウザで開いてみよう。 本来は、API の戻り値がアラートに表示されるはずだが、何も起こらない。
開発者ツールを開くと、どうやら CORS エラーが発生しているようだ。
この悲劇の原因は、同一生成元ポリシーとよばれるブラウザ側のセキュリティ機能が働いたためである。
超ざっくり話すと、本来、自分自身のサイトと異なるオリジンにあるリソースへの XHR はそもそも許されていないのだが、CORS (Cross-Origin Resource Sharing) という仕組みを使うことによってこの制約を回避できる。
CORS の有効化
CORS を有効化するには Lambda 関数の戻り値を修正して、レスポンスヘッダに Access-Control-Allow-Origin
を追加すればよい。
import json def lambda_handler(event, context): params = event.get('queryStringParameters') x = int(params.get('x', 0)) y = int(params.get('y', 0)) return { "statusCode": 200, "headers": { "Access-Control-Allow-Origin": "*" }, "body": json.dumps({ "result": str(x + y) }), }
なお、上記のコードはあらゆるオリジンからのアクセスを許可するコードになっている。 実際に商用サイトとして運用する場合は、"*"
になっている箇所を、アクセスを許可したい特定のオリジンに書き換えた方がよい。
再度、sam deploy
でデプロイしてみよう。
まとめとか
今日は Cloud9 と AWS SAM CLI を使用して、コマンドライン操作で Lambda プロジェクトを作成し、API として AWS にデプロイした。
また、クエリパラメータの受け渡しや、CORS 対応なども行なった。
前回のようにあちこちのページを往ったり来たりはせず、ほぼ Cloud9 だけで操作が完結したのも大きなポイントである。
本文中では詳しい説明を避けたが、プロジェクトはテキストファイル(Pythonやyaml、jsonなど)の集合体であり、git などでのバージョン管理が容易であるなど、前回、課題として挙げた内容のいくつかは AWS SAM によってクリアされる。
話の流れの都合上、AWS SAM において最も重要な template.yaml
にほぼ触れず、Lambda 関数本体と events.json を多少修正しただけに過ぎない。
しかしこれだけでも前回の開発体験と何がどう変わるのかを体感できたように思う。
なっっが。
実は、テーマの設定から記事の完成まで軽く5時間以上かかっているのだ……。
なんでそんなに時間かかってるのw
ねー。
今回、細かく話し出すと長くなりそうなところは、かなり思い切って削ったのだけど、どうしてもストーリーが長くなってしまった……。
API をデプロイするときに、一箇所だけデフォルトじゃない応答を入力したところ?
それと、最後の Access-Control-Allow-Origin ヘッダとかね。
だいぶ手を抜いた。
時々思うんだけど、そんな杜撰なガバガバ設定でいいもんなのです?
あんまり良くない。
オンプレの閉じた環境であれば、ちょっとした動作確認のつもりでゆるい設定のままデプロイすることはざらにあるよね。
ふむ。
で、次回は?
次回は、いよいよ template.yaml の中身をいじってみようと思う。
DynamoDB をひとつこさえて、そいつを読み書きする Lambda を SAM で書いてみようかと。
まーた長くなりそうw
ねー。
本当は GW に他にもやりたいことがいくつもあるのに、これだけで連休が終わっちゃいそう。