google-cloud

Google Cloud Platform: Clojure + {GAE, GCE, anything else on Google Platform}
mozinator2 2020-12-07T10:25:52.008200Z

If anyone is interested I can share some of my findings of using Clojure with Google CLoud Functions and / or Google Cloud run

domparry 2020-12-07T10:26:08.008900Z

Yes please!

domparry 2020-12-07T10:26:39.009800Z

We also use clojure a little on Cloud Run.

mozinator2 2020-12-07T10:27:40.010900Z

For cloud functions you would think that using gen-class and implementing the interface would work but it doesn't

mozinator2 2020-12-07T10:29:35.013700Z

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

mozinator2 2020-12-07T10:30:15.014400Z

But this is far from optimal because the cold boot time that way is 2 seconds

mozinator2 2020-12-07T10:30:49.014800Z

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);
  }
}

mozinator2 2020-12-07T10:30:57.015100Z

^^ this is how I got it working

mozinator2 2020-12-07T10:32:23.016600Z

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

mozinator2 2020-12-07T10:33:05.017700Z

The initial boot time problem is also with Cloud Run when you compile your clojure app as Uberjar

domparry 2020-12-07T10:33:57.019300Z

Super interesting. Was there a particular error you received when trying to run functions using gen-class?

mozinator2 2020-12-07T10:34:03.019500Z

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

mozinator2 2020-12-07T10:34:39.019600Z

Let me see if I can find some of the errors

mozinator2 2020-12-07T10:35:59.020200Z

To use the Google API's I am using clj-http-lite and buddy-sign to manually invoke the Google Cloud API REST endpoints

mozinator2 2020-12-07T10:39:13.021600Z

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]
               (-&gt; (http/get url http-opts)
                   :body
                   (ch/parse-string true)))
        project
        (hget
          (str "<https://firebase.googleapis.com/v1beta1/>"
               (s/join "/"
                       [ "projects"

mozinator2 2020-12-07T10:40:13.022400Z

It's still a work in progress. But at least this works with GraalVM

mozinator2 2020-12-07T10:42:45.022500Z

Exception in thread "main" java.lang.ExceptionInInitializerError at clojure.lang.Namespace.&lt;init&gt;(Namespace.java:34) at clojure.lang.Namespace.findOrCreate(Namespace.java:176) at clojure.lang.Var.internPrivate(Var.java:156) at minimal.core.&lt;clinit&gt;(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.&lt;clinit&gt;(RT.java:338) ... 11 moreException in thread "main" java.lang.ExceptionInInitializerError at clojure.lang.Namespace.&lt;init&gt;(Namespace.java:34) at clojure.lang.Namespace.findOrCreate(Namespace.java:176) at clojure.lang.Var.internPrivate(Var.java:156) at minimal.core.&lt;clinit&gt;(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.&lt;clinit&gt;(RT.java:338) ... 11 more

domparry 2020-12-07T10:45:28.022700Z

This might be a silly question, but did you (compile ns)` the namespace along with gen-class attemp?

domparry 2020-12-07T10:46:08.023Z

I had a similar stacktrace when using clojure in cloud dataflow

mozinator2 2020-12-07T10:48:12.023200Z

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

domparry 2020-12-07T10:48:52.023400Z

ah ok. So strange.

domparry 2020-12-07T10:49:07.023600Z

Thanks so much for sharing the learnings though. Really much appreciated.

mozinator2 2020-12-07T10:49:39.024300Z

np šŸ™‚ You're welcome !

domparry 2020-12-07T10:49:40.024500Z

Our use case for cloud run doesnā€™t rely on quick startup, so Iā€™m just running an uberjar

mozinator2 2020-12-07T10:50:15.025300Z

That makes things a lot easier for sure !

mozinator2 2020-12-07T10:51:11.026600Z

Google is working on having better GraalVM support for the Java Google Cloud libraries but it's still far away from being useful

šŸ‘ 1
domparry 2020-12-07T10:51:28.027300Z

Auth is also simple in our stack, just leveraging a service account with the required scopes, and then a token like this:

domparry 2020-12-07T10:51:38.027600Z

(defn get-gcp-token []
  (let [credentials-client (-&gt; (GoogleCredential/getApplicationDefault))
        _ (.refreshToken credentials-client)
        token (.getAccessToken credentials-client)]
    token))

(defn execute-bq-query [project query]
  (let [token (get-gcp-token)
        response (-&gt; (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-&gt;clj response))))

domparry 2020-12-07T10:52:05.027700Z

Thatā€™s pretty exciting!

mozinator2 2020-12-07T10:52:24.028500Z

which http client are you using ?

mozinator2 2020-12-07T10:52:32.028700Z

or is this a google specific client ?

domparry 2020-12-07T10:52:54.029Z

[clj-http.client :as client]

šŸ‘ 1
domparry 2020-12-07T10:53:33.029900Z

or do you mean this one? com.google.api.client.googleapis.auth.oauth2.GoogleCredential

mozinator2 2020-12-07T10:53:59.030600Z

the http one. Didn't know you can pass in oauth-token as a parameter šŸ™‚

domparry 2020-12-07T10:54:32.031500Z

ah. yeah. Itā€™s very handy

domparry 2020-12-07T10:55:16.032800Z

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ā€¦

mozinator2 2020-12-07T10:56:16.033800Z

@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

mozinator2 2020-12-07T10:57:43.034800Z

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.

domparry 2020-12-07T10:58:07.035400Z

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

domparry 2020-12-08T20:03:32.049300Z

Yes. I also want to use as much as possible in GCP

mozinator2 2020-12-07T10:59:26.036600Z

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.

mozinator2 2020-12-07T11:00:43.038200Z

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

mozinator2 2020-12-07T11:01:30.038300Z

The generated yaml files would convert to gcloud compatible resource descriptions ?

domparry 2020-12-07T11:01:35.038500Z

The client library docs arenā€™t amazing, but the more I get used to them, the more I like them.

domparry 2020-12-07T11:02:14.038700Z

This was specifically for GKE in our case.

domparry 2020-12-07T11:02:40.038900Z

describing our services, deployments, ingresses, certificates, etc.

domparry 2020-12-07T11:03:14.039100Z

I havenā€™t dug much into terraform. Itā€™s on my todo list. šŸ™ˆ

mozinator2 2020-12-07T11:04:34.039300Z

There are some good things and some bad things about it šŸ˜‰ ā€¢ The google provider has excellent support ā€¢ HCL files get very wieldy

mozinator2 2020-12-07T11:05:03.039500Z

Also struggling a bit with managing secrets. The terraform way of doing things is keep all secrets in your state file.

domparry 2020-12-07T11:05:50.039700Z

ah. Yes. Weā€™re trying to push towards Secrets Manager in GCP

domparry 2020-12-07T11:06:24.039900Z

But itā€™s not appropriate for everything.

domparry 2020-12-07T11:07:01.040100Z

Build time secrets in CI, Run time secrets we push to use secrets manager at startup

šŸ‘ 1
mozinator2 2020-12-07T11:07:48.040400Z

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.

domparry 2020-12-07T11:09:58.040700Z

Ah. nice. Feels like a thing that something like Hashicorpā€™s Vault might help with? (But then you have another thing to run)

mozinator2 2020-12-07T11:10:44.041Z

Exactly šŸ˜‰ My plan so far is to keep things as much as possible in GCP.

mozinator2 2020-12-07T11:12:57.041200Z

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

mozinator2 2020-12-07T18:33:12.043700Z

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.

slimslenderslacks 2020-12-07T20:05:44.043900Z

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.