シャッフル主任の進捗報告

興味のあるものを作ります。進捗を不定期にご報告します。

そうだ、教祖になろう。出エジプト記 第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化する

「入り口がひとつ 出口はふたつ これなぁんだ?」


昔、「上は大水 下は大火事 なぁんだ?」ってなぞなぞがあって
「湯舟の下にかまどがある風呂なんか今どき家庭にある?」
と違和感を持った子どものまま大人になったもの、なぁんだ?

教祖です。

いきなりですが、お詫びです。
前回の第1章2節の訂正 今度こそCloudFrontでサイトをHTTPS化するで、

次こそはAPI GatewayをCORS化していきたいと思います。

と言ったな、あれは嘘だ。

というか、勘違いがありました。
何が嘘だったのか、順番に説明します。

ブラウザがS3から取得したJavaScriptAPI Gatewayにアクセスしようとすると、CloudFrontとは異なるドメインと通信することになります。

通常、ブラウザはセキュリティ上の理由でJavaScriptによる別ドメインへのアクセスを禁止しています。
JavaScriptが自由に別ドメインへアクセスできてしまうと、悪意のあるサイトからユーザの知らないうちに別ドメインにアクセスされてしまうおそれがあるからです。

f:id:chief-shuffle:20191201235341p:plain

 

「ほんとにそんなことしてるのー?」

 

 

今何が嘘だったのか説明してるからちょっと待っ

 

「そんなんしなくてもアクセスできそうじゃなーい?」

 

 

わかりました。確認してみましょう。

API GatewayjQueryでアクセスする簡単なJavaScriptとHTMLをS3のバケットに配置してみます。

ソースはこのような感じです。
「url」にリクエストを送るJavaScriptです。

$(function(){
  $('button').click(function() {
    console.log("sending request...");
    $.ajax({
      type: "GET",
      url: $("#url").val()
    }).done(function(data, status, jqXHR){
      console.log("received response!");
      $("#crossDomain").val($(this)[0].crossDomain);
      $("#status").val(jqXHR.status);
      $("#response").html(data);
    }).fail(function(jqXHR, status, errorThrown){
      console.log("received error!");
      $("#crossDomain").val($(this)[0].crossDomain);
      $("#status").val(jqXHR.status);
      $("#response").html(jqXHR.responseText);
    });
  });
});

これを読み込むHTMLです。

<title>Cross-Origin test</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript" src="./js/cross.js"></script>

<div>
  url <input type="text" id="url" size="80" value="/index.html" />
</div>
<div>
  crossDomain <input type="text" id="crossDomain" />
</div>
<div>
  status <input type="text" id="status" />
</div>
<div>
  response <textarea id="response"></textarea>
</div>

<div>
  <button>Send</button>
</div>

これらのファイルをS3にアップします。

f:id:chief-shuffle:20191128071107j:plain

f:id:chief-shuffle:20191128071117j:plain

まず、同一ドメインのコンテンツにアクセスしてみます。
アップしたHTMLをCloudFront経由で表示して「Send」をクリック。
「status」に200(正常)、「response」に/index.htmlの取得結果が挿入されます。

f:id:chief-shuffle:20191128071616j:plain

次にAPI Gatewayの動的コンテンツ、つまり別ドメインにアクセスしてみます。

アクセスするのは第2章1節 LambdaとAPI Gatewayで動的コンテンツを生成するで作成したAPI Gatewayのリソースです。
直接アクセスした結果はこんな感じです。
固定文言と現在時刻を返すだけの単純なLambda関数を呼び出しています。

f:id:chief-shuffle:20191130144312j:plain

先ほどのHTMLを再度読み込み、「url」をAPI Gatewayのリソースにします。
「status」に0、「response」には何も表示されていません。
どうやらリクエストが送信される前にエラーになったようですね。
また、リクエストの属性である「crossDomain」が「true」になっています。

f:id:chief-shuffle:20191130150022j:plain

Chromeデベロッパーツールも見てみます。 「Console」タブにこのように出力されています。

f:id:chief-shuffle:20191130150301j:plain

