Serverless+SPAで超低費用な個人サービス構築のススメ

# Serverless+SPA構成
最近低費用でサービス運用できないかと、色々試してたのをまとめてみます。
Serverless構成だとEC2と比べて費用が安くなり、SPAであればルーティングがサーバ側制御ではないためS3のバケットに静的html、css、js、画像の配置にてサービス構築が可能となります。
SPAにはReactJSを採用してますが、Vue.jsやUnityとかのサービスでもいけると思います。
良い点ばかりではなく、デメリットも結構あるのでそれを考慮した上で導入しましょう。
個人でお金をかけたくなくてサービス運用したい場合などには良いかもしれません。
（Lambdaの無料枠＋アクセス量のみで計算される。ドメイン費込みでも月約$3程度）

メリット
・　とにかく費用が安い
・　AWSの設定周りの変更だけでスケールする
デメリット
・　利用しているサービスが多いため、デプロイ時などの障害発生ポイントが多い、各サービスに関する知識もある程度必要
・　Lambdaに関するAPI処理実行時の起動速度制限やメモリ上限がある
・　DBがDynamoDBでないとスケールしずらい
・　バックエンド側は完全AWS依存のため、別の環境に簡単には移行できない

Serverlessについてや各種AWSサービス料金に関しては下記参考にしてください
[AWS料金早見表](https://qiita.com/teradonburi/items/a382a17e1e0245b7d831)

ServerlessアーキテクチャとSPA（ReactJS）でサービス構築するには
以下のような構成になります。

<img width="654" alt="スクリーンショット 2017-12-29 5.37.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/0eeb8bb7-60c2-85d4-53f8-dc9ed6cc7018.png">


利用用途：
- CloudFormation: Serverlessフレームワーク経由でデプロイジョブを実行
- IAM: Lambdaの実行権限やDynamoDBアクセスをLambdaに付与
- CloudWatch: ログ収集
- Route53: ドメイン割当
- ACM: SSL証明書作成、SSL認証
- CloudFront: CDN、ドメインでの公開配信
- S3： SPAファイルを配置（ReactJSリリースビルド済みのファイル群）
- API Gateway: APIのエンドポイント作成
- Lambda: APIの処理を実行
- DynamoDB: データ保存

# Serverlessフレームワーク+ReactJSによる実装
サンプルのソースコードは次gitにあります。
[serverless-react](https://github.com/teradonburi/serverless-react)

API側の実装とデプロイは[serverlessフレームワーク](https://github.com/serverless/serverless)にて行います。
serverlessフレームワークはバージョン別で破壊的変更が入り、互換性がないことが多いので注意です。
今回はv1.24.1にて実装を行っています。
下記コマンドでServerlessフレームワークをインストールします。

```
$ npm install -g serverless
```

サンプルの構成です。
clientフォルダにはReactJSでのフロントエンドの実装、
serverless-apiフォルダにはServerlessフレームワークでのバックエンドの実装がそれぞれ格納してあります。
configフォルダには[node-config](https://github.com/lorenwest/node-config)の設定ファイルがあります。今回はAPIサーバの起動ドメインの設定を記述してあります。
今回はTODOサービスのサンプルを作成しました。
ReactJSからAPI経由でのDynamoDBのCRUD操作を行います。(create.js、list.js、update.js、delete.js)

```
├── client
│   ├── package.json
│   ├── src
│   │   ├── App.js
│   │   ├── components
│   │   │   ├── NotFound.js
│   │   │   ├── TodoPage.js
│   │   │   └── TopPage.js
│   │   ├── index.js
│   │   ├── reducer
│   │   │   ├── reducer.js
│   │   │   └── todo.js
│   │   └── static
│   │       └── index.html
│   ├── webpack.build.js
│   ├── webpack.config.js
│   └── yarn.lock
├── config
│   ├── default.js
│   └── production.js
└── serverless-api
    ├── lib
    │   └── dynamodb.js
    ├── offline
    │   └── migrations
    │       └── todo.json
    ├── package.json
    ├── serverless.yml
    ├── src
    │   ├── create.js
    │   ├── delete.js
    │   ├── list.js
    │   └── update.js
    └── yarn.lock
```

今回のサンプルをデプロイする上でドメインが必要です。（ローカルでの起動は問題はない）
ドメインを未購入の人は下記記事を参考にドメインの購入をしてください。（ドメインがないとデプロイ時にAWS側で自動割当されたものとなり、色々不便なので）
[Amazon Route 53でドメインを購入する](https://dev.classmethod.jp/cloud/register-domains-route53/)

## ローカルでの起動
次のコマンドでローカルでのサーバのシミュレーション起動ができます。

```server.sh
$ cd serverless-api
$ yarn
$ sls dynamodb install
$ SLS_DEBUG=* sls offline start
```

デバッグ方法は下記記事を参考にしてください
[Serverlessフレームワークでデバッグする（ブレークポイントかける）方法](https://qiita.com/teradonburi/items/3ab06f04edbdbdcb02a7)

上記サーバを立ち上げた状態で
クライアント側は下記コマンドでwebpack-dev-serverにて起動します。

```client.sh
$ cd client
$ yarn 
$ NODE_CONFIG_DIR=../config webpack-dev-server
# ブラウザを開く
$ open http://localhost:3001
```

<img width="951" alt="スクリーンショット 2017-12-29 5.26.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/b6f43e29-2d5e-6507-e29c-b63a18221dd0.png">

<img width="946" alt="スクリーンショット 2017-12-29 5.26.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/7c0c2c67-f53d-faba-b0ab-5ea4b08658d5.png">


# デプロイ（初回）
クライアント（ReactJS）のリリースビルドとserverlessフレームワークでのデプロイを行います。
ローカル時は下記node-configの設定ファイルにてクライアント側から指定のAPIサーバに対してリクエストを投げます。

```config/default.js
module.exports = {
  API_SERVER: 'http://localhost:3000',
  PORT: 3000,
}
```

リリースビルド時は下記production.jsの方が参照されます。
`teradonburi.info`の部分は各自取得したドメインで書き換えてください
私は`api.(ドメイン名)`にしました。

```config/producation.js
module.exports = {
  API_SERVER: 'https://api.teradonburi.info',
}
```

クライアント側は下記コマンドでwebpackにてリリースビルドを行えるようにしました。
distフォルダにビルドされたリリース用のSPAのファイル群が作成されます。

```
$ cd client
$ yarn
$ rm -rf dist;NODE_ENV=production NODE_CONFIG_DIR=../config parallel-webpack -p --config webpack.build.js
```

ReactJSでのSPA作成方法やリリースビルドの詳細に関しては下記を参考にしてください。
[ReactJSで作る今時のSPA入門（基本編）](https://qiita.com/teradonburi/items/fb91e5feacab5071cfef)
[ReactJSで作る今時のSPA入門（リリース編）](https://qiita.com/teradonburi/items/212fab958b896e6ee85a)

serverlessの設定ファイルのclientのbucketNameの`teradonburi.info`の部分は各自取得したドメインで書き換えてください
私は`static.(ドメイン名)`にしました。

```yaml
serverless.yml
custom:
  client:
    bucketName: static.teradonburi.info
```  

下記コマンドでCloudFormation経由で各サービスにデプロイします。
(CloudFormation、API Gateway、Lambda、S3、DynamoDB、CloudWatch)

```deploy.sh
# デプロイ先のAWSアカウントを明示的に指定(~/.aws/credentialsのアカウントが複数ある場合)
$ export AWS_ACCESS_KEY_ID=(アクセスキー)
$ export AWS_SECRET_ACCESS_KEY=(プライベートキー)
$ export AWS_REGION=(リージョン)
$ SLS_DEBUG=* sls deploy
# distフォルダを新規作成S3バケットにアップロード
$ SLS_DEBUG=* serverless client deploy
```

デプロイが完了するとCloudFormationのスタックが作成されます。

<img width="760" alt="スクリーンショット 2017-12-29 3.41.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/8065a48d-2e48-fdf0-bd4e-acff179512d1.png">

API Gatewayのエンドポイントが作成されます。

<img width="1172" alt="スクリーンショット 2017-12-29 3.44.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/282cd979-6da4-985e-2657-b06f975d8e6d.png">

Lambdaの関数が作成されます。

<img width="576" alt="スクリーンショット 2017-12-29 3.40.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/3d77f4ac-7d80-fe3c-555c-e8cf06718a40.png">

DynamoDBのテーブルが作成されます。

<img width="534" alt="スクリーンショット 2017-12-29 3.41.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/6dae5f9a-9375-d781-158c-42b0c9ca40b9.png">

S3のバケットが作成されます。

<img width="430" alt="スクリーンショット 2017-12-29 3.39.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/3996f1a4-dd65-f1e6-5c24-9f177e9fa3e4.png">


### serverless.ymlについて
serverless.ymlにはデプロイの設定情報を記述します。
pluginsに追加のプラグインを記述します。
ローカル実行用に[serverless-offline](https://github.com/dherault/serverless-offline)、[serverless-dynamodb-local](https://github.com/99xt/serverless-dynamodb-local)を導入しました。
S3へdistフォルダアップロード用に[serverless-finch](https://github.com/fernando-mc/serverless-finch)を導入しています。
DynamoDBのauto scalingに[serverless-dynamodb-autoscaling](serverless-dynamodb-autoscaling)を使用しています。

```yaml

plugins:
  - serverless-offline
  - serverless-dynamodb-local
  - serverless-finch
  - serverless-dynamodb-autoscaling
```

providerにはlamdaから実行できるIAMロールの指定を記述します。
今回はLambdaからDynamoDBアクセスするため、DynamoDBのIAMロールを追加します。

```yaml

provider:
  name: aws
  runtime: nodejs8.10
  region: ap-northeast-1
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
```

customにはその他の設定を記述します。
今回はやっていませんが、開発環境stage、商用環境stage別の設定なども可能です。
dynamodbにはローカルでdynamodbをシミュレーション起動する設定を記述しています。
clientにはserverless-finchでのdistフォルダのデプロイ先S3バケットを指定しています。

```
custom:
  # dynamodb local
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migrate: true
    migration:
      dir: offline/migrations # localテーブル定義ファイル
  # dynamodb auto scaling
  capacities:
    - table: TodosDynamoDbTable  # DynamoDB Resource name
      read:
        minimum: 1       # Minimum read capacity
        maximum: 10      # Maximum read capacity
        usage: 0.75      # Targeted usage percentage
      write:
        minimum: 1       # Minimum write capacity
        maximum: 10      # Maximum write capacity
        usage: 0.75      # Targeted usage percentage
  # s3
  client:
    bucketName: static.teradonburi.info
    distributionFolder: ../client/dist
```

### API Gatewayレスポンスの注意点
statusCode、headers、bodyには下記のようなレスポンスが必要です。
Access-Control-Allow-Credentials、Access-Control-Allow-Origin、Content-Typeに関してheaderの指定が必要です。
bodyはJSON文字列である必要があります。（今回はapplication/json想定、binaryの場合などはまた別の設定が必要）

```create.js
const response = {
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Credentials": true,
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin" : "*" // Required for CORS support to work
  },
  body: JSON.stringify(result.Attributes),
}
callback(null, response)
```

Serverlessフレームワークでの設定は未検証ですがAPI Gateway+Lambda経由の画像のアップロードも今では可能です。
参考：[API Gatewayがバイナリデータをサポートしたので試してみました](https://dev.classmethod.jp/cloud/aws/binary-data-supported-by-api-gateway/)

# CloudFrontの設定
参考：[S3 CloudFront Route 53 でReactで作ったSPAを配信する](https://qiita.com/keita-nishimoto/items/35ae8cc56f5c0a4766b4)
参考：[CloudFrontでS3のウェブサイトをSSL化する](https://qiita.com/jasbulilit/items/73d70a01a5d3b520450f)

CloudFrontを作成します。
Origin Domain NameにはS3のバケット名を指定します。
Origin IDは自動で入ります。
Object CachingをCustomizeにします。
TTLを全て0にしてます。（キャッシュを有効にする場合はS3デプロイ時に手動でInvalidationsにてキャッシュクリアする必要が出てきます。）
Forward CookiesをAllにします。
Compress Objects Automaticallyを有効にします。（gzip圧縮されてレスポンスが早くなるので）
Price ClassesはAll Edgeにすると日本からのアクセスは早くなりますが値段は上がります。（お財布と相談で）
Altanative Domain Names(CNAME)には`static.(ドメイン名)`を指定します。
SSL CertificateをCustom SSL Certificateにします。
→Request or Import a Certificate with ACMにてSSL証明書を作成します（後述）
Default Root Objectをindex.htmlにします。

<img width="771" alt="スクリーンショット 2017-12-29 5.28.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/1599747a-d0b8-7934-8858-c01b163a54ba.png">
<img width="769" alt="スクリーンショット 2017-12-29 5.30.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/5c31358f-cadb-bc68-cc0d-948e295436c8.png">
<img width="770" alt="スクリーンショット 2017-12-29 5.30.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/7eb6c9bb-fa1b-a0c3-e107-5596b0a41633.png">
<img width="770" alt="スクリーンショット 2017-12-29 5.31.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/47cc52a2-47ec-817b-8a8c-543a531a4156.png">

CloudFront作成後に払い出されたDomain Nameをコピーします。
<img width="446" alt="スクリーンショット 2017-12-29 5.22.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/0782715e-71e9-8aa7-762f-1a3548ccaf86.png">

Route53にて`static.(ドメイン名)`にCloudFrontのDomain NameをAレコードのaliasとして指定します。
<img width="282" alt="スクリーンショット 2017-12-29 5.21.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/141d991a-faa4-0119-1abe-9c6b5e89ec9d.png">


## ACMで独自ドメインのSSL証明書作成
ACMにてSSL証明書を作成します。
<font color="red">CloudFrontで利用するため、北部バージニア(us-east-1）リージョンで作成する必要があります。</font>
`*.(ドメイン名)`でサブドメインを作成します。
<img width="1188" alt="スクリーンショット 2017-12-29 5.04.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/eb6436dc-dc29-44da-5d46-67bc975dd2f7.png">

DNSの検証を選択し、確定とリクエストします。
<img width="1074" alt="スクリーンショット 2017-12-29 5.09.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/dc12219a-9ad8-471b-921b-8714366ebd49.png">

Route53にて払い出されたCNAMEを登録します。(Route53でのレコードの作成)
<img width="1161" alt="スクリーンショット 2017-12-29 5.10.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/2331fb65-9d54-ebee-1e7b-2015deb705df.png">

<img width="273" alt="スクリーンショット 2017-12-29 5.12.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/3eabf35e-68f8-b82c-a074-76c689f9a1b3.png">

## SPAのルーティングを有効にする
デフォルトのままだと
S3のエラーページが表示されてしまい、SPAのルーティングが有効になりません（Reactの場合、React-Routerが効かない）
そこでCloudFront側でエラーページの遷移を修正します。

Create Custom Error Responseを作成します。
<img width="623" alt="スクリーンショット 2017-12-29 5.14.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/fb8d52b8-1544-3366-b66c-67d5ed58b0de.png">

403エラーコードの場合にindex.htmlを指定するように修正します。
<img width="701" alt="スクリーンショット 2017-12-29 5.16.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/8fbe3471-8666-0196-b3d4-1bb5a1552b99.png">


# API Gatewayの設定
参考：[Amazon API Gateway にカスタムドメインを設定する](https://qiita.com/keita-nishimoto/items/b56a8e9abeaf6724a28b)
参考：[【新機能】Amazon API GatewayがACM (AWS Certificate Manager)に対応。簡単に独自ドメインAPIがSSL化。](https://dev.classmethod.jp/cloud/aws/amazon-api-gateway-integrates-acm/)

API Gatewayにカスタムドメインを割り振ります。
API Gatewayのカスタムドメイン名から作成します。
私は`api.(ドメイン名)`にしました。（config/production.jsと合わせてください）
作成にはかなり時間がかかります。（３０分以上？）

<img width="610" alt="スクリーンショット 2017-12-29 4.23.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/1b2c2f5b-386c-41f3-32e5-6b568aca8014.png">

保存するとターゲットドメイン名が出力されますのでコピーします。

<img width="401" alt="スクリーンショット 2017-12-29 4.26.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/b5047323-935d-8c7a-2e72-4ce86c8d5354.png">

Route53にてCreate Record SetsでAレコードを作成します。
aliasをyesにして先程のターゲットドメインを指定します。

<img width="275" alt="スクリーンショット 2017-12-29 4.29.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/55077/25ce45d1-2cdd-9152-76cd-ac32779ab177.png">

# 本番環境の確認
上記すべての設定が完了すれば
https://static.(ドメイン名)
にアクセスするとReactJSのSPAページが表示され、
https://api.(ドメイン名)
でAPI Gateway→Lambda→DynamoDBでのRest APIが動作するようになります。

# APIの再デプロイ
API部分は次コマンドで再デプロイします。

```
# デプロイ先のAWSアカウントを明示的に指定(~/.aws/credentialsのアカウントが複数ある場合)
$ export AWS_ACCESS_KEY_ID=(アクセスキー)
$ export AWS_SECRET_ACCESS_KEY=(プライベートキー)
$ export AWS_REGION=(リージョン)
$ SLS_DEBUG=* sls deploy
```

# S3の再デプロイ（アップロード）
serverless-finchの最新版では次のコマンドで再デプロイ可能

```
$ SLS_DEBUG=* sls client deploy
```