新着記事

Viewing posts for the category Amazon Web Service

CloudFront の Origin を、ボットかどうか(User-Agentによって)判定して切り替える

Webpack でビルドしたWebアプリを S3 にデプロイし、S3 + CloudFront でホストしていました。

ブラウザからのリクエストは問題ないのですが、検索エンジンやTwitterなどのボットからのアクセスの場合、JSを動かせないため内容のほぼ無いページが評価されてしまい、情報の価値がありません。

そのため、Lambda@Edge + CloudFront で、User-Agent を判定し、JSを動かせないようなUAの場合はオリジンをサーバサイドレンダリングするサーバに切り替えるようにしました。

Lambda@Edge とは

Lambda ファンクションを CloudFront のエッジサーバにデプロイし、動作させることができます。

CloudFront で動作させる箇所を4箇所から選ぶことができ、オリジンサーバへリクエストする際に Lambda ファンクションでリクエストヘッダを変更したり、オリジンサーバを変更したりできます。

AWSのページにわかりやすい図があります

ちなみに、Lambda@Edge について以前書いたブログ記事もあります

失敗例

AWSの図を見る限り、Origin Request で Lambda ファンクションを動かし、
リクエストヘッダの User-Agent ヘッダで Origin を変えれば良さそうです。

実際にそのようなコードは書けるのですが、Origin Request の際は User-Agent ヘッダは常に
「Amazon CloudFront」になるため、判定には使えません。

なお、CloudFront のビヘイビアの設定で、ユーザー端末の User-Agent ヘッダをオリジンまで通すことは可能です。
(ビヘイビア→ Cache Based on Selected Request Headers → Whitelist Headers で、User-sAgent を Add Custom)

ただし、これを行うと警告が出ます。

Some headers have a lot of possible values, and caching based on the values in these headers would cause CloudFront to forward more requests to your origin. We recommend that you do not cache based on the following headers:User-Agent

この警告にある通り、キャッシュの効率が悪くなるので、よほどの理由がない限りやらないほうが良いでしょう。

方針

このような方針にしました。

1. Viewer Request に入ってきた時点で、Lambda@Edge で User-Agent ヘッダを判定。
ホワイトリストで評価し、もしJSを扱えないようなUA(ボット等)の場合、
bot-user-request=1 のリクエストヘッダを付与する。

2. CloudFront のビヘイビアで、bot-user-request のHTTPヘッダは通す (Cache Based on Selected Request Headers にする)

3. Origin Request の時点で、Lambda@Edge で bot-user-request ヘッダがあれば、オリジンのドメインを変更

コード

Viewer Request

'use strict';

/*
Cloudfront の Viewer Request 用のファンクション。

Google Bot からのアクセスの場合、Httpヘッダの bot-user-agent = "1" を設定する。
この後、CloudFront のビヘイビアで bot-user-agent のヘッダーを通し、
さらに Origin Request のLambda で、bot-user-agent を判断し、
Originを切り替える
*/

const bots = [
'Twitterbot',
'facebookexternalhit',
'compatible; Google',
'Googlebot',
];


exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;

const isBot = bots.some(v => {
return request.headers['user-agent'][0].value.includes(v)
});
if (isBot) {
request.headers['bot-user-agent'] = [
{
'key': 'bot-user-agent',
'value': '1'
}
];
}

callback(null, request);
};

Viewer Response

'use strict';

/*
bot-user-agent の HTTPリクエストヘッダがあったら、
オリジンを書き換える。

*/
const newDomainName = 'new.example.com';

exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;

if ('bot-user-agent' in request.headers) {
if ('custom' in request.origin ) {
request.origin.custom.domainName = newDomainName;
request.headers['host'] = [{ key: 'host', value: newDomainName}];
// HTTPSでリクエストする場合
request.origin.custom.port = 443;
request.origin.custom.protocol = 'https';
}
}

callback(null, request);
};

AWS LambdaをCloudFrontエッジサーバで動かす (Lambda@Edge)

AWSの Lambda ファンクションを、CloudFrontのエッジサーバにデプロイして、リクエストやレスポンスをインターセプトして処理を行うことができます。Lambda@Edge と呼ばれます。

Webアプリケーションフレームワークにある「ミドルウェア」のような使用感で使えます。

