The technological optimism trap

Technology has the nature of a commodity, a fact that is often overlooked. Apart from the commercial benefits brought by monopoly, on the one hand, technology depends on the recognition of the market to highlight its value; on the other hand, technology also needs the feedback of the public to improve itself. Therefore, a huge user group is the cornerstone of its prosperity, and it needs to be known as much as possible. Whether you want to attract more projects and developers to a community, or bring a framework from obscurity to prominence, the process depends on a number of operations, many of which rely on the resources of the big companies behind them. From the near-death of Silverlight to the recent explosion of Flutter, everything follows a similar pattern.

Since it is a commodity for the public, businesses will inevitably defend and shout for it as stakeholders, which is understandable. However, under the influence of this, when technical personnel conduct research on a certain technology or passively receive updates from the industry, the information they get will unconsciously shift to the positive side, which is not necessarily a good thing for technical personnel. Because it’s hard to tell which of your senses is fact, which is opinion, which is conditional, and more importantly what it doesn’t tell you.

Serverless is one example.

This article is not a criticism of Serverless. Serverless is the natural product of Cloud Native architecture. From Infrastructure as a Service (IaaS) to Paas (Platform as a Service) and even Software as a Service (Saas), we see the migration process of outsourcing operation and maintenance capabilities. This helps build an elite team focused on delivering business value and responding flexibly to market changes — why do we write the same log-in module? How can you minimize the maintenance cost of your code? Serverless was born from these premises. But Serverless is only one of the solutions, not the only one, and more importantly, this article will make you realize that Serverless is by no means the preferred solution.

Every article about Serverless, for example, is bound to mention the slow first response time of the Serverless functions due to a cold start, but the information they provide usually comes to a screeching halt at this point, which doesn’t do us any good, nor does it cause us any alarm. If I go on to tell you that delays vary from vendor to vendor, and that the first startup delay for Azure Serverless on my project can be as long as 6 seconds, then I’m sure you’ll take that information more seriously and start lowering your expectations for it as a Web Server.

Another point this article would like to highlight is that while Serverless may seem like a “new” technology of the last few years, following best practices is still a long-established consensus in the “old” world; To actually apply it to an existing product, you need to care about content just as much as it did before Serverless. For example, of the Serverless top 10 security issues compiled by OWASP, I don’t think there is one that is “exclusive” to the Serverless architecture. One of Serverless’s advantages over traditional services may be that valuable experience is solidified into the platform and product form to ensure you don’t have to go wrong.

Considering the general knowledge, this paper mainly uses Azure and SERVERless services of AWS to explain the problem

The despised vendor lock-in

Supplier of three lock

Vendor lock-in is an inevitable problem in cloud native architecture. If you choose Azure as your cloud service provider, you will most likely choose Azure Blob Storage instead of AWS S3 as your Storage service, because the service fit from the same vendor is higher. It’s easier to maintain. Given the cost and risk, the likelihood of changing services after that point is almost zero.

Thanks to the fact that programming languages and frameworks are still universal, and that containerization technology has matured, vendors don’t have too much trouble developing regular business code. Whether you choose AWS EventBridge or Azure Event Grid, the event-driven decisions behind it will not change, and the core business code will not be affected. ExpressJS code can still be reused across service providers. The most obvious feature of this model is that business people can focus on developing business code, regardless of which vendor the company is buying. It sounds like an anti-pattern, but it’s possible to leave the code and environment fit entirely to operations.

The Serverless model, on the other hand, has risen like a thesis in which different vendors prioritize their own solutions based on their existing infrastructure under the premise of concept first. What’s interesting about this is that if you look at the current technical books on Serverless, the concepts and code implementations in the books are written around a single platform.

Serverless has a very important concept that embodies this aspect: trigger.

As the name implies, trigger is the trigger of function (function in this paper generally refers to the implementation code of Serverless on various cloud platforms, and contemporaneously refers to Azure Function and AWS Lambda), It is responsible for launching function. For example, for a function that responds to a front-end request, the HTTP request is its trigger.

But in the Serverless ecosystem, HTTP is the least important. Think back to our classic Serverless use case, creating thumbnails offline:

In this process, you need a function response to handle the abbreviated message, and after storage, you need a function to update the data into the database. The message service and the store service are function triggers.

