Amazon S3でSPAをサクッと公開する

# はじめに
ストレージサービスとして有名＆優秀なAmazon S3ですが、実は「静的ウェブサイトホスティング」という機能を使うことで、Vue.jsやReactで作ったSPAを簡単に公開することができます。また、AWS CLI を使用することでコマンド一発でサクッとデプロイすることができます。~~[heroku](https://jp.heroku.com/)や[firebase](https://firebase.google.com/?hl=ja)などのPaaSが充実している昨今、あまりS3でやるメリットない気もしますが。~~今回はその手順についてまとめてみました。

（ちなみに私がVue.jsをよく使うのでちょくちょくVue.jsが登場しますが、Reactでも同様の操作ができるはずなので、適宜読み替えていただければと思います。）

# お値段
まず一番大事なお金の話から。
S3の料金は「利用したストレージの容量」「リクエスト件数」「リクエストに対するデータ送信量」の3軸で計算されます。

| ストレージ料金 |  |
|:-------------:|:-----------:|
| 最初の 50 TB/月 | 0.025USD/GB |

| リクエスト料金 |  |
|:-------------------------------:|:-----------------------------:|
| PUT、COPY、POST、または LIST リクエスト | リクエスト 1,000 件あたり 0.0047USD |
| GET、SELECT および他のすべてのリクエスト | リクエスト 1,000 件あたり 0.00037USD |

| データ転送料金 |  |
|:-------------------------------:|:-----------------------------:|
| 1 GB まで/月 | 0.00USD/GB |
| 次の 9.999 TB/月 | 0.114USD/GB |

※[料金 - Amazon S3 | AWS](https://aws.amazon.com/jp/s3/pricing/)より一部抜粋

例えば、配信するコンテンツのデータサイズが1MB、リクエスト件数が100件/日の場合、
ストレージ料金：0.000025USD
リクエスト料金：0.00111USD
データ転送料金：0.228USD
で、合計約 **0.229USD/月** （日本円で約 **25.19円/月**）です。ばちくそ安いですね。

アカウント開設から1年以内の無料枠を利用すればほぼ0円に抑えられると思います。
※参考 - [AWS クラウド無料利用枠](https://aws.amazon.com/jp/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc)

# SPAの準備
各フレームワークでのビルドを実行し、
`index.html`と各フォルダ（css/js/img等）が揃っている状態にしましょう。
<img width="273" alt="スクリーンショット 2019-11-12 9.28.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/c0772a14-64eb-c386-3663-4e9aa0ee2541.png">

ちなみにVue.jsでのプロジェクト開始方法およびビルド方法はこちらの記事（[Vue CLI スタートガイド](https://qiita.com/kiyokiyo_kzsby/items/5e27a25b358e82dd408f)）にまとめてありますので、フロントフレームワークを全く触ったことないという方はこちらを参考にしてみてください。

# S3用のIAMユーザ作成
### AWSルートアカウントの作成
AWSを初めて使うという方はAWSルートアカウントを作成しましょう。
こちらを参考にすると良いと思います。
[AWS アカウント作成の流れ | AWS](https://aws.amazon.com/jp/register-flow/)

### S3用のIAMユーザの作成
ルートアカウントは全権限アカウントのため全てのAWSリソースへのアクセスができてしまいます。このルートアカウントで作業を続けることはセキュリティ上よろしくないので、S3のみ使用可能なIAMユーザを別途作成し、今後の作業はこのS3用IAMユーザで行います。

まずルートアカウントでログインし、マネジメントコンソールからIAMへ移動します（検索バーで「iam」と打てば出てきます）。
<img width="953" alt="スクリーンショット 2019-11-10 22.48.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/34a2d749-43f8-f337-f4e1-772ff8a273cd.png">

「ユーザー」メニューを選択すると、作成したIAMユーザの一覧が表示されます。
今回は新規でユーザを作成するので、青色ボタンの「ユーザーを追加」をクリックしましょう。
<img width="1065" alt="スクリーンショット 2019-11-10 22.52.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/45c34af9-7661-f5fc-ad64-adc7fe3cf725.png">

IAMユーザを作成するための設定画面が表示されます。

- ユーザー名は任意の名前で構いません（公開しようとしているSPA用のIAMユーザであることが分かるネーミングだと良いです）
- 「AWS マネジメントコンソールへのアクセス」にチェックを入れ、ログインパスワードを設定しましょう
<img width="903" alt="スクリーンショット 2019-11-10 23.05.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/08bf7672-e980-be3b-c202-ee9648e5ddcc.png">

- 「既存のポリシーを直接アタッチ」から、`AmazonS3FullAccess`を選択し、チェックを入れましょう
<img width="903" alt="スクリーンショット 2019-11-10 23.10.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/77d0e3f4-96fa-e2b9-7da3-0e66827c9db7.png">

それ以外はデフォルトの設定で問題ありません。

IAMユーザの作成が完了すると一覧に表示されるので、問題なく作成されているか確認しましょう。
<img width="1071" alt="スクリーンショット 2019-11-10 23.19.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/29b68c8b-ece5-50d9-4840-45b8e5f67b75.png">

IAMユーザの作成が完了したら、早速ログインしてみましょう。
右上に`IAMユーザ名 @ アカウント名`と表示されていれば問題なくログインできています。
<img width="1400" alt="スクリーンショット 2019-11-11 9.57.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/38ff5f0a-b1ef-df67-ad88-ae8e5f5cfe6a.png">

S3以外のサービスへのアクセスがブロックされているか確認するために、試しにIAMを開いてみましょう。
先ほどIAMユーザを作成した画面に移動しても「アクセス権限が必要です」と表示され、ユーザを新規作成できないようになっているはずです。
<img width="1218" alt="スクリーンショット 2019-11-11 10.01.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/8089a37f-1400-8d0b-d422-5bb9cdb29afd.png">

このように、利用用途ごとにIAMユーザを作成してAWSサービスへの権限を切り分け、意図しない操作が実行されないようにしましょう。

# S3でSPAを公開
先ほどログインしたS3用IAMユーザで作業を進めます。

### バケットの作成
S3の画面へ移動し、青色の「＋バケットを作成する」ボタンをクリックしましょう。
バケット作成に際しての設定画面が表示されるので、情報を入力しましょう。

- バケット名：任意の文字で構いません。ここで設定したバケット名が後ほど静的ホスティングする際のURLの一部として使われるので、それっぽい名前を付けましょう。
- リージョン：これも任意で構いませんが、「アジアパシフィック（東京）」を選択するのが無難です。
<img width="1086" alt="スクリーンショット 2019-11-11 10.23.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/46b70442-ea26-179a-4fa4-cee99278c3dc.png">

それ以外の設定は一旦デフォルトのままで問題ないです。

バケットが作成されるとバケット一覧に表示されるようになります。
<img width="1157" alt="スクリーンショット 2019-11-11 10.31.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/01f3f10f-63f1-de9c-5108-bb7e965274ea.png">

### コンテンツのアップロード
バケット名をクリックするとバケットの詳細画面に遷移することができます。
青色の「アップロード」ボタンをクリックし、公開したいSPAの各ファイル（index.htmlとcss/js/imgフォルダ等）をローカルからアップロードしましょう。
初回アップロード時にバケットの設定について色々と聞かれますが、全てデフォルトで問題ないです。
無事アップロードが完了するとこのような画面になると思います。
<img width="1361" alt="スクリーンショット 2019-11-11 10.49.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/e9dc7190-4600-1c4a-60d4-1586943ce3c2.png">

### 静的ウェブサイトホスティング機能の設定
「プロパティ」タブへ移動し、「Static website hosting」の設定を行います。

- 「このバケットを使用してウェブサイトをホストする」にチェックを入れます
- 「インデックスドキュメント」に`index.html`を指定します
<img width="990" alt="スクリーンショット 2019-11-11 10.52.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/18d6afa9-5991-cbab-d54b-0f938ef4a1cc.png">

上記の設定が完了したら「保存」ボタンをクリックしましょう。

なお、この画面で表示されている「エンドポイント」のURLがウェブサイトへのアクセス用URLになります。が、今の状態でこのURLにアクセスしようとしても403エラーが返ってきてしまいます。URLでのアクセスを許可するために、次の章で説明するバケットポリシーを設定しましょう。
<img width="905" alt="スクリーンショット 2019-11-11 11.00.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/da93c9b7-00e7-6248-558d-edf09d9ff29b.png">

### バケットポリシーの設定
S3に格納したオブジェクトが不用意にネットに晒されないよう、デフォルトでは外部からS3バケットへのアクセスは全て拒否するようになっています。先ほど403エラーが返ってきたのもそのためです。正しくWebページを表示させるためにはURLによる外部からのリクエストを明示的に許可する必要があります。

アクセス制御に関することは「アクセス制御」タブで行います。

まず、「ブロックパブリックアクセス」を開きます。「編集」をクリックし、以下の設定を行います。

- 「パブリックアクセスをすべてブロック」のチェックを外します
- 下から2つ目、「新しいパブリックバケットポリシーを介して...」のチェックを外します
- 一番下、「任意のパブリックバケットポリシーを介して...」のチェックを外します
<img width="1321" alt="スクリーンショット 2019-11-11 22.28.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/c4305a5e-3ad9-c253-cbc9-8c97733b1787.png">

これでバケットポリシーによるアクセス許可設定が有効になります。ここの設定を行わないと、いくらバケットポリシーで許可の設定を行ってもブロックされてしまうので注意しましょう。

次に「バケットポリシー」を開き、エディタ欄に以下のJSONを**バケット名を置き換えて**貼り付けましょう。
このJSONは`sample-hosting-kiyokiyo`バケットへのGetリクエストを許可するバケットポリシーです。[AWS公式チュートリアル](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/HostingWebsiteOnS3Setup.html#step3-add-bucket-policy-make-content-public)のものをそのまま抜粋しました。バケット名の部分のみ、自分が作成したバケット名に置き換えるのを忘れないようにしましょう。

```json
{
   "Version":"2012-10-17",
   "Statement":[{
 	"Sid":"PublicReadForGetBucketObjects",
         "Effect":"Allow",
 	  "Principal": "*",
       "Action":["s3:GetObject"],
       "Resource":["arn:aws:s3:::sample-hosting-kiyokiyo/*"
       ]
     }
   ]
 }
```

これで外部からS3に格納したファイルを取得できるようになりました。
先ほど403エラーが返ってきたURLでアクセスし直すと、今度は正しくWebページが表示されるようになっているはずです。
<img width="867" alt="スクリーンショット 2019-11-11 22.45.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/03ec016f-a4c0-c908-a41b-5a1a8bb7b9d3.png">

# S3へのデプロイコマンドを作る
上記の手順でWebページの公開はできるようになりましたが、Webページを更新するたびにIAMユーザでログインし、S3バケットに格納してあるファイルを削除して、ローカルにある新しいファイルをアップロードし直す、というのはかなり面倒です。Vue.jsでは
`npm run serve`でローカルサーバーを起動し、
`npm run build`でビルドを行うことができます。
それと同じノリで、
`npm run deploy`で、S3へのデプロイができるよう設定を組みましょう。

### AWS CLI を使用する
AWS CLI を使うと、ターミナル等のコマンドラインツールからAWSサービスを操作できるようになります。これを利用して、S3上の対象バケットにあるファイルを削除し、ローカルにある新規ファイルをアップロードするスクリプトを組みます。

こちらの記事でAWS CLI を使うための手順がまとめられているので参考にしてみてください。
[【初心者向け】MacユーザがAWS CLIを最速で試す方法 | Developers.IO](https://dev.classmethod.jp/cloud/aws/mac-aws-cli/)

かいつまんで説明しますと、

１：まずpipをインストールします（Python3.4以降であればPythonのインストールと同時に使えるそうです）。`pip -V`と打ってバージョンが表示されれば問題ないです。

```terminal
$ pip -V
pip 19.3.1 from /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/pip (python 3.7)
```

２：次にAWS CLI をインストールします

```terminal
$ pip install awscli
```

３： **ルートアカウント**でS3用IAMユーザのアクセスキーとシークレットアクセスキーを生成・取得します。IAMメニューで先ほど作成したS3用IAMユーザを選択し、「認証情報」タブの「アクセスキーの作成」ボタンをクリックします。アクセスキーとシークレットアクセスキーが表示されるので、手元に控えておきましょう。
<img width="978" alt="スクリーンショット 2019-11-11 23.24.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/7eae2c80-a49c-d270-eee1-47420c5326c0.png">

４：ターミナルに`aws configure`と入力しS3を操作するための設定を行います。`Access Key ID`と`Secret Access Key`に先ほど取得した情報を入力しましょう。`Default region`はS3バケットで指定したリージョン（アジアパシフィック（東京）の場合はap-northeast-1）を入力しましょう。`Default output format`はとりあえずtextで問題ありません。

```terminal
$ aws configure
AWS Access Key ID [None]: XXXX
AWS Secret Access Key [None]: XXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: text
```

動作確認として、`aws s3 ls`でS3に登録しているバケットが一覧表示されればOKです。

```terminal
$ aws s3 ls
2019-11-11 22:45:22 sample-hosting-kiyokiyo
```

### デプロイ用のシェルスクリプトを組む
index.html等が格納されているディレクトリを`dist`とします。
distと同じ階層にデプロイ用のスクリプトを記載した`deploy-s3.sh`を配置します。
ディレクトリ構造のイメージはこんな感じです。

```
(any directory)
 ├dist/
 │　├css/
 │　├img/
 │　├js/
 │　└index.html
 └deploy-s3.sh
```

`deploy-s3.sh`の中身はこんな感じで書きます。

```deploy-s3.sh
#!/bin/sh

aws s3 rm s3://sample-hosting-kiyokiyo/ --recursive
aws s3 cp dist s3://sample-hosting-kiyokiyo/ --recursive
```

1行目はシェルスクリプトを走らせるためのおまじないです。詳しく知りたい方はこちらの記事（[#!/bin/sh は ただのコメントじゃないよ！ Shebangだよ！](https://qiita.com/mohira/items/566ca75d704072bcb26f)）とかが参考になると思います。
2行目ではsample-hosting-kiyokiyoバケットの中身を再帰的（--recursive）に削除（rm）しています。
3行目ではdistディレクトリの中身をsample-hosting-kiyokiyoバケットにコピー（cp）しています。

AWS CLI でできることはこちらの[AWS CLI Command Reference](https://docs.aws.amazon.com/cli/latest/index.html)にまとまっているので、他のスクリプトを走らせたい方は調べてみてください。

### デプロイ用コマンドを作る
最後に、`npm run deploy`と入力したら先ほど作成した`deploy-s3.sh`が呼び出されるようにします。
npm runコマンドは`package.json`の`scripts`ブロックで設定できます。
`"deploy": "bash deploy-s3.sh"`をscriptsブロック内に追加しましょう。

```package.json
{
  （省略）
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "deploy": "bash deploy-s3.sh"
  },
  （省略）
}
```

これで作業としては完了です。
試しにローカルでの変更をS3にデプロイできるか確かめてみましょう。

まず、ローカルで適当に変更を行います。
サンプルとして今回はとりあえずApp.vueのHelloWorldタグを以下のように変えてみます。

```html
（変更前）
<HelloWorld msg="Welcome to Your Vue.js App"/>
（変更後）
<HelloWorld msg="Hi! My name is Kiyokiyo! Nice to meet you!"/>
```
ビルドコマンドを走らせます。

```terminal
$ npm run build
```
これでローカルのdistディレクトリ以下に必要なファイルが揃いました。

最後にデプロイコマンドを走らせます。

```terminal
$ npm run deploy
```
S3上のファイルがdeleteされ、ローカルのファイルがS3にアップロードされたことが、ターミナルの出力からも分かると思います。

WebページのURLにアクセスするとしっかり変更が反映されていますね。
<img width="871" alt="スクリーンショット 2019-11-12 9.17.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/eda7b257-3ae8-a3b2-286c-2fa4a136e705.png">

# おわりに
これでローカルで作成していた静的ウェブサイトを公開できるようになりました。
デプロイもコマンド一発で簡単にできるようになったので、開発速度もかなり向上したんじゃないでしょうか。

個人的な今後としては、LambdaやDynamoDBを利用したサーバーレスAPIとの通信にチャレンジしてみたいと思います。