An independent guide to building modern software for serverless and native cloud

Serverless Strategies

This is the third in a multi-part series that looks at how serverless and native cloud fit within larger software and IT industry trends.

The previous two posts in this series outlined a solution landscape that's centered around integration and is almost exclusively running in the cloud. Now, in this post, we're going to explore some of the serverless strategies you might pursue.

Using Managed APIs

In the solution landscape we've outlined, you're going to use REST APIs at every level. This might include experience APIs for application frontends, system APIs to expose backend services, and process APIs to orchestrate either.

One of our goals when using APIs is to enable near-real-time functional integration across systems. To illustrate what this might entail, let's look again at a student system, this time one that specifically integrates with an off-the-shelf assessment service. If we want two such systems to be integrated with near-real-time data synchronization, then each will need to provide pairs of inbound APIs and outbound web hooks. Although users might initially want to import section and enrolment data from a student system in bulk to the assessment tool, you would still want the student system to synchronize subsequent updates with that tool through an API. Similarly, for grades being returned from the assessment tool, a call back to a registered API in the student system would be a far better option than any export/import process.

The REST principles represent the best practices for APIs like this, learned from the scaling challenges of the web, which in terms of performing updates, include:

  • Granularity.
  • Statelessness.
  • Separation of commands from queries.
  • Separation of idempotent from non-idempotent updates.

Managed APIs are the key serverless technology for hosting custom APIs in the cloud. Using managed APIs, in combination with OpenAPI and OAuth 2.0, you also get support for:

  • Application- and user-level authorization using OAuth 2.0 auth flows.
  • Application-level API keys and request rate throttling.
  • API-first design that can output generated skeleton code and documentation.
  • Request validation using data schemas and field-level constraints.

Managed APIs mediate between client applications and the service code that backs the API, making it possible to package the API endpoints, and the implementing service code, independently of one another. It's also fairly easy, given this independence, to repackage the service code, switching, for example, from functions to containerized API endpoints, or the reverse, as needed.

Lastly, we'll note the general benefits that using serverless and cloud native services like this offer. These benefits include unified deployments using declarative infrastructure-as-code tools, built-in reliability and scalability, easy employment of least-privilege resource access, and nearly zero-cost development and test environments.

Writing Portable Code

Custom APIs require backend service code, and it's important to understand that the code for these services can be written to enable portability that will minimize technology lock-in.

The strategies to achieve portability basically build on those you would already be following in order to have adaptable, micro-service code. Our starting point, therefore, is service code where only the interfaces and their associated data classes are shared for consuming applications and services. All other service implementation code is private.

Beyond this, two additional strategies are needed to support code portability. First, the libraries or packages that contain these shared interface and data classes need to be free of any technology-specific dependencies. If the implementation code for a service calls a specific NoSQL database, for example, then that code will naturally have library dependencies for that product. The shared interface and data classes should not have any such dependencies. The same principle applies for whatever technology hosts the service code. If a service is called from an AWS Lambda, for example, then the interface code for that service should not have dependencies to things like the Lambda-specific request and response classes. However convenient these classes are as containers of multiple elements they should emphatically NOT be passed as inputs to a service interface.

Second, whatever context the service code needs at runtime, like access to configuration values or service connector instances, that context should be implemented using dependency injection. This makes it much easier to support different runtime scenarios, which you can see demonstrated in the sample code for this site's AWS Learning Serverless course. In that code, you'll see service code that can be run locally for testing, or run in the cloud, different scenarios made possible with different implementations of its service context interface.

This sample code also demonstrates the portability that we're aiming for. There are compiled libraries for the service code for the ConnectedCar solution, which you can see on the packages tab of my Github account. These packages are referenced from both Lambda-based solutions and containerized API solutions built for the AWS Elastic Container Service. Only a small fraction of the code for the backing services is contained in the Lambdas or containerized API endpoints, which act as thin protocol wrappers for the actual service implementation code.

As a result, the course sample code can be run in a variety of ways, targeting different AWS services. But it could also, without an enormous effort, be ported to equivalent Azure services. Ultimately, this is the payoff from following portability strategies like these, where you enable migration paths to alternative technology platforms, minimizing vendor lock-in.

