Blog & Press Releases
GCP Temporal Go

GCP IAM Binding with Temporal and Go (Gin)

BootLabs Engineering July 2022 8 min read
Orchestrating GCP IAM binding with a Temporal workflow in Go

Gin is a high-performance micro web framework written in Go. In this tutorial we'll use it to expose a REST API that performs a GCP IAM binding — but instead of doing the work inline, we'll orchestrate it through a Temporal workflow so the operation is durable, retryable, and observable. Along the way we cover goroutines, Temporal workers, workflows and activities, and the Google Cloud SDK.

Goals

  • Use Gin to build a RESTful API (performing GCP IAM binding via Go and Temporal).
  • Understand the parts of a Go web application.
  • Understand goroutines and why they're useful.
  • Understand Temporal Workflows and Activities.
  • Interact with the Google Cloud SDK from Go.

Prerequisites

You'll need Go, Temporal, Docker, and Postman (or any API-testing tool) installed. We'll use these packages:

github.com/gin-gonic/gin
github.com/sirupsen/logrus
go.temporal.io/sdk
google.golang.org/api

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. Every Go program runs on at least one goroutine — the main function itself executes on one. You start a new goroutine by prefixing a function call with the go keyword (e.g. go doWork()), and it runs concurrently with the rest of your program. We'll use this to run the Temporal worker alongside the Gin server.

Temporal

A Temporal application is a set of Workflow Executions. Each execution has exclusive access to its local state, runs concurrently with all other executions, and communicates with the outside world via message passing. Workflow Executions are lightweight — a single one consumes few compute resources, and when it's suspended (for example, waiting on a timer or a signal) it consumes none at all. A Temporal application can scale to millions or billions of these executions.

main.go — Server + Worker

We run the Temporal worker on a goroutine to initialise it, and start the Gin server in parallel:

main.go
package main

import (
   "github.com/gin-gonic/gin"
   "personalproject/temporal/worker"
)

func main() {
   r := gin.Default()
   channel1 := make(chan interface{})
   defer func() {
      channel1 <- struct{}{}
   }()
   go iamWorkFlowInitialize(channel1)

   r.POST("/iambinding", worker.IamWorkFlow)
   r.Run()
}

func iamWorkFlowInitialize(channel <-chan interface{}) {
   err := worker.IamWorker.Run(channel)
   if err != nil {
      panic(err)
   }
}

The Temporal Worker

In everyday conversation "Worker" can mean a Worker Program, a Worker Process, or a Worker Entity — Temporal's docs are careful to distinguish them. Here we create a worker, connect it to the Temporal server, and register our workflow and activity on a task queue:

worker/worker.go
package worker

import (
   "go.temporal.io/sdk/client"
   "go.temporal.io/sdk/worker"
   "os"
)

const IAMTASKQUEUE = "IAM_TASK_QUEUE"

var IamWorker worker.Worker = newWorker()

func newWorker() worker.Worker {
   opts := client.Options{
      HostPort: os.Getenv("TEMPORAL_HOSTPORT"),
   }
   c, err := client.NewClient(opts)
   if err != nil {
      panic(err)
   }

   w := worker.New(c, IAMTASKQUEUE, worker.Options{})
   w.RegisterWorkflow(IamBindingGoogle)
   w.RegisterActivity(AddIAMBinding)

   return w
}

The IamBindingGoogle workflow and the AddIAMBinding activity are registered on the worker. A Workflow Definition is the source for a Workflow Execution; an Activity executes a single well-defined action (short or long running) — calling another service, transcoding a file, sending an email, and so on.

Input Model

This struct defines the schema of the IAM inputs:

worker/iam_model.go
package worker

type IamDetails struct {
   ProjectID string `json:"project_id"`
   User      string `json:"user"`
   Role      string `json:"role"`
}

Request Parsing

LoadData unmarshals the JSON body received in the API request into a model:

worker/base.go
package worker

import (
   "bytes"
   "encoding/json"
   "fmt"
   "github.com/gin-gonic/gin"
   "io"
)

func LoadData(c *gin.Context, model interface{}) error {
   var body bytes.Buffer

   if _, err := io.Copy(&body, c.Request.Body); err != nil {
      customErr := fmt.Errorf("response parsing failed %w", err)

      return customErr
   }

   _ = json.Unmarshal(body.Bytes(), &model)

   return nil
}

Workflow Service Layer

This is the service layer for the workflow: an interface and a struct that implements it, which starts the workflow execution on the Temporal client.

worker/workflowsvc.go
package worker

import (
   "context"
   "go.temporal.io/sdk/client"
   "os"
)

var (
   IamSvc IamServiceI = &iamServiceStruct{}
)

type IamServiceI interface {
   IamBindingService(details IamDetails) error
}

type iamServiceStruct struct {
}

type iamServiceModel struct {
   client     client.Client
   workflowID string
}

