0
0

Microservices Auto-Configuration with Connectors & Bindings

Cloud-Native Dependency-Injection in Kubernetes Cloud Manager Article
Docs EInnovator Posted 24 Mar 21

Microservices Auto-Configuration with Connectors & Bindings: Cloud-Native Dependency-Injection in Kubernetes Cloud Manager

Dependency-Injection is about simplifying application configuration by automatically resolving the dependencies of software components. This idea can be extended to distributed applications by considering the components and dependencies as the different micro-services or backing services part of the architecture. A cloud platform should provide mechanisms to allow information about other services to be readily available to apps and microservices (e.g. connection URLs and credentials). Ideally, this should be done while keeping the apps agnostic about the cloud platform and environment.

The twin mechanisms of connectors and bindings — as available in Cloud Manager PaaS — makes the configuration of applications deployed in Kubernetes clusters particularly straightforward and convenient. By being aware of the stack each micro-service is implemented and knowing which services it connect to, the PaaS platform can automatically configure a known set of environment variables that the app it uses expects. Apps don’t have to be modified to comply with platform-specific details nor have to detect when it moves from the local development to a cloud environment. This simplifies the work of developers, increases app portability, and avoids possible issues resulting from erroneous manual configuration.

For example, if an app or micro-service is implemented in a Java/SpringBoot stack (VMWare sponsored), and a couple of bindings are used to define how to connect to a database and a message-broker (e.g. MySQL or MongoDB, and RabbitMQ/AMQP or Kafka), the environment variables that need to be set for proper configuration are well-defined by Spring Boot for each of these service. As such, Cloud Manager can automatically set these environment variables without requiring the configuration to be explicitly done by the developer. Because Spring Boot supports auto-configuration — the automatic creation of components based on settings, and dependency-injection — reflection-based configuration of components, setting the right environment variables allows the app to be fully configured without requiring it to be modified to pickup platform specific variables.

The same is true for other Java microservices frameworks that Cloud Manager supports, including: Quarkus (RedHat/IBM backed), Helidon (Oracle backed), and Micronaut. Apps implemented in other languages and frameworks, with or without support for auto-configuration, also benefit from this approach.

In this article, I give an overview of how connectors and bindings can be used to easily configure a distributed application with Kubernetes Cloud Manager. I mostly use the CLI here, but the UI can also be used to perform equivalent actions. The examples used focus on Java apps implemented with two selected frameworks — Spring Boot and to lesser extent Quarkus — to connect to databases, message-brokers, other microservices, and external services. But the mechanisms are general, and can be used as well to connect applications implemented with other languages and frameworks, and to connect to other types of services.

Microservice Auto-Configuration with Connectors and Bindings

Microservices Configuration

Configuration Sources

