Configure a Cloud Run service to access both an internal Cloud Run service and public Internet

Configure a Cloud Run service to access both an internal Cloud Run service and public Internet

About this codelab

subjectLast updated Feb 14, 2024
account_circleWritten by a Googler

1. Introduction

Overview

To secure network traffic for their services and applications, many organizations use a Virtual Private Cloud (VCP) network on Google Cloud with perimeter controls to prevent data exfiltration. A VPC network is a virtual version of a physical network that is implemented inside of Google's production network. A VPC network provides connectivity for your Compute Engine virtual machine (VM) instances, offers native internal passthrough Network Load Balancers and proxy systems for internal Application Load Balancers, connects to on-premises networks by using Cloud VPN tunnels and VLAN attachments for Cloud Interconnect, and distributes traffic from Google Cloud external load balancers to backends.

Unlike VMs, Cloud Run services are not associated with any particular VPC network by default. This codelab demonstrates how to change ingress (inbound connections) settings such that only traffic coming from a VPC can access a Cloud Run service (e.g. a backend service). In addition, this codelab shows you how to have a second service (e.g. a frontend service) access both the backend Cloud Run service through a VPC and also to continue to have public Internet access.

In this example, the backend Cloud Run service returns hello world. The frontend Cloud Run service provides an input field in the UI to collect a URL. Then the frontend service makes a GET request to that URL (e.g. the backend service), hence making this a service to service request (instead of a browser to service request). When the frontend service can successfully reach the backend, the message hello world is displayed in the browser. Then, you'll see how you can make a call to https://curlmyip.org to retrieve the IP address of your frontend service.

What you'll learn

  • How to allow only traffic from a VPC to your Cloud Run service
  • How to configure egress on a Cloud Run service (e.g. frontend) to communicate with an internal-ingress only Cloud Run service (e.g. backend), while maintaining public Internet access for the frontend service.

2. Setup and Requirements

Prerequisites

  • You are logged into the Cloud Console.
  • You have previously deployed a 2nd gen function. For example, you can follow the deploy a Cloud Function 2nd gen quickstart to get started.

Activate Cloud Shell

  1. From the Cloud Console, click Activate Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

If this is your first time starting Cloud Shell, you're presented with an intermediate screen describing what it is. If you were presented with an intermediate screen, click Continue.

d95252b003979716.png

It should only take a few moments to provision and connect to Cloud Shell.

7833d5e1c5d18f54.png

This virtual machine is loaded with all the development tools needed. It offers a persistent 5 GB home directory and runs in Google Cloud, greatly enhancing network performance and authentication. Much, if not all, of your work in this codelab can be done with a browser.

Once connected to Cloud Shell, you should see that you are authenticated and that the project is set to your project ID.

  1. Run the following command in Cloud Shell to confirm that you are authenticated:
gcloud auth list

Command output

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Run the following command in Cloud Shell to confirm that the gcloud command knows about your project:
gcloud config list project

Command output

[core]
project = <PROJECT_ID>

If it is not, you can set it with this command:

gcloud config set project <PROJECT_ID>

Command output

Updated property [core/project].

3. Create the Cloud Run services

Setup environment variables

You can set environment variables that will be used throughout this codelab.

PROJECT_ID=<YOUR_PROJECT_ID>
REGION=<YOUR_REGION, e.g. us-central1>
FRONTEND=frontend-with-internet
BACKEND=backend
SUBNET_NAME=default

Create the backend Cloud Run service

First, create a directory for the source code and cd into that directory.

mkdir -p egress-private-codelab/frontend-w-internet egress-private-codelab/backend && cd egress-private-codelab/backend

