CircleCIをローカルで動かしながらNuxt.jsの静的サイトをAWS S3にデプロイする

## TL;DR

 * CircleCIでビルド, テスト, デプロイを順序立てて実行するにはWorkflowsを使う
 * Workflowsはローカル実行できないため、どうしてもローカルで確認したい場合は各ステップを `build` に切り出す
 * デプロイ先となるAWSのアクセスキーなどはCircleCIのEnvrionment Variablesに登録する
 * CircleCI CLIのローカル実行ではEnvrionment Variablesを `-e` オプションで設定できる

## サンプルプロジェクト

今回はNuxt.jsで作ったフロントエンドプロジェクトをビルド/テスト/デプロイします。

https://github.com/ysd-marrrr/circleci-frontend-nuxt-deploy-20201111

`create-nuxt-app` は次の設定で作りました。

```bash
create-nuxt-app v3.4.0
✨  Generating Nuxt.js project in .
? Project name: map-sample
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier, StyleLint
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? Continuous integration: None
? Version control system: Git
```

## CircleCIのプロジェクト立ち上げ方

CircleCIのProjectsにはアカウント作成に使ったGitHub/Bitbucketのリポジトリが並んでいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/8b2afa25-d8f9-0168-c868-f7d0e384ed0f.png)

このリポジトリの Set Up Project から設定できますが、リポジトリにあるCircleCIの設定ファイルを使用するUse Existing Configを選ぶと **CIが即実行されます。** CircleCI側に環境変数の設定が必要でも即実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/c1e3e416-9e35-e084-32a7-c4f5363f9d73.png)


そのため、プロジェクトを新しく立ち上げる場合は次の手順がおすすめだと考えています。

 1. GitHub/Bitbucketにリポジトリを作成する
 2. CircleCIのSet Up ProjectからAdd Configをクリックして最低限動作する設定をリポジトリに追加する
 3. リポジトリをローカルにcloneして開発を始める
 4. 設定ファイル `.circleci/config.yml` を編集してCircleCI CLIで設定ファイルのバリデーションとローカル実行する。
 5. CircleCIのプロジェクト設定から必要な設定をする
 6. GitHub/BitbucketのリポジトリにpushしてCircleCIのサービスで実行する

すでにCircleCIの設定ファイルがある場合は、Use Existing Configをクリックして実行されたジョブを停止させるか失敗するまで待ちます。


## 複数のジョブに順序を設定する場合はWorkflowsを用いる

次の一連の作業をCircleCIに実行させます。

 1. Nuxtなどのプロジェクトから静的サイトを生成する
 2. プロジェクトのテストを実行する
 3. 生成された静的サイトをS3にデプロイする

この場合、Workflowsという機能を使います。 **この機能を使わないと `build` ジョブのみが実行されます。**

設定ファイルの全体は次の通りです。

```yml:.circleci/config.yml
version: 2.1
orbs:
    aws-s3: circleci/aws-s3@1.0.11
jobs:
  build:
    docker:
      - image: circleci/node:lts
    working_directory: /tmp/work
    steps:
      - checkout
      - restore_cache:
          name: Yarn パッケージのキャッシュの復元
          keys:
            - yarn-packages-{{ checksum "src/yarn.lock" }}
      - run:
          name: 依存関係のインストール
          command: yarn install --frozen-lockfile
          working_directory: src
      - save_cache:
          name: Yarn パッケージのキャッシュの保存
          key: yarn-packages-{{ checksum "src/yarn.lock" }}
          paths:
            - ~/.cache/yarn
      - run: 
          name: 静的サイトのビルド
          command: yarn generate
          working_directory: src
      - persist_to_workspace:
          root: .
          paths:
            - src

  test:
    docker:
      - image: circleci/node:lts
    working_directory: /tmp/work
    steps:
      - attach_workspace:
          at: .
      - run: 
          name: テスト(Jest)
          command: yarn test
          working_directory: src
      - persist_to_workspace:
          root: .
          paths:
            - src

  deploy:
    docker:
      - image: circleci/python:3.7-stretch
    working_directory: /tmp/work
    steps:
      - attach_workspace:
          at: .
      - aws-s3/sync:
          from: src/dist
          to: 's3://$AWS_TARGET_BUCKET'
          arguments: |
            --acl public-read \
            --cache-control "max-age=86400"
          overwrite: true

workflows:
  build-deploy:
    jobs:
      - build
      - test:
          requires:
            - build
      - deploy:
          requires:
            - test
```

`workflows` にWorkflowsで実行したいジョブを記述します。 `requires` を指定することでジョブに順序を設定できます。

## フロントエンドのビルドはcircleci/node

Node.jsを用いて開発しているため `circleci/node` のイメージを使います。 `yarn install` などYarnもこのイメージに入っています。

ステップ実行前の `working_directory` はその通り `checkout` などを実行する前に設定しようとするため、リポジトリ内のディレクトリは指定できません。

Monorepoなどの構成でステップに使うディレクトリを指定したい場合はステップ内で `working_directory` を設定します。

```yml:.circleci/config.yml
jobs:
  build:
    docker:
      - image: circleci/node:lts
    working_directory: /tmp/work
    steps:
      - checkout
      - run: 
          name: yarnパッケージの取得
          command: yarn install
          working_directory: src
      - run: 
          name: 静的サイトのビルド
          command: yarn generate
          working_directory: src
      - persist_to_workspace:
          root: .
          paths:
            - src
```

### 環境変数を使う場合

`process.env` など環境変数を使う場合はCircleCIのEnvironment Variablesで設定したものが使えます。

```json:packages.json
  env: {
    mapsKey: process.env.MAPS_KEY,
  },
```

CircleCIのダッシュボードからプロジェクトを開き、Project SettingsのEnvironment Variablesをで設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/bf1dc331-1e12-bf87-ad97-4dbacffcc19d.png)


CircleCI CLIでローカル実行する場合は `-e` オプションを任意の数だけ追加して環境変数を渡すことができます。

```bash
circleci local execute -e KEY1=VALUE1 -e KEY2=VALUE2
```

### Yarnパッケージのキャッシュ

また、Yarnパッケージは変更がなければ次回のビルドでキャッシュを使うことができます。これによりビルドが高速化します。

https://circleci.com/docs/ja/2.0/yarn/ をほぼそのまま使うと次の通りになります。
`checksum` のコマンドだけステップで指定する `working_directory` の影響を受けないため個別に設定する必要があります。

```yml:.circleci/config.yml
jobs:
  build:
    docker:
      - image: circleci/node:lts
    working_directory: /tmp/work
    steps:
      - checkout
      - restore_cache:
          name: Yarn パッケージのキャッシュの復元
          keys:
            - yarn-packages-{{ checksum "src/yarn.lock" }}
      - run:
          name: 依存関係のインストール
          command: yarn install --frozen-lockfile
          working_directory: src
      - save_cache:
          name: Yarn パッケージのキャッシュの保存
          key: yarn-packages-{{ checksum "src/yarn.lock" }}
          paths:
            - ~/.cache/yarn
      - run: 
          name: 静的サイトのビルド
          command: yarn generate
          working_directory: src
      - persist_to_workspace:
          root: .
          paths:
            - src
```

## AWS S3のデプロイはcircleci/python + aws-s3 Orb

一方で、AWS S3にデプロイする場合はAWS CLIが動作するPythonの環境が必要です。イメージは `circleci/python:3.7-stretch` を使っています。

CircleCIのEnvironment Variablesは次を設定します。

* AWS_REGION
* AWS_ACCESS_KEY_ID
* AWS_SECRET_ACCESS_KEY

AWS CLIをインストールするステップとデプロイに必要なコマンドを実行するステップをそのまま入れても対応できます。

aws-s3 Orbを使うとAWS CLIをインストールするステップを記述する必要がなくなり、コマンド実行も簡単な形で書くことができます。
aws-s3 Orbを使った場合もAWS CLIのインストールから始まるため実行時間はほぼ変わりません。

Orbを使う場合は、 **`versions: 2.1` と宣言してから** 使用するOrbを定義します。次にstepsでOrbを指定して引数を渡すとつかうようになります。

```yml:.circleci/config.yml
version: 2.1
orbs:
    aws-s3: circleci/aws-s3@1.0.11

...

  deploy:
    docker:
      - image: circleci/python:3.7-stretch
    working_directory: /tmp/work
    steps:

...

- aws-s3/sync:
          from: src/dist
          to: 's3://$AWS_TARGET_BUCKET'
          arguments: |
            --acl public-read \
            --cache-control "max-age=86400"
          overwrite: true
```

