素人基盤エンジニアがDockerでDjango REST Frameworkとreactを触るシリーズ④：CloudFront-S3

[素人基盤エンジニアがDockerでDjango REST Frameworkとreactを触るシリーズ③：reactの導入](https://qiita.com/LittleGreenMen/items/902ef02fb7d3914e5562)のつづき。
①からみたい場合はこちら。[素人基盤エンジニアがDockerでDjango REST Frameworkとreactを触るシリーズ①：Django](https://qiita.com/LittleGreenMen/items/f60c5a8c5138ddaaed79)の導入

# reactアプリのS3への配置
前回、reactとDjango REST FrameworkをEC2インスタンスで構築し、連携ができていることを確認したので、今回はreactで作成したフロントエンド部分をS3に配置し、CloudFrontで配信する形に変更する。
まずは、S3バケットを作成していく。
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/12ece77d-12a1-71c6-005d-16f40b885294.png)
![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/d7d8c175-73c2-0c29-d05f-91291aaf72ce.png)
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/651153d6-c6da-047d-f5e9-0f4eb52043f9.png)
![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/567bdc54-8ca5-6817-2e26-90b12de48d90.png)

バケットが作成できたので、reactをビルドし、AWS CLIを利用してバケットにビルドしたソースコードを配置する。※AWS CLIを利用する際は、aws configureコマンドで適切なIAMクレデンシャルを設定する。（IAMクレデンシャルの設定については割愛する）
また、reactで一部IPアドレスを書いていた部分があったが、CloudFrontの構成に変更すればポートはCloudFrontがルーティングしてくれるので、パスのみの記載に変更する。


```js:src/App.js(20行目と31行目を変更)
import React, { Component } from 'react';
import axios from 'axios';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            todos: [],
            hostname: ""
        };
    }

    componentDidMount() {
        this.getTodos();
        this.getHostname();
    }

    getTodos() {
        axios
            .get('api/todos/')
            .then(res => {
                this.setState({ todos: res.data });
            })
            .catch(err => {
                console.log(err);
            });
    }

    getHostname() {
        axios
            .get('api/hostname/')
            .then(res => {
                this.setState({ hostname: res.data });
            })
            .catch(err => {
                console.log(err);
            });
    }

    render() {
        return (
            <div>
                {this.state.todos.map(item => (
                    <div key={item.id}>
                        <h2>{item.title}</h2>
                        <p className="item_body">{item.body}</p>
                    </div>
                ))}

                <div className="box30">
                    <div className="box-title">HOSTNAME</div>
                    <p>{this.state.hostname.hostname}</p>
                </div>
            </div>
        );
    }
}

export default App;
```
yarn buildでビルドを実行する。

```shell
yarn build
```
すると、frontendの下にbuildというディレクトリができる。

```txt
frontend
∟build
∟node_module
∟package.json
∟public
∟README.md
∟src
∟yarn.lock
```

これをディレクトリごと先ほど作ったS3バケットにコピーする。※適切なIAM権限を付与している前提

```shell
aws s3 cp ./build s3://<your-bucket-name>/ --recursive 
```
GUIで対象のバケットを見てみると、ちゃんとオブジェクトが配置されていることがわかる。
![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/a9d63ad0-ce01-44d0-eba3-1cbb5b5c51df.png)

