/service-composition

Primary LanguageBallerinaApache License 2.0Apache-2.0

Build Status

Service Composition

A service composition is an aggregate of services collectively composed to automate a particular task or business process.

This guide walks you through the process of implementing a service composition using Ballerina language.

The following are the sections available in this guide.

What you’ll build

To understand how you can build a service composition using Ballerina, let's consider a real-world use case of a Travel agency that arranges complete tours for users. A tour package includes airline ticket reservation, hotel room reservation and car rental. Therefore, the Travel agency service requires communicating with other necessary back-ends. The following diagram illustrates this use case clearly.

alt text

Travel agency is the service that acts as the composition initiator. The other three services are external services that the travel agency service calls to do airline ticket booking, hotel reservation and car rental. These are not necessarily Ballerina services and can theoretically be third-party services that the travel agency service calls to get things done. However, for the purposes of setting up this scenario and illustrating it in this guide, these third-party services are also written in Ballerina.

Prerequisites

Optional requirements

Implementation

If you want to skip the basics, you can download the git repo and directly move to the "Testing" section by skipping "Implementation" section.

Create the project structure

Ballerina is a complete programming language that supports custom project structures. Use the following module structure for this guide.

service-composition
  └── guide
      ├── airline_reservation
      │   ├── airline_reservation_service.bal
      │   └── tests
      │       └── airline_reservation_service_test.bal
      ├── car_rental
      │   ├── car_rental_service.bal
      │   └── tests
      │       └── car_rental_service_test.bal
      ├── hotel_reservation
      │   ├── hotel_reservation_service.bal
      │   └── tests
      │       └── hotel_reservation_service_test.bal
      └── travel_agency
          ├── travel_agency_service.bal
          └── tests
              └── travel_agency_service_test.bal
  • Create the above directories in your local machine and also create empty .bal files.

  • Then open the terminal and navigate to service-composition/guide and run Ballerina project initializing toolkit.

   $ ballerina init

Developing the service

Let's look at the implementation of the travel agency service, which acts as the composition initiator.

Arranging a complete tour travel agency service requires communicating with three other services: airline reservation, hotel reservation, and car rental. All these services accept POST requests with appropriate JSON payloads and send responses back with JSON payloads. Request and response payloads are similar for all three backend services.

Sample request payload:

{"Name":"Bob", "ArrivalDate":"12-03-2018", "DepartureDate":"13-04-2018", 
 "Preference":<service_dependent_preference>};

Sample response payload:

{"Status":"Success"}

When a client initiates a request to arrange a tour, the travel agency service first needs to communicate with the airline reservation service to book a flight ticket. To check the implementation of airline reservation service, see the airline_reservation_service.bal file.

Once the airline ticket reservation is successful, the travel agency service needs to communicate with the hotel reservation service to reserve hotel rooms. To check the implementation of hotel reservation service, see the hotel_reservation_service.bal file.

Finally, the travel agency service needs to connect with the car rental service to arrange internal transports. To check the implementation of car rental service, see the car_rental_service.bal file.

If all services work successfully, the travel agency service confirms and arrange the complete tour for the user. The skeleton of travel_agency_service.bal file is attached below. Inline comments are added for better understanding. Refer to the travel_agency_service.bal to see the complete implementation of the travel agency service.

travel_agency_service.bal
import ballerina/http;

// Service endpoint
endpoint http:Listener travelAgencyEP {
    port:9090
};

// Client endpoint to communicate with Airline reservation service
endpoint http:Client airlineReservationEP {
    url:"http://localhost:9091/airline"
};

// Client endpoint to communicate with Hotel reservation service
endpoint http:Client hotelReservationEP {
    url:"http://localhost:9092/hotel"
};

// Client endpoint to communicate with Car rental service
endpoint http:Client carRentalEP {
    url:"http://localhost:9093/car"
};

