Willem Mulder
Willem Mulder

Reputation: 13994

How can I call a Google Cloud Function from Google App Engine?

I have an App Engine project.

I also have a Google Cloud Function.

And I want to call that Google Cloud Function from the App Engine project. I just can't seem to get that to work.

Yes, if I make the function full public (i.e. set the Cloud Function to 'allow all traffic' and create a rule for 'allUsers' to allow calling the function) it works. But if I limit either of the two settings, it stops working immediately and I get 403's.

The App and Function are in the same project, so I would at least assume that setting the Function to 'allow internal traffic only' should work just fine, provided that I have a rule for 'allUsers' to allow calling the function.

How does that work? How does one generally call a (non-public) Google Cloud Function from Google App Engine?

Upvotes: 5

Views: 1700

Answers (3)

Willem Mulder
Willem Mulder

Reputation: 13994

@GAEfan is correct.

As an addition: I used the official Google Auth library to give me the necessary headers.

const {GoogleAuth} = require('google-auth-library');
// Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc)
// this library will automatically choose the right client based on the environment.
const googleCloudFunctionURL = 'https://europe-west1-project.cloudfunctions.net/function';
(async function() {
  const auth = new GoogleAuth();
  let googleCloudFunctionClient = await auth.getIdTokenClient(googleCloudFunctionURL);
  console.log(await googleCloudFunctionClient.getRequestHeaders(googleCloudFunctionURL));
})();

Upvotes: 1

Puteri
Puteri

Reputation: 3789

As mentioned in the docs, Allow internal traffic only mentions the following:

Only requests from VPC networks in the same project or VPC Service Controls perimeter are allowed. All other requests are rejected.

Please note that since App Engine Standard is a serverless product, it is not part of the VPC and then the requests made from this product are not considered "Internal" calls, actually the calls are made from the Public IPs of the instances and for this reason you get an HTTP 403 error message.

Also using a VPC Serverless Connector won't work since this more a bridge to reach resources in the VPC (like VMs or Memorystore instances) but not a Cloud Function because this is also a Serverless product and it does not have an IP in the VPC.

I think here are three options:

  1. Using App Engine Flex:

    Since App Engine Flex uses VM instances, these instances will be part of the VPC and you'll reach the Function even when setting the "Allow internal traffic only" option.

  2. Use a VM as a proxy:

    You can create a VPC Serverless Connector and assign it to the app in App Engine. Then you can create a VM and reach the function using the VM as a proxy. This is not the best option because of the costs but at the end is an option.

  3. The last option considers that the function can use the Allow All Traffic option:

    You can set some security on the Cloud Function to only allow a particular Service Account and you can use this sample code to authenticate.

    EDITED:

    A good sample of the code for this option was shared by @gaefan in the other answer.

Upvotes: 3

GAEfan
GAEfan

Reputation: 11360

You need an auth header for the ping to the function url. It should look like:

headers = {
    ....
    'Authorization': 'Bearer some-long-hash-token'
}

Here is how to get the token:

import requests
token_response = requests.get(
    'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=' +
    'https://[your zone]-[your app name].cloudfunctions.net/[your function name]', 
    headers={'Metadata-Flavor': 'Google'})
    
return token_response.content.decode("utf-8")

'Allow internal traffic only' does not work as expected. My App Engine app is in the same project as the Functions, and it does not work. I had to turn on 'Allow all traffic', and use the header method.

Example:

def get_access_token():
    import requests
    token_response = requests.get(
        'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=' +
        'https://us-central1-my_app.cloudfunctions.net/my_function', 
        headers={'Metadata-Flavor': 'Google'})
        
    return token_response.content.decode("utf-8")
    
def test():
    url_string = f"https://us-central1-my_app.cloudfunctions.net/my_function?message=it%20worked"
    
    access_token = get_access_token()
    

    print(
        requests.get(url_string, headers={'Authorization': f"Bearer {access_token}"}
    )

Upvotes: 8

Related Questions