Access to XMLHttpRequest at 'https://AAAA.execute-api.ap-northeast-1.amazonaws.com/default/rebirth' from origin 'https://CCCC.cloudfront.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

リクエストがCORSポリシーに触れやがったからブロックしてやったぜい!
くやしかったら'Access-Control-Allow-Origin'ヘッダをリクエストにつけやがれい!
べらんめい!
と言われました。
ほらね。

 

「わかったよ。しょぼん。」

 

「ズボン!」「ブー。正解は…」


CORS(オリジン間リソース共有)は、ブラウザが制限している別ドメインへのアクセス(クロスオリジン)を可能にする構成で、この問題の解決策の一つです。
クロスオリジン設定を行い、別ドメイン間でのJavaScript通信を許可します。
「CORS化」だとこっちのことになっちゃいます。

f:id:chief-shuffle:20191201235341p:plain

今回やるのはCORS化ではありません。
なので、前回「CORS化します」と言ったのは嘘でした。すみません。

今回やるのは、S3とAPI GatewayをCloudFrontで統合して同一ドメインのコンテンツにする、です。
構成はこのようになります。

f:id:chief-shuffle:20191201235408p:plain

今回参考にさせていただいた記事です。勉強になりました。ありがとうございます。

dev.classmethod.jp

ユニクロヒートテックウルトラストレッチプリントレギンスパンツでしたー。」「そっちかー。」

では、CloudFrontとAPI Gatewayを連携させていきます。

CloudFrontで前回作成したDistributionを選択して「Distribution Settings」をクリック。

f:id:chief-shuffle:20191201223224j:plain

「Origins and Origin Groups」タブを選択して「Create Origin」をクリック。

f:id:chief-shuffle:20191201223257j:plain

「Origin Domain Name」にAPI GatewayのURLを貼り付けます。

f:id:chief-shuffle:20191201223307j:plain

「Origin Path」と「Origin ID」が勝手に入ります。
「Origin Protocol Policy」で「HTTPS Only」を選択。
「Create」をクリック。

f:id:chief-shuffle:20191201223318j:plain

次に「Behaviors」タブを選択して「Create Behaviors」をクリック。

f:id:chief-shuffle:20191201223353j:plain

