This blog is part of a
series of tutorials explaining how to write serverless functions using the Functions-as-a-Service offering in
SAP Cloud Platform Serverless Runtime
Quicklinks:
Quick Guide
Sample Project
In a
previous blog, we had a little introduction into the usage of
secrets and
config maps
Today, I’d like to go through an example which shows how to connect a function to a service running in
SAP Cloud Platform, Cloud Foundry Environment
As an example, we’re going to use the
Destination service and call a configured destination
Any useful purpose to do that?
Surprisingly: yes
This example could be enhanced to a checktool which runs every night and checks if the destinations are still responding as expected
Why useful?
It would help with troubleshooting and early identify errors
Indeed
Anyways, this sample can be modified to use any other platform service
Prerequisites
To follow this tutorial, you should be familiar with FaaS (see
series)
The function code is based on
Node.js
As usual, this tutorial can be followed in
Extension Center or with l
ocal dev environment
Overview
- Configure Destination in the cloud cockpit
Create Destination Service Instance and Service Key
Create Destination Configuration
- Create function which uses the destination
Create Function Project with secret and config map
Implement Function, calling destination service, using secret
- Run the sample
1. Configure Destination in the cloud cockpit
In our example, we want to use destinations from our function.
Destinations are usually used whenever a cloud app calls a URL of an external service.
With destinations, we can avoid hardcoding URLs, it solves CORS issues and maintenance
1.1. Create Destination Service Instance
So the first step is to create a service instance of "Destination" service
In your Space, go to Service Marketplace and search for the tile "Destination"
If you don't find it, you might need to add "Entitlement" (see
here for general description)
When creating the service instance,
no params are required
and the name can be
destinationinstance
1.2. Create Service Key
In the server-more world, we would deploy an app, with manifest, where we would define a binding to the service instance. Then, in the code, we access the service-credentials via env variables
In the serverless world, we don't deploy an app, so we don't have binding and no env variables
That's why we need to create a service key for the service instance.
The service key provides us with same credentials which we would get with binding
We click on the created instance name
Then choose “Service Keys” in the left pane
Finally we press “Create Service Key” and enter a name like
skfordestination
Now we can view the content of the service key
![](/legacyfs/online/storage/blog_attachments/2020/07/dest_sk.jpg)
I’ve marked the relevant properties
But for the moment, we leave it like it is
We don't close the browser window, as we'll need the service key content later
Why?
We need these credentials to call the destination programmatically
1.3. Create Destination Configuration
Until now we’ve only created a
destination service instance
Yes, it was necessary, otherwise we couldn't read the
destination configuration
Now we create a
configuration for a destination that should point to any URL
In the cockpit, we need to go to our subaccount, then expand “Connectivity”, click on “Destinations” and press “New Destination”
![](/legacyfs/online/storage/blog_attachments/2020/07/dest_config-1.jpg)
We create a destination of our choice, it doesn’t matter which URL, we won’t really use it seriously.
The screenshot shows an example
![](/legacyfs/online/storage/blog_attachments/2020/07/dest_config_details_wiki-1.jpg)
The above example points to an existing URL.
It specifies user credentials which aren't required. Just as example for later usage in the code
To illustrate the advantage of a destination:
The admin can decide to change the host of the destination to a different country, for example, (or landscape upgrade), and our function code wouldn't need to be adapted
2. Create Function which uses the Destination
Nest step is to create a function which programmatically uses the destination
2.1. Create Project Artifacts
I assume that you're familiar with Function-as-a-Service in
serverless runtime, so let's only roughly go through the required steps
Please refer to the
appendix for the file content
2.1.1. Create Project
We create a project with name
destichecker and runtime
nodejs10
Can be a project on local machine, or an "Extension" if you use the
Extension Center
2.1.2. Define Secret
We define a
secret (with name "destinationsecret") in the
faas.json
We create
data folder and
secrets subfolder and create a file (with name
destsrv.json) to carry the secret content
In our example, the file content is the content of the service key
As such, we go to our cloud cockpit and view the service key which we created above
We can just copy the whole content (yes, everything, including all the stuff which we’re not interested in) and we paste it into the file for our secret
And we save
![](/legacyfs/online/storage/blog_attachments/2020/07/secret_content-1.jpg)
2.1.3. Define Config Map
We define a
config in the
faas.json
In the
data folder, we create a file (
destcfg.json) to carry some configuration values
In our example, we store the name(s) of the
destination configuration(s) created above
Make sure to copy&paste the destination name correctly
![](/legacyfs/online/storage/blog_attachments/2020/07/config_content.jpg)
2.1.4. Create Function
In our
faas.json, we define a function (e.g. "checkdestfun") with module and handler and reference to the secret and the config.
We create the
lib folder (because predefined in the generated faas manifest) and the javascript file (
destichecker.js)
The javascript file contains the handler and some silly test code (for a first test)
2.1.5. Define Trigger
To test our example, we create a HTTP trigger (with name "checkdest"), later we can create an additional timer trigger to let our function do its useful work on a regular basis
Later?
Yes, but not in this blog
2.1.6. Try the Function
After verifying that our
faas.json looks like the one in the
appendix, we deploy our function project and invoke it with the HTTP trigger URL
e.g.
https://123-faas-http.tenant.eu10.functions.xfs.cloud.sap/checkdest
It should work as expected
2.2. Implement Function
Now we come to the implementation part.
This is about how to use the destination service programmatically.
The code is not specific for serverless functions.
The only difference is that we get the credentials - which are required to call the destination service - from the FaaS runtime instead of accessing the environment variables
What our function should do:
. Access the service credentials
. Call the destination service to get the configured destinations
. Afterwards, read the destination configuration to get the URL and authentication
. Finally, execute the URL
2.2.1. Access the Service Credentials
Above, we created a Service Key
Then stored the content (JSON) in a
secret
Now we use the API of the FaaS runtime to obtain the properties which we need from the service key:
const credentials = await context.getSecretValueJSON('destinationsecret', 'destsrv.json')
const destSrvUrl = credentials.uri
const oauthUrl = credentials.url
const clientId = credentials.clientid
const clientSecret = credentials.clientsecret
Similar approach to get the destination name from the
config map
2.2.2. Call the Destination Service
The Destination Service itself is protected with OAuth.
As such, we need 2 steps for calling the Destination Service:
1. Fetch JWT token
Here we need the info from the service key (clientid, clientsecret and oauthUrl)
const _fetchJwtToken = async function(oauthUrl, oauthClient, oauthSecret) {
...
const options = {
host: oauthUrl.replace('https://', ''),
path: '/oauth/token?grant_type=client_credentials&response_type=token',
headers: {
Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
2. Call Destination Service
Once we have the JWT token, we can use the Url from service key to call the service, and we use the destinationName which we had stored in a
config map
const _readDestinationConfig = async function(destinationName, destUri, jwtToken){
...
const options = {
host: destUri.replace('https://', ''),
path: '/destination-configuration/v1/destinations/' + destinationName,
headers: {
Authorization: 'Bearer ' + jwtToken
2.2.3. Read Destination Configuration
The result of the call to the Destination Service is the destination configuration for the requested destination name
In our Example, the destination configuration is simple.
We need the URL and the authentication info
We don't need to encode the user and password, this is done conveniently by the destination service
destiConfi.destinationConfiguration.URL
destiConfi.authTokens[0].http_header.value
2.2.4. Call the target endpoint
Now we have all information we need to call the endpoint which is configured in the destination configuration
Usually, in a destination configuration, we would extract only the variable part of a URL.
Which usually would be the host.
That's what we've done in our example
As such, in the code, we have to append all stable segments to the URL which we obtained from the destination configuration.
In our example, we call the info about "Cat" (hardcoded)
const _callEndpoint = async function(url, auth){
...
const options = {
host: url.replace('https://', ''),
path: '/Cat',
headers: {
Authorization: auth.http_header.value
See
appendix for full file content
2.2.5. Small Recap
In our function implementation, we want to call a target URL
The full URL is not stored in our function project.
We want to use a Destination
To call the destination service, we use credentials which we had stored in a
secret
First get a token
Then use it to get the destination config
Then read the config info to call the target endpoint
3. Run the sample
So finally, we copy all the function code from the appendix.
After deploy, we open the URL of our function and we get the expected result
![](/legacyfs/online/storage/blog_attachments/2020/07/result.jpg)
Of course, the next step would be to improve our checker:
Refactor the code, use config map for destination name, support multiple/all destinations check, support control URL parameters, send JSON response, support timer trigger,store check results in servless BackendService
etc
However, no next steps in this blog nor in future blogs
Summary
In this tutorial we've learned how to consume the destination service from a serverless function
The sample code can easily be adapted to any other service of SAP Cloud Platform
The essential learning was to copy a service key to the function project
Quick Guide
To consume a service in a function we have to:
- create service key for the service we want to use
- copy the service key into a file in the FaaS project
- define a
secret which points to that file
- in the function code, access the
secret
- use the credentials properties in order to call the cloud service
All Sample Project Files
Project Structure
![](/legacyfs/online/storage/blog_attachments/2020/07/projectStructure-3.jpg)
faas.json
{
"project": "destichecker",
"version": "0.0.1",
"runtime": "nodejs10",
"library": "./lib",
"secrets": {
"destinationsecret": {
"source": "./data/secrets"
}
},
"configs": {
"destinationconfig": {
"source" : "./data"
}
},
"functions": {
"checkdestfun": {
"module": "destichecker.js",
"handler": "doDestinationCheck",
"secrets": ["destinationsecret"],
"configs": ["destinationconfig"]
}
},
"triggers": {
"checkdest": {
"type": "HTTP",
"function": "checkdestfun"
}
}
}
package.json
{}
destsrv.json
{
"uaadomain": "authentication.eu10.hana.ondemand.com",
"tenantmode": "dedicated",
"clientid": "sb-clone123!b22273|destination-xsappname!0000",
"instanceid": "34ea5555-bbbb-bbbb-9999-4444200bbbbb",
"verificationkey": "-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwThn6OO9kj0bchkOGkqYBnV1dQ3zU/xxxxxxx+5/yDZUc0IXXyIWPZD+XdL+0EogC3d4+fqyvg/BF/F0t2hKHWr/UTXE6zrGhBKaL0d8rKfYd6olGWigFd+3+24CKI14zWVxUBtC+P9Fhngc9DRzkXqhxOK/EKn0HzSgotf5duq6Tmk9DCNM4sLW4+ERc6xzrgbeEexakabvax/Az9WZ4qhwgw+fwIhKIC7WLwCEJaRsW4m7NKkv+++++LKYesuQ9SVAJ3EXV86RwdnH4uAv7lQHsKURPVAQBlranSqyQu0EXs2N9OlWTxe8SoKJnRcRF2KxWKs355FhpHpzqyZflO5l98+O8wOsFjGpL9d0ECAwEAAQ==-----END PUBLIC KEY-----",
"xsappname": "clone34ea5555b4ee4bbb9999db444444b888!b22222|destination-xsappname!b000",
"identityzone": "myzone",
"clientsecret": "c2D3E4F5G6A1B2C3D4E5=",
"tenantid": "628e9f87-e539-49fc-a54f-1a1a1a1a1a",
"uri": "https://destination-configuration.cfapps.eu10.hana.ondemand.com",
"url": "https://mysubaccount.authentication.eu10.hana.ondemand.com"
}
destcfg.json
{
"name": "dest_wiki_en"
}
destichecker.js
const https = require('https');
// main entry
const _doDestinationCheck = async function (event, context) {
// we stored the desired destination name in a config map
const config = await context.getConfigValueJSON('destinationconfig', 'destcfg.json')
const destinationName = config.name
// use faas API to get credentials stored in secret
const credentials = await context.getSecretValueJSON('destinationsecret', 'destsrv.json')
const destSrvUrl = credentials.uri
const oauthUrl = credentials.url
const clientId = credentials.clientid
const clientSecret = credentials.clientsecret
// execute required steps to call the target URL
const jwtToken = await _fetchJwtToken(oauthUrl, clientId, clientSecret)
const destiConfi = await _readDestinationConfig(destinationName, destSrvUrl, jwtToken)
const result = await _callEndpoint(destiConfi.destinationConfiguration.URL, destiConfi.authTokens[0])
return `Check Result:\n${result.message}`
}
// JWT token is required to call the Destination Service
const _fetchJwtToken = async function(oauthUrl, oauthClient, oauthSecret) {
return new Promise ((resolve, reject) => {
const options = {
host: oauthUrl.replace('https://', ''),
path: '/oauth/token?grant_type=client_credentials&response_type=token',
headers: {
Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
}
}
https.get(options, res => {
res.setEncoding('utf8')
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
try {
const responseAsJson = JSON.parse(response)
const jwtToken = responseAsJson.access_token
if (!jwtToken) {
return reject(new Error('Error while fetching JWT token'))
}
resolve(jwtToken)
} catch (error) {
return reject(new Error('Error while fetching JWT token'))
}
})
})
.on("error", (error) => {
console.log("Error: " + error.message);
return reject({error: error})
});
})
}
// Call Destination Service. Result will be an object with Destination Configuration info
const _readDestinationConfig = async function(destinationName, destUri, jwtToken){
return new Promise((resolve, reject) => {
const options = {
host: destUri.replace('https://', ''),
path: '/destination-configuration/v1/destinations/' + destinationName,
headers: {
Authorization: 'Bearer ' + jwtToken
}
}
https.get(options, res => {
res.setEncoding('utf8')
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
try {
const destInfo = JSON.parse(response);
resolve(destInfo)
} catch (error) {
return reject(new Error('Error while parsing destination configuration'))
}
})
})
.on("error", (error) => {
console.log("Error while calling Destination Service : " + error.message);
return reject({error: error})
});
})
}
// call the URL extracted from Destination Config. Manually append required URI segments
const _callEndpoint = async function(url, auth){
return new Promise((resolve, reject) => {
const options = {
host: url.replace('https://', ''),
path: '/wiki/Cat',
headers: {
Authorization: auth.http_header.value
}
}
https.get(options, res => {
const status = res.statusCode
if(status < 400){
resolve({message: `Successfully called destination with target URL: ${url}${res.req.path}`})
}else{
reject({error: {message: `Invalid Url: ${url} with status ${status}`}})
}
})
.on("error", (error) => {
reject({error: error})
});
})
}
// export the handler function with readable alias
module.exports = {
doDestinationCheck : _doDestinationCheck
}