Nuxt.jsとAWSで招待状webページを作ったまとめ

# 概要
諸事情でパーティ招待状Webページをつくってみることにしたのが始まりです。

とりあえず無事にパーティも終わったので、開発時の記憶をさかのぼりながら残す備忘録ですが
申請から開発まで一通り殴り書くので、何かしら参考になれば幸いです。┗(^o^)┛
…ちょっと前の記憶を掘り起こしながらやるので、間違って書いてそうなところもある気がしますが温かい目で御覧ください :bow: 

開発環境はMacなので、Winの方は適宜読み替えていただけると幸いです :pray: 
AWS上で日本語表示できている部分は、日本語の画面で説明してる…はずです
# 成果物

### webページ
ざっくり3つの画面構成です。（3つ目はフリー素材やOSSの情報なので割愛）
GoogleMapや開催日を記載したホーム画面と、実際に参加者の情報を登録してもらう登録画面の２つです。

| ホーム画面 | 登録画面 |
|:-:|:-:|
|  <img width="325" alt="修正版" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/54f61687-ded0-6dd7-d46b-ee4957ffd9b4.png">| <img width="324" alt="スクリーンショット 2019-03-06 15.38.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/39229/4bc7e4c5-2f47-2aad-2dd7-53f41c8525eb.png">  |

### システム構成
今回のwebページのシステム概要はこんな感じ
<img src="https://qiita-image-store.s3.amazonaws.com/0/39229/285e1c59-5ddd-4280-2af7-5e4c7bc7ad40.png" border="1" width="450px"/>

Route53でドメインをとったうえで、「必要なときに必要な程度稼働してくれる」実運用を考えながら、今回そこまでやる必要はないシステムを作ってます。先人たちの知恵借りまくりです。

### Github
書いたコードは、以下の２つ。

| システム | リンク |
|:-:|:-:|
|webページ|https://github.com/tyabata/web-invite|
|api(lambda)|https://github.com/tyabata/lambda-api-invite|

個人情報とかcommitに入れちゃったのでgitのcommitログだけ消し去ってます :innocent: 

# 採用した技術
関連キーワードの羅列。詳細は次項から説明します。

### Server Side
[AWS](https://aws.amazon.com/jp/?nc2=h_lg)
`Lambda` `DynamoDB` `API Gateway` `CloudFront` `Route53` `S3`

**理由**
ちょっと前に自作IoTでGCP使ったので今回はAWS。
業務でスマートスピーカー開発してたときに触ったけど雰囲気でやってたので
復習でもしようかなという気持ちで選択しました。

### Client Side
[Nuxt.js](https://ja.nuxtjs.org/)
`Vue.js` `Vuex` `Vuetify` `TypeScript` `axios` `PWA` `PostCSS`

直近の業務で、React + Reduxを使っていたので今回はVue。
上記にいろいろ陳列してますが`nuxt-ts`でほぼ全て用意してるので、PostCSS以外はだいたいコマンド一発。それが今年の1月末の頃…
**nuxt-tsは2019/4/5ぐらいにnuxtに統合され 導入方法が変わっています**
 https://github.com/nuxt/nuxt.js/releases/tag/v2.6.0

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">Nuxt v2.4.0 is finally here 🎉<br>Official TypeScript support, smart prefetching and many more features &amp; bug fixes.<a href="https://t.co/Ljf29xYvXi">https://t.co/Ljf29xYvXi</a></p>&mdash; Nuxt.js (@nuxt_js) <a href="https://twitter.com/nuxt_js/status/1089948593392365568?ref_src=twsrc%5Etfw">2019年1月28日</a></blockquote>

「nuxtのconfigにtsの設定いれるのつら」
と思いながら格闘して環境構築完了した後日(2019/1末頃)、Nuxt公式が上をツイートしてて`nuxt-ts`を知り、結果的にはほぼ何もせず 「TypeScript」で「Vue+Vuex」を作る環境ができました。

**型はいいぞぉ**
型により構造の把握が楽になるし、ちゃんと書けてれば静的に問題に気付けるし以下略

# 開発手順概要
とりあえずAWSで登録をすませます
https://aws.amazon.com/jp/register-flow/

複数人で開発するならIAMとかで管理アカウントと分けましょう。と言いたいとこですが
今回は一人で かつ お仕事ではないので端折ります。

## Server Side
まずは「参加者情報を登録するAPI」「Webサーバの代わりにs3を使う」という流れ

1. `Route53`でドメイン取得 -> `Certificate Manager`でSSL証明書取得
2. `s3`準備 (linkのみ紹介)
3. `aws-cli`の導入
4. `Lambda`準備から`DynamoDB`にデータ登録まで
5. `API Gateway`と`Lambda`を接続
6. `CloudFront`で`S3`と`API Gateway`のマルチオリジンにバックポストする際の設定

必要なとき以外、見る必要も見られることもない招待ページなので
コンピューティング時間を減らして省エネ運用の構成をとって…いるように見せかけてやたら色々準備したのは勉強がてら実際に使うことを考えた構成を目指してみたという具合です。

## Client Side
Nuxt.jsを利用してページを開発 -> index.htmlを出力してS3にアップロードするまで
を以下の手順で説明していきます。

1. `nuxt-ts`で開発できる下準備
2. ページを作る
3. index.htmlを出力する
4. S3にアップロードする

# 開発詳細 : Server Side
ここからが本題です

## ドメイン取得と証明書作成まで

### Route53でドメイン取得

まずはドメインを取得します。
[新しいドメインの登録 - Amazon Route 53](https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/domain-register.html)

> - https://console.aws.amazon.com/route53/ でRoute53を開く
> - 登録方法
>   - 初めて : [Domain Registration] の [Get Started Now]を選択 
>   - 二回目以降 : [Registered Domains]を選択

画像は省きますが、基本は同じ。
リンク先の説明にならって作業をすれば、自分のドメインがつくれます。
トップドメインによってお値段が異なるので、今回は安価でよくみる `.net`を選択

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/e0485db6-f841-4ec8-cdf2-53e079c5a55f.png" width="450px" />


- トップドメイン以降の自分でほしい名前を入力
- チェックの結果がOKであれば、それをカートに入れる (`Add to cart`)
- 連絡先などを入力して進むと登録完了
- 登録したドメインが [Domain registration in progress]の状態からしばらく待つと[All Contacts]になれば完了。SOAレコードとかも同時に作成済みの状態になります。

ドメイン作成はここまでですが、**これだけは登録したタイミングで課金が発生**します。

### Certificate Managerで証明書作成
さすがにwebエンジニアとしてhttpのリンクで友人だけでなく、嫁の知人含めて招待ページ登録してねー。と公開するのは社会の窓全開でご挨拶してる気がするのでサボらずちゃんと作成します。

初回は 
[Provision certificates(証明書のプロビジョニング)] => [Get Started(今すぐ始める)] => [Request a Certificated(署名書のリクエスト)]
と選択していくと以下のような画面になると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/2fafa09d-eabd-be0d-3e82-1455a5d36194.png)

ドメイン名の入力欄に先程登録したドメインを入れましょう。
ここではワイルドカード証明書のリクエストもできるので、私は `*.hoge.net`のような名前で証明書を作成しました。
この後、進めていくとドメイン所有者（つまり自分）に下記のようなメールが飛びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/2f980c6d-8640-398d-bacd-6ab97fc0edf7.png)

