やること
- GitHub Actions で Nuxt.js で作成したアプリのビルド&デプロイを行う
- アプリケーションは(静的な)HTML にビルドする
- デプロイ先は S3 にする
- CloudFront 経由で配信
- SSL 証明書は Certificate Manager で管理
- ドメイン管理は Route 53 を使う
- WAF は使わない
完成版の GitHub Actions
先に完成版の Actions のコードを載せておきます
name: Deploy
on:
push:
branches:
- 'main'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-v1-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-v1-npm-
${{ runner.os }}-v1-
${{ runner.os }}-
- name: Install dependencies
run: npm ci
- name: Build
run: npm run generate
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
role-duration-seconds: 1200
- name: Deploy
env:
DEPLOY_BUCKET: ${{ secrets.DEPLOY_BUCKET }}
DISTRIBUTION_ID: ${{ secrets.DISTRIBUTION_ID }}
run: |
aws s3 sync ./dist s3://$DEPLOY_BUCKET --delete
aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/*'
アプリケーションを静的ビルド
静的サイトホスティングするための Nuxt 側の設定として、以下をnuxt.config.js
に設定する
{
target: 'static'
}
npx create-nuxt-app
(TypeScript 利用)で環境構築を行うと、@nuxt/typescript-runtime
パッケージがインストールされ、nuxt-ts
というバイナリが使える
./node_modules/.bin/nuxt-ts generate
を実行すると、ホスティングするとそのままブラウザが実行できる HTML へビルドしてくれる
上記はnpx create-nuxt-app
を使って環境構築した場合は、最初から npm-scripts でエイリアスされている
{
"scripts": {
"generate": "nuxt-ts generate"
}
}
GitHub Actions で書く
ビルドまでを Actions で書くと以下のようになる(抜粋)
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-v1-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-v1-npm-
${{ runner.os }}-v1-
${{ runner.os }}-
- name: Install dependencies
run: npm ci
- name: Build
run: npm run generate # nuxt-ts generate
S3 にデプロイ
デプロイ用に IAM ユーザーを作成するが、今回このユーザーには AssumeRole の権限のみをアタッチし、実際に S3 を操作する権限は IAM ロールに持たせる
S3 バケットを作成
AWS コンソール上でバケットを非公開で作成する
公開設定等はそのまま(デフォルト非公開になっている)で、バケット名だけ入力し、作成する
IAM ロールを作成
今回は、--delete
オプションが使えるs3 sync
コマンドを使って S3 にアップロードする
以下の S3 アクションのポリシーをアタッチした IAM ロールを作成する
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:ListBucket", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::[バケット名]/*", "arn:aws:s3:::[バケット名]"]
}
]
}
IAM ユーザーを作成
以下の AssumeRole のポリシーをアタッチした IAM ユーザーを作成する
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": ["sts:AssumeRole", "sts:TagSession"],
"Resource": "arn:aws:iam::[AWSアカウントID]:role/[上記で作ったロール名]"
}
]
}
後述するが、Actions でaws-actions/configure-aws-credentialsを使って、AWS 認証を行っている
この中で AssumeRole をリクエストする際にセッションタグを渡しているため、sts:TagSession
が必要になる
AssumeRole を使うと一時的な認証情報が発行されるが、この時に有効期限付きのセッションが生成される
ただ、role-skip-session-tagging
というオプションをtrue
にすれば、sts:TagSession
は不要にできると思う
この辺のコードは以下
https://github.com/aws-actions/configure-aws-credentials/blob/master/index.js
ロールの信頼関係の編集
上記で作ったロールの信頼関係タブから、以下を設定する
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::[AWSアカウントID]:user/[上記で作ったユーザー名]"
},
"Action": ["sts:AssumeRole", "sts:TagSession"]
}
]
}
GitHub Actions で書く
デプロイまでを Actions で書くと以下のようになる(抜粋)
steps:
- name: Build
run: npm run generate # nuxt-ts generate
# 以下を追加
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
role-duration-seconds: 1200
- name: Deploy
env:
DEPLOY_BUCKET: ${{ secrets.DEPLOY_BUCKET }}
run: aws s3 sync ./dist s3://$DEPLOY_BUCKET --delete
GitHub の Secrets
GitHub リポジトリのSettings -> Secrets
で以下を設定している
キー名 | 設定値 |
---|---|
AWS_ACCESS_KEY_ID | アクセスキー |
AWS_SECRET_ACCESS_KEY | シークレットアクセスキー |
DEPLOY_BUCKET | デプロイ先の S3 バケット名 |
AWS_ROLE_TO_ASSUME | 上記で作成したロールの ARN |
role-duration-seconds
について
ロールの最大セッション時間は、デフォルト 1 時間になっている
これに対して、configure-aws-credentials
のデフォルトのセッション時間は 6 時間となっている
よって、role-duration-seconds
オプションを指定し、configure-aws-credentials
のセッション時間を 1 時間以内にする必要がある
ここを超えるとバリデーションエラーで以下のようなエラーが出る
Error: The requested DurationSeconds exceeds the MaxSessionDuration set for this role.
CloudFront の設定
AWS コンソールの CloudFront から Create Distribution をクリック
Origin Settings
設定項目 | 設定値 | 備考 |
---|---|---|
Origin Domain Name | 上記で作成したバケットを選ぶ | |
Origin Path | - | |
Enable Origin Shield | No | Edge とオリジン(ここでは S3 バケット)間にキャッシュレイヤーを設置する リージョナルエッジキャッシュよりもオリジン寄りのキャッシュレイヤー リージョナルエッジキャッシュからオリジンへのリクエストを集約できるので、オリジンの負荷を軽減できる グローバル展開しているサービスだと使えそう |
Origin ID | 自動設定される | |
Restrict Bucket Access | Yes | |
Origin Access Identity | Create a New Identity | CloudFront から S3 バケットへのアクセスを許可するための OAI を作成する |
Grant Read Permissions on Bucket | Yes, Update Bucket Policy | S3 バケットに自動的に OAI のバケットポリシーを設定してくれる |
Origin Connection Attempts | 3 | オリジンへの接続試行回数 |
Origin Connection Timeout | 10 | オリジンへの接続を確立する際のタイムアウト値(秒) |
Origin Custom Headers | - | オリジンへのリクエストのカスタムヘッダー |
Origin Shield を有効化した場合の構成
Origin Shield を有効化すると、ユーザーからオリジンまでの通信は以下になる
User -> Edge locations -> Regional edge caches -> Origin Shield -> Origin
- オリジンへの通信はすべて Origin Shield に集約される
- 世界中のリージョナルエッジキャッシュがそれぞれオリジンと通信していたのを Origin Shield に集約できる
- キャッシュ効率が良い
- 世界中のリージョナルエッジキャッシュがそれぞれにキャッシュを保持し、キャッシュがない場合にオリジンへ通信するのに比べて、Origin Shield がある場合は、Origin Shield にキャッシュが集約できるので、キャッシュヒット率が上がる
Default Cache Behavior Settings
設定項目 | 設定値 | 備考 |
---|---|---|
Viewer Protocol Policy | HTTPS Only | HTTPS リクエストのみのアクセス可 HTTP リクエストが来た場合は 403 エラー |
Allowed HTTP Methods | GET, HEAD | GET 以外の POST などのリクエストは 403 エラー |
Field-level Encryption Config | - | HTTPS で POST された時に入力フォームなどフィールドレベルで暗号化するかどうか(今回は GET のみなので不要) |
Cache and origin request settings | Use a cache policy and origin request policy | |
Cache Policy | Managed-CachingOptimized | キャッシュの Default TTL が 24 時間のやつ |
Origin Request Policy | - | オリジンにリクエストできるヘッダー、Cookie、クエリストリングを制限できる |
Smooth Streaming | No | |
Restrict Viewer Access (Use Signed URLs or Signed Cookies) | No | アクセスを署名付き URL または署名付き Cookie のみに限定する |
Compress Objects Automatically | Yes | リクエストヘッダーにAccept-Encoding: br のように圧縮形式を指定してリクエストすると、CloudFront 側でよしなにコンテンツを圧縮して配信してくれる圧縮されているかの確認は、レスポンスヘッダーに Content-Encoding: br などが付与されていたら成功 |
Lambda Function Associations | - | Lambda@Edge 関数の設定 |
Enable Real-time Logs | No | リアルタイムログの有効化 |
Distribution Settings
今回は Certificate Manager 上で SSL 証明書の発行はしません
事前に SSL 証明書を発行してもらい、Certificate Manager でインポートしておきます
CloudFront で証明書を使用するには、Certificate Manager のリージョンをバージニア北部にする必要がある
設定項目 | 設定値 | 備考 |
---|---|---|
Price Class | Use U.S., Canada, Europe, Asia, Middle East and Africa | どのエッジロケーション(CloudFront、Route 53、WAF が配置されているデータセンター)を使うかを設定 |
AWS WAF Web ACL | None | WAF の設定 |
Alternate Domain Names (CNAMEs) | ドメイン名を設定 | |
SSL Certificate | Custom SSL Certificate Certificate Manager でインポートした証明書を選択する |
|
Custom SSL Client Support | Clients that Support Server Name Indication (SNI) | SNI をサポートするクライアントのみかすべてのクライアントにコンテンツを提供するかの設定 2010 年以降にリリースされたブラウザは SNI がサポートされているみたいなので、SNI をサポートするクライアントのみで良さそう すべてのクライアントにすると月 6 万の請求が発生する |
Security Policy | TLSv1.2_2019 | |
Supported HTTP Versions | HTTP/2, HTTP/1.1, HTTP/1.0 | |
Default Root Object | index.html | Nuxt を静的ビルドすると、dist ディレクトリ直下に index.html が生成されるので、これがルートになる |
Standard Logging | On | CloudFront の標準ログを出力するかどうか |
S3 Bucket for Logs | S3 バケット名を選択 | CloudFront のログ出力用に S3 バケットを作成しておく |
Log Prefix | 任意のディレクトリ名 | ログのバケット内でディレクトリを分けて管理したいときに設定(たとえば本番用とステージング用で分けるとか) |
Cookie Logging | Off | ログに Cookie を含めるかどうか |
Enable IPv6 | On | IPv6 アドレスからのリクエストに応答するかどうか |
Comment | - | |
Distribution State | Enabled | この Distribution の作成後、すぐに有効するかどうか |
Distribution の作成後、Status が Deployed になったら、使える状態になる
CloudFront のデフォルトドメイン.cloudfront.net
でアクセスできたら成功
S3 バケットのリージョンをバージニア北部以外で作った場合、307 レスポンスが返ってくる可能性がある
しばらく待てば見れるようになるが、もし、すぐに確認したい場合は、以下のドキュメントで紹介されているようにオリジンドメイン名にリージョン名を追加するやり方で確認できる
https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response/
自分の場合は、1 時間くらい放置しておいたら見れるようになっていた
CloudFront のキャッシュをクリア
デプロイした際は、CloudFront のキャッシュをクリアしたいので、以下を Actions に追記
steps:
- name: Deploy
env:
DEPLOY_BUCKET: ${{ secrets.DEPLOY_BUCKET }}
DISTRIBUTION_ID: ${{ secrets.DISTRIBUTION_ID }}
run: |
aws s3 sync ./dist s3://$DEPLOY_BUCKET --delete
aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/*'
以下のポリシーをアタッチ
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": ["cloudfront:CreateInvalidation"],
"Resource": [
"arn:aws:cloudfront::[AWSアカウントID]:distribution/[CloudFrontのディストリビューションID]"
]
}
]
}
Nuxt で複数のページコンポーネントがある場合
Nuxt で静的ビルドすると、ページコンポーネントごとにindex.html
が作成される
ただし、CloudFront の Default Root Object の設定では、ルートのindex.html
にしかこの設定は適用されないので、ルート(/
)以外の/setting
や/report
などでアクセスすると CloudFront は 403 エラーを返す
403 エラーは、CloudFront のエラーページの設定で一応回避できる
設定項目 | 設定値 | 備考 |
---|---|---|
HTTP Error Code | 403: Forbidden | |
Error Caching Minimum TTL (seconds) | 10 | |
Customize Error Response | Yes | |
Response Page Path | /index.html | |
HTTP Response Code | 200: OK |
CloudFront の 403 エラーを 200 にして、ルートのindex.html
を返している
Route 53
CloudFront のデフォルトドメイン.cloudfront.net
で公開しているので、Route 53 を設定し、任意のドメインで見れるようにする
任意のドメインはあらかじめ取得しておく
ホストゾーンの作成
Route 53 -> ホストゾーン
からホストゾーンの作成
ドメイン名を入力し、パブリックホストゾーンを選んで作成
レコードを作成
上記で作成したホストゾーンを選択し、レコードを作成
設定項目 | 設定値 | 備考 |
---|---|---|
ルーティングポリシー | シンプルルーティング | |
レコード名 | - | サブドメインをルーティングする場合は、サブドメイン名を入れる |
エイリアス | On | On にすると、対象の AWS リソースがデフォルトで持っているドメインを選択できる Off にすると、対象の AWS リソースの IP アドレスを直接入力する |
レコードタイプ | A | |
トラフィックのルーティング先 | CloudFront バージニア北部 CloudFront のデフォルトドメイン名 |
しばらく待つと設定したドメイン名でアクセスできるようになる