Skill 46 · Setting Up CloudWatch Observability
Subchapter 46.17
references/cloudwatch-omni/instrumentation/ecs-java.mdMarkdown8 KBView on GitHub
Wire the ADOT Java auto-instrumentation agent into an ECS task definition (Fargate or EC2 launch type). An init container copies the agent jar 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 agent’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 automatically.java files or its build configurationOn 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.
const taskDefinition = new ecs.FargateTaskDefinition(this, '<Service>TaskDefinition', {
// ... existing configuration unchanged ...
volumes: [
{ name: 'opentelemetry-auto-instrumentation-java' },
],
});A bind mount (a volume with only a name) is all that is needed — no EFS, no host path.
The Java init container copies a single jar, not a directory tree — the copy command differs from the other languages.
const initContainer = taskDefinition.addContainer('init', {
// Look up the latest tag — see instrumentation.md
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v2.30.0'),
essential: false,
memoryReservationMiB: 64,
cpu: 32,
command: ['cp', '-a', '/javaagent.jar', '/otel-auto-instrumentation-java/javaagent.jar'],
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'init-<service>',
logGroup: serviceLogGroup,
}),
});
initContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-java',
containerPath: '/otel-auto-instrumentation-java',
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 ...
// Note the leading space — JAVA_TOOL_OPTIONS is appended to, not replaced
JAVA_TOOL_OPTIONS: ' -javaagent:/otel-auto-instrumentation-java/javaagent.jar',
OTEL_SERVICE_NAME: '<service>',
},
});
mainContainer.addMountPoints({
sourceVolume: 'opentelemetry-auto-instrumentation-java',
containerPath: '/otel-auto-instrumentation-java',
readOnly: false,
});If the container already sets JAVA_TOOL_OPTIONS, append the -javaagent flag to the existing value rather than overwriting it — losing existing JVM flags (heap settings, GC options, other agents) will change how the application runs:
<EXISTING_JAVA_TOOL_OPTIONS> -javaagent:/otel-auto-instrumentation-java/javaagent.jarCheck the existing container environment before writing this value.
Set 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 JVM can start before the jar finishes copying, and -javaagent pointing at a missing file makes the JVM fail to start.
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-java" }],
"containerDefinitions": [
{
"name": "init",
"image": "public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v2.30.0",
"essential": false,
"command": ["cp", "-a", "/javaagent.jar", "/otel-auto-instrumentation-java/javaagent.jar"],
"mountPoints": [
{ "sourceVolume": "opentelemetry-auto-instrumentation-java", "containerPath": "/otel-auto-instrumentation-java", "readOnly": false }
]
},
{
"name": "<service>-container",
"environment": [
{ "name": "JAVA_TOOL_OPTIONS", "value": " -javaagent:/otel-auto-instrumentation-java/javaagent.jar" },
{ "name": "OTEL_SERVICE_NAME", "value": "<service>" }
],
"mountPoints": [
{ "sourceVolume": "opentelemetry-auto-instrumentation-java", "containerPath": "/otel-auto-instrumentation-java", "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. The JVM prints a Picked up JAVA_TOOL_OPTIONS: line on startup listing the -javaagent flag — that confirms the agent attached. 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 the ADOT Java auto-instrumentation agent into your ECS task definition.
Changes:
opentelemetry-auto-instrumentation-javainit container that copies javaagent.jar into that volumeJAVA_TOOL_OPTIONS (appended to any existing value) 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 and build config, the task role’s IAM policies, and the service’s sidecars — no CloudWatch Agent or collector was added.
Next steps:
JAVA_TOOL_OPTIONS preserves any JVM flags the container already set.Picked up JAVA_TOOL_OPTIONS: line in the logs.localhost:4317, gRPC — the Java agent’s default, not 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.”