Pythian Blog: Technical Track

Secure the Connection to Your VPC Network from Serverless Environments in Google Cloud

There might have been various situations where you have needed to connect to services created in your Virtual Private Cloud (VPC) network from a serverless environment such as Cloud Run, Cloud Functions or App Engine.  All those services in your VPC network use private IPs which can not be directly accessed from a serverless environment outside your VPC, and providing a public IP to these services is not a good option from a security/cost standpoint. 

To make a secure connection from serverless environments to private services (e.g. Compute Engine, Cloud Sql), we can use the Google Cloud-provided Serverless VPC connector. Serverless VPC Access allows your serverless environment to send requests to your VPC network using Google Cloud’s internal DNS and internal IP addressing systems. The responses to these requests also use your internal network.

Benefits

  • Requests sent to your VPC network are never exposed to the internet.
  • Communication through Serverless VPC Access can have less latency compared to the internet.

Reference Architecture

Configure Serverless VPC connector

Serverless VPC Access consists of a connector resource that is actually a cluster of VM instances ( f1-micro, e2-micro,e2-standard-4). Larger machine types provide more throughput.  

You can configure the minimum and maximum number of VM instances for this cluster with a  maximum number of instances of 10.

Note: Serverless VPC Access automatically scales out the number of instances in your connector as traffic increases. However, the number of connectors does not scale in. To prevent connectors from scaling out more than desired, set the maximum number of instances to as low number as possible/reasonable. If your connector has scaled out and you need to scale it back down, recreate the connector with the desired number of instances. This link explains the VPC connector throughput as per the machine size and number of instances.

When configuring the Serverless VPC connector, you need to associate the connector to a VPC network and assign an IP range. Below is the example code (in Terraform) for setting up a VPC connector in a GCP project.

Enable the API in the Google project:

resource "google_project_service" "vpcaccess" {
  project             = var.project_id
  service             = "vpcaccess.googleapis.com"
  disable_on_destroy  = false
}

Create the VPC network:

resource "google_compute_network" "custom-vpc" {
  project                 = var.project_id
  name                    = "test-vpc"
  auto_create_subnetworks = false
}

Create a specific subnet for VPC access:

resource "google_compute_subnetwork" "vpc-access-subnet" {
  project       = var.project_id
  name          = "vpc-access-subnet"
  ip_cidr_range = "10.100.1.32/28"
  region        = "us-central1"
  network       = google_compute_network.custom-vpc.id
}

Create VPC connector:

resource "google_vpc_access_connector" "connector" {
  project        = var.project_id
  region         = "us-central1"
  name           = "vpc-conn"
  subnet {
    name = google_compute_subnetwork.vpc-access-subnet.name
  }
  machine_type   = "e2-standard-4"
}

IP Address

You must configure /28 IP CIDR Range, unused inside the same VPC (example 192.168.1.0/28). The IP range must not overlap with any existing Subnet range inside the VPC.

You cannot use a different mask other than /28, and the reason behind it rests in the maximum configuration. You can have up to 10 connector instances; if you require more throughput, you will need to increase the machine type of the instances.

Note: 

  • When you directly use the CIDR range while creating a VPC connector, a new subnet is created, but that won’t be visible in the subnet list from the console and can not be listed using command either, gcloud compute networks subnets list. Thus, it is recommended to first create the subnet and use that subnet while creating the VPC connector for better management of resources.
  • An implicit firewall rule with priority 1000 is created on your VPC network to allow ingress from the connector's subnet or custom IP range to all destinations in the network. The implicit firewall rule is not visible in the Google Cloud console and exists only as long as the associated connector exists.

Configure your serverless environment to use a connector

After creating the serverless VPC connector, you need to configure your serverless environment to use this VPC connector to connect with your VPC network. To specify a connector during deployment, use the --vpc-connector flag. Below is an example of a Cloud Function service in Python. A similar approach can be used for App Engine and Cloud Run. Below is an example of setting up a cloud function. This function is going to provide the IP address for a given hostname. In this case, we should be able to get the IP address of a private compute VM from this cloud function because of serverless VPC access. 

Sample source code for cloud function to get the IP of a hostname created in a private VPC using internal DNS.

from flask import Flask, request, jsonify
import socket

