SPAとGoAppEngineのサービスをCircleCIでデプロイする（サンプル）


## はじめに
CircleCIでSPA（それ単体でも動くもの）とGoAppEngineをデプロイできるようにしたので設定方法のメモです。

### やりたいこと

こんなディレクトリ構成で

```
myrepo/
├── spa/
└── go-app/
```

SPAはそれ単体でも動いていて、GoAppEngine側にそのSPAを取り込んで動くようなWebアプリをデプロイしたい。

また、CI上のJobはSPAとGoAppでそれぞれ分けたい。
→JSとGo両方入ったイメージを作りたくないため。（自分でイメージを管理したくない等の理由。。。）

### 障害となったポイント

- SPAのJob(ビルド)の結果をGoAppのJobに渡すのに工夫が必要。 
    - Job間のImageが異なるので、キャッシュの復元時にPermission問題とかが出てくる。 
- AppEngineにデプロイできて、Goも使えるイメージの用意が大変。
    - これはやや手抜きですが今回メルカリのイメージを使って解決しました。

## 設定方法

### ディレクトリ構成
ほぼ上の再掲ですが、こんな感じです。

```
myrepo/
├── .circleci/
├── spa/
│   └── public/
└── go-app/
    └── public/
```

- `.circleci/`にはご存知の通りCircleCIのconfig.ymlが入ります。
- `spa/`には、Javascriptで作られたアプリが入っています。
    - npm scriptでビルド等ができるように準備されてるものとします。
    - 成果物は`public/`に生成されます。 [^1]
- `go-app/`には、GoAppEngineのアプリが入っています。
    - その下の`public/`に、SPAのビルド成果物を持ってくるようにします。

[^1]: ちなみに、今回は全く内容に出てきませんが自分はGatsbyJSで作った成果物を利用しています

### config.yml

早速ですがymlファイルです。ポイントとなる箇所にコメントを入れています。

```yaml
version: 2.1

jobs:
  build:
    # ポイント(1)
    working_directory: /tmp/myapp/spa
    docker:
      - image: circleci/node:10
    steps:
      - checkout:
          # ポイント(1)
          path: /tmp/myapp
      - run:
          name: Update npm
          command: "sudo npm install -g npm@latest"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: Install npm wee
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - node_modules
      - run:
          name: Build SPA
          command: npm run build
      # ポイント(2)
      - save_cache:
          key: assets-cache-{{ .Environment.CIRCLE_SHA1 }}
          paths:
            - /tmp/myapp/spa/public

  deploy:
    working_directory: /go/src/github.com/myrepo/myapp/go-app
    docker:
      - image: mercari/appengine-go:1.11
    steps:
      - checkout:
          path: /go/src/github.com/myrepo/myapp
      - run:
          name: Install dep
          command: go get -u github.com/golang/dep/cmd/dep
      - restore_cache:
          key: dependency-cache-{{ checksum "Gopkg.lock" }}
      - run:
          name: Install go deps
          command: make setup
      - save_cache:
          key: dependency-cache-{{ checksum "Gopkg.lock" }}
          paths:
            - vendor
      # ポイント(3)
      - restore_cache:
          key: assets-cache-{{ .Environment.CIRCLE_SHA1 }}
      - run:
          name: Get spa assets
          command: mv /tmp/myapp/spa/public ./public
      # ポイント(4)
      - run:
          name: Set gcloud credential
          command: |
            echo $GCLOUD_SERVICE_KEY | base64 --decode | gcloud auth activate-service-account --key-file=-
            gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
            gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
      - run:
          name: Deploy to GAE
          command: make deploy-ci

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

#### ポイント(1) 複数のジョブ間でキャッシュを復元できるようにSPA側は/tmpで作業する

これは上の方でも書きましたが、Permission問題などへの解決策です。
`save_cache`でビルドの成果物を複数のJobで取り回したいとき、
例えば

- /home ディレクトリ以下でcacheをしてみた場合
    - 複数Jobでユーザーが異なると、cacheを復元できない場合があります。
- / (root) ディレクトリ以下でcacheをしてみた場合
    - root権限を持たないユーザーで動くImageの場合、cacheを置けない/復元できない場合があります。

今回のconfig.ymlだとnode.jsのImageはcircleciユーザーで動いていて、appengine-goのimageはrootユーザーで動いてるので、後者に当たります。（/goの下とかにsave_cacheできれば最高なんだけど、circleciユーザー権限なんでそれができない)

このことについては[こちらにも記載](https://support.circleci.com/hc/ja/articles/360004250693-%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%AE%E5%BE%A9%E5%85%83%E3%81%8C-Permission-Denied-%E3%81%A7%E5%A4%B1%E6%95%97%E3%81%99%E3%82%8B)があります。
本当はジョブ間で共通のユーザー（またはイメージ）で動くようになってればこんなハマらないのですが
今回は自前でイメージを作りたくなかったのでこうしました。

#### ポイント(2) SPAの成果物をsave_cacheする
後にGoApp側でSPAを取ってこれるようにsave_cacheしています。
成果物はcommit単位で異なると想定して `.Environment.CIRCLE_SHA1` でキャッシュしています。

#### ポイント(3) GoApp側Jobでrestore_cacheでSPAの成果物を取得
上記の通りsave_cacheした成果物を復元しています。
そのすぐ下の`run`で、GoApp側の欲しい場所に成果物`public`をコピーしています。（ちょっとダサい・・・。）

#### ポイント(4) AppEngineのデプロイ用にCredentialを設定
[CircleCIのドキュメントに書いてる通り](https://circleci.com/docs/2.0/google-auth/)ですが、ここでGoogleCloudのCredentialを設定しています。

環境変数`GCLOUD_SERVICE_KEY`には、base64エンコードしたGoogleCloudサービスアカウントのキーファイル内容を設定しておきます。
以下のようなコマンドで生成できるはず。

```
base64 my-service-account-key.json
```

AppEngineにデプロイするため、サービスアカウントには少なくとも以下のような権限が必要です。

- App Engine デプロイ担当者
- App Engine サービス管理者
- Cloud Build 編集者
- ストレージのオブジェクト管理者

肝心のデプロイ部分は`make deploy-ci`で誤魔化してますが、中身は`gcloud app deploy --quiet`するだけです。


ポイントとしては以上です。


## 終わりに

今回のconfig.ymlではTestやLint等が抜けてるので、ちゃんとした物を作る場合は入れておきたいです。
SPAのtest等は、今のbuildの中でそのままやれば良いかなと思います。
Goのtest等は、ひとつjobを増やして今のdeployとは分けたいかな・・・。綺麗なやり方は模索中です

また、そんな風にjobが増えたりでキャッシュのsave/restoreがなんども行われる場合は、CircleCI2.0のcommandsを使うとDRYに書けるのでオススメです。今回は、そんなに複雑にならなかったので使いませんでした

以上、半分は自分向けの備忘録ですが、誰かの参考になれば幸いです。