ServerlessでCloudFrontプライベートWebサイトを提供する

# シナリオ

- コンテンツはS3へアップロードし、CloudFront経由で配信します
- 具体的には下記URLアクセスで参照できるようにします
- ただし、アクセスするためには署名付きCookieが必要とします
- Cookieなしの場合は閲覧できません

https://example.com/index.html

- 下記URLへアクセスした場合は5分間有効な署名付きCookieを発行します
- さらに、上記URLへリダイレクトします
- つまり、このルートでのみコンテンツを参照できることになります

https://example.com/enter

### ポイント（何がしたかったのか）
- 署名付きURLだと単一ファイル（コンテンツ）の応答しかできない
    - Webページの場合、1ファイルで構成するのは現実的ではない
- 署名付きURLだとURLが長くなる
    - パラメータたくさんなのが美しくない
- 動的に生成したい
    - 期限付きにしたい（ある程度コントロール可能にしておきたい）
    - しかし、署名用サーバーを設置するほど大げさにしたくはない

# 構成

自前構成のWebサーバーは設置せずに実現します

- CloudFront
- S3
- LambdaEdge

# 教材ソース

- https://github.com/shinsaka/aws-cf-signedcookie

これに沿った説明にします

```sh
$ git clone https://github.com/shinsaka/aws-cf-signedcookie.git
$ cd aws-cf-signedcookie
```

## 必要なもの
- AWSアカウント
    - ルートユーザーでAWSコンソールへログインできる必要があります
- 手元環境はLinux想定です（私はWSL Ubuntuで実行しています）
- 教材を使う場合
    - githubへアクセス必要です
    - gitコマンドが必要です
- 手順中でAWS CLIを使います（コンソールでも可）
- [Serverless FrameWork](https://www.serverless.com/) を使います
    - nodejsが必要になります

# デプロイ

## コンテンツをS3へアップロード

下記のような構成とします

```
/index.html
/403.html
/css/style.css
```

バケット作成してコンテンツをアップロードします

```sh
$ aws s3 mb s3://aws-cf-signedcookie-test
make_bucket: aws-cf-signedcookie-test
```

```sh
$ aws s3 sync www/ s3://aws-cf-signedcookie-test/
upload: www/403.html to s3://aws-cf-signedcookie-test/403.html
upload: www/index.html to s3://aws-cf-signedcookie-test/index.html
upload: www/css/style.css to s3://aws-cf-signedcookie-test/css/style.css
```

## CloudFrontで配信設定

AWSコンソールからCloudFront→Distributionを作成します
署名付きCookieしたいので `Restrict Viewer Access(Use Signed URLs or Signed Cookies)` を Yesにするところがポイントです
Deployedまで10分～15分ぐらいかかると思います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/bf466ebc-051e-97db-d190-143e7d34e81f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/b34c83d5-81a9-d007-462c-ae6e16c23649.png)


参考ページ: [CloudFrontの署名付きURLでS3にアクセスする方法](https://qiita.com/jeayoon/items/c148a8637a177c0f8c9f)
参考ページ: https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#private-content-creating-oai

## CloudFront エラーページを設定

アクセスできなかった場合に表示するエラーページを設定します
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/2a2d57fa-b172-7092-42c9-26f32b7e8aeb.png)

## CloudFrontキーペア設定

下記ページの手順通り、ルートユーザーでログインし、CloudFrontキーペアを作成します
- [AWS マネジメントコンソールで CloudFront キーペアを作成するには](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs-procedure)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/a16356ff-6cb4-150b-383f-301b1fc71b3a.png)

- ダウンロードしたプライベートキーファイルを教材ディレクトリへコピーします

## 設定ファイル記述

`sample_settings.json` を `settings.json` というファイルにコピーして編集します

