{"id":16118953,"url":"https://github.com/cch0/opinionated-spring-boot-solution","last_synced_at":"2025-04-06T09:50:57.494Z","repository":{"id":84547369,"uuid":"188713032","full_name":"cch0/opinionated-spring-boot-solution","owner":"cch0","description":"Opinionated Spring Boot solutions for common concerns in distributed system.","archived":false,"fork":false,"pushed_at":"2019-06-15T14:47:06.000Z","size":685,"stargazers_count":0,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-12T15:36:54.375Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cch0.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-05-26T17:34:42.000Z","updated_at":"2019-06-15T14:40:58.000Z","dependencies_parsed_at":"2023-03-12T23:29:17.313Z","dependency_job_id":null,"html_url":"https://github.com/cch0/opinionated-spring-boot-solution","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cch0%2Fopinionated-spring-boot-solution","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cch0%2Fopinionated-spring-boot-solution/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cch0%2Fopinionated-spring-boot-solution/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cch0%2Fopinionated-spring-boot-solution/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cch0","download_url":"https://codeload.github.com/cch0/opinionated-spring-boot-solution/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247463903,"owners_count":20942948,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-09T20:51:45.699Z","updated_at":"2025-04-06T09:50:57.466Z","avatar_url":"https://github.com/cch0.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Opinionated Spring Boot Solutions To Common Concerns In Distributed System\n\n## Goal\n\nThe goal of this repository is to illustrate how to solve common concerns a typical application would face in a distributed environment by using open source solutions with little or no configurations.\n\nThe focus is on using open source libraries that are developed to work with Spring Boot application such that the same solution can be applied to other Spring Boot applications with little or no effort. Developers can then focus on application and business logic. \n\n### What is Being Addressed?\n\n1. request and response logging\n2. distributed tracing\n3. API endpoint client\n4. service discovery\n5. configuration management\n6. secret management\n\n### What Is Not Being Addressed?\n\nThe solutions described here are NOT specifically for running application in the Cloud environment and/or in Kubernetes cluster. To take advantage of Cloud and/or Kubernetes, a different set of solutions are recommended and is covered in `to-be-created` repository.\n\n### But Opinionated?\n\nEvery problem can likely be solved differently and this is a good thing. The goal is not to convince anyone this solution is better or worse than any other solution but rather seves the purpose of providing a working solution. As technology evolves, what is considered a best/better solution is going to evolves as well. In other words, the opinionated solution provided here likely has an expiration date associated with it.\n\n\n## Contrived Example\n\nIn this repo, ideas and solutions are illustrated by applying them to a contrived example. There is not much real business related functionality implemented and the idea of the example is to have multiple services working together to loosely reflect a real world scenario.\n\n\n![example](./docs/img/example_configuration.png)\n\n## 1. Request \u0026 Response Logging\n\n[Logbook](https://github.com/zalando/logbook) is a Java library for HTTP request and response logging. The features it supports and flexibility it provides makes it a good solution to be included.\n\n### Features\n\n* Logging of HTTP request and response, including body, partial body or none at all.\n* Different logging format: json, http, curl, etc\n* Obfuscation of sensitive data\n* Customization: including inclusion and/or exclusion of endpoints and others\n* Auto configuration for Spring Boot application\n\n### Configuration\n\n**`pom.xml`**\n```\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.zalando\u003c/groupId\u003e\n    \u003cartifactId\u003elogbook-spring-boot-starter\u003c/artifactId\u003e\n    \u003cversion\u003e${logbook.version}\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n**Sample configuration in `application.yaml`**\n```\nlogbook:\n  format:\n    style: json\n  include:\n    - /accounts/**\n  exclude:\n    - /favicon.ico\n    - /actuator/**\nlogging:\n  level:\n    ROOT: INFO\n    org:\n      zalando:\n        logbook: TRACE\n```\n\n**Sample Log Messages**\n\nIncoming Log\n```\npayment-service_1  | 2019-05-28 00:33:05.947 TRACE [payment-service,32cf62ffab4de32b,fcfe924a3b3a35c6,true] 1 --- [nio-8080-exec-1] org.zalando.logbook.Logbook              : {\"origin\":\"remote\",\"type\":\"request\",\"correlation\":\"8be56ded8745bf16\",\"protocol\":\"HTTP/1.1\",\"remote\":\"172.23.0.4\",\"method\":\"GET\",\"uri\":\"http://payment-service:8080/paymentInfo/accounts/123\",\"headers\":{\"accept\":[\"*/*\"],\"connection\":[\"keep-alive\"],\"content-type\":[\"application/json\"],\"host\":[\"payment-service:8080\"],\"user-agent\":[\"Java/11.0.3\"],\"x-b3-parentspanid\":[\"32cf62ffab4de32b\"],\"x-b3-sampled\":[\"1\"],\"x-b3-spanid\":[\"fcfe924a3b3a35c6\"],\"x-b3-traceid\":[\"32cf62ffab4de32b\"]},\"body\":}\n```\n\nAfter Re-formatting\n```\n{\n  \"origin\": \"remote\",\n  \"type\": \"request\",\n  \"correlation\": \"8be56ded8745bf16\",\n  \"protocol\": \"HTTP/1.1\",\n  \"remote\": \"172.23.0.4\",\n  \"method\": \"GET\",\n  \"uri\": \"http://payment-service:8080/paymentInfo/accounts/123\",\n  \"headers\": {\n    \"accept\": [\n      \"*/*\"\n    ],\n    \"connection\": [\n      \"keep-alive\"\n    ],\n    \"content-type\": [\n      \"application/json\"\n    ],\n    \"host\": [\n      \"payment-service:8080\"\n    ],\n    \"user-agent\": [\n      \"Java/11.0.3\"\n    ],\n    \"x-b3-parentspanid\": [\n      \"32cf62ffab4de32b\"\n    ],\n    \"x-b3-sampled\": [\n      \"1\"\n    ],\n    \"x-b3-spanid\": [\n      \"fcfe924a3b3a35c6\"\n    ],\n    \"x-b3-traceid\": [\n      \"32cf62ffab4de32b\"\n    ]\n  },\n  \"body\": \"\"\n}\n```\n\n\nOutgoing Log\n```\npayment-service_1  | 2019-05-28 00:33:07.123 TRACE [payment-service,32cf62ffab4de32b,fcfe924a3b3a35c6,true] 1 --- [nio-8080-exec-1] org.zalando.logbook.Logbook              : {\"origin\":\"local\",\"type\":\"response\",\"correlation\":\"8be56ded8745bf16\",\"duration\":1242,\"protocol\":\"HTTP/1.1\",\"status\":200,\"headers\":{\"Content-Type\":[\"application/json;charset=UTF-8\"],\"Date\":[\"Tue, 28 May 2019 00:33:07 GMT\"],\"Transfer-Encoding\":[\"chunked\"]},\"body\":{\"id\":123,\"date\":\"2019-05-28T00:33:07.014+0000\"}}\n```\nAfter Re-formatting\n```\n{\n  \"origin\": \"local\",\n  \"type\": \"response\",\n  \"correlation\": \"8be56ded8745bf16\",\n  \"duration\": 1242,\n  \"protocol\": \"HTTP/1.1\",\n  \"status\": 200,\n  \"headers\": {\n    \"Content-Type\": [\n      \"application/json;charset=UTF-8\"\n    ],\n    \"Date\": [\n      \"Tue, 28 May 2019 00:33:07 GMT\"\n    ],\n    \"Transfer-Encoding\": [\n      \"chunked\"\n    ]\n  },\n  \"body\": {\n    \"id\": 123,\n    \"date\": \"2019-05-28T00:33:07.014+0000\"\n  }\n}\n```\n\n## 2. Distributed Tracing\n\nA common requirement for services in distributed system is the ability to trace requests sent/received between services and understand where most time is spent. This is espically important when serving a request involves multiple services and the interaction between them is too complex to comprehend easily.\n\n\n[Spring Cloud Sleuth](https://github.com/spring-cloud/spring-cloud-sleuth) is a distributed tracing tool for Spring Cloud. It instruments Spring components to collect trace information and can optionally send it to a Zipkin server for visualization.\n\n[Zipkin](https://github.com/apache/incubator-zipkin) is a distributed tracing system. It helps collecting tracing data, storing, lookup and visualization. Zipkin also provides a Spring Boot based server.\n\n\n\n### Features of Spring Cloud Sleuth\n\n* Trace and span IDs are added to Slf4J MDC and available in the log message.\n* Timing information is available for latency analysis.\n* [OpenTracing](https://opentracing.io/) compatible\n\n\n### Configuration\n\n**`pom.xml`**\n\n```\n\u003cdependencies\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003eorg.springframework.cloud\u003c/groupId\u003e\n        \u003cartifactId\u003espring-cloud-starter-sleuth\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n\n    \u003cdependency\u003e\n        \u003cgroupId\u003eorg.springframework.cloud\u003c/groupId\u003e\n        \u003cartifactId\u003espring-cloud-starter-zipkin\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n\u003c/dependencies\u003e\n\n\u003cdependencyManagement\u003e\n    \u003cdependencies\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.cloud\u003c/groupId\u003e\n            \u003cartifactId\u003espring-cloud-dependencies\u003c/artifactId\u003e\n            \u003cversion\u003e${spring-cloud.version}\u003c/version\u003e\n            \u003ctype\u003epom\u003c/type\u003e\n            \u003cscope\u003eimport\u003c/scope\u003e\n        \u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\u003c/dependencyManagement\u003e     \n```\n\n**Sample configuration in `application.yaml`**\n```\nspring:\n  application:\n    name: Account\n  sleuth:\n    sampler:\n      # demo purpose only\n      probability: 1.0\n  zipkin:\n    # zipkin server location\n    base-url: http://localhost:9411\n    enabled: true\n    service:\n      name: account-service\n```\n\n### HTTP Headers\n\n* X-B3-SpanId\n* X-B3-TraceId\n* X-B3-ParentSpanId\n* X-Span-Export\n\n### How Does It Work\n\nA new Trace ID will be created automatically the first time a request is entering into the system. As part of processing the same request, a new Span ID will be created automatically every time a process is interacting with external services (including but not limited to database, queue, etc). Those trace and span information are sent over to Zipkin server for storage and analysis.  \n\n![tracing01](./docs/img/zipkin_tracing_01.png)\n\n![tracing02](./docs/img/zipkin_tracing_02.png)\n\n\n## 3. API Endpoint Client\n\nA typical communication mechanism between services is through HTTP. For a sending side to communicate with the receiving side, there are a few options to be considered:\n\n* [Swagger Codegen](https://swagger.io/tools/swagger-codegen/)\n* Hand crafted Interface class by the sending service.\n* Model classes packaged in a module or library provided by the receiving service.\n* Declarative REST Client using Spring Cloud OpenFeign\n\n\nTo quickly implement the communication solution, [Spring Cloud OpenFeign](https://spring.io/projects/spring-cloud-openfeign) is a solution to be considered. It requires few little configuration and implementation effort. \n\n### Configuration\n\n**`pom.xml`** (on the sending service side)\n```\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.springframework.cloud\u003c/groupId\u003e\n    \u003cartifactId\u003espring-cloud-starter-openfeign\u003c/artifactId\u003e\n\u003c/dependency\u003e\n\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.springframework.cloud\u003c/groupId\u003e\n    \u003cartifactId\u003espring-cloud-starter-netflix-ribbon\u003c/artifactId\u003e\n\u003c/dependency\u003e\n```        \n\n**Sample configuration in `application.yaml`** (on the sending service side)\n\n```\n#1 feign configuration\nfeign:\n  okhttp:\n    enabled: false\n  httpclient:\n    enabled: true\n  client:\n    config:\n      # payment service connection configuration\n      payment:\n        connectTimeout: 5000\n        readTimeout: 10000\n        loggerLevel: full\n\nclient:\n  payment:\n    name: payment\n    url: http://localhost:8081\n```    \n\n**Sample Spring Configuration** (on the sending service side)\n\n```\n@FeignClient(name = \"payment\", url = \"${client.payment.url}\")\npublic interface PaymentClient {\n    // #2 endpoint declaration\n    @RequestMapping(method = RequestMethod.GET, value = \"/paymentInfo/accounts/{accountId}\", consumes = \"application/json\")\n    // #3 response class is defined on the sending side\n    PaymentInfo getPaymentInfo(@PathVariable long accountId);\n}\n```\n\n**API Endpoint Response Class** (on the sending service side)\n```\n// #3\npublic class PaymentInfo {\n    long id;\n    Date date;\n}\n```\n\n**Note**\n\n* **#1** receiving service configuration is defined in application.yaml file\n* **#2** API endpoint definition is declared here\n* **#3** API endpoint response class is defined on the sending side\n\n### Discussion\n\n* The solution decouples the services. Sending service does NOT need to depend on receiving service. Software release can then be done with less coupling.\n* Declarative configuration is easier than code generation.\n* Spring Cloud OpenFeign does depend on a Client Load Balancer solution. For it to work, an additional dependency (Ribbon) is also brought in as well. Whether or not using Client Load Balancer together in the Spring Boot application is recommended is up for debate.\n\n\n## 4. Service Discovery\n\n[Consul](https://www.consul.io/) provides a Key/Value Store for storing configuration and other metadata. In additional to that, Consul also provides **Service Discovery** and **Health Checking** functionalities. \n\n[Spring Cloud Consul](https://spring.io/projects/spring-cloud-consul) is chosen to provide Consul integrations for Spring Boot application. When application starts up, it registeres with Consul server and retrieves configuration data from it. Once registered, Consul performs health check on a regular basis and allows other applications to discover this service. When configuration data is updated, application can also discover the change by using Config Watch which runs on a configurable interval.\n\n### Configuration\n\n**`pom.xml`** \n```\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.springframework.cloud\u003c/groupId\u003e\n    \u003cartifactId\u003espring-cloud-starter-consul-all\u003c/artifactId\u003e\n\u003c/dependency\u003e\n```\n\n**Sample configuration in `bootstrap.yaml`**\n\nThe Consul configuration shown here assumes Consul Server is reachable at localhost. Dependents on the configuration, a Consul Agent running at localhost is more likely a better choice. Consul Agent then communicates with Consul Server on behave of the application so that application does not need to know where and how to communicate to Consul Server.\n\n\n```\nspring:\n  application:\n    name: Account\n  cloud:\n    consul:\n      host: localhost\n      port: 8500\n      discovery:\n        # to have unique name in Consul\n        instance-id: ${spring.application.name}:${random.value}\n      config:\n        enabled: true\n        watch:\n          # delay in milliseconds, the frequency of when ConfigWatch is called\n          delay: 1000\n        prefix: config\n```\n\nThe **DiscoveryController** class has sample code to demonstrate how to use **DiscoveryClient** to find out the network location of the dependent service.\n\n\n## 5. Configuration Management\n\n### Configuration\n\n**Sample configuration in `bootstrap.yaml`**\n\n```\nspring:\n  application:\n    name: Account\n  cloud:\n    consul:\n      host: localhost\n      port: 8500\n      config:\n        enabled: true\n        watch:\n          # delay in milliseconds, the frequency of when ConfigWatch is called\n          delay: 1000\n        prefix: config\n```\n\n**Configuration Data In Consul Key/Value Store**\n\nConfigure a key/value pair with the following information\n\n|Key|Value|\n|---|---|\n|config/Account/custom.description|some value|\n\n\nNote on the key format:\n  * `config` is the prefix configured in **bootstrap.yaml** file under `spring.cloud.consul.config.prefix`.\n  * Service name is `Account` and is case sensitive\n  * `custom.description` maps to the Configuration Properties class shown below\n\n\n![consul config](./docs/img/consul_configuration.png)\n\n\n**Configuration Property Class**\n\nAll the properties are encapsulated in the **CustomProperties** class and **ConfigurationProperties** annotation is configured with value `custom`. With this configuration, each key configured in Consul will have the `custom.{KEY_NAME}` format. An example of key would be `custom.description`.\n\n```\n@RefreshScope\n@Configuration\n@ConfigurationProperties(\"custom\")\npublic class CustomProperties {\n    private String description;\n}\n```\n\nNote:\n* With `RefreshScope` annotation, Config Watch will watch changes in Consul and update value accordingly.\n\n\n## Running Infrastructure Services\n\nInfrastructure services are the services which are supporting application services but itself does not contain business logic. Example such as Vault, Consul and Zipkin.\n\n### Preparation\n\n#### Create Vault Private Key and Certificate\n\nUse the following command to generate a private key and self-signed certificate. Make modification to the `subj` as needed. Note that SAN is configured for `DNS:vault` and `IP:127.0.0.1`. SAN is important in the sens that Spring Cloud Vault will verify Vault server's certificate when application tries to connect to it. \n\n```\nopenssl req \\\n       -newkey rsa:2048 -nodes -keyout vault-private.key \\\n       -x509 -days 365 -out vault.crt \\\n       -subj \"/C=US/ST=Washington/L=Seattle/O=Awesome Company/CN=awesome.dev\" \\\n       -reqexts SAN \\\n       -config \u003c(cat /etc/ssl/openssl.cnf \\\n        \u003c(printf \"\\n[SAN]\\nsubjectAltName=DNS:vault,IP:127.0.0.1\")) \\\n        -extensions SAN\n```        \n\n`vault/config` directory already includes a private key and certificate for Vault. If new files are generated, they should replace existing ones.\n\n#### Create Truststore For Application \n\nOne way for application to trust Vault's certificate is to add the certificate in its truststore. Use the following command to create a new truststore and import Vault's cert into it.\n\n```\nkeytool --keystore truststore.jks -import -file vault.crt\n```\n\n\n#### Vault TLS Configuration\n\n- In [docker-compose-infra.yaml](docer-compose-infra.yaml) file, an environment variable `VAULT_CACERT=/vault/config/vault.crt` is set to use the same certificate file as CA cert so that Vault will not complain about not being able to verify ceritifcate. This is only for demonstration purpose. \n\n\n- [local.json](vault/config/local.json) contains the Vault configuration. In this file, \n  - `tls_cert_file` and `tls_key_file` specifies the location of the certificate and private key files. \n\n  - `tls_disable_client_certs` is set to true such that client does not need to present a certificate when it tries to connect. This is also for demonstration purpose.\n\n\n### Run the Infrastructure Services Using Docker Compose\n\nUse the following command to run infrastructure services that applications depden on. \n\n```\ndocker-compose -f docker-compose-infra.yaml up -d\n```\n\n#### Initialize and Unseal Vault\n\nUse the following command to shell into Vault container.\n\n```\ndocker exec -it  $(docker ps | grep vault | awk '{print $1}') /bin/sh\n```\n\nOnce in, use the following command to unseal Vault\n\n```\nvault operator init\n```\n\nUse the 3 out of 5 unseal keys to unseal Vault. Take a note of root token as well.\n\n```\n/ # vault operator init\nUnseal Key 1: iAWUEldvuMRgE2jZg+EaFQOx9c1ecitbzeB6DKTP11sx\nUnseal Key 2: kTlBu/QVeBm5jptqXfHR6u+TGtbsRWIYhoelB2paxQIH\nUnseal Key 3: XMCKzQPdKhjlfLwyXWmn/F4ZB9ifS+U26MfeVMDrHnvz\nUnseal Key 4: XnjUkiBi3qvCR/47AvMqjBZjCbZPo5ykdWptUAlCaZ/0\nUnseal Key 5: cXIhVy9ZeWv7C93cjJOz5AWy7vX0Z61w52vh+cTYW2h8\n\nInitial Root Token: s.39FdyeIge13g3dX4VooS7tO0\n\n```\n\n#### Seed Vault With Secrets\n\nOnce Vault is unsealed, we can start storing secrets into the Vault. The actual content will be encrypted and stored in Consul.\n\nUse the following commands to create a new token for application to use.\n\n```\n# login with root token\nvault login \n\n# create new token for application to use. make a note of the new token and set up environment variable such as SPRING_CLOUD_VAULT_TOKEN\nvault token create\n\n# enable KV secret engine and specifically specify the path\nvault secrets enable -path=secret kv\n\n# this is only for demo purpose so that account service can read the values from the secret\nvault write secret/Account vault.username=mike vault.password=hello\n\n```\n\nAlternatively, Vault provides UI which you can use to create secrets. Login requres a token.\n\n![vault UI](docs/img/vault_secret.png)\n\n\n#### Application Configuration\n\nIn the application where vault is being used to retrieve information, use `@ConfigurationProperties` annotation and specifies `vault` as value. This way secret value with key `vault.username` will map to the `username` property in this class.\n\n```\n@Configuration\n@ConfigurationProperties(value = \"vault\")\n@EnableConfigurationProperties(VaultSecret.class)\npublic class VaultSecret {\n    private String username;\n    private String password;\n}\n```\n\n\n## Running Application Services\n\nWhen running each service individually (not using `docker-compose`): \n- Account Service is running at `localhost:8080`  \n- Payment Service is running at `localhost:8081`\n- Zipkin is assumed to be available at `localhost:9411`\n- Consul is assumed to be available at `localhost:8500`\n- Vault is assumed to be available at `localhost:8200`\n\n\nIf you would like to build the services as container, a Dockerfile has been provided for each service.\n\nExecute the following command to build container image for all services.\n\n```\ndocker-compose -f docker-compose.yaml build\n```\n\nA `docker-compose.yaml` has been provided to run all services on the host where docker and docker-compose are available.\n\n```\ndocker-compose -f docker-compose.yaml up -d\n```\n\nAfter all services are up and running, \n\n**Account Service** \n\nAccount Service is available at `http://localhost:8080`\n\nTo retrieve account information for an account id:\n\n```\nhttp://localhost:8080/accounts/123\n```\n\n\n**Payment Service**\nPayment Service is available at `http://localhost:8081`\n\nTo retrieve payment information for account of id \n\n```\nhttp://localhost:8081/paymentInfo/accounts/123\n```\n\n**Zipkin Server**\n\nZipkin is available at `http://localhost:9411`\n\n**Consul**\n\nConsul UI is available at `http://localhost:8500`\n\n**Vault**\n\nVault UI is available at `https://localhost:8200/ui`\n\n\n## Continuous Integration: Google Cloud Build\n\nA Cloud Build `cloudbuild.yaml` file has been provided and the github repository has been configured to have a build triggered every time there is a commit pushed to any branch.\n\nInside `cloudbuild.yaml`, a Docker Cloud Builder is used to execute the `docker build` command for each service using the respective `Dockerfile`.\n\nIt is possible to extend this file to also push container image to a container registry such as GCR. \n\n## Continuous Integration: Gitlab CICD\n\nA `.gitlab-ci.yaml` file has been provided such that a build will be triggered every time a commit is pushed to any branch. What it currently does is to build each service using the provided `Dockerfile. It is possible to extend current pipeline to push container images to container register or deploy to target environment.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcch0%2Fopinionated-spring-boot-solution","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcch0%2Fopinionated-spring-boot-solution","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcch0%2Fopinionated-spring-boot-solution/lists"}