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
をブラウザで開くと以下のようなテスト結果をみることができます。
きちんと契約に基づいてテストされているのがわかります。
テストを通すように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にスタブがアップロードされているのが確認できます。
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"}
契約に基づいた。スタブが起動されているのがわかります。