前回・Terraform構成の続編的なやつです。
API Gateway から Lambda が動いたとして、そこからどんな感じでコーディングしていけばいいのか、をイメージするための部品についてまとめていきます。
扱える情報を知る
ここでいう情報ってのは、具体的には handler に指定しているであろう event の内容です。
1 2 |
def lambda_handler(event, context): print(event) |
これは、実際に print してログから値を確認した方がイメージしやすいと思いますが、それとドキュメントも参照するとバッチリです。
例えば、この辺の値は扱う頻度が多いものになるでしょう。
event.get(“requestContext”).get(“domainName”)
API にアクセスした際の FQDN が入っています。api.example.com とかになります。同じ値で event.get(“headers”).get(“host”) にありますが、こちらはあくまでヘッダーですね。
event.get(“rawPath”)
/request/path みたいのが入ってます。event.get(“queryStringParameters”)
event.get(“rawQueryString”) には、a=A&b=B みたいな文字列が入っていますが、event.get(“queryStringParameters”) にはそれがパースされて、{‘a’: ‘A’, ‘b’: ‘B’} のように dict で入っています。……と、このくらい見れば、だいたいHTTPっぽいことができるんだ、ということがわかるでしょう。
return
lambda_handler の return 返り値として、こんな dict を返すことで、HTTPとしてのステータスコード、そしてレスポンス内容としてクライアントに見せることができます。
1 2 |
def lambda_handler(event, context): return {"statusCode": 200, "body": "Nice Body!!"} |
ステータスコードをメソッドにしておく
色んなAPIを用意して、色んな分岐条件を書いていくと、それぞれで 200 だの 401 だの書くのはだんだん気持ち悪くなっていくと思いますので、どこかのクラスにこういうメソッドを用意しておくとよいかもしれません。
1 2 3 4 5 6 7 8 9 10 11 |
def returnOK(self, body="OK"): return {"statusCode": 200, "body": body} def returnBadRequest(self, body="Bad Request"): return {"statusCode": 400, "body": body} def returnUnauthorized(self, body="Unauthorized"): return {"statusCode": 401, "body": body} def returnForbidden(self, body="Forbidden"): return {"statusCode": 403, "body": body} |
パスと関数を紐付けてみる
色んな種類を追加していけるように、こんな風にパスと関数を関連付けることもできます。(ワザと直接的な書き方で載せています)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import os import json def lambda_handler(event, context): path = event.get("rawPath") func = os.path.basename(path) if func not in globals(): print("Forbidden, not found %s." % func) return {"statusCode": 403, "body": "Forbidden"} res = eval(func)(event) print("OK, %s" % func) return {"statusCode": 200, "body": res} def describe_instances(event): ec2 = boto3.client('ec2') describe = ec2.describe_instances() return json.dumps(describe) |
これだと、https://api.example.com/describe_instances にアクセスすることで、def describe_instances を呼び出し、その内容を JSON で返すことができます。
でも実際にそれなりの規模のAPIにするなら、普通にルートを使って処理を分岐していくことになります。
サクッと構築しただけだと、デフォルトとして全リクエストを1つの lambda に任せることになって、event.get(“routeKey”) に $default が入るだけになっちゃいますが、処理をカテゴリ分けしたり、パラメータをパースするなら最初からちゃんとした方がよいでしょう。
ただ、これを terraform でやると、lambdaコーディング と両方面倒みることになるのが、ちょっとですが開発速度的に鈍る感じが嫌なところですね。
認証
リモートワークだの、スマホからのアクセスだのを考えると、色んな場所からアクセスができると便利なのですが、API の URL がわからないとアクセスできないとはいえグローバルに全開放しておくってのは、内容次第とはいえ基本ありえないわけです。で、HTTP API には一応アクセスコントロールの手段が用意されているのですが
そんなに大仰なものは必要ない気分もあるわけです。例えば、Basic認証でいいんだけど、みたいな感じです。ここでは、ただの平文パスワードを認証として書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 |
def lambda_handler(event, context): auth_key = "Authorization" auth_value = "ExamplePassword" headers = event.get("headers") client_value = headers.get(name.lower()) if client_value != auth_value: print("Unauthorized simple auth.") return {"statusCode": 401, "body": "Unauthorized"} return {"statusCode": 200, "body": "Success"} |
これで普通にアクセスすると、401 が返りますし、
1 2 3 4 5 |
$ curl -v https://api.example.com/describe_instances ... < HTTP/1.1 401 Unauthorized ... Unauthorized |
ヘッダーを渡せば、200 が返ります。
1 2 3 4 5 6 |
$ PASSWORD="ExamplePassword" $ curl -v https://api.example.com/describe_instances -H "Authorization: $PASSWORD" ... < HTTP/1.1 200 OK ... Success |
Basic認証
Basic認証にしたければ、比較文字列を変えればいいだけです。クライアント的に書くなら、
1 2 |
$ AUTH_VALUE=$(echo -n $USER:$PASSWORD | openssl base64) $ curl -s https://api.example.com/describe_instances -H "Authorization: Basic $AUTH_VALUE" |
こういうことなので、やってることはたいして変わりません。
※追記 2020/06/30 18:00
結局なんとなく書いたので、某クラスのメソッドのままコピペですが貼っておきます
1 2 3 4 5 6 7 8 9 |
def authBasic(self, auth_user, auth_value): header_key = "Authorization" client_value = self.getHeaderValue(header_key) if not client_value: return False client_basic = re.sub(r'^Basic (.*)$', r'\1', client_value) auth_text = "%s:%s" % (auth_user, auth_value) auth_basic = base64.b64encode(auth_text.encode('utf-8')).decode('utf-8') return client_basic == auth_basic |
HTTPSアクセスしかないため、どちらにせよヘッダーは暗号化の範囲なので、URLだけで全開放されているよりは、あったほうがマシっていうかほぼ大丈夫でしょってくらいになります。
とりあえずHTTPの知識があれば、たいていのことはできるのがわかりました。あとはどういう規模と性質のAPIにするかで、AWSリソースとコーディングをどのようにしていくかを決めて、開発していくことになります。
私個人としては大規模なことはしないですが、何かのシステムの都合上、チョロっと API が必要になった時に、サクッと用意できて、しかも馴染み深いHTTPでってのは非常に嬉しい仕組みです。thx AWS:-)