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

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

そうだ、教祖になろう。出エジプト記 第3章2節 LambdaからGoogleスプレッドシートにアクセスする

門の神「ズール」


前回の第3章1節 Cloud9でLambdaの開発環境を構築するではCloud9で開発環境をセットアップしました。 これでサーバサイドを実装できるようになったわけですが、いきなり全部作ると長いので少しずついきましょう。

まずはこのアプリケーションの外部記憶であるところのGoogleスプレッドシートへのアクセスを試してみます。 こういう構成です。

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

GoogleスプレッドシートへのアクセスはPythonのgspreadというパッケージがやってくれます。

まずは、Googleスプレッドシート側でアクセスに必要な認証情報を作成します。
今回、こちらのサイトを参考にさせていただきました。ありがとうございます。

qiita.com

Google API Consoleにログイン。

割り当てプロジェクトを作成します。
「+プロジェクトを作成」をクリック。

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

「プロジェクト名」を入力。「作成」をクリック。

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

左上のメニュー「APIとサービス」から「ライブラリ」を選択。

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

Google Drive API」を検索して「有効にする」。

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

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

さらに「Google Sheets API」を選択して「有効にする」。

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

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

認証情報を作成します。
実体は認証情報が書き込まれたJSONファイルです。
あとでサーバサイド処理で読み込んでGoogleスプレッドシートへの認証を行います。

「認証情報を作成」をクリック。

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

「使用するAPI」に「Google Sheets API」、「APIを呼び出す場所」に「ウェブサーバー」、「アクセスするデータの種類」に「アプリケーションデータ」、「いいえ、使用していません」を選択して「必要な認証情報」をクリック。

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

「サービスアカウント名」にわかりやすい名前を入力、「役割」を「Project」から「編集者」を選択、「キーのタイプ」に「JSON」を選択して「次へ」をクリック。

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

これでサービスアカウントキーのJSONファイルがダウンロードされました。

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

アクセスするGoogleスプレッドシートを共有設定します。
アクセスしたいGoogleスプレッドシートを開き、「共有」をクリック。

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

「他のユーザーとの共有」ダイアログの「ユーザー」欄にダウンロードしたJSONファイルの「client_email」のメールアドレスを入力します。
このメールアドレスは実在はせず、スプレッドシートを共有した相手を識別するためのIDのようです。

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

鍵の神「ビンツ」


さて、認証情報をサーバサイド処理に取り込みます。
ダウンロードしたJSONファイルをCloud9にアップロードします。
画面左部にドラッグ&ドロップ。

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

先ほどの参考サイトをもとにソースを変えてみます。
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

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

インストールしたパッケージを読み込むためにソースの先頭で「site-packages」フォルダにパスを通します。

import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '../site-packages'))

「Lambda(local)」で実行してみます。
API Gateway(local)」よりエラーの詳細が分かって便利です。

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

んーFileNotFoundErrorですね。ファイルが読み込めていないようです。

どうもLambdaは.py拡張子以外のファイルを自由に扱えないようです。
セキュリティ上、JSONファイルをS3やSecrets Managerに配備しておいてLambda実行時に取得するのがセオリーなのかもしれません。

破壊の神「ゴーザ」


「セオリーなど打ち壊してくれる!.pyファイルにしてしまえばよかろう!」

 

 

JSONファイルをsettings.pyというファイル名に変更し、内容の先頭に

ACCOUNT_KEY =

と付与しました。これでPythonのディクショナリ型宣言になります。

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

これを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)

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

「Run」。
今度はちゃんとワークシート名「lifes」が取れました。

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

認証情報をソースとして扱っているのが不安ですが…

 

「よかろうなのだ!」

 

…では、セキュリティ情報はまとめてS3かSecrets Managerで管理する、をバックログに積んでおきましょう。

「誰かにお前は神かって聞かれたら、イエスって答えるもんだぜ」


快適コーディングライフのためにもうひと工夫してみましょう。
Cloud9ではデフォルトでPythonの自動フォーマットが利きません。

ので、自動フォーマッタパッケージ「yapf」を入れます。
こちらを参考にさせていただだきました。ありがとうございます。

