Skip to content

Scaling Applications automatically with Operators

The application operator can perform automatic scaling of the front-end simple-microservice application. The simple-microservice application publishes metrics which are collected by Prometheus monitoring. Prometheus stores metrics from various sources and provides query capabilities. It is on the basis of this Prometheus data that auto-scaling decisions are made.

The simple-microservice application has been implemented with Quarkus. It uses Eclipse MicroProfile to track the number of invocations of its /hello endpoint (see code).

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.metrics.annotation.Counted;

@Path("/hello")
public class GreetingResource {
  @ConfigProperty(name = "greeting.message") 
  String message;

  @GET
  @Produces(MediaType.TEXT_PLAIN)
  @Counted(name = "countHelloEndpointInvoked", description = "How often /hello has been invoked")
  public String hello() {
    return String.format("Hello %s", message);        
  }
}

To allow Prometheus to scrape these metrics, a ServiceMonitor resource is used. This resource is created by the application operator (see code)

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    app: myapplication
  name: myapplication-metrics-monitor
  namespace: application-beta
spec:
  endpoints:
    - path: /q/metrics
  selector:
    matchLabels:
      app: myapplication

The Prometheus user interface can also be used to manually query this data.

Prometheus

To implement the scaling decision logic, a separate image/container is used. This container can be considered an extension to the application operator. The application operator sets up a CronJob for the operator-application-scaler so that it is run on a scheduled basis. The CronJob that is created by the controller looks like this. Note that the application name and namespace are passed in as parameter.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: application-scaler
  namespace: operator-application-system
spec:
  schedule: "0 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: application-scale
            image: docker.io/nheidloff/operator-application-scaler:v1.0.117
            imagePullPolicy: IfNotPresent
            env:
            - name: APPLICATION_RESOURCE_NAME
              value: "application"
            - name: APPLICATION_RESOURCE_NAMESPACE
              value: "application-beta"
          restartPolicy: OnFailure

The implementation of the actual operator-application-scaler application is trivial. It uses the Prometheus Go client library. Note that this library is still considered experimental. Alternatively consider the Prometheus HTTP API.

prometheusAddress := "http://prometheus-operated.monitoring:9090"
queryAmountHelloEndpointInvocations := "application_net_heidloff_GreetingResource_countHelloEndpointInvoked_total"
client, err := api.NewClient(api.Config{
  Address: prometheusAddress,
})
if err != nil {
  os.Exit(1)
}
v1api := v1.NewAPI(client)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, warnings, err := v1api.Query(ctx, queryAmountHelloEndpointInvocations, time.Now())
if err != nil {
  os.Exit(1)
}
resultVector, conversionSuccessful := (result).(model.Vector)
if conversionSuccessful == true {
  if resultVector.Len() > 0 {
    firstElement := resultVector[0]
    if firstElement.Value > 5 {
      // Note: '5' is only used for demo purposes
      scaleUp()
    } 
  }
}