I use cookies in order to optimize my website and continually improve it. By continuing to use this site, you are agreeing to the use of cookies.
You can find an Opt-Out option and more details on the Privacy Page!

Use the Open Service Broker API on Kubernetes

The Open Service Broker API is designed to define a standard how applications can get access to services in the cloud. The idea is, if you need a service you ask a standardised api for this service (like a Database or a Queue) and will get the needed information to connect to this service. This makes it easier to access services in the cloud via one standardised interface.

The Service Broker API was designed by Cloud Foundry Foundation and in 2016 in collaboration with other companies announced as an Open Standard.

Architecture

The architecture is very easy, there is a Service Broker, that connects to Systems that expose a Service Catalog. This Service Catalog describes which services a provider provides, what requirements exist, what plans exist and how much they are.

Now the Service Broker can ask via a ReST call to provision a resource or a service like a Database or a Queue. This can be done asynchronously or synchron, if it happens asynchronously then the Service Broker polls as long as it takes to provide the resource.

After this the Broker asks for a binding to this resource. Bindings are for example credentials to connect to these resource like a Database with an username, a password and a host url.

Now the System can use this resource as long as it needs this resource. After this it can ask the provider to remove the binding and then ask to remove the instance.

In the following diagram you can see the described process:

Infrastructur

Installation on Kubernetes

The Kubernetes Project also is interrested in the Open Service Broker API so there exists an Kubernetes Incubator project to bring the Service Broker API to Kubernetes as a native workflow. There exists a GitHub Project that is in development mode and provides a helm chart to get started using the Service Broker: Service-Catalog.

It is very easy to get started, first you need to download the Github repository and then run the helm chart. For me I changed the apiserver.service.nodePort.securePort from 30443 to 30888 because the port is in use by an Ingress controller additionally I changed the boolean of apiserver.auth.enabled to false.

git clone https://github.com/kubernetes-incubator/service-catalog.git
cd service-catalog/charts/catalog
helm install . --name catalog --namespace catalog --values values.yaml

After this there are two pods deployed in the catalog namespace:

NAME                                                  READY     STATUS    RESTARTS   AGE
catalog-catalog-apiserver-5d77486b6-52l9j             2/2       Running   0          5h
catalog-catalog-controller-manager-554c758786-v4fx6   1/1       Running   2          5h

Develop your own Catalog

For me I developed my own Service Catalog. The idea is to provide a service that can generate SSL Certificates for Domains requested and provide them when there is a binding request. The easiest way to do this is to use the Spring Cloud CloudFoundry Service Broker. It already implements the Api so you only have to implement the logic of generating a resource and binding them. In my case I generate from a root certificate a certificate that is valid for a specified time.

First the service Catalog must be defined:

@Configuration
public class CatalogConfig {

    @Bean
    public Catalog catalog() {
        return new Catalog(getCatalog());
    }

    private List<ServiceDefinition> getCatalog() {

        return Collections.singletonList(new ServiceDefinition(
                "certificate-generator-service-broker",
                "tlscertificate",
                "Service to generate certificates from root certificates e.g. for Ingress",
                true,
                false,
                getPlans(),
                Arrays.asList("Certificate", "Ingress TLS"),
                getServiceDefinitionMetadata(),
                null,
                null
        ));

    }

    private List<Plan> getPlans() {
        return Collections.singletonList(new Plan("tls-certificate-one-year",
                "default",
                "generates a certificate for one year"
        ));
    }

    private Map<String, Object> getServiceDefinitionMetadata() {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("displayName", "TLS Certificate");
        metadata.put("longDescription", "Service to generate certificates from root certificates e.g. for Ingress");
        metadata.put("providerDisplayName", "de.koudingspawn");

        return metadata;
    }

}

The Catalog describes the provided services, in this case it is only one service a tlscertificate-generator. The first value the certificate-geerator-service-broker is a unique value that describes the item in the catalog. The second value after the name tlscertificate is a boolean that describes that this Service in the Catalog is bindable and the second boolean describes that the plans in the catalog has no update functionality.

The plans part in the Catalog can describe which plans are available and how much they are. Plans must also contain a unique id and a unique name to identify them. The last interresting part is the service definition metadata that describes general information about the service.

As described in the diagram a Service Broker requests this catalog via a ReST call. Here you can see a sample output of the described catalog (/v2/catalog):

