Spring Cloud Contractをnon-JVMの言語で使う

はじめに

Spring Cloud ContractはCDC(Consumer Driven Contract)をサポートするSpring Cloud傘下のプロジェクトです。
このプロジェクトはJavaのプロジェクトであることにより、初期の段階では、JVM言語以外での利用ができない状況でした。しかし、その場合Spring Cloud Contractを利用する場合は開発言語もJVM言語の採用を行なう必要があり、マイクロサービスなどのアーキテクチャを採用する利点を削ることになりかねません。そこで、開発チームはJVM以外の言語でもその機能を提供するように開発が進められていました。
このブログでは以下のことを目指したいと思います。

  • Spring Cloud ContractをJVM言語以外の言語で利用する(Go)

Spring Cloud ContractやCDCの基本的なアイディアについては以前にブログを書いたのでよろしければそちらをご覧ください

どのようにするか?

Spring Cloud Contractはpolyglotな言語に対応するためにDockerを利用します。
Javaのレイヤーを抽象化するために、契約に基づいたテストを生成を行なうDockerイメージとそのスタブを作成するDockerイメージがDocker Hubで公開されています。このイメージに環境変数としと契約と諸々の情報を渡してやることで、CDCに必要なコンポーネントとして動作します。

環境

今回は開発言語としてJava使わずにSpringCloudContractを利用してみたいと思います。
ProducerサイドではGo(Gin)を使い、CondumerサイドはcURLでの検証を行います。
実行する環境は以下のとおりです。

$ uname -srvmpio
Linux 5.3.0-62-generic #56~18.04.1-Ubuntu SMP Wed Jun 24 16:17:03 UTC 2020 x86_64 x86_64 x86_64 GNU/

$ lsb_release -a
LSB Version:    core-9.20170808ubuntu1-noarch:security-9.20170808ubuntu1-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.4 LTS
Release:    18.04
Codename:   bionic

$ docker -v
Docker version 19.03.12, build 48a66213f

$ go version
go version go1.14.4 linux/amd64

Antifactoryの準備

Spring Cloud Contractをpolyglotに利用するためには、作成されるスタブを公開する必要があり、そのためにAirtifactoryなどのArtifact Managerを用意してやらなければなりません。

Dokcerを使って立てておきたいと思います。

$ mkdir -p  $DIR_TO_WORK/artifactory/var/etc/
$ touch $DIR_TO_WORK/artifactory/var/etc/system.yaml
$ chown -R 1030:1030  touch $DIR_TO_WORK/artifactory/var

$ docker run --name artifactory -v ${DIR_TO_WORK}/artifactory/var/:/var/opt/jfrog/artifactory -d -p 8081:8081 -p 8082:8082 docker.bintray.io/jfrog/artifactory-oss:latest

docker ps
CONTAINER ID        IMAGE                                               COMMAND                  CREATED             STATUS              PORTS                              NAMES
59c37b50f460        docker.bintray.io/jfrog/artifactory-cpp-ce:latest   "/entrypoint-artifac…"   3 seconds ago       Up 2 seconds        0.0.0.0:8081-8082->8081-8082/tcp   artifactory

これでローカルにAirtifactoryは立ちました。Airtifactoryを利用する際は初回ログインが必要です。localhost:8082にアクセスして、ログインします(デフォルトのユーザ名:admin、パスワード:password)。また、ログインの初期設定でMavenリポジトリを作成してください。
また、少し面倒なので、[Administration]⇨[Security]⇨[Setting]⇨[Allow Anonymous Access]にチェックを入れて、認証認可もオフにしておいてください。

契約を作成する

まずは簡単な契約を作成します。Spring Cloud Contractでは様々な契約の記述方法(Yaml、Groovy DSL、Spring Rest Doc等)がサポートされていますが、多言語の環境で利用する場合(Dockerを利用する場合)はYamlでの記述が求められます。
今回は以下のような簡単な契約を用いてCDCを行いたいと思います。

contract.yaml

request:
  method: GET
  url: /hello
  matchers:
response:
  status: 200
  body:
    "hello": "world"

