コンシューマ駆動契約テストに入門してみた〜CI/CD編〜

タム

2024.07.10

35

こんにちは。タムです。

今回は、コンシューマ駆動契約の2回目の投稿ということで、

前回課題として挙げていた、CI/CDパイプラインに組み込むタスクが完了したため、

具体的にどうやって対応したかを紹介させていただきます。

対応内容

今回のゴール

今回はこちらの構成を作成することをゴールとします。

※厳密には上記の構成とは少し異なり、PRパイプラインには組み込みません。

コンシューマ・プロバイダ共にデプロイパイプラインに組み込みます。

今回実装するのは以下の流れです。


  1. コンシューマ側のパイプライン
    1. mainブランチにコミットで発火
    2. Pactテスト実行
    3. テストが通ったらBrokerに契約ファイルをpublish
    4. デプロイ
  2. プロバイダ側のパイプライン
    1. mainブランチにコミットで発火
    2. Brokerから契約ファイルをimport
    3. Pactテスト実行
    4. テスト結果をBrokerにpublish
    5. テストが通ったらデプロイ


Broker

前回の記事を読んでいただけたらおわかりのように、

コンシューマ側でテストに成功すると契約ファイル(JSON)が作成され、

それをプロバイダ側に連携する必要がありました。


そこで前回はローカルで直接ファイルをコピペして置いていましたが、

CI/CDに組み込むにはリモートのサーバにファイルを置く必要があります。

単にファイルを共有するだけであれば、S3で事足ります。

ただ、当然のことながらS3だと本当にファイルを共有することしかできないです。


Pactではコンシューマとプロバイダの間を取り持つBrokerを提供しています。

これを使うと、単にJSONを共有するだけでなく、色々便利な使い方ができるようです。

なのでせっかくコンシューマ駆動を導入するなら、個人的には積極的にBrokerも使っていきたいです。


Pact BrokerにはRuby製アプリ、Docker、Pact Flow(インフラ込み)の3種類があります。

前の2つは自分でホスティングする必要があるので、大きく分類するなら2種類ですね。

Pact Flowの料金は無料プランもありますが、制約が色々あり

(特に2 integrationsが気になります。2つのサービス間までしか利用できないということでしょうか?)

有料プランになると万単位のお金がかかるので、実際に導入するのはなかなかハードルが高いと思います。

というわけで今回は、Dockerを自分でホスティングする方向で検討しました。


次にDockerをどこに配置するかですが、個人利用でたまに動かす程度なので

今回はシンプルにEC2 on Dockerの構成でグローバルIPを付与して直接外部からアクセスできるようにしました。


Consumer

Brokerにpublishする方法(Pact Broker CLI)ですが、

Dockerイメージ、Ruby製のスタンドアロン実行可能ファイル、NodeJS製のコマンドがあります。

NodeJSであればビルド環境にそのまま組み込めるため楽かなと思ったのですが、

CodeBuildで実行したところ以下のように依存関係でエラーが出て解決に時間がかかりそうだったので断念しました。

[Container] 2024/07/22 13:29:50.176274 Running command npm run test:publish
> auth0-next-custom@0.1.0 test:publish
> ./node_modules/.bin/pact-broker publish ./pacts --consumer-app-version=$(git rev-parse HEAD) --auto-detect-version-properties --broker-base-url=${PACT_BROKER_BASE_URL} --broker-username ${PACT_BROKER_USERNAME} --broker-password ${PACT_BROKER_PASSWORD}
XXXXXXXXXXXXXXXXX/auth0-next-custom/node_modules/@pact-foundation/pact-cli/standalone/linux-x64-2.4.6/pact/lib/ruby/bin.real/ruby: error while loading shared libraries: libcrypt.so.1: cannot open shared object file: No such file or directory
[Container] 2024/07/22 13:29:50.529456 Command did not exit successfully npm run test:publish exit status 127
[Container] 2024/07/22 13:29:50.534863 Phase complete: PRE_BUILD State: FAILED_WITH_ABORT
[Container] 2024/07/22 13:29:50.534880 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: npm run test:publish. Reason: exit status 127


したがって環境起因の問題が起こりにくいDockerイメージを使うことにしました。


ところで、コンシューマはNextJSアプリであり、Amplifyでホスティングしています。

なのでCI/CDもAmplifyで行っていました。

そこで問題なのはAmplifyのCI/CDではDockerを動かすことはできないようです。

調べたところ、AmplifyのCI/CDをトリガーすることは可能なので、

こちらを参考にCodeBuildから呼び出すようにしました。

なお、CodeBuildからDockerを呼び出す場合、特権を付与する必要があるので注意が必要です。


そうすると逆にAmplifyの自動デプロイが邪魔になってくるので、設定をオフにしました。


あとは、publishする際アプリバージョンを指定する必要がありますが、

Gitのコミットハッシュが推奨なのでその場合はCodeBuildからGitコマンドを実行する必要があります。

そのためソースステージの出力アーティファクトで完全クローンを選択する必要があるのでご注意ください。

また、リポジトリがGitHubを利用している場合は完全クローンするための権限

CodeBuildのサービスロールに付与する必要もあります。

