Enable AWS Application Signals for .NET on ECS
Overview
This guide provides complete steps to enable AWS Application Signals for ECS services (both EC2 and Fargate launch types), including distributed tracing, performance monitoring, and service mapping.
Prerequisites
- Services running on ECS (EC2 or Fargate launch types)
- Applications using .NET language
Implementation Steps
Constraints: You must strictly follow the steps in the order below, do not skip or combine steps.
Step 1: Setup CloudWatch Agent Task
1.1 Add CloudWatch Agent Permissions to ECS Task Role
const taskRole = new iam.Role(this, 'EcsTaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess'),
iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'),
],
});1.2 Create CloudWatch Agent Log Group
const cwAgentLogGroup = new logs.LogGroup(this, 'CwAgentLogGroup', {
logGroupName: '/ecs/ecs-cwagent',
removalPolicy: cdk.RemovalPolicy.DESTROY,
retention: logs.RetentionDays.ONE_MONTH,
});1.3 Add CloudWatch Agent Container to Each Task Definition
const cwAgentContainer = taskDefinition.addContainer('ecs-cwagent-{{SERVICE_NAME}}', {
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest'),
essential: false,
memoryReservationMiB: 128,
cpu: 64,
environment: {
CW_CONFIG_CONTENT: JSON.stringify({
"traces": {
"traces_collected": {
"application_signals": {}
}
},
"logs": {
"metrics_collected": {
"application_signals": {}
}
}
}),
},
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'ecs',
logGroup: cwAgentLogGroup,
}),
});Step 2: Add AWS Distro for OpenTelemetry Zero-Code Auto-Instrumentation to Main Service
2.1 Add Bind Mount Volumes to Task Definition
const taskDefinition = new ecs.FargateTaskDefinition(this, '{{SERVICE_NAME}}TaskDefinition', {
// Existing configuration...
volumes: [
{
name: "opentelemetry-auto-instrumentation-dotnet"
}
],
});2.2 Add ADOT Auto-instrumentation Init Container
For Linux Containers:
const initContainer = taskDefinition.addContainer('init', {
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-observability/adot-autoinstrumentation-dotnet:v1.9.2'),
essential: false,
memoryReservationMiB: 64,
cpu: 32,
command: ['cp', '-a', '/autoinstrumentation/.', '/otel-auto-instrumentation-dotnet'],
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'init-{{SERVICE_NAME}}',
logGroup: serviceLogGroup,
}),
});
initContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-dotnet',
containerPath: '/otel-auto-instrumentation-dotnet',
readOnly: false,
});For Windows Server Containers:
const initContainer = taskDefinition.addContainer('init', {
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-observability/adot-autoinstrumentation-dotnet:v1.9.2'),
essential: false,
memoryReservationMiB: 64,
cpu: 32,
command: ['CMD', '/c', 'xcopy', '/e', 'C:\\autoinstrumentation\\*', 'C:\\otel-auto-instrumentation', '&&', 'icacls', 'C:\\otel-auto-instrumentation', '/grant', '*S-1-1-0:R', '/T'],
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'init-{{SERVICE_NAME}}',
logGroup: serviceLogGroup,
}),
});
initContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-dotnet',
containerPath: 'C:\\otel-auto-instrumentation',
readOnly: false,
});2.3 Configure Main Application Container OpenTelemetry Environment Variables
For Linux Containers:
const mainContainer = taskDefinition.addContainer('{{SERVICE_NAME}}-container', {
// Existing configuration...
environment: {
// Existing environment variables...
OTEL_RESOURCE_ATTRIBUTES: 'service.name={{SERVICE_NAME}}',
OTEL_METRICS_EXPORTER: 'none',
OTEL_LOGS_EXPORTER: 'none',
DOTNET_STARTUP_HOOKS: '/otel-auto-instrumentation-dotnet/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll',
DOTNET_ADDITIONAL_DEPS: '/otel-auto-instrumentation-dotnet/AdditionalDeps',
DOTNET_SHARED_STORE: '/otel-auto-instrumentation-dotnet/store',
OTEL_DOTNET_AUTO_HOME: '/otel-auto-instrumentation-dotnet',
OTEL_TRACES_EXPORTER: 'otlp',
OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf',
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'http://localhost:4316/v1/traces',
OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT: 'http://localhost:4316/v1/metrics',
OTEL_AWS_APPLICATION_SIGNALS_ENABLED: 'true',
CORECLR_ENABLE_PROFILING: '1',
CORECLR_PROFILER: '{918728DD-259F-4A6A-AC2B-B85E1B658318}',
CORECLR_PROFILER_PATH: '/otel-auto-instrumentation-dotnet/linux-x64/OpenTelemetry.AutoInstrumentation.Native.so',
OTEL_DOTNET_AUTO_PLUGINS: 'AWS.Distro.OpenTelemetry.AutoInstrumentation.Plugin, AWS.Distro.OpenTelemetry.AutoInstrumentation',
},
});For Windows Server Containers:
const mainContainer = taskDefinition.addContainer('{{SERVICE_NAME}}-container', {
// Existing configuration...
environment: {
// Existing environment variables...
OTEL_RESOURCE_ATTRIBUTES: 'service.name={{SERVICE_NAME}}',
OTEL_METRICS_EXPORTER: 'none',
OTEL_LOGS_EXPORTER: 'none',
DOTNET_STARTUP_HOOKS: 'C:\\otel-auto-instrumentation\\net\\OpenTelemetry.AutoInstrumentation.StartupHook.dll',
DOTNET_ADDITIONAL_DEPS: 'C:\\otel-auto-instrumentation\\AdditionalDeps',
DOTNET_SHARED_STORE: 'C:\\otel-auto-instrumentation\\store',
OTEL_DOTNET_AUTO_HOME: 'C:\\otel-auto-instrumentation',
OTEL_TRACES_EXPORTER: 'otlp',
OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf',
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'http://localhost:4316/v1/traces',
OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT: 'http://localhost:4316/v1/metrics',
OTEL_AWS_APPLICATION_SIGNALS_ENABLED: 'true',
CORECLR_ENABLE_PROFILING: '1',
CORECLR_PROFILER: '{918728DD-259F-4A6A-AC2B-B85E1B658318}',
CORECLR_PROFILER_PATH: 'C:\\otel-auto-instrumentation\\win-x64\\OpenTelemetry.AutoInstrumentation.Native.dll',
OTEL_DOTNET_AUTO_PLUGINS: 'AWS.Distro.OpenTelemetry.AutoInstrumentation.Plugin, AWS.Distro.OpenTelemetry.AutoInstrumentation',
},
});2.4 Add Mount Point to Main Container
For Linux Containers:
mainContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-dotnet',
containerPath: '/otel-auto-instrumentation-dotnet',
readOnly: false,
});For Windows Server Containers:
mainContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-dotnet',
containerPath: 'C:\\otel-auto-instrumentation',
readOnly: false,
});2.5 Configure Container Dependencies
mainContainer.addContainerDependencies({
container: initContainer,
condition: ecs.ContainerDependencyCondition.SUCCESS,
});
mainContainer.addContainerDependencies({
container: cwAgentContainer,
condition: ecs.ContainerDependencyCondition.START,
});Completion
Before reciting the summary below (guidance for you, not for the user): report both managed policies — the task role now carries two, and earlier versions of this summary named only
CloudWatchAgentServerPolicy. CurrentCloudWatchAgentServerPolicyalready grants thexray:Put*and sampling actions, soAWSXRayDaemonWriteAccessis largely redundant here. If the user asks to trim to least privilege, verify against the live policy documents (aws iam get-policy-version) rather than hand-rolling an inline policy from this guide. Resource scoping is the more valuable axis than pruning actions:CloudWatchAgentServerPolicygrants everything onResource: "*", so dropping actions still leaveslogs:PutLogEventson every log group in the account. And do not assume step 1.2 covers the agent's log needs —/ecs/ecs-cwagentis the awslogs destination for the agent container's stdout, written under the task execution role. The Application Signals data the agent itself publishes goes to/aws/application-signals/data, which this guide does not pre-create, sologs:CreateLogGroupandlogs:CreateLogStreammust survive any trim. A denial does not surface in the console — check the agent's own log — and the symptom is that telemetry never starts arriving.
Tell the user:
"I've completed the Application Signals enablement for your .NET application. Here's what I modified:
Files Changed:
- IAM role: Added CloudWatchAgentServerPolicy and AWSXRayDaemonWriteAccess (two managed policies)
- ECS container: Installed and configured CloudWatch Agent as sidecar
- ADOT SDK container: Mounted ADOT SDK dependencies into Application container
- Application container: Enabled zero-code auto-instrumentation for .NET Application
Next Steps:
- Ensure that Application Signals is enabled in AWS account.
- Review the changes I made using
git diff - Deploy your infrastructure
- After deployment, wait 5-10 minutes for telemetry data to start flowing
Verification:
- Open AWS CloudWatch Console → Application Signals → Services
- Look for your service (named: {{SERVICE_NAME}})
Troubleshooting Refer to the CloudWatch APM troubleshooting guide.
Let me know if you'd like me to make any adjustments before you deploy!"