microCMSのWebhookから中継サーバー経由でGitLabのジョブを実行させる（比較的安全な）方法

# この記事を書いた背景
microCMSの記事の更新情報をWebhook経由で送信しCI/CDツ⁠ー⁠ルでデプロイする場合に、ソースをGitHubで管理している場合は、GitHub Actionsなどを使うケースが多いと思います。
ただクライアントによってはGitを管理しているのがGitLabだった場合GibLabのCI/CDを使いたいケースがあり、そのために調べたことをメモしておこうと思い書きました。

## GitLab CI/CDのパイプラインのトリガーの仕様

以下のように基本的にはGitLabのCI/CDの外部からのパイプラインのトリガーでは、`POSTクエリ`にアクセスのための`TOKEN`などを入れる形にする必要があります。
```bash
curl --request POST \
     --form token=<token> \
     --form ref=<ref_name> \
     "https://gitlab.example.com/api/v4/projects/<project_id>/trigger/pipeline"
```
[公式Docs - Trigger a pipelineより](https://docs.gitlab.com/ee/ci/triggers/#trigger-a-pipeline)

## micrCMSのWebhookの仕様

各サービスに合わせた設定はあるのですが、2023年5月8日時点ではGitLab用の設定項目はありません。
そうしますとカスタム通知を使用する形となります。
カスタム通知の設定では`POSTクエリ`を追加する項目は無く、追加できる項目はカスタムリクエストヘッダーとなります。
以下の公式にもあるようなGETクエリに入れる方法では問題なくジョブを実行できるのですが、`GETクエリ`にトリガーの`TOKEN`を入れるのはセキュリティ的な懸念があります。

```bash
curl --request POST \
    "https://gitlab.example.com/api/v4/projects/<project_id>/trigger/pipeline?token=<token>&ref=<ref_name>"
```

またカスタムリクエストヘッダーに`TOKEN`を入れて更新情報の送信し、それをGitLab単体で上手く処理をするという方法で上手く動くやり方を見つけることができませんでした。


## 発想の転換 - 中継サーバーの使用
そこで発想を転換して中継サーバー経由で行うという形をしてみました。
以下のような順序で実現できると思いサンプルを作成しテストをしてみました。

1. microCMSのWebhookのカスタム通知に`TOKEN`など必要なデータを格納し、一旦中継サーバーに送信させます。
1. 中継サーバーにて受け取ったデータのカスタムリクエストヘッダーから`TOKEN`、`ref_name`や`gitlab-ci.yml`の中で使用するであろう`variables`などを抽出します。
1. 抽出して各データをPOSTクエリにいれてGitLabに送信します。
1. GitLabでは受け取ったデータをもとに`gitlab-ci.yml`で指定したCI/CDのジョブを実行する

# 具体的な設定例
各サービスの具体的な設定例を記載していきます。

## GitLabの設定
GitLabのクラウド版（SaaS）を使用する想定で設定します。

### gitlab-ci.ymlの作成

左メニューのビルドのパイプラインエディタにてgitlab-ci.ymlを作り編集します。
```yaml
stages:          # List of stages for jobs, and their order of execution
  - build
  - test
  - deploy
variables:
  RUN_BUILD_MODE: "test"
test-job:
  stage: test
  script:
    - echo "$RUN_BUILD_MODE"
  rules:
    - if: '$RUN_BUILD_MODE == "test"'
build-job:
  stage: build
  script:
    - echo "$RUN_BUILD_MODE"
  rules:
    - if: '$RUN_BUILD_MODE == "build"'

```

テスト用としてなので、ビルドが走ったら`RUN_BUILD_MODE`変数の内容によってジョブを振り分けて`RUN_BUILD_MODE`の内容を`echo`するだけの処理になります。

### トリガーTOKENの追加

次にGitLab管理画面の設定の左メニューのCI/CDからパイプラインのトリガーで、トリガートークンの追加をしておきます。

## micrCMSのWebhookの設定
API設定のWebhookの設定をしていきます。

### 基本的な設定箇所

`Webhookの識別名（任意）`、`中継サーバーのURL`、`シークレット`を入力していきます。
`シークレット`はWebhookリクエストがmicroCMSからのものであることを検証するためのヘッダ値となります。
[micrCMSマニュアル | シークレット](https://document.microcms.io/manual/webhook-setting#hb2d39bd6cc)

### カスタムリクエストヘッダーの設定

1. GitLabのパイプラインのトリガー用トークンのためにKeyを`X-GITLAB-TOKEN`としてトークンを格納
2. `X-GITLAB-REF-NAME`にjobを実行するブランチ名を格納
3. `X-BUILD-MODE`にはgitlab-ci.ymlでjobを実行するstageを振り分けるためにvariablesで設定した変数へ渡す値を格納させる

ここへの値は、`build`や`test`、`review`などとして`gitlab-ci.yml`で処理を振り分けるための変数に入れるための値となります。

## 中継サーバーの設定
適当なサーバーを用意します。
例としてのサンプルはPHPで記載しておきますが、その他の言語でも良いと思います。

### 環境変数の追加
micrCMSのシークレット（キー）やGitLabのプロジェクトIDなどあまり外に漏らしたくないものはハードコーディングしないようにします。
環境変数にこれらを追加して使用します。`httpd.conf`や`php.ini`、`.htaccess`などに追加して設定します。

例：
```
setEnv MICROCMS_SECRET_KEY <micrCMSで設定したシークレット>
setEnv GITLAB_PROJECT_ID <GitLabのプロジェクトID>
```
### PHPプログラム
次にPHPの記述をしていきます。

```php
<?php
$data = file_get_contents('php://input');

/*
 * microCMSの環境変数に格納させたシークレットをgetenvで取得
*/ 
$secret = getenv('MICROCMS_SECRET_KEY');

/* 
* 検証するための処理
*/
//署名を生成
$expectedSignature = hash_hmac('sha256', $data, $secret);

// リクエストヘッダーから署名を取得
$signature = $_SERVER['HTTP_X_MICROCMS_SIGNATURE'];

// 署名の検証
if (!hash_equals($signature, $expectedSignature)) {
    //署名の検証がエラーならthrow new Exceptionで処理をストップさせる
    throw new Exception('Invalid signature.');
}

/*
 * microCMSのシグネチャチェックが通った場合処理を実行
 */
//各変数に仮の値を入れておく
$token = 'TOKEN';
$refName = 'REF_NAME';
$buildMode = 'test';
/*
 * GitLabのプロジェクトIDも環境変数に入れて管理
 * 汎用性を高めるならばmicrCMS側のカスタムヘッダに入れる方法が良い
 */
$projectId = getenv('GITLAB_PROJECT_ID');

// headerからTOKENやRefNameを取得し変数に入れておく
$headers = getallheaders();
foreach($headers as $header => $value) {
    switch($header) {
        case "X-GITLAB-TOKEN":
            $token = $value;
            break;
        case "X-GITLAB-REF-NAME":
            $refName = $value;
            break;
        case "X-BUILD-MODE":
            $buildMode = $value;
            break;
        default:
            // 他のヘッダーに対する処理があれば追加する
            break;
    }
}
// microCMSから必要な情報がこない場合はエラー処理
if($token === 'TOKEN' || $refName === 'REF_NAME') {
    throw new Exception('Value Not Set');
}

/*
* curlの準備
*/
$curl = curl_init();

$data = array(
    'token' => $token,
    'ref' => $refName,
    'variables[RUN_BUILD_MODE]' => $buildMode,
);

curl_setopt_array($curl, array(
    CURLOPT_URL => "https://gitlab.com/api/v4/projects/$projectId/trigger/pipeline",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($data),
));

$response = curl_exec($curl);
curl_close($curl);
?>
```

## 動作のチェック
### 記事の作成
microCMSで適当な記事を作成して公開ボタンをクリックします。
### ジョブの確認
GibLab側のビルドのパイプライン上でジョブの実行状況が確認できます。

# 今後の対応
とりあえず中継サーバーを挟むことでCI/CDのジョブを実行することができました。
ただ、microCMSさんは開発の速度がとても速い会社なので、もしかして近くにGitLab用の設定ができるようになるかもしれません。
そうなるとこの方法は無用になるかと思われます。