func (*iamServiceStruct) IamBindingService(details IamDetails) error {
   cr := new(iamServiceModel)
   opts := client.Options{
      HostPort: os.Getenv("TEMPORAL_HOSTPORT"),
   }
   c, err := client.NewClient(opts)
   if err != nil {
      panic(err)
   }

   cr.client = c

   workflowOptions := client.StartWorkflowOptions{
      TaskQueue: IAMTASKQUEUE,
   }

   _, err = cr.client.ExecuteWorkflow(context.Background(), workflowOptions, IamBindingGoogle, details)
   if err != nil {
      return err
   }

   return nil
}

Handler, Workflow & Activity

The Gin handler parses the request and calls the service; the workflow schedules the activity with retry and timeout options:

worker/workflow.go
package worker

import (
   "github.com/gin-gonic/gin"
   "github.com/sirupsen/logrus"
   "go.temporal.io/sdk/temporal"
   "go.temporal.io/sdk/workflow"
   "net/http"
   "time"
)

func IamWorkFlow(c *gin.Context) {
   var details IamDetails
   err := LoadData(c, &details)

   if err != nil {
      logrus.Error(err)
      c.JSON(http.StatusBadRequest, err)

      return
   }

   err = IamSvc.IamBindingService(details)
   if err != nil {
      logrus.Error(err)
      c.JSON(http.StatusBadRequest, err)

      return
   }

   c.JSON(http.StatusOK, err)
}

func IamBindingGoogle(ctx workflow.Context, details IamDetails) (string, error) {

   iamCtx := workflow.WithActivityOptions(
      ctx,
      workflow.ActivityOptions{
         StartToCloseTimeout:    1 * time.Hour,
         ScheduleToCloseTimeout: 1 * time.Hour,
         RetryPolicy: &temporal.RetryPolicy{
            MaximumAttempts: 3,
         },
         TaskQueue: IAMTASKQUEUE,
      })

   err := workflow.ExecuteActivity(iamCtx, AddIAMBinding, details).Get(ctx, nil)

   return "", err
}

The IamBindingGoogle workflow receives the workflow context and the IamDetails (project ID, username, and the role to grant in GCP), then hands them to the activity that performs the binding. ExecuteActivity is given the activity options — StartToCloseTimeout, ScheduleToCloseTimeout, a retry policy, and the task queue.

The GCP IAM Activity

The activity uses the Google Cloud Go SDK to perform the actual IAM binding. It grants the requested role to the user, prints the resulting binding, and then revokes it again — a self-contained demonstration of read-modify-write on an IAM policy:

worker/activity.go
package worker

import (
   "context"
   "flag"
   "fmt"
   "github.com/sirupsen/logrus"
   "google.golang.org/api/cloudresourcemanager/v1"
   "google.golang.org/api/option"
   "os"
   "strings"
   "time"
)