# ALBとWAFの作成
CloudFrontの、多数のエッジサーバからアクセス先へ通信が発生するという性質上、アクセス先（オリジン）ではセキュリティグループによってIPアドレスを制限することが難しい。
そのため、今回は立てているEC2サーバの前段にALBを配置し、CloudFrontで付与したヘッダを持つアクセスのみを通過させるようなWAFをアタッチすることによって、直接ALBへアクセスする経路を防御する。
まずはターゲットグループを作成する。
![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/536efa35-7ae0-1571-0891-47b9a84ae673.png)
![007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/91ca0dd2-d08e-9c32-a647-8ee89fd1c12e.png)
適当な名前をつけて、ポートをDjangoの待ち受けポートである8000に指定する。
VPCは、Djangoが動いているEC2サーバと同じVPCにする。
![008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/0d30e618-f750-26c0-e5b5-6574a0f637b6.png)
![009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/d82c5297-0955-223e-b696-0b5c236e728f.png)
Djangoが動いているEC2サーバをターゲットとして登録する。EC2サーバにチェックをいれて、[Include as pending below]を押下。
![010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/2e1e5152-e541-bef8-b88d-23c87851cd16.png)
すると下のTargetに追加されるので、これで作成を押す。
![011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/0136e00f-a074-afc8-02cf-bd8f77f9cad8.png)
ターゲットグループが作成できた。
次は、ALBを作成する。
![012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/24ef131e-b3c9-ed79-0e92-7d7370349f1d.png)
![013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/8fddd340-b1fe-2b5a-3433-26a4839f1275.png)
![014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/24b1ad42-4a79-4789-28ed-8f8c7102dfdc.png)
今回は、ロードバランサーのポートもDjangoの待ち受けポートと同じ8000番にする。
![015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/72423f43-6184-1f0e-3e23-bf5c5a1f865e.png)
VPCとサブネットを選択する。VPCはEC2サーバと同じVPCで、サブネットはInternetGWへのルーティングルールがあるパブリックサブネットを選択する。
![016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/06d3f1ac-5b9c-0e12-3a30-cb83365cae26.png)
![017.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/bf0bb523-e282-92f5-f3ac-a01a331a3eee.png)
セキュリティグループを選択する。セキュリティグループは、インバウンドは8000に対して0.0.0.0/0で開放していて、アウトバウンドは制限しないものをALB用に作成しておき、それを選択する。
また、EC2サーバにアタッチされているセキュリティグループは、このセキュリティグループからの8000番ポートのインバウンドアクセスを許可する用に変更しておく。
![018.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/3ac1d6e7-9a23-6aa3-ff8d-58fd7b947d9f.png)
ターゲットグループは、先ほど作成したターゲットグループを指定する。
![019.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/878e91c8-dffc-2d85-e865-9a15af7a3241.png)
![020.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/6b123e20-c2f0-33d1-4145-6a40e7c91a8f.png)
![021.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/1b58860e-12cf-3029-214c-a18256e18b9e.png)
これでロードバランサーまで作成された。
次にWAFを作成する。
今回は特にclassicを利用したい要件もないので、新しいほう(WAF v2)で作成していく。
![022.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/fa2ca535-3f3b-9d4b-2395-d3c761fc09d2.png)
![023.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/a58e1e35-0faf-757f-6c4b-9b39aac45e39.png)
WebACLを作成する前に、リージョンをALBを作成したリージョンに合わせておく。自分は東京リージョンで作成しているので、東京を指定。
![024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/160963d5-aa70-b168-af4d-8eabeae9de2e.png)
![025.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/c6611887-3691-a7ff-7b55-4575074cc1bf.png)
適当な名前を付ける。
![026.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/3abcd28e-d421-a1c9-51ef-6e15601fc1ef.png)
![027.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/1fc10c47-8377-df42-cb81-34e323b507bf.png)
リソースとして、先ほど作成したALBを登録する。
![028.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/4729dfd4-359f-cce0-96ed-4873de0c0dcf.png)
![029.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/1a9da96a-e88e-be10-c8e5-593d7be83792.png)
![030.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/2202f236-53e4-6f1b-3f79-af2bb9efa3fd.png)
ルールを作成していく。
次の通りに指定する。
If a request : mathces the statement
Inspect : Header
Header field name : x-pre-shared-key
Match type : Exactly matches string
String to match : sample-string
これは、リクエストのHeaderを検査して、x-pre-shared-keyというフィールドがsample-stringとなっているかどうかを調べるルールという意味。
x-pre-shared-keyやsample-stringはもちろん変えてもらってもいいが、その場合は後述のCloudFront作成時に付与するヘッダも同様に変更してやる必要がある。

![031.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/7c1d3725-f16b-586a-205b-828fb10380d8.png)
このルールに合致したアクセスのみを通したいので、ActionはAllowにする。
![032.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/2c361d03-a4c2-8e42-2114-462117061c11.png)
ルールに合致しなかったアクセスははじきたいので、Default ActionはBlockにする。
![033.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/4af77182-4bf5-3957-bf27-e58e72c11aae.png)
![034.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/ec53ce24-82c2-1c09-d7f3-9e27b418fcf8.png)
![035.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/300e7b6a-f8ce-62bb-c582-f27f8728af03.png)
![036.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/d055e1e8-fefa-95b0-7d60-4fb9ff0bd701.png)
![037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/a63f919e-4322-2252-0acb-8e3ec1f80147.png)
ここまでで、ALBとWAFが作成できた。

