Challenges in connecting services
When I think about my recent projects, I probably spent half of my time coding new services and the other half in connecting services. Service A calling Service B, or Service C calling an external service and using the result to feed into another Service D.
Connecting services is one of those things that ‘should be easy’ but in reality, it takes a lot of time and effort. You need to figure out a common connection format for services to use, make the connection, parse the results, and pass the results on. I’m not even mentioning error handling, retries and all those production readiness type features that you ultimately need to do.
That’s why I got really excited when I saw the following tweet from Filip Knapik talking about the beta availability of a new product called Workflows:
Need to orchestrate work across Google Cloud products, SaaS API's or other API's?
— Filip Knapik (@FilipKnapik1) August 27, 2020
Try Workflows, a new #GoogleCloud product that does it for you, with serverless scalability and fully managed infrastructure.
Learn more at https://t.co/GZpZtKVFVm#GoogleCloudWorkflows
What is Workflows?
In a nutshell, Workflows allows you to connect ‘things’ together. What kind of things? Pretty much anything that has a public API. You can connect multiple Cloud Functions together, or mix-match Cloud Functions with Cloud Run services with Google Cloud APIs or even external APIs.
In addition to connecting disparate services, Workflows handles parsing and passing values among services. It has built-in error handling and retry policies. And it’s serverless, scales seamlessly with demand with scaling down to zero.
This sounds great, let’s see what we can do with it!
Connect two Cloud Functions
As a first sample, let’s connect two Cloud Functions using Workflows.
First service is randomgen. It generates a random number between 1-100:
import random, json
from flask import jsonify
def randomgen(request):
randomNum = random.randint(1,100)
output = {"random":randomNum}
return jsonify(output)
Deploy to Cloud Functions:
gcloud functions deploy randomgen \
--runtime python37 \
--trigger-http \
--allow-unauthenticated
Second service is multiply. It multiplies the received input by 2:
import random, json
from flask import jsonify
def multiply(request):
request_json = request.get_json()
output = {"multiplied":2*request_json['input']}
return jsonify(output)
Deploy to Cloud Functions:
gcloud functions deploy multiply \
--runtime python37 \
--trigger-http \
--allow-unauthenticated
Connect the two services using a first workflow. Create a workflow.yaml
file:
- randomgenFunction:
call: http.get
args:
url: https://us-central1-workflows-atamel.cloudfunctions.net/randomgen
result: randomgenResult
- multiplyFunction:
call: http.post
args:
url: https://us-central1-workflows-atamel.cloudfunctions.net/multiply
body:
input: ${randomgenResult.body.random}
result: multiplyResult
- returnResult:
return: ${multiplyResult}
Notice how easy it is to call different services with just their url, parse the results of one and feed into the other service.
Deploy the workflow:
gcloud beta workflows deploy workflow \
--source=workflow.yaml
Execute the workflow:
gcloud beta workflows execute workflow
This gives you a command to see the progress of your workflow. In my case the command is like this:
gcloud beta workflows executions describe bc0e0c53-2b77-438a-a2dc-824be577f5f9 \
--workflow workflow
{
"body": {
"multiplied": 34
},
"code": 200,
"headers": {
...
Notice that we got a multiplied
value and status code 200
. Nice, our first
workflow is up and running!
Connect an external service
In the next sample, let’s connect to math.js as an external service in the workflow.
In math.js
, you can evaluate mathematical expressions like this:
curl https://api.mathjs.org/v4/?expr=log(56)
4.02535169073515
This time, we’ll use Cloud Console to update our workflow. Find Workflows
in
Google Cloud Console:
Go to our workflow and click on Definition
tab:
Edit the definition to include math.js
:
- randomgenFunction:
call: http.get
args:
url: https://us-central1-workflows-atamel.cloudfunctions.net/randomgen
result: randomgenResult
- multiplyFunction:
call: http.post
args:
url: https://us-central1-workflows-atamel.cloudfunctions.net/multiply
body:
input: ${randomgenResult.body.random}
result: multiplyResult
- logFunction:
call: http.get
args:
url: https://api.mathjs.org/v4/
query:
expr: ${"log(" + string(multiplyResult.body.multiplied) + ")"}
result: logResult
- returnResult:
return: ${logResult}
This will guide you to edit and deploy the workflow. Once done, click on Execute
to execute the workflow. You’ll be presented with the details of the execution:
Notice the status code 200
.
We just integrated an external service into our workflow, super cool!
Connect an authenticated Cloud Run service
In the last sample, let’s finalize our workflow with a call to a Cloud Run service. To make it more interesting, deploy an internal Cloud Run service. This means that the workflow needs to be authenticated to call the Cloud Run service.
The service
floor
returns math.floor
of the passed in number:
import json
import logging
import os
import math
from flask import Flask, request
app = Flask(__name__)
@app.route('/', methods=['POST'])
def handle_post():
content = json.loads(request.data)
input = float(content['input'])
return f"{math.floor(input)}", 200
if __name__ != '__main__':
# Redirect Flask logs to Gunicorn logs
gunicorn_logger = logging.getLogger('gunicorn.error')
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)
app.logger.info('Service started...')
else:
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
As you might know, Cloud Run deploys containers, so you need a Dockerfile
and
your container needs to bind to 0.0.0.0
and PORT
env variable, hence the
code above.
You can use the following Dockerfile.
Build the container:
export SERVICE_NAME=floor
gcloud builds submit --tag gcr.io/${PROJECT_ID}/${SERVICE_NAME}
Deploy to Cloud Run. Notice the no-allow-unauthenticated
flag. This makes sure
the service only accepts authenticated calls:
gcloud run deploy ${SERVICE_NAME} \
--image gcr.io/${PROJECT_ID}/${SERVICE_NAME} \
--platform managed \
--no-allow-unauthenticated
Service Account for Workflows
Before we can configure Workflows to call the Cloud Run service, we need to create a service account for Workflows to use:
export SERVICE_ACCOUNT=workflows-sa
gcloud iam service-accounts create ${SERVICE_ACCOUNT}
Grant run.invoker
role to the service account. This will allow the service
account to call authenticated Cloud Run services:
export PROJECT_ID=$(gcloud config get-value project)
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member "serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role "roles/run.invoker"
Update the workflow
Update the workflow.yaml
to include the Cloud Run service. Notice how we’re
also including auth
field to make sure Workflows passes in the authentication
token in its calls to the Cloud Run service:
- randomgenFunction:
call: http.get
args:
url: https://us-central1-workflows-atamel.cloudfunctions.net/randomgen
result: randomgenResult
- multiplyFunction:
call: http.post
args:
url: https://us-central1-workflows-atamel.cloudfunctions.net/multiply
body:
input: ${randomgenResult.body.random}
result: multiplyResult
- logFunction:
call: http.get
args:
url: https://api.mathjs.org/v4/
query:
expr: ${"log(" + string(multiplyResult.body.multiplied) + ")"}
result: logResult
- floorFunction:
call: http.post
args:
url: https://floor-wvdg6hhtla-ew.a.run.app
auth:
type: OIDC
body:
input: ${logResult.body}
result: floorResult
- returnResult:
return: ${floorResult}
Update the workflow. This time passing in the service-account
:
gcloud beta workflows deploy workflow \
--source=workflow.yaml \
--service-account=${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com
Execute the workflow:
gcloud beta workflows execute workflow
In a few seconds, you can take a look at the workflow execution to see the result. It should be an integer number returned from the floor function.
Wrap up
Hopefully, this post helped you to have a good idea on what Workflows is. I’m definitely impressed with the potential of this new product. I’ll try out some more complicated use cases in a future blog post.
In the meantime, if you want to check it out, here are some links:
- Workflows overview
- Workflows docs
- Quickstarts
- workflow-samples repo with the code in this post
Feel free to reach out to me on Twitter @meteatamel or read my previous posts on medium/@meteatamel.