Skill 46 · Setting Up CloudWatch Observability
Subchapter 46.18
references/cloudwatch-omni/instrumentation/ecs-nodejs.mdMarkdown8 KBView on GitHub
Wire the ADOT Node.js auto-instrumentation SDK into an ECS task definition (Fargate or EC2 launch type). An init container copies the SDK into a shared volume at task startup, so the application image is not rebuilt and the application source is not touched.
Read instrumentation.md first — it defines what is out of scope (OTLP endpoints, Application Signals). Deploying a collector is a separate, optional step — collector-ecs.md.
Do NOT:
OTEL_EXPORTER_OTLP_* — leave the SDK’s default endpoint in place — unless you are also deploying a collector (collector-ecs.md), which sets OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL deliberatelyOTEL_AWS_APPLICATION_SIGNALS_*, OTEL_AWS_SERVICE_EVENTS_*, or OTEL_AWS_DYNAMIC_INSTRUMENTATION_*CloudWatchAgentServerPolicy or AWSXRayDaemonWriteAccess to the task role — unless you are also deploying a collector (collector-ecs.md), which requires CloudWatchAgentServerPolicy on the task rolecdk deploy, terraform apply, or aws ecs update-service automaticallyserver.js or any other application source fileOn IAM: instrumentation on its own needs no permissions, so this change adds none. The task may later need permission to reach wherever telemetry is sent — that belongs with the destination, not this guide. If you deploy a collector instead (collector-ecs.md), the collector is what gets the IAM — the workload still gets none.
The NODE_OPTIONS value differs between CommonJS and ESM. Check package.json:
"type": "module" → ESM"type": "commonjs" or no type field → CommonJS (default)If the project uses import syntax in its entrypoint without "type": "module", ask the user rather than guessing.
const taskDefinition = new ecs.FargateTaskDefinition(this, '<Service>TaskDefinition', {
// ... existing configuration unchanged ...
volumes: [
{ name: 'opentelemetry-auto-instrumentation-node' },
],
});A bind mount (a volume with only a name) is all that is needed — no EFS, no host path.
const initContainer = taskDefinition.addContainer('init', {
// Look up the latest tag — see instrumentation.md
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-observability/adot-autoinstrumentation-node:v0.12.0'),
essential: false,
memoryReservationMiB: 64,
cpu: 32,
command: ['cp', '-a', '/autoinstrumentation/.', '/otel-auto-instrumentation-node'],
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'init-<service>',
logGroup: serviceLogGroup,
}),
});
initContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-node',
containerPath: '/otel-auto-instrumentation-node',
readOnly: false,
});essential: false matters — the init container exits after the copy, and an essential container exiting would stop the whole task.
const mainContainer = taskDefinition.addContainer('<service>-container', {
// ... existing image, ports, health check unchanged ...
environment: {
// ... existing environment variables preserved ...
// CommonJS. For ESM, see the note below.
NODE_OPTIONS: '--require /otel-auto-instrumentation-node/autoinstrumentation.js',
OTEL_SERVICE_NAME: '<service>',
},
});
mainContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-node',
containerPath: '/otel-auto-instrumentation-node',
readOnly: false,
});For ESM applications, replace the NODE_OPTIONS value with:
--import /otel-auto-instrumentation-node/autoinstrumentation.js --experimental-loader=/otel-auto-instrumentation-node/node_modules/@opentelemetry/instrumentation/hook.mjsSet OTEL_SERVICE_NAME from the existing ECS service or container name. Optionally add OTEL_RESOURCE_ATTRIBUTES for attributes such as deployment.environment=production.
mainContainer.addContainerDependencies({
container: initContainer,
condition: ecs.ContainerDependencyCondition.SUCCESS,
});Without this, the application can start before the SDK finishes copying and NODE_OPTIONS will point at a file that does not exist yet.
If the task definition is managed as JSON (or through Terraform’s container_definitions), the same four pieces are:
Terraform note: in aws_ecs_task_definition, container_definitions is jsonencode of only
the containers array — volumes is a sibling HCL block, not a key inside that JSON. Pasting the
whole object below into jsonencode(...) silently drops the volume, after which every mount path is
missing and the app starts uninstrumented:
resource "aws_ecs_task_definition" "app" {
# ...
volume { name = "opentelemetry-auto-instrumentation-<lang>" }
container_definitions = jsonencode([ /* the containers array only */ ])
}{
"volumes": [{ "name": "opentelemetry-auto-instrumentation-node" }],
"containerDefinitions": [
{
"name": "init",
"image": "public.ecr.aws/aws-observability/adot-autoinstrumentation-node:v0.12.0",
"essential": false,
"command": ["cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation-node"],
"mountPoints": [
{ "sourceVolume": "opentelemetry-auto-instrumentation-node", "containerPath": "/otel-auto-instrumentation-node", "readOnly": false }
]
},
{
"name": "<service>-container",
"environment": [
{ "name": "NODE_OPTIONS", "value": "--require /otel-auto-instrumentation-node/autoinstrumentation.js" },
{ "name": "OTEL_SERVICE_NAME", "value": "<service>" }
],
"mountPoints": [
{ "sourceVolume": "opentelemetry-auto-instrumentation-node", "containerPath": "/otel-auto-instrumentation-node", "readOnly": false }
],
"dependsOn": [{ "containerName": "init", "condition": "SUCCESS" }]
}
]
}After the user deploys and tasks recycle:
# The init container should have exited 0
aws ecs describe-tasks --cluster <cluster> --tasks <task-arn> \
--query 'tasks[0].containers[?name==`init`].[name,lastStatus,exitCode]'Then check the application container’s CloudWatch Logs for the literal string AWS Distro of OpenTelemetry automatic instrumentation started successfully — not a bare -i opentelemetry, which also matches the benign @aws/aws-distro-opentelemetry-instrumentation-vercel-ai Failed to register VercelAISpanProcessor line a healthy start always emits. Until a receiver exists at the default OTLP endpoint, exporter connection errors are expected — that is the next step, not a failure of instrumentation.
Tell the user:
“I’ve wired ADOT Node.js auto-instrumentation into your ECS task definition.
Changes:
opentelemetry-auto-instrumentation-nodeinit container that copies the ADOT Node.js SDK into that volumeNODE_OPTIONS (module format: CommonJS/ESM) and OTEL_SERVICE_NAME to the application container, plus the volume mount and a SUCCESS dependency on initNot changed: your application image, your application source, the task role’s IAM policies, and the service’s sidecars — no CloudWatch Agent or collector was added.
Next steps:
localhost:4318). Two ways to fix that: deploy an OTel Collector alongside it and export to that (collector-ecs.md), or point OTEL_EXPORTER_OTLP_ENDPOINT at an OTLP endpoint you already have.Let me know if you’d like adjustments before you deploy.”