血と汗となみだを流す

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

API Gatewayをはじめよう③(リソースで設定する項目)

前回

この記事は何?

  • リソース(resource)を作成する時に指定する項目やパラメータなどについて調べました

「リソース」の設定項目ざっくりまとめ

  • リソースで定義したリソースPATHは、API Gatewayを呼ぶ時のendpointのPATHの一部として使われる
    • /searchとか
    • /detailとか
  • プロキシリソースとして設定すると、全てのPATHのリクエストを処理してくれる
    • /search とか /detail 個別に処理するのでなく/配下をまとめて処理してくれる
    • backend側で上記のPATH情報が取得できる
  • CORS設定をしてクロスドメイン通信を可能にできる

リソース設定内の項目について

プロキシリソースとして設定

greedyパスパラメータegを使用して全てのサブリソースへのリクエストを処理します
{proxy+}. プロキシリソースを作成すると、ANYと呼ばれる特別なHTTPメソッドも作成されます。
ANYメソッドは全ての有効なHTTP動詞をサポートし、1つのHTTPエンドポイントまたはLambda統合にリクエストを転送します

greedyパス

  • 一般的なPATHに分類されるrequestのグループのPATHと動作を個別に指定する代わりに、PATHへの全てのrequestを傍受してそれらを同じ機能にルーティングする

リソース名

  • リソースの名称
  • どこで使われてるかわからん
  • 入力時にリソースパスと連動しているように見えるけど、別のものを入力できた
  • 保存後、確認できる場所なし(リソースパスは見えるが・・・)

リソースパス

波括弧を使用してパスパラメータを追加できます。
たとえば、リソースパス {username} は、"username" という名前のパスパラメータを表します。 
プロキシリソースとして /{proxy+} を設定すると、そのサブリソースへのすべてのリクエストがキャッチされます。
たとえば、/foo への GET リクエストがこの対象となります。
/ へのリクエストを処理するには、/ リソースで新しい ANY メソッドを追加します。
  • endpontのPathとなる文字列
  • {}で囲むとパスパラメータとなり、backend側(Lambda)で指定した文字列を取得できる

リソースパスをlambdaで取得してみる

  • プロキシリソースでリソースパスを{/proxytest+}とし、 /testPathへアクセスしてみる f:id:Anorlondo448:20181013190825p:plain
  • Lambda側は受け取ったeventを出力するようにしておく
console.log('Loading event')

exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify(event)
    };
    return response;
};
  • 結果(一部抜粋)
{
  "resource": "/{proxytest+}",
  "path": "/testPath",
  "httpMethod": "GET",
  "headers": null,
  "multiValueHeaders": null,
  "queryStringParameters": null,
  "multiValueQueryStringParameters": null,
  "pathParameters": {
    "proxytest": "testPath"
  },
  • 任意のPATHでLambdaまで処理が渡ってた!
  • 階層を深くして、/testPath/param1にアクセスしてみる f:id:Anorlondo448:20181013194044p:plain
"pathParameters": {
    "proxytest": "testPath/param1"
  },
  • 取れてた!PATHごとに個別に設定したくないときはまとめられて便利!

CORSを有効

API Gatewayはプリフライトリクエストに応答し、小規模なパフォーマンスの向上が得られます。
この選択では基本的なCORS設定でOPTIONSメソッドを設定し、全てのオリジン、全てのメソッド、および複数の共通ヘッダーを許可します。
この設定を更に制御する場合は、リソースの作成後に[アクション]ボタンの[CORSの有効化]を選択できます

プリフライトリクエス

  • サーバから応答するメソッド一覧を収集する
  • リクエストを投げる前に、そのリクエストが受け入れられるか事前にチェックする

次回

  • メソッド(Method)について

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);
        };
    }

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

今回の収穫