DomainやAccountIDや取得したRegionなどに問題がなければ、メールに記載されている
`To approve this request, go to~`と書いてあるあたりのリンクから遷移して承認完了させます。
下記のような画面まで行けば、証明書の作成まで完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/cc4b3725-a030-18a7-0fc7-c2776b01bb8b.png)

実際に証明書を設定したりするのはCloudFrontあたりを扱う項へ。

## S3の準備
ほか項目含めて全部書くと、やたら長いドキュメントになるので備忘録としてリンクだけ。
[S3 バケットを作成する方法 - Amazon Simple Storage Service](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/user-guide/create-bucket.html)

バケット作成後に追加で設定したものは

- バージョニングの有効化 + ライフサイクルから旧バージョンに対する削除の設定
    - 本番リリース後に問題発覚して戻すことがある場合、バージョン指定で戻せる
    - しかし一度動いてしまえば1日以上たっても変わらない
- cliコマンドからのデプロイするために対象アカウントのみ書き込みを有効化
- それ以外の全ユーザーに対してはReadのみ有効

**ライフサイクルの設定**については、作成したバケットの上部にある「管理」タブから
[ライフサイクルルールの追加]という項目からできます。
上記通り、一時的なロールバックを考慮してバージョニングの有効化をしたので
一日以上たった過去バージョンは削除する。というライフサイクルを設定しました。
（結果的に不要でした）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/f1084abe-5eb2-b906-ee3f-17fe63258073.png)

**アクセス権限**については、CloudFrontからアクセスが前提なので
本当は全ユーザー有効設定ではなくCloudFrontからのアクセスに対してReadを与えるような設定が良いと思います。
…色々調べながらやってたので、全ユーザーがreadできる方が都合がよかったのです… :bow: w
（といってもさすがにURLは公開してないです）

とりあえずこのタイミングでは、表示確認のための`index.html`に適当になんか書いたものをバケットのrootにおいといてください。

設定についてはこちらを参考にしてみてください。
[CloudFront ディストリビューションからのみ S3 バケットへのアクセスを許可する](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudfront-access-to-amazon-s3/)

アクセス権限は適切にね！！

## aws-cliの準備
Lambdaやs3にデプロイをするために、AWS用のコマンドラインツールである`aws-cli`からデプロイする準備をします。

### IAMでユーザーの作成
aws-cliでアクセスする際に使うユーザー設定を行います。
admin使ってもできるんですが、お勉強とお作法的に分けます。

下記リンク等を参考に必要な情報を作成します。
[最初の IAM 管理者のユーザーおよびグループの作成 - AWS Identity and Access Management](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/getting-started_create-admin-group.html)

[IAM](https://console.aws.amazon.com/iam)から、左カラムのナビゲーションメニューから `ユーザー`を選択し、上部にある`ユーザーを追加`を選択します。
すると下の画像のように、ユーザーを追加するための設定画面が出てくるので、下の表のように設定していってください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/a18730b5-3769-d6a4-49ec-b1e05abb45b9.png)

|項目|設定|補足|
|---|---|---|
|ユーザー名|cli|※なんでもOK|
|アクセスの種類|プログラムによるアクセス||
|グループの作成|**あとで説明**|既存のポリシーを直接アタッチすることもできます|
|タグ|なし|IAMの管理用です。個人開発で特にいらないので今回は省略|