Applications, traditional monoliths or microservice-based, can be configured from several sources, including: configuration files (e.g. YAML, XML, TOML, or property files), environment variables, command-line arguments, or an external service. While applications tend to use some combination of these sources, in the cloud and with containers environment variables are the preferred approach. It is a simple mechanism, and allows applications to be reconfigured without the need to run a test-build-package pipeline and create a new container image whenever a configuration change is desired (as per rule #3 of the 12 Factor Apps rulebook).

The use of an external configuration service is also a considered approach when deploying microservices architectures. It allows for different microservices to share common settings fetched from single-source of true. While it is possible to rely on a custom configuration service, more frequently a “out-of-the-box” reusable middleware solution is used (e.g. Spring Cloud Config Server). For settings related to other services identity and location, a more specialized kind of configuration service can be used — know as a service discovery service (e.g. Netflix Eureka, Consul, or old style RPC registries like Java RMI). The drawback of the external service approach is that it adds complexity to the architecture — at least one more component needs to be deployed and managed, and new failure modes are introduced.

Some modern microservice development frameworks provide a uniform API and approach to configure apps in face of different environment sources (e.g. Spring, Quarkus, and other Java frameworks based on micro-profiles). Thus, selecting one source of configuration over another or some combination vs. another, is mostly motivated by architectural and operational considerations, and may have limited effect on the application implementation details.

Configuration Modes

Application configuration is usually done in a static way — i.e. settings are obtained at deployment or restart time, and application components are configured based on these initial settings. For some advanced use cases, the external service approach can be extended to support dynamic configuration. In this case, the app is instructed to fetch updated settings from the external service and reconfigure its components when asked by the administrator (e.g. components with refresh scope in Spring Cloud reconfigure when triggered by the /refresh endpoint). Dynamic configuration configuration can also be accomplished without recurse to an external configuration service (e.g. JMX in traditional Java apps, or the logging actuator in Spring Boot).

When following a continuous-delivery approach applications are updated and (re)deployed frequently (e.g. 1+ times per day). So, for many use cases, the same benefits of true dynamic configuration can be approximated with static configuration and frequent redeployments. Kubernetes Cloud Manager supports the definition of environment variables at the level of a Space. All the apps deployed in a space (or a selected subset) are automatically configured from these common settings. Settings can also be defined at the level of a Cluster. Deployment level settings override (take precedence) over space level settings, which in turn override cluster level settings. This approach provides a simpler alternative to using an external configuration service, with in rigor is still static but does not introduce new failure modes.

Configuration with Connectors & Bindings

To further simplify app configuration, specially connecting to backing services and other microservices, Cloud Manager provides support the mechanism of Connectors and Bindings. A deployment/service can expose one or more connectors, each defining a set of key-value pairs used to export connection information to applications. They are defined with a JSON specification, and typically include information, such as: credentials (username/password, key/secret), service location (host/IP), access URL, and other connection settings. For example, a database service might export several connectors one for each database (schema) that it manages.

A Binding is a collection of settings for environment variables whose values are resolved from the Connector that it binds to. These settings constitute the specification (spec) of the binding. It is preferred to make the settings in the binding spec be depend on stack used to implement the application. This allows the app to be configured in a completely portable way. That is, rather than requiring the app to be aware and having to pick-up platform specific settings (e.g. as is done with VCAP_* variables in Cloud Foundry, GAE_* variables in GCP App Engine, or KUBERNETES_* variables in Kubernetes), or adding a library to abstract the cloud environment (e.g. as is done with Spring Cloud Connectors), by making the binding spec adapts to the needs of the app.

Additionally, by making the cloud platform aware of the stack the app and assuming the type of the service it connects to is known, the spec of the binding can be automatically generated. For example, a Spring Boot application connecting to a relational database implies the settings SPRING_DATASOURCE_*, and connecting to a RabbitMQ/AMQP broken implies the settings SPRING_RABBIT_*. Likewise, a Quarkus application connecting to a relational database implies the settings QUARKUS_DATASOURCE_*, and so forth. All these settings can be generated automatically, which greatly simplifies the work of both the app developer and the devops/administrator configuring the app.

A binding is matched with a connector by specifying a selector of the form service-name/connector-name. In Cloud Manager UI, the the service/connector of a binding can also selected via dropdown/combobox widgets.

When a connector is created, beyond the mandatory JSON spec and a name, it can be assigned an optional type and tags. When a binding is created, and the auto option provided, its specification is generated automatically. The type and/or tags of the matching connector, together with the stack of the service, is used determine which environment variable should be set by the binding.

Setup

Setup Cloud Manager

To follow the examples in this article you need to have access to Kubernetes cluster, and have Cloud Manager backend/UI and the CLI tool installed. I cover how this is done in details in other articles, so I direct you to one of these. You can also find the links to the documentation in the references at the bottom.

For quick reference, I show below how to run Cloud Manager in single-user mode with Docker.

docker run -p5005:2500 einnovator/einnovator-devops cm -d

To download and install Cloud Manager CLI tool ei, use this steps:

wget https://cdn.einnovator.org/cli/ei-latest.tgz
tar -xf ei-latest.tgz
cd einnovator-cli
chmod +x ei #linux/mac only
./ei

After the CLI is installed, you need to login into the backend with command login, as shown below. Modify the API endpoint as appropriate.

ei login -u username -p password http://localhost:5050

You also need to have at least one cluster configured in Cloud Manager. Use the command cluster import to import a cluster from a kubeconfig.yaml file. Use command ls -c to confirm the cluster was imported and get its name.

ei cluster import -f kubeconfig.yaml 
ei ls -c

Finally, you should create a space where the apps and services can be deployed. This is done with command space create, as shown below (the cluster name is assumed to be mycluster, and the space name dev). Command cd cluster/space is used to set the current (default) space and cluster.

ei create space mycluster/dev
ei cd mycluster/dev

Sample App

To illustrate the use of connector and bindings, I use a sample app named Superheros implemented in Spring Boot, whose source code is available in a public GitHub repository with Apache license, and pre-build in a DockerHub image repository. This is a simple application that manages a database of superheros with support for CRUD operations.

To deploy the app with Cloud Manager CLI tool use command run, as shown below.

do ei run superheros einnovator/einnovator-sample-superheros --port=80 --stack=boot

The option --port is used to specify the HTTP/TCP port the application container listen for connections. The option --stack=boot hints Cloud Manager about the stack the application is implemented — Java/Spring Boot, in this case. This stack setting makes Cloud Manager to set automatically the environment variable SERVER_PORT. In the following sections, I show how the stack info is also used to infer what other environment variables should be configured automatically as bindings are added to the app.

Connecting to a Database

Most applications connect to an external database for long term persistence. Without any further configuration, the Superheros app uses an embedded HSQL database. I will change this now by making the app connect to a MySQL database, using connectors and bindings to perform the configuration.

Installing a Database

As first step, I deploy a MySQL database as a separate service deployed in the same Kubernetes cluster/space. The installation of services in Kubernetes can be done with several tools, but to stay consistent I will use Cloud Manager CLI. Below, I use command install to deploy a mysql solution from a catalog named einnovator. Command ps is used to confirm that the service is running and was installed successfully — running as StatefulSet, in this case.

ei install einnovator/mysql
ei ps

Command market can be used to list and search available marketplace solutions across all catalogs. You can use this command to confirm that you have MySQL available in some catalog. A catalog named einnovator is setup by default when Cloud Manager is installed. If it is not available in your installation (e.g. was previously removed), you can use the command catalog create to add a catalog. To list all catalogs use command catalog ls.

ei catalog create einnovator --type=native --url=https://cnd.einnovator.org/charts

A database also needs to be created for the sample app. For MySQL, this can be done during setup (e.g by using environment variables, or an init script). Alternatively, we can use the tool mysql inside the pod of the DB to execute the command create database. When using ei, this can be done with command deploy exec — which by default runs the command in the first pod of a deployment.

For the create database command to work, we need to lookup and provide the database password for authentication. For the installed solution, the password is found in secret named mysql-credentials and data field password. Below, I use the command secret get with options -d and --decode to extract and decode the password and assign it to a variable.

export PWD=`ei secret get mysql-credentials -d=password --decode`
ei deploy exec mysql -- mysql -uroot -p$PWD -e "create database superheros"

Defining Database Connectors

The command connector add is used to create connectors for a service. Below, I create a new connector for service mysql with name superheros/root. The option --spec provides a JSON specification for the connector. Because entering the spec with JSON from the command-line tends to be a bit cumbersome (as double-quotes need to be escaped), it is often preferred to use a comma-separated list of settings.

ei connector add mysql superheros/root --type=mysql \
  --spec=password:^^mysql-credentials.password,username:root,uri:mysql://\${host}/superheros

The connector spec defines several variables, including: password looked up from secret mysql-credentials (syntax prefix ^^ resolves a secret data item, and prefix ^ resolves a configmap data item), username defined as root, and uri referring to variable ${host} (set automatically to the IP of the service endpoint) and database superheros. The option --type is an optional hint to allow auto-generation of binding specifications (see below).

Binding to a Database

A binding is a collection of settings for environment variables whose value are resolved from a connector. The appropriate environment variables to set depend on the stack used to implement the application. Below, I use command binding add to create a new binding for the app superheros. The selector mysql/superheros/root specifies that we are binding to the connector named superheros/root of service mysql.

ei binding add superheros mysql/superheros/root --auto
ei deploy restart supeheros

The option --auto ask for the binding specification to be generated automatically based on the stack of the app and the service type. Because we specified --stack=BOOT for the app early on, and the connector type is mysql, the environment variables to set are SPRING_DATASOURCE_*. The genereate binding spec is show below:

{"spring":{"datasource":{"url":"jdbc:${uri}","username":"${username}","password":"${password}"}}}

Once the app is restarted it will be running and fetching/storing data in the MySQL DB, rather than the embedded DB. You can confirm this my looking at the logs of the app (the table schema is auto-generated again), using the UI or with command ei deploy log superheros -l300.

Connecting to a Message-Broker

Message-brokers are ideal to implement asynchronous inter-process communication in microservice architectures. Below, I use RabbitMQ messaging-broker to illustrate how apps can easily be configured to send-receive messages with connectors and bindings.

Installing a Broker

RabbitMQ is most conveniently installed in a Kubernetes cluster using Helm charts. You can use helm command from your lapatop to perform the installation. Alternatively, you can use the command install of Cloud Manager CLI, has done before for MySQL.

Check that you have at least one solution for RabbitMQ in Cloud Manager marketplace by typing ei market rabbitmq. Confirm that it shown under a catalog of type HELM. If not, you can add a catalog known to have it, as follows:

ei catalog create bitami --type=helm --url=https://charts.bitnami.com/bitnami

To perform the installation with ei, assuming devops tools are installed in the cluster, type the command below.

ei install rabbitmq
ei ps

TIP: To use ei to install Helm charts, the devops tools image need to be running as a pod in the cluster. This can be done via the Cloud Manager UI in panel Cluster > Settings > Runtime > Devops Tools, or by specifying the option --tools when importing the cluster.

To create connectors to solutions installed from Helm charts you also need to make the solution managed by Cloud Manager. This is done with command deploy attach as shown below:

ei deploy attach rabbitmq

Defining Broker Connectors

Below, I use command connector add to create a connector the service rabbitmq with name default. The connector name default is somewhat special, as it can be omitted when defining bindings.

ei connector add rabbitmq default --type=amqp --spec=password:^^rabbitmq.rabbitmq-password,username:user,host:\${host}

The option --spec defines a comma-separated list of key-value pairs for the specification for the connector. Here, exporting the variables: password looked up from data in rabbitmq-password in secret rabbitmq, username defined as user, and host referring to built-in variable ${host}. The virtual host and ports are left unspecified, so they defaults are assumed. The option --type is an optional hint to allow auto-generation of binding specifications.

TIP: If you install RabbitMQ in a way different from the above, the credentials might need to be looked up in different way.

Binding to a Broker

Next, I create a binding for the app to connect to rabbitmq the service. The option --auto hints Cloud Manager to generate the specification automatically. Because the app stack in Spring Boot, the environment variables SPRING_RABBIT_ will be configured automatically.

ei binding add superheros rabbitmq --auto
ei deploy restart superheros

Command restart is used to restart the app and update the settings. Confirm that the environment variables are set automatically from the binding by inspeciting the deployment manifest. For this, you can use Cloud Manager UI panel Deployment > Instances > MetaData, command kubectl describe superheros, or command ei deploy manifest superheros.

Connecting to other Microservices

In addition to backing services, connectors and bindings can also be used to connect to other microservices, including: third-party reusable middleware services, such as a SSO Gateway or a Payments Gateway, and other custom services part of an overall architecture. Below, I illustrate these two scenarios by showing how to deploy and connect to EInnovator SSO Gateway, and discuss how to make the sample app API be accessible by other services.

Installing a SSO Gateway

To install EInnovator SSO Gateway I use the command install — the same procedure used before to install the backing services. The catalog name is einnovator and solution name einnovator-sso. The option --name is used to define a shorter name for the deployment.

ei install einnovator/einnovator-sso --name=sso

TIP: The EInnovator SSO Gateway will try to automatically bind to a AMQP service in the same space if you defined a connector named default, and tries to declare a queue and exchange. To prevent this run command ei connector rm sso amqp, or change the name of the RabbitMQ connector. You should also restart the SSO Gateway after this with command ei deploy restart sso.

Defining a SSO Connector

Command connector add is used to define a connector for the SSO Gateway named app. The type ei-sso is used to hint Cloud Manager on how to generate stack-aware bindings. The --spec option defines a comma-separated list of variables to export, including: the clientId and clientSecret, and the service URL from the implicit variable ${url}.

ei connector add sso app --type=ei-sso --spec=clientId:application,clientSecret:application\$123,url:\${url}

The specified clientId and clientSecret are default ones, setup automatically by the SSO Gateway for development purposes. For production environments, you should create new client app credentials for security reasons. (see below)

Without additional configuration, the implicit variable ${url} will take the value http://host-ip — where host-ip is the private IP address of the service. This is good enough if the connecting applications are using only the API of the SSO Gateway. For OAuth2 authentication with code grant or to use the UI, the SSO Gateway needs to accessible outside the cluster from a web-browser. The most convenient way to do this (although not the only one supported by Kubernetes), is to add a DNS route to the app. (see below)

Binding to the SSO Gateway

To bind the sample app to the SSO Gateway I use command binding add with option --auto. Cloud Manager will generate the binding spec to set the environment variables SSO_SERVER, SSO_CLIENTID, and SSO_CLIENTSECRET.

ei binding add superheros sso/app --auto
ei restart

Adding DNS Routes

To make the SSO Gateway accessible via a web browser, I will add a DNS route to the service. First, a domain needs to be defined (if not done yet) with command domain create. For secure HTTPS access, three files have to be provided — with a certificate, private key and certification authority chain — specified with options --crt, --key, and --ca. You can use command ls -d to list all domains.

# create an (unsecure) domain
ei domain create acme.com

# create a secure domain
ei domain create acme.com --crt=crt.pem --key=key.pem --ca=ca.pem

The command domain set can be used to make this the default domain. Otherwise, the option -d needs to be used to specify the domain when creating routes.

ei domain set acme.com

You also need to configure your DNS nameserver with a new A record, to route the domain *.acme.com to the public IP address of any of the nodes in the cluster. You can use command ei nodes ls -b to get these IP address(es).

The command route add is used to add the route to the SSO Gateway. The first argument is the deployment name, and the second the route name. After this command is executed, the SSO Gateway becomes available in URL https://sso.acme.com.

ei route add sso sso

You can now login to the SSO Gateway UI from a web browser. You will be asked to register the admin user, as first step. You can also use the UI to register new client apps in Admin > Clients > Add New. With the new clientId:clientSecret client credentials you can also to create additional connectors for the SSO service, and create new bindings that bind to these connectors.

The command connector refresh can be used to update the connector app to reflect the availability of a DNS route. This makes the value of the implicit variable ${url} to use the route and be set as https://sso.acme.com. Similarly, the command binding refresh updates the binding of the sample app. When the sample app is restarted becomes possible to login via OAuth2 (top-right dropdown).

ei connector refresh sso app
ei binding refresh superheros sso/app
ei restart superheros

Defining an API Connector

In addition to a UI, the sample app Superheros implements also a REST API for managing superhero resources with CRUD operations. To make this service API easily accessible by other services, we can define a default connector.

ei connector add superheros default --spec=url:\${url}

As was the case for the app connector of SSO Gateway, the implicit variable ${url} will take the value http://host-ip. To access the UI of the superheros app, and/or to access the API via a DNS host.domain, add one (or more) route(s) to the app.

ei route add superheros superheros
ei route add superheros heros

Client apps can then be configured to connect to the REST API (using the DNS route address) by defining a binding with a custom spec.

ei binding add myotherapp superheros --spec=superheros_server:\${url}

The variable superheros_server is some custom variable, assumed to be recognized by myotherapp.

Note: When service API is accessed via the private IP address (rather than a DNS host.domain), the connector-binding approach can be considered an overkill since Kubernetes exposes automatically this information in environment variables KUBERNETES_*_HOST. However, this requires the app to be modified to pickup this Kubernetes specific variables and detect that is running in a cluster rather than a (localhost) dev environment. With the binding, we can select which variables are set to match the ones the app expects by default. While libraries can be created to abstract the cloud environment (e.g. Spring Cloud Connector), this complicates matters and still requires the app to be modified to run in a cloud environment. Thus connector—binding approach is more flexible, as it does not require apps to be modified. In the spirit of dependency-injection systems, the cloud platform adapts to the app rather than the other way around.

Connecting to External Services

So far I considered only connectors for services running inside Kubernetes and in same cluster/space. Cloud Manager also supports external connectors that specify information about services running elsewhere (e.g. URLs and credentials). Possible use cases, include: databases running in a VM outside containers (for performance, security, or other reasons); a message-broker provisioned in a service-broker marketplace and managed by a third-party; or a SMTP mail service managed by a CSP. External connectors are defined at the space level via the UI or the CLI.

Defining an External Connector

I define below a connector to an external SMTP service, named mailer, managed by AWS. The command connector add is used as before, but now the option -x is used to specify that it is an external connector. The connector is created in the scope of the current space. Alternatively, the option -n can be used to specify another space.

ei connector add mailer -x --type=mail --tags=smtp \
  --spec=host:email-smtp.us-east-1.amazonaws.com,port:587,from:web@acme.com,username:^^mail.key,password:^^mail.secret

The type mail and tag smtp are used to hint Cloud Manager how to create binding specs. For a Spring Boot application this sets SPRING_MAIL_* variables. Notice that the connector spec refers to a secret name mail, so we need to define it. Below, I use command secret create for this.

MAIL_KEY=XXXXX
MAIL_SECRET=YYYYY
ei secret create mail --encode --data=key:$MAIL_KEY,secret:$MAIL_SECRET

Binding to an External Connector

To bind to an external service connector is similar to a deployment connector. Cloud Manager matches the selector of the binding with the connector name.

ei binding add superheros mailer 
ei superheros restart

Summary

I gave an overview of how to use the twin mechanisms of connectors and bindings, as available in Kubernetes Cloud Manager, to auto-configure applications and micro-services in a distributed environment. These mechanisms, when combined with frameworks that support auto-configuration and dependency-injection of components, offer a simple and elegant approach to configuration in microservice architectures.

The approach is also very uniform, which make it specially valuable in polyglot architectures. While different microservices may be implementing in different languages and frameworks (e.g. some in Java and some in GoLang) and connect to different kinds of services (e.g. relational and NoSql databases), the same consistent and mostly automatic approach is for configuration.

I used several examples to illustrated the approach, including: connecting to a database, connecting to a message-broker, connection to a SSO gateway, exposing the API endpoints of a service, and connecting to external services. I anticipate these kind of mechanisms will increase in important and gain developer mind-share as the complexity of architectures grow. If nothing else, the connector-binding approach enriches the set of options available to developers when configuring applications in the cloud and Kubernetes environments.

Learning More

Comments and Discussion

Content