With other words:
Secrets and Config Maps
in
SAP Cloud Platform Serverless Runtime
What's that?
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 Code
So what about those Secrets and Config Maps?w
Short answer:
To me, they look like simple property files
Example:
We want to write a function which needs to call a different REST endpoint
We don’t want to hardcode the URL and credentials in the code
So we're looking for a way to store such info outside the code, somehow in some property files
In the
server-and-file-system-less Function world, there’s a special mechanism to do so.
In this blog, we’re going through a simple example.
We're going to define a Secret and access it from within the code
Note:
This blog shows the usage of Extension Center, but everything can be done in your
local dev environment as well
Prerequisites
You should already be familiar with the Function-as-a-Service part of
SAP Cloud Platform Extension Center, serverless runtime
Otherwise, see
series of tutorials
Create Project
We can
create a new Extension (Functions project) or reuse an existing project
In my example, using "Template with function":
Extension name:
topsecret
Runtime:
nodejs10
Function name :
secretfunction
HTTP trigger name :
showsecret
After project creation, we want to define
secret and
config map.
Currently, there’s no UI support, so we have to create files and folders and edit the
faas.json manually
Define secret
First of all, we have to understand that a secret is a logical artifact, handled by the FaaS runtime
A secret points to a folder, where a file is located (can be multiple)
That file contains the secret information
To me, a
secret is like a secret treasure map. It doesn’t contain the treasure itself, only the path
The hidden treasure can be found there and it contains …. tasty cat food
So let's create files and folders first
Note:
As usual, by choosing silly names, I’ve tried to make clear that you’re free to choose the file and folder names
There’s only one exception:
The “data” folder must have name as “data”
Create Folder
To create a folder, select the
Code folder, then press the create-folder symbol
Enter folder name as
data
Then click on the new
data folder and create a subfolder called
donotopenthisfolder
Click on folder
donotopenthisfolder and press the file symbol
Enter file name as
hiddensecret.json
Enter secret values
Now open the file
hiddensecret.json
Paste the following content:
{
"Max": {
"username": "max@maxmail.com",
"password": "max123"
},
"Joe": {
"username": "joe@joemail.com",
"password": "joe123"
}
}
That’s not it
I mean, we've only created a file with secret info.
That's not THE
secret yet
Although the file is located in a folder which nobody would dare to open
Note:
Make sure to press “Save And Deploy” from time to time
Define Config Map
A
config map is similar like a secret, but not so top secret.
We use
config maps to store any configuration info which we don’t want to hardcode in the javascript module
Again, a config map is a file, located in a subfolder of the “data” folder
Create folder
Select the folder
data and create a subfolder with name
cfg
Select the folder
cfg and create a file with name
addressconfig.json
Enter configuration values
Paste the following content which contains an example for possible REST endpoint URLs
{
"dev": {
"user": "Max",
"url": "http://maxservice/endpoint"
},
"prod": {
"user": "Joe",
"url": "http://joeservice/endpoint"
}
}
Create another folder
Select the folder
cfg and create a file with name
addresstext.de
Enter configuration value
This file contains an example for plain text in German language:
Guten Morgen
Note:
Finally, our FaaS project should look like this:
Now really define Secret and Config Map
Up to now we’ve only created files.
Now we need to declare them in the manifest file, such that the FaaS runtime can take care of them
So now we really DEFINE the secret and config maps
We open
faas.json
Since we’ve used the template to create the extension project, the file initially would look like this:
We can see the elements which sound promising: “secrets” and “configs”
And we can see that the secrets are defined and where they are referenced:
A function has to declare which secret and config it wants to use
So now we can go ahead and enter the declarations and usages
As mentioned, a
secret is a logical artifact which is defined as an entry in
faas.json
To make it less logical and more physical, it has the
source property which points to a directory.
It is the directory which contains the files with configuration info
"secrets": {
"mysecret": {
"source": "./data/donotopenthisfolder"
}
},
And the secret has a name of our choice, such that it can be referenced.
BTW, same procedure with
config
Next step:
If we want to access secret info from the implementation of our function, we need to explicitly reference it.
To reference a secret, we just write the name in an array (can reference multiple secrets)
"functions": {
"secretfunction": {
"secrets": ["mysecret"],
Finally, the full
faas.json looks as follows
{
"project": "topsecret",
"version": "0.0.1",
"runtime": "nodejs10",
"library": "./lib",
"secrets": {
"mysecret": {
"source": "./data/donotopenthisfolder"
}
},
"configs": {
"myconfiguration": {
"source": "./data/cfg"
}
},
"functions": {
"secretfunction": {
"module": "index.js",
"handler": "handler",
"secrets": ["mysecret"],
"configs": ["myconfiguration"]
}
},
"triggers": {
"showsecret": {
"type": "HTTP",
"function": "secretfunction"
}
}
}
Note: saveanddeploy
View the values
Now let’s explore what we’ve just defined
In Extension Center, click on Form View, then “Secrets” tab and finally on the icon to view the details
The content of the secret is visualized in a nice way:
The dialog is well aware that the value of a secret is secret
Anyways, I still believe it looks like cat food in a treasure chest...
Let’s also view the details of the config:
We can see that we have 2 config maps and that one has json content and the second one has string content (german string, BTW)
Note the terminology:
The file name is the
key, the file content is the
value
In fact, it is a (config) map with key and value
We will need that later
Access secret and config in function code
We didn’t create the secret only for playing hide and seek
We want to access it in the impementation and extract the value
How to do this?
It is done with support of FaaS runtime, which offers a convenient API for it
When the runtime invokes our handler function, it passes an object to us which contains all required info: the
context object
How to use it?
The convenience API offers access to the value of the
secret, based on the name of the
secret
context.getSecretValue...
Furthermore, we can choose if we want the response as plain String or as JSON
context.getSecretValueJSON(…)
Next, we have to pass the information about which secret we want to use
Since there can be multiple secrets defined in
faas.json and referenced by any function, we have to pass the
secret name as first param
Remember that the secret basically is a folder, such that we also have to pass the file name as second param:
context.getSecretValueJSON('mysecret', 'hiddensecret.json')
As a response we get a JSON object (depending on the API-method we’ve chosen)
Example for node10 syntax:
const credentials = await context.getSecretValueJSON('mysecret', 'hiddensecret.json');
The API overview can be found in the
SAP Help Portal
Example for the syntax
getSecretValueJSON(name, key)
Explanation:
As already explained, the
“name” is the secret name (pointing to a folder)
“key” is the file name
Now we can open our function in the Code editor and replace the generated content with the following snippet:
module.exports = {
handler: async function (event, context) {
const credentials = await context.getSecretValueJSON('mysecret', 'hiddensecret.json');
const config = await context.getConfigValueJSON('myconfiguration', 'addressconfig.json')
const greeting = await context.getConfigValueString('myconfiguration', 'addresstext.de');
const url = config.dev.url;
const user = credentials.Max.username
const pwd = credentials.Max.password
console.log(`Calling URL: '${url}' with user '${user}' and password '${pwd}' and greeting '${greeting}'`)
return "Top Secret";
}
}
The example shows the usage of API for secret and configs and it shows how the different content is accessed: JSON and String
The example assumes that we would use the info for sending a request to a REST endpoint…
But we can skip that and just write the info to the log
Note: saveanddeploy
Run
After paste and save and deploy, we can invoke the function with the generated HTTP trigger URL
Our browser response contains just a silly text.
Now we regret that we’ve written the info text to the log instead to the HTTTP response
So we have to continue cligging digging for the treasure chest
Finally we find it:
What we see:
The secret information was extracted from the secret and config maps
Good job done nicely by the FaaS runtime
Maybe we think that it wasn’t a good idea to write the sensitive info to the log – but who cares what is done in a simple example?
We can also see that the runtime provides some info about the used environment, which is the nodejs runtime and the existing secrets and configs
This is another convenient service for us
Summary
We've learned that secrets and config maps are elements in the
faas.json which point to simple property files
And we've learned how to access the content of such files from the implementation code
Quick Guide
Create files and folders to store the desired config data
Concrete: create "data" folder and another subfolder and store the files there
Define the secret in the
faas.json
"secrets": {
"mysecret": {
"source": "./data/donotopenthisfolder"
}
},
Reference it from function in
faas.json
"secrets": ["mysecret"],
In code:
Use helper methods to access the secret, and pass secret name and file name:
context.getSecretValueJSON('mysecret', 'filename.json')
Appendix: All Sample Project Files
For your convenience, see here the project structure:
faas.json
{
"project": "topsecret",
"version": "0.0.1",
"runtime": "nodejs10",
"library": "./lib",
"secrets": {
"mysecret": {
"source": "./data/donotopenthisfolder"
}
},
"configs": {
"myconfiguration": {
"source": "./data/cfg"
}
},
"functions": {
"secretfunction": {
"module": "index.js",
"handler": "handler",
"secrets": ["mysecret"],
"configs": ["myconfiguration"]
}
},
"triggers": {
"showsecret": {
"type": "HTTP",
"function": "secretfunction"
}
}
}
package.json
{}
index.js
module.exports = {
handler: async function (event, context) {
const credentials = await context.getSecretValueJSON('mysecret', 'hiddensecret.json');
const config = await context.getConfigValueJSON('myconfiguration', 'addressconfig.json')
const greeting = await context.getConfigValueString('myconfiguration', 'addresstext.de');
const url = config.dev.url;
const user = credentials.Max.username
const pwd = credentials.Max.password
console.log(`Calling URL: '${url}' with user '${user}' and password '${pwd}' and greeting '${greeting}'`)
return "Top Secret";
}
}
Appendix: The Hidden Secret