Modernizing Web Applications

The technology choices are fairly straightforward if you want to build new services that will run on serverless. You can start with managed APIs, choose your authentication/authorization mechanisms, then pick between function and containerization options for compute, and between SQL or NoSQL options for the data tier.

But what if you have a legacy web application that you want to move to the cloud, where it can participate in the modern solution landscape we've outlined and gain the benefits outlined above for serverless technologies?

There are two broad strategy options for application modernization, as we've framed it. The first option involves re-platforming the application while largely leaving the code as is. (Assuming this code is written in a modern language that's a candidate for containerization). In broad outlines, these are the steps involved in re-platforming for serverless cloud services:

  • Containerize the application and run in the cloud on serverless container hosting service.
  • Migrate authentication to an OIDC identity service.
  • Migrate the database to, if possible, a serverless database service.
  • Scale the application horizontally with an application load balancer integrated with the identity service.
  • Store session state in a caching service if possible to better handle scale-downs and container instance updates.

An application re-platformed along these lines will benefit in several ways. Compared to an application running on-premises, or one following an IAAS lift-and-shift cloud strategy, it will be simpler to operate, having no servers or system software to keep updated. It will result in test and development environments that are easier to deploy and cost less to maintain. It will scale better, with built-in autoscaling in the application and database tiers. It will also be more secure, running within a locked-down virtual network, and taking advantage of an identity service that offers advanced security options, like adaptive multi-factor authentication. There's also nothing stopping a re-platformed application from making external APIs available through an API management service. As just covered, these APIs can then support all the modern standards, including REST, OpenAPI and OAuth 2.0.

The second modernization option involves re-architecting an application to gain the advantages of a micro-service architecture. As we've covered previously in this series, packaging code in smaller units, and having bounded context with only interface code being visible to client applications, gives you code that's more cohesive, and is therefore easier to maintain.

But, in the solution landscape that we've been outlining in this series, the benefits go beyond maintenance. Every service in an application that's broken out into its own code base and is then integrated into the application through a loosely coupled interface, becomes a service that can also be accessed by other applications and integration processes through an API. Furthermore, the client code that accesses the functionality of services like this through an API can now, potentially, integrate with equivalent functionality provided by a third-party service API.

An example of how this might realistically be used would be with an application that has a built-in workflow or case-management feature. Such a feature, once it no longer has access directly to backend data, but instead interfaces with a service API, can likely be extended to interface with external data through compatible, registered APIs. Having this kind of extensibility is a big win for a software product, since it offers a lot of benefit to enterprises that might want functional integration spanning multiple systems.

A fully re-architected, micro-service-backed application like this actually needs fewer cloud services. Here are the serverless services that support this architecture:

  • Object storage with custom domain name for web artifacts, assuming a reactive frontend.
  • API management for backing APIs for experiences, processes and systems.
  • Choice between functions and containers for compute.
  • Choice between NoSQL and SQL the data tier.

If, in particular, you're able to use functions instead of containerized APIs for the backend then the advantages of this architecture over the re-platforming strategy are:

  • Simplified cloud infrastructure, without the need to set up networking and autoscaling resources.
  • Lower running costs for test and development environments thanks to the usage-based pricing for serverless compute and database options.

Using Identity Services

We've touched on identity in the modernization strategies just outlined, but it's a sufficiently important technology to be worth exploring further.

The primary reason to adopt a cloud identity service is that, more than any other, it's a service that's best left to the cloud providers who have a level of proficiency in security and operations that smaller organizations are unlikely to match. But, in addition to these primary security benefits, there are also some interesting modernization and integration strategies that identity services can enable.

First, most cloud identity services are not just identity providers, but can also act as identity brokers that support inbound (and sometimes outbound) SAML and OIDC federation. Taking the student system and assessment service from above as an example, you might federate the identity provider used by the student system with the identity service of the assessment tool. If you set up the just-in-time account creation and attribute-mapping appropriately in the latter system's identity service, then you could enable a seamless single-sign-on experience across the two systems.

Second, it's also possible to share client IDs for OIDC providers across sibling applications in order to support user navigation between them without triggering additional logins. (Note that most legacy applications will need new interstitial landing pages to parse incoming JWT tokens and initialize sessions). You can use this strategy to split up an application from a development and deployment point of view, while presenting it as a unified, composite application to users. Splitting an application in this way yields benefits similar to those found when packaging backend code into smaller micro-services. Separate applications can run on different platforms, if necessary, and can be developed and deployed independently, by separate teams. This option also lets you re-architect a high-touch part of a newly separate application for a more interactive user experience, for example, while only re-platforming other, lower-touch parts.

Using Integration Services

APIs may be central to modern integration, but they're not sufficient on their own to add resiliency to cross-system integrations or long-running batch processes. Middleware may have an image problem with developers insofar as it's associated with legacy ESBs. But I don't think you can escape the need for middleware-style features like reliable messaging, publish/subscribe, content-based event routing, and service orchestration. The good news is that there are serverless integration services that can fill these middleware roles, while also offering the benefits found with serverless services generally, including built-in scalability, usage-based pricing, and support for cloud ecosystem services like those used for deployments and monitoring.

Something else that we mentioned above was how serverless services and properly set up lab accounts can enable tinkering and experimentation. This applies in particular to integration services. Traditional on-premises middleware is expensive, making it a scarce resource with access often managed by gatekeepers. As we outlined in the last part of the Beginner's Overview series, you can take advantage of policy-based guardrails in dedicated cloud lab accounts to work, as an integration architect, with a lot more autonomy. Prototyping a solution and working through design iterations in this way is an order of magnitude faster than doing the same thing on what are often locked-down environments running on-premises.

There isn't room here to go into a lot of detail about using these serverless integration services, but I can offer some observations. First, I think it's better to use APIs as the targets, when possible, for message reads and orchestration activities, rather than direct invocation of functions. You also need to be aware of the processing limits, in terms of request/response size and execution time, for managed APIs and functions. If you want to migrate a large, long-running process to serverless services, you'll likely need to break it into a series of incremental processing steps (which is probably advisable in any case).

We should also touch on connectivity, which is a big part of integration. An important point to understand is that you control access to serverless services at runtime in a different way than with traditional IAAS resources. IAAS resources run inside virtual networks, and as such you control application-level access to these resources primarily with network routing and firewall rules. PAAS resources, like serverless functions or databases, do not run inside user-managed virtual networks. Instead, you control access to these resources through cloud access management.

What does this mean in practice? To start, in an entirely serverless stack like that covered in the Learning AWS serverless course, a publicly reachable managed API layer can be secured with any combination of OAuth 2.0, API keys, or whitelisting of source IP addresses or named resources. The last option in this list makes it possible to limit API access to managed cloud resources like virtual networks, even when those are running under different cloud accounts.

Services that are invoked downstream from managed APIs have the resources they have access to controlled by the cloud access management service. These services are configured to assume specific execution roles with least-privilege access policies attached to them. For example, as shown in the sample code for the Learning AWS Serverless course, the execution role for an AWS Lambda that needs to read from a specific DynamoDB table can have an attached access policy that grants only the minimally required actions, and limited in scope to the targeted table resource.

So, establishing secure connectivity amongst entirely serverless services, even across accounts, is fairly straightforward. The complications arise when you need connectivity that bridges the separate IAAS and PAAS worlds. There are too many scenarios to cover here comprehensively, but the connectivity options can be summarized in this way:

  • Both AWS and Azure have similarly named "private link" mechanisms, which make PAAS services accessible from within IAAS virtual networks that don't otherwise allow egress.
  • Both cloud platforms also have mechanisms that give specific virtual network access to functions running outside them.
  • Both cloud platforms' API management services can be deployed privately, with access made available from specific virtual networks using private link and appropriately tailored resource policies.

Zooming back out, what these options mean, in the end, is that you can deploy APIs and serverless services in the cloud, control access to them through network configuration, application layer security protocols, or cloud access controls. These APIs can be accessed securely from on-premises over private network connections, and vice versa. They can also be securely accessed across cloud accounts to integrate single-tenanted products with their customer environments.