qiita.com

また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 

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

お、きれいに改行が入っています。

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

保存時に自動でフォーマットする設定は「AWS Cloud9」から「Preferences」を選択。
Python Support」の「Format Code on Save」をON、「Custom Code Formatter」を「yapf -i "$file"」と入力します。

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

これでソースを保存するたびに

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

こうじゃ!

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

あー気持ちいい。

次回はお待ちかね、サーバサイド処理を本格的に実装していきます。

そうだ、教祖になろう。出エジプト記 第3章1節 Cloud9でLambdaの開発環境を構築する

ソースはクラウドにいまし、


Route53以外のAWSサービスが繋がったので、アプリケーションを開発していきたいのですが、まずは開発環境が必要です。

PythonだとPCでPyCharmでやってもいいのですが、クライアントサイドのVue.jsもあるし、せっかくAWSなのでCloud9を使ってみます。
Cloud9はクラウドで開発ができちゃうというサービスです。
サーバレスではなく、サーバインスタンスであるEC2を立てます。起動時間で利用料が発生しますが、利用しないときは自動で停止され、最低スペックなら月額2ドルほどで済むようです。

では早速、始めてみましょう。

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

「Name」で開発環境の名前をつけます。

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

利用できるインスタンスはないので新規に「EC2」を作ります。
一番スペックの低い「t2.micro」を選択、OSは「Amazon Linux」を選択します。
自動で停止する設定はデフォルトの30分にしておきます。

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

すぐに開発環境の作成が始まります。

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

1分ほどで作成されました。 早いですね。

では、Lambda関数を作ってみましょう。
Lambda関数のベースを整えてくれるウィザード機能を使います。
このウィザードを使うと色々なリソースがプロビジョニングされます。
まだよくわからないところもあるので、適宜触れていきます。

右端の「AWS Resources」をクリック。
表示される「λ+」マークをクリック。

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

「Function Name」にLambda関数名、「Application Name」にLambdaアプリケーション名を入力します。

AWS Lambda アプリケーションは、 Lambda 関数、イベントソース、およびその他のリソースを組み合わせたもので、協調して動作することによってタスクを実行します。

ということなんで単にLambda関数を作るより余計なものまでできそうなのですが、ここは大いなるCloud9の意思にしたがっておきます。

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

ランタイムは「Python 3.6」が最新のようです。
テンプレートは「empty-python」を選びます。

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

あとでCloudFrontと連携するので、「Function trigger」に「API Gateway」を選択、「Resource Path」に第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化するで作ったAPIと同じパスを入力。
第2章2節の続き CloudFront以外からAPI Gatewayへのアクセスを閉じると同じくAPIキー必須にしたいので「Security」を「NONE_KEY」にします。

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

メモリとロールはデフォルトにしておきます。

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

「Finish」で作成終了です。

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

数秒で作成が終わります。楽。
左側のフォルダ構造は
ルート/Lambdaアプリケーション名/Lambda関数名
になります。

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

Cloud9は、クラウドリソースをテンプレートによりプロビジョニングするCloudFormationを使って色々なリソースをデプロイします。
CloudFormationを見てみると新しいスタックが存在しています。

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

このCloud Formationにより色々なリソースが作成されています。

IAMのロール。

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

Lambda関数。

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

同じくLambdaアプリケーションもありました。

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

API Gatewayもできています。
エンドポイントタイプが「Regional」でなく「Edge」になっていますが、本家サイトだとエッジ最適化は

CloudFront ディストリビューション経由でアクセスするエンドポイントです。

ともあるので、まあいいでしょう。

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

作成されたAPIを表示し、「メソッドリクエスト」をクリックすると、

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

APIキーの必要性」が「false」になっています。
さきほど「NONE_KEY」を選んだのですが、ここに反映されるわけではないのでしょうか。
よくわかりません。

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

「ステージ」を見ると、すでに「Prod」と「Stage」という2つのステージがデプロイされています。

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

ウィザードでできたものが多すぎて追いきれていない気がします。
CloudFormationのテンプレートを読めば全容が分かるんでしょう。

