Skip to content

Instantly share code, notes, and snippets.

@alexamies
Last active October 8, 2018 18:44
Show Gist options
  • Save alexamies/f2cda3c2db21dd4603d896a3b9079ab7 to your computer and use it in GitHub Desktop.
Save alexamies/f2cda3c2db21dd4603d896a3b9079ab7 to your computer and use it in GitHub Desktop.
Wrapping Google Cloud Functions HTTP Triggers in Endpoints

Wrapping Google Cloud Functions HTTP Triggers in Endpoints

As the Google Cloud Functions (GCF) ecosystem develops there are many useful examples of functions becoming available. However, it is still kind of hard to lock down the HTTP functions. For example, you may want to add authentication and protect the functions from attacks from the open Internet. On the other hand, Google Cloud Endpoints have been around for a little longer and designed specifically for HTTP access. You can restrict Endpoints with API keys, put them behind behind a load balancer to host on a consistent domain, and protect them with Cloud Armor. This post will demonstrate how to import a Nodejs HTTP Cloud Function into an Endpoints app.

Importing a Cloud Function to App Engine

The App Engine Nodejs runtime uses the Express, a popular web framework that is not especially opinionated and makes it simple to import a GCF function to an App Engine app. The JavaScript module approach used by GCF makes it easy to port code without changes.

Follow the instructions in the GCF Quickstart, including cloning of the GoogleCloudPlatform/nodejs-docs-samples. The Quickstart exports the function helloGET, which writes the 'Hello World' text to the HTTP response. The code for the Quickstart is contained in the directory nodejs-docs-samples/functions/helloworld. Notice that the Quickstart for Node.js in the App Engine is contained in the same GitHub repo.

Starting from the GoogleCloudPlatform directory, copy the GCF files to the relevant App Engine directory:

cp functions/helloworld/index.js appengine/hello-world/standard/.

Make a few changes to the App Engine code to import the function as per this line:

var mygcf = require('./index'); 

and replace the / route with this:

app.get('/', (req, res) => {
  res.status(200);
  mygcf.helloGET(req, res);
  res.end();
});

Install some extra stuff that is not needed by the HTTP trigger, but used elsewhere in the GCF Quickstart, so we do not need to change the GCF file:

cd appengine/hello-world/standard/
npm install -save @google-cloud/debug-agent
npm install -save pug
npm install -save safe-buffer

Try running the app locally

cd appengine/hello-world/standard
npm start

Check that it runs properly locally:

curl http://localhost:8080

Deploy to App Engine Standard:

gcloud app deploy

Check that the app is working properly

gcloud app browse

Importing a Cloud Function to Cloud Endpoints

Let's try the same thing for the App Engine Endpoints example. Follow the instructions in Getting Started with Endpoints on App Engine Flexible Environment. Replace the string YOUR-PROJECT-ID with your project id in the files openapi-appengine.yaml and replace ENDPOINTS-SERVICE-NAME in app.yaml with the full hostname.

Starting from the GoogleCloudPlatform directory, copy the file:

cp functions/helloworld/index.js endpoints/getting-started/.
cd endpoints/getting-started

Make a few changes to the App Engine code to import the function as per this gist. Specifically, add the following line near the top of app.js:

const mygcf = require('./index'); 

Replace the echo route with this implementation:

app.post('/echo', (req, res) => {
  res.status(200).json({ message: mygcf.helloGET(req, res) }).end();
});

Install the extra npm modules

npm install -save @google-cloud/debug-agent
npm install -save pug

Deploy the endpoint service and backend:

gcloud endpoints services deploy openapi-appengine.yaml
gcloud app deploy

Send requests to the API:

PROJECT_ID={Your project}
ENDPOINTS_HOST=${PROJECT_ID}.appspot.com
ENDPOINTS_KEY={Your key}
curl --request POST \
    --header "content-type:application/json" \
    --data '{"message":"hello world"}' \
    "${ENDPOINTS_HOST}/echo?key=${ENDPOINTS_KEY}"

Modifying the Endpoints API

The solution so far leverages the API of the Endpoints interface. That can be readily changed to better match the semantics of the functions you are porting. Replace your openapi-appengine.yaml and app.js files with the versions from the gist and then redeploy the endpoint service and backend:

gcloud endpoints services deploy openapi-appengine.yaml
gcloud app deploy

Send requests to the new APIs

curl --request GET \
    --header "content-type:application/json" \
    "${ENDPOINTS_HOST}/helloget?key=${ENDPOINTS_KEY}"
curl --request POST \
    --header "content-type:application/json" \
    --data '{"name":"Tester"}' \
    "${ENDPOINTS_HOST}/hellohttp?key=${ENDPOINTS_KEY}"

The new Endpoints API now is now more appropriate for the GCF example: a GET request to /helloget with no parameters and a POST request to /hellohttp with a parameter called 'name.'

/**
* Copyright 2017, Google, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// [START app]
'use strict';
// [START setup]
const express = require('express');
const bodyParser = require('body-parser');
const Buffer = require('safe-buffer').Buffer;
var mygcf = require('./index');
const app = express();
app.set('case sensitive routing', true);
app.use(bodyParser.json());
// [END setup]
app.get('/helloget', (req, res) => {
res.status(200).json({ message: mygcf.helloGET(req, res) }).end();
});
app.post('/hellohttp', (req, res) => {
res.status(200).json({ message: mygcf.helloHttp(req, res) }).end();
});
function authInfoHandler (req, res) {
let authUser = { id: 'anonymous' };
const encodedInfo = req.get('X-Endpoint-API-UserInfo');
if (encodedInfo) {
authUser = JSON.parse(Buffer.from(encodedInfo, 'base64'));
}
res.status(200).json(authUser).end();
}
app.get('/auth/info/googlejwt', authInfoHandler);
app.get('/auth/info/googleidtoken', authInfoHandler);
if (module === require.main) {
// [START listen]
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.');
});
// [END listen]
}
// [END app]
module.exports = app;
# [START swagger]
swagger: "2.0"
info:
description: "A simple Google Cloud Endpoints API example."
title: "Endpoints Example"
version: "1.0.0"
host: "YOUR-PROJECT-ID.appspot.com"
# [END swagger]
consumes:
- "application/json"
produces:
- "application/json"
schemes:
- "https"
paths:
"/helloget":
get:
description: "Say hello"
operationId: "helloget"
produces:
- "application/json"
responses:
200:
description: "Say hello"
schema:
$ref: "#/definitions/helloGetMessage"
"/hellohttp":
post:
description: "Hello message with parameter"
operationId: "hellohttp"
produces:
- "application/json"
responses:
200:
description: "Echo"
schema:
$ref: "#/definitions/helloHttpMessage"
parameters:
- description: "Name of person to greet"
in: body
name: name
required: true
schema:
$ref: "#/definitions/hellohttpMessage"
"/auth/info/googlejwt":
get:
description: "Returns the requests' authentication information."
operationId: "auth_info_google_jwt"
produces:
- "application/json"
responses:
200:
description: "Authenication info."
schema:
$ref: "#/definitions/authInfoResponse"
security:
- google_jwt: []
"/auth/info/googleidtoken":
get:
description: "Returns the requests' authentication information."
operationId: "authInfoGoogleIdToken"
produces:
- "application/json"
responses:
200:
description: "Authenication info."
schema:
$ref: "#/definitions/authInfoResponse"
security:
- google_id_token: []
definitions:
helloGetMessage:
type: "object"
helloHttpMessage:
type: "object"
properties:
name:
type: "string"
authInfoResponse:
properties:
id:
type: "string"
email:
type: "string"
# This section requires all requests to any path to require an API key.
security:
- api_key: []
securityDefinitions:
# This section configures basic authentication with an API key.
api_key:
type: "apiKey"
name: "key"
in: "query"
# This section configures authentication using Google API Service Accounts
# to sign a json web token. This is mostly used for server-to-server
# communication.
google_jwt:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
# This must match the 'iss' field in the JWT.
x-google-issuer: "jwt-client.endpoints.sample.google.com"
# Update this with your service account's email address.
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/jwk/YOUR-SERVICE-ACCOUNT-EMAIL"
# This must match the "aud" field in the JWT. You can add multiple
# audiences to accept JWTs from multiple clients.
x-google-audiences: "echo.endpoints.sample.google.com"
# This section configures authentication using Google OAuth2 ID Tokens.
# ID Tokens can be obtained using OAuth2 clients, and can be used to access
# your API on behalf of a particular user.
google_id_token:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
x-google-issuer: "https://accounts.google.com"
x-google-jwks_uri: "https://www.googleapis.com/oauth2/v3/certs"
# Your OAuth2 client's Client ID must be added here. You can add
# multiple client IDs to accept tokens from multiple clients.
x-google-audiences: "YOUR-CLIENT-ID"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment