そうだ、教祖になろう。出エジプト記 第3章2節 LambdaからGoogleスプレッドシートにアクセスする
門の神「ズール」
前回の第3章1節 Cloud9でLambdaの開発環境を構築するではCloud9で開発環境をセットアップしました。 これでサーバサイドを実装できるようになったわけですが、いきなり全部作ると長いので少しずついきましょう。
まずはこのアプリケーションの外部記憶であるところのGoogleスプレッドシートへのアクセスを試してみます。 こういう構成です。
GoogleスプレッドシートへのアクセスはPythonのgspreadというパッケージがやってくれます。
まずは、Googleスプレッドシート側でアクセスに必要な認証情報を作成します。
今回、こちらのサイトを参考にさせていただきました。ありがとうございます。
Google API Consoleにログイン。
割り当てプロジェクトを作成します。
「+プロジェクトを作成」をクリック。
「プロジェクト名」を入力。「作成」をクリック。
左上のメニュー「APIとサービス」から「ライブラリ」を選択。
「Google Drive API」を検索して「有効にする」。
さらに「Google Sheets API」を選択して「有効にする」。
認証情報を作成します。
実体は認証情報が書き込まれたJSONファイルです。
あとでサーバサイド処理で読み込んでGoogleスプレッドシートへの認証を行います。
「認証情報を作成」をクリック。
「使用するAPI」に「Google Sheets API」、「APIを呼び出す場所」に「ウェブサーバー」、「アクセスするデータの種類」に「アプリケーションデータ」、「いいえ、使用していません」を選択して「必要な認証情報」をクリック。
「サービスアカウント名」にわかりやすい名前を入力、「役割」を「Project」から「編集者」を選択、「キーのタイプ」に「JSON」を選択して「次へ」をクリック。
これでサービスアカウントキーのJSONファイルがダウンロードされました。
アクセスするGoogleスプレッドシートを共有設定します。
アクセスしたいGoogleスプレッドシートを開き、「共有」をクリック。
「他のユーザーとの共有」ダイアログの「ユーザー」欄にダウンロードしたJSONファイルの「client_email」のメールアドレスを入力します。
このメールアドレスは実在はせず、スプレッドシートを共有した相手を識別するためのIDのようです。
鍵の神「ビンツ」
さて、認証情報をサーバサイド処理に取り込みます。
ダウンロードしたJSONファイルをCloud9にアップロードします。
画面左部にドラッグ&ドロップ。
先ほどの参考サイトをもとにソースを変えてみます。
JSONファイルを使って認証したあとに特定の名前のスプレッドシートの1シート目のシート名を表示するプログラムです。
import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../site-packages')) import datetime import json import gspread from oauth2client.service_account import ServiceAccountCredentials def lambda_handler(event, context): scope = ['https://www.googleapis.com/auth/drive'] credentials = ServiceAccountCredentials.from_json_keyfile_name('./Shuffle Playism Project-e8939d859c3b.json', scope) gclient = gspread.authorize(credentials) spreadsheet = gclient.open('シャッフル再生教') worksheet = spreadsheet.sheet1 return { 'statusCode': 200, 'body': json.dumps('WorkSheet name is ' + worksheet.title + ' at ' + str(datetime.datetime.now())) }
インポートしているgspreadはGoogleスプレッドシートAPI、oauth2clientはOAuth認証のパッケージです。
これらパッケージをPythonにインストールしておかないとUnable to import
エラーが出ます。
ローカルPCならOSのパスが通っているpip
コマンドを打てばいいのですが、Lambda関数だとウィザードが作成したvenvフォルダ配下にあるpip
コマンドでインストールする必要があるようです。
※嘘でした。普通のpip3.6で問題ありません。コマンドを修正しましたが、キャプチャはそのままです。(2019/12/14)
Lambda関数フォルダの並びに「site-packages」フォルダを作ってそこにインストールしてみます。
cd server pip3.6 install gspread oauth2client -t site-packages
インストールしたパッケージを読み込むためにソースの先頭で「site-packages」フォルダにパスを通します。
import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../site-packages'))
「Lambda(local)」で実行してみます。
「API Gateway(local)」よりエラーの詳細が分かって便利です。
んーFileNotFoundError
ですね。ファイルが読み込めていないようです。
どうもLambdaは.py
拡張子以外のファイルを自由に扱えないようです。
セキュリティ上、JSONファイルをS3やSecrets Managerに配備しておいてLambda実行時に取得するのがセオリーなのかもしれません。
破壊の神「ゴーザ」
「セオリーなど打ち壊してくれる!.py
ファイルにしてしまえばよかろう!」
…
JSONファイルをsettings.py
というファイル名に変更し、内容の先頭に
ACCOUNT_KEY =
と付与しました。これでPythonのディクショナリ型宣言になります。
これをlambda_function.py
でインポートします。
sys.path.append(os.path.join(os.path.dirname(__file__), '.')) import settings
また、認証情報作成部分のメソッド名をfrom_json_keyfile_name
からfrom_json_keyfile_dict
に、第1引数をファイルパスからsettings.py
で定義したキー定数に変更します。
credentials = ServiceAccountCredentials.from_json_keyfile_dict(settings.ACCOUNT_KEY, scope)
「Run」。
今度はちゃんとワークシート名「lifes」が取れました。
認証情報をソースとして扱っているのが不安ですが…
「よかろうなのだ!」
…では、セキュリティ情報はまとめてS3かSecrets Managerで管理する、をバックログに積んでおきましょう。
「誰かにお前は神かって聞かれたら、イエスって答えるもんだぜ」
快適コーディングライフのためにもうひと工夫してみましょう。
Cloud9ではデフォルトでPythonの自動フォーマットが利きません。
ので、自動フォーマッタパッケージ「yapf」を入れます。
こちらを参考にさせていただだきました。ありがとうございます。
またpipでインストールしていくわけですが、フォーマッタはLambda関数とか関係なくCloud9のインスタンス上で操作するので、通常のpipでインストールします。
画面下部のターミナルウィンドウで以下を実行。
まずpipをアップグレードしてからyapfをインストールして、試しにフォーマットしてみています。
cd (Lambdaアプリケーション名) sudo python3.6 -m pip install --upgrade pip sudo python3.6 -m pip install yapf yapf -i (Lambda関数名)/lambda_function.py
お、きれいに改行が入っています。
保存時に自動でフォーマットする設定は「AWS Cloud9」から「Preferences」を選択。
「Python Support」の「Format Code on Save」をON、「Custom Code Formatter」を「yapf -i "$file"」と入力します。
これでソースを保存するたびに
こうじゃ!
あー気持ちいい。
次回はお待ちかね、サーバサイド処理を本格的に実装していきます。
そうだ、教祖になろう。出エジプト記 第3章1節 Cloud9でLambdaの開発環境を構築する
ソースはクラウドにいまし、
Route53以外のAWSサービスが繋がったので、アプリケーションを開発していきたいのですが、まずは開発環境が必要です。
PythonだとPCでPyCharmでやってもいいのですが、クライアントサイドのVue.jsもあるし、せっかくAWSなのでCloud9を使ってみます。
Cloud9はクラウドで開発ができちゃうというサービスです。
サーバレスではなく、サーバインスタンスであるEC2を立てます。起動時間で利用料が発生しますが、利用しないときは自動で停止され、最低スペックなら月額2ドルほどで済むようです。
では早速、始めてみましょう。
「Name」で開発環境の名前をつけます。
利用できるインスタンスはないので新規に「EC2」を作ります。
一番スペックの低い「t2.micro」を選択、OSは「Amazon Linux」を選択します。
自動で停止する設定はデフォルトの30分にしておきます。
すぐに開発環境の作成が始まります。
1分ほどで作成されました。 早いですね。
では、Lambda関数を作ってみましょう。
Lambda関数のベースを整えてくれるウィザード機能を使います。
このウィザードを使うと色々なリソースがプロビジョニングされます。
まだよくわからないところもあるので、適宜触れていきます。
右端の「AWS Resources」をクリック。
表示される「λ+」マークをクリック。
「Function Name」にLambda関数名、「Application Name」にLambdaアプリケーション名を入力します。
AWS Lambda アプリケーションは、 Lambda 関数、イベントソース、およびその他のリソースを組み合わせたもので、協調して動作することによってタスクを実行します。
ということなんで単にLambda関数を作るより余計なものまでできそうなのですが、ここは大いなるCloud9の意思にしたがっておきます。
ランタイムは「Python 3.6」が最新のようです。
テンプレートは「empty-python」を選びます。
あとでCloudFrontと連携するので、「Function trigger」に「API Gateway」を選択、「Resource Path」に第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化するで作ったAPIと同じパスを入力。
第2章2節の続き CloudFront以外からAPI Gatewayへのアクセスを閉じると同じくAPIキー必須にしたいので「Security」を「NONE_KEY」にします。
メモリとロールはデフォルトにしておきます。
「Finish」で作成終了です。
数秒で作成が終わります。楽。
左側のフォルダ構造は
ルート/Lambdaアプリケーション名/Lambda関数名
になります。
Cloud9は、クラウドリソースをテンプレートによりプロビジョニングするCloudFormationを使って色々なリソースをデプロイします。
CloudFormationを見てみると新しいスタックが存在しています。
このCloud Formationにより色々なリソースが作成されています。
IAMのロール。
Lambda関数。
同じくLambdaアプリケーションもありました。
API Gatewayもできています。
エンドポイントタイプが「Regional」でなく「Edge」になっていますが、本家サイトだとエッジ最適化は
CloudFront ディストリビューション経由でアクセスするエンドポイントです。
ともあるので、まあいいでしょう。
作成されたAPIを表示し、「メソッドリクエスト」をクリックすると、
「APIキーの必要性」が「false」になっています。
さきほど「NONE_KEY」を選んだのですが、ここに反映されるわけではないのでしょうか。
よくわかりません。
「ステージ」を見ると、すでに「Prod」と「Stage」という2つのステージがデプロイされています。
ウィザードでできたものが多すぎて追いきれていない気がします。
CloudFormationのテンプレートを読めば全容が分かるんでしょう。
世はすべて事もなし
なにはともあれ、動かしてみましょう。
動くは正義です。
「Stage」のURL+/
+リソース名にアクセスしますが、いきなりHTTPステータス502が返ってきました。
動かないじゃん。
これは、ウィザードで作ったlambda_function.py
の戻り値がHTTPレスポンス形式になってないからです。
ここまで色々作ってくれて、そこはやらないんかい!と思いますが、
lambda_function.py
を第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化すると同じにします。
レスポンスのbody
はちょっと変えてみます。
import datetime import json def lambda_handler(event, context): # TODO implement return { 'statusCode': 200, 'body': json.dumps('The sources\'re in the cloud, all\'s right with the world! at ' + str(datetime.datetime.now())) }
Ctrl+Sで保存して動きを確認します。
「Run」をクリックして、「API Gateway(local)」を選択します。
実行すると「Status 200 OK」が返ってきました。
ちなみに「API Gateway(remote)」を選ぶと表示されているソースではなく、Lambdaにデプロイされているソースが実行されるようです。
修正したソースをLambdaにデプロイしましょう。 右側のLambda関数名を選択して「↑」ボタンをクリックするだけです。これはいい。
APIキー必須にしたいので、API Gatewayの「使用量プラン」の「APIステージの追加」をクリック。
先ほどCloud9が作ってくれた「Stage」ステージを追加します。
「リソース」の方で「APIキーの必要性」を「true」に変えます。
「Stage」を再度デプロイします。
リソースを選んで、「APIのデプロイ」をクリック。
「Stage」を選択して「デプロイ」。
API Gatewayに直アクセスしてみると、無事「Fobidden」になってAPIキーなしのアクセスが拒否されているのが分かります。
ちなみにCloud9ではAPIキー不足によるエラーにはなりません。Cloud9はあくまでLambdaのテストのようです。
それでは、CloudFront経由でアクセスできるように設定していきます。
第2章2節の続き CloudFront以外からAPI Gatewayへのアクセスを閉じるで修正したDistributionを選択。
「Create Origin」をクリック。
「Origin Domain Name」に「Stage」のURLを貼り付けます。
「Origin Protocol Policy」は「HTTPS Only」、「Origin Custom Headers」にAPI GatewayのAPIキーを登録します。
次に「Behaviors」で/api/*
を選択して、「Edit」をクリック。
「Origin or Origin Group」をさきほど作ったOriginに変更します。
デプロイを待ってアクセスを確認すると、
新しく作ったLambda関数の結果に差し替えられました。
これでサーバサイドの開発環境が整いました。
次回はいよいよGoogleスプレッドシートにアクセスしていきます。
なお、EC2の一覧でCloud9用のインスタンスを確認できます。 Cloud9をしばらく放っておくと、ちゃんと「Stopped」になるみたいですね。
そうだ、教祖になろう。出エジプト記 第2章2節の続き CloudFront以外からAPI Gatewayへのアクセスを閉じる
正面玄関におまわりください。
どうも「続き」とか「訂正」が多いですね。
前回の第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化するではCloudFrontとAPI Gatewayを連携させました。
S3のときと同じく、CloudFrontの裏側にあるAPI Gatewayへの直接アクセスを防ぎたいと思います…
が、何がベストプラクティスなのかなかなかわからず、困りました。
思えば、毎回困ってばかりの気がします。
乳と蜜の流れる地はまだまだ遠いようです。
教祖です。
S3との連携のときは、CloudFrontがOAI(Origin Access Identity)を作ってくれて、S3のバケットポリシーまで書き換えてくれました。
しかし、いたれりつくせりはOriginがS3バケットのときだけで、API Gatewayはカスタムオリジンのため、対象外のようです。
ちぇっ。
こちらによると
API Gatewayには、APIキー認証以外にアクセス制限の機能がありません。(中略) また、API Gatewayへの直アクセスを禁止したい場合は、前述のAPIキー認証を有効化し、先日追加されたオリジンへのヘッダ追加機能でCloudFrontのオリジン設定にx-api-keyヘッダを追加、設定することで対応できます。
ということです。
APIキーは、簡単に言うとリクエストヘッダに埋め込んだ文字列が合っていればOKという仕組みです。
AWS本家だと、APIキーはあくまで利用量の管理用だから認証に利用するなよー!Cognito(認証サービス)使えよー!という記述が多くみられるので避けていたのですが、サービス間連携の認証であれば使ってもいいようです。
勝手口は閉鎖いたしました。
「リソース」でAPI Gatewayリソース下の「ANY」を選択して「メソッドリクエスト」をクリック。
「APIキーの必要性」を「true」に変更。
ステージをデプロイしなおします。
「ステージ」の「ステージの削除」をクリック。
「リソース」の「アクション」から「APIのデプロイ」をクリック。
APIキーを作ります。
API Gatewayで「APIキー」を選択、「アクション」から「APIキーの作成」をクリック。
適当な名前を入力しして、「保存」をクリック。
次に使用量プランを作ります。APIキーだけでいいかとおもったら使用量プランに紐づけないと動作しないようです。
「使用量プラン」を選択して、「作成」をクリック。
「名前」を適当に入力、「スロットリング」の「レート」、「バースト」、「クォータ」を適当に入力します。
期せずして予算管理っぽくなっていますね。
必要に応じてあとで変えましょう。
「APIステージの追加」をクリックしてデプロイしたステージを紐づけます。
「API」と「ステージ」を選択。
「次へ」。
APIキーを紐づけます。
「APIキーを使用量プランに追加」をクリック。
APIキーを登録。
「完了」。
これでAPIキーがないとアクセスできない状態になりましたので、一度直アクセスしてみます。
API GatewayのURL+/
+リソース名にアクセスします。
HTTPステータスコード403、ボディとして{"message":"Forbidden"}
が返されました。
よしよし。
次にCloudFront側です。
先にCloudFrontからのリクエストに埋め込むAPIキーを確認しておきます。
API Gatewayの「APIキー」の「表示」をクリック。
Distributionを選択して「Distribution Settings」をクリック。
「Origins and Origin Groups」タブのAPI Gatewayを選択して、「Edit」をクリック。
「Origin Custom Headers」の「Header Name」にx-api-key
、「Value」にAPI Gatewayで作成したAPIキーを入力。
「Yes, Edit」をクリックしてデプロイを待ちます。
ではアクセスしてみましょう。
CloudFrontのドメイン名+/
+リソース名にアクセスすると、
お!Lambdaの結果が表示されました。
ちょっとしたことかもしれませんが、やっぱり動くと嬉しいですね。
そうだ、教祖になろう。出エジプト記 第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化する
「入り口がひとつ 出口はふたつ これなぁんだ?」
昔、「上は大水 下は大火事 なぁんだ?」ってなぞなぞがあって
「湯舟の下にかまどがある風呂なんか今どき家庭にある?」
と違和感を持った子どものまま大人になったもの、なぁんだ?
教祖です。
いきなりですが、お詫びです。
前回の第1章2節の訂正 今度こそCloudFrontでサイトをHTTPS化するで、
と言ったな、あれは嘘だ。
というか、勘違いがありました。
何が嘘だったのか、順番に説明します。
ブラウザがS3から取得したJavaScriptでAPI Gatewayにアクセスしようとすると、CloudFrontとは異なるドメインと通信することになります。
通常、ブラウザはセキュリティ上の理由でJavaScriptによる別ドメインへのアクセスを禁止しています。
JavaScriptが自由に別ドメインへアクセスできてしまうと、悪意のあるサイトからユーザの知らないうちに別ドメインにアクセスされてしまうおそれがあるからです。
「ほんとにそんなことしてるのー?」
…
今何が嘘だったのか説明してるからちょっと待っ
「そんなんしなくてもアクセスできそうじゃなーい?」
…
わかりました。確認してみましょう。
API GatewayにjQueryでアクセスする簡単な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にアップします。
まず、同一ドメインのコンテンツにアクセスしてみます。
アップしたHTMLをCloudFront経由で表示して「Send」をクリック。
「status」に200(正常)、「response」に/index.html
の取得結果が挿入されます。
次にAPI Gatewayの動的コンテンツ、つまり別ドメインにアクセスしてみます。
アクセスするのは第2章1節 LambdaとAPI Gatewayで動的コンテンツを生成するで作成したAPI Gatewayのリソースです。
直接アクセスした結果はこんな感じです。
固定文言と現在時刻を返すだけの単純なLambda関数を呼び出しています。
先ほどのHTMLを再度読み込み、「url」をAPI Gatewayのリソースにします。
「status」に0、「response」には何も表示されていません。
どうやらリクエストが送信される前にエラーになったようですね。
また、リクエストの属性である「crossDomain」が「true」になっています。
Chromeのデベロッパーツールも見てみます。 「Console」タブにこのように出力されています。
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化」だとこっちのことになっちゃいます。
今回やるのはCORS化ではありません。
なので、前回「CORS化します」と言ったのは嘘でした。すみません。
今回やるのは、S3とAPI GatewayをCloudFrontで統合して同一ドメインのコンテンツにする、です。
構成はこのようになります。
今回参考にさせていただいた記事です。勉強になりました。ありがとうございます。
「ユニクロのヒートテックウルトラストレッチプリントレギンスパンツでしたー。」「そっちかー。」
では、CloudFrontとAPI Gatewayを連携させていきます。
CloudFrontで前回作成したDistributionを選択して「Distribution Settings」をクリック。
「Origins and Origin Groups」タブを選択して「Create Origin」をクリック。
「Origin Domain Name」にAPI GatewayのURLを貼り付けます。
「Origin Path」と「Origin ID」が勝手に入ります。
「Origin Protocol Policy」で「HTTPS Only」を選択。
「Create」をクリック。
次に「Behaviors」タブを選択して「Create Behaviors」をクリック。
「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」をクリック。
デプロイが開始されます。
10分ほど待ちます。
「Deployed」と表示されたので、CloudFrontの「Domain Name」/API Gatewayのリソース名にアクセスしてみます。
ん?
HTTPコード403が返ってきてしまいました。
API Gatewayで存在しないパスにアクセスした時と同じレスポンスですね。
なぜでしょうか。
これはまりました。 色々試して3日がかりでこちらの記事を発見しました。
こちらでは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のリソースを選択して、「アクション」から「リソースの作成」を選択。
「リソース名」にapi
と入力して、「リソースの作成」をクリック。
さらに作成された/api
リソースを選択して、「アクション」から「リソースの作成」を選択。
「リソース名」にLambda関数名を入力して、「リソースの作成」をクリック。
さらに作成されたリソースを選択して、「アクション」から「メソッドの作成」を選択。
「ANY」を選択して、チェックマークをクリックします。
「統合タイプ」に「Lambda関数」を選択、「Lambdaプロキシ統合の使用」をチェック。
「Lambda関数」にLambda関数名を入力して、「保存」をクリック。
権限追加のダイアログが出るので、「OK」をクリック。
メソッドが作成されます。一応、テストしておきましょう。雷マークの上のテストをクリック。
メソッドに「GET」を選択して、「テスト」をクリック。
ステータス200を確認します。
もともとのメソッドは削除しておきましょう。
リソースを選択して、「アクション」から「リソースの削除」を選択。
「削除」。
現在デプロイ済みのステージも削除しておきましょう。
左の「ステージ」を選択。ステージ名を選択して、「ステージの削除」をクリック。
「削除」。
これだけだと、削除されたAPIからのトリガがLambda関数に残ってしまうので、Lambda関数を開きます。
API Gatewayを選択すると、削除したリソースのとこが「そんなリソースがあってたまるけぇ!」てな具合に怒られてますので、「削除」をクリック。
続いて「保存」をクリックして削除を確定します。
API Gatewayに戻り、改めてデプロイします。
左の「リソース」を選択して、先ほど作成したリソース名を選択し、「アクション」から「APIのデプロイ」を選択。
[新しいステージ]を選択して、ステージ名にさきほど削除したのと同じものを入力。
「デプロイ」をクリック。
「URLの呼び出し」に表示されているURL+/
+作成したリソース名のパスにアクセス。
Lambdaの結果が表示されることを確認します。
では、さきほどのCloudFront経由でもう一度アクセスしてみましょう。 こちらでもLambdaの結果が表示されました。
それでは!
やっとここからが本当にやりたかったことです!
はじめに江戸っ子口調で怒られたJavaScriptからのアクセスを、今度は同一ドメインでやってみます。
アクセス確認用のHTMLを表示します。
「url」に先ほど作り直したリソースの相対パスを入力。
「Send」をクリックします。
おー!ついにS3に配置したJavaScriptによる、API Gatewayへのアクセスが実現できました。
「crossDomain」もfalse
と表示されており、S3とAPI GatewayがCloudFrontにより統合されて同一ドメインと認識されています。
これで一応、SPAを実現する土台は整ったことになります。
今回も長かったです。
ご覧いただいたみなさん、ありがとうございました。
そうだ、教祖になろう。出エジプト記 第1章2節の訂正 今度こそCloudFrontでサイトをHTTPS化する
王様は裸だ!
どうやら第1章2節 CloudFrontでサイトをHTTPS化するで設定した内容に誤りがあったようです。
自信満々に
うん、ちゃんとアクセスがブロックされています。
とか言っといてCroudFrontからもアクセスできなくなってしまっていました。
前回の第2章1節 LambdaとAPI Gatewayで動的コンテンツを生成するで予告した通り、CORS(サーバー間リソース共有)を試そうと前々回設定したCloudFront経由でのS3表示を確認したところ、以下のように表示されてしまいました。
???
私がやった手順と公式を比較してみると、
- [バケットアクセスの制限] で、[はい] を選択します。
ん、これ無いな。
項目がない。
注記 [バケットアクセスの制限] オプションが表示されない場合は、Amazon S3 オリジンはウェブサイトエンドポイントとして設定される場合があります。
エンドポイントを直入力しちゃったのが悪いのかな。
試しに編集画面で「Origin Domain Name」にフォーカスを当ててみると
選べる。
S3のバケット名を選択すると
出てきました。「Restrict Bucket Access」。
上がエンドポイント直貼り。下が選択した場合。
入力される値が違うみたいでした。うっかり。
一旦、S3のバケットポリシーを丸々削除して再トライします。
前々回作ったCloudFrontのDistributionとOAIも消しておきます。
一度DisabledにしないとDeleteできないみたいですね。
王様の耳はロバの耳
再度はじめからやってみた結果、うまくいきました。
誤解を招かないよう、前々回を更新しておきました。
こちらのページが大変参考になりました。ありがとうございました。
つまり、王様はケンタウロス?
だいぶ混乱してしまいましたが、 これでほんとうにS3へのHTTPSアクセスを確立できました。
そうだ、教祖になろう。出エジプト記 第2章1節 LambdaとAPI Gatewayで動的コンテンツを生成する
To be or not to be
次に何をするか迷っています。
選択肢は以下です。
サイトの公開を優先する
→Route53で独自ドメインの取得へ進む構築環境を整える
→CodeCommitやCloud9の設定へ進む
創世記 第3章1節では偉そうにアジャイルやりますと言ってしまいましたが、実はアジャイル開発の経験はほとんどないので、はじめに目指すべき方向をどう決めればいいのかよくわかりません。
とにかく公開を優先するか、
裏側まで一応作り込んでから公開するか、
いやいや、変更や運用をしやすい環境を整える方があとあと効率いい気もします。
参考になるサイトがありました。
すべての要求の実装が短期間内に終わるような場合は要求はあまり注意深く優先順位付けしない(しても仕方ない)。
んーなるほど。
あんまり悩んでも仕方ないかもしれません。
1.は後回しにしましょう。
コンテンツはまだ"Hello, World"なので公開しても仕方ないですからね。
3.も後回し。
とりあえず、手作業ベースでひと通りのサービスを覚えて、必要になったところで環境・運用系のサービスを使っていきましょう。
その方がブログをご覧のみなさんにもわかりやすいかもしれません。
ということで、2.のサーバサイド処理を構築していくことにします。
こういう構成です。
この時点ではJavaScriptを使わずブラウザから直接アクセスします。
サーバレスなのにサーバサイドとはこれいかに
まず、Lambdaです。
Lambdaはサーバレスで処理を実行できるサービスです。
起動タイミングは時間起動、Webリクエスト起動のほか、他サービスから呼び出されて起動することもできます。
今回はWebリクエストをトリガとして起動してみます。
プログラミング言語は、Java、PowerShell、Node.jsなど7種類をサポートしています。
今回はLambdaでは割とポピュラーで前に書いたことがあるPythonを使います。
こちらの記事を参考にさせていただきました。
AWSに入ってリージョンを選択します。
Webサイトの訪問者はほぼ日本人と思われるので、「アジアパシフィック(日本)」を選択します。
これ結構忘れがちです。
最初、バージニア北部に作っちゃって全部やり直しました。
「サービス」でLambdaを選択します。
関数の一覧が表示されます。まだ何もありません。
「関数の作成」をクリック。
「一から作成」を選択。
「関数名」を入力。「ランタイム」はPythonの最新を選択します。
「実行ロールの選択または作成」を開きます。
AWSではこのロールという概念が大事で各所で出てきます。
簡単に言うと、サービスからサービスを起動する場合、起動する側が持っているべき起動される側へのアクセス権限です。
今回の場合は、API GatewayからこのLambda関数を起動できる権限のことですね。
ロールは私のような慣れてない人にとっては割と罠で、1回やり直したりすると作成されたロールは削除されないので、同名のロールが存在しますと怒られます。 その場合は「既存のロールを使用する」を選択したり、 IAMのページでロールを削除したりしてください。
さて、「関数の作成」をクリックすると以下の画面に移ります。
ちょっとわかりづらいかもしれませんが、真ん中にある「λ(ラムダ)」というオレンジ色のアイコンがこの関数を表しています。
左側がこの関数を起動しうるサービス群の定義ですが、まだ何もありません。
これからAPI Gatewayを足していきます。
右側はこの関数から連携されうるサービス群です。「Cloudwatch Logs」が表示されていますが、実際に連携が定義されているわけではありません。
「+トリガーを追加」をクリック。
「トリガーを選択」でAPI Gatewayを選択します。
「API」で「新規APIの作成」、「セキュリティ」で「オープン」を選択。
「追加の設定」にはSPAで利用するCORS(オリジン間リソース共有)を設定できますが、今回は設定しません。
「追加」をクリック。
関数の処理を実装します。 真ん中の「λ」アイコンをクリックします。
画面下の「関数コード」にプログラムコードが出てきます。
すでにWebの正常レスポンスを返す実装がされていますね。
'statusCode'
(レスポンスコード)として200
(正常)を、'body'
(レスポンスボディ)で'Hello from Lambda!'
を返しています。
こりゃ親切。
特に何もいじりません。
まず、API Gatewayと連動させず、Lambda単体の挙動を確認してみましょう。
画面右上の「テスト」ボタンをクリック。
「テストイベントの設定」が表示されます。
Webリクエストをシミュレートするため、
「新しいテストイベントの作成」を選択、「イベントテンプレート」で「Amazon API Gateway AWS Proxy」を選択。
イベント名を何かしらつけます。
作成したイベント名を選択してもう一度「テスト」をクリック。
実行結果が表示されます。「詳細」を開くと関数コードのreturnで指定した値がJSON形式に変換されて表示されているのが分かるでしょうか。
{ "statusCode": 200, "body": "\"Hello from Lambda!\"" }
明るい蟻でもクライアント(暗いAnt)と言うが如し
それでは、このLambda関数をAPI Gateway経由で起動するよう設定を追加していきます。
画面の「API Gateway」をクリック。
その下に表示されるAPI名をクリック。
API Gatewayの設定画面に移ります。
「/」の下にLambda関数名と同じリソース名が表示されています。
クライアントからLambdaへのリクエスト送信(→方向)とそのレスポンス(←方向)が表現されています。
では、API Gateway経由のLambda起動をテストします。
クライアントの箱の中の「テスト」をクリック。
「メソッド」に「GET」を選択して「テスト」をクリックすると、その右側に結果が表示されます。
「ステータス: 200」で正常レスポンスが返りますね。
実はこれまでの手順でこのAPIはデプロイされて、インターネット経由でアクセスできるようになっています。
確認してみましょう。
画面左部から「ステージ」を選択し、「default」というステージ名をクリック。
画面中央部の「URLの呼び出し」のURLをそのままクリックするとパス違いでエラーが返されるのですが、
アドレスバーでURLの最後に先ほど作ったリソース名をつけると、
おお、Lambdaのreturn値が表示されました。
うまい!小遊三さんに座布団一枚で冬キャンプの刑!
これでやっと動的コンテンツを生成することができまし……
…
…ん?これ、動的である必要性が伝わりづらいね。
固定文字列返してるだけだもんね。
見てる人はS3でいいじゃんてなるね。
ということで、蛇足ですがさきほどのLambda処理をちょっと変えてみます。
現在時刻をレスポンスボディに足してみました。
import datetime import json def lambda_handler(event, context): # TODO implement return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda! at ' + str(datetime.datetime.now())) }
これをさきほどのAPI Gatewayのエンドポイントで表示すると、
これで誰がどう見ても『動的に生成されるコンテンツ』です。
※あとでキャプチャを貼りなおしたでの時刻が変になっていますが、気にしないでください。
今回はやり直しもあり、思わぬ長丁場でした。
カフェで4時間も粘ってしまったため、回復傾向だった体調がまた悪化するかもしれません。
みなさんの読みやすさのためにも今後は少しずつ刻んでいった方がいいかもしれませんね。
そうだ、教祖になろう。出エジプト記 第1章2節 CloudFrontでサイトをHTTPS化する
ブログ開くともう 7日たつなぁって
前回の第1章1節 S3で静的WebページをホスティングするではWebサイト構築の手始めとして静的Webページを作りました。
あれは創世記第3章の前に書き溜めておいたやつなので、実は構築からすでに1週間たっています。
サイトを公開したあとS3の設定ページを見て私はこんなことを言いました。
ほわっほわっほわっほわわわわ~ん
(回想シーンに入る音)
「パブリック」のオレンジ色が緊張感をあおります。
主が我らを見ておられます。
世界中の悪意のある攻撃者も我らを見ておられます。
(回想シーン終わり)
では、実際1週間でどれだけの閲覧があったか見てみましょう。
1週間で480ものリクエストがありました。
…
「うわっ…私の主、多すぎ…?」
これはこのWebサイトがバズったからでも、神がご覧になられているからでもありません。
どこかにS3のエンドポイントにランダムアタックする人がいるということです。
ぞわぞわしますね。
弊教団も令和時代の新興宗教としてセキュリティを考えていかないといけません。
昔の名前で出ています
シンプルなWebはHTTPというプロトコルで通信しています。
プロトコルとは、通信相手とあらかじめ取り決めておいた通信の方式やフォーマットです。
Webの場合、クライアントとサーバは同じプロトコルを共有して、それを守りながら通信することになります。
HTTPのプロトコルは誰でも知ることができるので通信を改ざんされてしまう恐れがあります。
この対策として通信内容を暗号化してリクエストを送るよう拡張したプロトコルがHTTPSです。
多くのショッピングサイトではこっちが採用されています。
今回はサイトへのアクセスをHTTPSに変更していきましょう。
AWSのCloudFrontというサービスをかまします。
つまり、こういう構成です。
なお、CloudFrontはあとでやるAPI Gatewayとの連携にも必要になります。
今回はこちらのサイトを参考にさせていただきました。
CloudFrontの前にS3のバケットポリシーを削除しておきます。
このあとの手順で自動で設定されるからです。
ではAWSでCloudFrontを検索します。
「Create Distribution」をクリック。
Webの「Get Started」をクリック。
設定を入力。
「Origin Domain Name」で前回構築したS3を選択。
※ここでエンドポイントを張りつけちゃうとあとあとOAIの設定がうまくいかないので注意です。
「Origin ID」が勝手に入ります。
S3へのアクセスをCloudFrontからにするため、「Restricted Bucket Access」で「Yes」を選択。
認証情報OAIを作成するため、「Origin Access Identity」を「Create a New Identity」にして「Comment」を入力。
OAIからのアクセスのみ許可するようS3のバケットポリシーを変更するため、「Grant Read Permissions on Bucket」を「Yes, Update Bucket Policy」にします。
HTTPはHTTPSに転送したいので「Vierwer Protocol Policy」で「Redirect HTTP to HTTPS」を選択。
それ以外の項目はデフォルトです。
通信負荷を減らすためのコンテンツのキャッシュ期間もいじれるようですね。
一番下の「Create Distribution」をクリック。
左のメニューの「Distributions」から一覧に戻るとデプロイが開始されています。
「Status」が「In Progress」から「Deployed」になるのをブログを書きながら待ちます。
10分くらいかかりました。
「Domain Name」をコピーしてブラウザのアドレスバーに貼り付けます。
怒られてしまいますが、慌てないでください。
アドレスバーの末尾に/index.html
を入力します。
HTTPSでアクセスできているのが確認できます。
アドレスバー直打ちでhttps://
をhttp://
でアクセスしようとしても、ちゃんとhttps://
にリダイレクトされます。
いちいち/index.html
を入力しなくてもいいように設定を変えてみます。
CloudFrontの「Distribution Settings」をクリック。
「General」タブの「Edit」をクリック。
「Defalut Root Object」に「index.html」を入力。
さきほどのURLの/index.html
なしでもアクセスできることを確認できます。
二人で名前消して
さて、S3からのアクセスの方はどうなったでしょうか。
先ほど「Create a New Identity」を選択したので、自動で制限が加えられているはずです。
S3のバケットポリシーを確認すると CloudFrontのOAIからのアクセス許可が設定されています。
エンドポイントにアクセスすると、
うん、ちゃんとアクセスがブロックされています。
これでリクエストをHTTPS化できました。
別にこれでセキュリティリスクがすべてなくなったわけではないのですが、まずはひと安心です。
他の対策は今後徐々にやっていきます。
次は何しましょうかね。