Cloud Run vs Secret Management for NodeJs Apps
As every DevOps team these days, we too struggle with application delivery. To be more precise: we want to make the process of code delivery and infrastructure management as painless as possible. Yes, there are obviously corners which couldn’t be cut and the issues to make a man with. But overall, that’s why we are here: To provide reasonable app delivery to the platform best suiting the app demands. Having a backend API that is not used all the time, we have chosen Cloud Run as the platform. But what about the secret management?
Well, if you were wondering if there is any support for secrets, wonder no more. It took a while since GCP Secret Manager was introduced (10.12.2019) and preview support to Cloud Run was implemented (12.5.2021). Some people would say that using secrets in KNative (which is the underlying system for Cloud Run) is not that hard to implement, but I am sure that’s just a blasphemy and Google has its reasons. In this small blog post, we are going to see how to use Secret Manager in Cloud Run. For that we’ll use Configuru, a wonderful small package helping us manage dotenv files in Ackee projects.
First, we have to start with setting up the environment variable CFG_JSON_PATH
in Dockerfile
\# BUILDER IMAGE
FROM node:14.16.0\-buster AS builder
...WORKDIR /usr/src/app
ENV CFG\_JSON\_PATH="/config/secrets.json"
...
The variable can be also set in Cloud Run itself. The main point here is to tell Configuru where to find the JSON file containing the secrets. Configuru uses this variable and reads the file it points to.
We picked /config
as the mount point due to issues with permissions. Once we mounted secrets into the app directory (/usr/src/app
), Cloud Run reported following issue:
Could not open file at path /usr/src/app/node_modules. The path is in a mounted secrets volume, but the exact path does not correspond to any secret specified in the mount configuration.
In a way, this actually makes a lot of the folders from FHS unusable. Once you are not sure, where your apps writes down or reads from, choose rather a new folder. That’s why we used /config
.
Now we can use Secret Manager. Create a new application secret with Terraform:
resource "google\_secret\_manager\_secret" "secret" {
secret\_id = "secret" ...
}
Do not forget to use JSON for the input value. So far, Secret Manager does not provide any input validation and could be used for any type of data. But Configuru only supports JSON and JSONC. If JSON is not formatted correctly, the debug output can be hard to interpret. Let’s use this simple secret:
{
"A\_SECRET": "hello world from GCP Secret Manager"
}
To give access to the secrets, give the Service Account used for Cloud Run service role name /Secret Manager Secret Accessor
. To honour the Principle of least privilege, you can assign permissions only for one particular secret. Let’s use an example from Terraform documentation:
resource "google\_secret\_manager\_secret\_iam\_member" "member" {
project = "PROJECT-ID"
secret\_id = google\_secret\_manager\_secret.secret.secret\_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:sa@PROJECT-ID.iam.gserviceaccount.com"
}
Where resource google_secret_manager_secret.secret
is a secret we created earlier with terraform.
Now we can configure Cloud Run itself. Due to label PREVIEW which is currently present in the configuration UI of the Cloud Run secrets, I thought you would rather benefit from YAML manifest representation:
...
spec:
...
template:
spec:
...
serviceAccountName: serviceAccount:sa@PROJECT-ID.iam.gserviceaccount.com
containers:
- image: ...
volumeMounts:
- name: SECRET\_NAME-peg-dax-xin
readOnly: true
mountPath: /config
volumes:
- name: SECRET\_NAME-peg-dax-xin
secret:
secretName: SECRET\_NAME
items:
- key: latest
path: secrets.json
As you noticed, I left out big parts of the manifest which do not contribute to the Secret Manager setup. Also, I presumed you would be interested only in the latest release of the secret. In case that’s not true, adjust the key in the secret items to your desired version.
From this point onward, everything is taken care of by Configuru. To make the example complete. Let’s create a configuration module in config.ts
. I will use well written example from the Getting Started part of README.md
:
import { createLoader, values, safeValues } from 'configuru'
const loader = createLoader()
const secretScheme = {
aSecret: loader.string.hidden('A\_SECRET'),
}
export default values(secretScheme)
export const safeConfig = safeValues(secretScheme)
Where A_SECRET
is a value you defined in the Secret Manager. Now you can use the variable anywhere in your code:
import config, { safeConfig } from './config'
console.log(config.aSecret) // hello \*\*\*nager
console.log(safeConfig.aSecret) // hello world from GCP Secret Manager
And we are done. In case you would rather appreciate using environment variables instead of mounting a JSON file, keep in mind that Configuru has storage precedence. For our use-case, the JSON file seems much more simple because it needs only one environment variable CFG_JSON_PATH
and that’s all. Hopefully, you found here something you can use in your next project. Also, I hope I didn’t mess up something and the example works as it should. If not, let me know in the comments