GoogleCloudFunctions GoのCI/CDを考えてみる

# GCF Goを利用する上でCI/CDを考えてみた
## ざっくり要件
* 自動テスト
* 自動デプロイ

一旦上記部分を中心に考えてみました。

# 自動テスト
この辺はとりあえず必ず実行したいところ。

* go test
* lint, race detector (今回は割愛)

実行タイミングとしては、

* PR作成時
* PRにcommit追加時
* ブランチにpush/merge時

あたりに自動実行されることを想定。
実行するのはJenkinsやCircleCIなどのCIツール上。
ただし、本番・開発環境と合わせるために、CI上でコンテナを起動してその環境下で実行します。

まとめると、

* PR作成などをhookにしてCIが動く
* CI上でコンテナを立ち上げる
* コンテナ上でテストを実行する

を考えました。
以下、実現するための環境構築やらコマンドです。
※サンプルではCIにCircleCIを選択

## Dockerfile
```Dockerfile
FROM golang:1.11-alpine

RUN apk add --update alpine-sdk --no-cache --virtual .build-deps

ENV GO111MODULE=on

COPY . /go/src/github.com/{YOUR_ACCOUNT}/{YOUR_REPOSITORY}
```

## go testコマンド
```
$ go test ./... -v -cover
```

## Circleciのconfig

```.circleci/config.yml
version: '2'
jobs:
  build:
    machine: true
    steps:
      - checkout
      - run:
          name: Create docker image
          command: |
		        docker build -t ${YOUR_IMAGE_NAME} .
      - run:
          name: Run container
          command: |
		        docker run -it -d --name ${YOUR_CONTAINER_NAME} ${YOUR_IMAGE_NAME} sh
      - run:
          name: Testing
          command: |
            docker exec -it ${YOUR_CONTAINER_NAME} sh -c "cd /go/src/${YOUR_ACCOUNT}/${YOUR_REPOSITORY} && go test ./... -v -cover"
workflows:
  version: 2
  build:
    jobs:
      - build
```

上記を準備した上で

* circleciのGUIからgithubと連携
* githubのリポジトリセッティングからwebhookのトリガーとなるイベントを設定

します。
実際に動作させてみた訳ではないですが、こんな感じで実行できるはずです。
※コマンドミス等ありましたらご指摘ください。

要件によるかと思いますが、テスト実行が完了したらコンテナを削除した方がいいかもしれないです。


# 自動デプロイ
以下をトリガーにして自動で実行させる。

* 特定ブランチにpush/merge
* tag作成

今回はCI上からGCFにデプロイをすることを想定します。

早速デプロイの設定やらコマンドやらに行きたいのですが、手動でデプロイを試している段階で問題が発生しました。以下で簡単に触れます。
※この辺は切り出して記事にしたい


## デプロイする上で直面したimport path問題
Goの1.11を利用していると、go.modファイルが作られます。そして、ローカル上で開発する際はgo.modファイル内でrequire, replaceディレクティブを使ってパッケージをimportする際の実態をコントールすることもあると思います。ローカルで開発している分には問題ないのですが、GCFにデプロイする際にパッケージが見つからないというエラーが返却されます。

issueなど諸々みてみたのですが、ローカルと本番環境や開発環境をうまく区別させる方法を見つけられませんでした。
※もしうまいやり方などあれば教えてください・・・！


## import path問題を解決した方法
考えました。
ただ、これが正しいという訳ではないので参考として読んでください。

結論、 `vendorディレクトリ` を使います。
せっかくModulesでvendorディレクトリとはさよならできたのにまた出てきたよ・・・とお思いの方もいるかと思います。すみません。

ただ、開発環境ではvendoringなど瑣末なことに気を取られたくないのですが、実行環境においてはvendorディレクトリがあってもいいしそれでもいいんじゃないかと考えました。

これでうまくデプロイすることができました。
なのでここからはデプロイ時にはvendorディレクトリで依存関係を管理すること前提で進めていきます。

ちょっと気をつけないといけない点などもあります。


## vendorディレクトリで依存環境を管理する上での注意点
実際に試してみたところ以下の点に注意が必要でした。

* Goがインストールされている環境が必要
* go.modをデプロイ対象から外す