上記の契約では/helloのリクエストに対してProducerは{”hello”: "world"}Json形式のレスポンスを200のレスポンスコードとともに返すという内容になっています。
この辺の契約の記述方法詳細はドキュメントをご確認ください。

Producerサイド

APIの作成

Producerサイドを作成していきたいと思います。今回はGoのGin Frame Workを用いてAPIを作成します。 まずはプロジェクトの作成します。

$ go mod init go-sample
$ go get -u github.com/gin-gonic/gin

次に簡単なハンドラーを実装し、アプリを起動します。

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.GET("/hello", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run(":8083")
}

起動します。

$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /hello                     --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8083

このアプリは/pingへのGETアクセスに対して{"message": "pong"}を返すだけの簡単なAPIとなってます。

$ curl localhost:8081/hello
{"message":"pong"}

このAPIをProducerとして、Spring Cloud Contractのテストを実行してみたいと思います。

Producerのテストを実行する

前述したとおり、Spring Cloud Contractではnon-jvm言語に対してDockerを使いJavaのレイヤーを抽象化しています。
ProducerサイドのDockerイメージは契約を読み取り、テストを自動生成&実行を行ってくれます。
Dockerイメージは以下のような引数を環境変数として受け取る必要があります。

環境変数 説明
PROJECT_GROUP 公開するスタブのプロジェクトのグループID、デフォルトはcom.example
PROJECT_VERSION 公開するスタブのプロジェクトのバージョン、デフォルトは0.0.1-SNAPSHOT
PROJECT_NAME 公開するスタブのプロジェクトのアーティファルトIDとして、デフォルトはexample
REPO_WITH_BINARIES_URL アーティファクトマネージャーのURL。デフォルトはhttp://localhost:8081/artifactory/libs-release-localであり、Artifactoryをローカルで起動した際のURL
REPO_WITH_BINARIES_USERNAME (オプション)アーティファクトマネージャーが認証を必要とする際のユーザ名
REPO_WITH_BINARIES_PASSWORD (オプション)アーティファクトマネージャーが認証を必要とする際のパスワード
PUBLISH_ARTIFACTS Airtifactをバイナリストレージに公開する。デフォルトはtrue

また、テスト実行時に利用される以下のような引数も設定してやる必要があります。

環境変数 説明
APPLICATION_BASE_URL テスト対象のアプリURL
APPLICATION_USERNAME (オプション)アプリケーションに認証が必要な場合にユーザ名を設定する
APPLICATION_PASSWORD (オプション)アプリケーションに認証が必要な場合にパスワードを設定する

それでは以上のような環境変数を設定しテストを実行します。

$ export LOCAL_IP="$(hostname -I | cut -d' ' -f1)"

$ docker run  --rm -e "APPLICATION_BASE_URL=http://${LOCAL_IP}:8083" \
-e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=contract-stub" \
-e "PROJECT_GROUP=dev.hirooka" -e "REPO_WITH_BINARIES_URL=http://${LOCAL_IP}:8081/artifactory/libs-release-local" \
-e "PROJECT_VERSION=0.0.1.RELEASE" -v "${DIR_TO_WORK}/contracts/:/contracts:ro" \
-v "${DIR_TO_WORK}/output:/spring-cloud-contract-output/" \
springcloud/spring-cloud-contract:2.1.2.RELEASE

このDockerイメージは${DIR_TO_WORK}/contracts/配下の契約をスキャンして、必要なテストを自動生成実行してくれます。 上記のdocker runを実行すると、以下のようなログを出力しテストが失敗します。

Caused by: org.gradle.api.GradleException: There were failing tests. See the report at: file:///spring-cloud-contract/build/reports/tests/test/index.html

テスト結果はコンテナ内の/spring-cloud-contract/build/reports/tests/test/index.htmlに出力されているようなので、ホスト側でマウントされている${DIR_TO_WORK}/output/reports/tests/test/index.htmlをブラウザで開くと以下のようなテスト結果をみることができます。

f:id:yuya_hirooka:20200706224935p:plain

きちんと契約に基づいてテストされているのがわかります。

