ここ最近自身で運営中のサービスにAPIを利用した機能を実装しようって事で、AWS API Gateway + Lambdaっていう形でいろいろやってみました。Let’s try FaaS.
こりゃ便利だなって部分とか、とってもしんどい部分があったのでちょっとだけノウハウをまとめてみました。未来の自分へ備忘録として。
まずAPI Gatewayを使った単純なAPI呼び出しの方法ですが、以下の構図ですね。
API Gateway + Lambda
API GatewayでURLを定義し外部からはそれを窓口にLambda処理を呼び出せます。
API Gatewayではステージへデプロイした時点で固有のパブリックURLが出来上がるのでそれを運用する形であれば難しい事考えずすぐに使えます。
ステージ: APIをデプロイする単位です。開発ステージ(dev)や本番ステージ(production)といった単位で分けたりします。 URLサンプル:https://62stwbyod1.execute-api.ap-northeast-1.amazonaws.com/default/...各リソースパス...
まあこのままだとあまりユーザーフレンドリーなURLとはいえません。
カスタムドメイン + ACM証明書 + API Gateway + Lambda
はい。上記のAWSが自動生成するパブリックなURLは嫌だって方はこちらの形で運用します。
個人や自社がもつ独自ドメインでAPIを運用する事ができます。
手順としてはRoute53に独自ドメインのルーティングを作成し、作成したAPI Gatewayに紐づけます。また、サーバ証明書(ACM)は必須のようです。※AWS Certificate Managerから無料で作成できます。
URLサンプル:https://app.webapi.nocote.com/...各リソースパス...
このようにユーザーにやさしいURLになりました。
※以下はRoute53による紐づけイメージ
アクセスにAPIキー認証をつける
APIへのアクセス認証を設けたい場合にお手軽なのが、APIキーを紐づけることです。ユーザーがリクエストするためにはx-api-key項目をヘッダに指定しないと認可エラー(403)となります。これにより簡易的な認証機能を実装できます。またAPIキーを各ユーザー固有のものにすればリクエスト元の個人を特定するのにも有効です。
APIキーに使用量プランを紐づけ
使用量プランというものがあります。APIキーは使用量プランに紐づけねばなりません。
使用量プランを設定することで、そのAPIキーでアクセスしてくるユーザー(またはシステム)に対してどの程度の回数アクセスできるか(クオータ)、どれくらいの頻度のアクセスを許可するか(レート)を制御します。
課金プラン実装のイメージがあるとピンと来ると思います。フリープランやビジネスプラン、プロプランによって月のリクエスト回数制限を制御したいって時に有効です。
とりあえずこのあたりまでのつながりをイメージできれば公開APIを実装できます。
他にもCognito等を使った認証方法があるようですがあまり汎用的でなく当方実装してないので割愛します。
ステージ・使用量プラン・APIキーの関係
使用量プランはあらかじめAPIのステージに紐づけておきます。
使用量プランとAPIキーおよびAPIのステージの紐づき関係には制約があります。
それはAPIキーを使ってアクセスしてきた人がどの使用量プランでそのアプリ(ステージ)を利用しているか特定できる事です。
ダメな例)
悪くない例)
良い例)
APIキーの作成や使用量プランとの紐づけは、PHP AWS-SDKなどを使ってサーバ側プログラムで行えます。ユーザーが利用するシステムに組み込んでおけばアカウント作成時などに自動で生成/割り当てる事ができます。
モデル
Json schemaでモデルを定義することができます。Swaggerドキュメントへの展開が主な利用用途となりそうですが、API Gatewayのコンソール上だけで管理しているとうっかり間違って消してしまった事があります・・・。ただ後述しますが現状それほど重要なファクターでもないので利用しないのもありです。
APIのバージョニング
APIのバージョニングには様々なケースが想定されます。たとえばAPIパスの途中やお尻にv1などつけるパターンもありますし、ドメインに組み込む、ドメインの次すなわち先頭リソースの前につけるってのもあります。AWSのバージョニングの標準的な考え方としては、この中の最後のやり方っぽいですね。例↓
https://pokapoka-nyankonabe.jp/v1/resource1/resource2
理由はこの位置にある事でカスタムドメイン名作成時にどのAPIのどのステージってやつを調整できるから。AWS GatewayはAPI単位でコピーができるのでv2を作るときにv1で使っているAPIをベースにコピーを作ることができるのです。v1をベースコピーしてv2用のAPI作って、カスタムドメインのパス/v2に紐づける。これによって、同一ドメインの/v1/リソース…、/v2/リソース…のどちらを使うかをユーザーに選択させる事ができます。
あくまでAPI Gatewayの考え方ですので、バージョニングのあるべき論は世の中の賢い方にお任せします(なげやり)。
レスポンスコード
参照系のAPIについては、200 OK以外はとりあえず以下のコードくらいを追加しておけばよいかな、と。
- 400 クライアントエラー。パラメータの指定ミスなど。
- 403 Forbidden。認可エラー、ユーザーアカウントに操作権限がない場合など。Api-keyが間違っている場合もこれが返る。
- 429 Too Many Requests:リクエストの頻度がレートの制限を超えた。Limit Exceeded:使用回数がクオータの設定数を超えた。
- 500 サーバエラー。内部例外が起きた場合などはコレ。
- 503 サービス利用不可。メンテナンス時など一時的なレスポンスとして。
- 504 タイムアウト。LambdaやAPI Gatewayの処理制限時間を超えた場合。Lambdaの制限時間オーバーはデフォルトでは200 OKが返るようになっているので統合レスポンスで504に書き換えるなどの工夫が要ります。
また更新系は201 createdやIDをユーザーに指定させる場合などは409 conflictなんかも考慮。
ブラウザからAjaxでAPIアクセスした際にレスポンスエラーも判定させる場合は、各レスポンスコードに対してAccess-Control-Allow-Originをつけてあげてください。クオータの制限エラーなどはデフォルト設定されているゲートウェイのレスポンスが返るのでこちらにもつける必要があります。
APIドキュメントの生成
Swaggerへのエクスポート機能があります。Swaggerの存在を知らずに使い始めた当初の私としてはAWS GatewayからエクスポートしたファイルをSwaggerHubで読み込ませることでAPIドキュメントがスムーズに作れて感動して震えました。
上記で設定したモデルやレスポンスコードの情報も出力されます。
でもSwaggerHubを使っていくうちに、モデルのJsonにはSwagger定義上のdescriptionやexampleなど指定できないのでAPIの説明書として足りない情報はSwaggerHub上でけっこう付け足さないといけないし、そもそもAWS Gatewayコンソールでモデルやレスポンスコード作っていくの面倒いし・・・なんかいまいち。
使いどころとしては、API Gateway上である程度固まってきたらSwaggerへエクスポートして初回作成。後はSwaggerHub上で編集していくのがいいと思いました。
Swagger(およびOpenAPI)定義書からの取り込みとAPIコピー
新しいAPIを作る時にSwaggerの定義ファイルを読み込ませて作る方法とAPIごとコピーする方法があります。
APIのコピーはすべての情報を新しい方に持っていけるけど、Swagger定義書はAPIのすべての内容を記述できるわけではないので限界があります。
たとえばSwagger定義にはAPI Gatewayが持つ統合リクエストや統合レスポンス、ステージの情報はありません。
ですので、新しいAPIを作る時に別のAPIからSwagger定義書のエクスポート->新しいAPIにインポートでコピーすると
・リソースのパス
・メソッドリクエスト
・メソッドレスポンス
・モデル
といった情報しかコピーできないと思ってください。
開発中リソースのセキュリティポリシー
APIキーで防いでいるとはいえ、不正なアクセスに対して念には念を入れておきます。
※ひょっとしたらAPI認証チェックを付け忘れているものもあるかもしれませんしね。
特に開発中において下記のようにリソースポリシーでアクセス元のIPを縛るとプライベートのアクセスに絞れて安心です。
Resource idに続けて/dev等ステージ名を指定するとステージ単位での制御ができます。
詳しくは、こちらを確認してください。
※リソースポリシーの設定はステージに対して有効にする場合、そのステージにデプロイする必要があります。またデプロイ後、30秒ほどのタイムラグを見といてください。
API Gatewayから呼び出すLambdaコードの開発スタイル
私は主にPythonを使用してます。書きやすいので。
で、問題は開発環境。
Lambdaコンソールからももちろん開発できるけど、細かい静的コードチェック等行ってくれたり、テストコード実行できるIDE使う方が間違いなく効率あがります。LambdaコンソールはCtrl-Zとかでコード戻したりした時にコードがバグる経験を何度かしてるんでがっつりは使えない印象です。
って事でCloud9を使ってみました。
Lambdaからの取り込みおよびLambdaへのデプロイが楽ちん過ぎてテンションあがります。
でもMacで使ってるとエディタのタブをタップした際にマウスポインタにタブが付いてくる現象が多発してテンション下がりました・・・。後はブラウザベースなのでエディタ上でのコードチェック処理が若干遅い、とか難点もあるもののLambdaコンソールよりは間違いなく捗りました!
Lambdaの開発においてはCloud9まじオススメですね。
ちなみにMySQLやPyYAMLなどの外部ライブラリはLayerパッケージ(Lambdaの機能です)でまとめました。
このLayerパッケージもCloud9上に同居させて必要な際はアップデートできるようにしてます。ただLayerは結局Zipで落としてLambdaコンソール上からアップしかできないんでもうちょっとデプロイ方法を工夫したい思いでいっぱいです。
補足ですがLayerで外部パッケージまとめるとメイン部分のサイズ小さいからLambdaコンソール上でソースが見えなくなるってケースがほとんど無くなります。間に合わせのちょっとした修正はLambdaコンソールからもできると便利ですしね。
もう1点工夫した点としては、DBへの接続情報などのファイルを共通ディレクトリで管理した事です。Cloud9の稼働している実態はLinux環境なのでシンボリックリンクが作成できますので、複数のLambda関数をCloud9上で開発できるようにしておき、業務固有の共通的な設定などは各Lambda関数のディレクトリからシンボリックリンクで共通のファイルを参照するようにすると構成管理上非常に楽になります。Cloud9上のpython実行でも参照できる上、Lambdaへのデプロイ時に一緒に付いてきてくれるからこの運用はいい感じです。Lambdaへのデプロイ時は実ディレクトリ(ファイル)になるところがポイントですね♪
あと、Linux環境なのでgitでバージョン管理させることもできます。
ただCodecommitを使っている方はgitリモートのアクセスはhttpsではなくsshを使用した方がいいです。Cloud9を立ち上げると.aws/creadentialsファイルが毎回置き換わるのでACCESSKEYなどが毎回必要になります。これはとても苦痛です。※ec2-userとは別のユーザーを作ってそちらでgitを上げる等工夫すればいいのかもしれませんが。
AWS Gatewayのしんどい部分
個人的にAWS Gatewayのいけてない点を書いていきます。
APIのパス書き換えが面倒
これ、ちょっとやそっとの苦痛じゃないです。
というのもまず現状はAWSコンソール(画面)上からAPIパス、もっというとリソースの単位で名前の変更や階層移動ができません。
※リソースっていうのはAPIパスで”/”で区切られた各部分です。
/data/item -> dataとitemがリソースになります。
コンソールからできないって事でパスの書き換えをどうするかっていうと、CLIやAPIを駆使して行います。
具体的には、下記のコマンドでリソースの単位で内部的に割り振られているIDを確認し、
そこから名前の変更したい対象のリソースのIDを拾います。
aws apigateway --region ap-northeast-1 get-resources --rest-api-id xxf6jxxxif ... { "path": "/data/item", "id": "l9ccj1", "pathPart": "item", "parentId": "14q5ym" }, ...
次にコマンドでそのリソースIDを指定してパラメータで”名前の変更”、”何に”を指定して書き換えます。下記の例ではitemをmenusに書き換えてます。
aws apigateway update-resource --region ap-northeast-1 --rest-api-id xxf6jxxxif --resource-id=l9ccj1 --patch-operations "op=replace,path=/pathPart,value=menus"
さらに階層移動とかになってくると悲劇です。
階層移動する結果、親のリソースIDがどれになるかを確認してコマンドから対象リソース、その親のリソースIDを指定してパラメータ指定で実行します。
aws apigateway update-resource --region ap-northeast-1 --rest-api-id xxf6jxxxif --resource-id=l9ccj1--patch-operations "op=replace,path=/parentId,value=親のリソースID"
やってみて何度も間違えました。しんどいです。
じゃあAPIのパス、新しく作った方がいいのではって事で新たな名前でパス作るのもありですがAPI Gatewayではそのリソースに紐づくメソッドに様々な構成要素を紐づけます。モデルとかリクエストパラメータとかレスポンスコードとかマッピングとか。
APIリソースを新しく作るってことはこれらも再度作る必要があるので非常に面倒なのです。
さらに細かい事ですが、私はリクエストパスにパスパラメータ(example.co.jp/{xxx}/item でいうところのxxxの部分)を使っていました。これを使うことで動的なパスに対応できます。
そしてこのパラメータは、API GatewayからSwaggerドキュメントを作成する際にも反映されて便利なのですが、CLIを使ってリクエストパスの書き換えを行うとこの部分のドキュメントへの反映がおかしくなってしまいました。オーマイガ。
具体的にいうと、リソース名の変更によって{xxx}を{yyy}に変更したのですが、ドキュメントに出力した際にはyyyが出力されずxxxと出力されるままになってしまいました。
画面上からはyyyで正しく構成されてるのでこのトラップには後で気づいた時に唖然としました。
それもあってSwaggerドキュメントの出力はサポート程度と認識してます。自分で正しく書いたSwaggerドキュメントにAWSコンソールから出力したレスポンスなどの情報をコピペで付加する程度がいいです。
#ブラウザでパス名の変更、位置変更ができるGUIを作りました!
AWS API GATEWAYのリソースパスを変更するGUI
レスポンスの設定が面倒
レスポンスについても結構面倒くさいです。
401や403といったコードごとに画面からポチポチやって設定する運用にしてるんですが非常に時間がかかります。おんなじようなレスポンスを各リソースのメソッドに繰り返す時とか発狂しそうになります。コピペ機能くれ!
※Swagger定義からの部分取り込みもできますが、設定ミスるとまた絶望です。
とりあえずAPI GatewayのコンソールのUIについては検討して欲しいとこがけっこう出てきました。コンソールからリクエストは投げましたけど果たして対応頂けるのか。届けこの想い。
現場からは以上です。