Code to Cloud deployment

Ballerina Code to Cloud is designed to allow developers to write code without thinking about the deployment platform.

This greatly simplifies the experience of developing and deploying Ballerina code in the cloud. It also enables using cloud-native technologies easily without in-depth knowledge.

As of now, Ballerina Code to Cloud supports generating the deployment artifacts of the platforms below.

  1. Docker
  2. Kubernetes

Set up the prerequisites

To complete this tutorial, you need:

  1. Docker installed and configured in your machine
  2. Kubectl installed and configured in a Kubernetes cluster

Note: macOS users with Apple Silicon chips need to set an environment variable named DOCKER_DEFAULT_PLATFORM to linux/amd64, before building the image. This is because the Ballerina Docker image is not supported on Apple Silicon chips yet.

export DOCKER_DEFAULT_PLATFORM=linux/amd64

How Code to Cloud works

Code to cloud builds the containers and required artifacts by deriving the required values from the code. This process happens when the package is being compiled. To override the default values given by the compiler, the Cloud.toml file needs to be created in the package directory.

Package layout

Ballerina encourages having one microservice per package. To adhere to that rule, Code to Cloud generates only one container per package. These artifacts can be found in the target directory of the package. A complete representation of the package layout is as follows.

├── Cloud.toml                               
├── Ballerina.lock
├── Ballerina.toml
├── Config.toml
├── entry.bal                          
└── target
    ├── bala
    │   └── module-0.0.1.bala
    ├── bin
    │   └── <module>.jar
    ├── docker
    │       └── Dockerfile                        
    └── kubernetes
            └── <module>-0.0.1.yaml 

Cloud.toml

The configuration file is used to override the default values generated by Code to Cloud.

Note: The Cloud.toml file is completely optional and you only have to specify the values you need to override. If the value is not specified for a certain field, the compiler will take the default value. All the supported properties can be found in the Code to Cloud specification.

Ballerina.toml

Contains metadata about the Ballerina package. This file is used to fetch defaults for deployment artifacts generation.

Note: The Ballerina.toml file can be used to specify the build option.

Copy
[build-options]
# cloud = "docker"
cloud = "k8s"

Config.toml

Contains the values, which need to be passed into the configurable variables. To learn more about this, see Configure a sample Ballerina service. When you are using the cloud-related features in Ballerina, the Config.toml file is not being packed with the container image as it can contain sensitive information. You need to explicitly provide this file in the runtime.

entry.bal

Represents any .bal file that has an entry point. The compiler will be using this file to retrieve service-related information for the deployment artifacts.

target/docker/

Contains the Docker artifacts generated by Code to Cloud. These artifacts will be required to build the Docker image.

target/kubernetes/

Contains the Kubernetes artifacts generated by Code to Cloud. These artifacts will be required to deploy the Ballerina application in Kubernetes.

Docker deployment

Create the Ballerina package

  1. Execute the bal new hello_docker command to create a new package named hello_docker.

  2. Replace the content of the ./hello_docker/main.bal file with the content below.

    Note: The source is completely focused on the business logic.

    main.bal

    Copy
    import ballerina/http;
    
    listener http:Listener helloEP = new (9090);
    
    configurable string name = ?;
    
    service /helloWorld on helloEP {
        resource function get sayHello() returns string {   
            return string `Hello, ${name}!`;
        }
    }
  3. Add cloud = "docker" under the [build-options] table into the Ballerina.toml file in the package.

    Ballerina.toml

    Copy
    [build-options]
    cloud = "docker"
  4. Create a file named Cloud.toml in the package directory and add the content below.

    Cloud.toml

    Copy
    [container.image]
    repository="wso2inc" # Docker hub name
    name="hello" # container name
    tag="v0.1.0"
    
    [settings]
    buildImage=true # you can make this false if you don't want to build the image and you just need the Dockerfile
  5. Create the Config.toml file in the package directory or anywhere else and paste the following content.

    Config.toml

    Copy
    name = "Docker"

Generate the artifacts

