Google Cloud Pub/Sub managed messaging service for actor communication.

Configuration#

Crossplane Composition Config (XRD for transport configuration):

apiVersion: asya.dev/v1alpha1
kind: PubSubTransport
metadata:
  name: pubsub-default
spec:
  projectId: my-gcp-project
  endpoint: ""  # Optional, for emulator testing
  topics:
    autoCreate: true  # Optional, defaults to true
  subscriptions:
    ackDeadlineSeconds: 300  # Optional, defaults to 300
    messageRetentionDuration: "604800s"  # Optional, defaults to 7 days

Sidecar environment variables (rendered by Crossplane composition):

  • ASYA_TRANSPORT=pubsub
  • ASYA_PUBSUB_PROJECT_ID → from config.projectId
  • ASYA_PUBSUB_ENDPOINT → from config.endpoint (optional, for emulator)

Topic and Subscription Creation#

Crossplane Composition creates Pub/Sub topics and subscriptions automatically when AsyncActor is reconciled.

Topic name: asya-{namespace}-{actor_name}

Subscription name: asya-{namespace}-{actor_name} (same as topic name)

Example: Actor text-processor in namespace default → Topic asya-default-text-processor, Subscription asya-default-text-processor

Service Account Permissions#

Sidecar permissions (via Workload Identity or service account key):

{
  "bindings": [
    {
      "role": "roles/pubsub.publisher",
      "members": [
        "serviceAccount:actor-sa@my-project.iam.gserviceaccount.com"
      ]
    },
    {
      "role": "roles/pubsub.subscriber",
      "members": [
        "serviceAccount:actor-sa@my-project.iam.gserviceaccount.com"
      ]
    }
  ]
}

Required IAM permissions:

  • pubsub.topics.publish
  • pubsub.subscriptions.consume
  • pubsub.subscriptions.get

Crossplane Provider permissions:

{
  "bindings": [
    {
      "role": "roles/pubsub.admin",
      "members": [
        "serviceAccount:crossplane-provider@my-project.iam.gserviceaccount.com"
      ]
    }
  ]
}

KEDA permissions (for autoscaling):

{
  "bindings": [
    {
      "role": "roles/monitoring.viewer",
      "members": [
        "serviceAccount:keda-sa@my-project.iam.gserviceaccount.com"
      ]
    }
  ]
}

KEDA Scaler#

triggers:
  - type: gcp-pubsub
    metadata:
      subscriptionName: asya-default-text-processor
      subscriptionSize: "5"
      credentialsFromEnv: GOOGLE_APPLICATION_CREDENTIALS

Scaling behavior: KEDA polls subscription metrics (undelivered message count) and scales actor replicas based on subscriptionSize target.

Subscription Configuration#

Default subscription settings (configured in Crossplane composition):

Parameter Value Description
ackDeadlineSeconds 300 (5 minutes) Time window to acknowledge message before redelivery
messageRetentionDuration 604800s (7 days) How long unacknowledged messages are retained
expirationPolicy.ttl "" (never) Subscription does not expire due to inactivity

Implementation Details#

Message delivery: Sidecar uses synchronous Pull RPC (not streaming StreamingPull) to receive messages one at a time.

Ack behavior: Ack() sends Acknowledge RPC with ack ID to confirm processing.

Nack behavior: Nack() calls ModifyAckDeadline with ackDeadlineSeconds: 0, making the message immediately available for redelivery.

Topic caching: Sidecar caches pubsub.Topic instances to avoid repeated lookups.

Delayed delivery: Pub/Sub does not support delayed message delivery. SendWithDelay() returns ErrDelayNotSupported.

DLQ Configuration#

(Configuration details TBD)

Pub/Sub supports dead-letter topics for messages that fail repeated delivery attempts. DLQ setup via Crossplane composition is planned but not yet implemented.

Workload Identity Setup#

Recommended for GKE deployments: Use Workload Identity to bind Kubernetes service accounts to GCP service accounts.

# Create GCP service account
gcloud iam service-accounts create actor-sa \
  --project=my-project

# Grant Pub/Sub permissions
gcloud projects add-iam-policy-binding my-project \
  --member="serviceAccount:actor-sa@my-project.iam.gserviceaccount.com" \
  --role="roles/pubsub.publisher"

gcloud projects add-iam-policy-binding my-project \
  --member="serviceAccount:actor-sa@my-project.iam.gserviceaccount.com" \
  --role="roles/pubsub.subscriber"

# Bind to Kubernetes service account
gcloud iam service-accounts add-iam-policy-binding \
  actor-sa@my-project.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:my-project.svc.id.goog[default/actor-service-account]"

# Annotate Kubernetes service account
kubectl annotate serviceaccount actor-service-account \
  -n default \
  iam.gke.io/gcp-service-account=actor-sa@my-project.iam.gserviceaccount.com

AsyncActor must reference the annotated Kubernetes service account:

spec:
  serviceAccountName: actor-service-account

Pub/Sub Emulator (Testing)#

For local testing, use the Pub/Sub emulator:

docker run -p 8085:8085 gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
  gcloud beta emulators pubsub start --host-port=0.0.0.0:8085

Sidecar configuration:

env:
  - name: ASYA_PUBSUB_PROJECT_ID
    value: test-project
  - name: ASYA_PUBSUB_ENDPOINT
    value: pubsub-emulator:8085

Known limitations:

  • Subscription updates fail due to emulator field mask case sensitivity (camelCase vs snake_case)
  • Crossplane composition uses managementPolicies: ["Create", "Observe", "Delete"] when ASYA_PUBSUB_ENDPOINT is set to work around this

Best Practices#

  • Use Workload Identity for pod-level GCP authentication (avoid service account keys)
  • Set appropriate ackDeadlineSeconds longer than expected processing time
  • Monitor subscription metrics (undelivered message count, oldest unacked message age)
  • Use DLQ for poison messages (when DLQ support is added)
  • Deploy topics in the same region as GKE cluster to reduce latency and egress costs
  • Enable message ordering if sequential processing is required (via subscription configuration)

Cost Considerations#

  • Free tier: 10 GB message throughput per month
  • After free tier: $40 per TiB ingress, $40 per TiB egress
  • Subscription storage: $0.27 per GiB-month for retained unacknowledged messages
  • Seeks: $0.01 per seek operation (replay from timestamp)
  • Scale to zero: Subscriptions persist — minimal cost when idle

See: Google Cloud Pub/Sub Pricing

Troubleshooting#

PermissionError: Verify Workload Identity binding and IAM permissions (pubsub.topics.publish, pubsub.subscriptions.consume).

NotFoundError on send: Topic does not exist. Verify Crossplane composition created the topic.

NotFoundError on receive: Subscription does not exist. Verify Crossplane composition created the subscription.

Messages not being acknowledged: Check ackDeadlineSeconds is longer than processing time. Increase if needed.

KEDA not scaling: Verify KEDA service account has monitoring.viewer role. Check subscription metrics in GCP Console.

Emulator connection failed: Verify ASYA_PUBSUB_ENDPOINT is reachable from sidecar pod. Use Kubernetes service name for in-cluster emulator.

Subscription update fails in emulator: Expected behavior — emulator has field mask case sensitivity issue. Crossplane composition skips updates when emulator is detected.