GAミント至上主義

Web Monomaniacal Developer.

Docker + Google Cloud Buildを使ってCIしようとしてる

これまでLinuxの開発サーバー上のシェルコマンドでDockerのビルドやイメージのプッシュ、GKEへのデプロイをしていたけど、先日のGitHub Actionsの発表など世の中CI/CDが当たり前になっているので、DockerとGCPでできる範囲でやってみる。

Cloud Buildについて

Gitレポジトリへのプッシュなどに反応して、Dockerfileのビルド、もしくはcloudbuild.yamlに書いたコマンドを実行してくれる。

今回は元々Dockerでやってたので、Dockerfileでやる。

ビルドしたあと、Container Registryにイメージをプッシュしてくれるところまで。

GKEへのデプロイはまだどうやるかわからない。

cloud.google.com

ソースコードのレポジトリを用意

対象のソースコードは下記。今回はGitHubに公開していたので連携して使う。
github.com

node.jsで動いてHTMLをPDFにするだけのシンプルなアプリケーションなので自動テストもしやすくて。ちょうどいいため。

GCPをすでに使っていて、GitHubか公開する気がないのであればCloud Source Repositoriesでいいと思う。

Cloud Source Repositories - コードを保存、管理するためのプライベート Git リポジトリ  |  Source Repositories  |  Google Cloud

* テストを準備
今回はmocha, chaiユニットテスト、superagentを使ってリクエストのテストを書いた。
網羅性は気にせず、本番で使っている動かないとまずいリクエストや、手動でテストが難しく不安なところだけ書いた。

テストが書けたら稼働中のコンテナ内でmochaコマンドを実行して、大丈夫だったらDockerfileに下記を追加してdocker build時に実行する。

Dockerfile

// 省略
COPY test /hcep/test
RUN mocha
// 省略

expressが終了しない問題

テストが終わってもexpressのコネクションが残っているようで、express.close()しても終わらず、ちょっとハマった。

下記のような丁寧な解決策もあるようだけど、今回はテストと割り切りprocess.exit()することに。
node.js - How Do I Shut Down My Express Server Gracefully When Its Process Is Killed? - Stack Overflow

describeのafter内でprocess.exit()してしまうとChrome側でまだ生きているというエラーが出たので最後に5秒sleepするようにしたら大丈夫だった。

test/requests.js

// 省略
const sleep = (waitSeconds, someFunction) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(someFunction())
    }, waitSeconds)
  })
}

describe('requests routes', (done) => {
  let app
  before(beforeDone =>{
    (async() => {
      const browserPage = await hcPage()
      app =  await expressApp(browserPage)
      beforeDone()
    })()
  })
  after(afterDone => {
    (async ()=> {
      await app.close() 
      sleep(5000, () => {
        console.log('process.exit!')
        process.exit()
      })
      afterDone()
    })()
  })

  const req = request(SERVER_URL)
  it('Health Check GET /hc', async () => {
    await req.get('/hc')
      .expect(200, 'ok', done)
  })
//以下略

before、afterの中でasync無名関数を実行しているのはbefore、afterがPromise返しに対応していないのか、だめだったため内部で同期処理するようにした。
もっとスマートな方法がありそう。

テストがこけるとビルドも止まりエラーで終了する。
ビルド結果のslack通知もほしいけど、Cloud Pub/SubとCloud Functions連携が必要になるのだろうか。めんどい。

Cloud Buildを設定

トリガーの設定は先程のGihHubのレポジトリを指定して、アカウント連携する。
イメージ名のタグに$COMMIT_SHAを入れた以外、特に設定は変えずそのまま。

f:id:uyamazak:20181227150357p:plain

これでビルドが成功したらコミットハッシュ付きのイメージがContainer Registryにプッシュされていくようになった。

動作確認

Cloud Buildの履歴で標準出力されたログを確認することができる

f:id:uyamazak:20181227155324p:plain

無事テストも実行されて通っている。

実行時間もcorei5 4コア、メモリ32GBのローカルサーバーに比べると、コミットごとと思えば遅いけどまあ問題ないレベル。

前回のビルド中にまたpushしてもビルドは再実行されない模様。その場合は手動で実行すればよさげ。

これから

他のCIツールと比べるとできることは少なそうだけど、GitとDockerfileを使っていて、Container Registryにアップするまでならシンプルですぐ使えると思った。

もっといろいろしようとすると独自のcloudbuild.yamlで書く必要があるのでちょっと面倒。

あとはステージング、本番環境への自動デプロイができればCDまでいけそう。

個人的にはすべての開発をクラウド上でやりたくてローカルマシンは端末、エディタとしてだけ使うような開発環境が理想。


Docker/Kubernetes 実践コンテナ開発入門

Docker/Kubernetes 実践コンテナ開発入門