DevOps Blog

Set up your application in GCP, scale it and secure it!

Written by Baptiste Guerin | 07-Sep-2021 12:03:01

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!