Execute the bal build command to build the Ballerina package and view the output below. If you haven't added the cloud option to Ballerina.toml file, you can execute bal build --cloud=docker to generate the artifacts and build the package by providing the build option inline.

Copy
$ bal build
Compiling source
        wso2/hello_docker:0.1.0

Generating executable

Generating artifacts...

        @kubernetes:Docker                       - complete 2/2 

        Execute the below command to run the generated docker image: 
        docker run -d -p 9090:9090 wso2inc/hello:v0.1.0

        target/bin/hello_docker.jar

Before invoking the container, let’s observe the Dockerfile below that has been generated. This is a Dockerfile created by the compiler extension from your code to run your HTTP service in the Docker environment easily.

target/docker/hello/Dockerfile

# Auto Generated Dockerfile
FROM ballerina/jvm-runtime:1.0

LABEL maintainer="dev@ballerina.io"

COPY ballerina-http-2.2.0.jar /home/ballerina/jars/ 
COPY ballerina-io-1.2.0.jar /home/ballerina/jars/ 
COPY wso2-hello_docker-0.1.0.jar /home/ballerina/jars/ 
...

RUN addgroup troupe \
    && adduser -S -s /bin/bash -g 'ballerina' -G troupe -D ballerina \
    && apk add --update --no-cache bash \
    && chown -R ballerina:troupe /usr/bin/java \
    && rm -rf /var/cache/apk/*

WORKDIR /home/ballerina
COPY wso2-hello_docker-0.1.0.jar /home/ballerina

EXPOSE  9090
USER ballerina

CMD java -Xdiag -cp "anjana-http2_test-0.1.0.jar:jars/*" 'anjana/http2_test/0/$_init'

Execute the Docker image

Follow the steps below to execute the Docker image separately.

  1. Execute the docker images command to verify if the Docker image is generated.

    Copy
    $ docker images
    REPOSITORY                    TAG                 IMAGE ID            CREATED              SIZE
    wso2inc/hello                 v0.1.0              60d95f0928b2        About a minute ago   228MB
    
  2. Execute the docker run -d -v <path/to/config>/Config.toml:/home/ballerina/Config.toml -p 9090:9090 wso2inc/hello:v0.1.0 command to run the generated Docker image.

Info: Make sure to set the correct path of the created Config.toml. Using volume mount is one way of providing the Config.toml file into the runtime. For other ways of providing configurable values, see the Ballerina specification.

Copy
$ docker run -d -v /home/wso2/c2c-guide/hello_docker/Config.toml:/home/ballerina/Config.toml -p 9090:9090 wso2inc/hello:v0.1.0
c04194eb0b4d0d78cbc8ca55e0527d381d8ab4a1a68f8ea5dd3770a0845d5fbb
  1. Execute the curl http://localhost:9090/helloWorld/sayHello command to access the service.
Copy
$ curl http://localhost:9090/helloWorld/sayHello
Hello, Docker!

Kubernetes deployment

Create the Ballerina package

Below sample describes a Ballerina application that reads a greeting string from a config map and greets the user upon HTTP request. By following the steps below, you can make a Kubernetes deployment that has container resource limits, horizontal pod autoscaling, config maps and liveness, readiness probes with the help of Code to Cloud.

  1. Execute the bal new hello_k8s command to create a new package named hello_k8s and go inside that directory.

  2. Replace the content of the main.bal file with the content below.

    main.bal

    Copy
    import ballerina/http;
    
    listener http:Listener helloEP = new (9090);
    
    configurable string greeting = ?;
    
    service /helloWorld on helloEP {
        resource function get sayHello() returns string|error? {
            return greeting + ", Kubernetes!";
        }
    }
  3. Create probes.bal with the following contents.

    probes.bal

    Copy
    import ballerina/http;
    
    listener http:Listener probeEP = new (9091);
    
    service /probes on probeEP {
        resource function get healthz() returns boolean {
            return true;
        }
        resource function get readyz() returns boolean {
            return true;
        }
    }
  4. Create the Config.toml file in the package directory and paste the following content into it.

    Config.toml

    Copy
    greeting = "Hello"
  5. Add cloud = "k8s" under the [build-options] table into the Ballerina.toml file in the package.

    Ballerina.toml

    Copy
    [build-options]
    cloud = "k8s"
  6. Create a file named Cloud.toml in the package directory and add the content below. You can use some properties from the docker sample here since cloud="k8s" option builds both Docker image and Kubernetes. You can see a brief description of the properties in the comments.

    Cloud.toml

    Copy
    [container.image]
    repository="wso2inc" # Ex - Docker hub repository name.
    name="hello-k8s" # Container name
    tag="v0.1.0"
    
    [cloud.deployment]
    min_memory="100Mi" # Minimum memory required for the container.
    max_memory="256Mi" # Maximum memory a single container can take.
    min_cpu="500m"  # Minimum CPU required for the container.
    max_cpu="500m" # Maximum CPU a single container can take.
    
    [cloud.deployment.autoscaling]
    min_replicas=2 # Minimum number of container instances ran at a given time
    max_replicas=5 # Maximum number of replicas of the container can scale up to
    cpu=60 # Average CPU utilization of pods
    
    [[cloud.config.files]] # Mounts the `Config.toml` file as a config map in Kubernetes.
    file="./Config.toml"
    
    [cloud.deployment.probes.liveness]
    port=9091
    path="/probes/healthz"
    
    [cloud.deployment.probes.readiness]
    port=9091
    path="/probes/readyz"

Generate the artifacts

Once you build the Ballerina package, the compiler extension will generate the artifacts required for k8s deployment.

Tip: If you are using Minikube, execute the eval $(minikube docker-env) command before building the image if you don't want to push the container to the Docker registry.

Tip: If you don't have the cloud="k8s" entry on the Ballerina.toml you can execute bal build --cloud=k8s command to provide the build option inline.

Copy
$ bal build
Compiling source
        wso2/hello_k8s:0.1.0

Generating executable

Generating artifacts...

        @kubernetes:Service                      - complete 1/2
        @kubernetes:Service                      - complete 2/2
        @kubernetes:ConfigMap                    - complete 1/1
        @kubernetes:Deployment                   - complete 1/1
        @kubernetes:HPA                          - complete 1/1
        @kubernetes:Docker                       - complete 2/2 

        Execute the below command to deploy the Kubernetes artifacts: 
        kubectl apply -f /home/wso2/c2c-guide/hello_k8s/target/kubernetes/hello_k8s

        Execute the below command to access service via NodePort: 
        kubectl expose deployment hello-k8s-deployment --type=NodePort --name=hello-k8s-svc-local

        target/bin/hello_k8s.jar

Note: Before invoking the Kubernetes service, observe the Kubernetes yamls that have been generated. You should be able to find services for ports that have been exposed, HorizontalPodAutoscaler for scaling and Deployment, and Config maps.

target/kubernetes/hello_k8s

Copy
---
---
apiVersion: "v1"
kind: "Service"
metadata:
  labels:
    app: "hello_k8s"
  name: "hello-k8s-svc"
spec:
  ports:
  - name: "port-1-hello-k8"
    port: 9090
    protocol: "TCP"
    targetPort: 9090
  - name: "port-2-hello-k8"
    port: 9091
    protocol: "TCP"
    targetPort: 9091
  selector:
    app: "hello_k8s"
  type: "ClusterIP"
---
apiVersion: "v1"
kind: "ConfigMap"
metadata:
  name: "config-config-map"
data:
  Config.toml: "greeting = \"Hello\"\n"
---
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
  labels:
    app: "hello_k8s"
  name: "hello-k8s-deployment"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: "hello_k8s"
  template:
    metadata:
      labels:
        app: "hello_k8s"
    spec:
      containers:
      - env:
        - name: "BAL_CONFIG_FILES"
          value: "/home/ballerina/conf/Config.toml:"
        image: "wso2inc/hello-k8s:v0.1.0"
        lifecycle:
          preStop:
            exec:
              command:
              - "sleep"
              - "15"
        livenessProbe:
          httpGet:
            path: "/probes/healthz"
            port: 9091
          initialDelaySeconds: 30
        name: "hello-k8s-deployment"
        ports:
        - containerPort: 9090
          name: "port-1-hello-k8"
          protocol: "TCP"
        - containerPort: 9091
          name: "port-2-hello-k8"
          protocol: "TCP"
        readinessProbe:
          httpGet:
            path: "/probes/readyz"
            port: 9091
          initialDelaySeconds: 30
        resources:
          limits:
            memory: "256Mi"
            cpu: "500m"
          requests:
            memory: "100Mi"
            cpu: "500m"
        volumeMounts:
        - mountPath: "/home/ballerina/conf/"
          name: "config-config-map-volume"
          readOnly: false
      nodeSelector: {}
      volumes:
      - configMap:
          name: "config-config-map"
        name: "config-config-map-volume"
---
apiVersion: "autoscaling/v1"
kind: "HorizontalPodAutoscaler"
metadata:
  labels:
    app: "hello_k8s"
  name: "hello-k8s-hpa"
spec:
  maxReplicas: 5
  minReplicas: 2
  scaleTargetRef:
    apiVersion: "apps/v1"
    kind: "Deployment"
    name: "hello-k8s-deployment"
  targetCPUUtilizationPercentage: 60

Execute the Kubernetes service

Follow the steps below to execute the Kubernetes service.

  1. Execute docker push <repository>/<name>:<tag> to push the container to docker hub. You can skip this step if you executed eval $(minikube docker-env) before building the container.

    Copy
    $ docker push wso2inc/hello-k8s:v0.1.0
    
  2. Execute the kubectl apply -f target/kubernetes/hello_k8s command to execute the service.

    Copy
    $ kubectl apply -f target/kubernetes/hello_k8s
    service/hello-k8s-svc created
    configmap/hello-k8s-config-json created
    deployment.apps/hello-k8s-deployment created
    horizontalpodautoscaler.autoscaling/hello-k8s-hpa created
    
  3. Execute the kubectl get pods command to verify the Kubernetes pods.

    Copy
    $ kubectl get pods
    NAME                                        READY   STATUS    RESTARTS   AGE
    hello-k8s-deployment-577d8dbf8-p8zg5   1/1     Running   0          37s
    hello-k8s-deployment-577d8dbf8-p8zg5   1/1     Running   0          57s
    
  4. Execute the kubectl expose deployment hello-k8s-deployment --type=NodePort --name=hello-k8s-svc-local command to expose the service via NodePort to test in the development environment.

    Copy
    $ kubectl expose deployment hello-k8s-deployment --type=NodePort --name=hello-k8s-svc-local
    service/hello-k8s-svc-local exposed
    
  5. Execute the kubectl get svc command to get the EXTERNAL-IP and port of the Kubernetes service.

    Copy
    $ kubectl get svc
    NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
    hello-k8s-svc         ClusterIP   10.96.173.207   <none>        9090/TCP,9091/TCP               5m11s
    hello-k8s-svc-local   NodePort    10.99.245.41    <none>        9090:30342/TCP,9091:30515/TCP   66s
    kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP                         130m
    

    Tip: If you are using Minikube, execute the minikube ip command to get the IP address.

    Copy
    $ minikube ip
    192.168.49.2
    
  6. Execute the curl http://192.168.49.2:30342/helloWorld/sayHello command to access the deployed service via cURL.

    Copy
    $ curl http://192.168.49.2:30342/helloWorld/sayHello
    Hello, Kubernetes!
    

Note: You can visit the Code to Cloud specification for detailed information about all the supported features. As mentioned in the beginning, Code to Cloud is a tool created to make the development process easier. It does not cover the operational properties in Kubernetes. If you want to have these additional features in your deployment, or you need to change the generated Kubernetes artifacts, you can use Kustomize for modifying the generated YAML.