血と汗となみだを流す

個人の思ったこと、やったことの吐き出し口です。

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

Javascriptがわからないのを克服していく②(cognitoと連携したユーザSignIn)

概要

今回

  • singinページのjavascriptを読む
  • 対象は同じ js/cognito-auth.js

前回と同じ様の読めるところすっ飛ばし

  • signin.htmlでjs/cognito-auth.js読み込み
  • signin.htmlのsigninFormがsubmitされるとhandleSignin()が実行される
  • signin()に、成功/失敗時のfunctionを渡して実行
    • 成功:「Successfully Logged In」と表示してride.htmlへ繊維
    • 失敗:エラーをalert()で表示

signin()

  • インラインでやってることを推測しながらコメントを追加
  • 参照しているソースコードはおそらくここのクラスだと推測して読む
    function signin(email, password, onSuccess, onFailure) {

        // メールアドレスとパスワードから、Cognitoの認証用データオブジェクトを生成する
        var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails({
            Username: email,
            Password: password
        });

        // createCognitoUser()(後述)でcognitoユーザオブジェクトを生成する
        var cognitoUser = createCognitoUser(email);

        // 上記の認証用データオブジェクトで認証処理を行う。詳細は以下(簡単に言うと認証してcallbackしている)
        // @see https://github.com/aws-amplify/amplify-js/blob/master/packages/amazon-cognito-identity-js/lib/CognitoUser.js#L253
        cognitoUser.authenticateUser(authenticationDetails, {
            onSuccess: onSuccess,
            onFailure: onFailure
        });
    }

createCognitoUser()

  • インラインでやってることを推測しながらコメントを追加
    function createCognitoUser(email) {

        // ユーザプールオブジェクトとメールアドレスから、Cognitoユーザオブジェクトを生成する
        return new AmazonCognitoIdentity.CognitoUser({
            Username: email,
            Pool: userPool
        });
    }

本日javascriptと和解できたところ

  • 基本フローは前回とほとんど一緒なのでスラスラ読めた!
  • 読めるようになると楽しさが増してくる

本日和解できなかったところ

  • CoginitoのJavascript SDKのマニュアルが見つからない・・・
  • おそらくAWS AMplifyのライブラリを使っているのだろうけど・・・どこだ?!

Javascriptがわからないので少しずつコード読んで和解していく(cognitoと連携したユーザ登録)

概要

  • サーバーレスウェブアプリケーションの構築をやっていた
  • 「モジュール2 ユーザ管理」通りにやっていたら、あまりにも簡単に認証ページが作成できた
  • が、AWS CognitoとJavascriptがどう動いているか全くわからず・・・
  • そろそろちゃんとJavascriptと向き合わないと行けないなと思い、コードを読んで理解することにした

コード

コード読んだ対象

  • ユーザ登録を行っているregister.htmlで読み込んでるjavascript

長くなってしまったので、先にユーザ登録フローのまとめ

  • register.htmlで登録フォーム表示
    • メールアドレス
    • パスワード
    • パスワード確認
  • submitすると js/cognito-auth.js のhandleRegister()が実行される
  • handleRegister()で以下の処理を実行
    • フォームの値を取得する
    • ユーザ登録処理成功/失敗時の処理を定義
    • ユーザ登録処理(register())を実行
  • register()で以下の処理
    • ユーザの登録データオブジェクトを生成
    • cognito-sdkのAmazonCognitoIdentityクラスを使ってcognitoにユーザを登録する

register.html

  • SignUpページ
  • メールアドレスと、パスワード2回入れる
  • formのデータをsubmitしたときの処理はどこかのJSに書いてあるっぽい
  • JS読み込みは以下の箇所
<script src="js/vendor/jquery-3.1.0.js"></script>
<script src="js/vendor/bootstrap.min.js"></script>
<script src="js/vendor/aws-cognito-sdk.min.js"></script>
<script src="js/vendor/amazon-cognito-identity.min.js"></script>
<script src="js/config.js"></script>
<script src="js/cognito-auth.js"></script>
  • js/vendor/ 配下はおそらくjQuery、bootstrapと、cognitoのSDK用のライブラリ
  • js/config.js はcognitoに接続するためのプールIDやアプリクライアントIDの設定用
  • js/cognito-auth.js が怪しいので見ていく