テストを通すようにProducerを修正する

それでは、今度はテストがきちんと通るようにProducer側を修正します。
先程のGoのファイルを以下のように書き換えます。

package main

import "github.com/gin-gonic/gin"

func main() {
  r := gin.Default()

  r.GET("/hello", func(c *gin.Context) {
    c.JSON(200, gin.H{
      "hello": "world",                                                                                                                                                                                     
    })
  })
  r.Run(":8083")
}

契約に装用にAPIが返すJsonを修正しました。 もう一度テストを実行してみます。

$  docker run  --rm -e "APPLICATION_BASE_URL=http://${LOCAL_IP}:8083" \
-e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=test" \
-e "PROJECT_GROUP=com.example" -e "REPO_WITH_BINARIES_URL=http://${LOCAL_IP}:8081/artifactory/libs-release-local" \
-e "PROJECT_VERSION=0.0.1.RELEASE" -v "${DIR_TO_WORK}/contracts/:/contracts:ro" \
-v "${DIR_TO_WORK}/output:/spring-cloud-contract-output/" \
springcloud/spring-cloud-contract:2.1.2.RELEASE

Setting project name to [test]
Running the build

> Configure project :
Will use contracts from the mounted [/contracts] folder

> Task :clean
> Task :compileJava NO-SOURCE
> Task :compileGroovy NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar
> Task :copyContracts
> Task :generateClientStubs
> Task :verifierStubsJar
> Task :assemble
> Task :generateContractTests
> Task :compileTestJava
> Task :compileTestGroovy NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses

> Task :test
Results: (1 tests, 1 successes, 0 failures, 0 skipped)

> Task :cleanOutput UP-TO-DATE
> Task :copyOutput
> Task :check
> Task :build
> Task :generatePomFileForMavenJavaPublication
> Task :publishMavenJavaPublicationToMavenRepository
> Task :generatePomFileForStubsPublication
> Task :publishStubsPublicationToMavenRepository
> Task :publish

Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/4.10.2/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 55s
14 actionable tasks: 13 executed, 1 up-to-date

今度は成功して、Artifactoryにスタブがアップロードされているのが確認できます。

f:id:yuya_hirooka:20200706234705p:plain
airtifactory

Consumerサイド

ConsumerサイドのDockerイメージはArtifactoryにアップロードされたスタブの情報に基づいたWireMockを立ててくれます。
Consumerサイドの検証として、スタブのイメージをローカルで実行して、curlでアクセスしてみます。 以下のコマンドを実行します。

export LOCAL_IP="$(hostname -I | cut -d' ' -f1)"
export STUBRUNNER_PORT="8084"
export STUBRUNNER_IDS="com.example:test:0.0.1.RELEASE:stubs:8083"


docker run  --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" \
-e STUBRUNNER_STUBS_MODE=REMOTE \
-e "STUBRUNNER_REPOSITORY_ROOT=http://${LOCAL_IP}:8081/artifactory/libs-release-local" \
-p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "8083:8083" \
springcloud/spring-cloud-contract-stub-runner:2.1.4.RELEASE

ポイントは、STUBRUNNER_STUBS_MODE=REMOTE環境変数として渡してやり、リモートのリポジトリからスタブを取得するようにしておくことです。
上記のdocker runを実行すると以下のログが出力されます。

2020-07-07 12:28:42.839  INFO 1 --- [qtp936653983-25] WireMock                                 : Admin request received:
127.0.0.1 - POST /mappings

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/1.8.0_222)]
Host: [localhost:9876]
Content-Length: [291]
Content-Type: [text/plain; charset=UTF-8]
{
  "id" : "b37535a3-0a07-4f6f-a04a-e6eba35b25da",
  "request" : {
    "url" : "/hello",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "{\"hello\":\"world\"}",
    "transformers" : [ "response-template" ]
  },
  "uuid" : "b37535a3-0a07-4f6f-a04a-e6eba35b25da"
}
$ curl localhost:8083/hello 
{"hello":"world"}

契約に基づいた。スタブが起動されているのがわかります。

参考仕様