It’s not hard to see that when you start writing function, you need to confirm what your cloud provider is offering this type of Service. The messaging Service on Azure could be Azure Service Bus. However, AWS is a Message Queuing Service. Different services provide different apis and models, and the way the code is integrated with the service is tailored, which is the first layer of locking.

Secondly, in order to access such services in function code, naked code is not allowed, because you need to pass the API Key in a specified way when accessing the service. The usual way to solve this problem is to directly integrate the client SDK provided by the vendor. Such as @Azure /service-bus or AWS SDK. In fact, from the moment the request is received, the code difference is already set. Although Azure and AWS both agree to respond to trigger requests in the form of event Handler functions, the signature of the two functions is quite different, and the context in which you can retrieve the functions is quite different. This is the second lock.

While both seem to cover both the hardware and software layers, the most important “invisible lock” is left out — the vendor’s will to design and write functions in a way they want you to.

Using the API architecture for example, Azure services like Azure Serverless or App Service can be independent of each other, even if you only buy one of the services, You can also configure API Management, Identity, and other attributes separately. The service is allowed to expose HTTP ports externally. Mobile devices can access Azure Serverless services directly in the architecture model presented on its website

In AWS, services are more vertical than Azure omnipotent. Most HTTP endpoints are hosted on the API Gateway, which provides you with rich functionality such as permission validation, log monitoring, caching, and so on. In the same backend architecture pattern presented on the AWS website, requests from mobile devices must go through the API Gateway

In Azure Serverless, each Serverless project has its own host.json configuration file. If we want to limit the maximum number of requests that function can handle, all you need to do is change the configuration item in this file:

{
  "extensions": {
    "http": {
      "routePrefix": "api"."maxConcurrentRequests": 100."customHeaders": {
        "X-Content-Type-Options": "nosniff"}}}}Copy the code

MaxConcurrentRequests in the code above can be used to control the number of concurrent requests.

In AWS, for synchronous HTTP requests, it is recommended that you implement API Gateway throtting and set AWS WAF rules

The downside of this layer of locking is that you have to design your solution within the vendor’s framework from the start. You could certainly opt out of API Gateway’s Lambda Authorizer as a solution for function permissions in AWS, but I’m not sure how far the other route would take you.

It doesn’t matter if you haven’t touched Lambda Authorizer, which I’ll cover in more detail later. As we will see in later chapters, while complaining about it, we have to admit that it is still following the best practices in the industry. We may seem to have no choice, but in fact the only shortcut we can take is the shortcut left by our predecessors.

Solutions of the “lock”

The good news is that there is still room for mitigation in the face of this visible crisis.

In 2019, ThoughtWorks published an article on How to Avoid Serverless Vendor Lock-in Fears Mitigating ServerLess Lock-in Fears, containing recommendations to reduce migration costs from hardware to software. But one of the most useful, in my opinion, is designing a good architecture for your program.

Although the low threshold of entry is one of the selling points of Serverless, its ceiling can still reach the same height as the traditional technology stack program, and the excellent design of the same line can give the convenience of later maintenance.

For example, a use case for sending outgoing mail is first written using Azure Serverless Function. In the httpTrigger entry Function, we can directly reference Azure SendGrid SDK to execute the sending service

import * as SendGrid from "@sendgrid/mail";
SendGrid.setApiKey(process.env["SENDGRID_API_KEY"] as string);
 
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest) :Promise<void> {
    const email = {
     to: '[email protected]'.// Change to your recipient
     from: '[email protected]'.// Change to your verified sender
     subject: 'Sending with SendGrid is Fun'.text: 'and easy to do anywhere, even with Node.js'.html: '<strong>and easy to do anywhere, even with Node.js</strong>',}await SendGrid.send(email);
}
Copy the code

Then if you want to migrate it to AWS Lambda, the sending mail part needs to be completely replaced with calling AWS SES:

import { SendEmailCommand } from "@aws-sdk/client-ses";
import { sesClient } from "./libs/sesClient.js";

// Set the parameters
const params = {
Destination: {},
Message: {}};const data = await sesClient.send(new SendEmailCommand(params));
Copy the code

But the truth is that we don’t really care who is delivering our mail, whether it’s SendGrid or SES. So when designing this program, we could have extracted a public email client and had the httpTrigger entry function call the client:

import emailClient from "./email-client";
 
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest) :Promise<void> {
 
// ...
await emailClient.send(email);
}
Copy the code

