A first look at serverless orchestration with Workflows


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:

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:

Workflows menu

Go to our workflow and click on Definition tab:

Workflows definition

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:

Workflows 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:

Feel free to reach out to me on Twitter @meteatamel or read my previous posts on medium/@meteatamel.


See also