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 - 認証情報サービスアカウントを作成しておきます。次に Google Cloud Console - Pub/Subトピックを作成します。そしてそのトピック対して用意したサービスアカウントにPub/Sub サブスクライバ―Pub/Sub パブリッシャーの権限を与えます。

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

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

package.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オブジェクトを取得して、通知を行うというシンプルなものです。

'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(購読)します。このとき1番目の引数が購読名になるため、ホスト名を使って複数のエージェントが存在する状態でもそれぞれに通知されるようにします。同じ名前にしてしまうと同一のサブスクライバーとみなされて、メッセージがいずれかのエージェントにしか飛びません。

'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

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 メッセージを経由してサブスクライバーがデプロイスクリプトを実行します。