During the migration process, the entry function hardly needs to be changed. The change only happens in the client, and we only need to retest and verify the client. If you’re using C#, we can even abstract EmailClient as an interface for injection. In short, we’re back to separation of concerns, or even hexagonal architecture.

Programming for interfaces has another advantage — it makes component testing easier.

We can extend the above process to get the API_KEY for using SendGrid from KeyVault after being triggered, and log using Application Insights after SendGrid is sent. The process is shown below

You might be interested in E2E (end to end) testing the entire functionality inside the dotted box. This is not impossible, but it is difficult and expensive. The difficulty comes first from the testing nature of E2E itself. If you remember the testing pyramid, the E2E tests at the top of the pyramid are the most expensive to run and maintain. Secondly, due to the differences of services provided by the third party serverless, it is difficult to build a stable offline testing environment in each person’s local area. The resulting uncertainty and dependence on the online environment are contrary to our expectation of quick feedback and repeated execution of the test.

So I recommend abstracting the Service Layer from the code in Serverless and testing it first. The service layer is the boundary of the application and the encapsulation of the business logic and use cases, and should be the least affected function even if stack migration occurs, as a point of risk in testing.

The service layer is no longer dealing with concrete vendor services but with abstract interfaces, which makes it easy to mock out dependencies and optimize the testing process for the service layer.

Old wine from Serverless

The authentication

No matter what technology stack you use, microservices, Serverless, low-code, etc., Authentication and Authorization are always issues you can’t escape. However, there is no difference in the pattern of licensing problem solving in different technology stacks. To unify the language here, the following use of “verification” refers to “authentication” and “authorization”.

Taking the microservices architecture as an example, it is unlikely that each set of microservices serving behind an interface will have an independent validation mechanism. If you do, you need to solve more than the following problems:

  • If each set of microservices has validation logic that needs to be shared, spreading similar code across different code bases incurs the cost of shotgun modification in the future
  • Specific business developers need to learn authentication logic that they should not care about, and exposed authentication code is inevitably coupled with business code
  • If we had to verify permissions once before accessing each set of microservices, it would add to our system latency and rework overall.

So we usually do validation at the Edge Layer of the system. It is recognized as industry best practice that we treat all calls outside our borders with suspicion and trust services within them unconditionally.

The embodiment of this boundary in modern enterprise architecture is the API Gateway. It’s worth emphasizing that authentication is just one of the responsibilities of the API Gateway, but it can do much more than that.

The official validation mechanism for AWS Lambda is the same:

The request from the client on the far left in the figure above must be authenticated by the API Gateway before it can access subsequent Lambda or EC2 services.

Having answered the question “where to verify”, we will continue to answer the second question by following the process above: how to verify.

Since each of the services/functions discussed above should not implement validation separately, the AWS API Gateway provides a validation mechanism called a Custom Authorizer (also known as a lambda Authorizer), Because authorizer is implemented by lambda functions), it works as follows:

  • When a client request arrives at the API Gateway, the Authorizer function can retrieve key information for validation, such as JWT, from the request
  • Assuming that the client logs in with Auth0, the Authorizer requires that the JWT be authenticated by Auth0
  • If the validation succeeds, the Authorizer returns the policy, and the API Gateway determines when to allow access to subsequent resources

It is not difficult to see from the above flow that validation passes or fails determines the code implementation from authorizer. However, whether you use JWT or SAML for validation, you still follow the classic process of traditional OAuth. I don’t want to write too much about OAuth, but the following flow chart may bring back many memories for you

In the above AWS authentication process, when the client sends a request to the AWS Lambda, we first need to authenticate with the Authorization Server before allowing the resource to be returned to the client. It is easy to see that authorizer is the embodiment of Step 6 in the flowchart

One explanation for the potential “error” : You might think that OAuth is not suitable for situations like AWS API Gateway because OAuth is essentially designed for “authorization” operations, which determine what resources you can access; The API Gateway example is an “authentication” scenario, where you are a legitimate user or not.

Your understanding of OAuth is right, so let’s continue with an in-depth explanation of OAuth: OAuth is essentially a delegation protocol, which opens up a way for software programs to access third-party resources as users. The client in OAuth does not refer to the user who owns these resources, but the application program entrusted by the user. For example, in the scene where netease Album needs to access the photos in the user’s Google web disk, client actually refers to netease Album. Because it only cares about authorized resources, it can dispense with caring about who and how authorized those resources are. To be brutally honest, netease Albums only cares about whether it can obtain the token that allows it to call Google API to get the file information.

