Crossplane and Developer Self Service

In an era where cloud computing has become the backbone of modern IT infrastructure, the need for efficient and secure resource management is paramount. Crossplane, a dynamic and versatile tool, has emerged as a game-changer in this space. It offers a comprehensive solution for orchestrating cloud resources and handling associated credentials. By abstracting away the complexities of provisioning resources in the cloud, we can enable a self service platform which allows Developers to provision the things they need with the guardrails imposed by a platform team.

If you missed it:

Self Service Provisioning

In the last post of our Crossplane series, we offered a broad overview of Crossplane and underscored its importance. Yet, we omitted a crucial aspect: credential binding. Provisioning a resource extends beyond creating the resource; it also involves ensuring that the essential credentials are readily available for seamless access within the consuming application.

Self Service Resources

The diagram illustrates how a Developer can request a database using a resource, fulfilled by Crossplane, which then provides the resource connection details. The Developer is shielded from the intricacies of resource creation. With the platform team setting guardrails and defaults, standardization is achieved. If the platform team decides to switch from Amazon RDS to another provider or even self-hosted databases provisioned by a Kubernetes operator, this change is easily implementable. The only contract between the developer and the platform team is the input to the composite resource, ensuring flexibility and adaptability.

Kubernetes Service Binding Specification

If you have been in the cloud native space for a while you may be familiar with Cloud Foundry. Cloud Foundry introduced the conecpt of service bindings which in of itself was initially implemented at Heroku. In Cloud Foundry, a service binding is the process of connecting an application to a service instance. A service instance is a running instantiation of a service, such as a database or a message queue. When you bind a service to an application, it establishes a connection and provides the necessary credentials or connection information for the application to interact with that service.

The Kubernetes Service Binding Specification brings the same functionality to Kubernetes.

  1. Application Developer - expects secrets to be exposed consistently and predictably
  2. Service Provider - expects secrets to be collected consistently and predictably
  3. Application Operator - expects secrets to be transferred from services to workloads consistently and predictably

The spec assumes three roles.

Application Developer

The service binding specification operates on the assumption that developers will retrieve credentials by reading from a predetermined location within the container. Each bound resource is named within a subdirectory of $SERVICE_BINDING_ROOT. Within this subdirectory, a straightforward text file contains the value corresponding to the key, simplifying the retrieval process.

 1$SERVICE_BINDING_ROOT
 2├── account-database
 3│   ├── type
 4│   ├── provider
 5│   ├── uri
 6│   ├── username
 7│   └── password
 8└── transaction-event-stream
 9    ├── type
10    ├── connection-count
11    ├── uri
12    ├── certificates
13    └── private-key

The service binding spec presribes a set of well known keys that can be used for binding. As long as this well known key is populated with the expected value the application should be able to connect to the resource.

Key
Description
host A DNS-resolvable host name or IP address
uri A valid URI as defined by RFC3986
username A string-based username credential
password A string-based password credential
certificates A collection of PEM-encoded X.509 certificates, representing a certificate chain used in mTLS client authentication
private-key A PEM-encoded private key used in mTLS client authentication

A binding can typically be directly consumed with features available in any programming language. However, opting for a language-specific library often enhances the code by adding semantic meaning. While there's no universally "correct" method for interacting with a binding, here's a partial list of available libraries you might consider using:

  • .Net
  • Go
  • Java
    1. Quarkus
    2. Spring Boot
  • NodeJS
  • Python
  • Ruby
  • Rust

See Language Libraries for a current list of languages and links to libraries that implement the service binding spec.

 1public static void main(String[] args) {
 2    Binding[] bindings = Bindings.fromServiceBindingRoot();
 3    bindings = Bindings.filter(bindings, "postgresql");
 4    if (bindings.length != 1) {
 5        System.err.printf("Incorrect number of PostgreSQL drivers: %d\n", bindings.length);
 6        System.exit(1);
 7    }
 8
 9    String url = bindings[0].get("url");
10    if (url == null) {
11        System.err.println("No URL in binding");
12        System.exit(1);
13    }
14
15    Connection conn;
16    try {
17        conn = DriverManager.getConnection(url);
18    } catch (SQLException e) {
19        System.err.printf("Unable to connect to database: %s", e);
20        System.exit(1);
21    }
22
23    // ...
24}

