新着記事

Viewing posts for the category Amazon Web Service

Svelte で作ったWebコンポーネントを接続国によって出し分ける

概要

11月28日、当社 TORICO の初の海外店舗、マンガ展 台湾 がオープンしました。

漫畫展 台灣

オープンする直前で、「漫画全巻ドットコム、スキマ、マンガ展、ホーリンラブブックスのサイトに、台湾からアクセスされた方に告知を出したい。告知を1回クリックしたらもう出さないようにしたい。」という開発要件が出たので、作ってみました。

接続元の判定と出し分け

今回は、 AWS の CloudFront で、接続元の国名のヘッダを付与する機能を使うことにしました。

CloudFront の入口でリクエストヘッダが付与されますので、CloudFront Functions でそのヘッダを見て動的な処理が行えます。
ヘッダに応じて、対象国であれば、バックエンドのパスを変化させることで、接続国に応じた処理を行えるように設計しました。

サーバの管理はなるべく減らしたいため、今回は完全にサーバレスでサービスを作っています。動的な処理は、CloudFront Function を使いました。

コンポーネントの開発

全サイトに共通のモーダルポップアップ表示を行う必要があります。当社エンジニアのまるさんから、Svelteを進められたので、Svelte で Web コンポーネントを書きました。

完成物

これらのサイトに、台湾からアクセスすると、モーダルポップアップが表示されます。

仕組みとしては、リクエストするスクリプトの内容が、接続国に応じて変化するようになっています。

Svelte で Web コンポーネントを作る

Svelte の概要

Svelte は、Vue のシングルファイルコンポーネントによく似た形で、1ファイルで1コンポーネントをちょうどよく書けます。

CSS はスコープドなものになり、変数は特に意識せずともリアクティブに扱うことができます。

Web コンポーネントとは、JS によって作られた独自の HTML タグをどんなサイトでも使えるようにする技術です。
コンポーネントを定義した JS ファイルをロードすることで、そのサイトで <my-component></my-component> のような独自のタグを使えるようになります。
そのサイトが 素の HTML + jQuery で作られているサービスでも、React や Vue で作られているサイトでも、Google Tag Manager からでも同じようにコンポーネントが使えます。

複数サービスで共通のチャットウィジェットや、広告ウィジェットの開発に適しています。

プロジェクトの開始

Svelte のプロジェクトの開始方法は、チュートリアルがあるのでこの手順で作成できます。

https://svelte.jp/docs#getting-started

npm create vite@latest myapp

対話的に質問されますので、適切に答えます。

Select a framework: Svelte
Select a variant: TypeScript

※ SvelteKit は今回使いませんでした。
単純な Web コンポーネントであれば、特に使わなくても良いと思います。

起動

npm run dev

カウンターが起動します。

Webコンポーネント化する

このカウンターを、Webコンポーネントとしてビルドをできるようにします。

実際の商用コードはカウンターとは違うものですが、今回は例としてカウンターのコンポーネントを使います。

Svelte コンポーネントを Web コンポーネントとして書き出すには、既に良い記事がいくつかあります。

SvelteではじめるWeb Components開発 - Qiita

src/main.ts の内容を下記の1行に修正

export * from './lib/Counter.svelte'

src/lib/Counter.svelte の最上部に1行追加

<svelte:options tag="my-counter" />

ルートディレクトリの index.html の内容を書きに修正

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<my-counter></my-counter>
<script type="module" src= "/src/main.ts"></script>
</body>
</html>

vite.config.ts を以下のように修正します。

import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'

// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: './src/main.ts',
name: 'my-counter',
fileName: (format) => `my-counter.${format}.js`,
formats: ['es'],
},
},
plugins: [svelte({
compilerOptions: {
customElement: true,
},
}),]
})

formats については、 ブラウザ用の JS を作るか、サーバ用の JS を作るかでいくつかの形式を指定できます。
デフォルトでは esumd で、umd はサーバサイドでの実行に使うものです。今回は、ブラウザ JS だけあれば良いので、es のみにしています。

独自のコンポーネントを開発する

今回は、開発したプロダクションコードの具体的な内容は割愛します。

開発サーバを実行

npm run dev

ビルド

動作が確認できたら、JS ファイルをビルドします。

npm run build

dist/my-counter.es.js が作成されます。

成果物のテスト

ビルド成果物を確認するには、プロジェクトのルートディレクトリに

<my-counter></my-counter>
<script type="module" src= "dist/my-counter.es.js"></script>

の内容を含む HTML を作り、ダブルクリックで起動すると良いと思います。

