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.
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/apiA 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.
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.
We run the Temporal worker on a goroutine to initialise it, and start the Gin server in parallel:
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)
}
}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:
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.
This struct defines the schema of the IAM inputs:
package worker
type IamDetails struct {
ProjectID string `json:"project_id"`
User string `json:"user"`
Role string `json:"role"`
}LoadData unmarshals the JSON body received in the API request into a model:
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
}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.
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
}The Gin handler parses the request and calls the service; the workflow schedules the activity with retry and timeout options:
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 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:
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:
Finally, spin up Temporal (with PostgreSQL, Elasticsearch, admin tools, and the web UI) via Docker Compose:
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: localExport 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 -dThen run the project:
go run main.goYou'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.
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.gitBootLabs 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.
Talk to our platform engineering team — we build durable, workflow-driven automation for cloud access control, provisioning, and operations.