bootstrap

js/cognito-auth.js が呼ばれるときに実行される処理ってどれ?

  • 関数の最初と最後だけ抜き出してみる
(function scopeWrapper($) {
    ..[省略]..
}(jQuery));
  • functionが()でかこまれてるのは「即時関数」にしている
    • 即時関数: 関数を定義すると同時に実行する
    • つまり、js/cognito-auth.jsが読み込まれると同時に上記関数は実行される
  • 後述の「userPoolオブジェクト生成」「onDocReady()」とかもここで実行されている
  • 引数で「jQuery」が渡されて、引数定義の「$」がjQueryオブジェクトとなる
    • つまり、「function scopeWrapper」内の「$」はjQueryオブジェクト

userPoolオブジェクト生成

  • js/cognito-auth.jsの呼び出し時にオブジェクト生成している
  • インラインでやってることを推測しながらコメントを追加
(function scopeWrapper($) {
    // サインイン用のページ
    var signinUrl = '/signin.html';

    // config.jsからAWS Cognitoユーザプールの認証情報を取得
    var poolData = {
        UserPoolId: _config.cognito.userPoolId,
        ClientId: _config.cognito.userPoolClientId
    };

    // userPoolオブジェクト定義
    var userPool;

    // ユーザプールID、アプリクライアントID、リージョンがセットされてなかったら、
    // register.html内にある「noCognitoMessage」部分を表示(エラーメッセージ的なやつ)
    if (!(_config.cognito.userPoolId &&
          _config.cognito.userPoolClientId &&
          _config.cognito.region)) {
        $('#noCognitoMessage').show();
        return;
    }

    // 認証情報から、Cognitoユーザプールオブジェクトを生成
    userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
  • 大まかに
    • config.jsの情報からユーザプールオブジェクトを生成

js/cognito-auth.jsのonDocReady()メソッド

  • formのsubmitの挙動に関する定義っぽい箇所があった
    /*
     *  Event Handlers
     */

    $(function onDocReady() {
        $('#signinForm').submit(handleSignin);
        $('#registrationForm').submit(handleRegister);
        $('#verifyForm').submit(handleVerify);
    });
  • submit()メソッド

handleRegister()メソッド

  • submitされると実行される
  • インラインでやってることを推測しながらコメントを追加
    // 画面上のイベントを引数でもらう
    function handleRegister(event) {

        // 「registrationForm」にあったパラメータの値を変数に入れる
        var email = $('#emailInputRegister').val(); // メールアドレス
        var password = $('#passwordInputRegister').val();   // パスワード
        var password2 = $('#password2InputRegister').val(); // パスワード(確認用)

        // 後ろにあるregister()メソッドの引数として、register()が成功したときに実行するfunctionを定義している
        var onSuccess = function registerSuccess(result) {

            // おそらく成功した場合にresultにユーザ情報が入っている
            var cognitoUser = result.user;

            // コンソールにユーザ名を表示
            console.log('user name is ' + cognitoUser.getUsername());

            // 「ユーザ登録成功してメール送ったから確認しろよ、認証コード入っているから、スパムフォルダとか見てね」的な確認メッセージを定義
            var confirmation = ('Registration successful. Please check your email inbox or spam folder for your verification code.');

            // 確認メッセージがあったら(っていうか上で定義しているから絶対あるのでは・・・)、verify.htmlに画面遷移する
            if (confirmation) {
                window.location.href = 'verify.html';
            }
        };

        // 後ろにあるregister()メソッドの引数として、register()が失敗したときに実行するfunctionを定義している
        var onFailure = function registerFailure(err) {

            // エラーを表示
            alert(err);
        };

        // eventの抑制。おそらくsubmitの処理をここで止めている。
        // register()にわたす引数(function)内で画面遷移を行っているから、submitによる画面遷移処理が不要なんじゃないかな?
        event.preventDefault();

        // パスワードとパスワード確認が同一だったら
        if (password === password2) {

            // register()メソッド(おそらくユーザ登録処理)に処理を渡す
            // 引数は
            //   email: 入力されたメールアドレス
            //   password: 入力されたパスワード
            //   onSuccess: 成功時に実行するfunction
            //   onFailure: 失敗時に実行するfunction
            register(email, password, onSuccess, onFailure);
        } else {

            // パスワード一致しなかったらアラート出して終わり(submitは止められているので元の画面のまま)
            alert('Passwords do not match');
        }
    }
  • 大まかに書くと、以下のような感じ
    • フォームの値を取得
    • ユーザ登録処理(register)の成功時に実行するfunctionを定義する
    • ユーザ登録処理(register)の失敗時に実行するfunctionを定義する
    • submitを止める
    • パスワード一致確認して、ユーザ登録処理を実行

register()メソッド

  • インラインでやってることを推測しながらコメントを追加
    /*
     * Cognito User Pool functions
     */

    // 引数4つ受け取る
    //   email: 入力されたメールアドレス
    //   password: 入力されたパスワード
    //   onSuccess: 成功時に実行するfunction
    //   onFailure: 失敗時に実行するfunction
    function register(email, password, onSuccess, onFailure) {

        // メールアドレスデータの連想配列
        var dataEmail = {
            Name: 'email',
            Value: email
        };

        // AmazonCognitoIdentityクラスにメールアドレスデータをセットしたオブジェクトを生成してるっぽい
        // @see https://github.com/aws-amplify/amplify-js/blob/bfa477662591bbd7a92bbb8486778924589e4adb/packages/amazon-cognito-identity-js/es/CognitoUserAttribute.js#L21
        var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);

        // userPoolオブジェクト(後述)のsignUpメソッドに以下の引数を渡している
        // メソッド自体はここか? @see https://github.com/aws-amplify/amplify-js/blob/bfa477662591bbd7a92bbb8486778924589e4adb/packages/amazon-cognito-identity-js/es/CognitoUserPool.js#L93
        //   {string} username: 入力されたメールアドレス(email)
        //   {string} password: 入力されたパスワード(password)
        //   {(AttributeArg[])=} userAttributes: 登録するユーザの属性情報配列([attributeEmail])
        //   {(AttributeArg[])=} validationData: バリデーションデータを入れるっぽいが、ここはnull
        //   {nodeCallback<SignUpResult>} callback: コールバック用の関数定義(function signUpCallback)
        userPool.signUp(email, password, [attributeEmail], null,
            function signUpCallback(err, result) {
                if (!err) {
                    // エラーじゃなかったら成功時に実行するfunctionを実行
                    onSuccess(result);
                } else {
                    // エラーだったら成功時に実行するfunctionを実行
                    onFailure(err);
                }
            }
        );
    }
  • 大まかに書くと、以下のような感じ
    • 引数からユーザ情報に登録するデータオブジェクトを作成
    • AmazonCognitoIdentityオブジェクトに登録データとコールバック用の関数を渡す
    • AmazonCognitoIdentityがsignUp処理を実行して、結果によってfunctionを実行
  • cognitoへの登録はAmazonCognitoIdentityがやってくれる模様

本日javascriptと和解できたところ

  • functionを()で囲むと即時関数となって、function定義と同時に実行される
  • bootstrapはCSSフレームワークで、jqueryが必要
  • submit()メソッドで、submitされた時に実行できるfunctionを定義できる
  • Callback関数の形式
    • 成功時/失敗時に実行するfunctionを変数に入れておいて引数で渡す(なるほどcallback)
  • Cognito-SDKのAmazonCognitoIdentityがいろいろやってくれる(がマニュアルが見つからない)

Elasticsearchを触りながら調べたことのメモ

概要

  • Amazon Elasticsearch Serviceを触って気になったところを調べながらoutputする
  • AWS関係なくElasticsearchの仕様に関することも気になったら調べる

Amazon Elasticsearch Service Domainって何?

Amazon Elasticsearch Service ドメインは、Amazon Elasticsearch Service コンソール、CLI、
または API を使用して作成する Elasticsearch クラスターです。

各ドメインは、指定した コンピューティング リソースおよびストレージリソースを備えたクラウド内の Elasticsearch クラスターとなります。

お客様はドメインの作成や削除、インフラストラクチャ属性の定義、アクセスやセキュリティの制御が可能です。
Amazon Elasticsearch Service ドメインは 1 つまたは複数実行できます。
  • つまりこれ
「Elasticsearch Service domain」は、Elasticsearchクラスターの実行に必要なすべてのリソースのコレクションです。

環境構築

  • terraformAmazon Elasticsearch Serviceを構築
  • Elasticsearch触るだけならローカルでコンテナ動かしてもよかったかもしれない
  • 上記つぶやいたら本業の人からコメントもらえた!
    • Javaが動く環境でtgz解凍→bin/elasticsearchで起動できる
    • 軽量で良いらしい・・・!

データ登録・検索

  • RESTで可能
  • 登録
    • /blogs/article/1
      • 「blogs」というインデックスに
      • 「article」というドキュメントを登録する
      • 「1」「2」「3」は、登録された個々のドキュメントのID
curl -H "Content-Type: application/json" -XPUT https://[es domain]/blogs/article/1 -d '
{
    "title": "Title First",
    "content": "I ate a ramen",
    "tags": ["dev", "calory", "Sugar"]
}
'

curl -H "Content-Type: application/json" -XPUT https://[es domain]/blogs/article/2 -d '
{
    "title": "Title Second",
    "content": "I brunk a beer",
    "tags": ["dev", "alcohol", "purintai"]
}
'

curl -H "Content-Type: application/json" -XPUT https://[es domain]/blogs/article/3 -d '
{
    "title": "Title Third",
    "content": "I go to bed",
    "tags": ["off", "sleep", "heeling"]
}
'
  • データ取得
    • 「content」フィールドが「beer」
$ curl -XGET https://[es domain]/_search?q=content:beer
{
    "title": "Title Second",
    "content": "I brunk a beer",
    "tags": ["dev", "alcohol", "purintai"]
}
}]}}
  • インデックス削除
    • 一発でINDEXごと消せるのちょっと怖い