権限を付与するために必要なConnectionArnはCodePipelineのソースステージの「詳細を表示」から確認できます。


コンシューマ側のCI/CDのための修正は以下のようになりました。

diff --git a/buildspec.yml b/buildspec.yml
new file mode 100644
index 0000000..7d35759
--- /dev/null
+++ b/buildspec.yml
@@ -0,0 +1,16 @@
+version: 0.2
+
+phases:
+  install:
+    on-failure: ABORT
+    commands:
+      - npm ci
+  pre_build:
+    on-failure: ABORT
+    commands:
+      - npm run test
+      - npm run publish
+  build:
+    on-failure: ABORT
+    commands:
+      - aws amplify start-job --app-id XXXXXXXXXX --branch-name main --job-type RELEASE
diff --git a/package.json b/package.json
index 3d79d38..22c0710 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,9 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "test": "jest",
+    "publish": "docker run --rm -w ${PWD} -v ${PWD}:${PWD} pactfoundation/pact-cli:latest publish ${PWD}/pacts --consumer-app-version=$(git rev-parse HEAD) --branch=$(git branch --contains | cut -d ' ' -f 2) --broker-base-url=${PACT_BROKER_BASE_URL} --broker-username ${PACT_BROKER_USERNAME} --broker-password ${PACT_BROKER_PASSWORD}"
   },
   "dependencies": {
     "@auth0/nextjs-auth0": "^3.5.0",


Provider

Providerはpythonで実装されていてAPI Gateway+Lambda構成でCodeBuildでデプロイしていました。

Consumerの方はtestとpublishを別々に実行する必要がありましたが、

pactmanの場合はtestの際のオプションの--pact-broker-urlでURLを指定することでBrokerから契約ファイルをダウンロードし、

--pact-publish-resultsで結果をアップロードすることが可能です。

こちらもコンシューマ同様Gitのコミットハッシュでバージョンを指定する場合はそのための設定が必要になってきます。

ですがDockerを使う必要がない分簡単で、CI/CDのための差分は以下のようになりました。

diff --git a/buildspec.yml b/buildspec.yml
index 09cffeb..216944e 100644
--- a/buildspec.yml
+++ b/buildspec.yml
@@ -1,15 +1,22 @@
 version: 0.2
 
 phases:
+  install:
+    on-failure: ABORT
+    commands:
+      - pip install -r requirements.txt
+
   pre_build:
     on-failure: ABORT
     commands:
       - aws s3 cp s3://XXXXXXXXXXXXXXXXXXXXXXXXX/google_login_back/dev.json .chalice/deployed/dev.json || true
+      - ./test.sh
+
   build:
     on-failure: ABORT
     commands:
-      - pip install -r requirements.txt
       - chalice deploy
+
   post_build:
     on-failure: ABORT
     commands:
diff --git a/docker-compose.yml b/docker-compose.yml
index cca3044..815fa32 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,7 +11,8 @@ services:
     environment:
       DB_ENDPOINT: http://dynamo:8000
       TEST_PORT: 8008
-      PYTEST_ADDOPTS: "-v --pact-files=pacts/*.json"
+      PACT_BROKER_BASE_URL: ${PACT_BROKER_BASE_URL}
+      PACT_BROKER_AUTH: ${PACT_BROKER_USERNAME}:${PACT_BROKER_PASSWORD}
     command: chalice local --host=0.0.0.0 --port=8002
   dynamo:
     image: amazon/dynamodb-local
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..2bca591
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+GIT_VERSION=$(git rev-parse HEAD)
+
+pytest tests/ -v --pact-broker-url=${PACT_BROKER_BASE_URL} --pact-provider-name=google-login-back --pact-publish-results --pact-provider-version=${GIT_VERSION}


BrokerでBasic認証している場合はPACT_BROKER_AUTH環境変数に設定することで通すことが可能です。

(もしくはURLに付与するやり方もあるようです)

さいごに

まとめ

今回はPact Brokerを使って契約ファイルをコンシューマ・プロバイダ間で共有し、

CI/CDにPactの検証を組み込むまでを行いました。

気をつけるポイントは以下です。

  • Broker
    • インフラをどうするか。ユースケースにもよるが一旦動けばいいレベルならEC2 on Dockerが早い&安い&楽?
  • Consumer
    • testとは別にpublishコマンドを叩く必要がある
    • Pact CLIはDockerが無難。CodeBuildに組み込むなら特権が必要
    • Consumer=フロントエンドかつAmplifyを使っているならデプロイをトリガーするコマンドが使える
    • Gitコマンドを打つなら完全クローンで
  • Provider
    • testの中でpublishもしてくれる(pactmanの場合)
    • Gitコマンドを打つなら完全クローンで

課題

現状のパイプラインだと、コンシューマ側で契約を変更した際に、

変更後もプロバイダが引き続きサポートしているかどうかがわからないという問題があります。

もしサポートしていなかった場合、そのままデプロイされてしまい、

不具合が発生する可能性があります。

あるべきとしては、コンシューマ側で契約を変更したら、

プロバイダが契約を満たすかどうかをチェックしに行くように

コンシューマ側のパイプラインを組む必要があります。

こちらは今後の課題として、でき次第また記事にしようと思います。

この記事をシェアする