npm run preview というコマンドもありますが、dist ディレクトリ以下に index.html が必要で、
dist ディレクトリはビルドするたびに消えてしまうのでコンポーネント開発では使いづらそうです。

実際に他のサービスからコンポーネントを使う際も、上記のようなスクリプトタグで使うことができます。その際、 script タグに type="module" を忘れずに指定します。ビルドしたコードには、ファイル直下に $ といった短い名前の関数が作られます。そのため、type="module" なしでグローバル領域に読み込んだ場合、既存のスクリプトで jQuery 等を使っている場合に誤動作につながります。

なお、type="module" をつけた場合、そのスクリプトは CORS 判定の対象となりますので、サーバ側は access-control-allow-origin の HTTP ヘッダを必ず返す必要があります。

S3の設定

本番環境で使えるようにするにするため、ビルド成果物の JS を、S3 にデプロイして静的サイトホスティングの機能でホスティングします。

バケットの命名の注意

S3 のバケットを作る際は、バケット名をサービスを提供する URL のホスト名と完全に同じにする必要があります

CloudFront で、CloudFront Functions を使ってリクエストヘッダーを操作する場合、S3に伝わる Host ヘッダーは必ずリクエストしている Host ヘッダーとなり、 Functions 内で変更することは制限されておりできないためです。

静的ウェブサイトホスティングの設定

Hosting a static website using Amazon S3 - Amazon Simple Storage Service

S3 の「プロパティ」タブの一番下から設定します。

バケットポリシーの設定

「アクセス許可」タブ内にバケットポリシーの設定欄があります。

パブリックでの読み取りを許可するポリシーにします。

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::world-visitor.torico-corp.com/*"
}
]
}

CORSヘッダーの設定

「アクセス許可」タブ内の一番下に Cross-Origin Resource Sharing (CORS) の設定欄があります。

[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]

ファイルのデプロイ

ビルド成果物を S3 にアップロードします。

接続国により2つのファイルを出し分ける設計とするため、今回のビルド成果物の他に、空の JS も別のファイル名でデプロイしておきます。

CloudFront の設定

オリジン

先程作成した、S3 の WebホスティングのURLをオリジンとします。

接続国に応じてリクエストヘッダーを付与する設定

CloudFront の「オリジンリクエストポリシー」という機能で、リクエスト国別のヘッダを付与することができます。

CloudFront の ポリシーページの、オリジンリクエストのカスタムポリシーで、「オリジンリクエストポリシーの作成」を選択し、追加するヘッダーを選べます。

Adding the CloudFront HTTP headers - Amazon CloudFront

CloudFront-Viewer-Country を追加すると、ヘッダーの値として2文字の国名コードが追加されます。「JP」等。大文字になります。

リクエストヘッダによってオリジンに要求するファイルのパスを変えるファンクション

付与したリクエストヘッダーを CloudFront Functions で判定し、リクエストパスを変化させます。
今回はこのようなスクリプトになりました。

なお、handle の引数の event の構造はこのページで紹介されています。

CloudFront Functions event structure - Amazon CloudFront

/*
接続元の国によってリクエストパスにプレフィックスをつけるファンクション
*/

/// 国コードを取得
function getCountryCode(event) {
return event.request.headers['cloudfront-viewer-country']
? event.request.headers['cloudfront-viewer-country'].value
: ''
}

/// 国コード(大文字)から判断するディレクトリプレフィックス
/// SG は初回は必要無いが、検証のために追加
function getDirectoryPrefix(countryCode) {
if(['TW', 'SG'].includes(countryCode)) {
return '/tw';
}
return ''
}

/// uri (パス) はルーティング対象か
// 検証用の index.html はルーティングしたくない。
function uriRootingRequired(uri) {
return ['/my-component.es.js'].includes(uri);
}

function handler(event) {
var countryCode = getCountryCode(event);
var directoryPrefix = getDirectoryPrefix(countryCode);
if (directoryPrefix && uriRootingRequired(event.request.uri)) {
event.request.uri = directoryPrefix + event.request.uri;
}
return event.request;
}

Google Tag Manager でロードする

今回作った Svelte のコンポーネントは、 Google Tag Manager からでも使うことができます。

ただし、Google Tag Manager で独自の HTML タグを書くとエラーとなって公開できないため、
document.createElement でタグを作成しています。

また、script タグに type="module" 忘れずに指定します。これをつけないとグローバル変数領域にロードされてしまうため、既存のスクリプトに悪影響があります。

