血と汗となみだを流す

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

API Gatewayをはじめよう②(API Gatewayの階層)

前回

この記事は何?

  • AWSコンソールで設定できる項目を書き出した
  • API Gatewayのリソースの階層(API、リソース、メソッド)について
  • リソースやメソッドなどについては次回以降書きます

項目全体

  • API
    • リソース(Resource )
      • メソッド(Method)
        • メソッドリクエスト(Method Request)
        • 統合リクエスト(Integration Request)
        • 統合レスポンス(Integration Response)
        • メソッドレスポンス(Method Response)
    • オーソライザー(Authorizer)
    • ゲートウェイのレスポンス(Gateway Response)
    • モデル(Model)
    • リソースポリシー(Resource Policy)
    • ドキュメント(Document)
    • ダッシュボード(Dashboard)
    • 設定(Setting)
  • 使用量プラン(Usage Plan)
  • APIキー
  • カスタムドメイン
  • クライアント証明書
  • VPCリンク
  • 設定

API Gatewayのリソースの階層

  • 最上位は「API
  • その下にリソース(resource)、更にその下にメソッド(method)を作る
    • メソッド(method)には以下4つがある
      • メソッドリクエスト(Method Request)
      • 統合リクエスト(Integration Request)
      • 統合レスポンス(Integration Response)
      • メソッドレスポンス(Method Response)
  • リソースのPATHがendpointの一部となる

次回

  • リソース(resource)について

API Gatewayをはじめよう①(Blackbeltを見て、API Gatewayがどんなことができるか知る)

この記事は何?

  • API Gatewayの奥が深すぎて、全てを理解するには時間が掛かりそうだから少しずつ触って吸収していくためのoutputです。
  • 果たしてこの順番でやっていくのが正しいかわかりませんが、一通りやったらどの順番が良いか改めてまとめ直す予定です。

まず最初に

  • Blackbeltを見て、API Gatewayがどんなことができるかを知りました
  • しかし2016年の資料なので、現時点(2018年)の仕様とは異なるかも
    • SlideShare中はACM未対応となっているが、現在は対応しているなど

Blackbeltから学んだ「API Gateway」とは

  • API管理の課題を解決してくれる
    • APIのバージョン管理
    • モニタリング
    • 管理とマネタイズ
    • 認証とアクセス権限の管理
    • トラフィック管理
    • アタックからの保護
    • インフラの管理とメンテナンス
  • responseをキャッシュしてくれる(TTL3600s)
  • 内部的にはCloudFrontを使っている
  • 「Usage Plan」という、外部にAPIを提供するときの無料/有料プランみたいなのが作れる
    • APIキーの所有者が使用できるリソースを制限するなど
    • 秒間リクエスト/バーストの許容/リクエスト可能数など
  • 認証・認可(※後述)
  • request/responseを他のデータ形式に変換できる
    • レガシーなbackendからのレスポンスをフィルタしたり、プライベートな情報を削除したり
    • GETリクエストのクエリストリングを元にPOSTデータを作る
    • LambdaからJSONを受け取りXMLに変換
    • Mockを作って、固定レスポンスを返すなど
  • API Gatwayと通信するためのSDKを生成できる
  • API定義をimport/exportできる
    • Swagger V2.0定義ファイルをサポート
  • backendにLambdaやEC2などと通信できる

価格

  • $4.25/100万リクエス
  • キャッシュのプロビジョニング料金
    • 何GiBキャッシュするかをコミットしておく

認証・認可

  • AWS Signature Version 4
    • IAMポリシーをアタッチしたIAMユーザのcredentialを使ってリクエストに署名
    • CognitoとSTSのようなtemp credentialを利用するとIAMロールと紐づく形で認証・認可が行われる
    • API Gatewayが生成するクライアントとSDKを利用する場合は自動的に利用可能
  • Custom Authorizer
    • OAuthやSAMLなどのベアラートークンを用いてAPIへのアクセウsを管理
    • lambdaファンクションを用いてAuthirozationヘッダの値を検証する
    • backendの呼び出し前にトークンの検証を行うファンクションを呼び出す
  • Cognito User Pools
    • User Poolsで認証を行う
    • 取得したIDトークンを基にAPIコール
  • いずれもメソッド単位で指定可能

