Migrating your API Gateway for Cloud Run to Apigee

In this article we explore an example migration story from API Gateway or Cloud Endpoints to Apigee. As discussed in this recently published article, all three API management offerings in Google Cloud have a specific target audience and address specific needs. The motivation for this article comes from our experience in supporting customers through an evolution of an API program. Many customers start out by focussing on the exposure of their services using APIs on a relatively straight-forward API gateway and over time expand their perception of APIs with more of an API product mindset. With that mindset many customers start embracing a full lifecycle API management platform such as Apigee. In this article we want to demonstrate what a migration from a relatively simple API Gateway exposure to an Apigee proxy could look like and give practical advice on how to leverage our open source tooling to achieve this.

Migration Goal

API Gateway and Cloud Endpoints both use the same OpenAPI Specification (OAS) extension format to define proxy behavior whilst Apigee uses a custom proxy bundle format. Following the process in this article we can perform automated one-off migrations from API proxy definitions in OAS format to Apigee proxy bundles. Because the Apigee proxy bundle provides a much richer feature set we recommend to use the Apigee proxy bundle format to specify the API functionality going forward. A plain OAS file (without extension annotations) can be used for describing the facade of API products for your API consumers. 

Should you wish to keep the OAS format as the source of truth for your API proxy behavior, you might want to fork the generator script to include your own OAS extensions to map to Apigee functionality that isn’t covered by the x-google-... OAS extensions of Cloud endpoints.

In this article we demonstrate the migration of an API Gateway. As Cloud Endpoints uses the same OAS extensions, the same process is applicable for Cloud Endpoints as well. For our demonstration we first set up an API Gateway that fronts a simple Cloud Run application. To highlight some of the API Gateway capabilities that we want to port to Apigee, the OpenAPI specification that is used in the API Gateway config includes:

  • Multiple paths
  • Different backends for some paths with x-google-backend extensions at the root and path level
  • Authenticated invocations of the backend cloud run service via a GCP service account
  • Authentication of a specific path using a JWT token

If you want to apply the migration approach of this article to your own API Gateway you obviously do not need to create the Cloud Run service and another API Gateway. In this case you can skip ahead to the Migrate to Apigee  section of this article.

Prerequisites for the follow-along example

If you want to follow along with the step by step migration example in this article, you will need the following prerequisites:

  • Apigee X or hybrid organization as the target for the migrated API proxies
  • An admin workstation. If you’re using cloud shell the following tools are recommended (although not necessary, see alternative below) in addition to the tools available by default:
    • xmllint 
      • Purpose: Deploy proxy using DevRel sackmesser
      • Install on Cloudshell or other Debian:
        which xmllint || sudo apt -y install libxml2-utils
      • Alternative: Zip and upload manually or use apigeecli
    • yq 
  • IAM permissions in your project to create the required resources
    • Service account
    • API Gateway
    • Cloud Run
    • Apigee Proxies

Basic Configuration for the Example

We want to start with by configuring the following environment variables:

  • PROJECT_ID - the GCP Project ID you want to use for this example
  • REGION - the GCP region where you want to create your API gateway and Cloud Run service. Please note that API Gateway is only supported in a subset of the GCP regions that are available for Apigee.
export PROJECT_ID="YOUR PROJECT_ID"
export REGION="europe-west1"

We then want to enable the following GCP services:

gcloud services enable apigateway.googleapis.com \
servicemanagement.googleapis.com \
servicecontrol.googleapis.com \
run.googleapis.com --project $PROJECT_ID

Example Cloud Run Setup

As mentioned in the introduction we want to use Cloud Run as an example backend service for our API gateway and then migrate the access to it over to Apigee.

CLOUD_RUN_NAME="cloud-run-test"

gcloud run deploy "$CLOUD_RUN_NAME" \
--image="gcr.io/cloudrun/hello" \
--platform=managed \
--no-allow-unauthenticated \
--region="$REGION" \
--project=$PROJECT_ID

Because we set our Cloud Run to only allow authenticated invocations we can now try to invoke it without and with an ID token:

SERVICE_URL="$(gcloud run services describe "$CLOUD_RUN_NAME" --format 'value(status.url)' --region $REGION --project $PROJECT_ID)"

# This should be "Forbidden" because of missing authentication
curl $SERVICE_URL

# This should be allowed
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" "$SERVICE_URL"

Congratulations, we now have a Cloud Run service that we can front with API management.

Exposing the Cloud Run Service with API Gateway

In API Gateway, unlike Apigee, the API proxy logic is configured through an OpenAPI Specification (aka Swagger). In this scenario we want to create an OAS file that describes the routing logic and exercises some features of API Gateway.

In the specification below we define three paths: requests for /hello and /assets/* are routed to the cloud run service that we just created. The path for /v1/httpbin/* is routed to an external web API at httpbin.org.

Because we previously saw that our Cloud Run backend requires an authenticated invocation we also configured the API gateway to use authentication to call the cloud run service (by implicitly leaving the x-google-backend.disable_auth property at false). For our external service we do not need to authenticate so we set the disable_auth property to true.

For the httpbin service we also demonstrate how the API Gateway can validate an incoming JWT token to authenticate the incoming request.

For convenience you can run the following command to create a OpenAPI specification that references the previously created Cloud Run service via the SERVICE_URL environment variable.

cat <<EOF > openapi2-run.yaml
swagger: '2.0'
info:
  title: test - testing api gateway
  description: Sample API on API Gateway with a Cloud Run backend
  version: 1.0.0
schemes:
- https
produces:
- application/json
x-google-backend:
  address: $SERVICE_URL
paths:
  /assets/{asset}:
    get:
      parameters:
        - in: path
          name: asset
          type: string
          required: true
          description: Name of the asset.
      summary: Assets
      operationId: getAsset
      responses:
        '200':
          description: A successful response
          schema:
            type: string
  /hello:
    get:
      summary: Cloud Run hello world
      operationId: hello
      responses:
        '200':
          description: A successful response
          schema:
            type: string
  /v1/httpbin/{something}:
    get:
      summary: httpbin
      operationId: httpbin
      parameters:
        - in: path
          name: something
          type: string
          required: true
          description: ID some path id
      responses:
        '200':
          description: A successful response
      x-google-backend:
        address: https://httpbin.org/anything
        path_translation: APPEND_PATH_TO_ADDRESS
        disable_auth: true
      security:
        - google_id_token: []
securityDefinitions:
  google_id_token:
    authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth"
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: "https://accounts.google.com"
    x-google-jwks_uri: "https://www.googleapis.com/oauth2/v3/certs"
    x-google-audiences: "demo-aud"
EOF

To allow the API Gateway to invoke the Cloud Run service we need to create a GCP service account:

SA_NAME="cloud-run-invoker"
SA_EMAIL="$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"
gcloud iam service-accounts create "$SA_NAME" \
       --description="SA to invoke Cloud Run" --project "$PROJECT_ID"

gcloud run services add-iam-policy-binding "$CLOUD_RUN_NAME" \
	 --member="serviceAccount:$SA_EMAIL" \
	 --role='roles/run.invoker' \
	 --region=$REGION \
	 --platform=managed --project "$PROJECT_ID"

Following the API Gateway Quickstart we create our API Gateway API resource:

API_ID=test-api
gcloud api-gateway apis create $API_ID --project=$PROJECT_ID

We then create a configuration based on the OAS that we created before

CONFIG_VERSION="demo-config-$(date '+%s')"
gcloud api-gateway api-configs create "$CONFIG_VERSION" --api=test --openapi-spec=openapi2-run.yaml   --project=$PROJECT_ID --backend-auth-service-account=$SA_EMAIL

Once the creation is complete we create a gateway to append the API config to it.

gcloud api-gateway gateways create test-gateway \
  --api=$API_ID --api-config=$CONFIG_VERSION \
  --location=$REGION --project=$PROJECT_ID

# Note: For updating a config on an existing gateway you need to use:

gcloud api-gateway gateways update test-gateway \
 --api-config=$CONFIG_VERSION --api=$API_ID --location=$REGION --project=$PROJECT_ID

That’s it! We can now test our API Gateway:

GW_HOSTNAME=$(gcloud api-gateway gateways describe test-gateway \
  --location=$REGION --project=$PROJECT_ID --format 'value(defaultHostname)')

echo "open https://$GW_HOSTNAME/hello in your browser"

If you copy the link to your browser you should see the awesome Cloud Run unicorn dance party:

strebel_0-1670407410998.png

The JWT validation is slightly less visually appealing but nevertheless interesting. First we want to verify that unauthenticated calls are not permitted:

curl "https://$GW_HOSTNAME/v1/httpbin/something"

This will fail because the security configuration in the OAS requires a Google ID token with an audience of test-aud to be sent in the request header. We can go ahead and create an API client SA and an ID token for it like so:

CLIENT_SA_NAME="api-client"
CLIENT_SA_EMAIL="$CLIENT_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"
gcloud iam service-accounts create "$CLIENT_SA_NAME" \
       --description="SA to call the API" --project "$PROJECT_ID"

gcloud iam service-accounts add-iam-policy-binding "${CLIENT_SA_EMAIL}" \
  --role=roles/iam.serviceAccountTokenCreator \
  --project="${PROJECT_ID}" \
  --member="user:$(gcloud config list account --format "value(core.account)")"

# If this fails, wait for a few seconds to let the permissions propagate
ID_TOKEN="$(gcloud auth print-identity-token --impersonate-service-account=$CLIENT_SA_EMAIL --audiences="demo-aud")"

curl "https://$GW_HOSTNAME/v1/httpbin/something" -H "Authorization: Bearer $ID_TOKEN"

Bonus Section 

Did you notice that the previous API Gateway response contained an Authorization header even though we said do not use the API gateway’s service account for authentication against HTTPbin?

If we investigate the Authentication header value:

curl "https://$GW_HOSTNAME/v1/httpbin/something" -H "Authorization: Bearer $ID_TOKEN" | jq -r '.headers.Authorization| split(" ")[1] | split(".")[1] | @base64d'

We get something like the following response:

{
  "aud": "demo-aud",
  "azp": "...",
  "exp": 1669369707,
  "iat": 1669366107,
  "iss": "https://accounts.google.com",
  "sub": "..."
}

Which confirms that the API Gateway forwarded our client authentication to the backend without us being able to confirm if this is intended behavior or not. Because we just created a GCP service account that can’t do anything other than invoking this very API gateway path, this isn’t a huge deal but something that we want to look out for and leverage the API management capabilities of Apigee to potentially prevent unintended leaking of the verified client credentials as we migrate.

Migrating to Apigee

To migrate the API configuration to Apigee we leverage Apigee’s open source tooling and clone the Apigee DevRel repository locally:

git clone https://github.com/apigee/devrel

Apigee DevRel includes a transformation utility that you can use to turn your OpenAPI specification with the x-google-... extensions into valid Apigee proxies.

Because an Apigee proxy is referenced by a base path we are going to split our existing OAS into 3 different proxies. For each of the proxies we pass the reference to the OAS file together with the name of the proxy and the base path we want to select:

./devrel/tools/endpoints-oas-importer/import-endpoints.sh --oas openapi2-run.yaml -n httpbin-v1 -b /v1/httpbin

./devrel/tools/endpoints-oas-importer/import-endpoints.sh --oas openapi2-run.yaml -n assets-v1 -b /assets

./devrel/tools/endpoints-oas-importer/import-endpoints.sh --oas openapi2-run.yaml -n hello-v1 -b /hello

The generator will create a proxy bundle for each of the three proxies above that you can inspect and optionally augment with your own Apigee policies that were not part of the initial API specification like a message logging policy or error handling. The proxy bundles can be found in the generated folder

ls generated/*                                                                                    

generated/assets-v1:
apiproxy

generated/hello-v1:
apiproxy

generated/httpbin-v1:
apiproxy

To deploy your API Proxy you can configure your Apigee target environment by replacing the following placeholders with your own organization, environment, and hostname:

APGIEE_X_ORG=my-apigee-org
APIGEE_X_ENV=my-apigee-env
APIGEE_X_HOST=test.api.example.com

Create a service account that we can use to authenticate Apigee for invoking the Cloud Run service:

APIGEE_SA_NAME="apigee-cloud-run-invoker"
APIGEE_SA_EMAIL="$APIGEE_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"
gcloud iam service-accounts create "$APIGEE_SA_NAME" \
       --description="Apigee SA to invoke Cloud Run" --project "$APIGEE_X_ORG"

gcloud run services add-iam-policy-binding "$CLOUD_RUN_NAME" \
	 --member="serviceAccount:$APIGEE_SA_EMAIL" \
	 --role='roles/run.invoker' \
	 --region=$REGION \
	 --platform=managed --project "$PROJECT_ID"


Finally we can deploy the generated proxies to Apigee. In this example we use the DevRel’s Apigee Sackmesser utility to deploy the API proxies. If you prefer to upload the proxies using the UI, Apigee API or any other tool you just need to ensure that you associate the service account we just created with the deployment.

APIGEE_TOKEN="$(gcloud auth print-access-token)"

./devrel/tools/apigee-sackmesser/bin/sackmesser deploy --googleapi -d "./generated/httpbin-v1" -t "$APIGEE_TOKEN" -o "$APIGEE_X_ORG" -e "$APIGEE_X_ENV" --deployment-sa $APIGEE_SA_EMAIL

./devrel/tools/apigee-sackmesser/bin/sackmesser deploy --googleapi -d "./generated/assets-v1" -t "$APIGEE_TOKEN" -o "$APIGEE_X_ORG" -e "$APIGEE_X_ENV" --deployment-sa $APIGEE_SA_EMAIL

./devrel/tools/apigee-sackmesser/bin/sackmesser deploy --googleapi -d "./generated/hello-v1" -t "$APIGEE_TOKEN" -o "$APIGEE_X_ORG" -e "$APIGEE_X_ENV" --deployment-sa $APIGEE_SA_EMAIL

We can now test the API calls for the proxies that do not have a client authentication specified. And optionally start a debug session for the assets-v1 proxy in the Apigee UI just for fun.

curl https://$APIGEE_X_HOST/assets/cloud_bg.svg
echo "Open https://$APIGEE_X_HOST/hello in your browser"

If we follow the browser link we see the familiar unicorn dance party:

 

strebel_1-1670407411364.png

as well as the expected API calls in the Apigee debug UI

strebel_2-1670407411025.png

 

For the authenticated calls to the internet facing service we see that the unauthenticated call fails:

curl https://$APIGEE_X_HOST/v1/httpbin/something

Because the generator added an OAuth validation policy based on the security specification in the OAS file. To make a successful call we therefore have to pass an ID token that fullfills the requirements as defined in the security requirement just like we did in the API Gateway before.

ID_TOKEN="$(gcloud auth print-identity-token --impersonate-service-account=$CLIENT_SA_EMAIL --audiences="demo-aud")"    

curl https://$APIGEE_X_HOST/v1/httpbin/something -H "Authorization: Bearer $ID_TOKEN"

This time around the call is successfully proxied and we get the response from the httpbin service. To prevent leaking the client Authorization header to the backend service the generator automatically injected an AssignMessage policy that removed the Authorization header from the request before forwarding it to the backend service. If you still wanted to pass the header along to achieve the same functionality as with the default API Gateway behavior you can just remove that policy from the request flow and redeploy your proxy.

Next steps

With this we conclude our little migration example and send you off to explore the new capabilities you have obtained by moving your API Gateway to a full lifecycle API management platform.

If you are interested in kickstarting your API program, here are a few pointers on how to proceed from here:

  • Think about how you want your API consumers to think about your APIs. Are there any logical groups of API proxies that you want to combine as an offering in the form of API products?
  • Create a developer portal that allows you to present and market your API proxies to your developer audience and have them onboard in a self-service fashion.
  • Put your newly generated proxy bundle in a source control management system and automate future deployments using an automated CI/CD pipeline.
  • Implement security and operational features that are shared between your API proxies or even explore the option of enforcing certain policies for all proxies of an environment.
Contributors
Version history
Last update:
‎12-08-2022 12:17 AM
Updated by: