/gcp-microservices-demo

Ballerina Implementation of GCP Online Boutique Sample

Primary LanguageBallerinaApache License 2.0Apache-2.0

Introduction

The online boutique is a cloud-native microservices demo application written by the Google cloud platform. It consists of a 10-tier microservices application. The application is a web-based e-commerce app using which users can browse items, add them to the cart, and purchase them. The microservices in the GCP demo are implemented using multiple programming languages such as Java, C#, Go, Javascript and Python.

Here, in this demo these microservices are written using Ballerina language to demonstrate the language features, showcase best practices when writing microservices, provide an in-depth understanding of how ballerina services can interact in a real-world scenario and highlight the support of Ballerina to deploy services natively in the cloud. In our implementation, communication between the microservices is handled using gRPC and the frontend is exposed via an HTTP service.

Architecture

image info

And also following is the automatically generated Ballerina design view of the demo application. image info

Microservices description

Service name Description
Frontend Exposes an HTTP server to outside to serve data required for the React app. Acts as a frontend for all the backend microservices and abstracts the functionality.
CartService Stores the product items added to the cart and retrieves them. In memory store and Redis is supported as storage options.
ProductCatalogService Reads a list of products from a JSON file and provides the ability to search products and get them individually.
CurrencyService Reads the exchange rates from a JSON and converts one currency value to another.
PaymentService Validates the card details (using the Luhn algorithm) against the supported card providers and returns a transaction ID. (Mock)
ShippingService Gives the shipping cost estimates based on the shopping cart. Returns a tracking ID. (Mock)
EmailService Sends the user an order confirmation email with the cart details using the Gmail connector. (mock).
CheckoutService Retrieves the user cart, prepares the order, and orchestrates the payment, shipping, and email notification.
RecommendationService Recommends other products based on the items added to the user’s cart.
AdService Provides text advertisements based on the context of the given words.

The same load generator service will be used for load testing. The original Go frontend service serves HTML directly using the HTTP server using Go template. In this sample, the backend is separated from the Ballerina HTTP service and React frontend.

Service Implementation

First, we will be covering general implementation-related stuff thats common to all the services and then we will be diving into specific implementations of each microservices.

As shown in the diagram, Frontend Service is the only service that is being exposed to the internet. All the microservices except the Frontend Service uses gRPC for service-to-service communication. You can see the following example from the Ad Service.

import ballerina/grpc;

@grpc:Descriptor {value: DEMO_DESC}
service "AdService" on new grpc:Listener(9099) {

    remote function GetAds(stubs:AdRequest request) returns stubs:AdResponse|error {
    }
}

Ballerina provides the capability to generate docker, and Kubernetes artifacts to deploy the code on the cloud with minimal configuration. To enable this you need to add the cloud="k8s" under build options into the Ballerina.toml file of the project.

[package]
org = "wso2"
name = "recommendationservice"
version = "0.1.0"

[build-options]
observabilityIncluded = true
cloud = "k8s"

Additionally, you could make a Cloud.toml file in the project directory and configure various things in the container and the deployment. For every microservice in this sample, we would be modifying the container org, name, and tag of the created kubernetes yaml. Additionally, we add the cloud.deployment.internal_domain_name property to define a name for the generated service name. This allows us to easily specify host name values for services that depend on this service. This will be explained in depth in the next section. You can find a sample from the recommendation service below. Service-specific features of the Cloud.toml will be covered in their own sections.

[container.image]
name="recommendation-service"
repository="wso2inc"
tag="v0.1.0"

[cloud.deployment]
internal_domain_name="recommendation-service"

Ballerina Language provides an in-built functionality to configure values at runtime through configurable module-level variables. This feature will be used in almost all the microservices we write in this sample. When we deploy the services in different platforms(local, docker-compose, k8s) the hostname of the service changes. Consider the following sample from the recommendation service. The recommendation service depends on the catalog service, therefore it needs to resolve the hostname of the catalog service. The value "localhost" is assigned as the default value but it will be changed depending on the value passed on to it in runtime. You can find more details about this on the configurable learn page.

