/ serverless

Deploying My First App to Cloud Functions

This past week I have been building a simple CRUD app for a freelance project. Since this is so small in scope, I decided to use express. A basic REST api was all I needed. When it came to setting up a development environment, I was going to setup a compute instance that had nginx, install node, and all that fun stuff. I realized this would be a cool time to see how hard it is to setup Google's Cloud Functions. It turns out there were only a few gotchas, and after a couple hours I had the whole app up and running without any servers to manage! It's truly awesome. This post is covering how I set this up on Google, but most of it will apply to AWS Lambda or whatever Azure is offering 😜

Export your function

This part sounds like a lot of work. How do you export an express app and have it work without a bunch of code changes? It turns out you can do this with a whole one line code change at the very minimum. This is because when you use Cloud functions that are triggered by an HTTP request, the function is passed in a request and response object. Express apps can take these as arguments. Take a look at the following code:

const express = require('express');
const app = express();

// register routes here

app.listen(8888, function() {
  console.log('App listening on port 8888!');
});

A basic express app looks just like this! All we need to do is add exports.app = app; at the bottom. Preferably, you'd remove your app.listen method, but you actually don't have to for things to work.

Apollo

Using Apollo with Cloud functions should be just as trivial. You cannot use subscriptions though. Websockets aren't supported, but I think supporting it is in the works. Export the express app like we did above and use graphqlExpress, you should be good to go!

Supporting Node v6

This is something to take note of. In my app, I'm using Node v8 which supports async-await. This also means it isn't on npm 5, so no lock files either, something to remember as well. I added in babel with the async-to-generator transform and babel-preset-env, so that my code will run on node v6. The big downside out of all of this is that you must commit your compiled babel output to your repo, or optionally upload a zip to Google Cloud any time you want to do a deploy.

First Deploy

It was at this point that I first deployed my app. I went into the interface and created a new function that deployed from a Cloud Source repository. I simply added it as an origin to my git repo and pushed my code to it. Filled in a few fields, and it deployed! Make sure your "function to execute" is the name of your exported app. In our case, we used exports.app.
Screen-Shot-2017-08-26-at-7.27.38-PM-1

Environment Variables

At this point, I realized none of the DB connection details and other environment stuff was there. I use dotenv which loads variables from a .env file. This means you have to commit your .env with your babel output, or as I mentioned before, upload a zip with all of this in it to update your function. So I threw this into my babel output folder and committed it. It's not ideal but everything is inside a private repo anyway. Went back, edited the function, and triggered another deploy.

Stateless auth

Since this app is pretty basic, I wanted to build the auth system as fast as I could. I was using in memory sessions with express-session. After hitting the login endpoint I realized clearly this wouldn't work. There's no shared memory across function executions. It was also helpful seeing all log output in Google's Stack driver.
Screen-Shot-2017-08-26-at-7.31.35-PM-1

I spent about 30 minutes swapping out the session middleware with express-jwt and sent out a token on login using jsonwebtoken. The frontend sends an Authorization header on every request and I was all set! Now I could login.... or so I thought!

Connecting to Cloud SQL

Now on login, I got a different error!
Screen-Shot-2017-08-26-at-7.34.37-PM-1

"How is the connection to MySQL timing out," I thought. I know it worked fine, because locally I could use that SQL instance just fine. A quick google search later, and I found this post. Turns out when Cloud functions first came out earlier this year, you couldn't connect to their own SQL service! At the end of June, undocumented support was added for connecting over a socket. So I added the socket path mentioned in that post to my knex setup:

connection: {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    socketPath: process.env.SOCKET
      ? '/cloudsql/project-name:us-west1:development'
      : null,
  },

Setting SOCKET to true in my env will trigger a connection over this undocumented socket path, that will hopefully be the official way very soon!

I did it!

After fixing all of these things and doing a deploy again, things started working! Overall it wasn't that bad, and would have been easier on AWS. It's really cool now that everything is done. I know that the only part of the architecture that needs to be scaled is my database instance. The actual api logic will scale forever without me touching a thing. Instead of paying $10-20 a month on an api server, it'll be completely free since it's under the 2 million requests per month free tier.

No Custom Domain Support

Yet another reason this feature is in beta. You can't alias your function to your own domain. It's not possible yet. Fortunately for me, this is just a dev environment, and I am hosting a client side React app on Cloud Storage which has a custom domain on it. It makes ajax requests to the cloud function to do everything.

Conclusion

I like the way things are headed. Hosted functions that handle 100% of the server setup is the future.

There's many upsides and the only downsides are temporary. One more downside that I have yet to mention is slow response times. I've noticed most calls are at least 300ms round trip. A compute instance would probably be in the 100ms range for most of these requests, if not faster.

I expect this to only get better though! Hopefully this random story of how I spent a Wednesday night helps someone else attempting to switch or build a new application using hosted functions. As always, leave a comment or hit me up @zachcodes with any feedback!