世はすべて事もなし


なにはともあれ、動かしてみましょう。

動くは正義です。

「Stage」のURL+/+リソース名にアクセスしますが、いきなりHTTPステータス502が返ってきました。
動かないじゃん。

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

これは、ウィザードで作ったlambda_function.pyの戻り値がHTTPレスポンス形式になってないからです。 ここまで色々作ってくれて、そこはやらないんかい!と思いますが、

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

lambda_function.py第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化すると同じにします。
レスポンスのbodyはちょっと変えてみます。

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

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)」を選択します。

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

実行すると「Status 200 OK」が返ってきました。
ちなみに「API Gateway(remote)」を選ぶと表示されているソースではなく、Lambdaにデプロイされているソースが実行されるようです。

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

修正したソースをLambdaにデプロイしましょう。 右側のLambda関数名を選択して「↑」ボタンをクリックするだけです。これはいい。

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

APIキー必須にしたいので、API Gatewayの「使用量プラン」の「APIステージの追加」をクリック。

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

先ほどCloud9が作ってくれた「Stage」ステージを追加します。

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

「リソース」の方で「APIキーの必要性」を「true」に変えます。

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

「Stage」を再度デプロイします。
リソースを選んで、「APIのデプロイ」をクリック。

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

「Stage」を選択して「デプロイ」。

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

API Gatewayに直アクセスしてみると、無事「Fobidden」になってAPIキーなしのアクセスが拒否されているのが分かります。

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

ちなみにCloud9ではAPIキー不足によるエラーにはなりません。Cloud9はあくまでLambdaのテストのようです。

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

それでは、CloudFront経由でアクセスできるように設定していきます。

第2章2節の続き CloudFront以外からAPI Gatewayへのアクセスを閉じるで修正したDistributionを選択。

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

「Create Origin」をクリック。

「Origin Domain Name」に「Stage」のURLを貼り付けます。
「Origin Protocol Policy」は「HTTPS Only」、「Origin Custom Headers」にAPI GatewayAPIキーを登録します。

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

