Region ID
The REGION_ID
is an abbreviated code that Google assigns
based on the region you select when you create your app. The code does not
correspond to a country or province, even though some region IDs may appear
similar to commonly used country and province codes. For apps created after
February 2020, REGION_ID.r
is included in
App Engine URLs. For existing apps created before this date, the
region ID is optional in the URL.
Learn more about region IDs.
Microservices on App Engine typically call one another using HTTP-based RESTful APIs. It is also possible to invoke microservices in the background by using Task Queues, and the API design tenets described here apply. It's important to follow certain patterns in order to ensure your microservices-based application is stable, secure, and performs well.
Using strong contracts
One of the most important aspects of microservices-based applications is the ability to deploy microservices completely independent of one another. To achieve this independence, each microservice must provide a versioned, well-defined contract to its clients, which are other microservices. Each service must not break these versioned contracts until it's known that no other microservice relies on a particular, versioned contract. Keep in mind that other microservices may need to roll back to a previous code version that requires a previous contract, so it's important to account for this fact in your deprecation and turn-down policies.
A culture around strong, versioned contracts is probably the most challenging organizational aspect of a stable, microservices-based application. Development teams must internalize an understanding of a breaking change versus a non-breaking change. They must know when a new major release is required. They must understand how and when an old contract can be taken out of service. Teams must employ appropriate communication techniques, including deprecation and turn-down notices, to ensure awareness of changes to microservice contracts. While this may sound daunting, building these practices into your development culture will yield great improvements in velocity and quality over time.
Addressing microservices
Services and code versions can be directly addressed. As a result, you can deploy new code versions side-by-side with existing code versions, and you can test new code before making it the default serving version.
Each App Engine project has a default service, and each service has a default code
version. To address the default service of the default version of a project,
use the following URL:
https://PROJECT_ID.REGION_ID.r.appspot.com
If you deploy a service named user-service
, you can access the default serving
version of that service using the following URL:
https://user-service-dot-my-app.REGION_ID.r.appspot.com
If you deploy a second, non-default code version named banana
to the user-service
service, you can directly access that code version
using the following URL:
https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com
Note that if you deploy a second, non-default code version named cherry
to the
default
service, you can access that code version by using the following URL:
https://cherry-dot-my-app.REGION_ID.r.appspot.com
App Engine enforces the rule that the names of code versions in the default service cannot collide with service names.
Direct-addressing specific code versions should only be used for smoke testing and to facilitate A/B testing, rolling forward, and rolling back. Instead your client code should address only the default, serving version of either the default service or a specific service:
https://PROJECT_ID.REGION_ID.r.appspot.com
https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com
This addressing style allows the microservices to deploy new versions of their services, including bug fixes, without requiring any changes to clients.
Using API versions
Every microservice API should have a major API version in the URL, such as:
/user-service/v1/
This major API version clearly identifies in the logs which API version of the microservice is being called. More importantly, the major API version yields different URLs, so that new major API versions can be served side-by-side with old major API versions:
/user-service/v1/ /user-service/v2/
There is no need to include the minor API version in the URL because minor API versions by definition will not introduce any breaking changes. In fact, including the minor API version in the URL would result in a proliferation of URLs and cause uncertainty about the ability of a client to move to a new minor API version.
Note that this article assumes a continuous integration and delivery environment where the main branch is always being deployed to App Engine. There are two distinct concepts of version in this article:
Code version, which maps directly to an App Engine service version and represents a particular commit tag of the main branch.
API version, which maps directly to an API URL and represents the shape of the request arguments, the shape of the response document, and the API behavior.
This article also assumes that a single code deployment will implement both old
and new API versions of an API in a common code version. For example, your
deployed main branch might implement both /user-service/v1/
and
/user-service/v2/
. When rolling out new minor and patch versions,
this approach allows you to split traffic
between two code versions independently of the API versions that the code
actually implements.
Your organization can choose to develop your
/user-service/v1/
and /user-service/v2/
on different code branches; that is, no one code deployment will
implement both of these at the same time. This model is also
possible on App Engine, but to split traffic
you would need to move the major API version into the service name itself.
For example, your clients would use the following URLs:
http://user-service-v1.my-app.REGION_ID.r.appspot.com/user-service/v1/ http://user-service-v2.my-app.REGION_IDappspot.com/user-service/v2/
The major API version moves into the service name itself, such as
user-service-v1
and user-service-v2
.
(The /v1/
, /v2/
parts of the
path are redundant in this model and
could be removed, though they still may be useful in log analysis.)
This model requires a bit more work because it likely requires
updates to your deployment scripts to deploy new services on
major API version changes. Also, be aware of the
maximum number of allowed services
per App Engine application.
Breaking versus non-breaking changes
It's important to understand the difference between a breaking change and a non-breaking change. Breaking changes are often subtractive, meaning they take away some part of the request or response document. Changing the shape of the document or changing the name of the keys can introduce a breaking change. New required arguments are always breaking changes. Breaking changes can also occur if the behavior of the microservice changes.
Non-breaking changes tend to be additive. A new optional request argument, or a new additional section in the response document are non-breaking changes. In order to achieve non-breaking changes, the choice of on-the-wire serialization is essential. Many serializations are friendly to non-breaking changes: JSON, Protocol Buffers, or Thrift. When deserialized, these serializations silently ignore extra, unexpected information. In dynamic languages, the extra information simply appears in the deserialized object.
Consider the following JSON definition for the service /user-service/v1/
:
{
"userId": "UID-123",
"firstName": "Jake",
"lastName": "Cole",
"username": "jcole@example.com"
}
The following breaking change would require re-versioning the service as
/user-service/v2/
:
{
"userId": "UID-123",
"name": "Jake Cole", # combined fields
"email": "jcole@example.com" # key change
}
However, the following non-breaking change doesn't require a new version:
{
"userId": "UID-123",
"firstName": "Jake",
"lastName": "Cole",
"username": "jcole@example.com",
"company": "Acme Corp." # new key
}
Deploying new non-breaking minor API versions
When deploying a new minor API version, App Engine allows for the new code
version to be released side-by-side with the old code version. On App Engine,
though you can directly address any of the deployed versions, only one version
is the default serving version; recall that there is a default serving version
for each service. In this example, we have our old code version, named apple
which happens to be the default serving version, and we deploy the new code
version as a side-by-side version, named banana
. Note that the microservice
URLs for both are the same /user-service/v1/
since we are deploying a
non-breaking minor API change.
App Engine provides mechanisms to automatically migrate the traffic from apple
to banana
by marking the new code version banana
as the default serving
version. When the new default serving version is set, no new requests will be
routed to apple
and all new requests will be routed to banana
. This is how
you roll forward to a new code version that implements a new minor or patch API
version with no impact to client microservices.
In the event of an error, rolling back is achieved by reversing the above
process: set the default serving version back to the old one, apple
in our
example. All new requests will route back to the old code version and no new
requests will route to banana
. Note that in-progress requests are allowed
to complete.
App Engine also provides the ability to direct only a certain percentage of your traffic to your new code version; this process is often called a canary release process and the mechanism is called traffic splitting in App Engine. You can direct 1%, 10%, 50%, or any percentage of traffic you'd like against your new code versions, and you can adjust this amount over time. For example, you could roll out your new code version over 15 minutes, slowly increasing the traffic and watching for any issues that might identify that a rollback is required. This same mechanism lets you A/B test two code versions: set the traffic split to 50% and compare the performance and error rate characteristics of the two code versions to confirm expected improvements.
The following image shows traffic-splitting settings in the Google Cloud console:
Deploying new breaking major API versions
When you deploy breaking, major API versions, the process of rolling forward and
rolling back is the same as for non-breaking, minor API versions. However, you
typically won't perform any traffic splitting or A/B tests because the breaking
API version is a newly released URL, such as
/user-service/v2/
. Of course, if you've changed the underlying
implementation of your old major API version, you may still want to use
traffic splitting to test that your old major API version continues to
function as expected.
When deploying a new API major version, it's important to remember that old
major API versions might also still be serving. For example, /user-service/v1/
might still be serving when /user-service/v2/
is released. This fact is an
essential part of independent code releases. You might only turn-down old major
API versions after you've verified that no other microservices require them,
including other microservices that may need to roll back to an older code
version.
As a concrete example, imagine that you have a microservice, named web-app
,
that depends on another microservice, named user-service
. Imagine that
user-service
needs to change some underlying implementation that will
make it impossible to support the old major API version that web-app
is currently using, such as collapsing firstName
and lastName
into a single field called name
. That is,
user-service
needs to turn-down an old major API version.
In order to accomplish this change, three separate deployments must be made:
First,
user-service
must deploy/user-service/v2/
while still supporting/user-service/v1/
. This deployment may require temporary code to be written to support backwards compatibility, which is a common consequence in microservices-based applicationsNext,
web-app
must deploy updated code that changes its dependency from/user-service/v1/
to/user-service/v2/
Finally, after the
user-service
team has verified thatweb-app
no longer requires/user-service/v1/
and thatweb-app
does not need to roll back, the team can deploy code that removes the old/user-service/v1/
endpoint and any temporary code they needed to support it.
While all this activity may seem onerous, it's an essential process in microservices-based applications and is precisely the process that enables independent development release cycles. To be clear, this process appears to be quite dependent, but importantly each step above can occur on independent timelines, and rolling forward and rollback occurs within the scope of a single microservice. Only the order of the steps is fixed and the steps could take place over many hours, days, or even weeks.
What's next
- Get an overview of microservice architecture on App Engine.
- Understand how to create and name dev, test, qa, staging, and production environments with microservices in App Engine.
- Learn the best practices for microservice performance.
- Learn how to Migrate an existing monolithic application to one with microservices.