例えば、S3 をオリジンに使っている時(サーバレス環境などで)、少しだけ動的に認証やリダイレクトなどのHTTPヘッダーを扱う処理をしたい時など、便利です。

ポイントとして、us-east-1 (バージニア北部) リージョンの Lambdaファンクションのみ選択できます。

AWSの公式ドキュメント

1. 概要

1-1. フックポイント(トリガー)

Amazonのページにわかりやすい絵があります

リクエスト:

クライアント → [Viewer Request]→ CloudFront → [Origin Request] → Origin

レスポンス:

クライアント ← [Viewer Response] ← CloudFront ← [Origin Response] ← Origin

Viewer Request, Origin Request, Origin Response, Viewer Response の4箇所で処理を差し込めます。

今回は、例として Origin Response にファンクションを差し込んでみます。

1-2. Lambdaファンクションのパターン

公式ドキュメントにサンプル集があります(便利)。

今回は、Origin Response で処理をさせるため、引数にはレスポンスが入っており、処理としてはレスポンスを返す( callback(null, response) を実行する) 必要があります。

リクエストとレスポンスを取得するファンクションの定番パターンは

'use strict';

exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
// なにか処理...
callback(null, response);
}

このようになります。responseを加工して返すか、新しいレスポンスデータを作って返すことになるでしょう。

1-3. eventの中身をすべてJson で表示するファンクション

最初は、event の中身をすべて見れるようなファンクションを作ると動作を早く理解できます。このようになります。

'use strict';

exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
response.status = 200;
response.statusDescription = 'Ok';
response.headers['content-type'] = [{
"key": "Content-Type",
"value": "application/json",
}];
response.body = JSON.stringify(event, null, ' ');
callback(null, response);
}

2. 実際に作ってみる

2-1. CloudFront ディストリビューションの作成

2-1-1. CloudFront のページ  から、 Create Distribution

2-1-2. Web の Get Started

2-1-3. Lambda Function Associations は、ここでは設定しない。(あとで Lambdaのページから設定する)

オリジンは適当なS3にでもしておいてください。

キャッシュ時間は、すべて0にしてキャッシュさせないようにすると検証が楽です。

Create Distribution で作成。

2-2. Lambda 関数の作成

2-2-1. us-east リージョンの Lambdaページから「関数の作成

2-2-2. ロールは、「カスタムロールの作成」

2-2-3. ロール作成ページが表示されるので、ロール名をつけて許可

2-2-4. このロールに、EdgeLambdaの「信頼関係」(サービスプリンシパル) が必要なので、一旦 IAM のロールの「信頼関係」タブを見る

2-2-5. 信頼関係の編集 をクリック

2-2-6. Principal をリストにして、"edgelambda.amazonaws.com" を追加して、ポリシーの更新。

"Service": [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]

2-2-7. 信頼されたエンティティに edgelambda.amazonaws.com が追加されている。

2-2-8. Lambda のページに戻ると、「既存のロール」に先程のロールが設定されるので、「関数の作成」をクリック。

2-2-5. 関数作成のウインドウが出るので、関数を書いて、「保存」。

'use strict';

exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
response.status = 200;
response.statusDescription = 'Ok';
response.headers['content-type'] = [{
"key": "Content-Type",
"value": "application/json",
}];
response.body = JSON.stringify(event, null, ' ');
callback(null, response);
}

2-2-6 「テスト」ボタンをクリック

2-2-7. このテストページでは、ファンクションの第一引数を設定してテストできる。

今回のLamda関数は、第一引数 event が何であってもオウム返しするだけなのでどんなデータでもテストできるが、Amaon CloudFront Access Request In Response が実際の Origin Response の event に近いので、これを使ってテストイベントを作る。

2-2-8. 「テスト」をクリックしてテストを実行。

2-3. CloudFront にデプロイ

作ったファンクションを CloudFront にデプロイする。

2-3-1. Lambda の Designer から、CloudFront を選択

2-3-2. 「グローバル展開が保留中」と表示されるので、下にある トリガーの設定で、「Lambda@Edgeへのデプロイ」をクリック

2-3-4. デプロイページで、「オリジンレスポンス」を選択し、確認チェックボックスを入れて「デプロイ」

デプロイされる。

2-4. テストリクエスト

curlかなんかでリクエスト。eventの内容がわかる。

% curl -v 'http://xxxxxxxxxxxxxx.cloudfront.net/'

