50 shades of .NET on AWS
How to host our .NET applications on AWS
TL;DR: this post details the decision trees we use to decide how to host our .NET applications on AWS depending on the use case.
I Love My Local Farmer is a fictional company inspired by customer interactions with AWS Solutions Architects and AWS Developer Advocates. Any stories told in this blog are not related to a specific customer. Similarities with any real companies, people, or situations are purely coincidental. Stories in this blog represent the views of the authors and are not endorsed by AWS.
In my last post, I’ve discussed how we’ve cut out a part of our .NET monolith application to migrate it on AWS. This post is about the next challenge we faced: “how to host our .NET applications on AWS?”.
As there are many options, I discuss here the decision trees we’ve built to select the best options to host our .NET apps depending on their characteristics and our requirements.
In this post, I’ve explained why our leadership has taken bold decisions regarding our migration strategy. We are not an IT company building technologies or operation technologies for others. We are an online marketplace selling the produce of farmers to consumers using technologies to support our business. As we are challenged on our market by new competitors, they asked us to accelerate and to prefer SaaS or managed services for everything that is not related to our core business. They asked us to concentrate all our efforts on building and improving our business critical applications and to reduce all the efforts for operating them.
This lead to a few core decisions that apply across all our engineering teams. They are a baseline for the decision tree we’ve built:
- Always prefer fully managed services to self-managed services when available and applicable
- For container orchestration, use Amazon Elastic Container Services (ECS) rather than Amazon Elastic Kubernetes (EKS) as it is more opinionated and straightforward to use
- For container execution, when it matches our hardware requirements, use AWS Fargate, their fully managed serverless compute engine for containers to avoid the burden of operating our own container hosts
This post also details why and how we’ve started to break our .NET Framework monolith into microservices to move gradually into AWS and recover agility and flexibility. So being able to host .NET microservices is part of our requirements.
You may also know that .NET Framework 4.8 is the last version of .NET Framework. We are on our way to migrate to .NET 6+ for everything new or that will last for a while. Thus, we need options to host .NET Framework and .NET 6+ applications.
Based on these requirements and needs, we’ve worked with our Operations team to identify four main use cases that we need to cover:
- Hosting our existing .NET Framework applications with low to no modifications
- Hosting .NET Framework microservices that we’ve just extracted from our .NET Framework monoliths and containerized
- Hosting containerized .NET 6+ microservices that we’ve ported from .NET Framework
- Hosting brand new .NET 6+ microservices
Our Operations team has categorized our use cases within the 7 Rs of Cloud Migration Strategy as follow:
For each of them, we’ve built a decision tree to decide the best option to host our .NET applications. Let’s walk through these decision trees together.
Rehost our existing .NET Framework applications
We have hosted our .NET Framework applications on Windows Server virtual machines for years. Our Operations team is highly skilled at operating Windows Server and IIS web server farms. We don’t plan to accelerate the release cadence of our monoliths. We rather prefer to invest into breaking them into microservices. We will concentrate our efforts on our new microservices and setup proper CI/CD pipelines.
Thus, for these applications, we have decided to continue to host them on virtual machines. On AWS, it means to host them on Amazon Elastic Cloud Compute instances (EC2). To stick with our corporate guidance, we don’t want to run self-managed EC2 instances to lower operations burden. To avoid this, AWS provides AWS Elastic Beanstalk. It will manage our EC2 instances for us as load balancers and auto scaling groups. The auto scaling group will automatically increase or decrease the number of instances based on a set of rules we define.
Replatform our .NET Framework microservices into containerized microservices
To standardize our packaging and deployment strategy, we have selected container as our corporate standard format for applications we want to modernize. It will allow us to streamline the deployment of our applications across our different environments and speed the process thanks to CI/CD pipelines whatever the programming language used by development teams.
Here, as we are discussing .NET Framework microservices, it means we need to run Windows containers. Fargate, the AWS serverless compute engine for containers supports Windows containers. However, it doesn’t offer us the all range of hardware capabilities offered by EC2 instances.
If it meets the workload hardware requirements, our standard option for this use case will be to host our .NET Framework containerized microservices on Fargate with ECS managed Windows containers.
If it doesn’t meet the hardware requirements, we will fall back on ECS with EC2 Windows instances. We could also have considered EKS, but it has been discarded at corporate level as I mentioned previously.
Refactor our .NET microservices to .NET 6
As .NET Framework 4.8 is the last version of .NET Framework and will not get any new features, we will port our .NET microservices to .NET 6+ and follow the future of .NET.
We could still run the refactored .NET microservices on Windows Server. Our Operations team has the required skills. However, they are also highly skilled at operating Linux. Running the .NET microservices on Linux is dramatically less expensive, as we can cut the Windows license costs.
Our first choice will be to run our refactored .NET containerized microservices on Linux. In the rare cases where we rely on a dependency that prevents us to move out of Windows Server, we will fall back on the same decision tree than for our .NET Framework microservices.
Running our .NET containerized microservices offer us several options.
For public Web application or API, AWS App Runner is the most appealing choice. Remember, our leadership asked us to sit in the passenger seat for operating our infrastructure. App Runner is a fully managed service for running Web application or API with no prior infrastructure experience required. Under the hood, it automatically deploys our application, load balances traffic with encryption and scales up and down the number of container instances to meet our traffic needs. But we don’t need to worry about all of this. We just need to provide it with the container image we want to run. So, App Runner will be our privileged choice for all our public Web applications or APIs.
However, not all our .NET microservices aim at being public. In fact, most of them will be private: private Web applications, APIs, background worker process or batch jobs. In this case, our favorite hosting option will be Fargate with ECS managed Linux containers. This option will allow us to deploy our container instances into private subnets. If a workload has specific hardware requirements, we still have the option to fall back on ECS with EC2 Linux instances.
Rebuild to brand new .NET 6+ serverless applications
For all our new .NET microservices and applications, we want to favor serverless microservices and applications. We are a developer oriented organization. While we recognize the benefits containers bring to the table, we do believe that managing container definitions and Docker files is not a developer thing.
We think that the future is in event-driven applications and systems. So, AWS Lambda, which is an event-driven compute engine, will be our favorite hosting option for all our new .NET 6+ microservices.
The .NET Lambda function programming model is pretty straightforward. The code below shows the default function handler method you get when you create a C# Lambda function for processing Amazon Simple Queue Service (SQS) events. SQS is a fully managed queue service. It can trigger your Lambda function each time a message or a batch of messages reach the queue. Your function handler receives an SQS event that contains the messages and all you have to do is to write the logic for processing the messages.
/// This method is called for every Lambda invocation.
/// This method takes in an SQS event object and can be used
/// to respond to SQS messages.
/// <param name="evnt"></param>
/// <param name="context"></param>
public async Task FunctionHandler(SQSEvent evnt,
foreach(var message in evnt.Records)
await ProcessMessageAsync(message, context);
AWS, really, offers 50 shades of .NET. Deciding which way to host is the right one for a workload may seem overwhelming at first.
We have built these decision trees to avoid the discussions for each .NET application we want to deploy on AWS. It will enable quick decision in the vast majority of our use cases.
Here is a quick summary for the straightforward cases:
- We will host .NET Framework monolith applications on AWS Elastic Beanstalk.
- We will host .NET Framework containerized microservices on AWS Fargate with Amazon ECS managed Windows containers.
- We will host .NET 6+ containerized microservices on AWS Fargate with Amazon ECS managed Linux containers.
- We will host new .NET 6+ microservices on AWS Lambda.
If you are a .NET shop, I do encourage to get inspiration from our decision trees and build your owns to fast-track your decisions. It is always frustrating to waste time with these questions at each project start.