Building a Private Dialogflow CX Webhook Using Cloud Functions, Service Directory, and an Internal…
Modern conversational applications often need to interact with backend systems that must remain private. If your organization uses VPC…
Building a Private Dialogflow CX Webhook Using Cloud Functions, Service Directory, and an Internal Load Balancer (with Bash & Terraform)
Modern conversational applications often need to interact with backend systems that must remain private. If your organization uses VPC Service Controls, or you simply want to avoid exposing backend endpoints on the public internet, you’ll need a secure architecture for Dialogflow CX webhooks.
Unfortunately, Cloud Functions can’t be called directly by Dialogflow CX inside a VPC-SC perimeter. But you can reach them privately by inserting a lightweight internal routing layer.
This guide walks you through an updated architecture using:
- Dialogflow CX private service routing
- Service Directory
- Internal TCP Load Balancer
- GCE instance running a simple reverse proxy (Nginx)
- Internal-only Cloud Function
Every step includes both Bash (gcloud) and Terraform options.
🧱 Architecture Overview
Dialogflow CX ⟶ Service Directory ⟶ Internal TCP Load Balancer ⟶ GCE Reverse Proxy ⟶ Cloud Function (Internal)
Why this works:
- Dialogflow CX can call internal endpoints resolved via Service Directory.
- The ILB distributes traffic across private backend instances
- A tiny GCE VM (no public IP) proxies all requests to your Cloud Function.
- The Cloud Function is set to internal-only ingress, protecting it from public access.
1. Prerequisites
GCP project with billing enabled
IAM roles:
- Compute Admin
- Service Directory Admin
- Cloud Functions Developer
- Dialogflow API Admin
gcloud CLI, or Terraform installed locally
2. Set Environment Variables (Bash)
export PROJECT_ID="my-dialogflow-secure"
export REGION="europe-west1"
export NETWORK="df-vpc"
export SUBNET="df-subnet"
export ILB_IP="10.10.20.99"
export ZONE="${REGION}-b"
export INSTANCE_NAME="df-proxy"
export INSTANCE_GROUP="df-proxy-ig"
export SERVICE_NAME="df-webhook"
3. Enable Required APIs
Bash
gcloud services enable \
compute.googleapis.com \
servicedirectory.googleapis.com \
dialogflow.googleapis.com \
cloudfunctions.googleapis.com \
vpcaccess.googleapis.com
Terraform
resource "google_project_service" "services" {
for_each = toset([
"compute.googleapis.com",
"servicedirectory.googleapis.com",
"dialogflow.googleapis.com",
"cloudfunctions.googleapis.com",
"vpcaccess.googleapis.com"
])
service = each.value
}
4. Create VPC + Subnet
Bash
gcloud compute networks create $NETWORK --subnet-mode=custom
gcloud compute networks subnets create $SUBNET \
--network=$NETWORK \
--range=10.10.20.0/24 \
--region=$REGION \
--enable-private-ip-google-access
Terraform
resource "google_compute_network" "vpc" {
name = var.network_name
}
resource "google_compute_subnetwork" "subnet" {
name = var.subnet_name
region = var.region
ip_cidr_range = "10.10.20.0/24"
network = google_compute_network.vpc.id
private_ip_google_access = true
}
5. Deploy GCE Proxy Instance (Nginx TCP Proxy)
The VM has no external IP.
Startup Script Example
#! /bin/bash
apt-get update
apt-get install -y nginx
cat <<EOF > /etc/nginx/nginx.conf
events {}
stream {
upstream cloud_fn {
server YOUR_CLOUD_FUNCTION_URL:443;
}
server {
listen 8080;
proxy_pass cloud_fn;
}
}
EOF
systemctl restart nginx
Bash
gcloud compute instances create $INSTANCE_NAME \
--zone=$ZONE \
--machine-type=e2-micro \
--network-interface=network=$NETWORK,subnet=$SUBNET,no-address \
--metadata-from-file startup-script=nginx-startup.sh
Terraform
resource "google_compute_instance" "proxy" {
name = var.instance_name
machine_type = "e2-micro"
zone = var.zone
boot_disk { initialize_params { image = "debian-cloud/debian-12" } }
network_interface {
network = google_compute_network.vpc.id
subnetwork = google_compute_subnetwork.subnet.id
# no external IP = private only
}
metadata = {
startup-script = file("${path.module}/nginx-startup.sh")
}
}
6. Create Internal Load Balancer
Bash
gcloud compute instance-groups unmanaged create $INSTANCE_GROUP \
--zone=$ZONE
gcloud compute instance-groups unmanaged add-instances $INSTANCE_GROUP \
--instances=$INSTANCE_NAME \
--zone=$ZONE
gcloud compute health-checks create tcp df-health --port 8080
gcloud compute backend-services create df-backend \
--load-balancing-scheme=INTERNAL \
--protocol=TCP \
--region=$REGION \
--health-checks=df-health
gcloud compute backend-services add-backend df-backend \
--instance-group=$INSTANCE_GROUP \
--instance-group-zone=$ZONE \
--region=$REGION
gcloud compute forwarding-rules create df-ilb \
--load-balancing-scheme=INTERNAL \
--address=$ILB_IP \
--ports=8080 \
--backend-service=df-backend \
--region=$REGION
Terraform
resource "google_compute_region_backend_service" "backend" {
name = "df-backend"
region = var.region
protocol = "TCP"
load_balancing_scheme = "INTERNAL"
health_checks = [google_compute_health_check.tcp.id]
backend {
group = google_compute_instance_group.proxy.self_link
}
}
7. Register Service Directory Entry
Bash
gcloud service-directory namespaces create webhook-ns --location=$REGION
gcloud service-directory services create $SERVICE_NAME \
--namespace=webhook-ns \
--location=$REGION
gcloud service-directory endpoints create webhook-endpoint \
--service=$SERVICE_NAME \
--namespace=webhook-ns \
--location=$REGION \
--address=$ILB_IP \
--port=8080
Terraform
resource "google_service_directory_namespace" "ns" {
provider = google
namespace = "webhook-ns"
location = var.region
}
resource "google_service_directory_service" "svc" {
name = var.service_name
namespace = google_service_directory_namespace.ns.id
}
resource "google_service_directory_endpoint" "endpoint" {
name = "df-endpoint"
service = google_service_directory_service.svc.id
address = var.ilb_ip
port = 8080
}
8. Deploy Cloud Function (Internal-Only)
Bash
gcloud functions deploy df-webhook-fn \
--region=$REGION \
--runtime=nodejs20 \
--entry-point=webhook \
--source=./function \
--trigger-http \
--no-allow-unauthenticated \
--ingress-settings=internal-only \
--vpc-connector=my-connector
Terraform
resource "google_cloudfunctions2_function" "fn" {
name = "df-webhook-fn"
location = var.region
build_config {
runtime = "nodejs20"
entry_point = "webhook"
source {
storage_source {
bucket = google_storage_bucket.src.name
object = google_storage_bucket_object.code.name
}
}
}
service_config {
ingress_settings = "ALLOW_INTERNAL_ONLY"
vpc_connector = google_vpc_access_connector.connector.id
}
}
9. Configure Dialogflow CX Webhook
In Dialogflow CX Console → Manage → Webhooks:
URL format:
https://REGION-dns.googleapis.com/v1/projects/PROJECT_ID/locations/REGION/namespaces/webhook-ns/services/df-webhook
- Authentication: Dialogflow Service Agent
- Test the flow — requests should route privately through:
Dialogflow → Service Directory → ILB → GCE Proxy → Cloud Function
🎉 Conclusion
With this architecture, you get:
✔ Fully private communication (satisfies VPC-SC) ✔ Flexible backend logic via Cloud Functions ✔ Scalable internal traffic routing ✔ No public IPs, no exposed endpoints
The Bash + Terraform dual approach ensures readers at any skill level can follow and build reproducible infrastructure.
메타데이터
- post_id
- eeb7580f12ff
- slug
- building-a-private-dialogflow-cx-webhook-using-cloud-functions-service-directory-and-an-internal-eeb7580f12ff
- url
- https://medium.com/@bolajiakerele2013/building-a-private-dialogflow-cx-webhook-using-cloud-functions-service-directory-and-an-internal-eeb7580f12ff
- canonical_url
- https://medium.com/@bolajiakerele2013/building-a-private-dialogflow-cx-webhook-using-cloud-functions-service-directory-and-an-internal-eeb7580f12ff
- author_url
- https://medium.com/@bolajiakerele2013
- status
- ok
- fetched_at
- 2026-07-25 15:44:25