```json
{
    "keypairId": "APKXXXXXXXXXXXXX",        キーペアIDです。プライベートキーファイル名と同じはずです
    "privatekeyFile": "pk-APKXXXXXXXXXXXXXXXXX.pem",    プライベートキーファイル名
    "resource": "https://dxxxxxxxxxxxxx.cloudfront.net/*"  デプロイしたCloudFront URL+* （署名付きCookieを保持している状態でアクセスできるパスになります）
}
```

## Lambda Functionをデプロイする
- （注意）この仕組みではプライベートキーファイルをLambda内にデプロイします
- LambdaEdgeにするため、必ず `us-east-1` へデプロイします（教材では serverless.ymlで指定してあります）

```sh
$ serverless deploy --pkfile pk-APKXXXXXXXXXXXXXXXXX.pem
```

## CloudFront Behaviors作成

`/enter` というパスでアクセスされた場合にLambdaを動作させるための設定を行います

- Path Pattern
    - `/enter`
- Lambda Function Associations
    - CloudFront Event
        - `Viewer Request`
    - Lamda Function ARN
        - 上記でデプロイしたLambda関数のARNをバージョン付きで設定します

LambdaFunctionの設定部分はLambda側の画面からも操作可能です
設定すると、またCloudFrontはデプロイを始めるので、完了するまで待ちます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/9ca60bda-3c52-3d93-9a9f-df10a293d842.png)

# 動作確認

## 直接アクセス

- アクセスできません

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/e05c9b05-08cd-412f-4f6b-147a30106431.png)

## 正規入り口からアクセス

https://***.cloudfront.net/enter へアクセスするとページが表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/8f67f33e-ba34-9a9a-4281-7a5d2a8d4844.png)

Cookieも来ています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/1fb42f6b-af2e-8708-e11e-0f752541da42.png)

5分経過後、リロードすると「アクセスできません」ページが表示されます

# curlで確認

## 直接アクセス
- 403エラー

```
$ curl -I https://dxxxxxxxxxxxxx.cloudfront.net/index.html
HTTP/2 403
content-type: text/html
content-length: 161
date: Sat, 18 Jul 2020 09:03:02 GMT
last-modified: Sat, 18 Jul 2020 07:13:32 GMT
etag: "6cadxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
accept-ranges: bytes
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 19069xxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: 67Bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
age: 362
```

## 正規入り口からアクセス

- Set-Cookie
- 302リダイレクト
- location: /index.html
 
```
$ curl -I --cookie-jar cookie.txt https://dxxxxxxxxxxxxx.cloudfront.net/enter
HTTP/2 302
content-length: 0
server: CloudFront
date: Sat, 18 Jul 2020 09:06:53 GMT
location: /index.html
set-cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0_; Path=/; Secure; HttpOnly
set-cookie: CloudFront-Key-Pair-Id=APKXXXXXXXXXXXXXXXXX; Path=/; Secure; HttpOnly
set-cookie: CloudFront-Signature=KxgiTZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX__; Path=/; Secure; HttpOnly
x-cache: LambdaGeneratedResponse from cloudfront
via: 1.1 adxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: fxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

- cookie付きでリダイレクト先へアクセス
- 200正常応答

```
$ curl -I --cookie cookie.txt https://dxxxxxxxxxxxxx.cloudfront.net/index.html
HTTP/2 200
content-type: text/html
content-length: 531
date: Sat, 18 Jul 2020 09:07:04 GMT
last-modified: Sat, 18 Jul 2020 07:13:33 GMT
etag: "fdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: hxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

# まとめ

- できるだけ簡単にするためにかなり手抜きしていますが、一応やりたいことは実現できたと思います
    - `/enter` で許可していますが、実際はユーザーごとの長めのハッシュ文字列を想定しています
- LambdaEdgeは制限が多いのですが、別の(not Edge)Lambdaを実行することはできますので、他のサービスとAPI連携などすることはできそうですね
    - 5秒制限、Layers使えない、環境変数使えない等
    - 長時間かかる処理は避けるべきとは思います