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.
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:
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.
If you want to follow along with the step by step migration example in this article, you will need the following prerequisites:
We want to start with by configuring the following environment variables:
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
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.
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:
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"
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.
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:
as well as the expected API calls in the Apigee debug UI
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.
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: