Kotlin Spring WebFlux, R2DBC and Redisson microservice in k8s 👋✨💫
Tags: kotlin, spring, kubernetes, microservices
Published: 2022-10-17 08:25:58

#### 👨‍💻 Full list what has been used:
[Spring](https://spring.io/) Spring web framework 
[Spring WebFlux](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html) Reactive REST Services 
[Spring Data R2DBC](https://spring.io/projects/spring-data-r2dbc) a specification to integrate SQL databases using reactive drivers 
[Redisson](https://redisson.org/) Redis Java Client 
[Zipkin](https://zipkin.io/) open source, end-to-end distributed [tracing](https://opentracing.io/) 
[Spring Cloud Sleuth](https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/index.html) auto-configuration for distributed tracing 
[Prometheus](https://prometheus.io/) monitoring and alerting 
[Grafana](https://grafana.com/) for to compose observability dashboards with everything from Prometheus 
[Kubernetes](https://kubernetes.io/) automating deployment, scaling, and management of containerized applications 
[Docker](https://www.docker.com/) and docker-compose 
[Helm](https://helm.sh/) The package manager for Kubernetes 
[Flywaydb](https://flywaydb.org/) for migrations<br/>


Source code you can find in the [GitHub repository](https://github.com/AleksK1NG/Kotlin-Spring-Postgres-Redis-Microservice).
he main idea of this project is the implementation of microservice using **Kotlin**, **Spring WebFlux**, **PostgresSQL**, and **Redis** with metrics and monitoring and deploying it to **k8s**. 
For interacting with PostgresSQL we will use reactive [Spring Data R2DBC](https://spring.io/projects/spring-data-r2dbc)
and for Redis caching using [Redisson](https://github.com/redisson/redisson).



All UI interfaces will be available on ports:

#### Swagger UI: http://localhost:8000/webjars/swagger-ui/index.html
<img src="https://i.postimg.cc/y6PNgrfr/Swagger-UI-2022-10-08-12-17-39.png" alt="Swagger"/>

#### Grafana UI: http://localhost:3000
<img src="https://i.postimg.cc/dVngVPQz/Spring-Boot-2-1-System-Monitor-Dashboards-Grafana-2022-10-15-15-17-52.png" alt="Grafana"/>

#### Zipkin UI: http://localhost:9411
<img src="https://i.postimg.cc/v83QHxBJ/Zipkin-2022-10-08-11-18-08.png" alt="Zipkin"/>

#### Prometheus UI: http://localhost:9090
<img src="https://i.postimg.cc/tTDHT8X3/Prometheus-Time-Series-Collection-and-Processing-Server-2022-10-08-11-19-05.png" alt="Prometheus"/>


Docker-compose file for this project:
```yaml
version: "3.9"

services:
  microservices_postgresql:
    image: postgres:latest
    container_name: microservices_postgresql
    expose:
      - "5432"
    ports:
      - "5432:5432"
    restart: always
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=bank_accounts
      - POSTGRES_HOST=5432
    command: -p 5432
    volumes:
      - ./docker_data/microservices_pgdata:/var/lib/postgresql/data
    networks: [ "microservices" ]

  redis:
    image: redis:latest
    container_name: microservices_redis
    ports:
      - "6379:6379"
    restart: always
    networks: [ "microservices" ]

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    command:
      - --config.file=/etc/prometheus/prometheus.yml
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    networks: [ "microservices" ]

  node_exporter:
    container_name: microservices_node_exporter
    restart: always
    image: prom/node-exporter
    ports:
      - '9101:9100'
    networks: [ "microservices" ]

  grafana:
    container_name: microservices_grafana
    restart: always
    image: grafana/grafana
    ports:
      - '3000:3000'
    networks: [ "microservices" ]

  zipkin:
    image: openzipkin/zipkin:latest
    restart: always
    container_name: microservices_zipkin
    ports:
      - "9411:9411"
    networks: [ "microservices" ]


networks:
  microservices:
    name: microservices
```

**Spring WebFlux** is like the documentation says a non-blocking web stack to handle concurrency with a small number of threads and scale with fewer hardware resources. 
In this project, we will use it with Kotlin Coroutines which Spring supports very well at this moment and we can use suspend functions.
In WebFlux we can use standard REST Controllers and [Functional Endpoints](https://spring.getdocs.org/en-US/spring-framework-docs/docs/spring-web-reactive/webflux/webflux-fn.html#:~:text=Spring%20WebFlux%20includes%20WebFlux.,contracts%20are%20designed%20for%20immutability.),
for this microservice chose the first one because everyone is familiar with it. Business domain and logic don't make sense for this example, as the domain model used BankAccount, BankAccountController REST Controller:

<img src="https://i.postimg.cc/bJBrmC9D/Monosnap-Swagger-UI-2022-10-14-12-56-33.png" alt="Swagger"/>

```kotlin
@RestController
@RequestMapping(path = ["/api/v1/bank"])
@Tag(name = "Bank Account", description = "Bank Account REST Endpoints")
class BankAccountController(private val bankAccountService: BankAccountService) {

    @PutMapping(path = ["{id}"])
    @Operation(method = "depositAmount", summary = "deposit amount", operationId = "depositAmount")
    suspend fun depositAmount(@PathVariable(required = true) id: UUID, @Valid @RequestBody depositAmountRequest: DepositAmountRequest) = withTimeout(httpTimeoutMillis) {
        ResponseEntity.ok(SuccessBankAccountResponse.of(bankAccountService.depositAmount(id, depositAmountRequest))
            .also { log.info("updated account: $it") })
    }

    @PostMapping
    @Operation(method = "createBankAccount", summary = "create new bank account", operationId = "createBankAccount")
    suspend fun createBankAccount(@Valid @RequestBody createBankAccountRequest: CreateBankAccountRequest) = withTimeout(httpTimeoutMillis) {
        bankAccountService.createBankAccount(createBankAccountRequest)
            .let {
                log.info("created bank account: $it")
                ResponseEntity.status(HttpStatus.CREATED).body(SuccessBankAccountResponse.of(it))
            }
    }

    @GetMapping(path = ["{id}"])
    @Operation(method = "getBankAccountById", summary = "get bank account by id", operationId = "getBankAccountById")
    suspend fun getBankAccountById(@PathVariable(required = true) id: UUID) = withTimeout(httpTimeoutMillis) {
        ResponseEntity.ok(SuccessBankAccountResponse.of(bankAccountService.getBankAccountById(id))
            .also { log.info("success get bank account: $it") })
    }

    @GetMapping(path = ["/email/{email}"])
    @Operation(method = "getBankAccountByEmail", summary = "get bank account by email", operationId = "getBankAccountByEmail")
    suspend fun getBankAccountByEmail(@PathVariable(required = true) email: String) = withTimeout(httpTimeoutMillis) {
        ResponseEntity.ok(SuccessBankAccountResponse.of(bankAccountService.getBankAccountByEmail(email))
            .also { log.info("success get bank account bu email: $it") })
    }

    @GetMapping(path = ["all/balance"])
    @Operation(method = "findAllAccounts", summary = "find all bank account with given amount range", operationId = "findAllAccounts")
    suspend fun findAllAccounts(
        @RequestParam(name = "min", defaultValue = "0") min: BigDecimal,
        @RequestParam(name = "max", defaultValue = "500000000") max: BigDecimal,
        @RequestParam(name = "page", defaultValue = "0") page: Int,
        @RequestParam(name = "size", defaultValue = "10") size: Int,
    ) = withTimeout(httpTimeoutMillis) {
        ResponseEntity.ok(bankAccountService.findByBalanceAmount(min, max, PageRequest.of(page, size))
            .map { SuccessBankAccountResponse.of(it) }
            .also { log.info("find by balance amount response: $it") })
    }

    companion object {
        private val log = LoggerFactory.getLogger(BankAccountController::class.java)
        private const val httpTimeoutMillis = 3000L
    }
}
```

The **service layer** is simple and has a few methods for creating, deposit amounts, and getting a bank account in different ways, for caching used the **Cache-Aside** pattern:
A **cache-aside** cache is the most common caching strategy available. The fundamental data retrieval logic can be summarized as follows:

1. When your application needs to read data from the database, it checks the cache first to determine whether the data is available.
2. If the data is available (a cache hit), the cached data is returned, and the response is issued to the caller.
3. If the data isn't available (a cache miss), the database is queried for the data. The cache is then populated with the data that is retrieved from the database, and the data is returned to the caller.

This approach has a couple of advantages: The cache contains only data that the application actually requests, which helps keep the cache size cost-effective. 
Implementing this approach is straightforward and produces immediate performance gains, whether you use an application framework that encapsulates lazy caching or your own custom application logic. 
A disadvantage when using cache-aside as the only caching pattern is that because the data is loaded into the cache only after a cache miss, 
some overhead is added to the initial response time because additional roundtrips to the cache and database are needed.

<img src="https://i.postimg.cc/rF48sp77/Zipkin-2022-10-15-14-55-48.png" alt="Zipkin"/>


```kotlin
@Service
interface BankAccountService {

    suspend fun depositAmount(id: UUID, depositAmountRequest: DepositAmountRequest): BankAccount

    suspend fun createBankAccount(createBankAccountRequest: CreateBankAccountRequest): BankAccount

    suspend fun getBankAccountById(id: UUID): BankAccount

    suspend fun getBankAccountByEmail(email: String): BankAccount

    suspend fun findByBalanceAmount(min: BigDecimal, max: BigDecimal, pageable: Pageable): PageImpl<BankAccount>
}
```

```kotlin
@Service
class BankAccountServiceImpl(
    private val bankAccountRepository: BankAccountRepository,
    private val redisCacheRepository: RedisCacheRepository,
    private val tracer: Tracer
) : BankAccountService {

    @Transactional
    override suspend fun depositAmount(id: UUID, depositAmountRequest: DepositAmountRequest) =
        withContext(Dispatchers.IO + tracer.asContextElement()) {
            val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountServiceImpl.depositAmount")

            try {
                val bankAccount = bankAccountRepository.findById(id) ?: throw BankAccountNotFoundException(id.toString()).also { span.error(it) }

                bankAccount.depositAmount(depositAmountRequest.amount)
                bankAccountRepository.save(bankAccount).also {
                    redisCacheRepository.setKey(id.toString(), it)
                    span.tag("bankAccount", it.toString())
                }
            } finally {
                span.end()
            }
        }

    override suspend fun createBankAccount(createBankAccountRequest: CreateBankAccountRequest): BankAccount =
        withContext(Dispatchers.IO + tracer.asContextElement()) {
            val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountServiceImpl.createBankAccount")

            try {
                bankAccountRepository.save(BankAccount.fromCreateRequest(createBankAccountRequest))
                    .also { span.tag("bankAccount", it.toString()) }
            } finally {
                span.end()
            }
        }

    override suspend fun getBankAccountById(id: UUID): BankAccount = withContext(Dispatchers.IO + tracer.asContextElement()) {
        val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountServiceImpl.getBankAccountById")

        try {
            val cachedBankAccount = redisCacheRepository.getKey(id.toString(), BankAccount::class.java)
            if (cachedBankAccount != null) return@withContext cachedBankAccount

            val bankAccount = bankAccountRepository.findById(id) ?: throw BankAccountNotFoundException(id.toString())

            redisCacheRepository.setKey(id.toString(), bankAccount)
            bankAccount.also { span.tag("bankAccount", it.toString()) }
        } finally {
            span.end()
        }

    }

    override suspend fun getBankAccountByEmail(email: String): BankAccount = withContext(Dispatchers.IO + tracer.asContextElement()) {
        val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountServiceImpl.getBankAccountByEmail")

        try {
            val cachedBankAccount = redisCacheRepository.getKey(email, BankAccount::class.java)
            if (cachedBankAccount != null) return@withContext cachedBankAccount

            val bankAccount = bankAccountRepository.findByEmail(email)
                ?: throw BankAccountNotFoundException("bank account with email: $email not found").also { span.error(it) }
            redisCacheRepository.setKey(email, bankAccount)
            bankAccount.also { span.tag("bankAccount", it.toString()) }
        } finally {
            span.end()
        }
    }

    override suspend fun findByBalanceAmount(min: BigDecimal, max: BigDecimal, pageable: Pageable): PageImpl<BankAccount> =
        withContext(Dispatchers.IO + tracer.asContextElement()) {
            val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountServiceImpl.findByBalanceAmount")

            try {
                bankAccountRepository.findByBalanceAmount(min, max, pageable)
            } finally {
                span.end()
            }
        }
}
```

The **repository layer** is more interesting, first let's implement the Postgres repository. 
In **Spring Data R2DBC** we can use a combination of exists **CoroutineSortingRepository** and our own custom implementation,
we only have to define our custom repository interface and implement and then simply extends it:


```kotlin
@Repository
interface BankAccountRepository : CoroutineSortingRepository<BankAccount, UUID>, BankAccountPostgresRepository {
    suspend fun findByEmail(email: String): BankAccount?
}
```

```kotlin
@Repository
interface BankAccountPostgresRepository {
    suspend fun findByBalanceAmount(min: BigDecimal, max: BigDecimal, pageable: Pageable): PageImpl<BankAccount>
}
```

```kotlin
@Repository
class BankAccountPostgresRepositoryImpl(
    private val template: R2dbcEntityTemplate,
    private val tracer: Tracer
) : BankAccountPostgresRepository {

    override suspend fun findByBalanceAmount(min: BigDecimal, max: BigDecimal, pageable: Pageable): PageImpl<BankAccount> =
        withContext(Dispatchers.IO + tracer.asContextElement()) {
            val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountPostgresRepositoryImpl.findByBalanceAmount")
            val query = Query.query(Criteria.where("balance").between(min, max))

            try {
                val accountsList = async {
                    template.select(query.with(pageable), BankAccount::class.java)
                        .asFlow()
                        .buffer(accountsListBufferSize)
                        .toList()
                }

                val totalCount = async { template.select(query, BankAccount::class.java).count().awaitFirst() }

                PageImpl(accountsList.await(), pageable, totalCount.await()).also { spanTagFindByBalanceAmount(span, it) }
            } finally {
                span.end()
            }
        }


    private fun spanTagFindByBalanceAmount(span: Span, data: PageImpl<BankAccount>) {
        span.tag("accountsList", data.content.size.toString())
            .tag("totalCount", data.totalElements.toString())
            .tag("pagination", data.pageable.toString())
    }

    companion object {
        const val accountsListBufferSize = 100
    }
}
```

If we want to have pagination like in JPA, we can use [PageImpl](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/PageImpl.html),
for work with **Postgres R2DBC** has [R2dbcEntityTemplate](https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#reference) with many usefully methods,
API is documented at Spring Docs.

<img src="https://i.postimg.cc/FFCSKc1b/Monosnap-Swagger-UI-2022-10-14-13-05-32.png" alt="Swagger"/>

For caching prefer use Redis, the most powerful and popular library in the java world is [Redisson](https://github.com/redisson/redisson).
Let's implement our Redis repository, for this example will create simple get and set by key methods:

```kotlin
@Repository
interface RedisCacheRepository {

    suspend fun setKey(key: String, value: Any)

    suspend fun setKey(key: String, value: Any, timeToLive: Long, timeUnit: TimeUnit)

    suspend fun <T> getKey(key: String, clazz: Class<T>): T?
}
```

```kotlin
@Repository
class RedisCacheRepositoryImpl(
    private val redissonClient: RedissonReactiveClient,
    private val mapper: ObjectMapper,
    private val tracer: Tracer
) : RedisCacheRepository {

    override suspend fun setKey(key: String, value: Any): Unit = withContext(Dispatchers.IO + tracer.asContextElement()) {
        val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountCacheRepositoryImpl.setKey")

        try {
            val serializedValue = mapper.writeValueAsString(value)

            redissonClient.getBucket<String>(getKey(key), StringCodec.INSTANCE)
                .set(serializedValue, cacheTimeToLiveSeconds, TimeUnit.SECONDS)
                .awaitSingleOrNull()
                .also {
                    log.info("redis set key: $key, value: $serializedValue")
                    span.tag("key", serializedValue)
                }
        } finally {
            span.end()
        }
    }

    override suspend fun setKey(key: String, value: Any, timeToLive: Long, timeUnit: TimeUnit): Unit = withContext(Dispatchers.IO + tracer.asContextElement()) {
        val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountCacheRepositoryImpl.setKey")

        try {
            val serializedValue = mapper.writeValueAsString(value)

            redissonClient.getBucket<String>(getKey(key), StringCodec.INSTANCE)
                .set(serializedValue, timeToLive, timeUnit)
                .awaitSingleOrNull()
                .also {
                    log.info("redis set key: $key, value: $serializedValue, timeToLive: $timeToLive $timeUnit")
                    span.tag("key", key).tag("value", serializedValue).tag("timeToLive", "$timeToLive $timeUnit")
                }
        } finally {
            span.end()
        }
    }

    override suspend fun <T> getKey(key: String, clazz: Class<T>): T? = withContext(Dispatchers.IO + tracer.asContextElement()) {
        val span = tracer.nextSpan(tracer.currentSpan()).start().name("BankAccountCacheRepositoryImpl.getKey")

        try {
            redissonClient.getBucket<String>(getKey(key), StringCodec.INSTANCE)
                .get()
                .awaitSingleOrNull()?.let {
                    mapper.readValue(it, clazz).also { value ->
                        log.info("redis get key: $key, value: $value")
                        span.tag("key", value.toString())
                    }
                }
                ?: return@withContext null
        } finally {
            span.end()
        }
    }


    private fun getKey(key: String): String = "$prefix:$key"

    companion object {
        private val log = LoggerFactory.getLogger(RedisCacheRepositoryImpl::class.java)
        private const val prefix = "bankAccountMicroservice"
        private const val cacheTimeToLiveSeconds = 250L
    }
}
```

<img src="https://i.postimg.cc/L6VmLW6V/Zipkin-2022-10-15-14-52-51.png" alt="Zipkin"/>

for serialization used a custom Jackson ObjectMapper bean, but of course, we can use provided by Redisson if we want:

```kotlin
@Configuration
class Serializer {

    @Bean(name = ["mapper"])
    @Primary
    fun getJsonSerializer(): ObjectMapper {
        return jacksonObjectMapper()
            .registerModule(ParameterNamesModule())
            .registerModule(Jdk8Module())
            .registerModule(JavaTimeModule())
            .registerModule(
                KotlinModule.Builder()
                    .withReflectionCacheSize(512)
                    .configure(KotlinFeature.NullToEmptyCollection, false)
                    .configure(KotlinFeature.NullToEmptyMap, false)
                    .configure(KotlinFeature.NullIsSameAsDefault, false)
                    .configure(KotlinFeature.SingletonSupport, false)
                    .configure(KotlinFeature.StrictNullChecks, false)
                    .build()
            )
    }
}
```

In Spring for error handling we have **@ControllerAdvice**:

```kotlin
@Order(2)
@ControllerAdvice
class GlobalControllerAdvice {

    @ExceptionHandler(value = [RuntimeException::class])
    fun handleRuntimeException(ex: RuntimeException, request: ServerHttpRequest): ResponseEntity<ErrorHttpResponse> {
        val errorHttpResponse = ErrorHttpResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.message ?: "", LocalDateTime.now().toString())
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON).body(errorHttpResponse).also {
            log.error("(GlobalControllerAdvice) INTERNAL_SERVER_ERROR RuntimeException", ex)
        }
    }

    @ExceptionHandler(value = [BankAccountNotFoundException::class])
    fun handleBankAccountNotFoundException(ex: BankAccountNotFoundException, request: ServerHttpRequest): ResponseEntity<ErrorHttpResponse> {
        val errorHttpResponse = ErrorHttpResponse(HttpStatus.NOT_FOUND.value(), ex.message ?: "", LocalDateTime.now().toString())
        return ResponseEntity.status(HttpStatus.NOT_FOUND).contentType(MediaType.APPLICATION_JSON).body(errorHttpResponse)
            .also { log.error("(GlobalControllerAdvice) BankAccountNotFoundException NOT_FOUND", ex) }
    }

    @ExceptionHandler(value = [InvalidAmountException::class])
    fun handleInvalidAmountExceptionException(ex: InvalidAmountException, request: ServerHttpRequest): ResponseEntity<ErrorHttpResponse> {
        val errorHttpResponse = ErrorHttpResponse(HttpStatus.BAD_REQUEST.value(), ex.message ?: "", LocalDateTime.now().toString())
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(errorHttpResponse)
            .also { log.error("(GlobalControllerAdvice) InvalidAmountException BAD_REQUEST", ex) }
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = [MethodArgumentNotValidException::class])
    fun handleInvalidArgument(ex: MethodArgumentNotValidException): ResponseEntity<MutableMap<String, String>> {
        val errorMap: MutableMap<String, String> = HashMap()
        ex.bindingResult.fieldErrors.forEach { error -> error.defaultMessage?.let { errorMap[error.field] = it } }
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(errorMap)
            .also { log.error("(GlobalControllerAdvice) WebExchangeBindException BAD_REQUEST", ex) }
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = [WebExchangeBindException::class])
    fun handleWebExchangeInvalidArgument(ex: WebExchangeBindException): ResponseEntity<MutableMap<String, Any>> {
        val errorMap = mutableMapOf<String, Any>()
        ex.bindingResult.fieldErrors.forEach { error ->
            error.defaultMessage?.let {
                errorMap[error.field] = mapOf(
                    "reason" to it,
                    "rejectedValue" to error.rejectedValue,
                )
            }
        }
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(errorMap)
            .also { log.error("(GlobalControllerAdvice) WebExchangeBindException BAD_REQUEST", ex) }
    }

    companion object {
        private val log = LoggerFactory.getLogger(GlobalControllerAdvice::class.java)
    }
}
```

<img src="https://i.postimg.cc/43m24YS9/Lens-2022-10-15-14-43-55.png" alt="Lens"/>

Next step let's deploy our microservice to k8s, 
we can build a docker image of the microservice in different ways, in this example using a simple multistage docker file:

```dockerfile
FROM --platform=linux/arm64 azul/zulu-openjdk-alpine:17 as builder
ARG JAR_FILE=target/Kotlin-Spring-Microservice-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM azul/zulu-openjdk-alpine:17
COPY --from=builder dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher", "-XX:MaxRAMPercentage=75", "-XX:+UseG1GC"]
```


For working with k8s like to use [Helm](https://helm.sh/), deployment for the microservice is simple and has deployment itself, Service, ConfigMap 
and ServiceMonitor.
The last one is required because for monitoring use [kube-prometheus-stack helm chart](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack)

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.microservice.name }}
  labels:
    app: {{ .Values.microservice.name }}
spec:
  replicas: {{ .Values.microservice.replicas }}
  template:
    metadata:
      name: {{ .Values.microservice.name }}
      labels:
        app: {{ .Values.microservice.name }}
    spec:
      containers:
        - name: {{ .Values.microservice.name }}
          image: {{ .Values.microservice.image }}
          imagePullPolicy: Always
          resources:
            requests:
              memory: {{ .Values.microservice.resources.requests.memory }}
              cpu: {{ .Values.microservice.resources.requests.cpu }}
            limits:
              memory: {{ .Values.microservice.resources.limits.memory }}
              cpu: {{ .Values.microservice.resources.limits.cpu }}
          livenessProbe:
            httpGet:
              port: {{ .Values.microservice.livenessProbe.httpGet.port }}
              path: {{ .Values.microservice.livenessProbe.httpGet.path }}
            initialDelaySeconds: {{ .Values.microservice.livenessProbe.initialDelaySeconds }}
            periodSeconds: {{ .Values.microservice.livenessProbe.periodSeconds }}
          readinessProbe:
            httpGet:
              port: {{ .Values.microservice.readinessProbe.httpGet.port }}
              path: {{ .Values.microservice.readinessProbe.httpGet.path }}
            initialDelaySeconds: {{ .Values.microservice.readinessProbe.initialDelaySeconds }}
            periodSeconds: {{ .Values.microservice.readinessProbe.periodSeconds }}
          ports:
            - containerPort: {{ .Values.microservice.ports.http.containerPort }}
              name: {{ .Values.microservice.ports.http.name }}
          env:
            - name: SPRING_APPLICATION_NAME
              value: microservice_k8s
            - name: JAVA_OPTS
              value: "-XX:+UseG1GC -XX:MaxRAMPercentage=75"
            - name: SERVER_PORT
              valueFrom:
                configMapKeyRef:
                  key: server_port
                  name: {{ .Values.microservice.name }}-config-map
            - name: SPRING_ZIPKIN_BASE_URL
              valueFrom:
                configMapKeyRef:
                  key: zipkin_base_url
                  name: {{ .Values.microservice.name }}-config-map
            - name: SPRING_R2DBC_URL
              valueFrom:
                configMapKeyRef:
                  key: r2dbc_url
                  name: {{ .Values.microservice.name }}-config-map
            - name: SPRING_FLYWAY_URL
              valueFrom:
                configMapKeyRef:
                  key: flyway_url
                  name: {{ .Values.microservice.name }}-config-map
            - name: SPRING_REDIS_HOST
              valueFrom:
                configMapKeyRef:
                  key: redis_host
                  name: {{ .Values.microservice.name }}-config-map
      restartPolicy: Always
      terminationGracePeriodSeconds: {{ .Values.microservice.terminationGracePeriodSeconds }}
  selector:
    matchLabels:
      app: {{ .Values.microservice.name }}

---

apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.microservice.name }}-service
  labels:
    app: {{ .Values.microservice.name }}
spec:
  selector:
    app: {{ .Values.microservice.name }}
  ports:
    - port: {{ .Values.microservice.service.port }}
      name: http
      protocol: TCP
      targetPort: http
  type: ClusterIP

---

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    release: monitoring
  name: {{ .Values.microservice.name }}-service-monitor
  namespace: default
spec:
  selector:
    matchLabels:
      app: {{ .Values.microservice.name }}
  endpoints:
    - interval: 10s
      port: http
      path: /actuator/prometheus
  namespaceSelector:
    matchNames:
      - default
        
---

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Values.microservice.name }}-config-map
data:
  server_port: "8000"
  zipkin_base_url: zipkin:9411
  r2dbc_url: "r2dbc:postgresql://postgres:5432/bank_accounts"
  flyway_url: "jdbc:postgresql://postgres:5432/bank_accounts"
  redis_host: redis
```
and values.yaml:

```yaml
microservice:
  name: kotlin-spring-microservice
  image: alexanderbryksin/kotlin_spring_microservice:latest
  replicas: 1
  livenessProbe:
    httpGet:
      port: 8000
      path: /actuator/health/liveness
    initialDelaySeconds: 60
    periodSeconds: 5
  readinessProbe:
    httpGet:
      port: 8000
      path: /actuator/health/readiness
    initialDelaySeconds: 60
    periodSeconds: 5
  ports:
    http:
      name: http
      containerPort: 8000
  terminationGracePeriodSeconds: 20
  service:
    port: 8000
  resources:
    requests:
      memory: '6000Mi'
      cpu: "3000m"
    limits:
      memory: '6000Mi'
      cpu: "3000m"
```
<img src="https://i.postimg.cc/NFNLQTL4/Lens-2022-10-08-13-39-49.png" alt="Lens"/>

As UI tool for working with k8s like use [Lens](https://k8slens.dev/) .

More details and source code of the full project you can find [GitHub repository here](https://github.com/AleksK1NG/Kotlin-Spring-Postgres-Redis-Microservice),
of course always in real-world projects, business logic and infrastructure code is much more complicated, and we have to implement many more necessary features.
I hope this article is usefully and helpfully, and be happy to receive any feedback or questions, feel free to [contact](https://www.linkedin.com/in/alexander-bryksin/) [me](https://github.com/AleksK1NG) by [email](alexander.bryksin@yandex.ru) or any [messengers](https://t.me/AlexanderBryksin) :)