# CloudFrontの構築
今回の肝となるCloudFrontを作成していく。
![038.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/10c824a7-6112-c4ec-7d23-cca3ccc5aada.png)
![039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/b0c28737-45af-ae6c-d155-72958f5adec8.png)
![040.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/96508ca0-693a-b383-bc3d-ef4ef83c9e1b.png)
Originの部分には、一番最初に作成したS3バケットを指定する。ただ、ここで注意したいのが、Origin Domain Nameのところを選択すると出てくるドロップダウンリストで選択できるs3のドメイン名は、\<bucket名>.<font color="Red">s3</font>.amazonaws.comというドメイン名になっており、今回はここを、\<bucket名>.<font color="Red">s3-ap-northeast-1</font>.amazonaws.comに変更してやる必要があるということ。
リージョン指定なしのドメイン名では、CloudFrontの伝播が完了するまではS3のURLにリダイレクトされる仕様になっているため、S3を公開せずにCloudFrontで表示させたい今回のような場合、最初の1日間はうまく表示ができなくなる。リージョンを指定する形に変更すれば、この問題を解決することができる。
![041.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/0f35facc-9928-7a85-5c9f-4d49d78628c0.png)
CloudFront⇒S3間はOrigin Access Identityという仕組みを使って、CloudFrontからのみS3を表示できるような構成とする。CloudFrontから設定をする場合は、赤枠で囲まれた3つのオプションを有効にするだけで、OAIの作成とS3のバケットポリシーの更新をやってくれるので、楽に設定が可能。
![042.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/7cdb88d5-de79-86d3-865b-329f3d3c0871.png)
デフォルトBehaviorのキャッシュの設定としては、今回はキャッシュを持たせないようにしたいので、Managed CachingDisabledを選択する。
![043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/e9c33866-b977-4224-a5af-8e9116a6b350.png)
あとは、Distribution SettingsでDefault Root Objectにindex.htmlを指定し、ディストリビューションを作成する。
![044.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/ba0bc2fe-4c7e-bb57-a293-4a7ecf4b9819.png)
これでディストリビューションのベースは完成したので、細かい部分を追加していく。
作成されたディストリビューション名をクリックすると詳細画面に遷移することができる。
![045.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/9792cd2d-7978-064e-83f5-47a8653d20ea.png)
![046.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/75130ab3-c094-810f-e8c7-ae731553dd01.png)
Originとして先ほど作成したALBを追加したいので、Create Originを押下
![047.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/b3ed2443-c4e1-661e-719a-40d6fd2d4dc8.png)
Origin Domain Nameには、作成したALBを指定する。ここは、素直にドロップダウンリストから選択してしまってOK。
HTTP Portは、ALBのリスナーポートと合わせる必要があるので8000番を指定する。
![048.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/04ed5788-30d3-1488-0231-d34c50005b04.png)
また、origin Custom Headersでは、WAFで指定したHeaderのフィールド名と値を入力する。
この手順通り進めている場合は、
Header Name : x-pre-shared-key
Value : sample-string
を入力する。
![049.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/2fac893f-193b-e297-82fd-c3d5bfae5535.png)
これでALBもOriginとして追加することができた。
今度は、Behaviorsの設定を追加して、どんなパスでアクセスが来た時にALBにアクセスをルーティングするかという部分を追加していく。

|Path Pattern|api*|admin*|static/admin*|static/rest_framework*|
|---|---|---|---|---|
|Origin or Origin Group|ALBOrigin|ALBOrigin|ALBOrigin|ALBOrigin|
|Allowed HTTP Methods|GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE|GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE|GET, HEAD|GET, HEAD|
|Cache Policy|Managed-CachingDisabled|Managed-CachingDisabled|Managed-CachingDisabled|Managed-CachingDisabled|
|Origin Request Policy|Managed-AllViewer|Managed-AllViewer|Managed-CORS-CustomOrigin|Managed-CORS-CustomOrigin|


この下から、スクリーンショット3枚分は、↑の表に書いてある列の数だけ繰り返し実施する。スクリーンショットは、代表してapi*のパスパターンの場合の構築時のスクリーンショットを掲載している。
![050.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/377b95dc-88f6-a184-bb83-3af21ffa2630.png)
![051.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/b7e3ddbf-9cd5-76b6-4056-a9ef98708078.png)
![052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/60bbad29-3c81-0aa5-c90b-853bff9ad2bb.png)

表のとおり、4つのbehaviorを追加すると、下図のようになる。
![053.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/695376c4-b49c-9346-175b-3e5444ac9bac.png)

![054.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/b9c86101-15f6-7469-77ae-52f18239d345.png)
最後に、Error Pagesというタブに移動して、Error時の挙動を追加する。
![055.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/4fd3b596-0bb0-1ee6-d555-7f16b50be22e.png)
403エラーが出た場合は改めて/のページに飛ばすような設定をいれる。
![056.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/f3fece73-c5af-9b15-ebe4-1bdf94a78ea2.png)
以上で設定は完了なので、EC2サーバに戻ってdocker-compose upでDjango REST Frameworkを立ち上げた後、

```shell
docker-compose up
```

CloudFrontのドメイン名にアクセスをすると。。。
![057.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/9801b250-6201-1f9c-964c-87b51a91e562.png)
無事、想定通りの画面が表示された。
今は表示をさせるだけのアプリになってしまっているので、今後はTODOを登録する用のテキストボックスや、Delete機能を追加していこうと思う。

つづく