Ruby functions on Google Cloud. A guide for automated testing and deployment
Tags: google-cloud functions serverless gcp continuous integration continuous deployment ci cdLess than a week ago, Google Cloud announced the availability of cloud functions using the ruby programming language. As an ex-ruby developer, I was delighted by this news as I will always have a soft spot in my heart for ruby as it was my first language I learned as a software developer.
For some fun, I wanted to explore how you might build, test and deploy your function code in an automated manner. But I wanted it to do something interesting, not just respond with Hello, world! Right now, as I type this, bitcoin is going through a huge spike in price which has caused a buzz, so why not combine two hot-right-now topics!
Overview
This guide will help you setup a ruby function, test it, and deploy it using free automation tools inside the free-tier from both Google Cloud and Github. If you don’t already have accounts for these services, feel free to sign up or just follow along.
We’ll be deploying (and I’ll include a link to the sample code) a ruby function which gets the current bitcoin price from a public API and returns it to the user.
Getting started
Before we begin, you can either clone the same repository and push it to your own account or you can create the files from scratch. If you want to learn something, try to follow along and enter the code, but the source code is there if you need it, make a mistake or get lost in the guide.
You’ll also need ruby 2.6 or greater installed on your system.
The ruby code
- Create a gemfile and add the following gems:
# Gemfile
source "https://rubygems.org"
gem "functions_framework", "~> 0.7"
gem "json"
group :test, :optional do
gem "minitest"
gem "webmock"
end
-
Create an
app.rb
file and anapp.test.rb
file. -
In order to write our function, we’re going to use the functions framework for readability and simplicity. We can declare our function as follows: First, require the libraries we’ll need for the function.
require "functions_framework"
require "net/http"
require "json"
Second, we’ll declare the function.
FunctionsFramework.http("getPrice") do |request|
uri = URI('https://api.coindesk.com/v1/bpi/currentprice.json')
response = JSON.parse(Net::HTTP.get(uri))
return response["bpi"]["usd"]["rate"]
end
Side note: we don’t have to use the return
statement, but I personally find ruby’s implicit return very strange to
look at, so I have included it here.
-
If we visit the above URL in our browser, we’ll see that we get a JSON response formatted data object. As you can see in the code on line 3, we’re going to need to parse it so that ruby can understand it and access the keys from the hash it will get converted to.
-
Let’s copy that response from our browser to the clipboard.
-
In our test file, we can paste the string representation of the JSON object into our test file as a mocked response from the API to make sure we’re going to pull out the right data to respond to our users of the cloud function.
Here’s what that looks like:
require "minitest/autorun"
require "functions_framework/testing"
require 'webmock/minitest'
class Test < Minitest::Test
include FunctionsFramework::Testing
mocked_response = "{\"time\":{\"updated\":\"Jan 19, 2021 13:22:00 UTC\",\"updatedISO\":\"2021-01-19T13:22:00+00:00\",\"updateduk\":\"Jan 19, 2021 at 13:22 GMT\"},\"disclaimer\":\"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org\",\"chartName\":\"Bitcoin\",\"bpi\":{\"USD\":{\"code\":\"USD\",\"symbol\":\"$\",\"rate\":\"37,036.3183\",\"description\":\"United States Dollar\",\"rate_float\":37036.3183},\"GBP\":{\"code\":\"GBP\",\"symbol\":\"£\",\"rate\":\"27,216.1385\",\"description\":\"British Pound Sterling\",\"rate_float\":27216.1385},\"EUR\":{\"code\":\"EUR\",\"symbol\":\"€\",\"rate\":\"30,542.4443\",\"description\":\"Euro\",\"rate_float\":30542.4443}}}"
WebMock.stub_request(:get, "https://api.coindesk.com/v1/bpi/currentprice.json").
to_return(body: mocked_response, status: 200)
def test_bitcoin_price_ticker
load_temporary "app.rb" do
request = make_get_request("https://localhost")
response = call_http("getPrice", request)
assert_equal 200, response.status
assert_equal "37,036.3183", response.body.join
end
end
end
As we can see from the test file, we’ve got a mocked response which was our actual response when we went to visit the server in our browser. We use webmock to stub out the request and return the string version of the JSON object the API sends over.
Our test asserts that the function will respond with a 200 status code and that the response body will contain the current bitcoin price.
-
Let’s validate everything is working. In your console, run
bundle install
to install all the necessary gems for our function and tests -
Then, run
ruby app.test.rb
in the console and make sure the tests are passing. If they aren’t you may have missed something along the way. -
Tests passing? Great! We can also run our function locally too, to see how it would work should it be deployed to the cloud. To do so, we can just run
bundle exec functions-framework-ruby --target getPrice
. This will start a local server which we can then access via http://localhost:8080 -
Let’s save and commit our ruby code now that we’ve built and tested our function locally.
Automation
Now the fun begins! We’ve written our code and now we want to show it to the world. First thing’s first though, we need to make sure it’s not a case of “works on my machine”!
Let’s build some automation.
-
In the code editor, create the following file:
.github/workflows/test.yaml
-
This file will run our CI at pull request time and also install any gems.
-
Create the following Github Action
name: CI tooling
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-ruby@v1
with:
ruby-version: "2.6"
- run: bundle install && ruby app.test.rb
With our automated tests done, we can now look at automated deployments!
- Again in the code editor, create a
cloudbuild.yaml
file. This is going to make use of GCP’s cloudbuild service which will deploy our function for us. - At this time, we’re just going to have the deployment step, since we’ve already unit tested the application. Update
the
cloudbuild.yaml
file with the following:
steps:
- name: "gcr.io/cloud-builders/gcloud"
args:
- functions
- deploy
- getPrice
- --source=.
- --trigger-http
- --runtime=ruby26
- --region=australia-southeast1
- --allow-unauthenticated
As I am sure you can guess, this will deploy the function in Australia region, and allow anyone to invoke the function
without the need to be authenticated. The other flags describe the runtime to use and where the source code is. Given
it’s just a simple flat directory, we denote this location with the use of a dot .
With the files written and the tests completed, it’s time to wire it up on the cloud. All we need to do is setup a trigger in the cloudbuild console to deploy this function each time a change is detected on the main branch. What this means is that the build will only fire after a pull request is merged (or someone pushes to the main branch), which as we’ve setup earlier will ensure the tests are running before such merges are made.
To setup a trigger, follow the process in the console here: https://console.cloud.google.com/cloud-build/triggers/add
Putting it all together
Ok, with the deployment config set it’s time to test!
Start by making sure all your code is committed and pushed.
Then create a branch, push it and then open a pull request. You should see your tests running (if not instantly wait
about 20 seconds) in the Checks
tab of the PR.
Once the tick appears indicating the tests have passed, merge your code. You’ll then see a build kicking off in the cloud console.
If you get any permissions based errors you may need to assign a special privilege to the cloudbuild service account.
As an example:
Note that this is quite a wide privilege set. I would recommend testing that this solves the permission issue, and then narrow down the scope to only allow the correct permission required to make the change. This is good security habits, and follows the principle of assigning the least amount of privilege to the service account in order for it to accomplish it’s task.
If permissions are all good at the end of the build cycle you’ll be able to see a URL of where you can access your newly built function.
Look in the logs for this:
If we visit https://australia-southeast1-jkpersonal.cloudfunctions.net/getPrice we’ll get a current price on the value of bitcoin.
Conclusion
Congratulations! If you made it this far, you now have a working ruby function deployed completely via automation which is tested too.
Are you a ruby developer? Did you learn something? I love feedback of any type! Please let me know if I helped you or if you can use what you learned here. You can find me on Twitter and Mechanical Rock at this handle or get in touch the old fashion way, via the contact form.
Useful links
- Source code of this work
- See the testing automation pull request
- Cloudbuild deploying functions docs
- Google announcing ruby functions
- Functions framework overview
This post was originally made to JKs Blog. It has been republished here with permission. You can find the original post here