Then, create a `package.json`` file with the following content:

{
    "name": "backend-service",
    "version": "1.0.0",
    "description": "",
    "scripts": {
        "start": "node index.js"
    },
    "dependencies": {
        "express": "^4.18.1"
    }
}

Next, create an index.js source file with the content below. This file contains the entry point for the service and contains the main logic for the app.

const express = require('express');

const app = express();

app.use(express.urlencoded({ extended: true }));

app.get('/', function (req, res) {
    res.send("hello world");
});

const port = parseInt(process.env.PORT) || 8080;
app.listen(port, () => {
    console.log(`helloworld: listening on port ${port}`);
});

Lastly, deploy the Cloud Run service running the following command.

gcloud run deploy $BACKEND --source . --allow-unauthenticated --region $REGION

Create the frontend Cloud Run service

Navigate to the frontend directory

cd ../frontend-w-internet

Then, create a package.json file with the following content:

{
  "name": "frontend",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.6.6",
    "express": "^4.18.2",
    "htmx.org": "^1.9.10"
  }
}

Next, create an index.js source file with the content below. This file contains the entry point for the service and contains the main logic for the app.

const express = require("express");
const app = express();
const port = 8080;
const path = require('path');
const axios = require('axios');

// serve static content (index.html) using
// built-in middleware function in Express 
app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));

// this endpoint receives a URL in the post body
// and then makes a get request to that URL
// results are sent back to the caller
app.post('/callService', async (req, res) => {

    const url = req.body.url;
    let message = "";

    try {
        console.log("url: ", url);
        const response = await axios.get(url);
        message = response.data;

    } catch (error) {
        message = error.message;
        console.error(error.message);
    }

    res.send(`
        ${message}
        <p>
        </p>
    `);
});

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`);
});

Create a public directory for the index.html file

mkdir public
touch public/index.html

And update the index.html to contain the following:

<html>
  <script
    src="https://unpkg.com/htmx.org@1.9.10"
    integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
    crossorigin="anonymous"
  ></script>
  <body>
    <div style="margin-top: 100px; margin-left: 100px">
      <h1>I'm the Request Tester service on the Internet</h1>
      <form hx-trigger="submit" hx-post="/callService" hx-target="#zen">
        <label for="url"> URL:</label>
        <input
          style="width: 308px"
          type="text"
          id="url"
          name="url"
          placeholder="The backend service URL"
          required
        />
        <button hx-indicator="#loading" type="submit">Submit</button>
        <p></p>
        <span class="htmx-indicator" id="loading"> Loading... </span>
        <div id="zen" style="white-space: pre-wrap"></div>
        <p></p>
      </form>
    </div>
  </body>
</html>

Lastly, deploy the Cloud Run service running the following command.

gcloud run deploy $FRONTEND --source . --allow-unauthenticated --region $REGION

Call the Backend Service

In this section, you'll verify that you have successfully deployed two Cloud Run services.

Open the URL of the frontend service in your web browser, e.g. https://frontend-your-hash-uc.a.run.app/

In the textbox, enter the URL for the backend service. Note that this request is routed from the front end Cloud Run instance to the backend Cloud Run service, and not from your browser.

You will see "hello world"

4. Set the Backend Service for internal ingress only

You can run the following gcloud command to incorporate a Cloud Run service into your private network.

gcloud run services update $BACKEND --ingress internal --region $REGION

If you were to try calling the backend service from the frontend service, you would receive a 404 error. The frontend Cloud Run service outgoing connection (or egress) goes out to the Internet first, so Google Cloud does not know the origin of the request.

5. Configure the Frontend Service to access the VPC

In this section, you will configure your frontend Cloud Run service to communicate with your backend service through a VPC.

To do this, you will need to add direct VPC egress to your frontend Cloud Run service to make sure it can reach internal IP addresses on the VPC network. Then, you'll configure egress such that only requests to private IPs will be routed to the VPC. This configuration will allow your frontend to still reach the public Internet. You can learn more in the documentation on receiving requests from other Cloud Run services.

Configure direct VPC egress

First, run this command to use direct VPC egress on your frontend service:

gcloud beta run services update $FRONTEND \
--network=$SUBNET_NAME \
--subnet=$SUBNET_NAME  \
--vpc-egress=private-ranges-only \
--region=$REGION

You can now confirm that your frontend service has access to the VPC:

gcloud beta run services describe $FRONTEND \
--region=$REGION

You should see output similar to

VPC access:
    Network:        default
    Subnet:          default
    Egress:          private-ranges-only

Enable Private Google Access

Next, you will enable Private Google Access on the subnet by running the following command:

gcloud compute networks subnets update $SUBNET_NAME \
--region=$REGION \
--enable-private-ip-google-access

You can verify Private Google Access has been enabled by running this command:

gcloud compute networks subnets describe $SUBNET_NAME \
--region=$REGION \
--format="get(privateIpGoogleAccess)"

Create Cloud DNS zone for run.app URLs

Lastly, create a Cloud DNS zone for run.app URLs such that Google Cloud can treat them as internal IP addresses.

In a previous step when you configured direct VPC egress to private-ranges-only. This means that outbound connections from your frontend service will only go to the VPC network if the destination is an internal IP. However, your backend service uses a run.app URL that resolves to a public IP.

In this step, you'll create a Cloud DNS zone for the run.app URLs to resolve to the private.googleapis.com IP address ranges, which are recognized as internal IP addresses. Now, any requests to these ranges will be routed through your VPC network.

You can do this by: https://cloud.google.com/run/docs/securing/private-networking#from-other-services

# do not include the https:// in your DNS Name
# for example: backend-<hash>-uc.a.run.app
DNS_NAME=<your backend service URL without the https://>

gcloud dns --project=$PROJECT_ID managed-zones create codelab-backend-service \
 --description="" \
 --dns-name="a.run.app." \
 --visibility="private" \
 --networks=$SUBNET_NAME

gcloud dns --project=$PROJECT_ID record-sets create $DNS_NAME. \
--zone="codelab-backend-service" \
 --type="A" \
 --ttl="60" \
--rrdatas="199.36.153.8,199.36.153.9,199.36.153.10,199.36.153.11"

Now when you try to reach the backend service for your website, you will see "hello world" returned.

And when you try to reach the Internet using https://curlmyip.org/, you will see your IP address.

6. Troubleshooting

Here are some possible error messages you may encounter if settings were not configured properly.

  • If you get an error getaddrinfo ENOTFOUND backend-your-hash-uc.a.run.app make sure you did not add the "https://" to the DNS A record
  • If you receive a 404 error when trying to access the backend after configuring the zone, you can either wait for the cache on the global run.app record to expire (e.g. 6 hours) or you can create a new revision (hence clearing the cache) by running the following command: gcloud beta run services update $FRONTEND --network=$SUBNET_NAME --subnet=$SUBNET_NAME --vpc-egress=private-ranges-only --region=$REGION

7. Congratulations!

Congratulations for completing the codelab!

We recommend reviewing the documentation on Private Networking on Cloud Run.

What we've covered

  • How to allow only traffic from a VPC to your Cloud Run service
  • How to configure egress on a Cloud Run service (e.g. frontend) to communicate with an internal-ingress only Cloud Run service (e.g. backend), while maintaining public Internet access for the frontend service.

8. Clean up

To avoid inadvertent charges, (for example, if this Cloud Run servcie is inadvertently invoked more times than your monthly Cloud Run invokement allocation in the free tier), you can either delete the Cloud Run service or delete the project you created in Step 2.

To delete the Cloud Run services, go to the Cloud Run Cloud Console at https://console.cloud.google.com/functions/ and delete the $FRONTEND and $BACKEND services you created in this codelab.

If you choose to delete the entire project, you can go to https://console.cloud.google.com/cloud-resource-manager, select the project you created in Step 2, and choose Delete. If you delete the project, you'll need to change projects in your Cloud SDK. You can view the list of all available projects by running gcloud projects list.