app = Flask(__name__)

def get_ip_address(hostname):
    try:
        ip_address = socket.gethostbyname(hostname)
        return ip_address
    except socket.error as e:
        return None

@app.route('/')
def get_ip(self):
    hostname = request.args.get('hostname', '')
    
    if not hostname:
        return jsonify({'error': 'Hostname parameter is missing'}), 400

    ip_address = get_ip_address(hostname)

    if ip_address:
        return jsonify({'hostname': hostname, 'ip_address': ip_address})
    else:
        return jsonify({'error': 'Failed to retrieve IP address for the given hostname'}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Create the zip archive for the code to be deployed in the cloud function:

resource "google_storage_bucket" "bucket" {
  project  = var.project_id
  name     = "test-bucket-vpc"
  location = "US"
}
data "archive_file" "main" {
  type        = "zip"
  output_path = pathexpand("${var.source_directory}.zip")
  source_dir  = pathexpand(var.source_directory)
}
resource "google_storage_bucket_object" "archive" {
  name                = "${data.archive_file.main.output_md5}-${basename(data.archive_file.main.output_path)}"
  bucket              = google_storage_bucket.bucket.name
  source              = data.archive_file.main.output_path
  content_disposition = "attachment"
  content_type        = "application/zip"
}
resource "google_project_service" "function" {
  project             = var.project_id
  service             = "cloudfunctions.googleapis.com"
  disable_on_destroy  = false
}
resource "google_cloudfunctions_function" "function" {
  project               = var.project_id
  region                = "us-central1"
  name                  = "test-function"
  description           = "My test function"
  runtime               = "python310"

  available_memory_mb   = 128
  source_archive_bucket = google_storage_bucket.bucket.name
  source_archive_object = google_storage_bucket_object.archive.name
  trigger_http          = true
  entry_point           = "get_ip"
  vpc_connector         =  google_vpc_access_connector.connector.id

  depends_on            = [
    google_project_service.function
  ]

}
resource "google_cloudfunctions_function_iam_member" "invoker" {
  project        = google_cloudfunctions_function.function.project
  region         = google_cloudfunctions_function.function.region
  cloud_function = google_cloudfunctions_function.function.name

  role   = "roles/cloudfunctions.invoker"
  member = "allUsers"
}

Configure your serverless environment to use a connector

Now, we can create a test VM, which will be created on a private VPC network. In the Terraform configuration below, we are creating the subnet “demovm-subnet” and creating the VM instance for demovm within the subnet.

resource "google_compute_subnetwork" "demovm-subnet" {
  project       = var.project_id
  name          = "demovm-subnet"
  ip_cidr_range = "10.99.1.0/24"
  region        = "us-central1"
  network       = google_compute_network.custom-vpc.id
}

resource "google_compute_instance" "demovm" {
  project      = var.project_id
  name         = "demovm"
  machine_type = "e2-micro"
  zone         = "us-central1-a"
  boot_disk {
    initialize_params {
      image = "ubuntu-os-cloud/ubuntu-2004-lts"
    }
  }
  network_interface {
    subnetwork = google_compute_subnetwork.demovm-subnet.self_link
  }
}

Test

Now, it's time to test our configuration. If we access the function URL with the hostname as an argument, we should get the IP address details. We are using the FQDN for the hostname, which accesses the VPC-scoped private DNS zone to get the IP of the host.

https:///test-function?hostname=demovm.c..internal

Conclusions

Serverless VPC access is a great way to connect Google-provided services to private network services. There are still some points which should be considered while designing the architecture. 

  1. Serverless VPC access sets up GCP Virtual machines in the backend and can not scale in/down. Hence, the machine size and the number of instances should be considered according to the load to manage the cost.
  2. It's always better to use some sort of automation, such as IAC, to create these services so that they can be destroyed and created quickly when needed for cost management. 
  3. If you are using serverless VPC access to connect to the services in a shared VPC network, there are two ways to do it. The VPC connector can be set up in each service project where the cloud runs or cloud functions are running, or it can be set up in the host project. The decision should be made depending on the requirements such as isolation, security and cost. 

Reference Links: https://cloud.google.com/vpc/docs/serverless-vpc-access

No Comments Yet

Let us know what you think

Subscribe by email