次は

  • API Gatewayを構成するリソース郡を見ていく

ENI(Elastic Network Interface)の上限数について調べてみた

概要

  • AWSコンソールで確認したところ、ENIの制限数が350なのに、350以上のENIが存在していた
  • ENIの制限事項を調べたら、オンデマンドインスタンスの上限でも決まることがわかった
  • LambdaやECSタスクのENIもカウントされることがわかった

ENIの制限 

LambdaのENI使用数

ECSタスクのENI使用数

【最高】AWS Loftに行ってきました!

2018年10月1日にOPENした、AWS Loft Tokyoに行ってきました!

前日の台風のせいで交通機関は乱れに乱れまくる中、だいぶ余裕も持って家を出たらOPEN時間より早く着いてしまいました。 f:id:Anorlondo448:20181002231508j:plain

OPEN初日ということで、会場では無料でカフェラテを頂くことができ、さらには写真で撮った画像が名言と共にラテアートに・・・!

凄すぎるでしょこれ・・・そっくり度まで出てるしwww
ちなみにカフェでの支払いはAmazon Payかクレジットカードのみとのこと。


11時にオープニングセレモニーが始まり、アマゾンウェブサービスジャパン代表取締役社長の長崎さんによるスタートアップ支援についての説明がありました。

  • AWS Activate」1年間で最大10万ドルの有料サポートが無料
  • Solution Architectによる技術支援

などなど・・・
手厚すぎるでしょうこれ・・・
さすがAWS

そして斬新なテープカットイベント!
テープの「AWS」って文字が光ってるwwww f:id:Anorlondo448:20181002234836j:plain


続いては、

  • 株式会社トレタの増井さん
  • 株式会社ユーザベース、株式会社UB Venturesの竹内さん
  • オープニングセレモニーで話された長崎さん