{
  "services": [
    {
      "id": "certificate-generator-service-broker",
      "name": "tlscertificate",
      "description": "Service to generate certificates from root certificates e.g. for Ingress",
      "bindable": true,
      "plan_updateable": false,
      "plans": [
        {
          "id": "tls-certificate-one-year",
          "name": "default",
          "description": "generates a certificate for one year",
          "metadata": {},
          "free": false
        }
      ],
      "tags": [
        "Certificate",
        "Ingress TLS"
      ],
      "metadata": {
        "longDescription": "Service to generate certificates from root certificates e.g. for Ingress",
        "providerDisplayName": "de.koudingspawn",
        "displayName": "TLS Certificate"
      },
      "requires": [],
      "dashboard_client": null
    }
  ]
}

Now we have to implement the ServiceInstanceService that will generate the key pair and delete it if the Broker requests it.

@Component
public class CertificateInstanceService implements ServiceInstanceService {

    private final ServiceInstanceRepository serviceInstanceRepository;
    private final X509CertificateGenerator x509CertificateGenerator;

    public CertificateInstanceService(ServiceInstanceRepository serviceInstanceRepository, X509CertificateGenerator x509CertificateGenerator) {
        this.serviceInstanceRepository = serviceInstanceRepository;
        this.x509CertificateGenerator = x509CertificateGenerator;
    }

    @Override
    public CreateServiceInstanceResponse createServiceInstance(CreateServiceInstanceRequest request) {

        Optional<ServiceInstance> optInstance = this.serviceInstanceRepository.findByServiceInstanceId(request.getServiceInstanceId());
        if (optInstance.isPresent()) {
            throw new ServiceInstanceExistsException(request.getServiceInstanceId(), request.getServiceDefinitionId());
        }

        ServiceInstance serviceInstance = new ServiceInstance(request);
        Optional<GeneratedKeyPair> optKeyPair = generateKeyPair(serviceInstance.getCn(), serviceInstance.getValidity());
        if (!optKeyPair.isPresent()) {
            throw new ServiceBrokerException("Could not generate key pair");
        }
        serviceInstance.setGeneratedKeyPair(optKeyPair.get());
        serviceInstanceRepository.save(serviceInstance);

        return new CreateServiceInstanceResponse();
    }

    @Override
    public GetLastServiceOperationResponse getLastOperation(GetLastServiceOperationRequest request) {
        return new GetLastServiceOperationResponse().withOperationState(OperationState.SUCCEEDED);
    }

    @Override
    public DeleteServiceInstanceResponse deleteServiceInstance(DeleteServiceInstanceRequest request) {
        Optional<ServiceInstance> optInstance = serviceInstanceRepository.findByServiceInstanceId(request.getServiceInstanceId());

        if (!optInstance.isPresent()) {
            throw new ServiceInstanceDoesNotExistException(request.getServiceInstanceId());
        }

        serviceInstanceRepository.delete(request.getServiceInstanceId());

        return new DeleteServiceInstanceResponse();
    }

    @Override
    public UpdateServiceInstanceResponse updateServiceInstance(UpdateServiceInstanceRequest request) {
        return null;
    }

    private Optional<GeneratedKeyPair> generateKeyPair(String cn, int validity) {
        try {
            GeneratedKeyPair certificate = x509CertificateGenerator.createCertificate(cn, validity);
            return Optional.of(certificate);
        } catch (Exception e) {
            return Optional.empty();
        }
    }
}

Here I use a MySQL Database to store the service instance requests in a Database together with the generated key pairs. When a new KeyPair is requested the Service Broker sends a message with two parameters, the certificate name (cn) and a validity of the certificate that describes how long the certificate should be valid.

If a Service Broker asks to delete a Service instance we simply remove it from the MySQL Database.

To return the certificate to the Service Broker we implement the ServiceInstanceBindingService that is normaly used to return credentials to the Service Broker to connect to resources.

@Component
public class CertificateServiceInstanceBindingService implements ServiceInstanceBindingService {

    private final ServiceInstanceRepository serviceInstanceRepository;

    public CertificateServiceInstanceBindingService(ServiceInstanceRepository serviceInstanceRepository) {
        this.serviceInstanceRepository = serviceInstanceRepository;
    }

    @Override
    public CreateServiceInstanceBindingResponse createServiceInstanceBinding(CreateServiceInstanceBindingRequest request) {
        Optional<ServiceInstance> optServiceInstance = serviceInstanceRepository.findByServiceInstanceId(request.getServiceInstanceId());

        if (!optServiceInstance.isPresent()) {
            throw new ServiceBrokerException("Not found service instance request");
        }

        ServiceInstance serviceInstance = optServiceInstance.get();

        Map<String, Object> certificateResponse = new HashMap<>();
        certificateResponse.put("tls.crt", serviceInstance.getChain());
        certificateResponse.put("tls.key", serviceInstance.getPrivatekey());

        return new CreateServiceInstanceAppBindingResponse().withCredentials(certificateResponse);
    }

