
Instrumenting applications on AWS ECS and AWS Fargate with AWS X-Ray
How to create a sidecar container, using the AWS CDK, to host the AWS X-Ray collector, in the same ECS Fargate task running the application.
- The X-Ray daemon, responsible to collect the traces from the application, needs to be deployed out of the application Docker image itself, to do not compete with the application in terms of memory and CPU usage;
- Each microservice needs to have its own X-Ray collector, to do not overload a single collector in case of high traffic of segments being generated by all services.
1
2
3
4
5
const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDefinition", {
cpu: 512,
memoryLimitMiB: 1024,
family: "products-service",
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
taskDefinition.addContainer("ProductsServiceContainer", {
image: ecs.ContainerImage.fromEcrRepository(props.repository, "1.0.0"),
containerName: "ProductsService",
logging: logDriver,
portMappings: [{
containerPort: 8080,
protocol: ecs.Protocol.TCP
}],
cpu: 384,
memoryLimitMiB: 896,
environment: {
PRODUCTS_DDB: productsDdb.tableName,
AWS_XRAY_DAEMON_ADDRESS: "0.0.0.0:2000",
AWS_XRAY_CONTEXT_MISSING: "IGNORE_ERROR",
AWS_XRAY_TRACING_NAME: "products-service"
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
taskDefinition.addContainer('xray', {
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/xray/aws-xray-daemon:latest'),
containerName: "XRayProductsService",
logging: ecs.LogDriver.awsLogs({
logGroup: new logs.LogGroup(this, "XRayLogGroup", {
logGroupName: "XRayProductsService",
removalPolicy: cdk.RemovalPolicy.DESTROY,
retention: logs.RetentionDays.ONE_MONTH
}),
streamPrefix: "XRayProductsService",
}),
cpu: 128,
memoryLimitMiB: 128,
portMappings: [{
containerPort: 2000,
protocol: ecs.Protocol.UDP,
}]
})
1
2
3
4
5
import * as iam from "aws-cdk-lib/aws-iam"
taskDefinition.taskRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('AWSXrayWriteOnlyAccess')
);
1
$ npm install aws-xray-sdk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as AWSXray from "aws-xray-sdk";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(AWSXray.express.openSegment(process.env.AWS_XRAY_TRACING_NAME));
AWSXray.config([AWSXray.plugins.ECSPlugin]);
AWSXray.captureHTTPsGlobal(require('http'));
app.use(AWSXray.express.closeSegment());
await app.listen(8080);
}
bootstrap();
1
2
3
4
5
6
7
import { captureAWSv3Client } from 'aws-xray-sdk';
constructor() {
this.tableName = process.env.PRODUCTS_DDB
const ddbClient = captureAWSv3Client(new DynamoDBClient({ }));
this.ddbDocClient = DynamoDBDocumentClient.from(ddbClient)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private async sendEvent(data: string): Promise<string> {
const segment = AWSXRay.getSegment() as AWSXRay.Segment
const command = new PublishCommand({
Message: data,
TopicArn: this.topicArn,
MessageAttributes: {
traceId: {
DataType: "String",
StringValue: segment.trace_id
},
}
})
const result = await this.snsClient.send(command)
return result.MessageId
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async handleMessage(message: AWS.SQS.Message): Promise<void> {
const startTime = new Date().getTime() / 1000
const body = JSON.parse(message.Body) as SNSMessage;
const traceId = body.MessageAttributes['traceId'].Value
const segment = new Segment(process.env.AWS_XRAY_TRACING_NAME, traceId)
segment.origin = "AWS::ECS::Container"
segment.start_time = startTime
segment.trace_id = traceId
segment.addPluginData({
operation: "HandleProductEvent",
region: process.env.AWS_REGION,
queue_url: process.env.AWS_SQS_QUEUE_PRODUCT_EVENTS_URL
})
const ns = getNamespace();
ns.run(async () => {
setSegment(segment)
///Code to process the message, let's say, to write some
//information to a DDB table
segment.close()
})
}