Skip to main content
If your Cloud SQL instance has no public IP and is only accessible within your VPC, there are several ways to securely connect it to BonData. The right approach depends on your security requirements, data volume, and infrastructure.
Using a different cloud? See AWS RDS or Azure SQL.

GCS + Cloud Functions

Self-service setup with Terraform

Tunnel Agent

Lightweight agent in your VPC

Private Service Connect

Private endpoint, no public internet

VPC Peering

Direct network link between VPCs

Cloud VPN

Encrypted tunnel over the internet

Cloud Interconnect

Dedicated physical connection
Not sure which option is right for you? The GCS + Cloud Functions approach works for most teams and you can set it up entirely on your own. For all other options, reach out to our team — we’ll help you evaluate your setup and find the best path forward.

Option 1: Export to GCS via Cloud Functions

RecommendedSelf-service
A Cloud Function runs inside your VPC via a Serverless VPC Access connector, queries Cloud SQL, converts results to Parquet, and writes them to GCS. BonData reads from GCS via its native S3-compatible integration or Google Cloud Storage.
Cloud SQL (private) ──▶ Cloud Function (VPC connector) ──▶ GCS bucket ──▶ BonData

                            Cloud Scheduler (cron)
Why this approach works best for most teams:
  • No firewall changes — the function connects via your VPC’s private network
  • Zero DB performance impact — queries run on your schedule
  • Database credentials never leave your GCP project
  • Fully self-service — no coordination with BonData needed

Deploy with Terraform

Create a bondata-cloudsql-export.tf file and fill in the variables at the top. This provisions the GCS bucket, Cloud Function, VPC connector, Cloud Scheduler job, and IAM in one apply.
Store db_password in Google Secret Manager and reference it via TF_VAR_db_password to avoid committing secrets.
# ──────────────────────────────────────────────
# Variables — fill these in
# ──────────────────────────────────────────────
variable "project_id"           { description = "GCP project ID" }
variable "region"               { default = "us-central1" }
variable "vpc_network"          { description = "VPC network name (e.g. default)" }
variable "vpc_connector_cidr"   { default = "10.8.0.0/28" description = "Unused /28 CIDR for the VPC connector" }
variable "db_host"              { description = "Cloud SQL private IP" }
variable "db_port"              { default = "5432" }
variable "db_name"              { description = "Database name" }
variable "db_user"              { description = "Database user" }
variable "db_password"          { sensitive = true }
variable "tables"               { default = "public.users,public.orders" description = "Comma-separated tables" }
variable "schedule"             { default = "0 * * * *" description = "Cron schedule (default: every hour)" }
variable "bucket_name"          { default = "bondata-cloudsql-exports" }

provider "google" {
  project = var.project_id
  region  = var.region
}

# ──────────────────────────────────────────────
# Enable required APIs
# ──────────────────────────────────────────────
resource "google_project_service" "apis" {
  for_each = toset([
    "cloudfunctions.googleapis.com",
    "cloudbuild.googleapis.com",
    "cloudscheduler.googleapis.com",
    "vpcaccess.googleapis.com",
    "run.googleapis.com",
  ])
  service            = each.value
  disable_on_destroy = false
}

# ──────────────────────────────────────────────
# GCS bucket
# ──────────────────────────────────────────────
resource "google_storage_bucket" "export" {
  name                        = var.bucket_name
  location                    = var.region
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
}

# ──────────────────────────────────────────────
# Serverless VPC Access connector
# ──────────────────────────────────────────────
resource "google_vpc_access_connector" "connector" {
  name          = "bondata-export"
  region        = var.region
  network       = var.vpc_network
  ip_cidr_range = var.vpc_connector_cidr

  depends_on = [google_project_service.apis]
}

# ──────────────────────────────────────────────
# Service account
# ──────────────────────────────────────────────
resource "google_service_account" "function" {
  account_id   = "bondata-export-fn"
  display_name = "BonData Cloud SQL Export"
}

resource "google_storage_bucket_iam_member" "writer" {
  bucket = google_storage_bucket.export.name
  role   = "roles/storage.objectCreator"
  member = "serviceAccount:${google_service_account.function.email}"
}

resource "google_project_iam_member" "log_writer" {
  project = var.project_id
  role    = "roles/logging.logWriter"
  member  = "serviceAccount:${google_service_account.function.email}"
}

