Versioning REST APIs in Azure Serverless

Developer Support

App Dev Manager Mike Barker explains how to version Logic Apps and Function Apps using recommended approaches.


I was recently asked by a customer about best practices for versioning and managing REST APIs in Azure serverless (that is, in Azure Functions and Azure Logic Apps).

Revisions vs. Versions

The first thing to clarify is the notion of versions vs. revisions in the context of API services.

For purposes of this discussion (and the same distinction is used elsewhere) a revision is a change to an underlying, implementing logic whilst maintaining backwards compatibility; and a version denotes a change to the API, or contract. Or, if you prefer, a revision is a non-breaking change, whilst a version is a breaking change.

Revisions are used when deploying optimisations, or bug fixes. Versions typically implement new or different logic with new parameters.

It is also important to note that an API change could be inbound (e.g. a new parameter) or outbound (e.g. a new output field). Sometimes this is more complex than it seems on the surface. For example, one can not necessarily guarantee that a new outbound field will not break the implementation of you API’s consumer.

It gets a bit “fuzzy” if you add a new optional input parameter to an API. Some would argue this is a revision, some argue it is a version. Personally (and I think the general accepted opinion) is that if a deployment is fully backwards compatible then it can be considered a revision.

Typically, the consumer is aware of the version it is calling, but not aware of the revision.

Versioning schemes

There are primarily three ways in which REST APIs are versioned:

  1. URL versioning: Where the URL will contain a segment, similar to: /api/v2/resource
  2. Query String versioning: Where the query string denotes the version, similar to: /api/resource?version=2
  3. Request header versioning: An HTTP header contains the version number of the API, similar to: Accept: application/vnd.mycompany.myapp.customer-v2+json

To the best of my knowledge there is no official, or generally accepted, best practice around this. A quick search on Stack Overflow immediately demonstrates varying opinions in the community on the use of these schemes.

At Microsoft, we use a mixture: Query String versioning in ARM, but URL versioning for Graph APIs. Similarly, AWS employs the URL version and query string version. Google typically prefers URL versioning.

Also, worth considering is your choice of how much detail to include in your version numbering.

  • You could simply go for a “v<major>” scheme (e.g. v1, v2, etc).
  • Additionally a minor tag can be included, e.g. “v<major>.<minor>” (e.g. v1.1, v1.2, v2.0 etc). Often this scheme is used when choosing the major to denote the version, whilst minor denotes the revision. The consumer can then control which revision it will target, and this is often used in both smoke and A/B testing. Often the minor versions only publicised internally.

    • You may wish to facilitate both “v<major>” and “v<major>.<minor>” by aliasing “v<major>” to point to “v<major>.<latest>” (i.e. where v3 and v3.2 point to the same revision).
  • Yet more detail can be added by including a patch number, e.g. “v<major>.<minor>.<patch>”, where major is used for new feature, minor is used for backward compatible changes, and patch is used for bug-fixes or optimisations.
  • You may also want to consider including a suffix like “preview”, “alpha”, or “beta” (or similar) for release candidates. (e.g. v2-preview1, v2-preview2, v2)
  • Another technique is to use a date-based numbering scheme (e.g. api/resource?version=2015-01-01). The version number now simply becomes the release date of the version (or some other date chosen from your release pipeline, e.g. build-date).

Again, to my knowledge there is no official best practice. Microsoft employs a date-based numbering scheme in ARM, and major.minor with suffix elsewhere. Google recommends using major.minor.patch, with suffix.

Maintaining and tracking backward-compatibility during the development lifecycle can be challenging as developers must be acutely aware of the impact any changes could bring to backwards compatibility. My recommendation would be to not weigh down your development and delivery process too much by utilising an overly verbose scheme which is not going to bring sufficient benefit.

  • A good compromise in the majority of cases is to use only “v<major>.<minor>”, with a suffix for preview builds.
  • If your development team is mature in their development process, all changes are well understood (probably peer reviewed), and you are executing frequent releases then there is benefit to be had in including the patch number too.
  • For APIs which are updated very infrequently, it may even be better to simply use a single version number (e.g. v1, v2).

If you are using URL versioning, then including the “v” in your version number helps consumers of your API to understand that this refers to a version number. For example, compare /api/2/entity to /api/v2/entity. The latter is easier to understand.

Logic Apps

Logic Apps contain a complete published history of the versions of the logic app. A new version is created every time the logic app is saved in the visual designer or published from an external source (e.g. Azure DevOps, or Visual Studio Publish).

(NB: Logic Apps have no concept of API revisions / versions. As far as it is concerned there are only versions of the workflow.)

