Let's explore the basics of Cloud Native apps and how to architect .NET apps for a future in the cloud.
As an ever-increasing number of software companies start running their apps in the cloud, you might be wondering what you can do to ease the transition for your apps. Even if you are not planning on making the jump any time soon, designing your apps to be cloud native doesn’t require a substantial investment and it will make your life easier. In this article, you’ll learn a handful of patterns and practices you can follow to make your applications more cloud compatible.
What does 'Cloud Native' mean?
A term we’ve started to hear increasingly is cloud native. Doing a web search on cloud native apps often yields a bunch of vague explanations. The best definition comes from the Cloud Native Computing Foundation - a consortium of organizations promoting the cloud native architectural style. We quote from the CNCF Charter:
Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds. Containers, service meshes, microservices, immutable infrastructure, and declarative APIs exemplify this approach. These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined with robust automation, they allow engineers to make high-impact changes frequently and predictably with minimal toil.
The Cloud Native Computing Foundation seeks to drive adoption of this paradigm by fostering and sustaining an ecosystem of open source, vendor-neutral projects. We democratize state-of-the-art patterns to make these innovations accessible for everyone.
The short version is that cloud native applications are a collection of stateless services that run in a containerized environment. Realistically speaking, this most likely means Docker containers running in Kubernetes. While cloud native usually means hosting apps in containers, the techniques in this article also apply to apps in PAAS services and on-prem cloud solutions.
Another term you might run into when researching cloud native applications is the concept of a '12 factor app'. This is an application methodology created by the folks at Heroku to describe how to make applications more compatible with cloud services like Heroku. Many of the design principles in this article are based on 12 factor concepts; but where the 12 factor framework is platform independent, we’ll focus on the .NET implementation of those principles.
The big advantage of cloud native development is that you are no longer dependent on a specific hosting provider. If you’re working in a large corporation, you don’t need to wait two months for an internal infrastructure team to click some buttons for you. If you’re already using a public cloud and you don’t like it, you can easily switch to another one. Your application is portable, scalable, and easy to manage. By following these principles, you can get the infrastructure of your application out of your way, so you can focus on building features for customers.
Additionally, following the principles below will make your application easier to deploy. Even if you don’t go to the cloud, you still win. Easier deployments mean you can go faster, which means you can go home sooner. More features in less time are good for everyone.
Automate Your Builds & Releases
If you are doing manual builds and releases for your software, take some time to automate them - you won't regret the effort. Tools like Azure DevOps make it easy to put together automated builds. No project that hits production should be without an automated build and release process. Even small projects should be automated. Automation is critical, especially as you scale your application. Additionally, make sure your automated builds run your tests. Automated testing allows you to deploy faster while maintaining code quality.
Use .NET Core
.NET Core has many advantages over the full .NET Framework. It’s leaner, faster, easier to use and it runs cross-platform. Each new version of .NET Core supports more full .NET Framework APIs and features. If you’re building a web application, .NET Core should be your default choice going forward.
For cloud applications, running cross-platform is a huge advantage. While you can run full .NET framework apps in PAAS services like Azure Web Apps, most container hosting platforms cater to Linux based containers. Even Azure Kubernetes Service only runs Linux containers (as of April 2019, Windows support is on the way). So it is in the best interests of the devloper to go platform agnostic - .NET Core allows just that.
Don’t Store Configuration in Your Application
Most applications have configurable settings that vary from environment to environment. These include database connection strings, encryption keys and feature toggles. It is usually a really bad idea to host any configuration settings within your application itself. Storing secrets in your application is often considered an open invitation for hackers.
There are two related application patterns - one is worse than the other. In some legacy applications, app secrets like encryption keys are hardcoded into the app as a constant. This practice is highly insecure. Anyone who can find your source code or decompile your binaries can easily see these fields and exploit them.
Another common practice is using a clear text config file to store application configuration and secrets. Lots of people do this, and while it’s better than storing configuration settings as hardcoded constants, it’s not an ideal configuration solution. This is especially true if you have publicly available source control. There are automated scripts that scan GitHub looking for API keys and other app secrets. Check in the wrong configuration setting, and your application is now a cryptocurrency miner.
Instead, developers should use environment variables to store app configurations. Developers could then use build tools to inject those settings into environments when deploying applications. One of the great things about .NET Core is that there’s no difference between accessing config settings that are injected from the environment or from a config file. For local applications, .NET Core has a user secrets file that can store local configuration settings. Develoeprs can use the secrets file for local development and build tools to set environment variables during deployments.
Build Stateless Services
Most cloud-based applications are distributed across a group of servers. Developers should treat these servers as cheap commodities that can go down at any moment. If you’re running in Kubernetes, it will build and destroy running processes as it sees fit. Developers should design applications accordingly. One way to prevent issues, is to avoid storing state in your application process. Even if one of your application processes gets terminated, another one can pick up where the dead one left off. Never assume a group of user requests is going to be handled by the same process.
In .NET, this means avoid using server specific features like Session State and In Memory Cache. Both features store state data in your process. Also, developers should avoid storing files on the server application process runs on. It is advisable to use an external storage mechanism, preferably one hidden behind an abstraction.
Abstract Infrastructure Dependencies Using Dependency Injection
When building any application, you should abstract dependencies using interfaces and then inject concrete implementations into your application using dependency injection. Not only does this practice make your application easier to unit test, but it also makes it easier to swap out infrastructure dependencies with cloud versions down the road.
High priorities for abstraction include databases, file stores, cache mechanisms, and any other state storage. Developers should abstract non-infrastructure code to make unit testing more manageable; but if you’re looking for cloud compatibility, abstract your infrastructure dependencies.
Avoid Machine-Based Dependencies
Developers should not store application libraries using machine-based mechanisms like the Global Application Cache (GAC). Instead, use NuGet packages to distribute shared libraries. In general, developers should avoid any machine-based setup, whenever possible. If your application requires a giant PowerShell script to build a folder structure and GAC libraries to run locally, that’s a signal that you have some refactoring to do.
Working Towards a Cloud Native Future
The patterns in this article should help developers build applications that are easy to transition to the cloud. Even if you aren’t going to the cloud, most of these practices encourage the creation of stateless services that are easy to build, maintain, and deploy. If you’re developing a new greenfield application, keep these principles in mind as you develop your architecture. Avoid creating technical debt that will slow you down later. If you’re working on an existing application, take some time to refactor it to use these principles. Together, off to the cloud we go!