# ──────────────────────────────────────────────
# Cloud Function source code
# ──────────────────────────────────────────────
data "archive_file" "function" {
  type        = "zip"
  output_path = "${path.module}/function.zip"

  source {
    content  = <<-PYTHON
import os, io, json, logging
from datetime import datetime, timezone
from google.cloud import storage
import psycopg2, pyarrow as pa, pyarrow.parquet as pq

logger = logging.getLogger(__name__)

DB = dict(host=os.environ["DB_HOST"], port=int(os.environ.get("DB_PORT","5432")),
          dbname=os.environ["DB_NAME"], user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"])
BUCKET  = os.environ["GCS_BUCKET"]
PREFIX  = os.environ.get("GCS_PREFIX", "cloudsql-exports")
TABLES  = [t.strip() for t in os.environ["TABLES"].split(",")]
CHUNK   = int(os.environ.get("CHUNK_SIZE", "50000"))
gcs = storage.Client()

def export_table(cur, table, ts):
    safe = table.replace('"','').replace('.','__')
    cur.execute(f"SELECT * FROM {table} LIMIT 0")
    cols = [d[0] for d in cur.description]
    cur.execute(f"DECLARE _c CURSOR FOR SELECT * FROM {table}")
    part, total = 0, 0
    bucket = gcs.bucket(BUCKET)
    while True:
        cur.execute(f"FETCH {CHUNK} FROM _c")
        rows = cur.fetchall()
        if not rows: break
        tbl = pa.table({c: [r[i] for r in rows] for i,c in enumerate(cols)})
        buf = io.BytesIO()
        pq.write_table(tbl, buf); buf.seek(0)
        blob = bucket.blob(f"{PREFIX}/{safe}/dt={ts}/part-{part:05d}.parquet")
        blob.upload_from_file(buf, content_type="application/octet-stream")
        total += len(rows); part += 1
    cur.execute("CLOSE _c")
    return total

def handler(request):
    ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")
    conn = psycopg2.connect(**DB)
    try:
        conn.autocommit = False; cur = conn.cursor()
        res = {}
        for t in TABLES:
            try: res[t] = export_table(cur, t, ts)
            except Exception as e: logger.error(f"{t}: {e}"); res[t] = str(e); conn.rollback()
        conn.commit()
    finally: conn.close()
    logger.info(json.dumps(res))
    return json.dumps({"results": res}), 200
    PYTHON
    filename = "main.py"
  }

  source {
    content  = <<-REQS
functions-framework==3.*
psycopg2-binary==2.9.9
pyarrow==15.0.0
google-cloud-storage==2.*
    REQS
    filename = "requirements.txt"
  }
}

resource "google_storage_bucket" "source" {
  name                        = "${var.bucket_name}-fn-source"
  location                    = var.region
  uniform_bucket_level_access = true
}

resource "google_storage_bucket_object" "source" {
  name   = "function-${data.archive_file.function.output_md5}.zip"
  bucket = google_storage_bucket.source.name
  source = data.archive_file.function.output_path
}

# ──────────────────────────────────────────────
# Cloud Function (2nd gen)
# ──────────────────────────────────────────────
resource "google_cloudfunctions2_function" "export" {
  name     = "bondata-cloudsql-export"
  location = var.region

  build_config {
    runtime     = "python312"
    entry_point = "handler"
    source {
      storage_source {
        bucket = google_storage_bucket.source.name
        object = google_storage_bucket_object.source.name
      }
    }
  }

  service_config {
    max_instance_count    = 1
    available_memory      = "512Mi"
    timeout_seconds       = 300
    service_account_email = google_service_account.function.email

    vpc_connector                 = google_vpc_access_connector.connector.id
    vpc_connector_egress_settings = "PRIVATE_RANGES_ONLY"

    environment_variables = {
      DB_HOST    = var.db_host
      DB_PORT    = var.db_port
      DB_NAME    = var.db_name
      DB_USER    = var.db_user
      DB_PASSWORD = var.db_password
      GCS_BUCKET = google_storage_bucket.export.name
      TABLES     = var.tables
    }
  }

  depends_on = [google_project_service.apis]
}

# ──────────────────────────────────────────────
# Cloud Scheduler (cron trigger)
# ──────────────────────────────────────────────
resource "google_service_account" "scheduler" {
  account_id   = "bondata-export-scheduler"
  display_name = "BonData Export Scheduler"
}

resource "google_cloudfunctions2_function_iam_member" "invoker" {
  project        = var.project_id
  location       = var.region
  cloud_function = google_cloudfunctions2_function.export.name
  role           = "roles/cloudfunctions.invoker"
  member         = "serviceAccount:${google_service_account.scheduler.email}"
}

resource "google_cloud_run_service_iam_member" "invoker" {
  project  = var.project_id
  location = var.region
  service  = google_cloudfunctions2_function.export.name
  role     = "roles/run.invoker"
  member   = "serviceAccount:${google_service_account.scheduler.email}"
}

resource "google_cloud_scheduler_job" "trigger" {
  name     = "bondata-cloudsql-export"
  region   = var.region
  schedule = var.schedule

  http_target {
    uri         = google_cloudfunctions2_function.export.url
    http_method = "POST"
    oidc_token {
      service_account_email = google_service_account.scheduler.email
    }
  }

  depends_on = [google_project_service.apis]
}

# ──────────────────────────────────────────────
# Outputs
# ──────────────────────────────────────────────
output "bucket"        { value = google_storage_bucket.export.name }
output "function_url"  { value = google_cloudfunctions2_function.export.url }

Deploy

terraform init
terraform apply \
  -var="project_id=my-gcp-project" \
  -var="vpc_network=default" \
  -var="db_host=10.0.0.3" \
  -var="db_name=production" \
  -var="db_user=bondata_user" \
  -var="db_password=CHANGEME" \
  -var="tables=public.users,public.orders"

Connect GCS to BonData

Once data is flowing, connect BonData to the bucket:
  1. In BonData, go to IntegrationsAdd IntegrationAmazon S3
  2. Use GCS interoperability keys (S3-compatible) with the endpoint https://storage.googleapis.com
  3. Enter your bucket name and the prefix (default: cloudsql-exports)
GCS is S3-compatible. Generate HMAC keys in the Cloud Storage SettingsInteroperability tab, then use them as Access Key ID / Secret Access Key in the BonData S3 integration.

Option 2: BonData Tunnel Agent

A lightweight Docker container that runs inside your VPC and creates a secure outbound tunnel to BonData. Once running, BonData can query your database directly through the encrypted connection — no inbound firewall rules, no VPN, no public exposure.
┌──────────────────────────────────────────────┐
│               Your GCP VPC                   │
│                                              │
│  ┌───────────┐       ┌────────────────┐     │
│  │ Cloud SQL │◀──────│ BonData Tunnel │─────┼──▶ BonData Cloud (port 443 outbound)
│  │ (private) │       │    Agent       │     │
│  └───────────┘       └────────────────┘     │
│                                              │
└──────────────────────────────────────────────┘
Best for: Teams that need real-time query access with minimal infrastructure changes. The agent only requires outbound HTTPS (port 443) and can run on any Docker host — Compute Engine, GKE, or Cloud Run. Database credentials stay in your environment and all traffic is encrypted end-to-end.

Get started with the Tunnel Agent

Contact our team to provision your tunnel token and walk through deployment for your environment.

Option 3: Private Service Connect

Google Private Service Connect creates a private endpoint in your VPC that routes traffic to BonData without it ever crossing the public internet. Traffic stays entirely within the Google network. Best for: Organizations with strict compliance requirements (HIPAA, SOC 2) that prohibit any data traversal over the public internet, even when encrypted. Private Service Connect provides the strongest network-level isolation without the complexity of VPC Peering or VPN. How it works:
  • BonData publishes a Private Service Connect service
  • You create a forwarding rule and endpoint in your VPC pointing to that service
  • Your Cloud SQL traffic flows privately through Google’s backbone — no NAT, no public IPs

Set up Private Service Connect

Contact our team to get BonData’s service attachment and configure Private Service Connect for your project.

Option 4: VPC Network Peering

VPC Network Peering creates a direct network route between your VPC and BonData’s VPC, allowing private IP communication across projects. Best for: Teams that want a simple, low-cost network link. VPC Peering on GCP has no per-hour charge, supports full-bandwidth communication, and works across projects and organizations. How it works:
  • A peering connection is established between your VPC network and BonData’s VPC network
  • Routes are automatically exchanged (or custom routes are exported)
  • Your Cloud SQL authorized networks are updated to allow connections from BonData’s IP range
GCP VPC Peering supports cross-project and cross-organization peering. CIDR ranges must not overlap.

Set up VPC Peering

Contact our team to exchange VPC details and coordinate the peering connection.

Option 5: Cloud VPN

Google Cloud VPN creates an encrypted IPsec tunnel over the public internet between your network and BonData’s infrastructure. Best for: Organizations that already have VPN infrastructure or need to connect from on-premises networks. Also useful when VPC Peering isn’t possible due to overlapping CIDR ranges or cross-cloud connectivity needs. How it works:
  • A Cloud VPN gateway is created in your VPC
  • An IPsec tunnel is established between your gateway and BonData’s endpoint
  • All traffic is encrypted and routed through the tunnel
  • HA VPN provides 99.99% availability with dual tunnels

Set up Cloud VPN

Contact our team to exchange gateway details and configure the VPN tunnel.

Option 6: Cloud Interconnect

Google Cloud Interconnect provides a dedicated physical network connection (10 Gbps or 100 Gbps) between your infrastructure and BonData, bypassing the public internet entirely. Best for: Enterprise environments with very high data volumes, strict latency requirements, or regulatory mandates for dedicated connectivity. Dedicated Interconnect provides the most consistent throughput and lowest latency of any option. Partner Interconnect is available for smaller bandwidth needs. How it works:
  • A physical cross-connect is established at a Google colocation facility (Dedicated) or through a supported partner (Partner)
  • A VLAN attachment routes traffic between your network and BonData
  • Traffic never touches the public internet — ideal for large-scale, continuous data sync
Dedicated Interconnect typically takes 2-4 weeks to provision. Partner Interconnect can be faster depending on the provider.

Set up Cloud Interconnect

Contact our team to discuss your throughput requirements and coordinate the connection.