When “HTTP Request” is used as the trigger of a Logic App, a “latest version” URL will be generated as well as a unique URL for each version. This means that you can deploy new versions of the logic app, but continue to call and execute previous versions.

The “latest version” URL will look something like:

https://prod-20.northeurope.logic.azure.com:443/workflows/e7b1ae9ffc52443584fd085fcaac5efb/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=JJGS_G6jyPmCuQzb3uD9alHoJxiRZwOTfEo8ORqiTjM

whilst a specific version URL will be something like:

https://prod-22.northeurope.logic.azure.com:443/workflows/e7b1ae9ffc52443584fd085fcaac5efb/versions/08586395317930571172/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Fversions%2F08586395317930571172%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=GrfWUFe4De6hzXM0GgtxvPZDo2M5koiFRKoJ3_hHfls

Notice, firstly, the addition of the version number in the URL. (Notice also that the encoded signature also changes.)

So, this immediately gives you the benefit of having natural versioning built into the Logic App.

Publish using an API manager

However, in a production environment scenario you would not want to publish the Logic App URLs directly.  There are a few reasons for this:

  1. If you want to release an API revision, then you will need to ensure all consumers are re-pointed to the new version’s URL.  This can be difficult if you are not in control of the consumers, or have a high management overhead if there are multi consumers.
  2. You are unable to move the logic to a new technology (say you decide a Function App would suit the logic better), with re-pointing all consumers.
  3. Bug fixes and rollbacks are no longer quick, atomic deployments as each consumer needs to be re-pointed.
  4. In a DR scenario where an Azure region is lost, it is laborious to switch all consumers to a new region.
  5. You may also wish to charge callers, and manage who has the right to call the API.

Therefore, you should consider fronting your logic app with an API manager or proxy. Options available in Azure would be Azure API Management, Azure Function Proxy (a light weight APIM), Azure Application Gateway (although, this is not the best use case for Gateway); or one could employ 3rd-party system like Apigee, MuleSoft etc. Using an API manager, you can provide vanity URLs and redirect calls into the correct version of the Logic App.

Additional benefits that an API manager brings are end-point high availability, access control, API unification, discoverability, documentation, response caching, and governance and management.

Employing an API manager in front of your Logic App you can now version your APIs in a human readable way. Your revision set may then look something like:

cid:image006.png@01D53339.AFF26AF0

(This figure uses the query string versioning scheme, but one could equally employ any of the other schemes)

As you can see from this example, you may consider Logic App versions 08586395319894348045 to 08586395316016742747 to be revisions of “version 1”, and have repointed the “version=1” URL in the API proxy to a new version as each interim revision was released (currently 1.2 is the latest revision of version 1).

Logic App version 08586395254944968031 would be exposed as API version=2.

Logic App version 08586395255018056378 was likely a ‘dud’ release (possibly a preview, or maybe a bug was identified) and is not accessible via the published API proxy URLs.

DevOps considerations

Using this methodology for versioning your Logic App, the JSON code file in your source repository should be tagged with the version number it is exposed as. The history of the revisions and versions will be maintained in the code repository as well the released version history in the Logic App itself.

A deployment is done by deploying the new version of the logic app, possibly testing it, and finalised by re-pointing the URL (possibly several URLs) from the API proxy (or creating a new version URL).

Rollback is handled simply by repointing the affected URL(s) in the API proxy. No code rollback is required.

Function Apps

Function Apps do not have the same kind of in-built versioning as Logic Apps. Having multiple concurrent versions requires multiple functions.

For example:

[FunctionName("GetFoo_v1")]
public static IActionResult GetFoo_v1(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/v1/foo")]HttpRequest req,
    ILogger log)
{
       // ...
}
[FunctionName("GetFoo_v2")]
public static IActionResult GetFoo_v2(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/v2/foo")]HttpRequest req,
    ILogger log)
{
       // ...
}

What you will immediately notice is that logic can quickly become duplicated in these two (or more) functions and so, where possible, move the business logic of the function into a separate method. Inputs to this method should either be optional and provide suitable defaults, or (my preferred option) each version should specify its own defaults.

To improve the maintainability of the functions across versions the input and output types should be strongly typed and versioned. This allows developers to lean on the compiler to ensure an update does not break a previous version, thus reducing their cognitive load and minimising mistakes.

e.g.