ここまで入力すると、確認の表示が出てくるので問題なければ次に進むと以下のようにアクセスキーとシークレットアクセスキーが取得できます。
どこかにメモっておきましょう。あとのcli設定に使います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/6a7e214d-2ed5-0501-a999-2ae97ad06b12.png)

#### IAMの概念(雑まとめ

グループ作成について説明をしていきます。
AWSに初めて触ると `ロール` `ユーザー` `グループ ` `ポリシー`といろんな言葉がでてきて
チンプンカンプンになる（な気もしてる）ので、私の雑まとめです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/f3adc747-f42c-5fbc-6660-73f8b3e2995c.png" width="500px"/>

- ポリシーは 「**AというポリシーはDynamoへのRead権限をもつ**」といったルール的なもの
- ユーザー、グループ、ロールはそれぞれポリシーをアタッチして使う
- ユーザーやグループは人の管理に使う
- ロールはシステムで使う

という雰囲気理解です。とりあえず関係性が雰囲気でもわかればOKかと。

上の画像で線を足し忘れましたが、ユーザーにポリシーをアタッチできます。
が、複数で開発する場合は「開発」というグループをつくってユーザーをそこに紐づけていくことで、わざわざ個別にポリシーをアタッチする手間を一つにまとめられる利点等があると思います。

- Lambdaにデプロイするには `AWSLambdaFullAccess`ポリシー
- S3にデプロイするには `AmazonS3FullAccess`ポリシー

といった具合で今回は

|項目|設定値|
|---|---|
|ユーザー名|cli|
|グループ名|deployment|
|ポリシー(グループに対して)|AWSLambdaFullAccess,AmazonS3FullAccess|

を設定しています。

「勉強だしとりあえず全権限ふっておけ」といった場合は
`AdministratorAccess`を設定すればOKです。
**仕事でやるなら用法用量はまもりまｓ（ｒｙ**

### aws-cli導入
brewで入れました。brew update行ってinstallから始めていきます。

```
brew install awscli

[~] aws --version                                            13:29:19
aws-cli/1.16.80 Python/3.7.1 Darwin/18.0.0 botocore/1.12.70
```
次にアクセスキーの設定をします。

```
[~] aws configure                                            14:52:40
AWS Access Key ID [****************XXXX]:
AWS Secret Access Key [****************XXXX]:
Default region name [us-east-1]:
Default output format [None]:
```

`Access Key ID`や `AWS Secret Access Key`は先程取得した値を設定してください。
regionはとりあえずTokyo(ap-northeast-1)とかでもいいと思います。
Outputは `json`や`Text`とかありますが、これはご随意に（説明略）

```
[~] aws s3 ls                                                14:52:50
2018-12-24 20:36:54 　hoge.xxxx.net
```
といったように確認コマンドで結果が返ってくればOKです。

## LambdaへのdeployとdynamoDBの設定
とうとうコードがでてきますが、API側はだいぶシンプルに書いたつもりなので概要だけ。
https://github.com/tyabata/lambda-api-invite

### APIのロジック

- Lambdaに対してリクエスト
    - `/invitees/user`へのGET と `/invitees`へのPUTを処理する
    - 実行例外は拾って、500を返すようにしている
- `/invitees/user` : GET
    - ページを初めて開いたときにアクセスされる。サーバ側でもつパスワードでcryptoした文字列をUserIDとして返す。UUIDは`crypto-js`を利用して時間から生成してる
    - ページ側がlocalstorageで保存しているので、消さない限りは再リクエストは発生しない（が、消されると新しいユーザーとして再度UUIDを発行します）
    - webページ用にサーバを建てないようにした結果。こうなった :innocent: 
-  `/invitees` : PUT
    -  ページに登録した情報とUserIDをセットにして登録リクエスト
    -  UserIDが正しく複合できるか確認
    -  問題なければ、登録情報が正しい値かチェックする
    -  すべてOKであればDynamoDBに登録

という流れ。

特に嫁側の方に登録時のハードルを上げないようにしたかったので、下を意識してざっくり作りました。

- ログインせずに登録できる。
- 一度画面閉じたあとに登録情報を更新できるようにidをもたせておきたい
- 私の友達が **絶対~~いたずら~~デバッグする**ので簡単に登録APIを叩かれないようにした


### lambdaへのデプロイ
シンプルなAPIで aws-cliもいれたので、package.jsonに以下のように記述

```
"zip": "zip lambda.zip -r node_modules src",
"first": "aws lambda create-function --function-name <lambdaに登録するfunction名> --zip-file fileb://lambda.zip --region <登録するlambdaのregion> --handler src/index.handler --runtime nodejs8.10 --role <登録するfunctionにアタッチするrole>"
```
これは**新規登録用**。nodeとかは古いので適当に置き換えてください :bow: 
アタッチしているroleについては後で説明します。

コードができたら `zip化` -> `first`でアップロード&function作成
をしています。

```
"deploy": "npm run zip && npm run upload",
"upload": "aws lambda update-function-code --function-name <lambdaに登録したfunction名> --zip-file fileb://lambda.zip --profile cli --publish",
```
こちらは**更新用**
一つ上のzip化するコマンドにあわせて、awsにアップロードしてfunctionを更新する`upload`コマンド。
そして、それらを一発で行うための `deploy`コマンドです。

ここは特別に複雑なことはしてないです。
前項の`aws-cli導入`で`AWSLambdaFullAccess`がポリシーとしてアタッチされている状態であれば、これで新規登録や更新が完了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/09393ff3-78da-8c66-cd43-4ee507d9a709.png)
Lambdaで登録したregionで開くと、上の通りに登録されていることが確認できると思います。

