Ballerina Services on OpenShift

Hemika Kodikara
Ballerina Swan Lake Tech Blog
8 min readMar 21, 2019

This post elaborates how you can write and deploy your Ballerina services in an OpenShift cluster effortlessly.

What is Ballerina ? Ballerina is a programming language built for the purpose of implementing integration scenarios which involves microservices, message brokers, databases and etc. With Ballerina, you can write services(http, grpc and etc) with minimum effort and deploy them on the cloud with Ballerina’s cloud native-ness. Visit ballerina.io for more information on the language.

In this post we will be implementing a Ballerina service which returns random quotes from the TV series Game of Thrones . @wsizoo’s game-of-thrones-quotes API is used in generating random quotes which would be the backend of our Ballerina service. Once the Ballerina service is written, annotations from the “ballerinax/kubernetes module are used in generating the artifacts necessary to deploy the service to an OpenShift cluster. I’ll be using Minishift in my local machine to demonstrate this capability of Ballerina. Make note that I am using a nightly build of Ballerina. i.e 0.990.4-SNAPSHOT. The support for OpenShift artifact generation is introduced post 0.990.3 release. Check the download list to get a correct Ballerina distribution.

Let’s look at the Game of Thrones Quote API for a bit. A random quote can be obtained by invoking the service as follows:

https://got-quotes.herokuapp.com/quotes

And get a response like:

{"quote":"A sword swallower, through and through.","character":"Olenna Tyrell"}

A random quote from a specific character can also be obtained from the API. This can be done using a query param:

https://got-quotes.herokuapp.com/quotes?char=tyrion

So the quote gets restricted to a quote by Tyrion:

{"quote":"You love your children. It's your one redeeming quality - that, and your cheekbones.","character":"Tyrion"}

The service that we’ll be implementing will be a little bit different in terms of the API. How ? Like this:

+---------------------+------------------------------+
| Game of Thrones API | Our Ballerina Service |
+---------------------+------------------------------+
| /quotes | /got/quotes |
| /quotes?char=tyrion | /got/quotes/character/tyrion |
+---------------------+------------------------------+

Yes, instead of using a query param we will use the path to state the character. Why change ? Just so that we have something to implement instead of writing a simple passthrough service. D’OH!

Ballerina Service Implementation

I’ll go through the implementation and explain the important stuff.

The “gotService” is the Ballerina service and its attached to an http listener endpoint. The listener endpoint uses the port 9090. Hence the service will startup on the 9090 port with the hostname as localhost or 0.0.0.0.

listener http:Listener gotServiceEP = new(9090);
....
service gotService on gotServiceEP {...}

There are 2 resource functions in the service.

  • First resource function(getQuote) is used in getting a random quote from any character.
  • Second resource function(getQuoteFromCharacter) gets a quote from a specific character.
@http:ResourceConfig {
methods: ["GET"],
path: "/quotes"
}
resource function getQuote(http:Caller caller, http:Request request) {...)
@http:ResourceConfig {
methods: ["GET"],
path: "/quotes/character/{character}"
}
resource function getQuoteFromCharacter(http:Caller caller, http:Request request, string character) {...}

The “validateCharacter” function validates the given character and returns a “character” string value untainted. An error is returned if invalid character is found. Check here for more information about the “untaint” keyword.

var validCharacter = validateCharacter(character);
if (validCharacter is string) {
...
} else {
// if invalid character
http:Response errorResponse = new;
errorResponse.statusCode = 400;
...
}

An http response is returned to the client with http status 400 if “validCharacter” value is of type error. Meaning that there is no such character.

http:Client gotQuoteAPI = new("https://got-quotes.herokuapp.com/quotes");

“gotQuoteAPI” variable is the http client for the backend API which we discussed earlier. The “getQuote” resource function uses this client to get a random quote by invoking as follows:

var gotQuoteResponse = gotQuoteAPI->get("/")

Similarly the “getQuoteFromCharacter“ function gets a quote from a character as follows:

var gotQuoteResponse = gotQuoteAPI->get("/?char=" + validCharacter)

The rest of the code in the resource functions are about responding the quote back to the caller client.

var response = caller->respond(quoteResponse);

Now let’s startup the service and test it using curl commands. Use “ballerina run” command to startup the server.

$> ballerina run got_quote_service.bal
Initiating service(s) in 'got_quote_service.bal'
[ballerina/http] started HTTP/WS endpoint 0.0.0.0:9090

Get a random quote:

$> curl -v http://localhost:9090/got/quotes/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /got/quotes/ HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 109
< server: ballerina/0.990.4-SNAPSHOT
< date: Thu, 21 Mar 2019 13:08:33 +0530
<
* Connection #0 to host localhost left intact
{"quote":"History is a wheel, for the nature of man is fundamentally unchanging.", "character":"Lord Rodrik"}

