Cloud-native deployment is not just about putting containers in the cloud; it is about fully leveraging the platform’s managed capabilities — auto-scaling, load balancing, service discovery, secret management, observability, and more. Google Cloud offers a full spectrum of deployment options from Serverless (Cloud Run) to Kubernetes (GKE). Understanding their differences and applicable scenarios lets you choose the most suitable architecture for your Agent system.
This article dives into Cloud Run and GKE deployment practices, including CI/CD pipelines, secret management, monitoring, alerting, and cost optimization.
Cloud Run Deployment: Serverless Simplicity
Cloud Run is Google Cloud’s fully managed container platform. It abstracts server management so you only need to focus on the container image. For Agent scenarios, Cloud Run’s per-request billing and auto-scaling are very attractive — instances can scale to 0 when there are no requests, and new instances start in milliseconds when requests arrive.
Build and Push the Image
# Option 1: use Cloud Build (recommended, no local Docker needed)
gcloud builds submit \
--tag gcr.io/$PROJECT_ID/my-agent:v1.2.3 \
--build-arg VERSION=v1.2.3 \
.
# Option 2: build locally and push (requires Docker and gcloud auth)
docker build \
--build-arg VERSION=v1.2.3 \
-t gcr.io/$PROJECT_ID/my-agent:v1.2.3 \
-t gcr.io/$PROJECT_ID/my-agent:latest \
.
docker push gcr.io/$PROJECT_ID/my-agent:v1.2.3
docker push gcr.io/$PROJECT_ID/my-agent:latest
# Option 3: use Cloud Build config (supports complex builds)
# cloudbuild.yaml
gcloud builds submit --config cloudbuild.yaml
# cloudbuild.yaml
steps:
# build image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '--build-arg'
- 'VERSION=${_VERSION}'
- '--build-arg'
- 'BUILD_TIME=${_BUILD_TIME}'
- '--build-arg'
- 'GIT_COMMIT=${SHORT_SHA}'
- '-t'
- 'gcr.io/$PROJECT_ID/my-agent:${_VERSION}'
- '-t'
- 'gcr.io/$PROJECT_ID/my-agent:latest'
- '.'
# push image
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/my-agent:${_VERSION}']
# security scan
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: 'bash'
args:
- '-c'
- |
gcloud artifacts docker images scan \
gcr.io/$PROJECT_ID/my-agent:${_VERSION} \
--remote
# deploy to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args:
- 'run'
- 'deploy'
- 'my-agent'
- '--image'
- 'gcr.io/$PROJECT_ID/my-agent:${_VERSION}'
- '--region'
- 'asia-east1'
- '--platform'
- 'managed'
- '--no-traffic' # deploy first, do not receive traffic until verified
substitutions:
_VERSION: v1.2.3
_BUILD_TIME: '2026-05-29T10:00:00Z'
images:
- 'gcr.io/$PROJECT_ID/my-agent:${_VERSION}'
- 'gcr.io/$PROJECT_ID/my-agent:latest'
Cloud Run Deployment Configuration
# basic deployment
gcloud run deploy my-agent \
--image gcr.io/$PROJECT_ID/my-agent:v1.2.3 \
--platform managed \
--region asia-east1 \
--memory 1Gi \
--cpu 1 \
--concurrency 100 \
--max-instances 10 \
--min-instances 1 \
--port 8080 \
--timeout 300 \
--set-env-vars "LOG_LEVEL=info,LOG_FORMAT=json,MAX_CONCURRENT=100" \
--allow-unauthenticated
# deploy with YAML config (recommended for version control)
gcloud run services replace service.yaml
# service.yaml - Cloud Run service configuration
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: my-agent
annotations:
run.googleapis.com/ingress: all # allow all ingress traffic
run.googleapis.com/execution-environment: gen2 # second-generation execution environment
spec:
template:
metadata:
annotations:
# autoscaling config
autoscaling.knative.dev/minScale: "1" # keep at least 1 instance to avoid cold start
autoscaling.knative.dev/maxScale: "20" # max 20 instances
autoscaling.knative.dev/targetConcurrency: "50" # 50 concurrent requests per instance
# connection config
run.googleapis.com/cpu-throttling: "false" # always allocate CPU (good for long connections)
run.googleapis.com/startup-cpu-boost: "true" # boost CPU at startup
# cloud monitoring
run.googleapis.com/execution-environment: gen2
spec:
containerConcurrency: 100 # max concurrent requests per container
timeoutSeconds: 300 # 5-minute request timeout
serviceAccountName: my-agent-sa@$PROJECT_ID.iam.gserviceaccount.com
containers:
- image: gcr.io/$PROJECT_ID/my-agent:v1.2.3
ports:
- containerPort: 8080
env:
- name: LOG_LEVEL
value: "info"
- name: LOG_FORMAT
value: "json"
- name: MAX_CONCURRENT
value: "100"
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: redis-url
key: latest
- name: GOOGLE_API_KEY
valueFrom:
secretKeyRef:
name: google-api-key
key: latest
resources:
limits:
cpu: "2"
memory: "2Gi"
startupProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 6 # must be ready within 30 seconds
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 10
failureThreshold: 3
Secret Management: Secret Manager
Never hard-code secrets in code or environment variables in production. Use Google Secret Manager:
# create secret
echo -n "your-api-key" | gcloud secrets create google-api-key --data-file=-
# create Redis connection string secret
echo -n "redis://10.0.0.3:6379/0" | gcloud secrets create redis-url --data-file=-
# list secret versions
gcloud secrets versions list google-api-key
# update secret
echo -n "new-api-key" | gcloud secrets versions add google-api-key --data-file=-
Reference a secret in Cloud Run:
# Option 1: inject as environment variable
env:
- name: GOOGLE_API_KEY
valueFrom:
secretKeyRef:
name: google-api-key
key: latest # or a specific version
# Option 2: mount as a file (safer, avoids env leakage)
volumeMounts:
- name: secrets
mountPath: /secrets
volumes:
- name: secrets
secret:
secretName: google-api-key
items:
- key: latest
path: api-key.txt
Read from Go code:
import (
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
)
func getSecret(ctx context.Context, name string) (string, error) {
client, err := secretmanager.NewClient(ctx)
if err != nil {
return "", err
}
defer client.Close()
req := &secretmanagerpb.AccessSecretVersionRequest{
Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", projectID, name),
}
result, err := client.AccessSecretVersion(ctx, req)
if err != nil {
return "", err
}
return string(result.Payload.Data), nil
}
// read secrets at initialization
func initSecrets(ctx context.Context) error {
apiKey, err := getSecret(ctx, "google-api-key")
if err != nil {
return fmt.Errorf("failed to get API key: %w", err)
}
os.Setenv("GOOGLE_API_KEY", apiKey)
redisURL, err := getSecret(ctx, "redis-url")
if err != nil {
return fmt.Errorf("failed to get Redis URL: %w", err)
}
os.Setenv("REDIS_URL", redisURL)
return nil
}
Cold Start Optimization
Cold starts are Cloud Run’s biggest challenge, especially for Agent applications that may need to load models or initialize connections:
# 1. keep minimum instances (avoid full cold start)
gcloud run services update my-agent --min-instances 1
# 2. use startupProbe to ensure readiness before receiving traffic
# 3. optimize container image size (smaller starts faster)
# 4. use second-generation execution environment (faster startup)
gcloud run services update my-agent --execution-environment gen2
// optimize startup: lazily initialize non-critical components
func main() {
// 1. start HTTP server first (fast readiness)
server := startServer()
// 2. initialize other components in the background
go func() {
if err := initLLMClient(); err != nil {
log.Printf("failed to init LLM client: %v", err)
}
}()
go func() {
if err := initRedis(); err != nil {
log.Printf("failed to init Redis: %v", err)
}
}()
// 3. wait for signals
// ...
}
GKE Deployment: Full Kubernetes Control
When you need finer control (custom networking, persistent storage, complex scaling strategies), GKE is the better choice.
Create the Cluster
# create a standard cluster
gcloud container clusters create my-cluster \
--zone asia-east1-a \
--machine-type e2-standard-2 \
--num-nodes 3 \
--enable-autoscaling \
--min-nodes 2 \
--max-nodes 10 \
--enable-autorepair \
--enable-autoupgrade \
--disk-size 100GB \
--disk-type pd-ssd \
--workload-pool=$PROJECT_ID.svc.id.goog # enable Workload Identity
# get credentials
gcloud container clusters get-credentials my-cluster --zone asia-east1-a
# verify connection
kubectl get nodes
Deployment Configuration
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: agent-system
labels:
istio-injection: enabled # enable Istio service mesh
---
# k8s/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-agent
namespace: agent-system
annotations:
iam.gke.io/gcp-service-account: my-agent-sa@$PROJECT_ID.iam.gserviceaccount.com
---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-agent
namespace: agent-system
labels:
app: my-agent
version: v1.2.3
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # allow 1 extra Pod during upgrade
maxUnavailable: 0 # keep all Pods available during upgrade
selector:
matchLabels:
app: my-agent
template:
metadata:
labels:
app: my-agent
version: v1.2.3
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: my-agent
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
# affinity: spread across different nodes
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- my-agent
topologyKey: kubernetes.io/hostname
# graceful shutdown
terminationGracePeriodSeconds: 60
containers:
- name: agent
image: gcr.io/$PROJECT_ID/my-agent:v1.2.3
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
env:
- name: PORT
value: "8080"
- name: LOG_LEVEL
value: "info"
- name: LOG_FORMAT
value: "json"
- name: MAX_CONCURRENT
value: "200"
- name: MAX_SESSIONS
value: "50000"
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: agent-secrets
key: redis-url
- name: GOOGLE_API_KEY
valueFrom:
secretKeyRef:
name: agent-secrets
key: google-api-key
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
# health checks
startupProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12 # 60-second startup window
livenessProbe:
httpGet:
path: /health
port: http
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
periodSeconds: 5
failureThreshold: 3
# security context
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# mount tmp directory
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
sizeLimit: 100Mi
---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-agent
namespace: agent-system
labels:
app: my-agent
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
- name: metrics
port: 9090
targetPort: metrics
protocol: TCP
selector:
app: my-agent
---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-agent
namespace: agent-system
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-agent
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-agent
namespace: agent-system
annotations:
kubernetes.io/ingress.class: gce
kubernetes.io/ingress.global-static-ip-name: my-agent-ip
networking.gke.io/managed-certificates: my-agent-cert
networking.gke.io/v1beta1.FrontendConfig: my-agent-frontend-config
spec:
rules:
- host: my-agent.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-agent
port:
number: 80
---
# k8s/certificate.yaml
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: my-agent-cert
namespace: agent-system
spec:
domains:
- my-agent.example.com
---
# k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: agent-secrets
namespace: agent-system
type: Opaque
stringData:
redis-url: "redis://10.0.0.3:6379/0"
google-api-key: ***
Deployment Commands
# apply all configs
kubectl apply -f k8s/
# view deployment status
kubectl get deployments -n agent-system
kubectl get pods -n agent-system -w
# view Pod logs
kubectl logs -f deployment/my-agent -n agent-system
# view HPA status
kubectl get hpa -n agent-system
# manual scaling
kubectl scale deployment my-agent --replicas=5 -n agent-system
# rolling update
kubectl set image deployment/my-agent agent=gcr.io/$PROJECT_ID/my-agent:v1.2.4 -n agent-system
# rollback
kubectl rollout undo deployment/my-agent -n agent-system
# view rollout history
kubectl rollout history deployment/my-agent -n agent-system
Monitoring Configuration: Cloud-Native Observability
Cloud Logging (Structured Logs)
import (
"cloud.google.com/go/logging"
"cloud.google.com/go/logging/logadmin"
)
func setupCloudLogging(ctx context.Context, projectID string) (*logging.Logger, error) {
client, err := logging.NewClient(ctx, projectID)
if err != nil {
return nil, err
}
logger := client.Logger("my-agent", logging.CommonResource(
&monitoredres.MonitoredResource{
Type: "cloud_run_revision",
Labels: map[string]string{
"service_name": "my-agent",
"revision_name": os.Getenv("K_REVISION"),
},
},
))
return logger, nil
}
// use Cloud Logging
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// log structured entry
h.logger.Log(logging.Entry{
Severity: logging.Info,
Payload: map[string]interface{}{
"message": "request received",
"request_id": r.Header.Get("X-Request-ID"),
"user_id": r.Header.Get("X-User-ID"),
"path": r.URL.Path,
"method": r.Method,
"remote_addr": r.RemoteAddr,
},
Trace: r.Header.Get("X-Cloud-Trace-Context"), // correlate with Trace
})
// ... handle request ...
}
Cloud Monitoring (Metrics)
import (
"cloud.google.com/go/monitoring/apiv3/v2/monitoringpb"
"google.golang.org/protobuf/types/known/metricpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func recordMetric(ctx context.Context, client *monitoring.MetricClient, value float64) error {
req := &monitoringpb.CreateTimeSeriesRequest{
Name: fmt.Sprintf("projects/%s", projectID),
TimeSeries: []*monitoringpb.TimeSeries{
{
Metric: &metricpb.Metric{
Type: "custom.googleapis.com/agent/request_latency",
Labels: map[string]string{
"service": "my-agent",
},
},
Resource: &monitoredres.MonitoredResource{
Type: "global",
},
Points: []*monitoringpb.Point{
{
Interval: &monitoringpb.TimeInterval{
EndTime: timestamppb.New(time.Now()),
},
Value: &monitoringpb.TypedValue{
Value: &monitoringpb.TypedValue_DoubleValue{
DoubleValue: value,
},
},
},
},
},
},
}
return client.CreateTimeSeries(ctx, req)
}
Cloud Trace (Distributed Tracing)
import (
"contrib.go.opencensus.io/exporter/stackdriver"
"go.opencensus.io/trace"
)
func initTracing(projectID string) error {
exporter, err := stackdriver.NewExporter(stackdriver.Options{
ProjectID: projectID,
})
if err != nil {
return err
}
trace.RegisterExporter(exporter)
trace.ApplyConfig(trace.Config{
DefaultSampler: trace.ProbabilitySampler(0.1), // 10% sampling rate
})
return nil
}
// use in request handling
func handleRequest(ctx context.Context, req *Request) {
ctx, span := trace.StartSpan(ctx, "agent.handleRequest")
defer span.End()
span.AddAttributes(
trace.StringAttribute("request_id", req.ID),
trace.StringAttribute("user_id", req.UserID),
)
// child span: LLM call
ctx, llmSpan := trace.StartSpan(ctx, "llm.call")
resp, err := llmClient.Generate(ctx, req.Input)
llmSpan.End()
if err != nil {
span.SetStatus(trace.Status{Code: trace.StatusCodeInternal, Message: err.Error()})
return
}
span.AddAttributes(trace.Int64Attribute("response_length", int64(len(resp))))
}
In-Depth Troubleshooting
Q: Cloud Run Cold Start Is Slow
Root cause analysis:
- Container image is too large, taking a long time to pull.
- Application initialization logic is too heavy.
- No min-instances configured; instances start only when requests arrive.
- First-generation execution environment is used.
Optimization plan:
# 1. configure minimum instances (trade cost vs latency)
gcloud run services update my-agent --min-instances 1
# 2. use second-generation execution environment
gcloud run services update my-agent --execution-environment gen2
# 3. optimize image size (use distroless)
# 4. lazily initialize non-critical components
# 5. use startupProbe to ensure readiness
Cost impact:
min-instances=0: zero cost when idle, but cold start latency (5-30s).min-instances=1: always 1 instance running, no cold start, but continuous billing.min-instances=3: suitable for high availability; cost is 3xmin-instances=1.
Q: GKE Pod Cannot Be Scheduled
Diagnosis:
# view Pod status
kubectl describe pod my-agent-xxx -n agent-system
# view events
kubectl get events -n agent-system --sort-by='.lastTimestamp'
# view node resources
kubectl top nodes
kubectl describe node
# view reasons for Pending Pods
kubectl get pods -n agent-system -o wide
Common causes and solutions:
| Cause | Symptom | Solution |
|---|---|---|
| Insufficient resources | Insufficient cpu / Insufficient memory | Scale node pool or lower requests |
| Image pull failure | ImagePullBackOff | Check image name, permissions, and network |
| Node taints | PodToleratesNodeTaints | Add tolerations |
| Affinity conflict | MatchNodeSelector | Adjust affinity config |
| PVC unbound | UnboundImmediatePVC | Check StorageClass |
Q: How to Pass Secrets Securely
Solution comparison:
| Solution | Security | Complexity | Use Case |
|---|---|---|---|
| Environment variables | Low | Low | Development / testing |
| Kubernetes Secret | Medium | Medium | GKE basic |
| Secret Manager | High | Medium | Recommended for Cloud Run |
| Workload Identity | Highest | High | GKE production |
| HashiCorp Vault | Highest | High | Multi-cloud environments |
Workload Identity configuration (GKE best practice):
# 1. create GCP service account
gcloud iam service-accounts create my-agent-sa \
--display-name="My Agent Service Account"
# 2. grant Secret Manager access
gcloud secrets add-iam-policy-binding google-api-key \
--member="serviceAccount:my-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
# 3. bind K8s ServiceAccount to GCP ServiceAccount
gcloud iam service-accounts add-iam-policy-binding \
my-agent-sa@$PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:$PROJECT_ID.svc.id.goog[agent-system/my-agent]"
# 4. configure ServiceAccount in K8s (see serviceaccount.yaml above)
Cost Optimization Strategies
Cloud Run Cost Optimization
# 1. set max instance limit
gcloud run services update my-agent --max-instances 10
# 2. optimize concurrency (each instance handles more requests)
gcloud run services update my-agent --concurrency 100
# 3. right-size memory (do not over-provision)
gcloud run services update my-agent --memory 512Mi
# 4. use request timeout control (prevent long occupation)
gcloud run services update my-agent --timeout 300
GKE Cost Optimization
# use Spot instances (save 60-90%)
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
nodeSelector:
cloud.google.com/gke-spot: "true"
tolerations:
- key: cloud.google.com/gke-spot
operator: Equal
value: "true"
effect: NoSchedule
# enable cluster autoscaling
gcloud container clusters update my-cluster \
--enable-autoscaling \
--min-nodes 1 \
--max-nodes 10
# use e2-medium machine type (best price/performance)
gcloud container node-pools create spot-pool \
--cluster my-cluster \
--machine-type e2-medium \
--spot \
--num-nodes 1 \
--enable-autoscaling \
--min-nodes 0 \
--max-nodes 10
Summary
Module 7 complete. We learned:
- The deep mechanisms of Agent Runtime architecture
- Complete production practices for CLI deployment
- High-availability solutions for Web deployment
- Docker containerization security and optimization
- Cloud Run / GKE cloud deployment strategies
Next, enter Module 8: A2A protocol — how Agents communicate with each other.
← Docker Containerized Deployment | A2A Protocol Introduction →
Want to learn more Go ADK hands-on tips? Follow the “Full Stack Summit - Mengshou Programming” WeChat official account for weekly Go / AI programming updates.