func AddIAMBinding(details IamDetails) error {
   projectID := details.ProjectID
   member := fmt.Sprintf("user:%s", details.User)
   flag.Parse()

   var role string = details.Role
   ctx1 := context.TODO()
   crmService, err := cloudresourcemanager.NewService(ctx1, option.WithCredentialsFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")))
   if err != nil {
      logrus.Errorf("cloudresourcemanager.NewService: %v", err)

      return err
   }

   addBinding(crmService, projectID, member, role)
   policy := getPolicy(crmService, projectID)
   var binding *cloudresourcemanager.Binding
   for _, b := range policy.Bindings {
      if b.Role == role {
         binding = b
         break
      }
   }
   fmt.Println("Role: ", binding.Role)
   fmt.Print("Members: ", strings.Join(binding.Members, ", "))

   removeMember(crmService, projectID, member, role)

   return nil
}

func addBinding(crmService *cloudresourcemanager.Service, projectID, member, role string) {

   policy := getPolicy(crmService, projectID)

   var binding *cloudresourcemanager.Binding
   for _, b := range policy.Bindings {
      if b.Role == role {
         binding = b
         break
      }
   }

   if binding != nil {
      binding.Members = append(binding.Members, member)
   } else {
      binding = &cloudresourcemanager.Binding{
         Role:    role,
         Members: []string{member},
      }
      policy.Bindings = append(policy.Bindings, binding)
   }

   setPolicy(crmService, projectID, policy)
}

func removeMember(crmService *cloudresourcemanager.Service, projectID, member, role string) {

   policy := getPolicy(crmService, projectID)

   var binding *cloudresourcemanager.Binding
   var bindingIndex int
   for i, b := range policy.Bindings {
      if b.Role == role {
         binding = b
         bindingIndex = i
         break
      }
   }

   if len(binding.Members) == 1 {
      last := len(policy.Bindings) - 1
      policy.Bindings[bindingIndex] = policy.Bindings[last]
      policy.Bindings = policy.Bindings[:last]
   } else {
      var memberIndex int
      for i, mm := range binding.Members {
         if mm == member {
            memberIndex = i
         }
      }
      last := len(policy.Bindings[bindingIndex].Members) - 1
      binding.Members[memberIndex] = binding.Members[last]
      binding.Members = binding.Members[:last]
   }

   setPolicy(crmService, projectID, policy)
}

func getPolicy(crmService *cloudresourcemanager.Service, projectID string) *cloudresourcemanager.Policy {

   ctx := context.Background()
   ctx, cancel := context.WithTimeout(ctx, time.Second*10)
   defer cancel()

   request := new(cloudresourcemanager.GetIamPolicyRequest)
   policy, err := crmService.Projects.GetIamPolicy(projectID, request).Do()
   if err != nil {
      logrus.Errorf("Projects.GetIamPolicy: %v", err)
   }

   return policy
}

func setPolicy(crmService *cloudresourcemanager.Service, projectID string, policy *cloudresourcemanager.Policy) {

   ctx := context.Background()
   ctx, cancel := context.WithTimeout(ctx, time.Second*10)
   defer cancel()

   request := new(cloudresourcemanager.SetIamPolicyRequest)
   request.Policy = policy
   policy, err := crmService.Projects.SetIamPolicy(projectID, request).Do()
   if err != nil {
      logrus.Errorf("Projects.SetIamPolicy: %v", err)
   }
}

In short, the activity:

  • Initialises the Resource Manager service, which manages Google Cloud projects.
  • Reads the allow policy for your project.
  • Modifies it by granting the requested role to your Google account.
  • Writes the updated allow policy.
  • Revokes the role again.

Temporal Setup with Docker

Finally, spin up Temporal (with PostgreSQL, Elasticsearch, admin tools, and the web UI) via Docker Compose:

.local/quickstart.yml
version: '3.2'

services:
  elasticsearch:
    container_name: temporal-elasticsearch
    environment:
      - cluster.routing.allocation.disk.threshold_enabled=true
      - cluster.routing.allocation.disk.watermark.low=512mb
      - cluster.routing.allocation.disk.watermark.high=256mb
      - cluster.routing.allocation.disk.watermark.flood_stage=128mb
      - discovery.type=single-node
      - ES_JAVA_OPTS=-Xms100m -Xmx100m
    volumes:
      - esdata:/usr/share/elasticsearch/data:rw
    image: elasticsearch:7.10.1
    networks:
      - temporal-network
    ports:
      - 9200:9200
  postgresql:
    container_name: temporal-postgresql
    environment:
      POSTGRES_PASSWORD: temporal
      POSTGRES_USER: temporal
    image: postgres:9.6
    networks:
      - temporal-network
    ports:
      - 5432:5432
  temporal:
    container_name: temporal
    depends_on:
      - postgresql
      - elasticsearch
    environment:
      - DB=postgresql
      - DB_PORT=5432
      - POSTGRES_USER=temporal
      - POSTGRES_PWD=temporal
      - POSTGRES_SEEDS=postgresql
      - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development_es.yaml
      - ENABLE_ES=true
      - ES_SEEDS=elasticsearch
      - ES_VERSION=v7
    image: temporalio/auto-setup:1.13.1
    networks:
      - temporal-network
    ports:
      - 7233:7233
    volumes:
      - ./dynamicconfig:/etc/temporal/config/dynamicconfig
  temporal-admin-tools:
    container_name: temporal-admin-tools
    depends_on:
      - temporal
    environment:
      - TEMPORAL_CLI_ADDRESS=temporal:7233
    image: temporalio/admin-tools:1.13.1
    networks:
      - temporal-network
    stdin_open: true
    tty: true
  temporal-web:
    container_name: temporal-web
    depends_on:
      - temporal
    environment:
      - TEMPORAL_GRPC_ENDPOINT=temporal:7233
      - TEMPORAL_PERMIT_WRITE_API=true
    image: temporalio/web:1.13.0
    networks:
      - temporal-network
    ports:
      - 8088:8088
networks:
  temporal-network:
    driver: bridge
  intranet:
volumes:
  esdata:
    driver: local

Run the Project

Export the required environment variables:

export TEMPORAL_HOSTPORT=localhost:7233
export GOOGLE_APPLICATION_CREDENTIALS={{path to your service-account key file}}

Start Temporal with Docker Compose:

docker-compose -f .local/quickstart.yml up --build --force-recreate -d

Then run the project:

go run main.go

You'll see the Gin engine start up, the APIs come online, and the Temporal worker running as a thread:

Running Gin server…

The Temporal UI is available at http://localhost:8088. Hit the POST /iambinding endpoint from Postman with a body containing the project ID, user, and role — and it's a success: the workflow completes and the IAM binding is applied (then revoked) in GCP.

Why route this through Temporal?

You could call the GCP IAM API directly from the handler. Routing it through a Temporal workflow buys you durability and automatic retries (the activity retries up to 3 times on failure), full visibility of every execution in the Temporal UI, and a clean separation between the API layer and the long-running cloud operation.

If you get stuck following along, clone the reference repository:

git clone https://github.com/venkateshsuresh/temporal-iamBinding-GCP-workflow.git

BootLabs builds cloud automation and platform engineering solutions across GCP, AWS, and Kubernetes. If you're automating cloud access control or building durable workflow-driven services, our platform team can help.

Automate your cloud operations with confidence.

Talk to our platform engineering team — we build durable, workflow-driven automation for cloud access control, provisioning, and operations.