What does it mean to be cloud native? One of the best ways to answer that question is to think in terms of how cloud native applications are designed.
To a certain degree, this is an ambiguous topic. There is no specific type of application design pattern, coding language or hosting infrastructure that you have to use for your application to be cloud native. Cloud native apps come in many shapes and sizes.
That said, most cloud native apps share certain design principles or follow certain patterns. Whether or not you choose to adhere to each cloud native design concept when building an app, you should at least be aware of approaches to cloud native development and how to take advantage of them.
To provide guidance, this article walks through six key cloud native application design principles and explains how to operationalize each one. Again, your mileage may vary, because there are many ways to “do” cloud native. But in general, if you are thinking in a cloud native way and embracing cloud native DevOps, you’ll be using these principles as your guide to application design.
1. Loosely coupled architectures
A key characteristic of cloud native development is using loosely coupled architectures. A loosely coupled architecture is an application design strategy in which the various parts of an application are developed, deployed and operated independently of each other.
Loosely coupled architectures are important to cloud native computing and DevOps for several reasons:
- Simplicity: By breaking complex applications into smaller, independent parts, developers can simplify the management of the entire application development lifecycle, from source code to runtime.
- Scalability: It’s faster and easier to scale individual parts of an application than it is to scale a large, monolithic application.
- Resilience: In a loosely coupled architecture, the failure of one application component typically does not cause the entire application to crash. This means that loosely coupled architectures increase resilience and reliability.
- Updates: Along similar lines, loosely coupled architectures make it easier to update application functionality because DevOps teams can update just the parts of the application that need to be changed, rather than having to redeploy a new version of the entire app.
There is no specific formula to follow for implementing a loosely coupled architecture. Probably the most common approach, however, is to break an application into a set of microservices, with each microservice responsible for handling a different type of function. In a shopping cart app, for example, one microservice might allow customers to add items to a cart while a second one allows them to remove items, and a third keeps track of the total value of items in the cart. You can then deploy each microservice in a separate container.
To ensure that your application architecture is loosely coupled, it’s critical to avoid creating “tight” dependencies between each part. For example, you typically would not want to have two microservices depend on the same database, or even the same set of integration tests. If you do, you lose the ability to operate and update each microservice independently of the others.
2. API-first design
API-first design is the principle that APIs are treated as “first-class citizens” when you develop an application. Instead of writing application source code and only later implementing an API, you design your APIs first and create source code based on them.
For cloud native applications, an API-first design approach is an obvious way to ensure that your application can integrate seamlessly with other applications via APIs. In addition, API-first design can help you to create a loosely coupled architecture using internal APIs that you create at the same time that you start developing your application.
Because there are many types of APIs, and many ways that an application could interact with APIs, there are no hard-and-fast rules for operationalizing API-first design principles. What matters most is simply ensuring that you think about how your APIs will work early in the development process. You should also factor API functionality into every decision you subsequently make as you update your application. API testing, too, should be part and parcel of your broader application testing process.
3. Scalable state management
Traditional applications typically manage state – meaning the data that they need to operate – by storing the data in the memory and/or file systems of whichever server hosts them.
With cloud native applications, however, developers are typically more sophisticated about the way they manage application state. In some cases, they may choose to design applications that are “stateless,” meaning the applications themselves don’t directly store any data at all. Instead, the applications could use APIs to manage access to the data they need to operate.
Other cloud native applications may be stateful, which means they do store data persistently. However, stateful cloud native applications typically don’t rely on local file systems to house that data. Instead, they might connect to cloud-based object storage systems, like AWS S3, via APIs to store data. Or, they could use storage resources that are shared across a cluster, such as Kubernetes PersistentVolumes. The advantage of this approach is that the storage is more scalable. Your application is not limited to the storage capacity of a local server.
What this means is that, when you design a cloud native app, you should answer these questions:
- Does my application require state management at all? If you can make your application stateless, you simplify the development process, while also reducing potential issues related to data security, backup and storage costs.
- How should I manage application state? While storing data in local file systems may make sense if you just need to test out an application, you’ll typically want a more sophisticated and scalable storage solution – like shared cluster storage – for stateful cloud native apps that you ship to production.
4. Environment-independent execution
One of the principles behind cloud native computing is that applications should have the flexibility to run anywhere. In other words, you should be able to take a cloud native app and deploy it on any public cloud or any type of operating system – within reason, at least.
Achieving this requires committing yourself to environment-independent execution when you design an app. An app that can be executed in any environment is not bound to a particular hosting platform or type of configuration in order to run.
One way to implement environment-independent execution is to use containers. Because most of the environment configuration for a containerized application exists in the container itself, the application will work the same way regardless of where you deploy it. For example, consider a line such as the following within a Dockerfile:
Any containers that run based on this Dockerfile will run applications on Ubuntu 18.04, since that is the base image specified in the Dockerfile. This will be true even if you were to deploy the containers on, say, a Fedora or Red Hat Enterprise Linux host.
Serverless functions also allow you to create environment-independent applications. With serverless functions, you simply write code, then upload it to a serverless compute environment, such as AWS Lambda. The code will execute based on triggers that you define. There is no need to configure the host operating system.
It’s worth noting that you will typically run up against some limitations when designing apps to be environment-independent. One is that you can’t run a Linux-based container on a Windows host, or vice versa. Another is that most serverless computing platforms only support certain types of programming languages, so they don’t allow you to execute any type of app in any type of environment.
In general, however, a well-designed cloud native app will support a much more flexible range of deployment environments than a conventional app that is tied to a specific operating system.
5. Cloud-agnostic design
Along similar lines, cloud native developers typically design their applications to be cloud-agnostic, which means the applications can run in any public cloud – or, for that matter, any private or hybrid cloud – with minimal reconfiguration required when moving between one environment and another.
The best way to achieve this goal is to avoid development and deployment tools that are tied to specific cloud platforms. Although vendor-specific development tools, such as AWS Elastic Beanstalk and Azure App Service, are useful in cases where you are fully committed to a particular public cloud, it’s generally a best practice to stick to third-party software delivery tools that can work on any cloud.
Cloud native developers should think as well about how they package applications. A containerized app can run in any cloud, or even any on-premises environment with Docker or Kubernetes present. On the other hand, an application binary that is directly executed on a host server may only work with a certain operating system or certain environment configuration, which means you’ll have to do some tweaking if you want to move between one cloud platform and another.
6. Standards-based telemetry
Another way to simplify cloud native computing is to design applications that use standardized telemetry frameworks.
Telemetry is the resources within an application that make it possible to collect data like metrics, logs and traces from the app, in order to track its health and address performance problems or errors.
In the past, applications typically either exposed telemetry data using custom telemetry logic that developers implemented themselves on an app-by-app basis, or they exposed metrics to the operating system so that agents external to the application could collect monitoring data about it. The former approach required a lot of development effort. The latter often limited the amount of data that could be collected.
Today, cloud native developers solve these challenges by using frameworks like OpenTelemetry. OpenTelemetry is a community-developed, set of tools that developers can integrate into applications to expose telemetry data. Because OpenTelemetry operates based on community-accepted standards, any monitoring tool that supports OpenTelemetry can collect data easily from any application that uses OpenTelemetry. OpenTelemetry also offers the benefit of automatically configuring applications to expose rich telemetry data, without requiring developers to write custom telemetry code themselves.
So, by using a standards-based telemetry framework like OpenTelemetry for your cloud native app, you get the best of both worlds: Deep observability on the one hand, and low telemetry development effort on the other.
Again, there is no universal set of rules you have to follow to design a cloud native app. But most cloud native apps are oriented around principles such as loosely coupled architectures, API first design, a smart approach to state management and a focus on being as environment-agnostic as possible. The ability to expose telemetry data using developer-friendly, standardized frameworks like OpenTelemetry is a common feature of cloud native applications, too.