If anyone is interested I can share some of my findings of using Clojure with Google CLoud Functions and / or Google Cloud run
Yes please!
We also use clojure a little on Cloud Run.
For cloud functions you would think that using gen-class and implementing the interface would work but it doesn't
How I solved it is implementing the handler using Java and then on the first request start Clojure using the java api and load the namespace. Keep it as an variable and then invoke the handler function implemented in clojure
But this is far from optimal because the cold boot time that way is 2 seconds
package minimal;
import clojure.java.api.Clojure;
import clojure.lang.IFn;
import com.google.cloud.functions.RawBackgroundFunction;
import com.google.cloud.functions.Context;
public class Main implements RawBackgroundFunction {
IFn acceptFn;
public Main() {
super();
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("minimal.core"));
acceptFn = Clojure.var("minimal.core", "accept");
}
@Override
public void accept(String json, Context context) {
acceptFn.invoke(json,context);
}
}
^^ this is how I got it working
After the initial "boot" the second function call much faster. But cloud functions "scale down" to zero after a while. And you have the 2 second boot time again as it starts up again
The initial boot time problem is also with Cloud Run when you compile your clojure app as Uberjar
Super interesting. Was there a particular error you received when trying to run functions using gen-class?
Now I ended up with a solution where I use GraalVM to compile the Clojure App to an executable. And that works great for boot time. But it doesn't work well to interact with the google API's
Let me see if I can find some of the errors
To use the Google API's I am using clj-http-lite
and buddy-sign
to manually invoke the Google Cloud API REST endpoints
this is how I authenticate and call a google API
(def sa (walk/keywordize-keys
(ch/parse-string
(slurp "service-account.json"))))
(defn authenticate [scopes]
(let [token
(bjwt/sign
{
:iat (ct/now)
:iss (-> sa :client_email)
:sub (-> sa :client_email)
:scope
(s/join " "
scopes)
:aud (-> sa :token_uri)
:exp (ct/plus (ct/now) (ct/seconds 20))
}
(bkeys/str->private-key (-> sa :private_key))
{:alg :rs256})
access-token
(-> (http/post
(-> sa :token_uri)
{:form-params
{:grant_type "urn:ietf:params:oauth:grant-type:jwt-bearer"
:assertion token}})
:body
(ch/parse-string true)
:access_token
)
]
access-token
)
)
(defn fetch-sessions []
(let [access-token (authenticate ["<https://www.googleapis.com/auth/cloud-platform>"
"<https://www.googleapis.com/auth/cloud-platform.read-only>"
"<https://www.googleapis.com/auth/firebase>"
"<https://www.googleapis.com/auth/firebase.readonly>"
"<https://www.googleapis.com/auth/userinfo.email>"
"<https://www.googleapis.com/auth/firebase.database>"])
http-opts {:headers {"Authorization" (str "Bearer " access-token)}}
hget (fn [url]
(-> (http/get url http-opts)
:body
(ch/parse-string true)))
project
(hget
(str "<https://firebase.googleapis.com/v1beta1/>"
(s/join "/"
[ "projects"
It's still a work in progress. But at least this works with GraalVM
Exception in thread "main" java.lang.ExceptionInInitializerError at clojure.lang.Namespace.<init>(Namespace.java:34) at clojure.lang.Namespace.findOrCreate(Namespace.java:176) at clojure.lang.Var.internPrivate(Var.java:156) at minimal.core.<clinit>(Unknown Source) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490) at com.google.cloud.functions.invoker.NewBackgroundFunctionExecutor.forClass(NewBackgroundFunctionExecutor.java:71) at com.google.cloud.functions.invoker.runner.Invoker.startServer(Invoker.java:245) at com.google.cloud.functions.invoker.runner.Invoker.main(Invoker.java:129) Caused by: <http://java.io|java.io>.FileNotFoundException: Could not locate clojure/core__init.class, clojure/core.clj or clojure/core.cljc on classpath. at clojure.lang.RT.load(RT.java:462) at clojure.lang.RT.load(RT.java:424) at clojure.lang.RT.<clinit>(RT.java:338) ... 11 moreException in thread "main" java.lang.ExceptionInInitializerError at clojure.lang.Namespace.<init>(Namespace.java:34) at clojure.lang.Namespace.findOrCreate(Namespace.java:176) at clojure.lang.Var.internPrivate(Var.java:156) at minimal.core.<clinit>(Unknown Source) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490) at com.google.cloud.functions.invoker.NewBackgroundFunctionExecutor.forClass(NewBackgroundFunctionExecutor.java:71) at com.google.cloud.functions.invoker.runner.Invoker.startServer(Invoker.java:245) at com.google.cloud.functions.invoker.runner.Invoker.main(Invoker.java:129) Caused by: <http://java.io|java.io>.FileNotFoundException: Could not locate clojure/core__init.class, clojure/core.clj or clojure/core.cljc on classpath. at clojure.lang.RT.load(RT.java:462) at clojure.lang.RT.load(RT.java:424) at clojure.lang.RT.<clinit>(RT.java:338) ... 11 more
This might be a silly question, but did you (compile
ns)` the namespace along with gen-class attemp?
I had a similar stacktrace when using clojure in cloud dataflow
Don't think so. I made sure that when creating the uberjar that all classes got compiled with :aot all
Also I verified that all classes were compiled by unzipping the uberjar and checking that all class files were there.
And I used a decompiler to check if the structure of the generated class with handler got properly generated
ah ok. So strange.
Thanks so much for sharing the learnings though. Really much appreciated.
np š You're welcome !
Our use case for cloud run doesnāt rely on quick startup, so Iām just running an uberjar
That makes things a lot easier for sure !
Google is working on having better GraalVM support for the Java Google Cloud libraries but it's still far away from being useful
https://github.com/GoogleCloudPlatform/google-cloud-graalvm-support
Auth is also simple in our stack, just leveraging a service account with the required scopes, and then a token like this:
(defn get-gcp-token []
(let [credentials-client (-> (GoogleCredential/getApplicationDefault))
_ (.refreshToken credentials-client)
token (.getAccessToken credentials-client)]
token))
(defn execute-bq-query [project query]
(let [token (get-gcp-token)
response (-> (client/post (format "<https://bigquery.googleapis.com/bigquery/v2/projects/%s/queries>" project)
{:body (json/encode {:query query
:useLegacySql false})
:oauth-token token
:throw-exceptions false})
:body
(json/decode true))]
(if (:error response)
nil
(bq-response->clj response))))
Thatās pretty exciting!
which http client are you using ?
or is this a google specific client ?
[clj-http.client :as client]
or do you mean this one? com.google.api.client.googleapis.auth.oauth2.GoogleCredential
the http one. Didn't know you can pass in oauth-token as a parameter š
ah. yeah. Itās very handy
Weāre moving toward using the Java client libraries more and more though. So that we donāt have to keep decoding the data structures that the APIās returnā¦
@domparry what are you using to set up the google project ? I am currently looking into using terraform to do this but I would love to do it more the clojure way
This is something I still need to figure out. I was looking into using the GRPC version of the Google API's. But the java GRPC library has some issues with GraalVM. My plan was to compile the protocol buffer files to Clojure somehow to have an easy way to have some verification.
ah, weāre still doing it manually. Also looking to take a step in that direction. I was looking at something the other day that wrapped yaml as mapsā¦. I thought about actually just writing some babashka scrips and yaml as maps
Yes. I also want to use as much as possible in GCP
My idea so far is at some point to convert the terraform files I have now to Clojure EDN files and use the source code of https://github.com/luchiniatwork/terra in a babashka file.
Not sure if all the Clojure code in there is Babashka compatible. But it's really not a lot of code so it shouldn't be too hard to get it working
The generated yaml files would convert to gcloud compatible resource descriptions ?
The client library docs arenāt amazing, but the more I get used to them, the more I like them.
This was specifically for GKE in our case.
describing our services, deployments, ingresses, certificates, etc.
I havenāt dug much into terraform. Itās on my todo list. š
There are some good things and some bad things about it š ā¢ The google provider has excellent support ā¢ HCL files get very wieldy
Also struggling a bit with managing secrets. The terraform way of doing things is keep all secrets in your state file.
ah. Yes. Weāre trying to push towards Secrets Manager in GCP
But itās not appropriate for everything.
Build time secrets in CI, Run time secrets we push to use secrets manager at startup
So what I am trying to get working now is to set an initially generated password using the terraform script and load it into a GCP secretmanager secret. Then reference the latest version of that secret in the different resources. That way I can update the password using the secretmanager. And re-run the terraform script to update all passwords.
Ah. nice. Feels like a thing that something like Hashicorpās Vault might help with? (But then you have another thing to run)
Exactly š My plan so far is to keep things as much as possible in GCP.
The previous version of our software uses a lot of Kubernetes using GKE and custom kubernetes clusters. I kinda want to move away from that
When testing out if your docker containers run on cloud run make sure to use the gvisor runtime for docker. I ran into an issue where jetty was trying to set certain sockopts that were not supported by gvisor so I switched to http-kit and then it worked. It was difficult to figure out because it worked on my machine with docker but not on cloud run.
Ya, saw exactly the same problem and ended up with a java wrapper very similar to what @mozinator created: https://github.com/atomist-skills/gcf-java11-clojure/blob/master/src/main/java/functions/Main.java There is a thread over in the gcp slack on exactly this problem too - I'll post in this channel if anything comes of it.