Cognito#AWS岩田智哉2020.07.30この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。CX事業本部@大阪の岩田です。Lambdaという構成のメリットとして完全従量課金であるという点が挙げられますが、頻繁に利用される環境では従量課金が逆にデメリットとなり、自前でEC2上にアプリを構築する方が安く上がるというケースも有りえます。Cognitoユーザープールと連携させてみます。等々各サービスに必要な機能を横断的に提供するコンポーネントを指します。Gatewayについてはマイクロサービスアーキテクチャパターンとして以下のリンクで紹介されているので、よければ参考にして下さい。FrontendsKongに関する詳細な説明は割愛します。古いですが、Kongについてはこちらのブログでも紹介されているので、参考にして下さい。やってみるAWS上にKongの環境を構築するにはDockerイメージAMICloudFormationのテンプレート等が利用可能ですが、今回はせっかくなのでEC2上に自力で構築してみようと思います。Linux2のEC2を起動して進めていきます。後ほどLambdaと連携させる際に便利なので、LambdaのInvoke権限を持ったロールを事前に作成し、EC2にアタッチしておいて下さい。PostgreSQLのインストールKongが利用するバックエンドのDBはPostgreSQLもしくはCassandraが選択可能です。本来はアクセス増に伴ってスケールアウト可能なCassandraを利用するのが望ましいと思いますが、今回はお試しなのでPostgreSQLを利用します。いうことで、まずはPostgreSQLの環境を構築します。postgresqlKong用にデータベースとユーザーを作成します。psqlから以下のSQLを実行します。続いてKongのインストールと初期設定です。今回はKong公式のRPMファイルを利用します。依存関係の解決で利用するため、epelを有効化します。http://localhost:8001/{"plugins":{"enabled_in_cluster":[],"available_on_server":{"grpc-web":true,"correlation-id":true,"pre-function":true,"cors":true,"rate-limiting":true,"loggly":true,"hmac-auth":true,"zipkin":true,"request-size-limiting":true,"azure-functions":true,"request-transformer":true,"oauth2":true,"response-transformer":true,...略レスポンスが返却されればOKです。KongとLambdaを連携する続いてKongとLambdaを連携させてみましょう。Kongのエンドポイントに対して、/lambda1というパスでアクセスされた場合にEC2からLambdaを起動してレスポンスを返却するように設定します。事前に適当なLambdaを作成しておきます。イベントデータをそのままJSONで返却するだけのLambdaを作成しました。}このLambdaをKongのバックエンドとして統合していきます。サービスを作成します。ここではlambda1という名前でサービスを作成します。157{"host":"localhost","id":"898427b9-8356-4143-a492-1f575857ea5b","protocol":"http","read_timeout":60000,"tls_verify_depth":null,"port":8000,"updated_at":1596069207,"ca_certificates":null,"created_at":1596069207,"connect_timeout":60000,"write_timeout":60000,"name":"lambda1","retries":5,"path":null,"tls_verify":null,"tags":null,"client_certificate":null}続いて、作成したサービスlambda1に/lambda1というルートを追加します。5{"id":"4fe908ac-8fef-412b-b328-971cf583e7b7","path_handling":"v0","paths":["\/lambda1"],"destinations":null,"headers":null,"protocols":["http","https"],"created_at":1596069247,"snis":null,"service":{"id":"898427b9-8356-4143-a492-1f575857ea5b"},"name":null,"strip_path":true,"preserve_host":false,"regex_priority":0,"updated_at":1596069247,"sources":null,"methods":null,"https_redirect_status_code":426,"hosts":null,"tags":null}idが発行されるので控えておきましょう。この例だと4fe908ac-8fef-412b-b328-971cf583e7b7がidです。続いてlambda1というサービスからLambdaをInvokeするためにKongのプラグインを有効化します。Function名>"}}これでKong経由でLambdaを実行する準備ができました。試しに先程登録したルートにアクセスしてみましょう。先程まではKongの管理用APIを実行するために8001ポートにアクセスしていましたが、今度は一般ユーザー向けのエンドポイントにアクセスするため8000ポートへアクセスします。Functionのレスポンスが返却されてきました。Functionにイベントデータが渡せていないようです。GatewayとLambdaをプロキシ統合した場合とは大きく振る舞いが変わってきそうです。\"bar\"}"}今度はレスポンスボディにクエリストリングで指定した内容が返却されてきました。GETリクエストの場合はイベントデータとしてクエリストリングがLambdaに渡されているようです。Gatewayのユースケースとして定番のLambdaとの連携ですが、ここまでの作業でKongでも代替可能になりました。Gatewayの定番ユースケースであるCognitoユーザープールとの連携に挑戦します。Cognitoユーザープールから発行された妥当なトークンを持つユーザーだけAPIにアクセス可能になるのがゴールです。Cognitoユーザープールの作成手順などは特に説明しませんので、事前に適当なユーザープールを作成しておいて下さい。先程作成したlambda1というサービスでjwtのプラグインを有効化します。Kongにはjwtのプラグインが提供されており、このプラグインを利用することでjwtのトークンを検証することが可能になります。5{"created_at":1596069762,"id":"c2eb3b21-39a9-4a45-beb0-42b24701b950","tags":null,"enabled":true,"protocols":["grpc","grpcs","http","https"],"name":"jwt","consumer":null,"service":{"id":"898427b9-8356-4143-a492-1f575857ea5b"},"route":null,"config":{"secret_is_base64":false,"run_on_preflight":true,"uri_param_names":["jwt"],"key_claim_name":"iss","header_names":["authorization"],"maximum_expiration":0,"anonymous":null,"claims_to_verify":null,"cookie_names":[]}}続いて/lambda1というルートでjwtのプラグインを利用するように設定を追加します。先程控えておいた/lambda1というルートのIDを指定してプラグイン有効化のリクエストを発行します。5{"created_at":1596069832,"id":"bc01c780-251b-4432-b144-f6ae483c0c93","tags":null,"enabled":true,"protocols":["grpc","grpcs","http","https"],"name":"jwt","consumer":null,"service":null,"route":{"id":"4fe908ac-8fef-412b-b328-971cf583e7b7"},"config":{"secret_is_base64":false,"run_on_preflight":true,"uri_param_names":["jwt"],"key_claim_name":"iss","header_names":["authorization"],"maximum_expiration":0,"anonymous":null,"claims_to_verify":null,"cookie_names":[]}}コンシューマを作成します。4{"custom_id":"1","created_at":1596069924,"id":"88860da7-8e57-41ed-96a7-33fc729ee787","tags":null,"username":null}custom_idの指定は適当な値でOKです。リクエストに成功したら発行されたコンシューマのIDを控えます。この例だと88860da7-8e57-41ed-96a7-33fc729ee787がIDです。続いてコンシューマの設定を行います。JWTのトークンを検証するためにCognitoユーザープールの公開鍵を登録する必要があるので、Cognitoのエンドポイントから公開鍵を取得します。https://cognito-idp.ap-northeast-1.amazonaws.com/<ユーザープールID>/.well-known/jwks.jsonというURLにアクセスします。こんな感じで公開鍵が2つ公開されていると思います。]}この2つの公開鍵を登録...したいところなのですが、Kongでは1つのissuer対して1つの公開鍵しか登録できないようなので、片方だけ登録します。順番としてはメチャクチャなのですが、先にCognitoユーザープールからIDトークンを発行しておき、発行されたIDトークンをjwt.ioで確認し、発行されたIDトークンとkidが一致する方のjwkオブジェクトをコピーします。続いてコピーした公開鍵をpem形式に変換します。pemに変換可能です。jwkのオブジェクトを丸々入力欄に貼り付けて変換しましょう。KEY-----変換後の公開鍵をEC2上に保存します。ここではjwt.pemという名前で保存したことにします。公開鍵をコンシューマーに登録します。KEY-----\n\n","consumer":{"id":"88860da7-8e57-41ed-96a7-33fc729ee787"},"key":"https:\/\/cognito-idp.ap-northeast-1.amazonaws.com\/ap-northeast-1_fTJgs3leV","algorithm":"RS256"}rsa_public_key=@jwt.pemの部分で、先程EC2に保存した公開鍵を指定しています。6{"created_at":1596069762,"id":"c2eb3b21-39a9-4a45-beb0-42b24701b950","tags":null,"enabled":true,"protocols":["grpc","grpcs","http","https"],"name":"jwt","consumer":null,"service":{"id":"898427b9-8356-4143-a492-1f575857ea5b"},"route":null,"config":{"secret_is_base64":false,"run_on_preflight":true,"uri_param_names":["jwt"],"key_claim_name":"iss","header_names":["authorization"],"maximum_expiration":0,"anonymous":null,"claims_to_verify":["exp"],"cookie_names":[]}}準備完了です。先程と同様に普通にLambda用のエンドポイントにアクセスしてみましょう。Cognitoユーザープールと統合しているので、未認証のリクエストはエラーになるのが期待値です。http://localhost:8000/lambda1{"message":"Unauthorized"}期待値通りエラーになりました。続いてAuthorizationヘッダにCognitoユーザープールから発行されたIDトークンをセットして、再度リクエストを試みます。GatewayをKongに置き換えた風の構成が完成です。GatewayをKongに置き換えてみました。マネージドサービスの従量課金のコストがネックになる場合、かつしっかりとした運用体制が取れる場合はKongのようなOSSを自前運用するという選択肢もアリかもしれません。自前運用を検討される場合は、単に従量課金のコストだけで評価するのではなく、OSやミドルウェアレイヤの保守対応に必要なマンパワーも含めた上で評価して頂くようにお願いします。