Going back to the API Gateway example, the resources represented by the API are usually public, and AWS, as the natural owner of the resource, does not care who the client is behind it, nor does it have the right to limit how many applications the user may authorize. It only cares about the authentication credentials in the request. In this respect, lambda’s verification work coincides with OAuth’s.

If we abstract OAuth again, we can call it token-based authentication. Compared with traditional user name and password authentication, it has the following advantages:

  • Security is improved by not exposing user names and passwords to customers
  • The access duration is limited and the access permission can be revoked at any time
  • Fine-grained controls the resources that users can access

For example, Azure Serverless supports token based authentication. After you set the HttpTrigger authLevel parameter to function, You need to get the Function Key value from the UI and put it in an HTTP header named x-function-key to get the request to Function, otherwise

It can be seen that when solving the authentication problem in the Serverless scenario, we still rely on the valuable wealth left by predecessors.

Deploy Serverless

Finally, a brief mention of Serverless deployment

Flexibility and lightness are one of serverless’s key selling points, and the super easy deployment is the best example of these features. AWS, for example, supports Lambda deployment by uploading a ZIP file; Firebase supports Cloud Function deployment on the CLI. Azure Serverless develops an Azure Functions plug-in for VSCode that allows you to deploy Azure Function in one click during IDE development.

If your understanding of these improvements is simply that we can get code into production more quickly without having to do a lot of work, then I suggest you avoid them. Because manual deployment in software delivery is a class of antipattern behavior: This kind of one-step manual deployment means that you have to do manual testing to verify that the function is working, and if you deploy directly to production without testing the commissioning environment, it makes it impossible to verify that the assumptions made in development are still true in production. There is no supporting mechanism to ensure that our code rolls back to the last stable version even after a problem occurs. The ideal of continuous delivery can be summed up by quoting the book Continuous Delivery: Software distribution can (and should) be a low-risk, frequent, cheap, rapid, and predictable process.

So serverless delivery still needs to be managed; configuration management, compilation, automated testing, grayscale publishing, and so on are still available to Serverless. So why don’t Serverless take the next step and provide us with a richer delivery solution? The answer to that question is yes and no.

The reason for saying yes is that existing platform tools already support such goals. For example, the Azure DevOps platform allows us to create pipelines for Serverless applications and manage artifacts after each build; Azure Serverless also supports grayscale publishing (Deployment slots), which gives you the option to publish your built code to a staging environment first and then replace the existing Production code with one click when it is verified.

The negative answer is also valid because all of these facilities are not tailored just for Serverless. Almost all Azure services can be deployed through the Azure DevOps platform, and all services are treated equally on Azure DevOps.

So it’s not hard to see that Azure DevOps isn’t selling pre-built standardized processes, but rather common capabilities that support customization. They don’t stop there because of their limited coding capabilities, but because they can’t abstract further at the code level.

If you have any experience in coding, you know that the most difficult part of software development is not the coding, but the initial programming, and the appropriate integration of patterns. However, you should also understand that even familiar MVC patterns have different meanings in different programming languages, and can even be implemented differently in the same programming language (e.g. Angular’s bidirectional binding pattern for backbone.js’s event mechanism). We can’t define it exactly in the same code.

Continuous delivery knowledge has a similar nature, and most of the time we need to design the delivery process for the project with a specific focus. For example, choose the right Git workflow for your team, determine if it’s necessary to add smoke tests to your project, and so on. These can’t be calculated in code, which is often the hardest part, because you need to evaluate the project and communicate with the team before you decide on a solution. Because there are so many uncertainties in the DevOps process, and projects vary so much from one another, the only thing a platform can do is find the greatest common diator (all projects need deployment, all projects need grayscale publishing, all projects need environment variable management), Or it will package all possibilities as common capabilities for you (such as Azure DevOps platform)

The end of the

We can only talk about it here because of space. I hope you can draw a clearer outline of serverless from the above text. It’s hard to convince ourselves that Serverless is a pure innovation, more of a legacy. But there is no need to be depressed, this is the norm of technological innovation, innovation is not a castle in the air, it is only inherited from the classic, can surpass the classic.

This article is also published in my Zhihu column: Hitchhiker’s Guide to Front-end Technology. Please subscribe