    @Override
    public void deleteServiceInstanceBinding(DeleteServiceInstanceBindingRequest deleteServiceInstanceBindingRequest) {

    }
}

Here we return the certificates as a map with the tls.crt and tls.key parameters. This allows us to use the generated certificates later as tls certificates for an ingress instance like nginx or traefik.

Here I won’t show the logic on how to generate a certificate based on a certificate chain. For more details please look in the source code.

With this information we are now able to use the Service Catalog:

How to use the Service Catalog in Kubernetes

First we need to connect our Kubernetes api server to the developed catalog. Therefore we need to define a ClusterServiceBroker:

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ClusterServiceBroker
metadata:
  name: certificate-broker
spec:
  url: http://url:8080

This allows the Kubernetes api server to connect to our catalog and ask for provided service classes. If this happens successfully we first should see that the connection was successful:

$ kubectl get clusterservicebrokers certificate-broker -o yaml
apiVersion: servicecatalog.k8s.io/v1beta1
kind: ClusterServiceBroker
metadata:
  annotations:
  creationTimestamp: 2017-11-17T16:28:26Z
  finalizers:
  - Kubernetes-incubator/service-catalog
  generation: 1
  name: certificate-broker
  resourceVersion: "13"
  selfLink: /apis/servicecatalog.k8s.io/v1beta1/clusterservicebrokers/certificate-broker
  uid: 565060af-cbb4-11e7-b9a7-0a580af40235
spec:
  relistBehavior: Duration
  relistDuration: 15m0s
  relistRequests: 0
  url: http://url:8080
status:
  conditions:
  - lastTransitionTime: 2017-11-17T16:28:26Z
    message: Successfully fetched catalog entries from broker.
    reason: FetchedCatalog
    status: "True"
    type: Ready
  lastCatalogRetrievalTime: 2017-11-17T16:28:26Z
  reconciledGeneration: 1

And after this we should see a list of available services provided by the catalog:

$ kubectl get clusterserviceclasses -o=custom-columns=NAME:.metadata.name,EXTERNAL\ NAME:.spec.externalName
NAME                                   EXTERNAL NAME
certificate-generator-service-broker   tlscertificate

Then we can ask for a new instance, in this case for a new tls certificate:

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceInstance
metadata:
  name: tls-certificate
  namespace: microservices
spec:
  clusterServiceClassExternalName: tlscertificate
  clusterServicePlanExternalName: default
  parameters:
    validity: 1
    cn: "example.com"

As you can see a tlscertificate request will be made with the default plan. This are the names of the service and the plan for generating a certificate. It asks for a certificate that is valid for 1 (in this case year) and is signed for a domain called example.com.

After this we ask for a binding:

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceBinding
metadata:
  name: tls-certificate
  namespace: microservices
spec:
  instanceRef:
    name: tls-certificate

Here we ask to bind the instance tls-certificate (spec.instanceRef.name) to the namespace microservices. After this is applyed with kubectl you should see a new certificate in the secret resource:

kubectl get secret -n microservices
NAME                  TYPE                                  DATA      AGE
default-token-gd5ls   kubernetes.io/service-account-token   3         5h
tls-certificate       Opaque                                2         1h

This tls-certificate secret now contains the tls.key and tls.crt Base64 encoded:

apiVersion: v1
data:
  tls.crt: <chained certificate>
  tls.key: <key>
kind: Secret
metadata:
  creationTimestamp: 2017-11-17T20:53:36Z
  name: tls-certificate
  namespace: microservices
  ownerReferences:
  - apiVersion: servicecatalog.k8s.io/v1beta1
    blockOwnerDeletion: true
    controller: true
    kind: ServiceBinding
    name: tls-certificate
    uid: 6163447b-cbd9-11e7-b9a7-0a580af40235
  resourceVersion: "1561474"
  selfLink: /api/v1/namespaces/microservices/secrets/tls-certificate
  uid: 618bcf90-cbd9-11e7-afd4-000c29c7a8ef
type: Opaque

So for repitition, first we asked for a ServiceInstance. In this case the developed Server generated a certificate with the certificate name example.com. Then we ask for the certificate via a ServiceBinding. This means that the server transfers the certificate and the private key and stores them in Kubernetes as a secret.

Now we can use this tls-certificate for example with an Ingress Controller.

Björn Wenzel

Björn Wenzel

My name is Björn Wenzel. I’m a DevOps with interests in Kubernetes, CI/CD, Spring and NodeJS.