Posted on 7 September 2021, updated on 12 May 2023.
Deploying applications to Google Cloud is very easy, especially with Cloudrun. However, people often miss out on what the platform can offer in terms of maintainability and security.
To deploy an application that is both secure and fast while keeping the code as simple and well documented as possible, you should use an API Gateway.
Here’s what an API Gateway can do for you:
- authenticate all users before they reach your backend
- provide a single endpoint for multiple services
- transform a monolith backend into a microservices architecture
- Centralize and manage API versioning
Overview
In this article, we’ll deploy a simple Cloud Run backend that will only be callable from the API Gateway which will authenticate users before transferring the requests to the backend.
We will first create a Cloud Run instance which can only be called by a specific service account. Then we’ll deploy an API Gateway which will be associated with the service account which is allowed to call the Cloud Run backend and which will authenticate users for us. To finish we’ll use a python script to login to firebase and send a request to the Cloud Run backend through the API Gateway.
Setting up a secure CloudRun instance
Creating a CloudRun instance is very quick but it also allows unauthenticated calls to your instance by default, this means it makes your application public. However, most backends require users to be authenticated. Making your CloudRun publicly available means that you - or your developers - will have to authenticate each call made to your CloudRun instance. Moreover, Cloud Run charges you based on the execution time of your application, thus authenticating calls within your Cloud Run will result in a billing increase.
Before you begin you’ll need:
- A GCP project with billing enabled
- A firebase project within your GCP project
- To activate the following APIs
- Cloud Run API
- Service Management API
- API Gateway API
- Service Control API
Let’s deploy our cloud run backend. To do so we’ll use Terraform:
provider "google" {
project = "< project_id>"
region = "europe-west1"
}
provider "google-beta" {
project = "< project_id> "
region = "europe-west1"
}
resource "google_service_account" "service_account_apigw" {
account_id = "apigw-sa"
display_name = "API GW Service Account"
}
resource "google_cloud_run_service" "hello" {
name = "hello"
location = "europe-west1"
template {
spec {
containers {
image = "gcr.io/cloudrun/hello"
}
}
}
}
output "cloudrun_endpoint" {
value = google_cloud_run_service.hello.status[0].url
}
# Only allow the API Gateway service account to call your Cloud Run instance
resource "google_cloud_run_service_iam_member" "public_access" {
provider = google
service = google_cloud_run_service.hello.name
location = google_cloud_run_service.hello.location
project = google_cloud_run_service.hello.project
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.service_account_apigw.email}"
}
To deploy your Cloud Run backend you can run the following commands:
gcloud auth application-default login
terraform init
terraform apply
You’ve just deployed 3 resources:
- A service account (service_account_apigw)
- A cloud run instance (hello)
- A policy that only allows the service account you created to call your Cloud Run instance
By going to the console you can get the public url of your cloud run instance. If you try to load the page in your browser you’ll notice you get a 403 because only the service account you created is allowed to call that instance.
Setting up the API Gateway
Now let’s create our api gateway. To do so you’ll need a physical gateway and a configuration for it. You’ll need to go to firebase and the cloudrun console to obtain the information needed to fill in the swagger configuration.
You’ll first need to generate a firebase admin service account, you can use this url (just put the right project_id) https://console.firebase.google.com/u/1/project/<project-id>/settings/serviceaccounts/admins
Make sure you save the credentials (json file) and the email of the service account.
While you’re in the firebase console you should enable email/password authentication by enabling the Email/Password provider here https://console.firebase.google.com/u/1/project/<project-id>/authentication/providers
Lastly, you can initialize your firebase web app by clicking on “add app” in your firebase project . No need to enable firebase hosting, however, you’ll need to add a field to the json file that’s generated for your: databaseURL: “”
Now let’s deploy our gateway.
swagger.yaml
swagger: '2.0'
info:
title: Example Gateway
description: API Gateway with firebase auth
version: 1.0.0
schemes:
- https
produces:
- application/json
securityDefinitions:
firebase:
authorizationUrl: ''
flow: implicit
type: oauth2
x-google-issuer: "<firebase_admin_serviceaccount_email>"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/<firebase_admin_serviceaccount_email>"
x-google-audiences: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
paths:
/v1/hello:
get:
security:
- firebase: []
description: Hello
operationId: hello
responses:
'200':
description: Success
x-google-backend:
address: '<cloudrun_url>'
gateway.tf
resource "google_api_gateway_api" "api" {
provider = google-beta
api_id = "my-api"
}
resource "google_api_gateway_api_config" "api_cfg" {
provider = google-beta
api = google_api_gateway_api.api.api_id
api_config_id = "config1"
openapi_documents {
document {
path = "spec.yaml"
contents = filebase64("./swagger.yaml")
}
}
lifecycle {
create_before_destroy = false
}
gateway_config {
backend_config {
google_service_account = google_service_account.service_account_apigw.name
}
}
}
resource "google_api_gateway_gateway" "api_gw" {
provider = google-beta
api_config = google_api_gateway_api_config.api_cfg.id
gateway_id = "api-gw"
lifecycle {
ignore_changes = [
api_config
]
}
}
output "apigw_endpoint" {
value = google_api_gateway_gateway.api_gw.default_hostname
}
API config: The API configuration created when you upload an API definition. Each time you upload an API definition, API Gateway creates a new API config. You cannot modify an API Config. If you want to change it you’ll need to create a new config and associate the gateway with that new config. The service account used to call the backends has to be associated with the config (not the gateway!).
Gateway: an envoy-based, high-performance, scalable proxy that hosts the deployed API config. Deploying an API config to a gateway creates the external facing URL that your API clients use to access the API.
Congratulations! You have successfully deployed a simple but yet secure and scalable backend! Now let’s test it.
You can try to request your backend like so:
curl "https://gateway_id-<hash>-uc.gateway.dev/v1/hello" → will return a 401
curl “https://cloudru” → will return a 403
Both methods should give you a 403.
Go to the firebase console and generate credentials for your app:
https://console.firebase.google.com/u/1/project/<project-id>/settings/serviceaccounts/adminsdk
script.py
import os
import pyrebase
import firebase_admin
from firebase_admin import auth
# ===== ADMIN APP FOR MANAGING USER ===== #
# App initialization
firebase_admin_app = firebase_admin.initialize_app()
# Signup => using firebase_admin
def signup():
print("Sign up...")
email = input("Enter email: ")
password=input("Enter password: ")
try:
# More properties for user creation can be found on SDK Admin doc
user = auth.create_user(email=email, password=password)
# Create a custom token using userID after creation
additional_claims = {'profile': "a-user-profile"}
custom_token = auth.create_custom_token(user.uid, additional_claims)
except Exception as e:
print(e)
return
# ===== FIREBASE APP FOR LOGIN USER ===== #
config = {
'apiKey': "<apiKey>",
'authDomain': "<project-id>.firebaseapp.com",
'projectId': "<project-id>",
'storageBucket': "<project-id>.appspot.com",
'messagingSenderId': "<messagingSenderId'>",
'appId': "<appId>",
'databaseURL': ""}
firebase_app = pyrebase.initialize_app(config)
authentication = firebase_app.auth()
# Login => using firebase (not admin)
def login():
print("Log in...")
email=input("Enter email: ")
password=input("Enter password: ")
try:
user = authentication.sign_in_with_email_and_password(email, password)
print("User authenticated")
print("User ID :")
print(user['localId'])
# Create a custom token using userID after authentication (localId = uid)
additional_claims = {'profile': "a-user-profile"}
custom_token = auth.create_custom_token(user["localId"], additional_claims)
print("JWT Token :")
print(custom_token)
except Exception as e:
print("Authentication failed")
print(e)
return
# Main
ans=input("Are you a new user? [y/n]")
if ans == 'n':
login()
elif ans == 'y':
signup()
Now you can use the JWT token you just created to deploy the gateway:
curl --header "Authorization: Bearer <token>" "https://gateway_id-<hash>-uc.gateway.dev/v1/hello"
Congratulations! You have successfully deployed a small yet secure application with an API Gateway that exposes all your routes and authenticates users for you. Now you can modify the image used by CloudRun to put your own REST API!