次に「Behaviors」で/api/*を選択して、「Edit」をクリック。
「Origin or Origin Group」をさきほど作ったOriginに変更します。

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

デプロイを待ってアクセスを確認すると、

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

新しく作ったLambda関数の結果に差し替えられました。

これでサーバサイドの開発環境が整いました。

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

次回はいよいよGoogleスプレッドシートにアクセスしていきます。

なお、EC2の一覧でCloud9用のインスタンスを確認できます。 Cloud9をしばらく放っておくと、ちゃんと「Stopped」になるみたいですね。

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

そうだ、教祖になろう。出エジプト記 第2章2節の続き CloudFront以外からAPI Gatewayへのアクセスを閉じる

正面玄関におまわりください。


どうも「続き」とか「訂正」が多いですね。
前回の第2章2節 CloudFrontでAPI Gatewayを同一ドメイン化するではCloudFrontとAPI Gatewayを連携させました。

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

S3のときと同じく、CloudFrontの裏側にあるAPI Gatewayへの直接アクセスを防ぎたいと思います…
が、何がベストプラクティスなのかなかなかわからず、困りました。
思えば、毎回困ってばかりの気がします。
乳と蜜の流れる地はまだまだ遠いようです。

教祖です。

S3との連携のときは、CloudFrontがOAI(Origin Access Identity)を作ってくれて、S3のバケットポリシーまで書き換えてくれました。
しかし、いたれりつくせりはOriginがS3バケットのときだけで、API Gatewayはカスタムオリジンのため、対象外のようです。
ちぇっ。

こちらによると

dev.classmethod.jp

API Gatewayには、APIキー認証以外にアクセス制限の機能がありません。(中略) また、API Gatewayへの直アクセスを禁止したい場合は、前述のAPIキー認証を有効化し、先日追加されたオリジンへのヘッダ追加機能でCloudFrontのオリジン設定にx-api-keyヘッダを追加、設定することで対応できます。

ということです。

APIキーは、簡単に言うとリクエストヘッダに埋め込んだ文字列が合っていればOKという仕組みです。
AWS本家だと、APIキーはあくまで利用量の管理用だから認証に利用するなよー!Cognito(認証サービス)使えよー!という記述が多くみられるので避けていたのですが、サービス間連携の認証であれば使ってもいいようです。

勝手口は閉鎖いたしました。


「リソース」でAPI Gatewayリソース下の「ANY」を選択して「メソッドリクエスト」をクリック。

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

APIキーの必要性」を「true」に変更。

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

ステージをデプロイしなおします。
「ステージ」の「ステージの削除」をクリック。

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

「リソース」の「アクション」から「APIのデプロイ」をクリック。

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

APIキーを作ります。

API Gatewayで「APIキー」を選択、「アクション」から「APIキーの作成」をクリック。

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

適当な名前を入力しして、「保存」をクリック。

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

次に使用量プランを作ります。APIキーだけでいいかとおもったら使用量プランに紐づけないと動作しないようです。

「使用量プラン」を選択して、「作成」をクリック。

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

「名前」を適当に入力、「スロットリング」の「レート」、「バースト」、「クォータ」を適当に入力します。
期せずして予算管理っぽくなっていますね。
必要に応じてあとで変えましょう。

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

APIステージの追加」をクリックしてデプロイしたステージを紐づけます。

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

API」と「ステージ」を選択。

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

「次へ」。

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

APIキーを紐づけます。
APIキーを使用量プランに追加」をクリック。

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

APIキーを登録。

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

「完了」。

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

これでAPIキーがないとアクセスできない状態になりましたので、一度直アクセスしてみます。
API GatewayのURL+/+リソース名にアクセスします。

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

HTTPステータスコード403、ボディとして{"message":"Forbidden"}が返されました。
よしよし。

次にCloudFront側です。

先にCloudFrontからのリクエストに埋め込むAPIキーを確認しておきます。
API Gatewayの「APIキー」の「表示」をクリック。

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

Distributionを選択して「Distribution Settings」をクリック。

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

「Origins and Origin Groups」タブのAPI Gatewayを選択して、「Edit」をクリック。

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

「Origin Custom Headers」の「Header Name」にx-api-key、「Value」にAPI Gatewayで作成したAPIキーを入力。

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

「Yes, Edit」をクリックしてデプロイを待ちます。

ではアクセスしてみましょう。
CloudFrontのドメイン名+/+リソース名にアクセスすると、

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

お!Lambdaの結果が表示されました。
ちょっとしたことかもしれませんが、やっぱり動くと嬉しいですね。

そうだ、教祖になろう。出エジプト記 第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の直アクセスを禁止したいのですが、だいぶ疲れたので今すぐ安息日を取ることにします。

そうだ、教祖になろう。出エジプト記 第1章2節の訂正 今度こそCloudFrontでサイトをHTTPS化する

王様は裸だ!


どうやら第1章2節 CloudFrontでサイトをHTTPS化するで設定した内容に誤りがあったようです。

自信満々に

うん、ちゃんとアクセスがブロックされています。

とか言っといてCroudFrontからもアクセスできなくなってしまっていました。

前回の第2章1節 LambdaとAPI Gatewayで動的コンテンツを生成するで予告した通り、CORS(サーバー間リソース共有)を試そうと前々回設定したCloudFront経由でのS3表示を確認したところ、以下のように表示されてしまいました。

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

???

私がやった手順と公式を比較してみると、

docs.aws.amazon.com

  1. [バケットアクセスの制限] で、[はい] を選択します。

ん、これ無いな。

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

項目がない。

注記 [バケットアクセスの制限] オプションが表示されない場合は、Amazon S3 オリジンはウェブサイトエンドポイントとして設定される場合があります。

エンドポイントを直入力しちゃったのが悪いのかな。

試しに編集画面で「Origin Domain Name」にフォーカスを当ててみると

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

選べる。
S3のバケット名を選択すると

出てきました。「Restrict Bucket Access」。

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

上がエンドポイント直貼り。下が選択した場合。
入力される値が違うみたいでした。うっかり。

一旦、S3のバケットポリシーを丸々削除して再トライします。 f:id:chief-shuffle:20191127073546j:plain

前々回作ったCloudFrontのDistributionとOAIも消しておきます。
一度DisabledにしないとDeleteできないみたいですね。

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

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

王様の耳はロバの耳


再度はじめからやってみた結果、うまくいきました。
誤解を招かないよう、前々回を更新しておきました。

こちらのページが大変参考になりました。ありがとうございました。

dev.classmethod.jp

つまり、王様はケンタウロス


だいぶ混乱してしまいましたが、 これでほんとうにS3へのHTTPSアクセスを確立できました。

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

そうだ、教祖になろう。出エジプト記 第2章1節 LambdaとAPI Gatewayで動的コンテンツを生成する

To be or not to be


次に何をするか迷っています。
選択肢は以下です。

  1. サイトの公開を優先する
    →Route53で独自ドメインの取得へ進む

  2. サーバサイド処理の構築を優先する
    →LambdaやAPI Gatewayの設定へ進む

  3. 構築環境を整える
    →CodeCommitやCloud9の設定へ進む

創世記 第3章1節では偉そうにアジャイルやりますと言ってしまいましたが、実はアジャイル開発の経験はほとんどないので、はじめに目指すべき方向をどう決めればいいのかよくわかりません。

とにかく公開を優先するか、
裏側まで一応作り込んでから公開するか、
いやいや、変更や運用をしやすい環境を整える方があとあと効率いい気もします。

参考になるサイトがありました。

www.ryuzee.com

すべての要求の実装が短期間内に終わるような場合は要求はあまり注意深く優先順位付けしない(しても仕方ない)。

んーなるほど。
あんまり悩んでも仕方ないかもしれません。

1.は後回しにしましょう。
コンテンツはまだ"Hello, World"なので公開しても仕方ないですからね。

3.も後回し。
とりあえず、手作業ベースでひと通りのサービスを覚えて、必要になったところで環境・運用系のサービスを使っていきましょう。
その方がブログをご覧のみなさんにもわかりやすいかもしれません。

ということで、2.のサーバサイド処理を構築していくことにします。
こういう構成です。

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

この時点ではJavaScriptを使わずブラウザから直接アクセスします。

サーバレスなのにサーバサイドとはこれいかに


まず、Lambdaです。
Lambdaはサーバレスで処理を実行できるサービスです。

起動タイミングは時間起動、Webリクエスト起動のほか、他サービスから呼び出されて起動することもできます。
今回はWebリクエストをトリガとして起動してみます。
プログラミング言語は、JavaPowerShell、Node.jsなど7種類をサポートしています。
今回はLambdaでは割とポピュラーで前に書いたことがあるPythonを使います。

こちらの記事を参考にさせていただきました。

qiita.com

AWSに入ってリージョンを選択します。
Webサイトの訪問者はほぼ日本人と思われるので、「アジアパシフィック(日本)」を選択します。
これ結構忘れがちです。 最初、バージニア北部に作っちゃって全部やり直しました。

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

「サービス」でLambdaを選択します。

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

関数の一覧が表示されます。まだ何もありません。

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

「関数の作成」をクリック。
「一から作成」を選択。

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

「関数名」を入力。「ランタイム」はPythonの最新を選択します。

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

「実行ロールの選択または作成」を開きます。
AWSではこのロールという概念が大事で各所で出てきます。
簡単に言うと、サービスからサービスを起動する場合、起動する側が持っているべき起動される側へのアクセス権限です。
今回の場合は、API GatewayからこのLambda関数を起動できる権限のことですね。

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

ロールは私のような慣れてない人にとっては割と罠で、1回やり直したりすると作成されたロールは削除されないので、同名のロールが存在しますと怒られます。 その場合は「既存のロールを使用する」を選択したり、 IAMのページでロールを削除したりしてください。

さて、「関数の作成」をクリックすると以下の画面に移ります。

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

ちょっとわかりづらいかもしれませんが、真ん中にある「λ(ラムダ)」というオレンジ色のアイコンがこの関数を表しています。 左側がこの関数を起動しうるサービス群の定義ですが、まだ何もありません。
これからAPI Gatewayを足していきます。
右側はこの関数から連携されうるサービス群です。「Cloudwatch Logs」が表示されていますが、実際に連携が定義されているわけではありません。

「+トリガーを追加」をクリック。
「トリガーを選択」でAPI Gatewayを選択します。

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

API」で「新規APIの作成」、「セキュリティ」で「オープン」を選択。
「追加の設定」にはSPAで利用するCORS(オリジン間リソース共有)を設定できますが、今回は設定しません。
「追加」をクリック。

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

API Gateway」がトリガに追加されました。

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

関数の処理を実装します。 真ん中の「λ」アイコンをクリックします。

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

画面下の「関数コード」にプログラムコードが出てきます。

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

すでにWebの正常レスポンスを返す実装がされていますね。
'statusCode'(レスポンスコード)として200(正常)を、'body'(レスポンスボディ)で'Hello from Lambda!'を返しています。
こりゃ親切。
特に何もいじりません。

まず、API Gatewayと連動させず、Lambda単体の挙動を確認してみましょう。
画面右上の「テスト」ボタンをクリック。

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

「テストイベントの設定」が表示されます。
Webリクエストをシミュレートするため、
「新しいテストイベントの作成」を選択、「イベントテンプレート」で「Amazon API Gateway AWS Proxy」を選択。
イベント名を何かしらつけます。

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

作成したイベント名を選択してもう一度「テスト」をクリック。

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

実行結果が表示されます。「詳細」を開くと関数コードのreturnで指定した値がJSON形式に変換されて表示されているのが分かるでしょうか。

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

{
  "statusCode": 200,
  "body": "\"Hello from Lambda!\""
}

明るい蟻でもクライアント(暗いAnt)と言うが如し


それでは、このLambda関数をAPI Gateway経由で起動するよう設定を追加していきます。
画面の「API Gateway」をクリック。

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

その下に表示されるAPI名をクリック。

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

API Gatewayの設定画面に移ります。
「/」の下にLambda関数名と同じリソース名が表示されています。
クライアントからLambdaへのリクエスト送信(→方向)とそのレスポンス(←方向)が表現されています。

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

では、API Gateway経由のLambda起動をテストします。
クライアントの箱の中の「テスト」をクリック。

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

「メソッド」に「GET」を選択して「テスト」をクリックすると、その右側に結果が表示されます。
「ステータス: 200」で正常レスポンスが返りますね。

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

実はこれまでの手順でこのAPIはデプロイされて、インターネット経由でアクセスできるようになっています。
確認してみましょう。
画面左部から「ステージ」を選択し、「default」というステージ名をクリック。

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

画面中央部の「URLの呼び出し」のURLをそのままクリックするとパス違いでエラーが返されるのですが、

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

アドレスバーでURLの最後に先ほど作ったリソース名をつけると、

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

おお、Lambdaのreturn値が表示されました。

うまい!小遊三さんに座布団一枚で冬キャンプの刑!


これでやっと動的コンテンツを生成することができまし……

…ん?これ、動的である必要性が伝わりづらいね。
固定文字列返してるだけだもんね。
見てる人はS3でいいじゃんてなるね。

ということで、蛇足ですがさきほどのLambda処理をちょっと変えてみます。
現在時刻をレスポンスボディに足してみました。

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

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のエンドポイントで表示すると、

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

これで誰がどう見ても『動的に生成されるコンテンツ』です。
※あとでキャプチャを貼りなおしたでの時刻が変になっていますが、気にしないでください。

今回はやり直しもあり、思わぬ長丁場でした。
カフェで4時間も粘ってしまったため、回復傾向だった体調がまた悪化するかもしれません。
みなさんの読みやすさのためにも今後は少しずつ刻んでいった方がいいかもしれませんね。

次はAPI GatewayとCloudFrontをつなげたいと思います。

そうだ、教祖になろう。出エジプト記 第1章2節 CloudFrontでサイトをHTTPS化する

ブログ開くともう 7日たつなぁって


前回の第1章1節 S3で静的WebページをホスティングするではWebサイト構築の手始めとして静的Webページを作りました。
あれは創世記第3章の前に書き溜めておいたやつなので、実は構築からすでに1週間たっています。
サイトを公開したあとS3の設定ページを見て私はこんなことを言いました。

ほわっほわっほわっほわわわわ~ん
(回想シーンに入る音)

f:id:chief-shuffle:20191116142224j:plain 「パブリック」のオレンジ色が緊張感をあおります。
主が我らを見ておられます。
世界中の悪意のある攻撃者も我らを見ておられます。

(回想シーン終わり)

では、実際1週間でどれだけの閲覧があったか見てみましょう。

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

1週間で480ものリクエストがありました。

 

 

「うわっ…私の主多すぎ…?」

これはこのWebサイトがバズったからでも、神がご覧になられているからでもありません。
どこかにS3のエンドポイントにランダムアタックする人がいるということです。
ぞわぞわしますね。
弊教団も令和時代の新興宗教としてセキュリティを考えていかないといけません。

昔の名前で出ています


シンプルなWebはHTTPというプロトコルで通信しています。
プロトコルとは、通信相手とあらかじめ取り決めておいた通信の方式やフォーマットです。
Webの場合、クライアントとサーバは同じプロトコルを共有して、それを守りながら通信することになります。

HTTPのプロトコルは誰でも知ることができるので通信を改ざんされてしまう恐れがあります。
この対策として通信内容を暗号化してリクエストを送るよう拡張したプロトコルHTTPSです。
多くのショッピングサイトではこっちが採用されています。

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

今回はサイトへのアクセスをHTTPSに変更していきましょう。
AWSのCloudFrontというサービスをかまします。
つまり、こういう構成です。

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

なお、CloudFrontはあとでやるAPI Gatewayとの連携にも必要になります。

今回はこちらのサイトを参考にさせていただきました。

www.wakuwakubank.com

CloudFrontの前にS3のバケットポリシーを削除しておきます。
このあとの手順で自動で設定されるからです。

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

ではAWSでCloudFrontを検索します。

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

「Create Distribution」をクリック。

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

Webの「Get Started」をクリック。

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

設定を入力。
「Origin Domain Name」で前回構築したS3を選択
※ここでエンドポイントを張りつけちゃうとあとあとOAIの設定がうまくいかないので注意です。

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

「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」にします。

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

HTTPはHTTPSに転送したいので「Vierwer Protocol Policy」で「Redirect HTTP to HTTPS」を選択。

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

それ以外の項目はデフォルトです。
通信負荷を減らすためのコンテンツのキャッシュ期間もいじれるようですね。
一番下の「Create Distribution」をクリック。

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

左のメニューの「Distributions」から一覧に戻るとデプロイが開始されています。

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

「Status」が「In Progress」から「Deployed」になるのをブログを書きながら待ちます。
10分くらいかかりました。

「Domain Name」をコピーしてブラウザのアドレスバーに貼り付けます。

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

怒られてしまいますが、慌てないでください。
アドレスバーの末尾に/index.htmlを入力します。

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

HTTPSでアクセスできているのが確認できます。

アドレスバー直打ちでhttps://http://でアクセスしようとしても、ちゃんとhttps://にリダイレクトされます。

いちいち/index.htmlを入力しなくてもいいように設定を変えてみます。
CloudFrontの「Distribution Settings」をクリック。

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

「General」タブの「Edit」をクリック。
「Defalut Root Object」に「index.html」を入力。

さきほどのURLの/index.htmlなしでもアクセスできることを確認できます。

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

二人で名前消して


さて、S3からのアクセスの方はどうなったでしょうか。
先ほど「Create a New Identity」を選択したので、自動で制限が加えられているはずです。

S3のバケットポリシーを確認すると CloudFrontのOAIからのアクセス許可が設定されています。

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

エンドポイントにアクセスすると、

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

うん、ちゃんとアクセスがブロックされています。

これでリクエストをHTTPS化できました。
別にこれでセキュリティリスクがすべてなくなったわけではないのですが、まずはひと安心です。
他の対策は今後徐々にやっていきます。

次は何しましょうかね。