そして、type="module" を入れた場合、なぜか「document.writeをサポートする」にチェックが入ってないとスクリプトが動作しないため、チェックを入れます。

【合格体験記】3週間でAWS Certified Solutions Architect - Associate (SAA)を取得した話

エンジニアの高津です。

8/6にCLF、8/26にSAAに合格したのでそれについて書いていこうと思います。

なぜ受けようと思ったのか

会社からの推奨もあって4月に基本情報技術者試験を受けたのですが、

「基本情報くらいノー勉で受からなきゃ話にならん」

といきがってノー勉で受けたところ見事にあと1問のところで午前試験で落ちてしまいました。

運が悪かったと思いつつもあまりにも不甲斐ない結果で終わってしまったので流石に何か他の資格でもとっておこうと思い、色々検討した結果、今まで個人開発等で触ったことがありかつ弊社でも採用しているクラウドサービスであるAWSの資格試験は非常に馴染みが深く障壁が小さいと思い挑戦しようと思いました。

また普段使う機会のないサービスを学んだり体系的な知識を身につけるという面でも今後にとってプラスになるのではないかと思い受験しました。

勉強開始前のスペック

  • 個人開発で基本的なサービス(EC2,S3,Route53,RDS,IAM,CloudFront etc)は触ったことがある
  • 実務では前のインターン先で少し触ったくらい

CLF受験

CLFは巷では「簡単すぎて受ける価値が無い」などと言われていたりもしますが、SAAの勉強をいきなり開始するのは少し腰が重いと思って受けることにしまいした。結果的にCLFを最初に受けたことでCLF合格→SAA合格までのハードルがかなり低く感じて非常に良かったと思っています。

また、合格特典でSAA受験費用が半額になるのも非常に良いです。

勉強方法

AWS認定資格試験テキスト AWS認定 クラウドプラクティショナー

AWS認定資格試験テキスト AWS認定 クラウドプラクティショナー という参考書を1周して脳にある程度インデックスを貼ってからUdemyの模擬試験問題集をひたすら解きました。

このときに学んだことが土台となってSAAの勉強にもかなり生きた実感はあります。

結果としては797点(700点が合格ライン)で100点近く余裕を持って合格できました。

SAA受験

CLFと同様まずは参考書で体系的な知識をインプットしようと思い**AWS認定 ソリューションアーキテクト − アソシエイト教科書という参考書を1周しました。**

CLFの勉強で使用した参考書のSAA版である**AWS認定資格試験テキスト AWS認定ソリューションアーキテクト - アソシエイトよりも図を用いて丁寧に解説されていたので頭に入ってきやすかったです。しかし、これ1冊で合格するのはCLFと同様難しいのでUdemyの過去問をひたすら解きました。**

CLF合格後、通勤の時間で軽く勉強する程度でまったりと勉強していたのですが、8月下旬にSAA02→SAA03に改定されて試験範囲が広くなることを知り慌てて1週間前に申し込んで追い込みました。

勉強する時間が全然確保できなかったので正直厳しいと思いましたがなんとか合格できました。

改定される前に取得できて本当に良かったです。

資格を取得して思ったこと

資格を取得しても何か特別な能力が身についたり給料が急に上がったりはしませんが、AWS全般的に自身がついたと思います。今までは必要にかられて少し勉強する程度だったのでAWSへの苦手意識もあり、そこまで自信はなかったです。しかし、取得後はプライベートでも気楽にAWSを触れるくらいハードルが下がり、会社内で飛び交うAWS関連の会話も難なくついていけるようになりました。車の免許を取得しただけで日頃から運転しないと運転できないペーパードライバーであるようにAWSの資格も取得しただけでは形だけのものになってしまうのでしっかり手を動かしてAWSを触り続けることは非常に重要です。資格を取得するだけではさほど意味は成さないと思いますがAWSを触るきっかけとしては資格取得は悪くないと個人的に思いました。

今後も余裕があればDVAやSOAも取得して4冠を目指して行きたいと思います。

AWS RDSで大量のデータを削除する時等に、性能劣化を避けるために確認すべき項目(クレジット残高)

RDS で、大量のIO を伴う処理を行うと、処理途中で性能が大きく劣化することがあります。
不要になった大量の過去データをバッチで削除する時によく発生します。

これは、ストレージへの IO を規定量より高い頻度で行った時に減少するクレジットがあり、それが 0 になった時、パフォーマンスに制限がかかってしまうためです。

