R-Powered Services that are Simple, Scalabale, and Secure
By: Sam Terfa
Tags: Github, Docker, Google Cloud Run, R, API

Photo by Ian Battaglia on Unsplash

tl;dr

R-powered services running on Google Cloud Run can act as webooks, APIs, apps, and even dashboards. Now that Github offers continuous integration and deployment with Cloud Run, publishing and updating these services has never been easier. This post outlines a minimal setup which will get you going. Future posts will cover using secrets securely, making interactive Slack apps, and more.

Overview

R scripts can do many amazing things besides statistical analyses. With the power of the Plumber package, they can back webpages and applications with very little code. Here are a couple custom Google Sheets examples from two previous posts of mine: Using R and Python in Google Sheets Formulas and R and Python Plotting in Google Sheets. It’s crazy what is possible!

There are many options for hosting these services but I have become especially fond of Google Cloud Run because it is easy to use, very inexpensive, and highly scalable. Recently, Google announced the availability of continuous deployment from Github to Cloud Build, which takes code pushes to Github repositories and deploys them to Cloud Run automatically!

The following is the quickest way to get set up to do this. You can fork my Github repo samterfa/r_services if you’d like to have the source repo all set. My plan is to build on this post and cover using Google Secret Manager to pass credentials to your apps securely, creating interactive Slack apps, and more, all powered by R!

Creating a Google Cloud Project

Working in Google Cloud starts at the Google Cloud Console where projects live and are provisioned. Setting up a new project is very simple but does require billing to be set up. I promise, it generally costs me less than a dollar to set up and utilize a Cloud Run project for a month; it’s highly worth it!

Begin by creating a new project using the “Select a project” dropdown in the top left. You can name your project whatever you like.

I like to organize my files locally to mirror the Google Cloud project and Cloud Run services in the following way.

.

├── {Google Cloud Project Name}

| ├──{Cloud Run service 1 name}

| ├── ...

| ├──{Cloud Run service 2 name}

| ├── ...

| ├── ...

Here’s the file structure for samterfa/r_services. It’s a pretty standard set up.

.

├── r_services

| ├── minimal

| ├── Dockerfile

| ├── Plumber.R

| ├── ...

Configuring Cloud Run

Next, we select Cloud Run from the main menu in the upper left. Then create a service.

Choose Cloud Run (fully managed) and select a region near you. Name your service whatever you like.

After clicking Next, we get to the good stuff. We can choose to “continuously deploy new revisions from a source repository”. Selecting that, enabling more APIs, and clicking SET UP WITH CLOUD BUILD allows us to choose Github as our repository provider and choose a repo to sync with.

You can select the branch you’d like to use as the source. I will be using a Dockerfile to instruct Google Cloud Build how to the build my app. If you’ve never used Docker do not worry! You can literally copy my Dockerfile and Cloud Build will know what to do. Given the file structure we’ve set up, you should make sure to set the Dockerfile path to /{service_name}/Dockerfile, and not just Dockerfile.

Save your configuration. Finally, we will “Allow unaunthenticated invocations” for simplicity, and click Create. You should now see some action as Google Cloud sets authentication policies for your service, creates a Github continuous deployment build trigger, builds your container, and deploys it to Cloud Run to begin receiving traffic. If you see the orange message I have below, that means your repo wasn’t prepopulated so the trigger needs to wait for a proper push to Github.

To check on the status of your build at any time, go to the menu in the upper left and select Cloud Build -> History. This will show you any builds in progress.

Configuring Your R Scripts

In order to do something useful with your service, you will need to create/modify an R file named Plumber.R by convention. In it you will set up endpoints for your service which can receive requests such as GET and POST API calls. Below is an example.



#' #' @apiTitle My R Service#' @apiDescription This service runs R scripts on Google Cloud Run.

#* Random Number from Uniform Distribution

#*

#*

#*

#*

function(min = 0, max = 1){



x <- runif(n = 1,

min = as.numeric(min),

max = as.numeric(max))



paste0('<h3>', x, '</h3>')

} # EXAMPLE 2#* Random Number from Uniform Distribution#* @param min Lower limit of the distribution.#* @param max Upper limit of the distribution.#* @get /runif#* @serializer htmlfunction(min = 0, max = 1){x ', x, '')

#* Confirmation Message

#*

#*

function(msg=""){

"My R Service Deployed!"

} # EXAMPLE 1#* Confirmation Message#* @get /testing#* @serializer textfunction(msg=""){"My R Service Deployed!"

