ECS Architecture & Deployment for .NET
Overview
This guide outlines the blueprint for containerizing a .NET 8/9 application and running it on Amazon ECS, breaking down the core differences between the Fargate and EC2 launch types using infrastructure-as-code and native JSON configurations.
🏗️ 1. Architecture Flow: ECS Control Plane vs. Data Plane
graph TD User([Client / Browser]) --> ALB[Application Load Balancer] subgraph AWS Cloud [AWS Region] ALB --> TargetGroup[Target Group :8080] subgraph ECS Cluster ["ECS Cluster (Managed Control Plane)"] subgraph FargatePath [Launch Type: FARGATE] FTask1["Task: .NET API Container"]:::dotnet FTask2["Task: .NET API Container"]:::dotnet end subgraph EC2Path [Launch Type: EC2] EC2Host["EC2 Instance (t3.medium)"] --> EC2Task1["Task: .NET Container"]:::dotnet EC2Host --> EC2Task2["Task: .NET Container"]:::dotnet end end end TargetGroup --> FTask1 & FTask2 & EC2Task1 classDef dotnet fill:#512BD4,stroke:#fff,stroke-width:2px,color:#fff;
🐳 2. The Foundation: Production .NET Dockerfile
Before deploying to ECS, your .NET application needs a multi-stage container build optimized for Linux containers.
# Stage 1: Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApi.csproj", "./"]
RUN dotnet restore "MyApi.csproj"
COPY . .
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 2: Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# Create a non-root user for security best practices
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser
COPY --from=build /app/publish .
# Expose modern default port for ASP.NET
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "MyApi.dll"]
📝 3. The Blueprint: ECS Task Definition (JSON)
An ECS Task Definition is your application’s blueprint. It tells ECS what Docker image to pull, how much CPU/Memory to allocate, and how to map ports.
Universal Compatibility
This exact Task Definition schema works for both Fargate and EC2 launch types. The difference is dictated solely by how you provision the service/cluster, not the task definition itself.
{
"family": "dotnet-api-task",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789012:role/MyDotNetAppApplicationRole",
"containerDefinitions": [
{
"name": "dotnet-api",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-dotnet-api:latest",
"essential": true,
"portMappings": [
{
"containerPort": 8080,
"hostPort": 8080,
"protocol": "tcp"
}
],
"environment": [
{
"name": "ASPNETCORE_ENVIRONMENT",
"value": "Production"
},
{
"name": "DOTNET_GC_HeapHardLimitPercent",
"value": "75"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/dotnet-api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}Critical .NET Setting on Fargate
Setting
DOTNET_GC_HeapHardLimitPercentis essential on Fargate. Because Fargate tasks have strict, unyielding memory ceilings, letting the .NET Garbage Collector dynamically consume host memory can cause a sudden OOM (Out of Memory) crash.
⚙️ 4. Infrastructure Provisioning (Terraform Code)
Here is how you provision the compute layer in Terraform, illustrating the switch between Fargate and EC2.
Option A: ECS Service using Fargate (Serverless)
resource "aws_ecs_service" "dotnet_fargate_service" {
name = "dotnet-api-fargate"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = data.aws_subnets.private.ids
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "dotnet-api"
container_port = 8080
}
}
Option B: ECS Service using EC2 (Managed Instances)
To use EC2, your cluster must have registered EC2 container instances running the ecs-agent, and your service launch type changes:
resource "aws_ecs_service" "dotnet_ec2_service" {
name = "dotnet-api-ec2"
cluster = aws_ecs_cluster.ec2_cluster.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 4
launch_type = "EC2"
# Placement strategies can be defined for EC2 instances
ordered_placement_strategy {
type = "spread"
field = "attribute:ecs.availability-zone"
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "dotnet-api"
container_port = 8080
}
}
⚖️ Summary Comparison Table for your Knowledge Base
| Feature / Dimension | ECS on Fargate | ECS on EC2 |
|---|---|---|
| Compute Management | Fully serverless (AWS manages underlying VMs). | You manage, patch, and size EC2 instances. |
| Scaling Dynamics | Spun up via software invocation; cold starts take ~30s. | Instantly scaled if EC2 warm pool capacity exists. |
| Cost Profile | Pay-per-second based strictly on task resource configuration. | Pay for running instances 24/7 (cheaper if utilization is high/steady). |
| Customization | Low (cannot modify kernel parameters or install host agents). | Absolute root access to host OS and storage layers. |