≪2019/06/20追記≫
この記事を公開したのは2018-10-19です。
2019-06-20時点でより簡単さ、運用の容易さを求めるならAWS Amplify Consoleもおすすめです。
morishitaです。
Nuxt で実装した SPA を S3 + CloudFront で配信する機会があったのでそれを書きます。
NuxtのSPA自体については、標準的な作りでTypescript、Pug、 Sassを使ってますよってことぐらいしか書くことがないのですっ飛ばして、 S3とCloudFrontでどのようにSPAをホストしたのか、そしてCodeBuildを使ったデプロイの自動化について書きます。
概要は次のとおりです。
- NuxtのSPAをホストするための S3とCloudFrontを設定する
- CodeBuildを使ってS3にデプロイする
- GIthubリポジトリのmasterにpushしたら動く
- Gulpを利用して、古いファイルの削除やCloudFrontのキャッシュ無効化も実施。
ちなみに、主なライブラリ等のバージョンは次のとおりです。
- Nuxt.js 2.2.0
- Typescript 3.1.3
- Gulp 3.9.1
以下、nuxt.actindi.net
というドメインで配信するとして説明します。
※ nuxt.actindi.net
は例であって実際には存在しません。念のため。
S3 の設定
バケット nuxt.actindi.net
を作成します。
バケット内のディレクトリ構成は次のとおりです。
nuxt.actindi.net # S3バケット ├ app/ # Nuxt SPAをデプロイするディレクトリ └ data/ # Nuxt で読み込むJSONファイルを置くディレクトリ
app
ディレクトリには nuxt build
の生成物をデプロイします。
data
ディレクトリには JSON ファイルを置きます。このファイルを Nuxt SPA が動的に読み込んで表示します。
このJSON ファイルは別のアプリケーションが任意のタイミングで更新します1。
このバケットには公開してよいリソースのみ配置するので Static website hostingを有効にして次の項目も設定します。
設定項目 | 設定値 |
---|---|
インデックスドキュメント | index.html |
エラードキュメント | /app/index.html(※後述) |
次のバケットポリシーも設定しておきます。
これでこのバケットに置いたファイルへのパブリックアクセスを設定しています。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::nuxt.actindi.net/*"] } ] }
そして、data ディレクトリのJSONファイルはXHRで取得するのでCORSの設定もしておきます。
<?xml version="1.0" encoding="UTF-8"?> <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <MaxAgeSeconds>3000</MaxAgeSeconds> <AllowedHeader>*</AllowedHeader> </CORSRule> </CORSConfiguration>
CloudFront の設定
HTTPS にするために CloudFront を利用します。
AWSコンソールのタブごとに設定内容を説明します。
General
目的が HTTPS 化なので、SSL Certificate を設定します。
ここでは nuxt.actindi.net
で配信したいので Alternate Domain Names に nuxt.actindi.net
を設定します。
SSL Certificate には ACM で作った証明書を設定します。
Origin
キャッシュの有効期限を分けたいのでOrigin は 2 つ登録します。
1 つ目は次の設定。
オリジン(1) | |
---|---|
Origin Domain Name | S3 バケットの公開ドメイン |
Origin Path | / (表示上は空) |
もう 1 つは次の設定。
オリジン(2) | |
---|---|
Origin Domain Name | S3 バケットの公開ドメイン |
Origin Path | /app |
Behaviors
Behaviors は上記で作ったオリジンそれぞれに作ります。
まずは、オリジン(1)/
に対する設定。
設定項目 | 設定値 |
---|---|
Path Pattern | /data/* |
Origin | オリジン(1)/ |
Whitelist Headers | Origin (CORSのため) |
Object Caching | Customize を選択し、TTLはすべて0 にする |
dataディレクトリ以下のファイルの更新を即時反映するため、キャッシュさせないようにTTLをすべて0
にするのがポイントです。
続いて、オリジン(2)/app
に対する設定。
設定項目 | 設定値 |
---|---|
Path Pattern | Default(*) |
Origin | オリジン(2)/app |
Object Caching | Use Origin Cache Headers |
こちらはSPAを構成するファイル用なのでキャッシュさせます。
後述しますがデプロイ時に古いキャッシュを無効化するので、デプロイしても反映されないということは起こりません。
Error Pages
次の様に404エラーが発生した場合、index.htmlを返すようにします。重要です。
設定項目 | 設定値 |
---|---|
HTTP Error Code | 404: Not Found |
Customize Error Response | Yes |
Response Page Path | /index.html |
HTTP Response Code | 200: OK |
なぜこの設定が必要かというと、NuxtではPagesにhoge.vue
のような固定ルーティングのページを作るとnuxt build
したときに実体として/hoge/index.html
ファイルが生成されます。
しかし、_id.vue
のような動的ルーティングするページを作った場合、それに対応するファイルは生成されません2。
その状態で /some_dir/10
の様なパスにアクセスするとCloudFrontはS3にファイル/some_dir/10
を探しに行きます。開発者の意図としては/some_dir/_id.vue
で処理したいのですが、対応するファイルが無いので404エラーとなっていまいます。
Nuxtではindex.html
でリクエストを受けないと何も処理できないのでS3上に存在しないパスへのリクエストにはとにかく index.html
を返すようにします。そのため上記のError Pages設定を行います。(その上でNuxtの動的ルーティング上でも存在しなければNuxt上で404ページを表示します)。
Lambda Edgeで処理する方法もあるのですが、少々大げさな気がしますしLambdaのコストがかかるのでこうしました。
S3の設定でもエラードキュメントを設定しましたが、あの設定は念のためなので上記の様にError Pagesを設定すればS3のエラードキュメントの設定はたぶん不要です。
Nuxtの設定
デプロイする先が用意できたので、デプロイするものを作るための設定です。
ポイントとなるのはnuxt.config.jsの次の箇所です。
let generateDir = {}; if (process.env.DEPLOY_ENV === 'S3'){ generateDir = { generate: { dir: "dist/app" }, } } module.exports = { mode: 'spa', ...generateDir, // 〜中略〜 }
何をやっているのかというと、DEPLOY_ENV
という環境変数にS3
という値が設定されていれば、dist/app
ディレクトリにビルド生成物を出力するという設定です(デフォルトでは dist
ディレクトリに出力されます)。
これにより、app
ディレクトリを丸ごとS3にアップすれば良くなります。
また、modeを 'spa'
に設定しています。
CodeBuildでのデプロイ
デプロイ先とデプロイするものが揃ったので、デプロイ処理を作ります。
デプロイにはCodeBuildを使います。
CodeBuildのプロジェクトは次のように設定して作成します。
設定項目 | 設定値 |
---|---|
ソースプロバイダ | GithubのNuxtのSPAのリポジトリを設定 |
Webhook | 有効にしてブランチフィルタに ^master$ を設定 |
環境イメージ | aws/codebuild/nodejs:10.1.0 |
インスタンスタイプ | 3 GB メモリ、2 vCPU(最低スペックで十分) |
Buildspec | buildspec.yml |
Webhookを有効にすると、GithubのリポジトリにWebhookを作ってくれます。
ただ、この時作られるWebhookには、Pull requestsの操作時もトリガーするように設定されてしまいます。
不要で邪魔なのでGithubのWebhook設定画面で Pull requestsのチェックを外して、Pushesのみにします。
ブランチフィルタを設定しているので masterにpushまたはRPをmasterにマージしたタイミングでCodeBuildが実行されるようになります。
アーティファクトは出力しません。
環境変数には次の4つの値を設定します。
環境変数名 | 設定値 |
---|---|
AWS_BUCKET_NAME | nuxt.actindi.net |
AWS_CLOUDFRONT | 作成したCloudFrontのディストリビューションID |
API_URL_BROWSER | https://nuxt.actindi.net/data |
DEPLOY_ENV | S3 |
上2つは あとで説明するGulpで参照するものです。
API_URL_BROWSER
はNuxtに組み込まれているaxios-moduleの設定で、axiosのリクエストパスのプリフィックスを上書きするものです3。
DEPLOY_ENV
は前述したビルド生成物の出力先を変更するフラグですね。
で、CodeBuildプロジェクトで実行する処理を記述した buildspec.ymlは次のとおりです。
version: 0.2 phases: install: commands: - apt-get update -qq && apt-get install -y apt-transport-https - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - sudo apt-get update && sudo apt-get install yarn pre_build: commands: - echo 'Start pre_build phase' - yarn build: commands: - echo 'Start build phase' - nuxt build post_build: commands: - echo 'Start post_build phase' - ./node_modules/.bin/gulp deploy
特に難しいことはしていません。
install フェーズ
残念ながら、CodeBuild標準で用意されているNode.js環境にはyarn
がインストールされていないので インストールしています。
pre_buildフェーズ
yarn
を使ってモジュールをインストールしているだけです。
buildフェーズ
nuxt build
を実行してアプリケーションをSPAとしてビルドしています。
post_build フェーズ
生成物をS3にアップロードしているのですが、そのためにGulpを使っています。
Gulpの設定
CodeBuildからS3にビルド生成物をアップロードするために次の gulpfile.js を使いました。
var gulp = require('gulp'); var awspublish = require('gulp-awspublish'); var cloudfront = require('gulp-cloudfront-invalidate-aws-publish'); var parallelize = require('concurrent-transform'); // https://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html var config = { // Required params: { Bucket: process.env.AWS_BUCKET_NAME }, // Optional deleteOldVersions: true, originPath: '/app', // ポイント① distribution: process.env.AWS_CLOUDFRONT, // Cloudfront distribution ID region: process.env.AWS_DEFAULT_REGION, headers: { /*'Cache-Control': 'max-age=315360000, no-transform, public',*/ }, // Sensible Defaults - gitignore these Files and Dirs distDir: 'dist', // ポイント② indexRootPath: true, cacheFileName: '.awspublish', concurrentUploads: 10, wait: true, // wait for Cloudfront invalidation to complete (about 30-60 seconds) } gulp.task('deploy', function() { // create a new publisher using S3 options // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property var publisher = awspublish.create(config, config); var g = gulp.src('./' + config.distDir + '/**'); // publisher will add Content-Length, Content-Type and headers specified above // If not specified it will set x-amz-acl to public-read by default g = g.pipe(parallelize(publisher.publish(config.headers), config.concurrentUploads)) // Invalidate CDN if (config.distribution) { console.log('Configured with Cloudfront distribution'); g = g.pipe(cloudfront(config)); } else { console.log('No Cloudfront distribution configured - skipping CDN invalidation'); } // Delete removed files // ポイント③ if (config.deleteOldVersions) g = g.pipe(publisher.sync(config.originPath)); // create a cache file to speed up consecutive uploads g = g.pipe(publisher.cache()); // print upload updates to console g = g.pipe(awspublish.reporter()); return g; });
このgulpfile.js は実はNuxtの公式ドキュメントの次のページにあるサンプルほぼそのままです。
https://nuxtjs.org/faq/deployment-aws-s3-cloudfrontnuxtjs.org
このgulpfile.jsは次の2つのライブラリを使ってS3へのアップロードとCloudFrontのキャッシュ無効化を行っています。
- gulp-awspublish:S3にファイル/ディレクトリをアップロードしてくれる。古いファイルの削除も可能で大変便利。
- gulp-cloudfront-invalidate-aws-publish:更新されて古くなったファイルのCloudFrontキャッシュを無効にしてくれます。大変便利。
ただ、Nuxtの公式ドキュメントの例そのままでは困るところがあり、いくつか手を入れています。
順にポイントを説明します。
ポイント①
deleteOldVersions: true
にすることで、S3上の古いファイルを削除してくれます。
しかし、バケット全体を対象にするので、そのままだと /data
ディレクトリまで削除されてしまいます。
それは困るので originPath: '/app'
で影響範囲を /app
ディレクトリ配下のみに限定しています。
ポイント②
distDir: 'dist'
ディレクトリ以下をまるごとS3にアップしてくれます。
nuxt build
の生成物は /dist/app
ディレクトリに出力されるので、S3には/app
ディレクトリごとアップされます。
ポイント③
publisher.sync()
は、古くなったS3上のファイルを削除してくれる関数ですが、
もともとのコードではpublisher.sync()
の様に引数無しで使われています。
ポイント①で設定したoriginPath
を渡すことにより、/app
ディレクトリとその中身のみ削除の対象とするようになります。
ポイント④
ポイント④はコードから削除した部分です。
Nuxtの公式ドキュメントにも書いてありますが、もともとのコードはAWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
を環境変数で渡すように書かれており、
次のコードが記載されています。
var config = { // 〜略〜 accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // 〜略〜 }
CodeBuildを実行するロールに次の権限を与えておけば、これらの値をわざわざ設定しなくても S3とCloudFrontへの操作が可能になります。なので不要となり削除しました。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObjectAcl", "s3:GetObject", "s3:AbortMultipartUpload", "s3:ListBucket", "s3:DeleteObject", "s3:PutObjectAcl", "s3:ListMultipartUploadParts" ], "Resource": [ "arn:aws:s3:::nuxt.actindi.net", "arn:aws:s3:::nuxt.actindi.net/*" ] }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "cloudfront:ListInvalidations", "cloudfront:GetInvalidation", "cloudfront:CreateInvalidation" ], "Resource": "*" } ] }
他のCIの仕組みを使うと AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
という秘密情報を渡さないといけなくなり
その管理どうしようと考えないといけなくなるのですが、CodeBuildではそれが不要です。便利ですね。
これですべての設定の説明は終わりです。
冒頭の図を再掲しますが、次のような流れが実現できます。
まとめ
- Nuxtで実装したSAPをS3+CloudFrontでホストしました。
- 動的ルーティングを持つSPAも運用可能です。
- CodeBuildで自動デプロイも楽々。
最後に
アクトインディは今のところRailsメインの会社ですが、Railsだけではありません。
あれこれ一緒に試してみたいエンジニアを募集しています。
-
APIを作っても良かったのですが、今回の案件ではデータ量も多くないし、更新頻度も多くないので簡易に済ませました。↩
-
nuxt generate
でも_id.vue
は無視され何も生成されません。↩ -
https://github.com/nuxt-community/axios-module/blob/dev/docs/options.md#browserbaseurl↩