はじめに
CloudFront + S3な環境に対してCircleCIからデプロイしたいケースは結構あると予想しているのですが1ググっても出てこなかったので詳細を覚えているうちに誰かのお役にたてるよう、記事に残しておきます。
目標
- コストを低く押さえたい
- 管理コストを多く払いたくないのでなるべくサーバ立てたくない
- GitOpsに寄せつつも、本番だけはデプロイタイミングをGitと切り離して任意のタイミングで制御したい
前提条件
- フロントエンド専用リポジトリを利用している
- バックエンドに同梱されていない(バックエンドからホストしない)
- SEOは不要
実際に作ってデプロイしたアプリ
development想定 -> https://d1memmxu7q6yg2.cloudfront.net/
staging想定 -> http://d31zh8j8zfwafr.cloudfront.net/
production想定 -> https://dnn8bwm84bdn2.cloudfront.net
サンプルコード
設計
環境(AWS周り)
- 環境はdevelopment/staging/productionの3種類
- デプロイ先はS3
- サーバを用意しなくて済む&低コスト…!!
- CloudFrontから配信
- サーバを用意しなくて済む&低コスト…!!
- CIツールはCircleCI
- 所属会社の環境がそうだから前提に加えたというのが一番の理由ですが、ジョブが設定ファイル化されているのは差分管理や履歴管理ができるため気に入っています。
- Githubと連携して使えて、トリガーも柔軟に設定できるので非常にGitOpsフレンドリーだと思います。
- IAMはデプロイに必要な最低限の権限のみを与えたユーザを使用する
Nuxt.js
- Nuxt.jsはSPAモード
- 静的コンテンツをホストすれば済むため、サーバ立てる必要がありません。
- SEOいらないなら無駄な複雑さを持ち込まないようにSPAにするほうが良いと判断しました
- UniversalモードだとCSRとSSRのどちらで動いているか考える必要性が出てきてしまいメンテコストが上がると判断しました。
- dotenvを利用して、環境毎にトップ画面で出す文字列を差し替えるようにします。
GitとCircleCI
- develop,stagingブランチへのPRマージでビルドとデプロイを実行
- GitOpsっぽい!
- (masterブランチへ)特定のprefixがついたタグがついたらテストとビルドを実行
- GitOpsに寄せつつ、人の手でデプロイタイミングを制御できる…!
- その後、CircleCI上で承認したらデプロイ
- 承認フェーズを設けてるのはタグ付からリリースまでのタイムラグを想定したため
- サービス停止を伴うリリースの場合に、準備だけでも事前に済ませたい、などのシチュエーションを想定
- 承認フェーズを設けてるのはタグ付からリリースまでのタイムラグを想定したため
- ロールバック方法
- ロールバックしたいコミットに新しいタグを付けて対応する
S3の解説
bucket
以下の4つのbucketを作成します。
- nuxt-cloudfront-s3-circleci-sample-accesslog
- nuxt-cloudfront-s3-circleci-sample-development
- nuxt-cloudfront-s3-circleci-sample-staging
- nuxt-cloudfront-s3-circleci-sample-production
各bucketの説明
1. nuxt-cloudfront-s3-circleci-sample-accesslog
すべての環境のS3への直接アクセス時とCloudFrontへのアクセスログを格納します。私自身は、環境毎にbucketを用意したほうが良いと考えていますが「こういうこともできるよ」と示すためだけにこうしてます。
アクセス制御がやりやすくなるという理由で、実際に本番で運用するときは環境毎にbucketを用意するつもりです。
2. nuxt-cloudfront-s3-circleci-sample-development
development環境用のbucketです。
nuxt-cloudfront-s3-circleci-sample-accesslog/development-s3
にアクセスログを出力します。
3. nuxt-cloudfront-s3-circleci-sample-staging
staging環境用のbucketです。
nuxt-cloudfront-s3-circleci-sample-accesslog/staging-s3
にアクセスログを出力します。
4. nuxt-cloudfront-s3-circleci-sample-production
production環境用のbucketです。
nuxt-cloudfront-s3-circleci-sample-accesslog/production-s3
にアクセスログを出力します。
bucketの設定
アクセスログのbucketはすべてデフォルトのため、詳細な説明は割愛します。
ちなみに、実際にアクセスログが出力されるとこんな感じになります。
バケット名とリージョン設定
特に説明の必要はないと思います。
アクセスログ設定
- 『バケットへのアクセスリクエストを記録します』にチェックを入れアクセスログを保存します。
- ターゲットバケットにはアクセスログ用のbucketを指定します。
- ログ出力先をルートから変更したい場合、任意のパスを設定します。
- 今回は、
{環境名}-s3/
としてディレクトリにしました。次のセクションで説明するcloudfrontのアクセスログは[環境名]-cloudfront
と言うディレクトリにする想定です。
- 今回は、
ブロックパブリックアクセスの設定
こちらの設定はデフォルトのままとし、すべてのアクセスを遮断します。このままだとcloudfrontからもアクセスできません。
cloudfrontにアクセスさせるために必要なポリシーは次のセクションのcloudfrontで設定します。
CloudFrontの作成
デフォルトのままの箇所を割愛して設定を変更している部分のみピックアップします。
Origin Settings
- 『Origin Domain name』に該当するS3のURLを指定します。
- 『Ristrict Bucket Access』を
Yes
に指定します。- コンテンツへのアクセスを必ずCloudFrontを経由させるように強制します。
- 『Origin Access Identity』を
Create a New Identity
に指定します。- (2)のために必要です。CloudFrontのユーザをS3のアクセスのために割り当てる必要があります。これはIAMユーザとは別のようです。
- 今回は自動作成させ、かつ環境の数だけ用意します。
- 1つのOrigin Access Identityを使い回すことも可能です。2
- 『Grant Read Permissions on』を
Yes, Update Bucket Policy
に指定します。- (3)のユーザからのアクセスを許可するためのポリシーを対象S3に自動で設定してくれます。
Distribution Setting
- 『Default Root Object』に
index.html
を指定します。 - 『Bucket for Logs』にアクセスログ保存用のbucketを指定します。
- 私自身は明確に助かったなあというエピソードはそれほどありませんが、何かの調査や分析用途に残しておいたほうが無難だと考え設定しています。
- 『Log Prefix』には、ログ出力先をルートから変更したい場合、任意のパスを設定します。
- 今回は、
{環境名}-cloudfront
としました。
- 今回は、
以上の設定でひとまず作成までは完了です。Create Distribution
ボタンを押し、Distiributionを作成してください。
最後の仕上げとして、ルート以外にアクセスされた場合に/index.html
に転送する設定を行います。
Error Pages
対象のDistributionをクリックして詳細ページに遷移します。
Error Pageタブをクリックし、Create Custom Error Responseボタンをクリックします。
ルート以外にアクセスした場合、/index.html
に転送しつつ、200 OKを応答するように設定します。
ルート以外にアクセスした場合、S3に存在しないオブジェクトのパスだとCloudFrontのデフォルトでは403を返却します。
それを/index.html
へのアクセスに転送しつつ、応答コードも200 OKに変更する設定です。
- カスタマイズするエラーレスポンスを指定します。
- カスタマイズするので『yes』を指定します。
- カスタマイズした応答の静的ファイルを指定する箇所ですが、やりたいことはルートへの転送なので、
index.html
を指定します。 -
index.html
へ転送後の応答は200 OKが正常のため、そう指定します。
設定完了
これでCloudFrontの設定完了です。
Distributionへの反映には少々時間がかかるのでのんびり待ちましょう。
補足
なお、『Default Cache Behavior Settings』のセクションに『Allowed HTTP Methods』という項目があり、OPTIONや更新系のその他のメソッドを許可するかどうか指定できますが、SPAで動かしているのであればGETとHEADのみ許可していればいいはずです。
もし、その他のメソッドも許可する必要があるケースをご存じの方は、ぜひコメントで教えて下さい。
IAMの設定
デプロイで必要になる最低限の権限を与えたポリシーを作成し、デプロイ専用のIAMユーザにそれを割り付ける設計にしました。
作成したポリシーは以下になります。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:ListBucket"
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::{ your bucket name, wild card is okay }"
},
{
"Effect": "Allow",
"Action": [
"cloudfront:GetInvalidation",
"cloudfront:CreateInvalidation"
],
"Resource": [
"arn:aws:cloudfront::{ your account id }:distribution/{ your development distribution id }",
"arn:aws:cloudfront::{ your account id }:distribution/{ your staging distribution id }",
"arn:aws:cloudfront::{ your account id }:distribution/{ your production distribution id }"
]
}
]
}
S3
S3にファイルをアップロードするため、当然s3:PutObject
が必要です。
デプロイ時に古いファイルをS3から削除するのであれば、s3:ListBucket
とs3:DeleteObject
が必要になります。
また、操作を許可するbucketの指定を行ったほうが良いでしょう。
配列で指定するかワイルドカードを使用して指定しましょう。
CloudFront
キャッシュの無効化の操作をするため、cloudfront:GetInvalidation
とcloudfront:CreateInvalidation
が必要です。
また、操作を許可するDistributionの指定を行ったほうが良いでしょう。
こちらも配列かワイルドカードでの指定が可能です。
Nuxt.jsの解説
アプリ
nuxt.js自体は余り解説することがありません。
npx create-nux-app
したあとにアプリとして手を加えたのはSPAモードにしていることと、以下のようにトップ画面にデモ用にdotenvから取得した値を表示しているくらいです。
dotenv
dotenv
ディレクトリを掘って、環境毎に分岐する値をdotenvに保存しています。
今回のサンプルだとこのような感じです。
ENVIRONMENT={environment}
AWS_BUCKET_NAME={your bucket name}
AWS_CLOUDFRONT={your cloud front distribution id}
資格情報(AWSのアクセスキーなど)はこのファイルには含めません。3
デプロイ周り
基本的にはNuxt公式のDeploy Nuxt on Amazon Web Services - AWS w/ S3 + CloudFrontの通りです。
gulpを利用してデプロイします。
grupfile.js
一点だけ変更点というか、注意点があります。
S3ですべてのパブリックアクセスをブロックしている場合、'x-amz-acl': 'private'
をヘッダーに加える必要があるのでその点を注意してください。
以下に、設定箇所だけ抜粋します。
headers: {
'x-amz-acl': 'private' // S3はすべてのパブリックアクセスをブロックしているので、privateにする必要がある
},
deploy.sh
変更点は2点です。yarnを使用していることと、yarnでローカルパッケージのgulpを呼び出していることです。
今回CIツールはCircleCIで利用しますが、gulpはインストールされていないのでローカルパッケージを利用する必要があります。
置き換え後のものを一応以下に記載します。
$ yarn generate
$ yarn run gulp deploy
Git
特に想定しているブランチ戦略はありません。
環境に対応したブランチが必要なためdevelop、staging、masterブランチを使います。
名前を見ただけで想像はついているかと思いますが、対応は以下のとおりです。
- develop -> dev環境
- staging -> stg環境
- master -> prod環境
CircleCI
こちらは設定ファイルで説明します。
作り込んでいるところのみ、コメントで解説します。
version: 2
jobs:
test:
docker:
- image: circleci/node:12.16.0
steps:
- checkout
- restore_cache:
keys:
- yarn-{{ checksum "package.json" }}
- run: yarn install
- save_cache:
paths:
- node_modules
key: yarn-{{ checksum "package.json" }}
# 実際にlistやtestを実行する場合はここに定義する
# - run: yarn lint
# - run: yarn test
deploy-development:
docker:
- image: circleci/node:12.16.0
steps:
- checkout
- restore_cache:
keys:
- yarn-{{ checksum "package.json" }}
- run: mv ./dotenv/.env.development ./.env # dev環境用のdotenvをリネームして使う
- run: ./deploy.sh
deploy-staging:
docker:
- image: circleci/node:12.16.0
steps:
- checkout
- restore_cache:
keys:
- yarn-{{ checksum "package.json" }}
- run: mv ./dotenv/.env.staging ./.env # stg環境用のdotenvをリネームして使う
- run: ./deploy.sh
deploy-production:
docker:
- image: circleci/node:12.16.0
steps:
- checkout
- restore_cache:
keys:
- yarn-{{ checksum "package.json" }}
- run: mv ./dotenv/.env.production ./.env # prod環境用のdotenvをリネームして使う
- run: ./deploy.sh
workflows:
version: 2
test:
jobs:
- test:
filters:
branches:
ignore: # test jobは以下のブランチでは実行しない。マージされたときにはすでにtestは通っているはず。
- develop
- staging
- master
deploy:
jobs:
- deploy-development:
filters:
branches:
only:
- develop # developブランチへのマージで発火することを想定している
- deploy-staging:
filters:
branches:
only:
- staging # stagingブランチへのマージで発火することを想定している
- hold:
type: approval
filters:
tags:
only: /^release-.*/ # リリースタグをトリガーに指定するが、CircleCI上から承認しなければリリースされない
branches:
ignore: /.*/ # リリースタグで発火させるためにはすべてのブランチを対象にする必要がある
- deploy-production:
requires:
- hold # CircleCI上での承認操作でこのjobを発火する
filters:
tags:
only: /^release-.*/ # リリースタグをトリガーに指定するが、CircleCI上から承認しなければリリースされない
branches:
ignore: /.*/ # リリースタグで発火させるためにはすべてのブランチを対象にする必要がある
参考にしたサイトなど
課題というか宿題というか
qa環境へのデプロイをどうするか?
releaseブランチを用意してdevelopment,staging環境と同じ流れでデプロイすると良さそうな気がしています。
実際にやる機会があればこの記事に追記するかもしれません。
もしsandbox環境を用意する場合のフローをどうするか?
本番環境と同じで良いと思っています。
もし、sandboxを本番リリース前の事前評価環境と兼用するとすれば、本番リリース前にsandbox環境へリリースするかもしれません。
blue/green deploymentへの対応方法は?
blue/greenそれぞれのdistribusionを用意し、blue用リリースタグとgreen用リリースタグの命名規則でそれぞれに対応するように作り込むと思います。
さらに、承認jobをはさみつつRoute 53の向き先の変更もCircleCIから切り替えるようにすると尚良いと思います。
しかし、そこまで作り込むのが難しそうであれば、リリースまではCircleCIで対応し、向き先の変更のみAWSのマネコンやshell実行でも十分に運用できそうな気はします。
ALBからCloudFrontへforwardする方法があるなら、ALBを使うほうがより簡単かもしれません。
環境毎にAWS複数アカウントを用意している場合のAWS資格情報の取得をどうするか?
環境が分かれるため、CircleCIの環境変数では管理しづらくなります。
そのため、追加で資格情報を保存しておくための別のRepositoryを用意し、そこから資格情報をdotenvにコピーしてくるjobを定義する必要があると想定しています。
これは、そのうち私自身が検証する可能性が高いです。この記事に追記するかもしれません。
最後に
以上は、実際に本番運用するつもりで検証済みとはいえまだ本番には未適用のもののため、改善ポイントや運用しにくい部分があるかもしれません。
実際に本番に適用してみて気づいたことをそのうち記事に反映できたら良いなと思っています。
何かお気づきのことがあればフィードバックを頂けると大変うれしいです。
以上です!