そしてLambdaにアタッチしているroleですが、 `CloudWatchにログを流す` `DynamoにアクセスしてR/Wする`といったポリシーをふったroleをアタッチしています。

詳細説明は省きます :bow: 
ただ、Lambdaのログを流すために設定しておくことで、問題があったときにも気づけるので登録しておきましょう。
[Amazon CloudWatch Logs とは - Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html)

### DynamoDBへ登録

[DynamoDB](https://ap-northeast-1.console.aws.amazon.com/dynamodb)にデータを登録していく準備をします。
上記のリンクから`テーブルの作成`を選び、テーブル名やプライマリキーを設定して作成を押します。
今回は、前項あたりで説明した UserIDをプライマリキーに設定するため、`uid:文字列`として設定しました。
…うろ覚えですが、コレ以上はDynamoDBでの設定はなかったはずです。

コード上ではここらへん
https://github.com/tyabata/lambda-api-invite/blob/master/src/dynamo.js#L30

普通のDBみたいにテーブル定義がどうとか行わなくても、データをputすればそのobjectの要素通りにカラムを自動で作ります。そこらへんはお手軽。

ただ、PUTするときに空文字とかを入れようとするとエラーになるので、チェックして弾いてあげるか
設定されていないときのデフォルト値が必要であれば、登録前に足してあげましょう。

```
const AWS = require('aws-sdk');
// 登録先のregionを設定する
AWS.config.update({
  region: 'us-east-1'
});
const docClient = new AWS.DynamoDB.DocumentClient();
docClient.put(
   {
　　 // テーブル名
    TableName: "table名をここに",
    Item: item
   },
   (error, data) => { // 割愛
```
コード概要はこんな感じです。登録などに必要な権限的なものは、実行しているLambdaにアタッチされているロールのポリシーに依存しています。

前項でIAMでユーザーを作ったときと同様に

- IAMの左カラムの`ロール`から`ロールを作成` 
- ロールを使用するサービスとして Lambdaを選択
- ロールにアタッチするポリシーとして `AmazonDynamoDBFullAccess`をセット
- タグは任意(管理用です）
- 最後にロール名とロールの説明を書いて完了。

ちなみにFullAccessはいらないぜ。といった場合は、他にもロールがあるのですが適切なものがない場合は独自でポリシーを作成することもできます。が、これもここでは省略します。

### 動作確認
とりあえずここまでちゃんと設定できているか確認のためにテストしてみましょう。

直接リクエスト送るには、この先のAPI Gatewayへの登録などが必要になります。
しかし、Lambdaのテストを使って、試しに処理を実行させることは可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/0bbe6cad-f3cc-12cc-1577-1ddd1dba7db2.png)
Lambdaの関数の画面から、右上のテストボタンの左にあるプルダウンを選択し`テストイベントの設定`を押します。
そこで、画像のようにhandlerが受け付けたときに処理するbodyを用意してあげることで 疎通確認とDynamoへの登録の確認ができると思います。

## API GatewayとLambdaを接続

API Gatewayを使い、そのバックポスト先として先程作成したLambdaのfunctionのARN(Amazon Resource Name)を指定することで、外からhttpリクエストでLambdaを実行できるようになります。

API Gatewayを選択すると、下のように新規作成時の画面が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/655c7414-5c14-aeb5-9eea-5fdfa62eee86.png" width="400px" />

ここでは**REST API**と **新しいAPI**を選択し、API名を入力して作成を開始します。

### APIのパスとメソッドの設定

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/5f70d387-2df7-8001-32a9-162a94eac612.png" width="400px" />
API作成後に上記のような画面になるので `アクション`から`リソースの作成`を選択します。

- リソース名 : 適切な名前
- リソースパス : 今回は`/invitees`

をすることで、リソース一覧にパスが追加されます。
`/invitees`以下にパスを設定する場合は選択した状態で、上のように`アクション`から作成を同じ手順で行います。

リクエストメソッドを足す場合も `アクション`から`メソッドの作成`を選ぶと、一覧に新しいプルダウンが表示されるのでメソッドを選ぶと、下記のような画面が表示されるので
今回は 総合タイプに`Lambda関数`。Lambda関数に **先程作成したLambda functionのARN**を設定してください。

ARNは、Lambdaのfunctionの画面右上にある文字列で
`arn:aws:lambda:us-east-1:111111111111:function:invitees_prod`
みたいな形式のものです。
その他、regionなどはLambdaにあわせて設定して保存を押すと作成完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/a04b5c43-eabc-d982-73ec-ca06d228dc63.png)

### リクエスト/レスポンスの詳細設定

ここから、API Gatewayの詳細を設定していきます。
今回は作ったもののうち、`PUT : /invitees`について説明していきます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/129a458c-975a-f5ca-578a-859d9675dd61.png" width="600px" />

|項目|概要
|---|---|
|メソッドリクエスト|作成したタイミングの値が入ってる。今回はこのまま|
|総合リクエスト|リクエスト情報の制御的なところ。マッピングテンプレートをいじります(後ほど)|
|Lambda|設定されているLambda functionの名前が表示されていればOK|
|総合レスポンス|返すレスポンスを制御するところ。ここに400や500のパターンを追加する(後ほど)|
|メソッドレスポンス|こちらも200, 400, 500の３つを追加しておく|

#### リクエストのマッピングについて