* Trying ****:****:****:****:****:****:****:****...
* TCP_NODELAY set
* Connected to xxxxxxxxxxxxxx.cloudfront.net (****:****:****:****:****:****:****:****) port 80 (#0)
> GET / HTTP/1.1
> Host: xxxxxxxxxxxxxx.cloudfront.net
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 2663
< Connection: keep-alive
< x-amz-bucket-region: ap-northeast-1
< Date: Sat, 19 Jan 2019 13:30:09 GMT
< Server: AmazonS3
< X-Cache: Error from cloudfront
< Via: 1.1 xxxxxxxxxxxxxx.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: xxxxxxxxxxxxxx
<
{
"Records": [
{
"cf": {
"config": {
"distributionDomainName": "xxxxxxxxxxxxxx.cloudfront.net",
"distributionId": "xxxxxxxxxxxxxx",
"eventType": "origin-response"
},
"request": {
"clientIp": "****:****:****:****:****:****:****:****",
...
...

この出力は、今回の Lambda 関数の第一引数そのものなので、Lambda のテストデータとしてコピペしておくと今後のテストに便利です。

2-5. 修正後のデプロイ

アクションメニューに「Lambda@Edge へのデプロイ」項目が追加されるのでここからデプロイすると楽です。

AWS CloudFrontにACMの独自SSL証明書を適用する

CloudFront には、AWS ロードバランサー (ELB,ALB) と同じように ACM (Certificate Manager) で無料で作成したSSL証明書を適用することができます。

ポイントは、us-east-1 (バージニア北部) リージョンのACM で作った証明書のみ選択できるというところです。

1. ACMでSSL証明書を作る

1-1. us-east-1 の ACM へ行く https://console.aws.amazon.com/acm/home?region=us-east-1#/

1-2. 証明書のリクエスト をクリック

1-3. パブリック証明書のリクエスト → 証明書のリクエスト

1-4. ドメイン名を入力。「この証明書に別の名前を追加」すると、SSL証明書の SAN (Subject Alternative Name ) になる。

1-5. 検証方法はお好みで。DNSだと、使用後にレコードを消さないと後々レコードを見つけた時に混乱の原因になるので、削除するひと手間が多くかかる。Eメールの方が好み。

検証が終われば、SSL証明書が使えるようになります。

2. CloudFront に適用する

2-1. CloudFrontの、Distribution Settings を開く

2-2. Alternate Domain Names に、使うドメインを書く

2-3. SSL Certificate で Custom SSL Certificate (example.com): を選ぶと、us-east-1 (バージニア北部) リージョンのACMで作った証明書の一覧が出てくる。
先程作ったSSL証明書を選択。

これで 独自ドメインのSSL証明書が CloudFront で使えます。

あとは、DNSで、使うドメインのCNAME を CloudFront のドメイン (xxxxxxxxxxx.cloudfront.net) に向ければ、独自ドメインのHTTPリクエストがSSL通信でこの CloudFront を経由するようになります。

Search

Recent Tweets

  • ytyng

    ytyng @ytyng

    キン肉マン 40巻無料読み放題 https://t.co/UINciSV10K #漫画全巻ドットコム
    2 months ago

  • 尚月地『艶漢』公式ツイッター

    尚月地『艶漢』公式ツイッター @AdekanOfficial

    ytyng

    (リプライズ)祝祝祝・池袋虜グランドオープン!! https://t.co/JUt8qcuPIX
    4 months, 1 week ago

  • 尚月地『艶漢』公式ツイッター

    尚月地『艶漢』公式ツイッター @AdekanOfficial

    ytyng

    おはようございます!! 祝祝祝・池袋虜グランドオープン!! 艶漢アンダーグラウンド展が池袋虜で初日。地下へと降りる細い階段は、詩郎たちが待つ艶漢迷宮への入り口。アングラならではのこと、していきます。(担★彡) #艶漢アングラ… https://t.co/ZhnYMKiTgp
    4 months, 1 week ago

  • ytyng

    ytyng @ytyng

    エイプリルフール版の Stack Overflow がクソすぎて内容が全然頭に入ってこない(褒めている)
    4 months, 3 weeks ago

  • ytyng

    ytyng @ytyng

    イケメンすぎてすごい https://t.co/UIgv3xgab6
    7 months ago