Another strategy you can follow, given the serverless integration services and connectivity options outlined above, is a hybrid architecture. With this strategy you can locate system APIs on-premises while executing integration processes that invoke these APIs in the cloud. This is a way to include systems that have to remain on-premises, for whatever reasons, within larger cloud-hosted integration processes. It also makes it possible to maintain data at rest on-premises while still gaining some of the advantages of serverless services in the cloud. If storing operational data for a long running process in the cloud is also an issue, then you can potentially expose an API for an on-premises ODS and integrate that with cloud-based integration services.

Setting up Accounts, Governance & Automation

Sometimes a piecemeal approach to managing cloud environments and the systems deployed to it is taken, which also reflects how heterogeneous on-premises environments are often managed. However, it's worth understanding the degrees of automation that are available in the cloud, both for deployments and for governance. Using these features to more comprehensively manage cloud environments can have real benefits.

As with other areas touched on here, this is a large topic that I will more fully cover in subsequent articles. But here's an outline of how you can set up your cloud environment to take advantage of governance, and deployment automation:

  • Within your enterprise-level cloud account, create additional task- and workload-specific accounts for things like users, security monitoring, builds, and various development, testing, production, and lab workloads.
  • Define an organizational hierarchy to overlay these accounts and to which policies can be attached that will inherit generalized rules above them in the hierarchy.
  • Define policies that appropriately constrain which services can be run in which accounts, with additional configuration constraints, if required.

With these measures in place, you're able to grant least-privilege access to resources for users and system execution roles, and limit the blast radius in the event that a user account or execution role is ever compromised.

Having set up your accounts and policies, you'll then want to manage all subsequent resource changes using source-controlled declarative infrastructure-as-code (IAC). In fact, I like to go one step further by using declarative IAC to define and deploy native pipelines that in turn use declarative IAC to deploy solutions. (You can learn about using CloudFormation and AWS native pipelines in the Learning AWS Serverless course). Two big advantages of using declarative IAC to manage cloud resources are, first, it's self-documenting, and second, at least in native form (AWS CloudFormation and Azure Bicep) updates are transactional, which is an incredibly useful feature.

With all of the prior serverless API, integration, connectivity features in mind, as well as these account, governance and IAC measures, we can paint a picture of how much automation is possible in the cloud. To start, a fully serverless solution, even one with complicated cross-account or hybrid connectivity elements, can have full working environments deployed at the push of a button (or torn-down similarly). As noted, this is particularly effective for developers or architects prototyping solutions in lab accounts, allowing for very rapid design iterations.

You also have the capability to cookie-cutter new workload accounts that might contain multiple solutions. This can be useful for product vendors, who have to set up single-tenanted environments for their customers that, along with deployed applications, require things like predefined cross-account user access and connectivity. Note as well that AWS and Azure both have features that make it possible to create new working environments that also include catalogs of approved and tailored services.

What this all Means

In the first two posts of this series we described an all-cloud solution landscape with integration at its center. What this post has hopefully shown, by surveying some of the strategies that they support, is how comprehensive serverless technologies have become. After many years of continuous evolution, serverless now has an answer to more or less any mainstream application or integration service need. With serverless you can host managed APIs, described by OpenAPI, and secured by OAuth 2.0. You can build custom services, support modernized application architectures as well as different architecture styles for integration. Moreover, when following the portability strategies outlined above, you have a lot of power to sidestep vendor lock-in and deploy your code to different technologies across different cloud vendors.

So there's nothing stopping you at this point from building the best possible solutions, in the cloud, using serverless. When you do start building serverless solutions, you will, or course, get the benefits you expect in terms of low-maintenance, high-performance, and speed of development. What takes some time to appreciate, I think, is the degree to which the latest serverless technologies and the whole ecosystem of cloud tools and supporting services work together synergistically.

As a result of these synergies, once you've fully adopted serverless and have mastered some of the cloud practices outlined in the section above, you'll realize a very real economy of effort. You'll not only be in a position to respond and adapt to change, which is our primary goal. But, if my own experience is anything to go by, you'll also be able to design and deploy complex solutions more easily than in the past.

Next

The next post in this series will expand on those elements of code portability and cloud readiness that were touched on here, to show what a bottom-up, serverless adoption roadmap looks like.