**コレはパッと身よくわからなくてつらかった**ので、ここでは概要だけ。
詳細はリファレンスや先人の知恵をおかりするのをオススメします。
[API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス - Amazon API Gateway](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html)

上記で設定した項目のうち **総合リクエスト**のを選択し、下部にある**マッピングテンプレート**の項目を選びます。

「テンプレートが定義されていない場合」を選択し、`Content-Type`に`application/json`を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/1e1d8006-a496-0508-466b-91aaf9ce1b3a.png)

追加すると、さらに下にテンプレートを入力する欄が表示されるので、今回は下のように入力しました。

```
{
    "method": "$context.httpMethod",
    "body" : $input.json('$'),
    "path" : "$context.resourcePath",
    "headers": {
        #foreach($param in $input.params().header.keySet())
        "$param": "$util.escapeJavaScript($input.params().header.get($param))"
        #if($foreach.hasNext),#end
        #end
    }
}
```
とりあえずわからなくてもいいです。私もドキュメントにならった説明しかできなないです。
拡張したくなったらリファレンスを読みましょう :innocent: 
雑にいうとAPI Gatewayで受けたリクエストを展開してLambdaのコードで受けやすい形に変換してるといったところになると思います。

上のようにテンプレートにはめ込んだものを handlerの第一引数で受け取って処理を実行しています。
https://github.com/tyabata/lambda-api-invite/blob/master/src/index.js#L22

#### レスポンスのマッピングについて
Lambdaがいくらエラーメッセージを返しても、API Gatewayは200を返してしまいます。
そこで、特定の文字列が含まれるときは `400`や`500`で返せるように設定します。

`統合レスポンス`を選択します
<img width="950" alt="スクリーンショット 2019-06-23 18.04.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/f6b94bfe-1e12-4cf1-91b8-20a58e54de00.png">

ココらへんはシンプルに
`.*"status" *: *400.*`という文字列がレスポンスに含まれていれば 400を返す
といった設定です。500も同様。

次に一つページをもどって、全体の画面からメソッドレスポンスを選択し、HTTPのステータスという項目に400や500などを追加します。
これで、リクエストに対して適切なステータスコードを返せるようになります。

### APIのデプロイ
忘れがちですが、これをやらないと反映されないので、メソッド作成等と同様にアクションから **APIのデプロイ**を押しましょう。
デプロイ対象のステージが表示されるので、選択して`デプロイ`を押すことで初めて反映されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/c0def2a2-712a-dcd1-a0db-8f1555a1aa01.png)

…ステージの説明を書き忘れていましたが、コンソールの左にAPIごとにステージという項目があり
ソレを選択して、 `beta`とか`prod`とかステージを分けておくと、開発環境と本番環境を分けておくことができます。

例えば `beta`ステージの場合は、下のようなパスになります。
`https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/beta`

<img width="693" alt="スクリーンショット 2019-06-23 19.31.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/4022c05d-052b-9135-b33c-7b80b0605738.png">
このURLはCloud Frontの設定でも使います。

### 動作確認


最後に動作確認として、メソッドの実行画面の左側にある 雷マークがついた`テスト`を選択してテストを行います。
メソッドテストと表示されている画面に遷移したら、一番したの`リクエスト本文`の項目に
リクエストするbodyの情報をセットします。

右側に実行時のログが表示されます。ただしく設定ができていれば、成功時のレスポンス内容がログに表示されると思います。

## CloudFrontでS3とAPI Gatewayにバックポストする
やっとServer Sideの説明の最後に来ました…！ 総集編？です。

取得したドメインを設定したCloud Frontでリクエストを受け取って、s3からhtmlを返すか、APIにリクエストするかをパスによってバックポストする設定を追加していきます。

### ２つのオリジンの追加

- [Cloud Front](https://console.aws.amazon.com/cloudfront)を開いて、`Create Distribution`から作成
- `Web`の`Get Started`を選択
- `Origin Domain Name`にAPI Gatewayの設定を追加していきます。
- `https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/beta`というURLなので下の通りに設定する
    - **Origin Domain Name** `xxxxxxxx.execute-api.us-east-1.amazonaws.com`
    - **Origin Path** `/beta`
    - **Origin ID**は勝手に入力されるやつのまま
    - **Origin Protocol Policy**は `HTTPS Only`
- Behavior Settingsは以下の通り
    - **Viewer Protocol Policy** `Redirect HTTP to HTTPS`
    - **Allowed HTTP Methods** GETとPUTが選択できる一番下の項目
- Distribution Settings
    - **Default Root Object** : `http:web.xxx.net/`にアクセスしたときに`/index.html`にアクセスするように設定する

何か漏らしてる気がするけど、これで一旦作成を完了させる。
これでhomeに新しく作成されたDistributionが表示される。
作成完了には少し時間がかかりますが、作成が完了したら次に `Create Origin`でs3の設定を足していきます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/53fa3f44-6dab-fa7a-9267-768861f6ca2e.png)

