Cody is writing an app and a function that need a backing service like a MySQL database.
A backing service is any service the app consumes over the network as part of its normal operation. Examples include datastores (such as MySQL or CouchDB), messaging/queueing systems (such as RabbitMQ or Beanstalkd), SMTP services for outbound email (such as Postfix), and caching systems (such as Memcached).
https://12factor.net/backing-services
The question "How does Cody connect his function or app to a backing service?" quickly comes up. So far, we don’t have a very good answer when deploying the apps and functions to Kubernetes. This document discusses a Proof-of-concept implementation to solve this problem for code deployed using riff or PFS.
Cody can provision a backing service in a variety of ways and we might have to adjust how we bind to them based on the way that they are provisioned.
The four ways of provisioning that we cover in this document are:
-
Independent Services Marketplace or ISM for short
To allow for several ways of provisioning the service we can introduce a Binding
object to hold the information that is needed for a succesfull binding and we can populate this object in different ways depending on the way the service was provisioned.
The Binding
object will be used to bridge the various ways of provisioning the service with the needs of the app for properties needed to connect to the service at runtime. It will either refer to keys in a secret or provide the actual value based on user input. It might contain fields like:
- name - secretRef - uri - uriKey - jdbcUrl - jdbcUrlKey - database - databaseKey - username - usernameKey - passwordKey
The Binding resource could look something like this:
apiVersion: service.projectriff.io/v1alpha1
kind: Binding
metadata:
name: mypets
namespace: demo
spec:
secretRef: mypets
uriKey: uri
usernameKey: username
passwordKey: password
Most of the work is going to be in the area of mapping and providing the connection properties needed by the app based on data provided by the service bindings and secrets and any additional info provided by the user.
Cody can create a new database using the following Helm command:
helm install --name mypets --namespace demo \
--set mysqlUser=riff --set mysqlDatabase=mypets stable/mysql
We can see the following in the NOTES section when the chart is executed :
MySQL can be accessed via port 3306 on the following DNS name from within your cluster:
mypets-mysql.demo.svc.cluster.local
This gives us most of the info we need to bind to the new database. The uri
can be constructed by using mysql://<service-host>:<service-port>/<database>
The only missing piece is the generated password. When the chart is executed it also creates a secret named mypets-mysql
with mysql-password
and mysql-root-password
keys, so this should give us all that we need.
Now Cody can create the service binding:
riff binding create mypets --namespace demo --secret-ref mypets-mysql \
--uri mysql://mypets-mysql.demo.svc.cluster.local:3306/mypets \
--username riff \
--password-key mysql-password
Note
|
We are using the |
Cody can provision the database using the svcat
CLI:
svcat provision mypets --namespace demo \
--class mysql --plan 5-7-14 -p mysqlDatabase=mypets
He can then check on the status:
$ svcat get instances --namespace demo
NAME NAMESPACE CLASS PLAN STATUS
+--------+-----------+-------+--------+--------+
mypets default mysql 5-7-14 Ready
Next Cody creates the Service Catalog binding:
svcat bind mypets --name mypets --namespace demo
This command creates a servicebinding.servicecatalog.k8s.io
object as well as a Secret named mypets
in the demo
namespace.
We can use the Secret and create a binding.service.projectriff.io
object that will provide all the information needed to create a connection.
riff binding create mypets --namespace demo --secret-ref mypets \
--uri-key uri \
--username-key username \
--password-key password
Note
|
Depending on the broker that was used to provision the service there might not be enough information available in the Secret and we might have to supplement additional connection parameters like we had to above when using the Helm chart. |
ISM is still a work-in-progress and this section is based on the its-always-sunny
branch and installed as outlined in the End-to-End PKS ISV Service document.
Note
|
We are using a slightly modified MySQL chart for this example. Starting with the kibosh-sample mysql chart we are then adding database name and username to the binding/secret that is generated. |
First we’ll see what services are available for provisioning:
$ ism service list
SERVICE PLANS BROKER DESCRIPTION
mysql medium, small ksm-broker Fast, reliable, scalable, and easy to use open-source relational database system.
Cody can now provision the database using the ism
CLI:
ism instance create --name=mypets \
--service=mysql --plan=small --broker=ksm-broker \
--parameters='{"mysqlDatabase":"mypets"}'
He can then check on the status:
$ ism instance list
NAME SERVICE PLAN BROKER STATUS CREATED AT
mypets mysql small ksm-broker created 2019-07-08 11:30:22 -0400 EDT
Cody can now create the ISM service binding:
ism binding create --name=mypets --instance=mypets --runtime=pfs-demo/demo/mypets
This command creates a servicebinding.osbapi.ism.io
resource as well as a Secret named mypets
in the demo
namespace.
We can use this Secret to create a binding.service.projectriff.io
resource that extracts the information needed to connect to the service.
riff binding create mypets --namespace demo --secret-ref mypets
Crossplane provides a way to provision a database in addition to all other features that it provides. We’ll provision a MySQL database using GCP Cloud SQL in this example.
Alana has already configured Crossplane so Cody can now simply provision a database:
Note
|
We could use the experimental "xpl" CLI generated by Mark Fisher, but decided to show the YAML file here for completenes. |
apiVersion: storage.crossplane.io/v1alpha1
kind: MySQLInstance
metadata:
name: mypets-cloud
namespace: demo
spec:
classReference:
name: standard-mysql
namespace: crossplane-system
engineVersion: "5.7"
Applying this MySQLInstance
resource creates a Secret in the demo namespace with the following content:
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: mypets-cloud
namespace: demo
data:
endpoint: MTAuMTEuMTIuMTM=
password: ZYbnFQVnZ180UnlSQXI4MlBuTk44OWlmQWdB
username: cm9vdA==
Using the information in the secret Cody can create the following binding:
riff binding create mypets --namespace demo --secret-ref mypets-cloud \
--host-key endpoint \
--uri 'mysql://${MYPETS_HOST}/mypets' \
--username-key username \
--password-key password
Yes, that is something we’ll address going forward. In some instances we don’t need to create the projectriff binding and could use the servicebinding.osbapi.ism.io
or servicebinding.servicecatalog.k8s.io
resources directly.
We have a sample app mypets-app that is a Spring Boot app that uses Spring Data Rest to build a web app that provides a REST style API for persisted data. The entity model is very simple consisting of a Pet entity that has a name field and a PetType reference that defines what type of pet it is.
@Entity
@Table(name = "pets")
public class Pet {
@Id @GeneratedValue
Long id;
String name;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "type_id")
private PetType type;
...
@Entity
@Table(name = "types")
public class PetType {
@Id @GeneratedValue
Long id;
String name;
...
We can create an application.build.projectriff.io
resource to build the app.
riff application create mypets --namespace demo \
--git-repo https://github.com/trisberg/mypets-app.git
Note
|
If the database service was provisioned by Crossplane then we need to add a JDBC Socket Factory dependency to allow the app to connect to the Cloud SQL instance. The branch |
When Cody deploys a function or an app he can simply reference the binding that he created earlier. Let’s see what Binding
resources we have available:
$ riff binding list --namespace demo
NAME REF AGE
mypets secret:mypets-mysql 17m
Note
|
The |
Since Cody is deploying a Spring Boot based application he selects spring-boot
as the binding type.
riff handler create mypets --namespace demo \
--application-ref mypets \
--binding-ref mypets \
--binding-type spring-boot
We can use the following command to invoke the app:
riff handler invoke mypets /pets --namespace demo -- -w '\n'
This should show an empty list of Pets. To add a Pet we can use this command:
riff handler invoke mypets /pets --json --namespace demo -- \
-d '{"name": "Millie", "type": {"name": "dog"}}' -w '\n'
This should produce the following output:
{
"name" : "Millie",
"type" : {
"name" : "dog"
},
"_links" : {
"self" : {
"href" : "http://mypets.demo.example.com/pets/1"
},
"pet" : {
"href" : "http://mypets.demo.example.com/pets/1"
}
}
}
Now we should see the new Pet in the list of pets when invoking the /pets
endpoint:
$ riff handler invoke mypets /pets --namespace demo -- -w '\n'
{
"_embedded" : {
"pets" : [ {
"name" : "Millie",
"type" : {
"name" : "dog"
},
"_links" : {
"self" : {
"href" : "http://mypets.demo.example.com/pets/1"
},
"pet" : {
"href" : "http://mypets.demo.example.com/pets/1"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://mypets.demo.example.com/pets{?page,size,sort}",
"templated" : true
},
"profile" : {
"href" : "http://mypets.demo.example.com/profile/pets"
},
"search" : {
"href" : "http://mypets.demo.example.com/pets/search"
}
},
"page" : {
"size" : 20,
"totalElements" : 1,
"totalPages" : 1,
"number" : 0
}
}
We have a sample function pets that is a Spring Boot based function that queries the mypets
database based on a pet type argument. It will return the name of all pets that match the provided pet type.
@Bean
public Function<String, String> pets() {
return s -> {
List<String> pets = jdbcTemplate.queryForList(
"select p.name from pets p" +
" where p.type_id in (select t.id from types t where t.name = ?)",
String.class, s);
return pets.toString();
};
}
We can create an function.build.projectriff.io
resource to build the function.
riff function create pets --namespace demo \
--git-repo https://github.com/trisberg/pets.git
Note
|
If the database service was provisioned by Crossplane then we need to add a JDBC Socket Factory dependency to allow the app to connect to the Cloud SQL instance. The branch |
Since Cody is deploying a Spring Boot based function he selects spring-boot
as the binding type.
riff handler create pets --namespace demo \
--function-ref pets \
--binding-ref mypets \
--binding-type spring-boot
Cody should now have the pets
function deployed and connected to the provisioned database.