The code snippet above demonstrates utilizing the Java library to automatically construct a database connection using the details from the secret.

Service Provider

Service providers expose bindings through a Secret resource with data required for connectivity. In order to support binding the service must expose a field called status.binding.name. This field contains the name of the secret that contains the connectionDetails.

1apiVersion: example.dev/v1beta1
2kind: Database
3metadata:
4  name: database-service
5...
6status:
7  binding:
8    name: production-db-secret

Implementing the provisioned service contract frees the ServiceBinding creator from needing to know about the name of the Secret holding the credentials. The service can update the secret name exposed over time.

Our Crossplane composition definition exposes the name of this secret for us. It is copied from the writeConnectionSecretToRef.name Due to some limitations within Crossplane this field must be temporarily stored within the labels of the CompositeResource. We will see how to simplify this using Composition Functions in a later post.

1# copy the secret to the Compisition
2- type: FromCompositeFieldPath
3  fromFieldPath: "spec.writeConnectionSecretToRef.name"
4  toFieldPath: "metadata.labels['binding']"
5# copy the secret to the status.binding.name field
6# this is needed for the the service binding spec
7- type: ToCompositeFieldPath
8  fromFieldPath: "metadata.labels['binding']"
9  toFieldPath: "status.binding.name"

Application Operator

Application operators bind application workloads with services by creating ServiceBinding resources. The specification’s Service Binding section describes this in detail. The ServiceBinding resource need to specify the service and workload details.

 1apiVersion: servicebinding.io/v1beta1
 2kind: ServiceBinding
 3metadata:
 4  name: account-service
 5spec:
 6  service:
 7    apiVersion: com.example/v1alpha1
 8    kind: AccountService
 9    name: prod-account-service
10  workload:
11    apiVersion: apps/v1
12    kind: Deployment
13    name: online-banking

You can also match the workload by label selectors.

 1apiVersion: servicebinding.io/v1beta1
 2kind: ServiceBinding
 3metadata:
 4  name: online-banking-frontend-to-account-service
 5spec:
 6  name: account-service
 7  service:
 8    apiVersion: com.example/v1alpha1
 9    kind: AccountService
10    name: prod-account-service
11  workload:
12    apiVersion: apps/v1
13    kind: Deployment
14    selector:
15      matchLabels:
16        app.kubernetes.io/part-of: online-banking
17        app.kubernetes.io/component: frontend

An Implementation

For learning sake I have commited a complete example of using Crossplane to provision the Postgres Helm Chart and binding that resource to the Spring Pet Clinic automatically with Spring Boots binding implementation. The custom resource is called XMyHelmishDataStore and exposes the service.binding.name field with the name of the connection.

In addition the service binding and deployment are shown here.

Conclusion.

In summary, Crossplane is a versatile and powerful tool for simplifying the provisioning and management of cloud resources and credentials. Passing credentials automatically to an application using a service binding specification allows for decoupling applications from their deployment configuration.

Dynamic Configuration: Services often need configuration information (e.g., connection strings, API keys) to interact with other services. The service binding specification facilitates the dynamic injection of these configurations into an application.

Abstraction of Service Dependencies: The specification can abstract away the specifics of service dependencies, making it easier to manage and update those dependencies without requiring changes to the application code.

Consistent Configuration: Enforcing a standard way to bind services can help maintain uniformity across different applications, making it easier to manage and troubleshoot.

Security: With a standardized configuration, securely managing credentials and sensitive information required for service-to-service communication becomes much simpler. Multiple applications can bind to the same service and credentials will automatically updated when they are rotated.

Ease of Deployment: With a standardized service binding approach, deploying and scaling applications that depend on various services becomes more straightforward, as the configuration can be managed consistently.

Posts in this Series

comments powered by Disqus