- `Create Origin`を選択
- **Origin Domain Name** でs3のオリジンを選択する(サジェストで表示されます）
- **Origin Access Identity**で `Create a New Identiy`を選択する。(**理想** : こうすることで、s3へのアクセスをcloud frontのみにできる。が、今回はやってないです)

これでs3のオリジンも追加しました。

### アクセスしたパスでバックポストするオリジンを分ける
次に `Origins and Origin Groups`の隣の `Behaviors`タブを選択して
`Create behavior`を追加します。

今回は、jsやcssなどをs3に置くので **API以外のパスはすべてs3に流れるように設定します**

で設定したのが以下の２つのパス

- `Default(*)` : S3にバックポストする
    - **Viewer Protocol Policy** は `Redirect HTTP to HTTPS`
    - **Allowed HTTP Methods**は `GET HEAD`
    - **Cache Based on Selected Request Headers**は whitelist
        - Originとリクエスト情報だけforwardする(しなくてもいいけど）
- `invitees/*` : API Gateway にバックポストする
    - GETとPUTは受け付けられるように
    -  **Cache Based on Selected Request Headers**は whitelist
        -  Originと一応Authorizationだけforwardしてる
        -  キャッシュがじゃまになるAPIしかないので、cacheの設定は問題ないように修正。

### CNAMEや証明書の設定
最初に作った証明書をやっと使うときが来ました。
Distributionの`General`タブから、Editボタンを押し設定を編集していきます。

- CNAME(Alternate Domain Names) : `web.xxxx.net` といった証明書にあわせて自分がつけたホスト名を設定
- SSL Certificate : `Custom SSL Certificate`と書いてるほうを選択して先程作成した証明書を選択する。

ソレ以外はだいたいデフォの設定のままでOK

### おまけ Error Pageを設定
最後におまけで、Error Pageの設定をします。
ほぼ全部のパスがs3に流れるが、適当なパスを入れるとs3のエラー画面が表示されてしまいs3使ってる感がモロバレなので、設定をします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/d12d16ad-d08b-e0a4-fc7c-3d09de00cef0.png)

**Create Custom Error Response**を選択して

- **HTTP Error Code** : `403`
- **Error Caching Minimum TTL** : よしなに
- **Customize Error Response** : `YES`
    - **Response Page Path** : `/error.html`
    - **HTTP Response Code** : `404` 

s3がなにもないパスにアクセスしたときに403を返すので、これで代わりにerror用のhtmlを表示させることができます。

### 動作確認
おつかれさまでした！備忘録的に書いてるので何か漏らしてる気がしますが、
これで問題がなければ 設定したドメインでアクセスしたら s3作成時においた `index.html`が表示でき、APIのパスにアクセスすればAPIの結果が返ってくるようになるはずです。

…もし動かなかったらごめんなさい。 :bow: 
ググれば多分解決できる程度にn番煎じネタではあると思います…たぶん

# 開発詳細 : Client Side
とりあえず一息ついたら次に表示系の開発をしていきましょう。
Serverと比べるとだいぶ楽かもです。

**一応補足**
https://github.com/nuxt/nuxt.js/releases/tag/v2.6.0
この記事は、2019/04/05にv2.6がでるより前に作ったものの備忘録です。

一応 nuxt-tsを削除してnuxtに置き換えてみたので、気になったら下の変更箇所を見てみてください 
https://github.com/tyabata/web-invite/commit/6d99fbbdde76b84b6c8baa4d1c7b73f3a5683afa

## NuxtとTypeScriptの開発環境を構築する
ここは、僕が2019/2頃にやった `nuxt-ts`もすでに古いので、公式や他の皆々様が書いている記事をご覧になる方が良いと思います 
https://ja.nuxtjs.org/guide/typescript/

`create-nuxt-app`で作るところは一緒で下の設定になります。
（対話式で導入するモジュールを選んでいきます）

```
> Generating Nuxt.js project in /Users/xxxxx/Documents/workspace/vscode/sample
? Project name sample
? Project description My sensational Nuxt.js project
? Use a custom server framework none
? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier, Axios
? Use a custom UI framework vuetify
? Use a custom test framework jest
? Choose rendering mode Single Page App
? Author name xxxxx
? Choose a package manager yarn
```

TypeScript対応はここから下を入れてjsをtsに入れ替えていく作業になると思います。

```
yarn add ts-node @nuxt/typescript
```

簡単なページはこれで完結できるのですが
webpackがラップされてる感じなのでアレコレ拡張するのが煩わしく感じることもあると思うので、プロダクトにあわせて使う使わないは判断すれば良いかなと思います。

## ページを作る
といっても、基本はVueとVuexです。

- Nuxtにのっとって書く
- axiosで用意したAPIと通信をする
- Vuetifyで基本的なデザイン作成
- ロジックの分離を意識する

といったぐらいのことしか…やってないかも :thinking: 
とりあえず書きます！

### create-nuxt-appから少しだけ設定を変える

`create-nuxt-app`をつくると、ルートに`store`や`page`といったフォルダができます。
ちょっぱやで作る分にはいいのですが、気になる人（自分含む）は
例えば`src`というフォルダ以下に移したい場合は`srcDir`というフィールドに設定します。

```js:nuxt.config.ts
const config: NuxtConfiguration = {
  srcDir: 'src/',
  mode: 'spa',
...
```

また、cssを書く用にPostCSSを入れたかったので

ここに、ページで使うcssを設定
https://github.com/tyabata/web-invite/blob/master/nuxt.config.ts#L47

そして`nuxt.config.js`のbuild以下に下のように設定を入れました。

```typescript:nuxt.config.ts
  build: {
    extend(config: any, context: any) {
    },
    cssSourceMap: true,
    postcss: {
      plugins: {
        'postcss-import': {},
        'postcss-mixins': {},
        'postcss-preset-env': {},

        'postcss-nested': {},
        // css minify
        csswring: {}
      }
    }
  }
```

これで設定は完了です。このアプリでは`src/assets/postcsss`以下にPostCSSを使ったcss郡がおいてあります。
…post cssをassetは違う気がするな :thinking: 

ちなみに テンプレのvueファイルでPostCSSをつかった記述をする場合は下の通りにstyleタグを足します

```vue:index.vue
<template>
</template>

<script lang="ts">
</script>

<style lang="postcss" scoped>
  @import '@/assets/postcss/invite.css';
</style>
```

### Vuetifyをつかってレイアウトを作る
[Vue.js Material Component Framework — Vuetify.js](https://vuetifyjs.com/ja/)
今回作ったページはほぼほぼVuetifyの力をつかって調整していて、cssを使ったのは微調整ぐらいでした。
Vueでマテリアルデザインに沿ったレイアウトを作るためのいろんな機能を提供してくれるので
基本的にはタグを埋め込んで終わり。という感じです。

一応元Android開発をしていたので、ToolbarとかSnackbarとかきいて何かわかりますが、聞き馴染みのない方には最初苦労するかもしれません（主に検索で）
比較的公式のドキュメントもまとまっている……と思います。

#### ページのコンポーネント構造について

そもそも今回作ったコンポーネントの構造を雑に説明するとこんな感じです。

```vue:layouts/default.vue

<template>
  <v-app>
    <nuxt/>
  </v-app>
</template>
```
上はおまじない的なやつです。vuetifyに欠かせない部分になります。
このテンプレートをベースに、index.vueはつくっています

```vue:index.vue(概略)

<template>
  <v-container>
    <!-- 下タブの選択によってアニメーションしながら切り替えるためのコンポーネント -->
    <v-window /> 
    <v-snackbar />
    <v-bottom-nav />
  </v-container>
</template>
```

レスポンシブなページの対応はだいたいVuetifyがやってくれるのでcssはほぼ何もがんばりません。
また、`v-bottom-nav`などは、ページに下部に固定できたり、カードや検索窓なども提供されているので、アプリっぽいwebページを作るにはとても強力なツールかなと思いました（雑感）

#### コンポーネント紹介
**v-window**

[Windows — Vuetify.js](https://vuetifyjs.com/ja/components/windows)
ページ遷移のトランジションが簡単にできちゃうコンポーネント
![saaaample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/fd9b7dd8-08f3-51d9-8749-5df17807231f.gif)

```vue

    <v-window v-model="activePage" :touchless="true">
      <v-window-item :value="'home'">
        <home/>
      </v-window-item>
      <v-window-item :value="'register'">
        <register/>
      </v-window-item>
      <v-window-item :value="'other'">
        <other/>
      </v-window-item>
    </v-window>
```

デフォルトは、スワイプによる画面遷移が備わっていますが
調整する暇もないし、タブによるアクションのみにするため
`touchless  = true`としています。

あとは上の通りですが、`activePage`に入ってる値によって出す表示を切り替えているだけです。
ページによって要素の高さが違うと思うので、遷移時に気になる場合はscroll位置の調整などもしてみてください。

### ロジックの分離について
今回の要件だけならVueだけでもよいのですが、NuxtでVuexが簡単に入れられるので
意識して実装してみました。

[Vuex とは何か？ | Vuex](https://vuex.vuejs.org/ja/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/85705ade-1082-d5bb-2adc-393a68972d16.png)

公式の図からVuexにおけるフロー図です。
このwebページでは、ducksパターンでstoreを作成しています。
React + Reduxでもそうなのですが、`action type`や`action` や`reducer(Redux)` `mutation(Vuex)`をそれぞれ分けると管理がつらいので、どうせそれぞれが密なものであれば一つのファイルでまとめよう。っていうデザインパターンです。

[erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux](https://github.com/erikras/ducks-modular-redux)

あとは

- Vueでイベントがあれば、Actionを呼ぶ
- Actionごとの処理をする
- 処理後に状態の変更をcommitをしてstateに反映する
- Vue側で新しい状態を反映する

という流れにそって書きました。

それぞれが、役割以上のロジックを持たないようにしましたが
今回はそれとあわせて一つ意識していることについて書いておきます。

#### 「templateは状態の変え方をしらない」ようにする

Smart UIにならないようにしましょうというやつです。
Atomicデザインを目指したり目指さなかったりしても、可能な限りViewで表示以外のロジックを持つべきではないと思います。

例えば Vueファイルのクラス内で、ボタンが押されたときに次のページに遷移するという処理のために

```
 this.$store.dispatch('goToNextPage', current + 1)
```
という書き方をしたこともあると思いますが、一度ここで立ち止まって考えてみましょう。

**本当に次のページは「currentに1を足した値」でいいのか**

「そりゃ1ページの次は2ページでしょ？」といえば普通なのですが、それはあくまで現実の話で
コードに落とし込んだときに、この書き方は「Viewが表示の変え方を**+1すること**と知ってしまっている」状態になります。
もしかしたら、文字列で管理されているかもしれません。

例えばですが、表示系からは「次のページへいく」という振る舞いだけ呼び出し

```ts
 this.$store.dispatch('goToNextPage')
```
そして、Actionで以下のように「次のページは、今のページ + 1である」という振る舞いの詳細を定義する。

```ts

goToNextPage(context: ActionContext<IState, any>, payload: any) {
    const nextPage = context.state.current + 1;
...
```

という書き方で、可能な限りViewからロジックを剥がすことで
そのコンポーネントの再利用性が高まっていくと思います :thumbsup: 


**余談ですが**、ReduxでContainer Componentで書いたとき、connectの第三引数の `mergeProps` を使うことで、テンプレート側に状態を渡さずに dispatchイベントで現在の状態を知る。といった書き方もできるので、「テンプレート側に値を渡してたぜ！」という方はぜひぜひ。

```ts

import { connect } from 'react-redux'
// 色々省略
export default connect(
   mapStateToProps,
   mapDispatchToProps,
   mergeProps // これ
}(component)
```

redux connect mergePropsとか調べれば色々出てくるかと思います！
[react-redux/connect.md at master · reduxjs/react-redux](https://github.com/reduxjs/react-redux/blob/master/docs/api/connect.md#connect)

### デプロイをする
最後の工程です。
**index.htmlの出力** と **s3のアップロード**について書いていきます。

#### htmlの出力
今回はサーバまわりを書いてないですが
`create-nuxt-app`で作っている場合、`/pages`以下のvueファイルの名前にならってエントリポイントが作成されます。

そして、`nuxt generate`コマンドはそのエントリでアクセスしたときに表示されるhtmlをファイルとして出力します。

```:例
/pages
- index.vue
- error.vue
```
↓このようなファイルをおいている場合、上のコマンドを実行すると

```
dist/index.html
dist/error.html
```
というファイルと関連するcssやjsが出力されていると思います。
こちらをs3にアップロードしていきます。

#### generateの結果をs3へアップロードする
とうとう最後の作業です。以下のようなコマンドをpackage.jsonのscriptに足しています。

```json:package.json
    "upload": "aws s3 sync ./dist s3://${npm_package_config_bucket} --include \"*\" --acl public-read --cache-control \"max-age=900\" --profile=cli",
```

ビルドする内容が複雑になりすぎて、scriptに書きたくない量になりそうなら
適宜いい感じにgulpとか使ってあげてください。
npm configの変数については後述しています。

まず`aws s3 sync`コマンドで、**指定のフォルダをまるっとs3の特定バケットにアップロード**しています
`aws s3 sync ./dist s3://${npm_package_config_bucket} `

ソレ以外のオプションについては↓のとおりです。

|option|説明|
|---|---|
|include|**exclude**とあわせて使うオプションです。今回は不要で実はミスしていました|
|acl|s3においたコンテンツに対するACL設定です。 `public-read`で全Userが参照可能になります|
|cache-control|s3においたコンテンツにCache-Controlを設定します|
|profile|aws-cliにdefault以外のユーザーを設定してるとき、コマンドを実行するユーザー設定を指定します|

不要なファイルを上げないようにexcludeといったオプションも細かく設定していくべきなときもありますが、今回は特に気にせず `nuxt generate`した結果を全部アップロードしています。

これで、全てが完了です（たぶん…）

#### おまけ: npm configで公開したくない値をscript上で変数化する

ついでですが `npm config`を使って**git上にバケット名を公開しない**ようにしています。
privateな場所であれば気にしなくてもいいですが、今回は最終的にpublicなリポジトリに公開をするため、`npm config`でscript上で使われる値をセットできます。

例として`package.json`に定義した`name`が`appname`の場合

```
npm config set appname:bucket <値>
```
とsetするとscriptで

```
"hoge": "echo ${npm_package_config_bucket}"
```
のように `npm_package_config_<変数名>` 形でscript上で展開することもできます。

# 動作確認
ここまでやったすべての確認をしていきましょう。

- 作成したドメインでアクセスができた
- 作成したドメインのルートにアクセスしたら index.htmlにアクセスするようになっているので、s3にデプロイしたindex.htmlが表示された
- 初回アクセスで、userIdを取得するために /inviteesのパスにリクエストして、レスポンスが返ってきた
- 入力フォームに値を入力してOKを押したら登録成功した
- DynamoDBに登録した情報が表示されている

備忘録的に順にかいてますが、3月前の記憶がベースなのでもしかしたら足りない情報があるかもしれません…

もし失敗してCloudFrontにキャッシュ設定をしたせいでs3を更新しても反映されない。
といった場合、CloudFrontの画面から `Invalidation`のタブを選択して、`Create Invalidation`のボタンを押すとパスを入力する画面が表示されると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39229/5484ac32-fc6c-e054-1b33-47df0fb0164d.png)

こちらの入力欄に`/*`と入れて`Invalidate`を実行すると まるっとキャッシュを消してくれます（実行完了に少しだけ時間がかかります）
**注意** やりすぎると課金にかかわるので、やりすぎないかお財布と相談してください :bow: 

# まとめ

言い訳になりますが、2019/2末ぐらいには開発完了していてそれを出してから放置をしていたため、この備忘録も当時の記憶を頼りに書いてるのでだいぶヌケモレあるんじゃないかな…と思います。
フロントにいたっては、あまり特殊実装もしてないのであとは「コード見て」状態になってますね…／(^o^)＼

多分おかしいところあるきがするのでアレば教えてください :bow:

とりあえず整理もせずひたすら書いたのでクソ長いですが、
もしここまで読んでくださった方がいたら本当にありがとうございました！

会社だと整った環境があるので、なかなか１から作ることはないからこそ
お勉強としてやってみましたが…それでもAWSで完結するんだから楽ですよね :innocent: 

パーティも終わってこれも書き終わったのでAWSの解約をして
趣味のゲームづくり(Unity)に力を注いでいきたいなという そんな今日このごろ