Serverless Kotlin on Google Cloud Run
Learn how to build a serverless API using Ktor then dockerize and deploy it to Google Cloud Run. By Kshitij Chauhan.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Serverless Kotlin on Google Cloud Run
25 mins
- Getting Started
- Defining Serverless
- Understanding Cloud Run
- Using Docker
- Getting Started with Ktor
- Creating the HTTP Server
- Starting the Server
- Detecting the Client’s IP Address
- Fetching Locations Using IP Addresses
- Adding Kotlinx Serialization and Ktor Client
- Using Ktor Client
- Fetching Location Data
- Containerizing the Application
- Pushing Images to Artifact Registry
- Deploying Image to Cloud Run
- Consuming the API
- Defining a Service Interface
- Using the Service Interface
- Integrating with ViewModel
- Where to Go From Here?
Starting the Server
In the Application.kt file, add a main
method that starts the server:
val server = ... fun main() { server.start(wait = true) }
The wait
parameter tells the application to block until the server terminates.
At this point, you have everything you need to get a basic server up and running. To start the server, use the green icon next to main
IntelliJ:
If everything went well, you’ll see logs indicating your server is running!
To test your server, use the curl
command line utility. Enter the following command in the terminal:
curl -X GET "http://0.0.0.0:8080/"
You’ll see the correct response: “Hello, world!”.
➜ ~ curl -X GET "http://0.0.0.0:8080/" Hello, world!
curl: (7) Failed to connect to 0.0.0.0 port 8080 after 0 ms: Address not available
, replace your curl request with curl -X GET "http://0.0.0.0:8080/"
.You could update the networking configuration in order to continue using 0.0.0.0 but that’s outside the scope of this tutorial. In subsequent requests, you’ll have to keep using localhost instead of 0.0.0.0.
Detecting the Client’s IP Address
In the routing
function, add a route that returns the client’s IP address back to them. To get the client’s IP address, use the origin
property of the request
object associated with a call
.
import io.ktor.server.plugins.* // Add this in the `routing` block: get("/ip") { val ip = call.request.origin.remoteHost call.respond(ip) }
This adds an HTTP GET route on the “/ip” path. On each request, the handler extracts the client’s IP address using call.request.origin.remoteHost
and returns it in the response.
Restart the server, and try this new route using curl
again:
➜ ~ curl -X GET "http://0.0.0.0:8080/ip" localhost% ➜ ~
The server responds with localhost, which just means the client and server are on the same machine.
Fetching Locations Using IP Addresses
To fetch a client’s location from their IP address, you need a geolocation database. There are many free third-party services that let you query geolocation databases. IP-API is an example.
IP-API provides a JSON API to query the geolocation data for an IP address. To interact with it from your server, you’ll need to make HTTP requests to it using an HTTP client. For this tutorial, you’ll use the Ktor client.
Additionally, you’ll need the ability to parse JSON responses from IP-API. Parsing and marshalling JSON data is a part of data serialization. Kotlin has an excellent first-party library, kotlinx.serialization, to help with it.
The process of detecting the client’s location will look like this:
Adding Kotlinx Serialization and Ktor Client
The kotlinx.serialization
library requires a compiler plugin as well as a support library.
Add the compiler plugin inside the plugins of the build.gradle.kts file:
plugins { // ... kotlin("plugin.serialization") version "1.6.10" }
Then add these dependencies to interop with it using Ktor:
dependencies { // ... implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-serialization:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") }
Here’s a description of these artifacts:
-
io.ktor:ktor-client-core
provides core Ktor client APIs. -
io.ktor:ktor-client-cio
provides a Coroutines-based Ktor client engine. -
io.ktor:ktor-client-serialization
,io.ktor:ktor-serialization-kotlinx-json
andio.ktor:ktor-client-content-negotiation
provide APIs to serialize request/response data in JSON format using thekotlinx.serialization
library.
Using Ktor Client
So far you’ve used Ktor as an application server. Now you’ll use the other side of Ktor: an HTTP client.
First, create a data class to model the responses of IP-API. Create a file named IpToLocation.kt, and add the following code to it:
package com.yourcompany.android.serverlesskt import kotlinx.serialization.Serializable @Serializable data class LocationResponse( val country: String, val regionName: String, val city: String, val query: String )
Then, create a function that sends an HTTP request to IP-API with the client’s IP address. In the same file, add the following code:
import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* /** * Specifies which fields to expect in the * response from the API * * More info: https://ip-api.com/docs/api:json */ private const val FIELDS = "country,regionName,city,query" /** * Prefix URL for all requests made to the IP to location API */ private const val BASE_URL = "http://ip-api.com/json" /** * Fetches the [LocationResponse] for the given IP address * from the IP to Location API * * @param ip The IP address to fetch the location for * @param client The HTTP client to make the request from */ suspend fun getLocation(ip: String, client: HttpClient): LocationResponse { // 1 val url = buildString { append(BASE_URL) if (ip != "localhost" && ip != "_gateway") { append("/$ip") } } // 2 val response = client.get(url) { parameter("fields", FIELDS) } // 3 return response.body() }
getLocation
fetches the location data for an IP address using IP-API. It uses an HttpClient
supplied to it to make the HTTP request.
First, it constructs the URL to send the request to. Second, it adds FIELDS
as a query parameter to the URL. This parameter tells IP-API which fields you want in the response (learn more here). Finally, it sends an HTTP GET request to the constructed URL and returns the response.
Fetching Location Data
To use getLocation
, you must create an instance of the Ktor HTTP client. In the Application.kt file, add the following code above main
:
import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation import io.ktor.serialization.kotlinx.json.* val client = HttpClient(CIO) { install(ClientContentNegotiation) { json() } }
This not only creates an HttpClient
, but it also adds a Ktor feature ContentNegotiation
(aliased as ClientContentNegotiation
to avoid import collision with the server feature of the same name) for JSON serialization/deserialization.
Then, add a route to your server to fetch the location data. In the routing
block, add the following route:
get("/location") { val ip = call.request.origin.remoteHost val location = getLocation(ip, client) call.respond(location) }
Note this route responds with an object of type LocationResponse
, which should be deserialized to the JSON format before sending it to the client. To tell Ktor how to deal with this, install the server-side ContentNegotiation
plugin.
First, add the following dependency in the build.gradle.kts file:
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
In the Application.kt file, modify the configuration block for embeddedServer
by adding the following code:
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation val server = embeddedServer(Netty, port=8080) { install(ServerContentNegotiation) { json() } // ... }
Finally, restart the server and use curl
to send a request to the “/location” route. You’ll see a response like this:
➜ ~ curl -X GET "http://0.0.0.0:8080/location" { "country":"<country>", "regionName":"<state>", "city":"<city>, "query":"<ip>" } ➜ ~
That’s it for your back-end API! So far you’ve built three API routes:
-
/
: Returns “Hello, world!”. -
/ip
: Returns the client’s IP address. -
/location
: Fetches the client’s IP geolocation data and returns it.
The next step is to containerize the application to deploy it on Cloud Run.