After some time of developing serverless systems (especially on AWS) I take a look back and try to summarize what I have learned so far.
First, I should explain what I mean by saying serverless as long as this became already a buzzword and could have different meanings.
Let think about a serveless system simply as about a system with only managed, elastic (auto-scaling) resources. This means, hardware (CPU, memory, storage) and platform (OS, runtime) provisioning is always included as a part of the cloud-provider service.
- Everything is as serverless as possible, only managed, auto-scaling services are used.
- The development of a service is finished, once an infrastructure template exists and runs without error.
- Every client communication with the cloud services is possible only via an HTTPS facade API.
- Design of an API is use-case- and business-driven as well as the architecture-components.
- Implementation has no impact on an API and it's completely hidden behind it.
- In the implementation, the business-logic is decoupled from the cloud-provider.
- References to resources are handed over to services via environment variables.
- For each service there are unit + integration tests.
- Each service is accessible only via API and events.
- Multitenancy (when used) is a solid, mandatory part of every service.
Everything is as serverless as possible
On can spin-up thousands of functions in a second and doesn't have to take care of load-balancing or any other resource management - this all is done by the provider of the serverless service (by the definition). The natural consequence is that if only one part of the system is non-serverless, the whole system becomes failure-prone.
Consider a serverless function connected to a non-serverless database. After spinning-up an amount of function greater than the access limit of the database, next calls of the function will freeze and eventually fail with a timeout.
Isolation of a failure and fall-back strategy for dealing with a non-serverless resource in a severless system is crucial for success.
The development is finished, once an infrastructure template exists
Infrastructure as Code is an important part of a serverless deployment. The template of the service infrastructure as an input for the deployment process encourages the so-called DevOps culture.
Client communicates with the services only via its HTTPS API
The client code should never use any provider-related SDKs. The communication must be agnostic and independent.
This frees your clients from the vendor lock-in, which is definitely a good idea, because the client code is usually not in the hand of the services developers - freeing clients makes services more independent (for example, a service doesn't break any client by changing a cloud provider).
Design is use-case- and business-driven
Following principles of Domain-Driven Design (DDD) will provide a great insight of how to separate code into services.
If helps to keep the API stable as domain seems to be more relevant for clients (customers) than technical aspects.
Implementation has no impact on an API
Connected to the previous one, not only an API must be domain-driven, but any implementation details must not leak into it.
I guess this is not surprising as it counts to the very basic principles of software design in general.
Business-logic is decoupled from the cloud-provider
Serverless is a kind of deployment which means it should be implemented in the very outer layer of code calling domain logic as its dependency (never the other way around).
Resources are referenced via environment variables
The point III of The Twelve Factors in practice.
The references should be resolved automatically in service templates in deployment time.
Each service has unit and integration tests
Test-driven Development (TDD) is a great method to build a software of high quality.
Don't accept any service without tests as finished.
For the whole product there should be a set of end-to-end test for testing whole scenarios from a client point of view on APIs.
Each service is accessible only via API and events.
All the service's internal resources (e.g. file storage, database etc.) are hidden and from outside denied for access.
It has been already said that implementation must not have any impact on the API, this point says the same from the other side: no other service running in the same environment must access a service's internals (even if this is technically possible).
Multitenancy is a solid, mandatory part of a service.
When sharing services among customers (the opposite would be to build a "silo" stack for each customer), the multitenancy must be a part of every service - considered from design and the very beginning of development.
- Names of the deployment stacks follow the pattern "<service-name>-<stage>".
- Automatic tests are executed in the development-stage DEV, acceptance and manual tests in the test-stage QA.
- Developers (stage Dev) and testers (stage QA) have no access to the production (stage Prod).
- The deployment of product artifacts is realized and implemented as a build pipeline (Continuous Delivery).
Names of the deployment stacks follow the pattern "<service-name>-<region>-<stage>"
It's important to bring order to the system resources and make the management of them human-friendly. In this case, similar as in code, the naming is very helpful.
Each resource must be deployable in every region within the account, which is enabled by using the region name a a suffix.
The name of the stage (
prod, etc.) as a suffix allows the developer to deploy multiple stages inside a single account (for test purposes or costs optimizing).
Developers and testers have no access to the production
This is possible thru multiple deployment stages strategy where different stages are deployed as a continuous process for different purposes till the last - production - stage.
Deployment implemented as a build pipeline
Building, testing, deploying from templates into stages is realized via a single (per product) pipeline.
- Principle of least privilege is used for every resource.
- Encrypt everything.
Principle of least privilege is used for every resource
Don't give a service rights to do more than it actually should do. This could save you from an unpleasant surprise.
Similar for multi-tenancy systems: the isolation of customer data must be enforced directly by underlying constrains, not only by the logic in code.
All the communication and all the data must be encrypted.
It's a part of the contract where the keys are to be found.
Generally, the serverless development is not much different from a standard software development. The biggest difference is in the possibility (duty as well) for a developer to be an active part of the deployment process.
Above I tried to summarize a few basic principles useful to follow in such a development.
But the biggest lesson I have learned: (software) principles should never lead to dogma; they should provide a hint on unclear crossroads.
Have a serverless day!