SonarQubeのQuality Gateの結果をGitLab Pipeline上で取得してデプロイ判定する

※最近のSonarQubeの場合はmavenなら`<sonar.qualitygate.wait>true</sonar.qualitygate.wait>`が使えるみたいなので、そちらを使った方がシンプルになる点に注意。

SonarQubeのQuality Gateの結果をGitLabのPipelineで受け取ってデプロイ判定に利用するFeasibility検証した時のメモ。

SonarQubeにはQuality Gateと呼ばれる、コードの品質（≠バグの量）が一定以上かどうかを多角的な条件から判断する機能がある。Quality Gateによって一定品質に満たしていないと判断されたコードは仮にテストで問題が検出されなくてもデプロイしたくない場合がある。このようなニーズにおいて、Quality Gateの結果によってデプロイすべきかどうかをGitLabのパイプライン上で判断するための実機検証となる。

## 確認内容

確認したい内容は以下のフローが実装できるか、になる。ただし、SonarQubeの分析結果を受け取ったところから先はFeasibility確認は出来ているため、実際に確認するのは分析結果を受け取って、分析結果からイメージビルドを実行するかどうか、のところまでとなる。

```mermaid
sequenceDiagram
    participant User
    participant GitLab (Runner)
    participant SonarQube
    participant ArgoCD
    participant k8s
    activate User
    User->>GitLab (Runner): Code Push
    deactivate User
    activate GitLab (Runner)
    GitLab (Runner)->>SonarQube: Code分析
    activate SonarQube
    SonarQube->>GitLab (Runner): 分析結果
    deactivate SonarQube
    GitLab (Runner)->>GitLab (Runner): 分析結果の確認
    GitLab (Runner)->>GitLab (Runner): コンテナイメージのビルド
    GitLab (Runner)->>GitLab (Runner): ArgoCDが監視するManifestの更新
    GitLab (Runner)->>ArgoCD: 強制Sync(argocd app sync)
    deactivate GitLab (Runner)
    activate ArgoCD
    ArgoCD->>k8s: コンテナイメージのデプロイ
    deactivate ArgoCD
    activate k8s
    deactivate k8s
```

## 前提
以下が利用できるものとする。
- GitLab
- GitLab Runner
- SonarQube

