JavaScript
Node.js
AWS
reactjs
serverless

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料金早見表

ServerlessアーキテクチャとSPA(ReactJS)でサービス構築するには

以下のような構成になります。

スクリーンショット 2017-12-29 5.37.03.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

API側の実装とデプロイはserverlessフレームワークにて行います。

serverlessフレームワークはバージョン別で破壊的変更が入り、互換性がないことが多いので注意です。

今回はv1.24.1にて実装を行っています。

下記コマンドでServerlessフレームワークをインストールします。

$ npm install -g serverless

サンプルの構成です。

clientフォルダにはReactJSでのフロントエンドの実装、

serverless-apiフォルダにはServerlessフレームワークでのバックエンドの実装がそれぞれ格納してあります。

configフォルダには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でドメインを購入する


ローカルでの起動

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


server.sh

$ cd serverless-api

$ yarn
$ sls dynamodb install
$ SLS_DEBUG=* sls offline start

デバック方法は下記記事を参考にしてください

Serverlessフレームワークでデバッグする(ブレークポイントかける)方法

上記サーバを立ち上げた状態で

クライアント側は下記コマンドでwebpack-dev-serverにて起動します。


client.sh

$ cd client

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

スクリーンショット 2017-12-29 5.26.35.png

スクリーンショット 2017-12-29 5.26.55.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入門(基本編)

ReactJSで作る今時のSPA入門(リリース編)

serverlessの設定ファイルのclientのbucketNameのteradonburi.infoの部分は各自取得したドメインで書き換えてください

私はstatic.(ドメイン名)にしました。

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のスタックが作成されます。

スクリーンショット 2017-12-29 3.41.14.png

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

スクリーンショット 2017-12-29 3.44.03.png

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

スクリーンショット 2017-12-29 3.40.58.png

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

スクリーンショット 2017-12-29 3.41.37.png

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

スクリーンショット 2017-12-29 3.39.37.png


serverless.ymlについて

serverless.ymlにはデプロイの設定情報を記述します。

pluginsに追加のプラグインを記述します。

ローカル実行用にserverless-offlineserverless-dynamodb-localを導入しました。

S3へdistフォルダアップロード用にserverless-finchを導入しています。

DynamoDBのauto scalingにserverless-dynamodb-autoscalingを使用しています。


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

providerにはlamdaから実行できるIAMロールの指定を記述します。

今回はLambdaからDynamoDBアクセスするため、DynamoDBのIAMロールを追加します。


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がバイナリデータをサポートしたので試してみました


CloudFrontの設定

参考:S3 CloudFront Route 53 でReactで作ったSPAを配信する

参考:CloudFrontでS3のウェブサイトをSSL化する

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にします。

スクリーンショット 2017-12-29 5.28.23.png

スクリーンショット 2017-12-29 5.30.43.png

スクリーンショット 2017-12-29 5.30.57.png

スクリーンショット 2017-12-29 5.31.10.png

CloudFront作成後に払い出されたDomain Nameをコピーします。

スクリーンショット 2017-12-29 5.22.44.png

Route53にてstatic.(ドメイン名)にCloudFrontのDomain NameをAレコードのaliasとして指定します。

スクリーンショット 2017-12-29 5.21.14.png


ACMで独自ドメインのSSL証明書作成

ACMにてSSL証明書を作成します。

CloudFrontで利用するため、北部バージニア(us-east-1)リージョンで作成する必要があります。

*.(ドメイン名)でサブドメインを作成します。

スクリーンショット 2017-12-29 5.04.44.png

DNSの検証を選択し、確定とリクエストします。

スクリーンショット 2017-12-29 5.09.58.png

Route53にて払い出されたCNAMEを登録します。(Route53でのレコードの作成)

スクリーンショット 2017-12-29 5.10.44.png

スクリーンショット 2017-12-29 5.12.51.png


SPAのルーティングを有効にする

デフォルトのままだと

S3のエラーページが表示されてしまい、SPAのルーティングが有効になりません(Reactの場合、React-Routerが効かない)

そこでCloudFront側でエラーページの遷移を修正します。

Create Custom Error Responseを作成します。

スクリーンショット 2017-12-29 5.14.00.png

403エラーコードの場合にindex.htmlを指定するように修正します。

スクリーンショット 2017-12-29 5.16.41.png


API Gatewayの設定

参考:Amazon API Gateway にカスタムドメインを設定する

参考:【新機能】Amazon API GatewayがACM (AWS Certificate Manager)に対応。簡単に独自ドメインAPIがSSL化。

API Gatewayにカスタムドメインを割り振ります。

API Gatewayのカスタムドメイン名から作成します。

私はapi.(ドメイン名)にしました。(config/production.jsと合わせてください)

作成にはかなり時間がかかります。(30分以上?)

スクリーンショット 2017-12-29 4.23.28.png

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

スクリーンショット 2017-12-29 4.26.23.png

Route53にてCreate Record SetsでAレコードを作成します。

aliasをyesにして先程のターゲットドメインを指定します。

スクリーンショット 2017-12-29 4.29.23.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