「Path Pattern」に/api/*を入力、「Origin or Origin Group」で先ほど登録したOriginを選択。
Viewer Protocol Policyは「Redirect HTTP to HTTPS」、「Object Caching」で「Customize」を選択。
キャッシュは動作確認に邪魔なので、「Minimum TTL」、「Maximum TTL」、「Default TTL」を0にします。
「Create」をクリック。

f:id:chief-shuffle:20191201223343j:plain

デプロイが開始されます。
10分ほど待ちます。

f:id:chief-shuffle:20191201223401j:plain

「Deployed」と表示されたので、CloudFrontの「Domain Name」/API Gatewayのリソース名にアクセスしてみます。

f:id:chief-shuffle:20191201224209j:plain

ん? HTTPコード403が返ってきてしまいました。
API Gatewayで存在しないパスにアクセスした時と同じレスポンスですね。
なぜでしょうか。

これはまりました。 色々試して3日がかりでこちらの記事を発見しました。

qiita.com

こちらではCloud FrontからS3へ連携しようとされているのですが、

path pattern でディレクトリを指定する場合、S3 上のパスと協調させておく必要があります。

ということです。

CloudFront側でPath patternを/api/*と設定したら、連携先のAPI Gatewayのリソースも
https://~/api/rebirth
となるようにしておかないといけないみたいです。
今のAPI Gatewayのリソースは
https://~/rebirth
こうなっちゃってるので、存在しない/api付きのリソースを要求されて403を返しています。
これは考えてみたら当たり前で、例えばS3向けのPath patternを/*.pngと設定したら、S3も
https://~/any.png
であるはずです。

ということで、API Gatewayの方をいじります。
Lambdaからデフォルトで作ったAPI Gatewayのリソースのパスを作り直します。

API Gatewayのリソースを選択して、「アクション」から「リソースの作成」を選択。

f:id:chief-shuffle:20191201231911j:plain

「リソース名」にapiと入力して、「リソースの作成」をクリック。

f:id:chief-shuffle:20191201231922j:plain

さらに作成された/apiリソースを選択して、「アクション」から「リソースの作成」を選択。

f:id:chief-shuffle:20191201232013j:plain

「リソース名」にLambda関数名を入力して、「リソースの作成」をクリック。

f:id:chief-shuffle:20191201232030j:plain

さらに作成されたリソースを選択して、「アクション」から「メソッドの作成」を選択。

f:id:chief-shuffle:20191201232042j:plain

「ANY」を選択して、チェックマークをクリックします。

f:id:chief-shuffle:20191201232055j:plain

「統合タイプ」に「Lambda関数」を選択、「Lambdaプロキシ統合の使用」をチェック。
「Lambda関数」にLambda関数名を入力して、「保存」をクリック。
権限追加のダイアログが出るので、「OK」をクリック。

f:id:chief-shuffle:20191201232110j:plain

f:id:chief-shuffle:20191201232120j:plain

メソッドが作成されます。一応、テストしておきましょう。雷マークの上のテストをクリック。

f:id:chief-shuffle:20191201232135j:plain

メソッドに「GET」を選択して、「テスト」をクリック。

f:id:chief-shuffle:20191201232150j:plain

ステータス200を確認します。

f:id:chief-shuffle:20191201232201j:plain

もともとのメソッドは削除しておきましょう。
リソースを選択して、「アクション」から「リソースの削除」を選択。

f:id:chief-shuffle:20191201232218j:plain

「削除」。

f:id:chief-shuffle:20191201232232j:plain

現在デプロイ済みのステージも削除しておきましょう。
左の「ステージ」を選択。ステージ名を選択して、「ステージの削除」をクリック。

f:id:chief-shuffle:20191201232250j:plain

「削除」。

f:id:chief-shuffle:20191201232259j:plain

これだけだと、削除されたAPIからのトリガがLambda関数に残ってしまうので、Lambda関数を開きます。
API Gatewayを選択すると、削除したリソースのとこが「そんなリソースがあってたまるけぇ!」てな具合に怒られてますので、「削除」をクリック。

f:id:chief-shuffle:20191201233700j:plain

続いて「保存」をクリックして削除を確定します。

f:id:chief-shuffle:20191201233711j:plain

API Gatewayに戻り、改めてデプロイします。
左の「リソース」を選択して、先ほど作成したリソース名を選択し、「アクション」から「APIのデプロイ」を選択。

f:id:chief-shuffle:20191201232853j:plain

[新しいステージ]を選択して、ステージ名にさきほど削除したのと同じものを入力。
「デプロイ」をクリック。

f:id:chief-shuffle:20191201232536j:plain

「URLの呼び出し」に表示されているURL+/+作成したリソース名のパスにアクセス。
Lambdaの結果が表示されることを確認します。

f:id:chief-shuffle:20191201232556j:plain

では、さきほどのCloudFront経由でもう一度アクセスしてみましょう。 こちらでもLambdaの結果が表示されました。

f:id:chief-shuffle:20191201232609j:plain

それでは!
やっとここからが本当にやりたかったことです!

はじめに江戸っ子口調で怒られたJavaScriptからのアクセスを、今度は同一ドメインでやってみます。

アクセス確認用のHTMLを表示します。
「url」に先ほど作り直したリソースの相対パスを入力。

f:id:chief-shuffle:20191201232713j:plain

「Send」をクリックします。

f:id:chief-shuffle:20191201232732j:plain

おー!ついにS3に配置したJavaScriptによる、API Gatewayへのアクセスが実現できました。
「crossDomain」もfalseと表示されており、S3とAPI GatewayがCloudFrontにより統合されて同一ドメインと認識されています。
これで一応、SPAを実現する土台は整ったことになります。

今回も長かったです。
ご覧いただいたみなさん、ありがとうございました。

本当はAPI Gatewayの直アクセスを禁止したいのですが、だいぶ疲れたので今すぐ安息日を取ることにします。