血と汗となみだを流す

クラウドエンジニアになるための修業の場

JavascriptとCognitoとAPI Gatewayがわからないのを克服していく③(API GatewayとcognitoとLambdaの連携)

概要

モジュール 3、4で使っている技術

(おまけ)サービス名がAWSで始まったりAmazonで始まったり

  • 真偽は確かではないが、以下って聞いたことがある

モジュール 3:サーバーレスサービスバックエンドの概要

  • Lambda/DynamoDBを使ってWebアプリケーションのrequestを処理するためのバックエンドプロセスを構築
  • 手順通りにやれば問題なし
  • Lambdaのコードは後で読む(後述))

モジュール 4:RESTful API

  • モジュール3で作成したLambdaをAPI Gatewayで公開する
  • インターネット越しにパブリックアクセスできるが、Cognitoユーザプールで保護される
  • CognitoユーザプールAuthorizer作成でエラーが発生した
レスポンス
Response Code(応答コード): 401
レイテンシー 153
Unauthorized request: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx
  • 理由はわからないがAuthorizerを再作成して「Cognito ユーザープール」を入力しないでプルダウンで選択する形にしたら動いた。。。

API Gateway設定で気になったところ

  • endpoint typeの種類
    • リージョン: 現在のリージョンにデプロイされる
    • エッジ最適化: CloudFrontネットワークにデプロイされる。地理的に分散したクライアントへの接続時間が改善される
    • プライベート: VPCからのみアクセス可能

Authorizer

  • Lambda/Cognitoユーザプールを使用して、APIの承認ができる
  • トークンのソース
    • Cognitoユーザプールに送信するヘッダー
  • トークンの検証
    • 設定してある場合、Cognitoで認証する前に正規表現を使用して受信トークンの検証を行う
    • 複数のアプリから接続される場合、接続元の検証を行うっぽい(JWTのaudを検証?)

resource

  • API Gateway CORS を有効にする」をチェック
    • CORS(Cross-Origin Resource Sharing)
    • 異なるドメインへのリソースへアクセスできるようになる仕組み

method

  • 「Lambda プロキシ統合の使用」
    • requestはLambdaにプロキシされる
    • requestの詳細がhandler関数の「event」で参照できるようになる

プリフライトリクエス

  • こちらのページに詳しく解説がありました。
  • はじめにOPTIONSメソッドによるリクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうかを確かめる

Lambdaに付与するIAMロールについて

  • いつもLambdaを作る時に何となく付与していた「AWSLambdaBasicExecutionRole」を見てみる
  • CloudWatch Logs関連のロールしかついてなかった
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

インラインポリシー

  • IAMポリシー一覧には出てこない模様
  • 設定したIAMロールにのみ表示

メインディッシュ

Lambdaのコードを読む

全体の構成

  • const(定数定義)
  • exports.handler
  • function findUnicorn()
  • function recordRide()
  • function toUrlString()
  • function errorResponse()

const(定数定義)

  • lambdaで使う定数の定義ね
// Node.jsでセキュアのランダムな文字列を生成するクラスのオブジェクトを生成
const randomBytes = require('crypto').randomBytes;

// 我らがaws-sdkの読み込み
const AWS = require('aws-sdk');

// DynamoDBのドキュメント管理をするクライアントオブジェクトを生成
// @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html
const ddb = new AWS.DynamoDB.DocumentClient();

// fleetって、艦隊?
// Map状を動くUnicornの名前とかのデータJSON
// 「Rocinante」は・・・俺の地元のingress agentの名前
const fleet = [
    {
        Name: 'Bucephalus',
        Color: 'Golden',
        Gender: 'Male',
    },
    {
        Name: 'Shadowfax',
        Color: 'White',
        Gender: 'Male',
    },
    {
        Name: 'Rocinante',
        Color: 'Yellow',
        Gender: 'Female',
    },
];

callback関数について

function errorResponse()

  • エラーレスポンスをコールバックするための関数の模様
// 引数
//   errorMessage: エラーメッセージ
//   awsRequestId: AWSへのリクエストID
//   callback: コールバック関数 
function errorResponse(errorMessage, awsRequestId, callback) {
  // コールバック関数に以下を渡す
  // Lambda関数の失敗の実行結果: null
  // 関数の実行結果
  callback(null, {

    // ステータスコード500
    statusCode: 500,

    // エラーメッセージとリクエストIDのJSON
    body: JSON.stringify({
      Error: errorMessage,
      Reference: awsRequestId,
    }),

    // ヘッダー
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  });
}

function toUrlString()

function toUrlString(buffer) {
    return buffer.toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
}

exports.handler