`--acl public-read` をつけないとバケットのWebホスティングを有効にしても403 Forbiddenになります。


## ジョブで複数のimageを設定する方法は難しい

NodeでビルドしながらPython製のAWS CLIを使ったデプロイをしたい、を実現する方法としてジョブで使うイメージを複数指定する方法があります。

しかし、現在のバージョンで組み合わせられるイメージを見つけることができませんでした。

```yml:.circleci/config.yml
jobs:
  build:
    docker:
      - image: circleci/node:14.15.0
      - image: circleci/python:3.7
```

> Error:
> Unexpected environment preparation error: Error response from daemon: can't join IPC of container 53363ebc1e04ac6613b863d3fe80f6f28757182d7e725250c8981f55b8c4d3b2: non-shareable IPC (hint: use IpcMode:shareable for the donor container)                                             >                                                             >                                                             > Step failed
> Task failed


## ジョブ間のデータはやり取りされない: `persist_to_workspace` を使う

ジョブを続けて書いてWorkflowsで連結しても **前のジョブで生成したデータにはアクセスできません。**

次のジョブに移る前に `persist_to_workspace` と宣言してから、次のジョブで `attach_workspace` を使ってデータを取り出します。

`persist_to_workspace`の`root`はジョブの作業ディレクトリから相対/絶対パスで指定し、`paths`はその中で次のジョブに残したいディレクトリを指定します。

`attach_workspace`の`at`は同様にジョブの作業ディレクトリから相対/絶対パスで指定します。

```yml:.circleci/config.yml
jobs:
  build:
    docker:
      - image: circleci/node:lts
    working_directory: /tmp/work
    steps:
      - run: 
          name: yarnパッケージの取得
          command: yarn install
          working_directory: src
      - run: 
          name: 静的サイトのビルド
          command: yarn generate
          working_directory: src
      - persist_to_workspace:
          root: .
          paths:
            - src

  test:
    docker:
      - image: circleci/node:lts
    working_directory: /tmp/work
    steps:
      - attach_workspace:
          at: .
      - run: 
          name: テスト(Jest)
          command: yarn test
          working_directory: src
```

## CircleCI CLIのローカル実行ではWorkflowsはサポートされない

Workflowsを指定しても `build` ジョブだけが実行されます。Workflowを通して動作確認したい場合はローカルではなくCircleCIのサービスで実行する必要があります。

ジョブを選んで実行する方法が見つからなかったので、各ジョブを切り出して `build` ジョブとして実行するようにしています。
もちろん「静的サイトのビルドをしないと `deploy` ジョブで必要な `dist` にファイルが生成されない」など前のジョブに依存する場合は前提となる状態を再現しています。


## ローカル実行は `persist_to_workspace` もサポートされない

`persist_to_workspace` を使ったジョブで `circleci local exec` を実行すると次のメッセージが表示されます。
こちらもCircleCIのサービス側で実行すると正しく処理されます。

> Warning: skipping this step: Missing workflow workspace identifiers, this step must be run in the context of a workflow


## 参考

ワークフローを使用したジョブのスケジュール - CircleCI
https://circleci.com/docs/ja/2.0/workflows/

CircleCI2.0のWorkflowを試してみる - Qiita
https://qiita.com/sawadashota/items/ba89382d563bc90bb5cd

環境変数の使用 - CircleCI
https://circleci.com/docs/ja/2.0/env-vars/

CircleCI のローカル CLI の使用 - CircleCI
https://circleci.com/docs/ja/2.0/local-cli/#limitations-of-running-jobs-locally

デプロイの構成 - CircleCI
https://circleci.com/docs/ja/2.0/deployment-integrations/

Nuxt.js+CircleCIで静的ページをAWSのS3へデプロイする - Qiita
https://qiita.com/nishinoshake/items/21af46b4101392ffc666

CircleCI での Yarn (npm の代替) の使用 - CircleCI
https://circleci.com/docs/ja/2.0/yarn/

Configuring CircleCI - CircleCI
https://circleci.com/docs/2.0/configuration-reference/#persist_to_workspace