[FunctionName("GetFoo_v1")]
public static IActionResult GetFoo_v1(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/v1/devices")]HttpRequest req,
    ILogger log)
{
    string requestBody = new StreamReader(req.Body).ReadToEnd();
    GetFooRequest_v1 request = JsonConvert.DeserializeObject<GetFooRequest_v1>(requestBody);
    Foo foo = GetFoo(request.Name, DateTime.MinValue); // Default value applied for the startDate argument
    GetFooResponse_v1 response = new GetFooResponse_v1(foo.Name, foo.Age); // foo.Description property is ignored in this version
    return new JsonResult(response);
}
[FunctionName("GetFoo_v2")]
public static IActionResult GetFoo_v2(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/v2/devices")]HttpRequest req,
    ILogger log)
{
    string requestBody = new StreamReader(req.Body).ReadToEnd();
    GetFooRequest_v2 request = JsonConvert.DeserializeObject<GetFooRequest_v2>(requestBody);
    Foo foo = GetFoo(request.Name, request.StartDate); // The v2 request carried an additional property "StartDate", not available in v1
    GetFooResponse_v2 response = new GetFooResponse_v2(foo.Name, foo.Age, foo.Description); // foo.Description property is included, due to GetFooResponse_v2
    return new JsonResult(response);
}
private static Foo GetFoo(
    string name,
    DateTime startDate)
{
    // ...
}

In this example the requests and responses are modelled by versioned classes. If developer makes a change to the GetFooRequest_v2 request class to expect a new parameter, she can rest assured this will not impact version one. Likewise, the version two response class here may include a “description” property, where this is not emitted by version 1. Further versioned responses can be added with full confidence that previous versions are not impacted.

It is tempting at this point for a developer to inherit the version of the input and output POCOs, using a class hierarchy, ie. to implement GetFooResponse_v2 as

class GetFooResponse_v2 : GetFooResponse_v1
{
public string Description { get; set; }
}

I strongly recommend against this as it creates an implicit, brittle dependency between v1 and v2 (i.e. v2 can only extend v1. Properties cannot be removed or renamed).

The implementing logic for “GetFoo” is unified across all versions reducing code duplication, and suitable defaults are provided to this method by the versions. This may not always be possible but where it is possible it does improve maintainability.

Versions and revisions

This works well for versions what about revisions?

Revisions are substantially harder to maintain in functions as the implementing logic must be fully duplicated in order to make a change and not affect a previous revision. In the example above, if you want to update the GetFoo method to optimise its performance or remedy a minor bug this will impact ALL versions.

Unless you have definite requirements that necessitate this, I strongly recommend not maintaining a revision history with functions.

If, however, you do have a requirement to do so, then consider creating multiple Function Apps and deploying each new revision side-by-side into its own new Function App. Use tagging in your code base to track what has been deployed in each revision.

Publish using an API manager

You’ll notice the Function Apps are using URL based versioning, but again do not publicise these URLs directly. As with Logic Apps, front these with an API manager. The same reasoning and benefits apply for Functions as for Logic Apps, so I shan’t repeat them here.

DevOps considerations

Versions are naturally tracked within function app code base and so the tagging strategy employed for logic apps is not as necessary here. If you choose to implement side-by-side releases for revisions, then it will become necessary again to maintain tagging.

A deployment is made by rolling out the code to your function app. New versions will require a new entry in the API manager, but existing versions will continue to be pointed to correctly and do not require an update. Rollbacks are handled by re-deploying the previous version of the function.

One can improve the deployment and rollback process by employing deployment slots. This allows you to stage the deployment and test it before executing a slot swap to make your new deployment live. Similarly, a rollback is handled by switching the slots again to reverse the deployment.

Again, if you are employing multiple function apps to provide side-by-side deployments you will need to think carefully about how you design your release pipeline. You may want concurrent versions in production, but not want every code commit to result in a new function app being created in your dev environment, thanks to CI/CD. This will mean that your release scripts to production may differ from dev, and so you will need to compensate for these differences.

Final thoughts

In this blog post we have explored breaking and non-breaking changes, REST API versioning schemes, and numbering formats for REST API versions.

We’ve looked at the inbuilt versioning provided by Logic Apps and how you can leverage this in your versioning strategy. We also looked at Functions and these can be coded to cope with multiple versions, whilst ensuring good code maintainability and design.

As a final thought I would urge you to consider retiring old versions of your APIs. Publicise this well to you consumers and provide them with an upgrade path to utilise new versions. The greater the amount of code you are supporting in production the greater the overhead of managing that code. Maintaining old versions can inhibit your ability to innovate and be agile.

 

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Erik Madsen 0

    Using the term “revision” muddies the waters.  I recommend you stick to the nomenclature used in Semantic Versioning and use the term “version” exclusively.  If you wish to be more specific you can refer to one of the four terms in the Major.Minor.Patch.Build version.

  • Ian Kemp 0

    You should edit your links to remove Trend Micro servers…

Feedback usabilla icon