GitLab CI から Google Cloud Pub/Sub 経由で自動デプロイ

自分用のちょっとしたWebサービスをさくら VPS環境に構築しています。こんなオレオレサービスでも Git リポジトリの master ブランチに push したら自動的にデプロイできるようにしたい。毎回 SSH して手動で更新はしたくない。

一般的な方法だと、どうしてもデプロイ先のサーバーにリクエストの受け先を用意する必要があります。概ね HTTP 経由で受けないといけないので、そこから派生するアクションの内容から鑑みても、セキュリティ的にグローバル空間に置かれたサーバでやりたくないものです。

そこで自前で Pub/Sub サービスを経由してやってみることにしました。Pub/Sub であれば懸念事項のアクセスを受け入れることなく、Subscriber も Pull 型で設置できます。

## Google Cloud Console

まず [Google Cloud Console - 認証情報](https://console.cloud.google.com/apis/credentials) で**サービスアカウント**を作成しておきます。次に [Google Cloud Console - Pub/Sub](https://console.cloud.google.com/cloudpubsub/) で**トピック**を作成します。そしてそのトピック対して用意したサービスアカウントに**Pub/Sub サブスクライバ―**と**Pub/Sub パブリッシャー**の権限を与えます。

## 自動デプロイの仕組みを作る

スクリプトは Node.js を使います。gcloud モジュールをグローバルで使えるようにしておきます。下記の package.json の定義とおり サブスクライバーとなる **agent.js** と通知をする **publish.js** をそれぞれ作ります。

### package.json

```json
{
  "name": "myapp",
  "scripts": {
    "agent": "NODE_PATH=`npm root -g` node agent.js",
    "publish": "NODE_PATH=`npm root -g` node publish.js"
  },
  "dependencies": {
    "gcloud": "^0.36.0"
  },
  "private": true
}
```

## 通知

GitLab CI から呼び出して Pub/Sub にデプロイメッセージを発行するスクリプトを作ります。

### publish.js

gcloudオブジェクトからpubsubオブジェクトを取得して、トピック名からtopicオブジェクトを取得して、通知を行うというシンプルなものです。

```javascript
'use strict';

const gcloud = require('gcloud')({
  keyFilename: 'サービスアカウントのキーJSONファイルパス',
  projectId: 'Google Cloud ConsoleのプロジェクトID'
});

const pubsub = gcloud.pubsub();
const topic = pubsub.topic('トピックID');

topic.publish({
  data: {
    command: 'deploy'
  }
}, function(err) {
  if (err) {
    console.error(err);
  } else {
    console.info('Published deploy message!');
  }
});

```

## デプロイエージェント

トピックのサブスクライバーとしてメッセージを受信したら、Gitリポジトリから最新の内容を反映してデーモンを再起動するエージェントを作ります。

### agent.js

topicオブジェクトを取得して、subscribe（購読）します。このとき１番目の引数が購読名になるため、ホスト名を使って複数のエージェントが存在する状態でもそれぞれに通知されるようにします。同じ名前にしてしまうと同一のサブスクライバーとみなされて、メッセージがいずれかのエージェントにしか飛びません。

```javascript
'use strict';

const os = require('os');
const path = require('path');
const execFile = require('child_process').execFile;

const gcloud = require('gcloud')({
  keyFilename: 'サービスアカウントのキーJSONファイルパス',
  projectId: 'Google Cloud ConsoleのプロジェクトID'
});

function startDeploy() {
  const child = execFile('deploy.sh',
    (error, stdout, stderr) => {
      if (error) {
        throw error;
      }
      console.log(stderr);
      console.log(stdout);
    });
}

const pubsub = gcloud.pubsub();
const topic = pubsub.topic('トピックID');
const name = 'sub_' + os.hostname();

var opts = {
  autoAck: true, // メッセージを取得したことの承認を自動で行う
  reuseExisting: true, // 再購読時にエラーならないように再利用する
  interval: 60
};

topic.subscribe(name, opts, (err, subscription, apiResponse) => {
  if (err) {
    console.error(err);
    return;
  }

  console.info('Subscribed');

  subscription.on('message', (msg) => {
    const data = msg.data;
    const command = (data && data.command) || null;

    if (command === 'deploy') {
      console.info('Received deploy message id:%s', msg.id);

      try {
        startDeploy();
      } catch (ex) {
        console.error(ex);
      }
    } else {
      console.warn('Received unexpected message:');
      console.warn(msg);
    }
  });
});

```

注意事項として Pull 型サブスクライバーは接続の度に課金カウンタが上がるので、 interval はできるだけ長くした方が良いです。
メッセージが来たら下記のような deploy.sh スクリプトを用意してリポジトリを更新してサービスをリフレッシュします。

### deploy.sh

```
#!/bin/bash
git fetch --all
git checkout --force origin/master

npm run deploy # サービスリフレッシュ
echo "Deployed"
```

## GitLab CI からキック

下記の設定ファイルをリポジトリに設置するとデプロイステージで発行します。gcloudモジュールのインストールはそれなりの大きさで時間がかかるので、 [https://hub.docker.com/r/tilfin/gitlab-deployer/] で公開している Docker で Node.js と gcloud を入れてあります。これコンテナを deploy タグ付きで CI Runner 登録しておく良いでしょう。

### .gitlab-ci.yml

```yaml
stages:
  - deploy

deploy_job:
  type: deploy
  script:
    - npm run publish
  tags:
    - deploy
  only:
    - master
```

## 動作確認

実行環境でも Node.js + gcloud をセットアップしておき `npm run agent` してサブスクライバーを起動しておきます。
GitLab のリポジトリの master に push すると Pub/Sub メッセージを経由してサブスクライバーがデプロイスクリプトを実行します。

[tilfin's note](http://tilfin.hatenablog.com/entry/2016/07/21/023221) よりクロスポスト