An opinionated Kotlin backend service - Build & Deployment

Emanuel Moecklin
Nerd For Tech
Published in
4 min readApr 28, 2021

--

This is the second part of my series on Kotlin based backend services.
If you missed the first part: An opinionated Kotlin backend service — Framework.

Gradle Build

I had four main requirements for the build system:

  1. I wanted to use Gradle (not Maven)
  2. Use a single language for code, configuration, build and deployment (as much as possible)
  3. I wanted to manage plugin and dependency versions centrally
  4. I wanted to support a mono repo approach with each backend service being a separate Gradle module

Using Kotlin instead of Groovy supports the goal of using a single language for everything.

There are two main approaches to managing dependency versions centrally (instead of duplicating them in each module):

Property file

Define the versions in the top level gradle.properties file:

kotlinVersion=1.4.32
ktorVersion=1.5.3

For plugins use the pluginManagement function in the settings.gradle.kts file:

pluginManagement {
val kotlinVersion: String by settings
plugins {
kotlin("jvm") version kotlinVersion
}
}

The build.gradle.kts files will look like this:

// Plugins
plugins {
kotlin("jvm") /// no version needed
}
/// Dependencies
val kotlinVersion: String by project
val ktorVersion: String by project
implementation(kotlin("stdlib", kotlinVersion))
implementation("io.ktor:ktor-server-core:$ktorVersion")

buildSrc Folder

This is my preferred approach since it supports my single language to rule them all approach.

buildSrc is basically a special module that is compiled before any other module. Gradle automatically compiles and tests its code and puts it in the classpath of your build script.

For our purposes we only use it to manage dependencies and keep the module/service build files short and with little redundancy. All plugins and dependencies are managed in the two files Plugin.kt and Dependency.kt.

That way applying plugins and defining dependencies in each module becomes very simple:

plugins {
Plugin.modulePlugins.forEach { (n, v) -> id(n) version v }
}
dependencies {
Dependency.implementation.forEach(::implementation)
Dependency.runtime.forEach(::runtimeOnly)
Dependency.testImplementation.forEach(::testImplementation)
}

TaskInfo

I added TaskInfo as a Gradle plugin to display task dependencies and types. E.g. running ./gradlew tiTree assemble results in:

Containerization

Dockerization is pretty much standard for today’s backend services and gives flexibility to deploy in almost any environment, local, on-premise, cloud, Kubernetes, OpenShift, you name it.

One of the challenges was to build the app in a container using Gradle. A lot of tutorials explain the process but most of them install the build tools as part of the build process, then build and then run the application (all in the same container).

I prefer to use a multi-stage build using one image for the build and another one to run the application. That way I can use a pre-configured image for the build without the need to install anything (reducing the build time = cost) and another one for the runtime environment. Separation of environments for build and runtime also seems to be a good approach security wise.

Build

FROM gradle AS buildWORKDIR /appbuild
COPY . /appbuild
RUN gradle wrapper # better safe than sorry
RUN ./gradlew :account-service:clean :account-service:assemble

Deploy/Run

The artifact from the build step is copied to the runtime container using the COPY command:

FROM openjdk:8-jre-alpine

WORKDIR /app

COPY --from=0 appbuild/account-service/build/libs/account-service.jar application.jar

CMD ["java", "-server", "-jar", "application.jar"]

To build the runtime container use this command in the root directory of the project:

DOCKER_BUILDKIT=1 docker build -f account-service/Dockerfile -t ktor-template/account .

It will create an image with the tag “ktor-template/account” (command docker images):

which can then be run using:

docker run ktor-template/account

docker-compose

While the template contains only a single service, my intention was (as mentioned above) to support a mono repo approach with multiple services in the same repository. Using docker-compose seemed a good way to manage multiple services (plus shared services like database, gateway, messaging services, monitoring etc.).

I don’t want to elaborate on the docker-compose.yml file here but will explain some of the details later in the series when I talk about the setup and integration of the database. Of interest at the moment is only how to start the application (which was already explained in part 1 of the series):

  • in the root directory of the project run docker-compuse up (or sudo docker-compuse up)
  • Open http://localhost:2000

That’s it for part 2 of the series. If you enjoyed this follow up with An opinionated Kotlin backend service — API Routing and Documentation.

As usual feel free to provide feedback. Happy coding!

--

--