Get a random quote from Tyrion:

$> curl -v http://localhost:9090/got/quotes/character/tyrion
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /got/quotes/character/tyrion HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 112
< server: ballerina/0.990.4-SNAPSHOT
< date: Thu, 21 Mar 2019 13:08:55 +0530
<
* Connection #0 to host localhost left intact
{"quote":"It's not easy being drink all the time. If it were easy, everyone would do it.", "character":"Tyrion"}

Get a random quote from an invalid character:

$> curl -v http://localhost:9090/got/quotes/character/ragnar
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /got/quotes/character/ragnar HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< content-type: application/json
< content-length: 41
< server: ballerina/0.990.4-SNAPSHOT
< date: Thu, 21 Mar 2019 13:09:49 +0530
<
* Connection #0 to host localhost left intact
{"err":"invalid character found: ragnar"}

We get a bad request(400 http status) when we use an invalid character.

Seems like our Ballerina service is working as expected. Yay!

Tyrion is Impressed!

Deploying the Ballerina Service to OpenShift

With the help of Ballerina annotations from the “ballerinax/kubernetes” module, the following artifacts are generated:

  • An OpenShift BuildConfig for building the docker image for our Ballerina service. When a build is triggered, the built docker image will be in the OpenShift’s docker registry.
  • An OpenShift ImageStream for the docker image.
  • An OpenShift Route to expose the service to the public.
  • A Kubernetes Deployment to create pods from the image generated by the OpenShift BuildConfig.
  • A Kubernetes Service to access the pods.

First we need to create a new project in our OpenShift cluster. Let’s say the name of the project is “got-quote-proj”.

oc new-project got-quote-proj \
--description="Game of Thrones Quotes"

Next in the source code we need to import the “ballerinax/kubernetes” module. This module is included in the ballerina distribution by default. You don’t need to pull the package from central.io.

import ballerinax/kubernetes;

Add the @kubernetes:Service annotation to the listener variable. This will generate the Kubernetes Service.

@kubernetes:Service {}
listener http:Listener gotServiceEP = new(9090);

Add the following @kubernetes:Deployment annotation to the service.

@kubernetes:Deployment {
namespace: "got-quote-proj",
registry: "172.30.1.1:5000",
buildImage: false,
buildExtension: "openshift"
}

The namespace should have the value of the OpenShift project name. The value for “registry” is the IP address and port of the docker registry. This value can be found using the following command:

minishift openshift registry

We need to set the “buildImage” value to “false” because by default a Docker image is built when executing the “ballerina build” command on the Ballerina file. We dont need that as the OpenShift BuildConfig is suppose to take care of it.

The value “openshift” needs to be set to the “buildExtension” field of the annotation. The OpenShift BuildConfig artifact will get generated only when this value is set.

Add the @openshift:Route annotation as follows to the listener. You will need to import the “ballerinax/openshift” module as well:

@openshift:Route {
host: "www.got-quote.com"
}

So the Ballerina service is exposed by the “www.got-quote.com” url.

Heres a trimmed version of the source code with the annotations.

Next lets run the build command which will generated the artifacts mentioned earlier.

$> ballerina build got_quote_service.balCompiling source
got_quote_service.bal
Generating executable
./target/got_quote_service.balx
@kubernetes:Service - complete 1/1
@kubernetes:Deployment - complete 1/1
@kubernetes:Docker - complete 3/3
@kubernetes:Helm - complete 1/1
@openshift:BuildConfig - complete 1/1
@openshift:ImageStream - complete 1/1
@openshift:Route - complete 1/1
Run the following command to deploy the OpenShift artifacts:
oc apply -f /Users/hemikak/oc-sample/target/kubernetes/got_quote_service/openshift
Run the following command to start a build:
oc start-build bc/got-quote-service-openshift-bc --from-dir=./target --follow
Run the following command to deploy the Kubernetes artifacts:
kubectl apply -f /Users/hemikak/oc-sample/target/kubernetes/got_quote_service

Here are the OpenShift artifacts:

---
apiVersion: "build.openshift.io/v1"
kind: "BuildConfig"
metadata:
annotations: {}
labels:
build: "got-quote-service-openshift-bc"
name: "got-quote-service-openshift-bc"
namespace: "got-quote-proj"
spec:
nodeSelector: {}
output:
to:
kind: "ImageStreamTag"
name: "got_quote_service:latest"
source:
binary: {}
strategy:
dockerStrategy:
dockerfilePath: "kubernetes/got_quote_service/docker/Dockerfile"
forcePull: false
noCache: false
triggers: []
---
apiVersion: "image.openshift.io/v1"
kind: "ImageStream"
metadata:
annotations: {}
labels:
build: "got-quote-service-openshift-bc"
name: "got_quote_service"
namespace: "got-quote-proj"
---
apiVersion: "route.openshift.io/v1"
kind: "Route"
metadata:
annotations: {}
labels: {}
name: "gotserviceep-openshift-route"
namespace: "got-quote-proj"
spec:
host: "www.got-quote.com"
port:
targetPort: 9090
to:
kind: "Service"
name: "gotserviceep-svc"
weight: 100

Let’s deploy the above OpenShift artifacts using the command from the build output.

$> oc apply -f /Users/hemikak/oc-sample/target/kubernetes/got_quote_service/openshiftbuildconfig.build.openshift.io/got-quote-service-openshift-bc created
imagestream.image.openshift.io/got_quote_service created
route.route.openshift.io/gotserviceep-openshift-route created

Now lets trigger a new build which will create the docker image for our Ballerina service. Use the command from the build output and execute it from the same folder where we invoked “ballerina build”.

$> oc start-build bc/got-quote-service-openshift-bc --from-dir=./target --followUploading directory "target" as binary input for the build ...Uploading finished
build.build.openshift.io/got-quote-service-openshift-bc-1 started
Receiving source from STDIN as archive ...
Step 1/7 : FROM ballerina/ballerina-runtime:0.990.4-SNAPSHOT
---> bfef5b5f929f
Step 2/7 : LABEL maintainer "dev@ballerina.io"
---> Using cache
---> 4ac00c2a20f8
Step 3/7 : COPY got_quote_service.balx /home/ballerina
---> 838285f5591a
Removing intermediate container 66f06e0f46f8
Step 4/7 : EXPOSE 9090
---> Running in 7fe1bd73835f
---> 6535c12c904b
Removing intermediate container 7fe1bd73835f
Step 5/7 : CMD ballerina run got_quote_service.balx
---> Running in 526474fda065
---> 898b57ad0a6e
Removing intermediate container 526474fda065
Step 6/7 : ENV "OPENSHIFT_BUILD_NAME" "got-quote-service-openshift-bc-1" "OPENSHIFT_BUILD_NAMESPACE" "got-quote-proj"
---> Running in 00af58d6a83e
---> 24a733f8fef8
Removing intermediate container 00af58d6a83e
Step 7/7 : LABEL "io.openshift.build.name" "got-quote-service-openshift-bc-1" "io.openshift.build.namespace" "got-quote-proj"
---> Running in 2b441e2d4eb3
---> 50f3dbb7af68
Removing intermediate container 2b441e2d4eb3
Successfully built 50f3dbb7af68
Pushing image 172.30.1.1:5000/got-quote-proj/got_quote_service:latest ...
Pushed 1/6 layers, 18% complete
Pushed 2/6 layers, 34% complete
Pushed 3/6 layers, 72% complete
Pushed 4/6 layers, 84% complete
Pushed 5/6 layers, 97% complete
Pushed 6/6 layers, 100% complete
Push successful

Let’s check if the docker image is there in the registry. Use the “eval $(minishift docker-env)” command to set the Docker host and etc for the docker command. Note: The size of the docker image you get would less than whats on the output below.

$> eval $(minishift docker-env)
$> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
172.30.1.1:5000/got-quote-proj/got_quote_service latest 50f3dbb7af68 About a minute ago 181MB

Our docker image is now in the registry. Now all thats left is to deploy the Kubernetes artifacts. Use the command from the build output.

$> kubectl apply -f /Users/hemikak/oc-sample/target/kubernetes/got_quote_serviceservice/gotserviceep-svc created
deployment.apps/got-quote-service-deployment created

Let’s check if the pods are up and running.

$> oc get pods
NAME READY STATUS RESTARTS AGE
got-quote-service-deployment-65c6fbd94f-k8nkl 1/1 Running 0 39m

We have set the url “www.got-quote.com” in our OpenShift Route artifact. Since I am too bored to set DNS to resolve to the OpenShift cluster. I am gonna set DNS resolving in the curl command mapping the said url to the IP of OpenShift cluster. You can get the OpenShift cluster IP using “minishift ip” command. In my case it was “192.168.99.101”.

$> curl --resolve 'www.got-quote.com:80:192.168.99.101' http://www.got-quote.com/got/quotes/character/tyrion{"quote":"The greatest fools are ofttimes more clever than the men who laugh at them", "character":"Tyrion"}

TA-DA! . We got our Ballerina service up and running on OpenShift!. All we had to put was a few annotation and it’s all deployable in a cluster.

Try it out and leave some comments. Thanks!

--

--