// @see https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-handler.html
// 
// Lambda関数のハンドラーの引数
//   event: Lambdaが受信したイベントオブジェクト
//   context: Lambda実行中のランタイム情報オブジェクト(関数の残り実行時間とか) @see https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-context.html
//   callback: 明示的に呼び出し元にcallbackする場合のメソッド
exports.handler = (event, context, callback) => {

    // API Gatewayから認証情報が入力eventに入っているっぽい
    // @see https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
    if (!event.requestContext.authorizer) {

      // 認証情報が無かったらerrorResponse()をコールして終了
      errorResponse('Authorization not configured', context.awsRequestId, callback);
      return;
    }

    // ランダムな文字列からrideIdを生成
    const rideId = toUrlString(randomBytes(16));

    // rideIdをログ出力
    console.log('Received event (', rideId, '): ', event);

    // Because we're using a Cognito User Pools authorizer, all of the claims
    // included in the authentication token are provided in the request context.
    // 私達はCognitoユーザープール認証を使っているため、認証トークンに含まれるclaimsの全てはrequestコンテキストで提供される
    // This includes the username as well as other attributes.
    // これはユーザ名や他の属性も含んでいる
    // 
    // →つまり、Lambdaに渡される認証トークンでCognitoユーザプールのユーザ情報が取れる
    const username = event.requestContext.authorizer.claims['cognito:username'];

    // The body field of the event in a proxy integration is a raw string.
    // プロキシ統合(API Gateway?)のイベントのフィールドのBodyは、生の文字列です
    //
    // In order to extract meaningful values, we need to first parse this string
    // into an object. 
    // 意味のある値を抽出するには、まず。この文字列を解析してオブジェクトにする必要がある。
    //
    // A more robust implementation might inspect the Content-Type
    // header first and use a different parsing strategy based on that value.
    // より堅牢な実装では最初にContent-Typeヘッダを解析して、その値に寄って異なる方法の解析ストラテジを使用します
    //
    // →API Gatwayから渡される値の解析方法について書かれているっぽい
    // イベントの本文をパースする
    const requestBody = JSON.parse(event.body);

    // Mapをクリックした地点から緯度経度を取得
    const pickupLocation = requestBody.PickupLocation;

    // 移動してくるUnicornを取得
    const unicorn = findUnicorn(pickupLocation);

    // recordRide()を実行
    recordRide(rideId, username, unicorn).then(() => {
        // You can use the callback function to provide a return value from your Node.js Lambda functions. 
        // Node.jsのLambda関数からの値のreturnを提供するためのこのコールバック関数を使うことができる
        
        // The first parameter is used for failed invocations. 
        // 最初のパラメータは失敗時の呼び出しに使われる

        // The second parameter specifies the result data of the invocation.
        // 2番めのパラメータは呼び出しの結果データを出力する

        // Because this Lambda function is called by an API Gateway proxy integration
        // the result object must use the following structure.
        // Lambda関数はAPI Gatewayのプロキシ統合から呼び出されるため、結果オブジェクトは次のような構造を使う必要がある
        callback(null, {
            statusCode: 201,
            body: JSON.stringify({
                RideId: rideId,
                Unicorn: unicorn,
                UnicornName: unicorn.Name,
                Eta: '30 seconds',
                Rider: username,
            }),
            headers: {
                'Access-Control-Allow-Origin': '*',
            },
        });
    }).catch((err) => {
        console.error(err);

        // If there is an error during processing, catch it and return
        // from the Lambda function successfully. Specify a 500 HTTP status
        // code and provide an error message in the body. This will provide a
        // more meaningful error response to the end client.
        errorResponse(err.message, context.awsRequestId, callback)
    });
};

function findUnicorn()

// This is where you would implement logic to find the optimal unicorn for
// this ride (possibly invoking another Lambda function as a microservice.)
// For simplicity, we'll just pick a unicorn at random.
function findUnicorn(pickupLocation) {

    // ログに緯度経度を出力
    console.log('Finding unicorn for ', pickupLocation.Latitude, ', ', pickupLocation.Longitude);

    // ランダムに移動するUnicornを選出
    return fleet[Math.floor(Math.random() * fleet.length)];
}

function recordRide()

function recordRide(rideId, username, unicorn) {
    // DynamoDBにputした結果を返す
    return ddb.put({
        // Ridesテーブルに
        TableName: 'Rides',

        // 以下のItemを登録
        Item: {
            RideId: rideId, // ID
            User: username, // ユーザ名
            Unicorn: unicorn, // 移動したUnicornオブジェクト
            UnicornName: unicorn.Name,  // 移動したUnicornの名前
            RequestTime: new Date().toISOString(),  // requestされた時間
        },
    }).promise();
}

参考にさせてもらったブログなど

今回の収穫

  • Javascriptのコードをしっかり読めばそれほど難しいことはしていない。(aws-sdk最高)
  • API GatewayとCognitoの連携が結構簡単にできる
  • API GatewayからLambdaに渡されるeventに認証情報とかも入っている
  • API GatewayでCORSが可能
  • API Gatewayの設定項目が多くて、まだまだ奥が深そう

わからなかったこと

  • tokenをどこで使うか・・・