制限のかかった状態では通常通りのサービス運用はできなくなってしまうため、過去データ削除などの高負荷のバッチは、RDSのモニタリングページを見ながら注意深く実施する必要があります。

今回は、パフォーマンス低下を避けるために確認すべき RDS クレジットのメトリクスについて書きます。

各メトリクスの詳細は、AWS の公式ドキュメントに詳細な解説があります。 

高負荷な処理を行う際に確認すべきメトリクス

EBS Byte Balance

AWS のドキュメントによると、「RDS データベースのバーストバケットに残っているスループットクレジットの割合。」とのこと。データ転送量で減少していくのでしょうか。重い SQL を実行することで減ることがあります。

0になるとパフォーマンスが大幅に劣化します。

クレジットを消費するような SQL を実行しなければ、自動的に回復します。

EBS IO Balance

AWSドキュメントによると「RDS データベースのバーストバケットに残っている I/O クレジットの割合。」とのこと。広範囲の DELETE 文など、 IO が多く発生する SQLを実行するとどんどん減っていき、0になるとパフォーマンスが大幅に劣化します。クレジットを消費するような SQL を実行しなければ、自動的に回復します。

Burst Balance

AWSドキュメントによると「汎用 SSD (gp2) のバーストバケット I/O クレジットの利用可能パーセント。」プロビジョンド IOPS の RDSには項目がありません。GP2 ストレージの場合に、広範囲の DELETE 文など、 IO が多く発生する(IOバーストがされる) SQLを実行すると減っていきます。

CPU クレジット残高

T系インスタンスの場合にあります。CPUを多く使う(CPUバーストがされる)処理を行うと減っていき、0になるとパフォーマンスが大幅に劣化します。T系のEC2とお使いであれば、おなじみの項目ですね。

バイナリログのディスク使用状況とリードレプリカのレプリケーション遅延

リードレプリカをマスターより低い性能で運用している場合、マスターで行った処理を同じ速度で処理することができず、レプリケーション遅延が発生することがあります。

この時、リードレプリカが処理できないバイナリログはマスターの RDS に蓄積されるため、バイナリログのサイズが溜まっていき、リードレプリカの遅延もどんどん大きくなります。

解消させるにはマスターの処理のペースをリードレプリカに合わせて減らすしかありません。

状況に気づかずに大きな遅延とバイナリログができてしまった場合、遅いリードレプリカの処理を待つより、一度リードレプリカを削除し、新たに作り直したほうが時間短縮になる場合があります。

まとめ: 高負荷な SQL を発行する時に気をつけること

クレジット消費で一番やりがちなのは DELETE 文で多くのレコードを消そうとした場合です。また、その DELETE で変更されたストレージを最適化させるための OPTIMIZE TABLE でも多くのクレジットが消費されることがあります。

DELETE を行う場合、量にもよりますが、一発で済まそうとせず、範囲を絞って何度か実行するような SQLでメンテナンスしたほうがトラブルを避けられます。また、範囲指定もインデックスを使うようにし、意図しない広範囲のロックを避ける必要があります。

OPTIMIZE TABLE は範囲を絞ることはできないため、メトリクスを監視しながら、クレジットが 50% を切るようであれば停止を検討したほうが良いでしょう。

OPTIMIZE TABLE に限らず、10分以上かかるSQL の場合は、AWSコンソールから上記 EBS Byte BalanceEBS IO BalanceBurst BalanceCPU クレジット残高バイナリログのディスク使用量またリードレプリカの遅延 のメトリクスを常に監視し、クレジットの使い切りを発生させないようにしてください。

実務経験0 入社一年目のエンジニアが任されたセキュリティの話

2021年春に入社しました、情報システム部の清瀬です。

TORICOの情報システム部では、セキュリティの向上を上半期の目標に掲げておりました。上半期も終わりに近づいてきたということで、新卒で入社した私がどんなセキュリティ向上のために携わってきた業務を記事にしようとおもいます。多くのシステムでは Django を使用しているためコード部分は Django 前提の話になります。

1. インフラレベルでの対応

AWS WAF の導入

  • SQLインジェクション対策
  • クロスサイトスクリプティング
  • その他、包括的な対策

TORICOでは一部の社内用アプリを除き、全てのサービスをAWSでデプロイしています。それら全てのサーバを悪意のあるリクエストから守るために、AWS WAF を導入しました。