@get /testing means {your_service_url}/testing can receive GET requests. Your Cloud Run service URL acts like a base url for these calls. My base url looks like http://minimal...run.app as can be seen below.

Now is a great time to test that pushing to Github indeed updates your service. After pushing your changes to Github, take a look at Cloud Build -> History if you would like to check on the progress of the build. It should only take a couple minutes to deploy the updated service. Then type in {your_service_url}/testing into a browser and see what your fancy service can do!

This result is a bit underwhelming. It didn’t require R at all. But you may realize that we could have used R code to generate the output instead of just returning static text. Let’s add a second example to see something a bit more fancy.



#' #' @apiTitle My R Service#' @apiDescription This service runs scalable R scripts on Google Cloud Run.

#* Random Number from Uniform Distribution

#*

#*

#*

#*

function(min = 0, max = 1){



x <- runif(n = 1,

min = as.numeric(min),

max = as.numeric(max))



paste0('<h3>', x, '</h3>')

} # EXAMPLE 2#* Random Number from Uniform Distribution#* @param min Lower limit of the distribution.#* @param max Upper limit of the distribution.#* @get /runif#* @serializer htmlfunction(min = 0, max = 1){x ', x, '')

#* Confirmation Message

#*

#*

function(msg=""){

"My R Service Deployed!"

} # EXAMPLE 1#* Confirmation Message#* @get /testing#* @serializer textfunction(msg=""){"My R Service Deployed!"

This example takes arguments min and max which are passed in through query parameters. This example also serializes the returned data as html which is pretty cool. Go ahead and update Plumber.R, push to Github, wait a couple of minutes, and navigate to {your_service_url}/runif?min=0&max=1. You should see something like this.

This last example starts to actually look useful. You can make an interactive datatable using the DT package super quickly! Imagine doing some interactive dashboarding with your own curated data instead of iris… again.



#' #' @apiTitle My R Service#' @apiDescription This service runs R scripts on Google Cloud Run.

#* iris datatable

#*

#*

function(){



DT::datatable(iris)

} # EXAMPLE 3#* iris datatable#* @get /iris#* @serializer htmlwidgetfunction(){DT::datatable(iris)

#* Random Number from Uniform Distribution

#*

#*

#*

#*

function(min = 0, max = 1){



x <- runif(n = 1,

min = as.numeric(min),

max = as.numeric(max))



paste0('<h3>', x, '</h3>')

} # EXAMPLE 2#* Random Number from Uniform Distribution#* @param min Lower limit of the distribution.#* @param max Upper limit of the distribution.#* @get /runif#* @serializer htmlfunction(min = 0, max = 1){x ', x, '')

#* Confirmation Message

#*

#*

function(msg=""){

"My R Service Deployed!"

} # EXAMPLE 1#* Confirmation Message#* @get /testing#* @serializer textfunction(msg=""){"My R Service Deployed!"

What about Dockerfile?

What did that Dockerfile do anyway? A Dockerfile is simply a set of instructions for Docker to follow to build an image (our app). Let’s take a look.

FROM rocker/tidyverse #### Install CRAN or Github packages not in rocker/tidyverse.

RUN install2.r plumber # Add more packages separated by spaces. # RUN installGithub.r # Uncomment to add Github packages. #### Copies the files in this directory to files in your container.

COPY [".", "./"] #### This starts your R-powered service.

ENTRYPOINT ["Rscript", "-e", "pr <- plumber::plumb(commandArgs()[9]); pr$run(host='0.0.0.0', port=as.numeric(Sys.getenv('PORT')), swagger = F)"]

CMD ["Plumber.R"]

From the comments it should be relatively clear what this file does. It instructs docker to pull a template image with the Tidyverse preloaded. Next, the the plumber package is installed since it doesn’t come standard with the Tidyverse. You can also install Github-hosted packages using the installGithub.r command in your Dockerfile. Files are then copied into the app directory, and plumber::plumb() makes the endpoints we defined available for API calls.

There are fancier things we could use to build and deploy to Cloud Run but a Dockerfile is so simple and straightforward; it’s a great place to start.

What’s Next?

Armed with this simple set up which continuously deploys from Github, you are ready to do some pretty interesting things. My next post will go over passing credentials to your app securely using Google Secret Manager. This is vital if your service is going to interact with anything interesting such as Google Sheets or Slack.