## 前準備
空のGitリポジトリを用意し、JUnit5のサンプルの[maven部分](https://github.com/junit-team/junit5-samples/tree/main/junit5-jupiter-starter-maven)をコピーしておく。
```
git clone https://github.com/junit-team/junit5-samples.git
git clone https://mygitlab.info/myapp/junit5-jupiter-starter-maven.git
cp -a junit5-samples/junit5-jupiter-starter-maven/* junit5-jupiter-starter-maven/
cd junit5-jupiter-starter-maven
git add *
git commit -m "nocomment"
git push origin main
```
また、JaCoCoのカバー率を利用してQualityGateのSuccess/Failureを制御したいため、JaCoCoを利用するための修正を含めておく。\
pom.xmlの修正内容は以下になる。
```diff
@@ -16,6 +16,11 @@
 	<dependencyManagement>
 		<dependencies>
+			<dependency>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<version>0.8.8</version>
+			</dependency>
			<dependency>
 				<groupId>org.junit</groupId>
@@ -36,6 +41,11 @@
 	<build>
 		<plugins>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<version>0.8.8</version>
+			</plugin>
			<plugin>
 				<artifactId>maven-compiler-plugin</artifactId>
```


次に、SonarQubeのProjectを作成し、一度スキャンは実施しておく。Project作成〜スキャン実施の詳細は割愛するが、最終的には以下のようなコマンドをソースコードのrootディレクトリ上で実行することになる。
```
mvn sonar:sonar \
  -Dsonar.projectKey=maven-sample \
  -Dsonar.host.url=https://mysonarqube.info \
  -Dsonar.login=2577f240df389a5e4fbfaf6ff2fc5c52626aafc7
```

また今回のサンプルでQuality GatesでFailedを起こすための厳しいQuality Gatesを作成しておく。
SonarQubeのUI上の上部のQuality GatesからCreateを選択し、Line Coverageが80%以下だとFailedになるQuality Gatesを作成する。
![1659432820962.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/6c5e54e5-97df-e115-dd13-0ad4a8208dc8.png)
作成後、ProjectのProject Settings -> Quality Gateから"Always use a specific Quality Gate"を選択し、作成したQuality Gateを選択してSaveする。


## パイプラインの作成
以下の手順で構築する。
1. GitLabがSonarQubeのWebhookを受け取るための設定
2. SonarQubeのWebhook設定
3. .gitlab-ci.ymlの作成
4. テストラン

### GitLabがSonarQubeのWebhookを受け取るための設定
GitLabのプロジェクトでSettings -> CI/CD -> Pipeline triggersを選択し、Tokenを作成する。

![1659428558945.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/1fc8fee7-7bd2-204a-e40e-bc754003b3fc.png)


作成後、下の"Use webhook"に表示されているURLをコピーし、`REF_NAME`をブランチ名、`TOKEN`を先程取得したTokenに置き換える。
```
https://mygitlab.info/api/v4/projects/5/ref/main/trigger/pipeline?token=95218b76965deee8494d2bca7a5a37
```

### SonarQubeのWebhook設定
SonarQubeのProjectを選択後、Project Settings -> Webhookから"Create"を選択する。
Nameは好みで、URLは先程GitLabで取得した"Use webhook"の値を設定する。Secretは最初から値が入っているが、そのままで問題ない。
![1659429601799.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/497547bb-8fd0-0afe-126d-4e4640075b88.png)

### .gitlab-ci.ymlの作成
GitLabのプロジェクトに戻り、CI/CD -> Editorを選択し、.gitlab-ci.ymlを作成する。

.gitlab-ci.ymlでは以下のステージを実装する。
- sonarqube-scan：SonarQubeの呼び出し
- build-image：Quality Gateの結果がFAILEDだと停止、SUCCESSならイメージビルド

.gitlab-ci.ymlの先頭に以下を記載する。
```yaml
stages:
  - sonarqube-scan
  - build-image
```

#### sonarqube-scan
先程実行したコードスキャンをmavenイメージを使ってpush時のみ実行する。.gitlab-ci.ymlの記載は以下になる。
```yaml
sonarqube-scan:
  image: maven:3-jdk-11
  rules:
  - if: '$CI_PIPELINE_SOURCE == "push"'
  stage: sonarqube-scan
  script:
  - |
    set -x
    mvn jacoco:prepare-agent test jacoco:report sonar:sonar -Dsonar.projectKey=maven-sample -Dsonar.host.url=https://mysonarqube.info -Dsonar.login=2577f240df389a5e4fbfaf6ff2fc5c52626aafc7
```

#### build-image
SonarQubeのwebhook設定により、スキャン完了後にwebhook契機で実行するステージとなる。
SonarQubeから飛んでくるPayloadは[こちら](https://docs.sonarqube.org/latest/project-administration/webhooks/)に例が記載してある。\
必要な箇所だけ抜き出すと、以下の部分になる。
```json
{
    "qualityGate": {
        "conditions": [
            ：（省略）
        ]
        "name": "SonarQube way",
        "status": "OK"
    }
}
```
これを利用したステージは以下となる。
```yaml
build-image:
  image: gcr.io/kaniko-project/executor:debug
  rules:
  - if: $TRIGGER_PAYLOAD
  stage: build-image
  before_script:
    wget -O ./jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 && chmod +x ./jq
  script:
  - |
    set -x 
    QUALITY_GATE_STATUS=`cat ${TRIGGER_PAYLOAD} | ./jq -r ".qualityGate.status"`
    if [ "$QUALITY_GATE_STATUS" == "OK" ]; then
      echo "Starting Build Image"
    else
      echo "Failed. Aborting."
      exit 1
    fi
```
webhook契機でPipelineが呼び出される場合は`TRIGGER_PAYLOAD`環境変数が設定されるため、これがある時のみ動くようにする。
`script:`内では`TRIGGER_PAYLOAD`をjqによりパースしてQualityGateのstatusを抽出している。今回イメージビルドするところは確認対象外なので、実装はif文による分岐までとしている。

### テストラン
Quality GateでLine Coverageが80%以下ならFailedになるように設定しているため、Line Coverageを調整しながらパイプラインの実行結果を確認する。
実際にコードに修正してpushした結果はこちら。
![1659433460602.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/920ac435-adb1-0bee-9702-75d673a7491c.png)
1つ目のパイプライン(#67)でSonarQubeによるコードスキャンを実施し、2つ目のパイプライン(#68)でQuality Gateの結果を確認している。\
サンプルの初期状態ではカバレッジは100%なので、build-imageのジョブの実行結果は以下のように成功した。
```shell
+ + ./jq -r .qualityGate.status
cat /builds/myapp/junit5-jupiter-starter-maven.tmp/TRIGGER_PAYLOAD
+ QUALITY_GATE_STATUS=OK
+ '[' OK '==' OK ]
+ echo 'Starting Build Image'
Starting Build Image
Cleaning up project directory and file based variables
00:00
Job succeeded
```
次にGitLabのUIから`src/main/java/com/example/project/Calculator.java`をEditで開き、以下の修正を加える。
```diff
package com.example.project;

public class Calculator {

	public int add(int a, int b) {
		return a + b;
	}
+   public int sub(int a, int b) {
+       return a - b;
+   }
}
```
これをCommit changeで保存すると、再度パイプラインが実行される。この時、JUnitのテストコードの不足によりカバレッジは66.67%と低下し、Quality Gateの80%の基準に未達となる。build-imageのジョブも以下のような結果となり、ジョブの実行は失敗する。
```shell
+ ./jq -r .qualityGate.status
+ cat /builds/myapp/junit5-jupiter-starter-maven.tmp/TRIGGER_PAYLOAD
+ QUALITY_GATE_STATUS=ERROR
+ '[' ERROR '==' OK ]
+ echo 'Failed. Aborting.'
Failed. Aborting.
+ exit 1
Cleaning up project directory and file based variables
00:00
ERROR: Job failed: command terminated with exit code 1
```
以上より、想定通りの動作確認が出来た。

## まとめ
Quality Gateの結果をwebhookを使うことでGitLabのパイプラインで取得して、デプロイ可否の判断材料として利用できる見込みを得た。