// Travel agency service to arrange a complete tour for a user
@http:ServiceConfig {basePath:"/travel"}
service<http:Service> travelAgencyService bind travelAgencyEP {

    // Resource to arrange a tour
    @http:ResourceConfig {methods:["POST"], consumes:["application/json"],
        produces:["application/json"]}
    arrangeTour(endpoint client, http:Request inRequest) {
        http:Response outResponse;
        json inReqPayload;

        // JSON payload format for an HTTP OUT request
        json outReqPayload = {"Name":"", "ArrivalDate":"", "DepartureDate":"",
            "Preference":""};

        // Try parsing the JSON payload from the user request

        // Reserve airline ticket for the user by calling airline reservation service

        // Reserve hotel room for the user by calling hotel reservation service

        // Renting car for the user by calling the car rental service

        // If all three response positive status, send a successful message to the user
        outResponse.setJsonPayload({"Message":"Congrats! Your journey is ready!"});
        _ = client -> respond(outResponse);
    }
}

Let's now look at the code segment that is responsible for parsing the JSON payload from the user request.

// Try parsing the JSON payload from the user request
match inRequest.getJsonPayload() {
    // Valid JSON payload
    json payload => inReqPayload = payload;
    // NOT a valid JSON payload
    any => {
        outResponse.statusCode = 400;
        outResponse.setJsonPayload(
                {"Message":"Invalid payload - Not a valid JSON payload"});
        _ = client -> respond(outResponse);
        done;
    }
}

outReqPayload.Name = inReqPayload.Name;
outReqPayload.ArrivalDate = inReqPayload.ArrivalDate;
outReqPayload.DepartureDate = inReqPayload.DepartureDate;
json airlinePreference = inReqPayload.Preference.Airline;
json hotelPreference = inReqPayload.Preference.Accommodation;
json carPreference = inReqPayload.Preference.Car;

// If payload parsing fails, send a "Bad Request" message as the response
if (outReqPayload.Name == () || outReqPayload.ArrivalDate == () ||
    outReqPayload.DepartureDate == () || airlinePreference == () ||
    hotelPreference == () || carPreference == ()) {
    outResponse.statusCode = 400;
    outResponse.setJsonPayload({"Message":"Bad Request - Invalid Payload"});
    _ = client -> respond(outResponse);
    done;
}

The above code shows how the request JSON payload is parsed to create JSON literals required for further processing.

Let's now look at the code segment that is responsible for communicating with the airline reservation service.

// Reserve airline ticket for the user by calling Airline reservation service
// construct the payload
json outReqPayloadAirline = outReqPayload;
outReqPayloadAirline.Preference = airlinePreference;

// Send a post request to airline service with appropriate payload and get response
http:Response inResAirline = check airlineReservationEP -> post("/reserve",
                                                    untaint outReqPayloadAirline);

// Get the reservation status
var airlineResPayload = check inResAirline.getJsonPayload();
string airlineStatus = airlineResPayload.Status.toString();
// If reservation status is negative, send a failure response to user
if (airlineStatus.equalsIgnoreCase("Failed")) {
    outResponse.setJsonPayload({"Message":"Failed to reserve airline! " +
            "Provide a valid 'Preference' for 'Airline' and try again"});
    _ = client -> respond(outResponse);
    done;
}

The above code shows how the travel agency service initiates a request to the airline reservation service to book a flight ticket. airlineReservationEP is the client endpoint you defined through which the Ballerina service communicates with the external airline reservation service.

Let's now look at the code segment that is responsible for communicating with the hotel reservation service.

// Reserve hotel room for the user by calling Hotel reservation service
// construct the payload
json outReqPayloadHotel = outReqPayload;
outReqPayloadHotel.Preference = hotelPreference;

// Send a post request to hotel service with appropriate payload and get response
http:Response inResHotel = check hotelReservationEP -> post("/reserve",
                                                    untaint outReqPayloadHotel);

// Get the reservation status
var hotelResPayload = check inResHotel.getJsonPayload();
string hotelStatus = hotelResPayload.Status.toString();
// If reservation status is negative, send a failure response to user
if (hotelStatus.equalsIgnoreCase("Failed")) {
    outResponse.setJsonPayload({"Message":"Failed to reserve hotel! " +
            "Provide a valid 'Preference' for 'Accommodation' and try again"});
    _ = client -> respond(outResponse);
    done;
}