によるスタートアップトークショー・・・だったのですが、ここらへんからリモートで障害対(ry)がありちゃんと聞くことができず・・・もったいない(;´Д`)

ただ、「好奇心を持ち続けること」という言葉だけは心に響きました。

ここで午前は終了となり、午後13時からソリューションアーキテクトの塚田さんによる「イノベーションを起こし続ける開発組織のカルチャー」のお話や今後のAWS Loftでのイベントについて。

セッション良すぎた・・・
これはいろんな人に聞いてもらいたいやつでした。
(資料公開されないかな・・・)
(動画で配信して欲しいな・・・)

目次は大きく分けて4つでAWSの開発組織における、

の話で、どれもこれも素晴らしい内容でした!
全部書き留められなかったのでとりあえず、心に残ったものを列挙

カニズム

  • Amazonでは会議でのプレゼンテーションツールの利用は殆ど無い
    • プレゼンテーション形式の会議は話し手の話術に依存する
    • 聞き手に取っての捉え方が変わってしまう恐れがある
  • 6 pagerと呼ばれる形式のレポートで行われる

アーキテクチャ

  • システムと組織。これらは分けて考えられない
  • DevOpsの実践
    • 文化(Culture)+実践(Practice)+ツール(Tool)
  • AWSの歴史
    • 1995サービスローンチ
    • 2001年、アプリケーションが肥大化し、モノリスが肥大化
    • 2004年、最初のサービスはSQS
  • モノリシックな開発サイクル
  • デプロイが一大イベントとなってしまっていた
  • microservice化した
  • Two-Pizza Teams
    • 少数精鋭のチームのほうがよりスピーディに進めることができる
    • Owenership & Autonomy
  • 作るものに対する全ての責任を負う
    • プロダクト計画の策定
    • ロードマップ
    • 開発
    • 運用/カスタマーサポート
    • 説明責任
  • 大きな組織の一部分
  • 自律的なスタートアップのイメージ
  • QAは誰がやる?
    • チームがやる
  • オンコールは誰が?
    • チームがやる
  • Opsは誰が?
    • Not Exist
    • チームがOpsもやる
  • 全てはサービスチームに存在し、自身の役割に集中する
  • チームには権限が与えられ、多くの自由が認められている
    • ただしチームとして高い水準を維持する必要がある
  • チームの水準を高く維持する

カルチャー

  • CI/CD
  • APOLLOというデプロイサービスを作った
  • MVP(Minimum Viable Product)
    • 必要最低限の能力を兼ね備えたプロダクト
    • フィードバックを重視、開発の優先度付もこれに基づく
  • PressRelase/FAQを先に作る
  • 6 pager/1 pager

組織

  • Our Leadership Principles
  • 全てのアマゾニアンが心がける信条
  • リーダーシッププリンシプル
  • 判断基準にもなり、共通認識によるショートカットになる

塚田さんのセッションの後は「Night - Opening Party」の準備時間までコワーキングスペースでもくもく!
この間に「Ask An Expertカウンター」でSolution Architectの方とCloudFrontの使い方について相談させて頂きました。

すぐ近くにSolution Architectの方がいるなんて、何て素晴らしい環境なんだ・・・!
自分たちで考えた対応方法なども聞いてもらい、フィードバックをもらったり、他の方法の検討を一緒にしてくれたり、濃密な時間を過ごしました。

月イチとかで、AWS Loftで作業したいなと本気で思いました。
AWSリソースの構成とか使い方とかその場で聞きながら作業できるの最強なのでは・・・


17:30からは「Night - Opening Party」!!!
シャレオツな料理と、 f:id:Anorlondo448:20181002235917j:plain f:id:Anorlondo448:20181003000012j:plain f:id:Anorlondo448:20181002235945j:plain

Amazon Dashボタンで光り方が変わる提灯wwww f:id:Anorlondo448:20181003000042j:plain f:id:Anorlondo448:20181003000104j:plain

スゲェ!シャレオツ!!!

ネットワーキングタイムでは、「凍らない方のいぬ」の方や、コンテナに強いポジティブな方と名刺交換できました。
二人共雲の上の存在でしたが、お話できて嬉しかった・・・・!

f:id:Anorlondo448:20181003000623j:plain f:id:Anorlondo448:20181003000651j:plain

素敵なお土産を頂き、素晴らしい体験をさせてもらった一日でした!

Solution Architectの方以外にも、AWSにつよい人がたくさん集まりそうな予感がするので、AWS Loftで今後どのようなイベントが発生していくのかとても楽しみです!

ああ〜〜はやく次行きてぇ

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

概要

対象

  • 実際に公開するページ(ride.html)で動く以下Javascript
    • js/ride.js
  • インラインでコメントしながら理解する

js/ride.js

メソッド一覧

  • function rideScopeWrapper($)
  • function requestUnicorn(pickupLocation)
  • function completeRequest(result)
  • function handlePickupChanged()
  • function handleRequestClick(event)
  • function animateArrival(callback)
  • function displayUpdate(text)

function rideScopeWrapper($)

/*global WildRydes _config*/

// グローバル変数「WildRydes」を定義。定義済みだったら定義済みの変数を使う
var WildRydes = window.WildRydes || {};

// 「WildRydes」のmapオブジェクトを定義。こちらも同じく定義済みだったら定義済みの変数を使う
WildRydes.map = WildRydes.map || {};

// ()でくくってあるので、読み込みと同時に実行される即時関数
(function rideScopeWrapper($) {

    // 認証トークン
    var authToken;

    // 認証トークンを取得する。取得できたらsetAuthToken()を実行する
    WildRydes.authToken.then(function setAuthToken(token) {
        if (token) {

            // トークンがあったら認証トークンにセット
            authToken = token;

        } else {

            // なかったらサインインページに遷移
            window.location.href = '/signin.html';
        }
    }).catch(function handleTokenError(error) {

        // exception発生したら画面にエラー表示して、サインインページに遷移
        alert(error);
        window.location.href = '/signin.html';
    });
    ...

    // Register click handler for #request button
    $(function onDocReady() {
        // requestボタンをクリックしたらhandleRequestClick()(後述)を実行するように定義
        $('#request').click(handleRequestClick);

        // signOutボタンをクリックしたら無名関数を実行
        $('#signOut').click(function() {
            // サインアウト処理
            WildRydes.signOut();

            // サインアウトしたメッセージを表示
            alert("You have been signed out.");

            // サインインページに遷移
            window.location = "signin.html";
        });

        // 地図上で「pickupChange」eventが発生したらhandlePickupChanged()を実行
        $(WildRydes.map).on('pickupChange', handlePickupChanged);

        // 認証トークンを取得する。取得できたらupdateAuthMessage()(後述)を実行する
        WildRydes.authToken.then(function updateAuthMessage(token) {

            if (token) {
                // トークンがある場合、メッセージを画面表示
                displayUpdate('You are authenticated. Click to see your <a href="#authTokenModal" data-toggle="modal">auth token</a>.');
                // 認証トークンにトークンをセット
                $('.authToken').text(token);
            }
        });

        // Lambdaのendpointが無かったら「#noApiMessage」のメッセージを表示
        if (!_config.api.invokeUrl) {
            $('#noApiMessage').show();
        }
    });
    ...
}(jQuery));

function handlePickupChanged()

  • 地図がクリックされた時にボタンの属性を変える
    function handlePickupChanged() {
        // requestボタンのjQueryオブジェクト
        var requestButton = $('#request');

        // requestButtonオブジェクトのテキストに文字列をセット
        requestButton.text('Request Unicorn');

        // requestButtonオブジェクトの「disabled」にfalseをセット。(requestボタンの非表示を解除)
        requestButton.prop('disabled', false);
    }

function requestUnicorn(pickupLocation)

    function requestUnicorn(pickupLocation) {

        // ajax通信を行う
        $.ajax({

            // POSTで通信
            method: 'POST',

            // 通信先はAPI Gateway
            url: _config.api.invokeUrl + '/ride',

            // ヘッダをセット
            // API Gatewayのオーサライザをセットしたときの「トークンのソース」にセットしたキー
            headers: {
                Authorization: authToken
            },

            // 地図上でクリックした地点の緯度経度情報をJSONにしたものをBodyにセット
            data: JSON.stringify({
                PickupLocation: {
                    Latitude: pickupLocation.latitude,
                    Longitude: pickupLocation.longitude
                }
            }),

            // コンテンツタイプセット
            contentType: 'application/json',

            // 成功時の処理。completeRequest()を実行
            success: completeRequest,

            // エラー時の処理
            error: function ajaxError(jqXHR, textStatus, errorThrown) {

                // エラーログを出力して、エラー画面を表示
                console.error('Error requesting ride: ', textStatus, ', Details: ', errorThrown);
                console.error('Response: ', jqXHR.responseText);
                alert('An error occured when requesting your unicorn:\n' + jqXHR.responseText);
            }
        });
    }

function completeRequest(result)

  • requestUnicorn()でAPI Gatewayへの通信が成功した時に呼び出される
  • 画面右上のメッセージ表示やボタン制御などを行う
    function completeRequest(result) {
        var unicorn;    // Unicornオブジェクト格納変数
        var pronoun;    // 代名詞?

        // API Gatewayから受信した結果をログ出力
        console.log('Response received from API: ', result);

        // 結果からUnicornオブジェクトをセット
        unicorn = result.Unicorn;

        // Unicornの性別によって、メッセージに表示する代名詞をセット
        pronoun = unicorn.Gender === 'Male' ? 'his' : 'her';

        // Unicornの名前、色などをメッセージに表示
        displayUpdate(unicorn.Name + ', your ' + unicorn.Color + ' unicorn, is on ' + pronoun + ' way.');

        // animateArrival()(後述)を実行
        animateArrival(function animateCallback() {

            // Unicornが来たというメッセージを表示
            displayUpdate(unicorn.Name + ' has arrived. Giddy up!');

            // 地図上の緯度経度を外す
            WildRydes.map.unsetLocation();

            // requestボタンを非表示
            $('#request').prop('disabled', 'disabled');

            // requestボタンの表示を「Set Pickup」に変更
            $('#request').text('Set Pickup');
        });
    }

function handleRequestClick(event)

  • requestボタンがクリックされたときの挙動
    function handleRequestClick(event) {
        // 地図の選択された地点の緯度経度を取得
        var pickupLocation = WildRydes.map.selectedPoint;

        // eventの処理を停止
        event.preventDefault();

        // requestUnicorn()を実行
        requestUnicorn(pickupLocation);
    }

function animateArrival(callback)

  • 選択した地点に現在地をセット
    function animateArrival(callback) {
        // 地図の選択された地点の緯度経度を取得
        var dest = WildRydes.map.selectedPoint;

        // 現在地をリセット
        var origin = {};

        // 選択された地点に現在地をセット
        if (dest.latitude > WildRydes.map.center.latitude) {
            // 
            origin.latitude = WildRydes.map.extent.minLat;
        } else {
            origin.latitude = WildRydes.map.extent.maxLat;
        }

        if (dest.longitude > WildRydes.map.center.longitude) {
            origin.longitude = WildRydes.map.extent.minLng;
        } else {
            origin.longitude = WildRydes.map.extent.maxLng;
        }

        // 地図の描画
        WildRydes.map.animate(origin, dest, callback);
    }

function displayUpdate(text)

    function displayUpdate(text) {
        // メッセージフィールドにテキストを表示
        $('#updates').append($('<li>' + text + '</li>'));
    }

わかったこと

わからなかったこと

  • 認証・認可って何?(裏で全部やってくれてるっぽい)

Javascriptがわからないのを克服していく④(地図上の位置情報を取得してオブジェクトを移動する)

概要

対象

  • 実際に公開するページ(ride.html)で動く以下Javascript
    • js/esri-map.js
  • インラインでコメントしながら理解する

js/esri-map.js

  • 地図上でピンやUnicorn画像を動かすための処理
  • Tokenなどの処理はなかった

全体

/*global WildRydes _config*/

// グローバル変数「WildRydes」を定義。定義済みだったら定義済みの変数を使う
var WildRydes = window.WildRydes || {};

// 「WildRydes」のmapオブジェクトを定義。こちらも同じく定義済みだったら定義済みの変数を使う
WildRydes.map = WildRydes.map || {};

// ()でくくってあるので、読み込みと同時に実行される即時関数
(function esriMapScopeWrapper($) {
    ...
}(jQuery));

require.js

    // esriのモジュールをrequireして、requireCallback()を実行する
    require([
        'esri/Map',
        'esri/views/MapView',
        'esri/Graphic',
        'esri/geometry/Point',
        'esri/symbols/TextSymbol',
        'esri/symbols/PictureMarkerSymbol',
        'esri/geometry/support/webMercatorUtils',
        'dojo/domReady!'
    ], function requireCallback(
        ...
    ) {
        ...
    });

requireCallback()

  • 地図上の位置情報を取得して、ピンやUnicorn画像を移動させる処理
function requireCallback(
        // 引数はrequireしたオブジェクト達
        Map, MapView,
        Graphic, Point, TextSymbol,
        PictureMarkerSymbol, webMercatorUtils
    ) {
        // グローバル変数のmapオブジェクト
        var wrMap = WildRydes.map;

        // 「gray-vector」っていうフリーのマップを使ってmapオブジェクト生成
        var map = new Map({ basemap: 'gray-vector' });

        // 地図表示用オブジェクト生成
        var view = new MapView({
            center: [-122.31, 47.60],
            container: 'map',
            map: map,
            zoom: 12
        });

        // 地図上に配置するピンのオブジェクト生成
        var pinSymbol = new TextSymbol({
            color: '#f50856',
            text: '\ue61d',
            font: {
                size: 20,
                family: 'CalciteWebCoreIcons'
            }
        });

        // ピンに向かってくるUnicornのオブジェクト生成
        var unicornSymbol = new PictureMarkerSymbol({
            url: 'images/unicorn-icon.png',
            width: '25px',
            height: '25px'
        });

        var pinGraphic;
        var unicornGraphic;

        // 与えられた緯度経度を地図の中心に持ってくる
        function updateCenter(newValue) {
            wrMap.center = {
                latitude: newValue.latitude,
                longitude: newValue.longitude
            };
        }

        // 与えられた数値を基に、表示領域の更新
        function updateExtent(newValue) {
            var min = webMercatorUtils.xyToLngLat(newValue.xmin, newValue.ymin);
            var max = webMercatorUtils.xyToLngLat(newValue.xmax, newValue.ymax);
            wrMap.extent = {
                minLng: min[0],
                minLat: min[1],
                maxLng: max[0],
                maxLat: max[1]
            };
        }

        // 地図を表示
        view.watch('extent', updateExtent);
        view.watch('center', updateCenter);
        view.then(function onViewLoad() {
            updateExtent(view.extent);
            updateCenter(view.center);
        });

        // 地図上でクリックされたら、eventを取得してhandleViewClick()を実行
        view.on('click', function handleViewClick(event) {

            // 地図上のクリックした地点を取得
            wrMap.selectedPoint = event.mapPoint;

            // ピンを削除
            view.graphics.remove(pinGraphic);

            // クリックした地点に新しいピンを生成
            pinGraphic = new Graphic({
                symbol: pinSymbol,
                geometry: wrMap.selectedPoint
            });

            // 地図に新しいピンを追加
            view.graphics.add(pinGraphic);

            // 「pickupCahge」eventを発生させる
            // event発生時の処理はride.jsのhandlePickupChanged()
            $(wrMap).trigger('pickupChange');
        });

        // アニメーション関数オブジェクト生成
        wrMap.animate = function animate(origin, dest, callback) {
            // 開始時刻
            var startTime;

            // animateFrame()関数を変数に入れる
            var step = function animateFrame(timestamp) {
                var progress;
                var progressPct;
                var point;
                var deltaLat;
                var deltaLon;

                // 開始時刻がなければ現在時刻を設定
                if (!startTime) startTime = timestamp;

                // 経過時間
                progress = timestamp - startTime;

                // 「経過時間/2000」と「1」の小さい方
                progressPct = Math.min(progress / 2000, 1);

                // 移動先と現在地の緯度経度の差分に上記計算したprogressPctを乗算
                deltaLat = (dest.latitude - origin.latitude) * progressPct;
                deltaLon = (dest.longitude - origin.longitude) * progressPct;

                // 移動先の地点のオブジェクトを生成
                point = new Point({
                    longitude: origin.longitude + deltaLon,
                    latitude: origin.latitude + deltaLat
                });

                // Unicorn画像情報を削除
                view.graphics.remove(unicornGraphic);

                // 新しい位置情報を持ったUnicorn画像を生成
                unicornGraphic = new Graphic({
                    geometry: point,
                    symbol: unicornSymbol
                });

                // Unicorn画像を追加
                view.graphics.add(unicornGraphic);

                // progressPctが1以下の場合、ブラウザ上でアニメーションを行う
                // @see https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame
                if (progressPct < 1) {
                    requestAnimationFrame(step);
                } else {
                    callback();
                }
            };
            // ブラウザ上でアニメーションを行う
            requestAnimationFrame(step);
        };

        // 位置情報が設定されていない場合、ピンを削除
        wrMap.unsetLocation = function unsetLocation() {
            view.graphics.remove(pinGraphic);
        };
    }

参考にさせてもらったページ

今回の収穫

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をどこで使うか・・・