それでは、これらを踏まえてcircleciの設定やらその他諸々がどうなったか書いていきます。


## 手順
* circleci上で動かすデプロイ用のコンテナイメージを作成
  * Goがインストールされている
  * google cloud sdkがインストールされている
* .circleci/config.ymlにデプロイ用のjobを追加
* circleciのGUIからEnvironment Variablesからgcp連携用のキーをセット(base64でエンコードしたもの)
* circleciのGUIからEnvironment Variablesからimage取得用のパスワードをセット
* .gcloudignoreにgo.modを追加


## デプロイ用のコンテナ作成
```Dockerfile
FROM google/cloud-sdk:alpine

RUN apk add --update alpine-sdk --no-cache

RUN curl -Lso go.tar.gz "https://dl.google.com/go/go1.11.linux-amd64.tar.gz" \
    && tar -C /usr/local -xzf go.tar.gz \
    && rm go.tar.gz

ENV PATH /usr/local/go/bin:$PATH
```

このファイルを元にイメージ作成し、GCPのコンテナレジストリなどにアップしておき、circleciのjobで使用します。
※cloud-sdkが入っているイメージをベースにそこにGoをインストールしています。


## .circleci/config.yml
```.circleci/config.yml
  deploy:
    docker:
      - image: {YOUR_GCP_REGION}/{YOUR_PROJECT_ID}/{YOUR_IMAGE_NAME}:{TAG_VERSION}
        auth:
          username: _json_key
          password: $GCLOUD_SERVICE_KEY_PLAIN
    steps:
      - checkout
      - run:
          name: Auth gcp
          command: |
            echo $GCLOUD_SERVICE_KEY | base64 -d > ${HOME}/gcloud-service-key.json
            gcloud auth activate-service-account --key-file ${HOME}/gcloud-service-key.json
      - run:
          name: Setting gcp
          command: |
            gcloud --quiet config set project {YOUR_PROJECT_ID}
            gcloud --quiet config set compute/zone {YOUR_PROJECT_ZONE}
      - run:
          name: Make vendor
          command: |
            go mod vendor
      - run:
          name: Deploy
          command: |
            gcloud functions deploy helloWorld --entry-point HelloWorld --runtime go111 --trigger-http
workflows:
  version: 2
  build-and-deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - develop
```

ポイントは

* imageをpullする際のパスワードの設定
* go mod vendorでvendorディレクトリで依存関係を管理
* workflowsの中でdeployはbuildが成功してからとすること
* developブランチにpush/mergeされた時のみとすること（要件によります）

です。


## .gcloudignore
go.mod, go.sumを追加します。


## デプロイまとめ
これで一通り必要なところは書きました。
ちょっとトリッキーかもしれませんが、ローカル場ではimportパス問題など発生せずにデプロイできたので一旦良しとします。


# まとめ
GCF GoのCI/CDを考えてきましたが、一筋縄ではいきませんでしたが、一応構築できる目処がたったので良しとします。サクッとできてしまうエキスパートな方を尊敬せずにはいられない・・・

今回circleciのjobは具体的に試していないので今後どこかの環境で試してみる予定です。
もしこういうやり方もあるよですとか、もっといいやり方あるよなどありましたら是非コメント頂きたいです。


# 番外編
今回はCI上からGCFにデプロイをすることを想定してまとめてみましたが、
後から調べてみると、GCPのCloud Source RepositoriesとCloud Buildを利用してのソリューションもあります。

https://cloud.google.com/blog/products/gcp/automatic-serverless-deployments-with-cloud-source-repositories-and-container-builder

個人的には、Cloud build用のymlファイルに分かりやすくかけますし、
デプロイコマンドのみをバージョン管理できそうなのでこっちの方がありなんじゃないかと思いました。

公式のサンプルから抜粋
https://cloud.google.com/cloud-build/docs/configuring-builds/build-test-deploy-artifacts

```
steps:
- name: 'gcr.io/cloud-builders/gcloud'
  args:
  - functions
  - deploy
  - [FUNCTION_NAME]
  - --source=.
  - --trigger-http
```

こういった形でデプロイコマンドのみにフォーカスしたファイルをgithub上で管理することになるので、保守上良いかなと思っています。