Skip to Content
Technical Articles

Writing Function-as-a-Service [7]: How to use Platform Services

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 Extension Factory, 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 local dev environment

Overview

  1. Configure Destination in the cloud cockpit
    Create Destination Service Instance and Service Key
    Create Destination Configuration
  1. Create function which uses the destination
    Create Function Project with secret and config map
    Implement Function, calling destination service, using secret
  1. 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

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”

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

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

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

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

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

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
}

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.