Merry Xmas!!! ヘルスケア部門エンジニアの小林です。
この記事は Riot.js Advent Calendar 2017 25日目の記事です。
今年はRiot.jsによるフロントエンド開発と、Serverless Framework+AWSによるバックエンド開発が中心の1年でした。フロントからバックまで自由にいじらせてもらえる環境を頂けたことに大変感謝しております。今回はそのノウハウをまとめてみます。
執筆当初はRiot.jsとServerless Frameworkの比率を1:1くらいで書く想定をしていましたが、ビックリするくらいほぼ同じ境遇で既に先駆者がおりました。。。非常に良くまとめらていますので是非御覧ください。
そこでここでは、主にServerless Frameworkでコードとインフラを管理するあたりについてまとめます。各ツールの基本的な使い方については省略しますのでご了承ください。
Riot.js Advent Calendarなのに上述の理由により申し訳程度しかRiot要素を載せられませんでした...すみません。フロントエンド開発者がバックエンドをサクッと構築したい場合に少しでも役に立てばなと思います。
システム構成
AWSでSPAを構築する際の鉄板構成だと思われる以下の構成で構築します。
- フロントエンド
- CloudFront + S3
- Origin Access IdentityによりCloudFront経由でのアクセスのみ許可
- バックエンド
- 認証: Cognito
- ビジネスロジック: API Gateway + Lambda + DynamoDB
今回はCognito周りは割愛します。そのうち書こうかな。
プロジェクト構成
Serverless Frameworkのプロジェクト構成にフロントエンド周りを突っ込む構成です。
. ├── lambda │ ├── api │ │ └── xxx.js │ └── async │ └── xxx.js ├── lib │ └── xxx.js ├── static │ ├── src │ │ ├── assets │ │ │ └── index.html │ │ ├── constant │ │ │ └── xxx.json │ │ ├── entries │ │ │ └── xxx.js │ │ └── tags │ │ ├── components │ │ │ ├── xxx.README.md │ │ │ └── xxx.tag.html │ │ └── xxx │ │ ├── xxx.README.md │ │ └── xxx.tag.html │ ├── .babelrc │ └── webpack.config.babel.js ├── package.json ├── serverless.yml └── README.md
lambda
- AWS Lambdaのhandlerとして使うJSコードはこちらに格納
api
(WebAPI用途のもの)とasync
(それ以外) で分ける
static
- フロントエンド周りはこちらに格納
- Riot.js + webpack + babel の構成
- Riotタグについては、使い回しの効く汎用タグ(components)とそれ以外で分けている
命名がテキトーなのは大目に見てください。
フロントエンド実装
前述の通り、Riot.js + webpack + babel の構成です。
Riot.jsで開発する際は、こちらのスタイルガイドを参考にしています。
webpack.config.babel.js
import path from 'path'; import webpack from 'webpack'; export default function(env, argv) { return [{ entry: { xxx: './src/entries/xxx.js' }, output: { path: path.join(__dirname, './.deploy', env, 'scripts'), filename: '[name].js' }, module: { rules: [{ test: /\.tag.html$/, exclude: /node_modules/, enforce: 'pre', use: 'riot-tag-loader' }, { test: /\.js$|\.tag.html$/, exclude: /node_modules/, use: 'babel-loader' } ] }, resolve: { extensions: ['*', '.js', '.tag.html'] }, plugins: [ new webpack.ProvidePlugin({ riot: 'riot' }), new webpack.optimize.UglifyJsPlugin() ] } ]; }
$ webpack --env [dev/prod]
で開発環境と本番環境をビルドし分けられるように.deploy/[env]/
にビルド結果を出力
s3-deploy
フロントエンドのデプロイ、つまりS3へのファイル配置には s3-deploy
というパッケージを使用しています。
$ s3-deploy [対象ファイル] --cwd [ルートディレクトリ] --bucket [S3バケット名] --profile [AWSプロファイル名] --region [リージョン] --private
バックエンド実装
API Gateway + Lambda + DynamoDBを基本構成としてバックエンドAPIを実装しつつ、その他必要なインフラ群もAWS CloudFormationで自動構築していきます。
バックエンドAPIの実装については、Serverless FrameworkのGet Started的な内容なのでここでは省略させていただきます。
フロントエンド用のインフラを構築
CloudFront+S3をServerless Frameworkで構築してしまいます。Serverless FrameworkはAWS CloudFormationのラッパーなので、CloudFormationの文法に沿って serverless.yml
に定義していきます。
- S3
- HTML、CSS、JS等の静的コンテンツをここにホスト
- CloudFront
- S3の前段に置くCDN
- OriginAccessIdentity
- これを設定すると、CloudFront経由でしかS3にアクセスできなくなる
serverless.yml
... resources: Resources: StaticContentsS3: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: AccessControl: Private BucketName: [バケット名] Tags: - Key: Name Value: ${self:service}-${opt:stage} StaticContentsS3Policy: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: StaticContentsS3 PolicyDocument: Statement: - Effect: Allow Principal: AWS: Fn::Join: - " " - - "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity" - Ref: StaticCloudFrontOriginAccessIdentity Action: s3:GetObject Resource: Fn::Join: - "/" - - Fn::GetAtt: - StaticContentsS3 - Arn - "*" StaticContentsCloudFront: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Enabled: true Comment: "Delivery static contents" PriceClass: PriceClass_200 DefaultRootObject: index.html Origins: - Id: S3Origin DomainName: Fn::GetAtt: - StaticContentsS3 - DomainName S3OriginConfig: OriginAccessIdentity: Fn::Join: - "/" - - origin-access-identity/cloudfront - Ref: StaticCloudFrontOriginAccessIdentity DefaultCacheBehavior: AllowedMethods: - HEAD - GET CachedMethods: - HEAD - GET Compress: true DefaultTTL: 900 MaxTTL: 1200 MinTTL: 600 ForwardedValues: QueryString: true SmoothStreaming: false TargetOriginId: S3Origin ViewerProtocolPolicy: https-only StaticCloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: Ref: AWS::StackName Outputs: StaticContentsCloudFrontUrl: Value: Fn::Join: - "" - - "https://" - Fn::GetAtt: - StaticContentsCloudFront - DomainName
バックエンドのデプロイ
インフラ、API共に下記コマンドでまとめて展開・デプロイができます。マジ卍。
$ sls deploy -s [env]
package.json
最終的に package.json
はこうなりました。gulp等のタスクランナーは使わず、npm-scriptsを駆使しています。
{ ... "scripts": { "setup": "npm i", "deploy:dev": "npm run setup && ./node_modules/.bin/sls deploy -s dev", "deploy:prod": "npm run setup && ./node_modules/.bin/sls deploy -s prod", "build:dev": "npm run setup && cd static && mkdir -p .deploy && rm -rf .deploy/dev && cp -r ./src/assets ./.deploy/dev && ../node_modules/.bin/webpack --env dev", "build:prod": "npm run setup && cd static && mkdir -p .deploy && rm -rf .deploy/prod && cp -r ./src/assets ./.deploy/prod && ../node_modules/.bin/webpack --env prod", "upload:dev": "npm run build:dev && ./node_modules/.bin/s3-deploy './static/.deploy/dev/**' --cwd './static/.deploy/dev/' --bucket [S3バケット名] --profile [AWSプロファイル名] --region ap-northeast-1 --private", "upload:prod": "npm run build:prod && ./node_modules/.bin/s3-deploy './static/.deploy/prod/**' --cwd './static/.deploy/prod/' --bucket [S3バケット名] --profile [AWSプロファイル名] --region ap-northeast-1 --private" }, "dependencies": { ... }, "devDependencies": { "aws-sdk": "^2.174.0", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-es2015": "^6.24.1", "babel-preset-es2015-riot": "^1.1.0", "riot": "^3.7.4", "riot-tag-loader": "1.0.0", "s3-deploy": "^0.8.0", "serverless": "1.25.0", "webpack": "^3.10.0" } ... }
$ npm run deploy:[env]
でバックエンドのデプロイ$ npm run upload:[env]
でフロントエンドのデプロイ
うーん、命名テキトーなので言葉ややこし。
まとめ
Serverless Frameworkでフロントエンドとバックエンドの全てを同時に管理することができました。プロトタイプや小規模開発であればこれくらいで十分ではないかと思います。