AWS WAF は2019年に大幅アップデートされ、今までのものは WAF Classic と名称を変更しました。新しいWAFの最大のメリットは、るAWSによって作られたルールを簡単に導入できる点です。例えば AWSManagedRlesCommonRuleSet では、OWASPに記載された主要な脆弱性・リスク10個をカバーしてくれています。クロスサイトスクリプティングや悪意のあるボットを排除してくれるルールをボタン一つで導入できるため、包括的なセキュリティ対策をすることができます。もちろん、今までのようにオリジナルのルールを作ることができます。そのため、社内からのアクセスには一部のルールを緩和させたりなどもできるようになっています。

余談ですが、WAFの導入の検証を担当したのは、まだ私がインターン1週間くらいの時でした。検証のために簡易的な Django アプリを Ec2 にデプロイしてWAFの導入でクロスサイトスクリプティングやSQLインジェクションがきちんとブロックされるのかを検証しました。インターンなのにこんな重要そうな仕事をさせていただけるのかと驚いたのを今でも覚えています。

過去に使用していた IP アドレスのルーティングを解除

これはセキュリティとは少し違う話かもしれませんが、ドメインの整理などもしました。一部の未使用のドメインが、既に手放してある IP アドレスと結びついたまま放置されていました。もし IP アドレスが悪意のあるサイトに使われていた場合、最悪ユーザーを悪意のあるサイトに誘導してしまうということも起こり得ます。そうならないためにも、不要なドメインを整理しました。

2. アプリレベルでの対応

サービスのAdmin ・社内アプリ を社内からのみ許可する

  • 不正なアクセス対策
  • 個人情報の保護

こちらは Django の Middleware を使って対応しました。サブネットマスクの表現方法などが複数あるため IP を取り扱うのは面倒くさそうだなと思っていましたが、python には ipaddress という便利なモジュールが標準で搭載されています。このモジュールを使うことで簡単に実装することができました。このタスクは上記の WAF でも実装できますが、1つのALBで複数の社内アプリを制御していたためアプリレベルで対応することにしました。アプリレベルのため、各アプリに適した制限を導入することも簡単にできます。

ログインセッションキーを適切な設定にする

  • セッションハイジャック対策
  • CSRF対策

大事なセッションを管理してくれるクッキーにはセキュリティ対策は欠かせません。TORICOの全てのサイトで、その大切なCookieの設定が適切になるように対策をしました。具体的には HttpOnly Secure SameSite=Lax  の3つの設定です。これらの設定を簡単に説明すると、

HttpOnly: JavaScript からセッションにアクセスできないようにする。設定されていなかった場合、JS 経由で セッション情報が漏洩する危険がある。

Secure: https 通信でのみ セッションを送るようにする。設定されていなかった場合、暗号化されない Http 通信でセッションが送られてしまう危険がある。

SameSite: Strict, Lax, None の3つの設定がある。Lax の場合、別オリジンからの POST の時はセッションを送らない。None の場合、例えばECサイトの届け先を変更するフォームのある偽サイトからユーザーのセッションを使ってデータが送信されてしまう。

Django では 2系以上であれば settings で設定することができます。1系の場合は SameSite を設定することができません。クッキーセッションの注意点として、あまりにも厳しい設定にした場合、サービスがうまく動かなくなる可能性があります。特に SameSite は決済部分でエラーがでることがあるそうです。

ファイル出力時に追跡IDとログを残す

  • 情報漏洩元の究明

TORICOではできるだけ業務を自動化するために、いろいろな情報をCSVなどに出力します。万が一そういったデータが社外に流出した時、どのデータが、どこから流出したのかを特定しやすいように追跡IDをそれぞれのファイルに追記するようにしました。ログにはリクエストの情報を全てを保存しているため、いつ・誰が・どの検索ワードで CSV を出力したのかを追うことができます。 Django には DB へのログ出力機能はないため、DBに出力する場合には自作する必要があります。

3. データベースの対応

重要なフィールドの暗号化

  • 個人情報の保護
  • データ漏洩時の影響を最小限にする

重要なフィールドを可逆暗号化することで、万が一情報漏洩した時でもその影響を最小限に留められるようにしました。可逆暗号は Django のオリジナルのフィールドを作ることで簡単に対応することができました。具体的な実装方法についてはこちらの記事で書きました。また、一部の古い社内アプリではアカウントのパスワードがハッシュ化されていなかったため、 bcrypt という方法で不可逆暗号化もしました。

ソルトとペッパーの役割の違いなどが曖昧だったため、とても勉強になったタスクだったなと思っています。

最後に

上半期に行った主要なセキュリティ対策を紹介していきました。セキュリティは深刻な障害の原因になるため、普段のコーディングから気をつけていきたいと思います。

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