configurable string catalogHost = "localhost";

@grpc:Descriptor {value: DEMO_DESC}
service "RecommendationService" on new grpc:Listener(9090) {
    private final stubs:ProductCatalogServiceClient catalogClient;

    function init() returns error? {
        self.catalogClient = check new ("http://" + catalogHost + ":9091");
    }

    remote function ListRecommendations(stubs:ListRecommendationsRequest request)
          returns stubs:ListRecommendationsResponse|error {
        ...
    }
}

You can override the value using Config.toml. Note that this "catalog-service" is the same value as cloud.deployment.internal_domain_name in the Cloud.toml of the Catalog Service.

catalogHost="catalog-service"

Then you could mount this Config.toml into Kubernetes using config maps by having the following entry in the Cloud.toml

[[cloud.config.files]]
file="./k8s/Config.toml"

Kustomize

Kustomize is a tool where you can add, remove or update Kubernetes yamls without modifying the original yaml. This tool can be used to apply more modifications to the generated yaml from code to cloud. In the kustomization.yaml in the root directory, you can find a sample kustomize definition. This simply takes all the generated yamls from each project and combines them into one. Then we need to add an environment variable to specify where the Config.toml is located for the email service. This is done by using kustomize patches. You can see the sample code below.

kustomization.yaml

resources:
  - adservice/target/kubernetes/adservice/adservice.yaml
  - cartservice/target/kubernetes/cartservice/cartservice.yaml
  - checkoutservice/target/kubernetes/checkoutservice/checkoutservice.yaml
  - currencyservice/target/kubernetes/currencyservice/currencyservice.yaml
  - emailservice/target/kubernetes/emailservice/emailservice.yaml
  - frontend/target/kubernetes/frontend/frontend.yaml
  - paymentservice/target/kubernetes/paymentservice/paymentservice.yaml
  - productcatalogservice/target/kubernetes/productcatalogservice/productcatalogservice.yaml
  - recommendationservice/target/kubernetes/recommendationservice/recommendationservice.yaml
  - shippingservice/target/kubernetes/shippingservice/shippingservice.yaml

patchesStrategicMerge:
- secret-env-patch.yaml

secret-env-patch.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: emailservice-deployment
spec:
  template:
    spec:
      containers:
        - name: emailservice-deployment
          env:
            - name: BAL_CONFIG_FILES
              value: "/home/ballerina/conf/Config.toml:"

Running the Microservices on the Cloud

