Vue.js + AWS S3でのHTTPS対応とCircle CIでの自動デプロイ手順

Vue.js で作ったSPAを静的サイトとしてAWS S3（+ CloudFront）からホスティングすれば、ランニングコストを低く抑えられるのでオススメです。アクセス数にもよりますが、個人のウェブサイトなどであれば**100円程度/月**で済んだりします。

今回はそんな Vue.js の **AWS S3 へのデプロイ手順**と、CloudFront を使った**独自ドメイン + HTTPS 対応方法**、さらに CircleCI を使った **CI/CD の導入**までザッと整理します。

# 最終的な本番環境イメージ

最終的な本番環境の構成イメージは次のとおりです。

![vue_s3_cloudfront_circleci.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138789/6eacd1ac-e264-5bc9-54d8-bfa3696a11b3.jpeg)


# Vue.js アプリの準備 (前提)

前提として、ここでは Vue.js アプリのデプロイ手順を扱います。

Vue.js の基礎知識については、過去の記事をご覧ください。

* [Vue.jsを100時間勉強して分かったこと](https://qiita.com/kskinaba/items/3e8887d45b11f9132012)
* [Vue.js ユニットテストの基本まとめ](https://qiita.com/kskinaba/items/d23259060e6e13b7353c)
* [Vue.jsの開発環境をDockerで構築する手順](https://qiita.com/kskinaba/items/44077529ff2abcd1fcd4)

# 1. AWS S3 へのデプロイ手順

まずは単純な AWS S3 へのデプロイ手順です。

## S3の準備

本番サーバとなるS3のセットアップです。

### バケット新規作成

[S3 のコンソール](https://console.aws.amazon.com/s3/home?region=ap-northeast-1)からバケットを新規作成します。

「バケット」はドメインと同じようなネーミングにしておくと分かりやすいです。「ブロックパブリックアクセス」は静的サイトとして公開するので**全てオフ**にします。

### 静的サイトとして使う

バケット作成後、プロパティタブから「静的ウェブサイトホスティング」を選択して、静的サイトとして使えるようにします。

<img width="1024" alt="スクリーンショット 2019-11-23 12.27.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138789/16bd3b5d-0386-d791-7b78-2aac724a756d.png">

設定項目は次のとおりです。

* インデックスドキュメント（必須）: `index.html`
* エラードキュメント（必須）: `error.html`

### バケットポリシーの追加

アクセス権限タブから「バケットポリシー」を選択し、次のようにJSONコードでポリシーを追加します。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        }
    ]
}
```

## S3へのデプロイ

これで最低限の本番環境は整ったので、開発コンソールで本番用コードをビルドします。

```
npm run build
```

生成される `dist` ディレクトリを先ほど作った S3 にアップします。

そしてブラウザから次のURLを叩けば、 Vue.js の静的サイトが表示されます。

```
<bucket-name>.s3-website-<AWS-region>.amazonaws.com/dist
```

例えば、バケット名が「mybucket」、リージョンが「東京（ap-northeast-1）」の場合は、S3のURLは次のようになります。

```
mybucket.s3-website-ap-northeast-1.amazonaws.com/dist
```

これでデプロイ自体は完了です。

# 2. CloudFront での独自ドメイン + HTTPS化手順

次にデプロイしたS3の静的サイトを、**独自ドメイン + HTTPS でアクセスできるようにします。**

これは **Route53 + AWS Certificate Manager + CloudFront** で実現できます。

## Route53 独自ドメイン登録

### ケース1: Route53 で新規取得

コンソールの[登録済みドメインページ](https://console.aws.amazon.com/route53/home?region=us-east-1#DomainListing:)の「ドメインの登録」から独自ドメインを購入します。

### ケース2: Route53 のサブドメインを使う

Route53 に登録済のドメインのサブドメインを使う場合は、必要な追加設定はありません。

### ケース3: 他社ドメインを Route 53 に登録

他社で取得したドメインを使う場合、「Route53」と「他社ネームサーバ」のそれぞれで設定が必要です。

#### Route 53 での設定

コンソールの[ホストゾーンページ](https://console.aws.amazon.com/route53/home?region=us-east-1#hosted-zones:)の「ホストゾーンの作成」から他社取得ドメインを登録します。

タイプ「NS」と「SOA」がデフォルトで自動生成されます。「NS」は他社ネームサーバに入力する情報です（次のような4つの値）。

```
ns-2xx.awsdns-26.com.
ns-1xxx.awsdns-36.org.
ns-1xxx.awsdns-33.co.uk.
ns-8xx.awsdns-40.net.
```

#### 他社ネームサーバでの設定

Route53 で自動生成された NS レコードの値を、他社ネームサーバ1〜4として登録します。

## AWS Certificate Manager で証明書登録

[AWS Certificate Manager のコンソール](https://console.aws.amazon.com/acm/home?region=us-east-1#/)にて、Route53 に登録したドメインの証明書を取得します。

*（注意）CloudFront で HTTPS 化をする場合、必ず「US EAST」で登録しないといけません。東京リージョンを選ばないように注意しましょう。*

`*.独自ドメイン` と設定すれば、サブドメイン分もまとめて登録できます。

証明書の発行が完了すると、DNS 設定が書かれたCSVファイルをダウンロードできます。

## Route53 に CNAME 追加

ダウンロードした DNS 設定の内容にしたがって、Route53 に CNAME を追加します。

ドメインのホストゾーン画面から「レコード設定の作成」を選択し、次のように設定します。

* 名前: DNS 設定の `Record Name`（例 `_3560a31fe01xxxxxxxxxx`）
* タイプ: `CNAME`
* 値: DNS 設定の `Record Value`（例 `_xxx.acm-validations.aws.`）

（僕の環境下では、追加した CNAME はすぐに反映されました。）

## CloudFront の設定

ここまで来て、やっと CloudFront の設定ができます。

[CloudFront のコンソール](https://console.aws.amazon.com/cloudfront/home?region=ap-northeast-1#)からディストリビューションを追加します。「Web」を選択して、次のように設定していきます。

### Origin Settings

* Origin Domain Name: 本番 S3 バケットを選択
* 以外、デフォルト

### Default Cache Behavior Settings

* Viewer Protocol Policy: Redirect HTTP to HTTPS
* 以外、デフォルト

### Distribution Settings

* Alternate Domain Names (CNAMEs): 本番で利用するドメイン
* SSL Certificat: Custom SSL Certificate（ACM で作った証明書を選択） 
* Default Root Object: `dist/index.html`
* 以外、デフォルト

以上の内容でディストリビューションを作成します。

作成後、CloudFront ディストリビューションの「Domain Name」をコピーします。

## Route53 に CloudFront のエンドポイントを追加

Route53 ホストゾーンの「レコードセットの追加」から、先ほどコピーした「Domain Name」をAレコードとして追加します。

* 名前: 独自ドメイン（もしくはサブドメイン）
* タイプ: A
* 値: CloudFront ディストリビューションの「Domain Name」

ブラウザから独自ドメインを叩いて、S3の静的サイトがHTTPSで表示できればOKです。

## 403, 404エラーに対応

```
Failed to load resource: the server responded with a status of 403
```

`vue-router` などで URL が動的に変わる場合、何も設定をしていないとブラウザのコンソール上でこういったエラーが出てしまいます。

例えば、コンタクトページ `/contact` を Vue.js SPA 内で作った場合、S3上に `dist/contact.html` という実際のファイルがないので、このようなエラーが出ると考えられます。

### 解決方法

CloudFront でエラーページの設定すれば、このエラーを解決できます。

CloudFronnt ディストリビューションの「Error Pages」タブからカスタムエラーレスポンスを作成します。

* HTTP Error Code: 403, 404（2回に分けて作成）
* Customize Error Response: Yes
* Response Page Path: `/`
* HTTP Response Code: 200 OK

# 3. CircleCI での CI/CD 導入手順

コードを更新するたびに手作業でテストして、ビルドして、デプロイするのは大変なので、**CircleCI で一連の作業を全て自動化します。**

## 事前準備: アカウント連携

アカウントを持っていない場合は、[CircleCI](https://circleci.com/) のホームページからサインアップして、自身の GitHub アカウントと連携させます。

## IAM ポリシーの追加

CircleCI に付与する S3 のファイル更新権限を準備します。

[IAM のコンソール](https://console.aws.amazon.com/iam/home?#/policies) から「ポリシーの作成」を選択し、次の JSON でポリシー内容を規定します。（`<bucketname>` は各自のバケット名）

```json
{
   "Version": "2012-10-17",
   "Statement":[
      {
         "Effect":"Allow",
         "Action":[
            "s3:ListBucket",
            "s3:GetBucketLocation"
         ],
         "Resource":"arn:aws:s3:::<bucketname>"
      },
      {
         "Effect":"Allow",
         "Action":[
            "s3:PutObject",
            "s3:PutObjectAcl",
            "s3:DeleteObject"
         ],
         "Resource":"arn:aws:s3:::<bucketname>/*"
      }
   ]
}
```

ポリシー作成後に表示される「アクセスキーID」と「シークレットアクセスキー」をコピーしておきます。

<img width="1028" alt="スクリーンショット 2019-11-22 21.39.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138789/3089fa50-16cb-ec3d-e2c1-64dabf316ec4.png">

## CircleCI の権限設定

CircleCI プロジェクトの設定画面「AWS Permissions」で、先ほど生成した「アクセスキーID」と「シークレットアクセスキー」を登録します。

<img width="1368" alt="スクリーンショット 2019-11-22 21.39.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138789/5dfd3592-f518-195c-9402-26c5e3509e6c.png">

## CircleCI の CI/CD 設定

プロジェクトのルートパスに  `.circleci/config.yml` を作成し、CircleCI の CI/CD 設定を書きます。

AWS S3 に自動デプロイする設定は次のとおりです。

*（注意）YAMLファイルではインデントが重要です*

```yml:.circleci/config.yml
version: 2.1
 
orbs:
  aws-s3: circleci/aws-s3@1.0.11
 
executors:
  default:
    docker:
      - image: circleci/node:10.17.0
      - image: circleci/python:2.7

commands:
  npm_install:
    steps:
      - restore_cache:
          key: dependency-cache-{{ checksum "package.json" }}
      - run: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package.json" }}
          paths:
              - node_modules

jobs:
  build:
    executor: default
    working_directory: ~/repo
    steps:
      - checkout
      - npm_install
      - run: npm run test

  deploy:
    executor: default
    working_directory: ~/repo
    steps:
      - checkout
      - npm_install
      - run:
          name: build
          command: API_ID=$API_ID API_TOKEN=$API_TOKEN npm run build
      - aws-s3/sync:
          from: dist
          to: s3://mybucket/dist
          overwrite: true

workflows:
  version: 2
  continuous-deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: master
```

### 全般

`orbs` は、デプロイに必要なコマンドなどの設定がまとめられたパッケージで、ここでは `circleci/aws-s3@1.0.11` を使っています。

コマンドの実行環境には `docker` の `circleci/node:10.17.0` と `circleci/python:2.7` を使っています。

`jobs` で `build` プロセス（CI）と `deploy` プロセス（CD）を定義し、`workflows` で処理の順番を指定しています。

`commands` では、各プロセスで実行するコマンドの実行内容を定義しています。

### CI (継続的インデグレーション)

`build` プロセスの部分です。`npm_install` して `npm run test` するだけの単純な設定です。

これで GitHub に push されたコードに対して自動テストが実行されます。（マージ前のブランチ画面でテストが成功したか確認できます。）

### CD (継続的デリバリー)

`deploy` プロセスの部分です。

まず、`npm_install` し、`npm run build` で本番用コード `dist` を生成します。

そのビルドしたコードを `aws-s3/sync` で S3 にアップロードします。

`workflows` で `filters: branches: only: master` を指定しているので、master ブランチへのマージのみをトリガーとして、この `deploy` プロセスが走ります。

### 環境変数の設定

`.circleci/config.yml` で使う環境変数は、CircleCI の管理画面（プロジェクトページ > Environment Variables）で定義します。

#### .env ファイルの取り扱い

Vue.js アプリで使う環境変数は、`.env` ファイル内に `VUE_APP_` の接頭辞で定義します（公式ドキュメントは[こちら](https://cli.vuejs.org/guide/mode-and-env.html#environment-variables)）。

この `.env` ファイルには API キーなどの機密情報が含まれるので、通常は `.gitignore` で git の管理対象から外します。

一方、CircleCI では GitHub から本番用コードをビルドするので、`.env` がないと問題が発生してしまいます。

これを回避するために、`npm run build` 実行時に CircleCI の管理画面で定義した環境変数を渡してあげます。

例えば、`API_ID` と `API_TOKEN` という環境変数を定義した場合は、次のようになります。

```
command: API_ID=$API_ID API_TOKEN=$API_TOKEN npm run build
```

なお、渡された環境変数を Vue.js 内で使うには、`webpack.config.js` に `webpack.DefinePlugin` を追加しないといけません。（これについての詳細は、今後別記事にまとめます。）

*※ もっと効率的な環境変数の渡し方があれば教えていただきたいです！*

## デプロイ動作確認

コードを変更して GitHub にプッシュし、master ブランチにマージさせます。

そのマージをトリガーに処理が走れば、CircleCI の管理画面にステータスが表示されます。

`deploy` プロセスまで完了したら、変更が本番環境に反映されます。

### 本番ファイル更新がすぐに反映されない？

S3 の本番ファイルを更新しても、CloudFront のエッジサーバ上にキャッシュが残っていると、その更新は即座に反映されません。

CloudFront の Invalidation を実行すれば、このキャッシュをリセットできます。より詳しい内容は以下の記事でまとめています。

* [AWS S3更新時にLambdaでCloudFrontのInvalidationを自動実行](https://qiita.com/kskinaba/items/dcf9693dd034517e114a)

# References

- [例: 静的ウェブサイトをセットアップする - Amazon Simple Storage Service](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/HostingWebsiteOnS3Setup.html)
- [Vue.js Router: history mode and AWS S3 (RoutingRules) - StackOverflow](https://stackoverflow.com/questions/43095823/vue-js-router-history-mode-and-aws-s3-routingrules)
- [circleci/aws-s3@1.0.12 - CircleCI](https://circleci.com/orbs/registry/orb/circleci/aws-s3)
- [Modes and Environment Variables - Vue CLI](https://cli.vuejs.org/guide/mode-and-env.html#environment-variables)