$ curl -XDELETE https://[es domain]/blogs

mappingとは

  • RDBでいうテーブル定義
  • データの型によって自動でmappingしてくれる
  • 検索の要件によっては、自動でmappingしないで手動でやる
  • INDEXに既にデータが入っている場合、mappingの変更はできない
  • 変更したい場合はaliasを使って、既存のmappingから新しいmapping定義に移行させる方法を取る

  • mapping取得

curl -XGET https://[es domain]/blogs/_mapping
{
    "blogs": {
        "mappings": {
            "article": {
                "properties": {
                    "content": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "tags": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "title": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    }
                }
            }
        }
    }
}

indexオプション

  • 指定なしの場合は analyzed が適用(解析する)
  • not_analyzed の場合、解析しない

analyzerオプション

わかったこと

  • Amazon Elasticsearch Serviceの「domain」はElasticsearchクラスターの実行に必要なすべてのリソースのコレクション
  • 登録/検索がRESTで簡単にできるの素晴らしい
  • 自動でmappingしてくれる
  • フィールドの形態素解析とかやってくれるのとても便利
  • 以下2つから、マスターデータは別途持っておいたほうが良さそう
    • Elasticsearchのmapping変更はできない。aliasを使って新しいmappingに移行する
    • curl -XDELETE でINDEXごと消える。

あとがき

  • まだまだ奥が深そうだが、一旦概要はつかめたのでここまで
  • 次はAPI Gateway + LambdaからElasticsearch Serviceにアクセスしてみる
プライバシーポリシー