Setting up Email Credentials

  • A Gmail Account with access
    https://support.google.com/mail/answer/56256?hl=en

  • New project with Gmail API enabled on the API Console.

    • Visit Google API Console, click Create Project, and follow the wizard to create a new project.
  • OAuth Credentials

    • Go to OAuth Consent Screen, select User Type as Internal and click Create. Add an App name, User support email and Developer email address click Save.
    • On the Credentials tab, click Create Credentials and select OAuth Client ID.
    • Select the Web application application type, enter a name for the application, and specify a redirect URI (enter https://developers.google.com/oauthplayground if you want to use OAuth 2.0 Playground to receive the Authorization Code and obtain the Access Token and Refresh Token).
    • Click Create. Your Client ID and Client Secret will appear.
    • In a separate browser window or tab, visit OAuth 2.0 Playground. Click on the OAuth 2.0 Configuration icon in the top right corner and click on Use your own OAuth credentials and provide your OAuth Client ID and OAuth Client Secret.
    • Select the required Gmail API scopes from the list of APIs (auth.gmail.send).
    • Then click Authorize APIs.
    • When you receive your authorization code, click Exchange authorization code for tokens to obtain the refresh token and access token.
  • Create the GmailConfig.toml file in emailservice/ and paste the following code after replacing the values.

    [gmailConfig]
    refreshToken = "<your-refresh-token>"
    clientId = "<your-client-id>"
    clientSecret =  "<your-client-secret>"

Docker-Compose

Then execute the build-all-docker.sh to build the Ballerina packages and Docker images, and then execute docker-compose up to run the containers.

./build-all-docker.sh
docker-compose up

For running the react application you require the following prerequisites.

  • Node(>= v16.15.0)
  • NPM

Once all the prerequisites are installed. You can start the React application by executing following commands from the ui/ directory.

npm install
npm start

Kubernetes

You can use build-all-k8s.sh script to build the Kubernetes artifacts.

If you are using Minikube, you can execute the following command to configure your local environment to re-use the Docker daemon inside the Minikube instance.

build-all-k8s.sh minikube

If you are not using Minikube, you can execute the following command and push the docker images to your Docker registry manually.

build-all-k8s.sh

You can execute the following command to build the final YAML file. Kustomize is used for combining all the YAML files that have been generated into one.

kubectl kustomize ./ > final.yaml

You can deploy the artifacts into Kubernetes using the following command.

kubectl apply -f final.yaml

You can expose the frontend service via node port to access the backend services from the react app.

kubectl expose deployment frontend-deployment --type=NodePort --name=frontend-svc-local

Execute kubectl get svc and get the port of the frontend-svc-local service.

Execute kubectl port-forward svc/frontend-svc-local 27017:9098 to forward the frontend listening interface to localhost.

Change the value of the FRONTEND_SVC_URL variable in ui/src/lib/api.js to the frontend service (Example Value - http://localhost:27017')

Running the Microservices locally

  • Set up the email service with email credentials as explained above.
  • Build and publish the client_stubs and money modules to the local central as follows. Execute below commmands within each module.
bal pack
bal push --repository local
  • Inside each module directory, execute bal run to start the service.
  • Once all the services are up, start the React application by executing following commands from the ui/ directory.
npm install
npm start

Observe services tracing information with Jaeger

After running the docker container, you can view the tracing information on Jaeger via http://localhost:16686/. You can select the service in the drop-down box as follows. image info

Then click on Find Traces and you will be able to view spans of services in the gcp-microservice. image info

For more information about observability in Ballerina, please visit Observe Ballerina Programs.

Key Highlights of Ballerina based implementation

  • Native and extensive gRPC support - Ballerina supports generating server and client codes using the .proto file using the bal grpc command. Ballerina has services and clients as first-class constructs and gRPC builds upon that foundation.
  • Easy data processing with query expressions - Ballerina provides first-class support to write queries for data processing. Query expressions contain a set of clauses similar to SQL to process the data. You can have much more complicated queries using the limit and let keywords, ordering, joins, and so on. You can use query expressions not only for arrays but for streams, and tables as well.
  • Concurrency-safe execution - Ballerina is designed for network-based applications. The concept of isolation in Ballerina simplifies development by ensuring the safety of shared resources during concurrent execution. Ballerina Compiler warns if the application is not concurrent safe and helps to make it concurrent safe and performant at the same time.
  • Convenient integration capabilities - Ballerina makes it very easy to invoke other microservices, log, and handle errors. The configurable feature helps to configure the value of the variable by overriding it in the runtime.
  • Built-in XML support - Ballerina programming language contains built-in support for XML data. It supports defining, validating, and manipulating XML directly from the language syntax itself.
  • Native support for testing - Ballerina’s test framework allows you to test your microservices effortlessly. It also supports assertions, which help to verify the expected behavior of a piece of code. These assertions can be used to decide if the test is passing or failing based on the condition. Moreover, Ballerina has object mocking features that allows you to do this without even running a service.

Deviations from GCP Microservices Demo

  • Ports used by the services are different.
  • In-house logic (based on Luhn algorithm) used for credit card validation.
  • Google Cloud Spanner based datastore is not supported by the cart service.
  • Health check service is not available for gRPC services.
  • Tracing is enabled for all the ballerina gRPC services.
  • GCP Frontend service is represented by two separate services (UI and frontend).