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:
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.