# Instrument a Java Application on Amazon ECS with ADOT 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](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](collector-ecs.md). ## Prerequisites - An existing ECS service running a Java application - The task definition available to edit — as CDK, Terraform, CloudFormation, or raw JSON ## Critical Requirements **Do NOT:** - Add a CloudWatch Agent sidecar container, or any collector sidecar — **unless you are also deploying a collector** ([collector-ecs.md](collector-ecs.md)), where the CloudWatch Agent sidecar is the primary collector - Set `OTEL_EXPORTER_OTLP_*` — leave the agent's default endpoint in place — **unless you are also deploying a collector** ([collector-ecs.md](collector-ecs.md)), which sets `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_PROTOCOL` deliberately - Set `OTEL_AWS_APPLICATION_SIGNALS_*`, `OTEL_AWS_SERVICE_EVENTS_*`, or `OTEL_AWS_DYNAMIC_INSTRUMENTATION_*` - Add `CloudWatchAgentServerPolicy` or `AWSXRayDaemonWriteAccess` to the task role — **unless you are also deploying a collector** ([collector-ecs.md](collector-ecs.md)), which requires `CloudWatchAgentServerPolicy` on the **task** role - Run `cdk deploy`, `terraform apply`, or `aws ecs update-service` automatically - Modify the application's `.java` files or its build configuration **On 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](collector-ecs.md)), the collector is what gets the IAM — the workload still gets none. ## Step 1: Add a shared volume to the task definition ```typescript const taskDefinition = new ecs.FargateTaskDefinition(this, '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. ## Step 2: Add the ADOT init container The Java init container copies a **single jar**, not a directory tree — the copy command differs from the other languages. ```typescript 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-', 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. ## Step 3: Configure the application container ```typescript const mainContainer = taskDefinition.addContainer('-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: '', }, }); mainContainer.addMountPoints({ sourceVolume: 'opentelemetry-auto-instrumentation-java', containerPath: '/otel-auto-instrumentation-java', readOnly: false, }); ``` ### If JAVA_TOOL_OPTIONS is already set 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: ``` -javaagent:/otel-auto-instrumentation-java/javaagent.jar ``` Check 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`. ## Step 4: Make the application wait for the init container ```typescript 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. ## Raw task definition JSON 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: ```hcl resource "aws_ecs_task_definition" "app" { # ... volume { name = "opentelemetry-auto-instrumentation-" } container_definitions = jsonencode([ /* the containers array only */ ]) } ``` ```json { "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": "-container", "environment": [ { "name": "JAVA_TOOL_OPTIONS", "value": " -javaagent:/otel-auto-instrumentation-java/javaagent.jar" }, { "name": "OTEL_SERVICE_NAME", "value": "" } ], "mountPoints": [ { "sourceVolume": "opentelemetry-auto-instrumentation-java", "containerPath": "/otel-auto-instrumentation-java", "readOnly": false } ], "dependsOn": [{ "containerName": "init", "condition": "SUCCESS" }] } ] } ``` ## Verify After the user deploys and tasks recycle: ```bash # The init container should have exited 0 aws ecs describe-tasks --cluster --tasks \ --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. ## Completion **Tell the user:** "I've wired the ADOT Java auto-instrumentation agent into your ECS task definition. **Changes:** - Added a bind mount volume, `opentelemetry-auto-instrumentation-java` - Added a non-essential `init` container that copies `javaagent.jar` into that volume - Added `JAVA_TOOL_OPTIONS` (appended to any existing value) and `OTEL_SERVICE_NAME` to the application container, plus the volume mount and a `SUCCESS` dependency on `init` **Not 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:** 1. Review the diff — confirm `JAVA_TOOL_OPTIONS` preserves any JVM flags the container already set. 2. Deploy and let the tasks recycle. Look for the `Picked up JAVA_TOOL_OPTIONS:` line in the logs. 3. **Telemetry has nowhere to go yet.** No OTLP endpoint was configured, so the agent is using its default (`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](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."