The travel agency service communicates with the hotel reservation service to book a room for the client as shown above. The client endpoint defined for this external service call is hotelReservationEP.

Finally, let's look at the code segment that is responsible for communicating with the car rental service.

// Renting car for the user by calling Car rental service
// construct the payload
json outReqPayloadCar = outReqPayload;
outReqPayloadCar.Preference = carPreference;

// Send a post request to car rental service with appropriate payload and get response
http:Response inResCar = check carRentalEP -> post("/rent", untaint outReqPayloadCar);

// Get the rental status
var carResPayload = check inResCar.getJsonPayload();
string carRentalStatus = carResPayload.Status.toString();
// If rental status is negative, send a failure response to user
if (carRentalStatus.equalsIgnoreCase("Failed")) {
    outResponse.setJsonPayload({"Message":"Failed to rent car! " +
            "Provide a valid 'Preference' for 'Car' and try again"});
    _ = client -> respond(outResponse);
    done;
}

As shown above, the travel agency service rents a car for the requested user by calling the car rental service. carRentalEP is the client endpoint defined to communicate with the external car rental service.

Testing

Invoking the service

  • Navigate to service-composition/guide and run the following commands in separate terminals to start all four HTTP services. This will start the Airline Reservation, Hotel Reservation, Car Rental and Travel Agency services on ports 9091, 9092, 9093 and 9090 respectively.
   $ ballerina run airline_reservation/
   $ ballerina run hotel_reservation/
   $ ballerina run car_rental/
   $ ballerina run travel_agency/
  • Invoke the travel agency service by sending a POST request to arrange a tour.
   curl -v -X POST -d '{"Name":"Bob", "ArrivalDate":"12-03-2018",
   "DepartureDate":"13-04-2018", "Preference":{"Airline":"Business", 
   "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \
   "http://localhost:9090/travel/arrangeTour" -H "Content-Type:application/json"

Travel agency service will send a response similar to the following:

   < HTTP/1.1 200 OK
   {"Message":"Congratulations! Your journey is ready!!"}

Writing unit tests

In Ballerina, the unit test cases should be in the same module inside a folder named as 'tests'. When writing the test functions the below convention should be followed.

  • Test functions should be annotated with @test:Config. See the below example.
   @test:Config
   function testTravelAgencyService () {

This guide contains unit test cases for each service implemented above.

To run the tests, open your terminal and navigate to service-composition/guide, and run the following command.

   $ ballerina test

To check the implementations of these test files, refer to the airline_reservation_service_test.bal, hotel_reservation_service_test.bal, car_rental_service_test.bal and travel_agency_service_test.bal.

Deployment

Once you are done with the development, you can deploy the services using any of the methods that are listed below.

Deploying locally

  • As the first step, you can build Ballerina executable archives (.balx) of the services that we developed above. Navigate to service-composition/guide and run the following command.
   $ ballerina build <Module_Name>
  • Once the .balx files are created inside the target folder, you can run them using the following command.
   $ ballerina run target/<Exec_Archive_File_Name>
  • The successful execution of a service will show us something similar to the following output.
   Initiating service(s) in 'target/travel_agency.balx'
   [ballerina/http] started HTTP/WS endpoint 0.0.0.0:9090

Deploying on Docker

You can run the service that we developed above as a Docker container. As Ballerina platform includes Ballerina_Docker_Extension, which offers native support for running ballerina programs on containers, you just need to put the corresponding Docker annotations on your service code.

Let's see how we can deploy the travel_agency_service we developed above on Docker. When invoking this service make sure that the other three services (airline_reservation, hotel_reservation, and car_rental) are also up and running.

  • In our travel_agency_service, we need to import ballerinax/docker and use the annotation @docker:Config as shown below to enable Docker image generation during the build time.
travel_agency_service.bal
import ballerina/http;
import ballerinax/docker;

@docker:Config {
    registry:"ballerina.guides.io",
    name:"travel_agency_service",
    tag:"v1.0"
}

@docker:Expose{}
endpoint http:Listener travelAgencyEP {
    port:9090
};

// http:Client endpoint definitions to communicate with other services

@http:ServiceConfig {basePath:"/travel"}
service<http:Service> travelAgencyService bind travelAgencyEP {
  • Now you can build a Ballerina executable archive (.balx) of the service that we developed above, using the following command. This will also create the corresponding Docker image using the Docker annotations that you have configured above. Navigate to service-composition/guide and run the following command.
   $ ballerina build travel_agency
  
   Run following command to start Docker container:
   docker run -d -p 9090:9090 ballerina.guides.io/travel_agency_service:v1.0
  • Once you successfully build the Docker image, you can run it with the docker run command that is shown in the previous step.
   $ docker run -d -p 9090:9090 ballerina.guides.io/travel_agency_service:v1.0

Here we run the Docker image with flag -p <host_port>:<container_port> so that we use the host port 9090 and the container port 9090. Therefore you can access the service through the host port.

  • Verify Docker container is running with the use of $ docker ps. The status of the Docker container should be shown as 'Up'.
  • You can access the service using the same curl commands that we've used above.
   curl -v -X POST -d '{"Name":"Bob", "ArrivalDate":"12-03-2018",
   "DepartureDate":"13-04-2018", "Preference":{"Airline":"Business", 
   "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \
   "http://localhost:9090/travel/arrangeTour" -H "Content-Type:application/json"

Deploying on Kubernetes

  • You can run the service that we developed above, on Kubernetes. The Ballerina language offers native support for running a ballerina programs on Kubernetes, with the use of Kubernetes annotations that you can include as part of your service code. Also, it will take care of the creation of the Docker images. So you don't need to explicitly create Docker images prior to deploying it on Kubernetes. Refer to Ballerina_Kubernetes_Extension for more details and samples on Kubernetes deployment with Ballerina. You can also find details on using Minikube to deploy Ballerina programs.

  • Let's now see how we can deploy our travel_agency_service on Kubernetes. When invoking this service make sure that the other three services (airline_reservation, hotel_reservation, and car_rental) are also up and running.

  • First we need to import ballerinax/kubernetes and use @kubernetes annotations as shown below to enable kubernetes deployment for the service we developed above.

NOTE: Linux users can use Minikube to try this out locally.

travel_agency_service.bal
import ballerina/http;
import ballerinax/kubernetes;

@kubernetes:Ingress {
  hostname:"ballerina.guides.io",
  name:"ballerina-guides-travel-agency-service",
  path:"/"
}

@kubernetes:Service {
  serviceType:"NodePort",
  name:"ballerina-guides-travel-agency-service"
}

@kubernetes:Deployment {
  image:"ballerina.guides.io/travel_agency_service:v1.0",
  name:"ballerina-guides-travel-agency-service"
}

endpoint http:Listener travelAgencyEP {
    port:9090
};

// http:Client endpoint definitions to communicate with other services

@http:ServiceConfig {basePath:"/travel"}
service<http:Service> travelAgencyService bind travelAgencyEP {    
  • Here we have used @kubernetes:Deployment to specify the Docker image name that will be created as part of building this service.
  • We have also specified @kubernetes:Service so that it will create a Kubernetes service, which will expose the Ballerina service that is running on a Pod.
  • In addition we have used @kubernetes:Ingress, which is the external interface to access your service (with path / and host name ballerina.guides.io)

If you are using Minikube, you need to set a couple of additional attributes to the @kubernetes:Deployment annotation.

  • dockerCertPath - The path to the certificates directory of Minikube (e.g., /home/ballerina/.minikube/certs).

  • dockerHost - The host for the running cluster (e.g., tcp://192.168.99.100:2376). The IP address of the cluster can be found by running the minikube ip command.

  • Now you can build a Ballerina executable archive (.balx) of the service that we developed above, using the following command. This will also create the corresponding Docker image and the Kubernetes artifacts using the Kubernetes annotations that you have configured above.

   $ ballerina build travel_agency
  
   Run following command to deploy kubernetes artifacts:  
   kubectl apply -f ./target/travel_agency/kubernetes
  • You can verify that the Docker image that we specified in @kubernetes:Deployment is created, by using docker images.
  • Also the Kubernetes artifacts related our service, will be generated under ./target/travel_agency/kubernetes.
  • Now you can create the Kubernetes deployment using:
   $ kubectl apply -f ./target/travel_agency/kubernetes 
 
   deployment.extensions "ballerina-guides-travel-agency-service" created
   ingress.extensions "ballerina-guides-travel-agency-service" created
   service "ballerina-guides-travel-agency-service" created
  • You can verify Kubernetes deployment, service and ingress are running properly, by using following Kubernetes commands.
   $ kubectl get service
   $ kubectl get deploy
   $ kubectl get pods
   $ kubectl get ingress
  • If everything is successfully deployed, you can invoke the service either via Node port or ingress.

Node Port:

   curl -v -X POST -d '{"Name":"Bob", "ArrivalDate":"12-03-2018",
   "DepartureDate":"13-04-2018", "Preference":{"Airline":"Business", 
   "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \
   "http://localhost:<Node_Port>/travel/arrangeTour" -H "Content-Type:application/json"  

If you are using Minikube, you should use the IP address of the Minikube cluster obtained by running the minikube ip command. The port should be the node port given when running the kubectl get services command.

Ingress:

Add /etc/hosts entry to match hostname. For Minikube, the IP address should be the IP address of the cluster.

   127.0.0.1 ballerina.guides.io

Access the service

   curl -v -X POST -d '{"Name":"Bob", "ArrivalDate":"12-03-2018",
   "DepartureDate":"13-04-2018", "Preference":{"Airline":"Business", 
   "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \
   "http://ballerina.guides.io/travel/arrangeTour" -H "Content-Type:application/json" 

Observability

Ballerina is by default observable. Meaning you can easily observe your services, resources, etc. However, observability is disabled by default via configuration. Observability can be enabled by adding following configurations to ballerina.conf file and starting the ballerina service using it. A sample configuration file can be found in service-composition/guide/travel_agency/.

[b7a.observability]

[b7a.observability.metrics]
# Flag to enable Metrics
enabled=true

[b7a.observability.tracing]
# Flag to enable Tracing
enabled=true

To start the ballerina service using the configuration file, run the following command

   $ ballerina run --config travel_agency/ballerina.conf travel_agency

NOTE: The above configuration is the minimum configuration needed to enable tracing and metrics. With these configurations default values are load as the other configuration parameters of metrics and tracing.

Tracing

You can monitor ballerina services using in built tracing capabilities of Ballerina. We'll use Jaeger as the distributed tracing system. Follow the following steps to use tracing with Ballerina.

  • You can add the following configurations for tracing. Note that these configurations are optional if you already have the basic configuration in ballerina.conf as described above.
   [b7a.observability]

   [b7a.observability.tracing]
   enabled=true
   name="jaeger"

   [b7a.observability.tracing.jaeger]
   reporter.hostname="localhost"
   reporter.port=5775
   sampler.param=1.0
   sampler.type="const"
   reporter.flush.interval.ms=2000
   reporter.log.spans=true
   reporter.max.buffer.spans=1000
  • Run Jaeger Docker image using the following command
   $ docker run -d -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp -p5778:5778 \
   -p16686:16686 p14268:14268 jaegertracing/all-in-one:latest
  • Navigate to service-composition/guide and run the travel_agency_service using the following command
   $ ballerina run --config travel_agency/ballerina.conf travel_agency
  • Observe the tracing using Jaeger UI using following URL
   http://localhost:16686

Metrics

Metrics and alerts are built-in with ballerina. We will use Prometheus as the monitoring tool. Follow the below steps to set up Prometheus and view metrics for travel_agency service.

  • You can add the following configurations for metrics. Note that these configurations are optional if you already have the basic configuration in ballerina.conf as described under Observability section.
   [b7a.observability.metrics]
   enabled=true
   reporter="prometheus"

   [b7a.observability.metrics.prometheus]
   port=9797
   host="0.0.0.0"
  • Create a file prometheus.yml inside /tmp/ location. Add the below configurations to the prometheus.yml file.
   global:
     scrape_interval:     15s
     evaluation_interval: 15s

   scrape_configs:
     - job_name: prometheus
       static_configs:
         - targets: ['172.17.0.1:9797']

NOTE : Replace 172.17.0.1 if your local Docker IP differs from 172.17.0.1

  • Run the Prometheus Docker image using the following command
   $ docker run -p 19090:9090 -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml \
   prom/prometheus
  • Navigate to service-composition/guide and run the travel_agency_service using the following command
  $ ballerina run --config travel_agency/ballerina.conf travel_agency
  • You can access Prometheus at the following URL
   http://localhost:19090/

NOTE: Ballerina will by default have following metrics for HTTP server connector. You can enter following expression in Prometheus UI

  • http_requests_total
  • http_response_time

Logging

Ballerina has a log module for logging to the console. You can import ballerina/log module and start logging. The following section will describe how to search, analyze, and visualize logs in real time using Elastic Stack.

  • Start the Ballerina Service with the following command from service-composition/guide
   $ nohup ballerina run travel_agency/ &>> ballerina.log&

NOTE: This will write the console log to the ballerina.log file in the service-composition/guide directory

  • Start Elasticsearch using the following command

  • Start Elasticsearch using the following command

   $ docker run -p 9200:9200 -p 9300:9300 -it -h elasticsearch --name \
   elasticsearch docker.elastic.co/elasticsearch/elasticsearch:6.2.2 

NOTE: Linux users might need to run sudo sysctl -w vm.max_map_count=262144 to increase vm.max_map_count

  • Start Kibana plugin for data visualization with Elasticsearch
   $ docker run -p 5601:5601 -h kibana --name kibana --link \
   elasticsearch:elasticsearch docker.elastic.co/kibana/kibana:6.2.2     
  • Configure logstash to format the ballerina logs

i) Create a file named logstash.conf with the following content

input {  
 beats{ 
     port => 5044 
 }  
}

filter {  
 grok{  
     match => { 
	 "message" => "%{TIMESTAMP_ISO8601:date}%{SPACE}%{WORD:logLevel}%{SPACE}
	 \[%{GREEDYDATA:package}\]%{SPACE}\-%{SPACE}%{GREEDYDATA:logMessage}"
     }  
 }  
}   

output {  
 elasticsearch{  
     hosts => "elasticsearch:9200"  
     index => "store"  
     document_type => "store_logs"  
 }  
}  

ii) Save the above logstash.conf inside a directory named as {SAMPLE_ROOT}\pipeline

iii) Start the logstash container, replace the {SAMPLE_ROOT} with your directory name

$ docker run -h logstash --name logstash --link elasticsearch:elasticsearch \
-it --rm -v ~/{SAMPLE_ROOT}/pipeline:/usr/share/logstash/pipeline/ \
-p 5044:5044 docker.elastic.co/logstash/logstash:6.2.2
  • Configure filebeat to ship the ballerina logs

i) Create a file named filebeat.yml with the following content

filebeat.prospectors:
- type: log
  paths:
    - /usr/share/filebeat/ballerina.log
output.logstash:
  hosts: ["logstash:5044"]  

NOTE : Modify the ownership of filebeat.yml file using $chmod go-w filebeat.yml

ii) Save the above filebeat.yml inside a directory named as {SAMPLE_ROOT}\filebeat

iii) Start the logstash container, replace the {SAMPLE_ROOT} with your directory name

$ docker run -v {SAMPLE_ROOT}/filbeat/filebeat.yml:/usr/share/filebeat/filebeat.yml \
-v {SAMPLE_ROOT}/guide/travel_agency/ballerina.log:/usr/share\
/filebeat/ballerina.log --link logstash:logstash docker.elastic.co/beats/filebeat:6.2.2
  • Access Kibana to visualize the logs using following URL
   http://localhost:5601