From 375cbfd934c29deb9ce9a15ee299da3c2ff39306 Mon Sep 17 00:00:00 2001 From: Michele Mancioppi Date: Wed, 15 Mar 2023 14:22:18 +0100 Subject: [PATCH] feat(ecs): add experimental autotrace for ECS services and task definitions (#5) --- API.md | 165 +++++- README.md | 249 ++++++++- src/lumigo.ts | 481 ++++++++++++++++-- test/lumigo-ecs-tracing.test.ts | 222 ++++++++ ....test.ts => lumigo-lambda-tracing.test.ts} | 43 +- 5 files changed, 1045 insertions(+), 115 deletions(-) create mode 100644 test/lumigo-ecs-tracing.test.ts rename test/{lumigo.test.ts => lumigo-lambda-tracing.test.ts} (92%) diff --git a/API.md b/API.md index cfca96f..29374d9 100644 --- a/API.md +++ b/API.md @@ -3,27 +3,57 @@ ## Structs -### LumigoAspectProps +### LumigoProps -#### Initializer +#### Initializer ```typescript -import { LumigoAspectProps } from '@lumigo/cdk-constructs-v2' +import { LumigoProps } from '@lumigo/cdk-constructs-v2' -const lumigoAspectProps: LumigoAspectProps = { ... } +const lumigoProps: LumigoProps = { ... } ``` #### Properties | **Name** | **Type** | **Description** | | --- | --- | --- | -| lambdaEnableW3CTraceContext | boolean | *No description.* | -| lambdaNodejsLayerVersion | number | *No description.* | -| lambdaPythonLayerVersion | number | *No description.* | +| lumigoToken | aws-cdk-lib.SecretValue | *No description.* | --- -##### `lambdaEnableW3CTraceContext`Optional +##### `lumigoToken`Required + +```typescript +public readonly lumigoToken: SecretValue; +``` + +- *Type:* aws-cdk-lib.SecretValue + +--- + +### LumigoTraceProps + +#### Initializer + +```typescript +import { LumigoTraceProps } from '@lumigo/cdk-constructs-v2' + +const lumigoTraceProps: LumigoTraceProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| lambdaEnableW3CTraceContext | boolean | *No description.* | +| lambdaNodejsLayerVersion | number | *No description.* | +| lambdaPythonLayerVersion | number | *No description.* | +| traceEcs | boolean | *No description.* | +| traceLambda | boolean | *No description.* | + +--- + +##### `lambdaEnableW3CTraceContext`Optional ```typescript public readonly lambdaEnableW3CTraceContext: boolean; @@ -33,7 +63,7 @@ public readonly lambdaEnableW3CTraceContext: boolean; --- -##### `lambdaNodejsLayerVersion`Optional +##### `lambdaNodejsLayerVersion`Optional ```typescript public readonly lambdaNodejsLayerVersion: number; @@ -43,7 +73,7 @@ public readonly lambdaNodejsLayerVersion: number; --- -##### `lambdaPythonLayerVersion`Optional +##### `lambdaPythonLayerVersion`Optional ```typescript public readonly lambdaPythonLayerVersion: number; @@ -53,31 +83,79 @@ public readonly lambdaPythonLayerVersion: number; --- -### LumigoProps +##### `traceEcs`Optional -#### Initializer +```typescript +public readonly traceEcs: boolean; +``` + +- *Type:* boolean + +--- + +##### `traceLambda`Optional ```typescript -import { LumigoProps } from '@lumigo/cdk-constructs-v2' +public readonly traceLambda: boolean; +``` -const lumigoProps: LumigoProps = { ... } +- *Type:* boolean + +--- + +### TraceEcsServiceDefinitionProps + +#### Initializer + +```typescript +import { TraceEcsServiceDefinitionProps } from '@lumigo/cdk-constructs-v2' + +const traceEcsServiceDefinitionProps: TraceEcsServiceDefinitionProps = { ... } ``` #### Properties | **Name** | **Type** | **Description** | | --- | --- | --- | -| lumigoToken | aws-cdk-lib.SecretValue | *No description.* | +| applyAutoTraceTag | boolean | *No description.* | --- -##### `lumigoToken`Required +##### `applyAutoTraceTag`Optional ```typescript -public readonly lumigoToken: SecretValue; +public readonly applyAutoTraceTag: boolean; ``` -- *Type:* aws-cdk-lib.SecretValue +- *Type:* boolean + +--- + +### TraceEcsTaskDefinitionProps + +#### Initializer + +```typescript +import { TraceEcsTaskDefinitionProps } from '@lumigo/cdk-constructs-v2' + +const traceEcsTaskDefinitionProps: TraceEcsTaskDefinitionProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| applyAutoTraceTag | boolean | *No description.* | + +--- + +##### `applyAutoTraceTag`Optional + +```typescript +public readonly applyAutoTraceTag: boolean; +``` + +- *Type:* boolean --- @@ -135,8 +213,6 @@ public readonly layerVersion: number; ### Lumigo -TODO: Document tracing functions one-by-one TODO: Document using as Aspect to trace all functions. - #### Initializers ```typescript @@ -161,16 +237,61 @@ new Lumigo(props: LumigoProps) | **Name** | **Description** | | --- | --- | +| asEcsExtension | *No description.* | +| traceEcsService | *No description.* | +| traceEcsTaskDefinition | *No description.* | | traceEverything | *No description.* | | traceLambda | *No description.* | | visit | *No description.* | --- +##### `asEcsExtension` + +```typescript +public asEcsExtension(): ITaskDefinitionExtension +``` + +##### `traceEcsService` + +```typescript +public traceEcsService(service: Ec2Service | FargateService | QueueProcessingEc2Service | QueueProcessingFargateService | NetworkLoadBalancedEc2Service | NetworkLoadBalancedFargateService | ApplicationLoadBalancedEc2Service | ApplicationLoadBalancedFargateService | ScheduledEc2Task | ScheduledFargateTask | ApplicationMultipleTargetGroupsEc2Service | ApplicationMultipleTargetGroupsFargateService | NetworkMultipleTargetGroupsEc2Service | NetworkMultipleTargetGroupsFargateService, props?: TraceEcsServiceDefinitionProps): void +``` + +###### `service`Required + +- *Type:* aws-cdk-lib.aws_ecs.Ec2Service | aws-cdk-lib.aws_ecs.FargateService | aws-cdk-lib.aws_ecs_patterns.QueueProcessingEc2Service | aws-cdk-lib.aws_ecs_patterns.QueueProcessingFargateService | aws-cdk-lib.aws_ecs_patterns.NetworkLoadBalancedEc2Service | aws-cdk-lib.aws_ecs_patterns.NetworkLoadBalancedFargateService | aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedEc2Service | aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedFargateService | aws-cdk-lib.aws_ecs_patterns.ScheduledEc2Task | aws-cdk-lib.aws_ecs_patterns.ScheduledFargateTask | aws-cdk-lib.aws_ecs_patterns.ApplicationMultipleTargetGroupsEc2Service | aws-cdk-lib.aws_ecs_patterns.ApplicationMultipleTargetGroupsFargateService | aws-cdk-lib.aws_ecs_patterns.NetworkMultipleTargetGroupsEc2Service | aws-cdk-lib.aws_ecs_patterns.NetworkMultipleTargetGroupsFargateService + +--- + +###### `props`Optional + +- *Type:* TraceEcsServiceDefinitionProps + +--- + +##### `traceEcsTaskDefinition` + +```typescript +public traceEcsTaskDefinition(taskDefinition: TaskDefinition, props?: TraceEcsTaskDefinitionProps): void +``` + +###### `taskDefinition`Required + +- *Type:* aws-cdk-lib.aws_ecs.TaskDefinition + +--- + +###### `props`Optional + +- *Type:* TraceEcsTaskDefinitionProps + +--- + ##### `traceEverything` ```typescript -public traceEverything(root: App | Stack, props?: LumigoAspectProps): void +public traceEverything(root: App | Stack, props?: LumigoTraceProps): void ``` ###### `root`Required @@ -181,7 +302,7 @@ public traceEverything(root: App | Stack, props?: LumigoAspectProps): void ###### `props`Optional -- *Type:* LumigoAspectProps +- *Type:* LumigoTraceProps --- diff --git a/README.md b/README.md index cb89cbe..8ec612f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,45 @@ With `npm`: npm install '@lumigo/cdk-constructs-v2' ``` +### Supported Constructs + +#### Supported AWS Lambda Constructs + +The Lumigo CDK integration applies automated distributed tracing to the following constructs that manage AWS Lambda functions: + +| AWS CDK Package | Constructs | Notes | +|-----------------|------------|-------| +| [`aws-cdk-lib/aws-lambda`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda-readme.html) | [`Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html) | | +| [`aws-cdk-lib/aws-lambda-nodejs`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html) | [`NodejsFunction`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html) | | +| [`@aws-cdk/aws-lambda-python-alpha`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html) | [`PythonFunction`](https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_aws-lambda-python-alpha.PythonFunction.html) | The `PythonFunction` is not GA in AWS CDK 2, but is supported by the Lumigo CDK integration regardless | + +#### Supported Amazon ECS Constructs + +**Note:** The automatic instrumentation of Amazon ECS workloads is currently **experimental**. + +The Lumigo CDK integration applies automated distributed tracing to the following constructs that manage Amazon ECS workloads: + +| AWS CDK Package | Constructs | Notes | +|-----------------|------------|-------| +| [`aws-cdk-lib/aws-ecs`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs-readme.html) | [`Ec2Service`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.Ec2Service.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs-readme.html) | [`Ec2TaskDefinition`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.Ec2TaskDefinition.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs-readme.html) | [`FargateService`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.FargateService.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs-readme.html) | [`FargateTaskDefinition`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.FargateTaskDefinition.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`ApplicationLoadBalancedEc2Service`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedEc2Service.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`ApplicationLoadBalancedFargateService`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedFargateService.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`ApplicationMultipleTargetGroupsEc2Service`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationMultipleTargetGroupsEc2Service.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`ApplicationMultipleTargetGroupsFargateService`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationMultipleTargetGroupsFargateService.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`NetworkLoadBalancedEc2Service`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.NetworkLoadBalancedEc2Service.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`NetworkLoadBalancedFargateService`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.NetworkLoadBalancedFargateService.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`NetworkMultipleTargetGroupsEc2Service`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.NetworkMultipleTargetGroupsEc2Service.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`NetworkMultipleTargetGroupsFargateService`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.NetworkMultipleTargetGroupsFargateService.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`QueueProcessingEc2Service`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.QueueProcessingEc2Service.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`QueueProcessingFargateService`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.QueueProcessingFargateService.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`ScheduledEc2Task`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ScheduledEc2Task.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | +| [`aws-cdk-lib/aws-ecs-patterns`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html) | [`ScheduledFargateTask`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ScheduledFargateTask.html) | Experimental [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#supported-packages) and [Python](https://github.com/lumigo-io/opentelemetry-python-distro#supported-packages) distributed tracing | + +The automated distributed tracing will work for all Node.js and Python processes [dynamically linked](https://stackoverflow.com/questions/311882/what-do-statically-linked-and-dynamically-linked-mean) against [GNU C Library](https://www.gnu.org/software/libc/) (which is used by virtually all container base images except [Alpine Linux](https://www.alpinelinux.org/)) or [musl libc](https://musl.libc.org/) (for [Alpine Linux](https://www.alpinelinux.org/)-based containers). + ## Usage The Lumigo CDK integration enables you to trace all the applicable constructs inside an CDK App or a Stack, as well on a function-by-function basis. @@ -30,7 +69,7 @@ The only requirement to use the Lumigo CDK integration is to have the [Lumigo to ### Instrumenting the entire CDK application -The following code will apply Lumigo autotracing to all the [supported constructs](#supported-constructs): +The following code will apply Lumigo autotracing to all the [supported AWS Lambda constructs](#supported-constructs): ```typescript import { Lumigo } from '@lumigo/cdk-constructs-v2'; @@ -40,18 +79,37 @@ const app = new App(); // Add here stacks and constructs -new Lumigo({lumigoToken:SecretValue.secretsManager('LumigoToken')}).traceEverything(app); +new Lumigo({lumigoToken:SecretValue.secretsManager('LumigoToken')}).traceEverything(app); // This will trace all Lambda functions managed with supported constructs + +app.synth(); +``` + +Currently, the autotrace injection of Amazon ECS workloads is opt-in: + +```typescript +import { Lumigo } from '@lumigo/cdk-constructs-v2'; +import { App, SecretValue } from 'aws-cdk-lib'; + +const app = new App(); + +// Add here stacks and constructs + +new Lumigo({lumigoToken:SecretValue.secretsManager('LumigoToken')}).traceEverything(app, { + traceEcs: true, // This activates adding tracing to Amazon ECS task definitions and services as well +}); app.synth(); ``` ### Instrumenting a CDK stack -The following code will apply Lumigo autotracing to all the [supported constructs](#supported-constructs) in the instrumented stack: +The following code will apply Lumigo autotracing to all the [supported AWS Lambda constructs](#supported-constructs) in the instrumented stack: ```typescript import { Lumigo } from '@lumigo/cdk-constructs-v2'; -import { App, SecretValue } from 'aws-cdk-lib'; +import { App, SecretValue, Stack, StackProps } from 'aws-cdk-lib'; +import { Function } from 'aws-cdk-lib/aws-lambda'; +import { Construct } from 'constructs'; export class NodejsStack extends Stack { @@ -81,11 +139,54 @@ new Lumigo({lumigoToken:SecretValue.secretsManager('LumigoToken')}).traceEveryth app.synth(); ``` +```typescript +import { Lumigo } from '@lumigo/cdk-constructs-v2'; +import { App, SecretValue, Stack, StackProps } from 'aws-cdk-lib'; +import { FargateService } from 'aws-cdk-lib/aws-ecs'; +import { Function } from 'aws-cdk-lib/aws-lambda'; +import { Construct } from 'constructs'; + +export class NodejsStack extends Stack { + + constructor(scope: Construct, id: string, props: StackProps = {}) { + super(scope, id, props); + + new Function(this, 'MyLambda', { + code: new InlineCode('foo'), + handler: 'index.handler', + runtime: Runtime.NODEJS_14_X, + }); + + new FargateService(this, 'MyFargateService', { + ... + }); + + } + +} + +const app = new App(); + +const stack = new NodejsStack(app, 'NodejsTestStack', { + env: { + region: 'eu-central-1', + } +}); + +new Lumigo({lumigoToken:SecretValue.secretsManager('LumigoToken')}).traceEverything(stack, { + traceEcs: true, // This activates adding tracing to Amazon ECS task definitions and services as well +}); + +app.synth(); +``` + ### Instrumenting single functions ```typescript import { Lumigo } from '@lumigo/cdk-constructs-v2'; -import { App, SecretValue } from 'aws-cdk-lib'; +import { App, SecretValue, Stack, StackProps } from 'aws-cdk-lib'; +import { Function } from 'aws-cdk-lib/aws-lambda'; +import { Construct } from 'constructs'; interface NodejsStackProps extends StackProps { readonly lumigo: Lumigo; @@ -121,10 +222,100 @@ const stack = new NodejsStack(app, 'NodejsTestStack', { app.synth(); ``` +### Instrumenting single ECS services + +```typescript +import { Lumigo } from '@lumigo/cdk-constructs-v2'; +import { App, SecretValue, Stack, StackProps } from 'aws-cdk-lib'; +import { FargateService } from 'aws-cdk-lib/aws-ecs'; +import { Construct } from 'constructs'; + +interface NodejsStackProps extends StackProps { + readonly lumigo: Lumigo; +} + +export class NodejsStack extends Stack { + + constructor(scope: Construct, id: string, props: NodejsStackProps = {}) { + super(scope, id, props); + + const service = new FargateService(this, 'MyFargateService', { + ... + }); + + props.lumigo.traceEcsService(service); + } + +} + +const app = new App(); + +const lumigo = new Lumigo({lumigoToken:SecretValue.secretsManager('LumigoToken')}); + +const stack = new NodejsStack(app, 'NodejsTestStack', { + env: { + region: 'eu-central-1', + }, + lumigo, +}); + +app.synth(); +``` + +### Instrumenting single ECS task definitions + +Instrumenting at the level of the Amazon ECS task definition enables you to share the instrumented task definition across multiple ECS services: + +```typescript +import { Lumigo } from '@lumigo/cdk-constructs-v2'; +import { App, SecretValue, Stack, StackProps } from 'aws-cdk-lib'; +import { FargateService, FargateTaskDefinition } from 'aws-cdk-lib/aws-ecs'; +import { Construct } from 'constructs'; + +interface NodejsStackProps extends StackProps { + readonly lumigo: Lumigo; +} + +export class NodejsStack extends Stack { + + constructor(scope: Construct, id: string, props: NodejsStackProps = {}) { + super(scope, id, props); + + const taskDefinition = new FargateTaskDefinition(, this 'MyFargateTaskDefinition', { + ... + }) + new FargateService(this, 'MyFargateService1', { + taskDefinition: taskDefinition, + ... + }); + new FargateService(this, 'MyFargateService2', { + taskDefinition: taskDefinition, + ... + }); + + props.lumigo.traceEcsTaskDefinition(taskDefinition); + } + +} + +const app = new App(); + +const lumigo = new Lumigo({lumigoToken:SecretValue.secretsManager('LumigoToken')}); + +const stack = new NodejsStack(app, 'NodejsTestStack', { + env: { + region: 'eu-central-1', + }, + lumigo, +}); + +app.synth(); +``` + ### Lambda Layer Version Pinning -Unless specified otherwise, when instrumenting a Lambda function, the Lumigo CDK integration will use the latest known Lambda layer at the moment of publishing the Lumigo CDK integration version. -(It is considered bad practice in CDK Construct designs to have API calls take place inside the `synth` phase, so new versions of the `@lumigo/cdk-constructs-v2` will regularly be released, pointing at the latest layers.) +Unless specified otherwise, when instrumenting a Lambda function, the Lumigo CDK integration will use the latest known Lambda layer at the moment of publishing the adopted version of the `@lumigo/cdk-constructs-v2` package. +(It is considered bad practice in CDK Construct designs to have API calls take place inside the `synth` phase, so new versions of the `@lumigo/cdk-constructs-v2` are regularly released, pointing at the latest layers.) The pinning of specific layer versions can be performed at the level of the entire application or stack: @@ -206,10 +397,44 @@ export class MyNodejsLambdaStack extends Stack { } ``` -## Supported Constructs +The Lumigo tracers used for ECS tracing natively use the W3C TraceContext standard, and no opt-in is necessary. + +## How does it work? + +Like any other CDK construct, the Lumigo CDK integration contributes changes to the CloudFormation templates that [CDK generates for you](https://docs.aws.amazon.com/cdk/v2/guide/home.html). +The changes that the Lumigo CDK integration applies are focused on enabling out-of-the-box distributed tracing for the AWS Lambda and Amazon ECS workloads you manage via AWS CDK. + +### Instrumentation of AWS Lambda functions + +When encountering a [supported AWS Lambda-related construct](#supported-aws-lambda-constructs), the Lumigo CDK integration: + +1. adds a Lambda layer containing the tracer for the Lambda function runtime; the layer to be used depends on the runtime, and the version to be used ([Node.js](./src/lambda_layers_nodejs.json), [Python](./src/lambda_layers_python.json)) is the latest at the time of the release of the version of the `@lumigo/cdk-constructs-v2` package. Using a different version of the layer is supported via [version pinning](#lambda-layer-version-pinning). +1. sets the `LUMIGO_TRACER_TOKEN` environment variable with, as value, the plain-text value of the [`SecretValue`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.SecretValue.html#static-unsafewbrplainwbrtextsecret) passed on instantiation of the `Lumigo` object. The value should be a valid [Lumigo Token](https://docs.lumigo.io/docs/lumigo-tokens). +1. adds runtime-dependent environment variables as documented in the [Lumigo AWS Lambda manual tracing](https://docs.lumigo.io/docs/manual-instrumentation) documentation. + +### Instrumentation of Amazon ECS workloads + +**Note:** The automatic instrumentation of Amazon ECS workloads is currently **experimental**. + +#### Modifications applied to Amazon ECS task definitions + +When encountering a [supported Amazon ECS-related construct](#supported-amazon-ecs-constructs), the Lumigo CDK integration: + +1. Adds an [ephemeral volume](https://docs.aws.amazon.com/AmazonECS/latest/userguide/using_data_volumes.html) called `lumigo-injector` to the task definition +1. Adds a container called `lumigo-injector` that uses a [public Amazon ECR image](public.ecr.aws/lumigo/lumigo-autotrace:latest); the image contains the latest copies of the [Lumigo OpenTelemetry Distro for Node.js](https://github.com/lumigo-io/opentelemetry-js-distro) and [Lumigo OpenTelemetry Distro for Python](https://github.com/lumigo-io/opentelemetry-python-distro), alongside an `LD_PRELOAD` injector that is not unlike this [OpenTelemetry Injector](https://github.com/mmanciop/opentelemetry-injector). +1. The `lumigo-injector` volume is mounted to all containers in the task definition, including the `lumigo-injector` container. +1. All containers other than `lumigo-injector` get added: + 1. A [container dependency](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDependency.html) on the completion of the `lumigo-injector` container. + 1. The `LUMIGO_TRACER_TOKEN` environment variable with, as value, the plain-text value of the [`SecretValue`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.SecretValue.html#static-unsafewbrplainwbrtextsecret) passed on instantiation of the `Lumigo` object. + 1. The `LD_PRELOAD` environment variable pointing to the Lumigo Injector's `lumigo_injector.so` file, which performs as needed the activation of the Python or Node.js tracers delivered by the Lumigo OpenTelemetry Distros for Node.js and Python as needed. + +#### How tracing works for Amazon ECS task definitions + +The automatic instrumentation of Amazon ECS task definitions works based on the principle that the runtimes (CPython, Node.js, etc.) inside your Amazon ECS tasks can be traced with OpenTelemetry Distros like Lumigo's that have no-code activation capabilities (see [Node.js](https://github.com/lumigo-io/opentelemetry-js-distro#no-code-activation), [Python](https://github.com/lumigo-io/opentelemetry-python-distro#no-code-activation)) if: -The Lumigo CDK integration applies autotrace to the following constructs by adding a Lambda layer containing the right tracer for the Lambda function runtime, and environment variables: +1. **Tracer delivery:** The files of the tracers are available on the filesystem of your containers in a way that the application process has read-access to them +1. **Tracer activation:** The tracer is activated therough the application's process environment -* [`Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html) from the [`aws-cdk-lib/aws-lambda`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda-readme.html) package. -* [`NodejsFunction`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html) from the [`aws-cdk-lib/aws-lambda-nodejs`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html) package. -* [`PythonFunction`](https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_aws-lambda-python-alpha.PythonFunction.html) from the [`@aws-cdk/aws-lambda-python-alpha`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html) package. +The tracer delivery is accomplished by adding the shared `lumigo-injector` volume, and copy into it the Lumigo OpenTelemetry Distros before all other containers can start (thanks to the container dependencies). +The tracer activation is based on manipulating the process environment of your applications inside the containers via the Lumigo Injector. +The Lumigo Injector is an [`LD_PRELOAD`](https://stackoverflow.com/questions/426230/what-is-the-ld-preload-trick) object that ensure that, when your application looks up environment variables like `NODE_OPTIONS` or `AUTOWRAPT_BOOTSTRAP` (used for the activation of the Lumigo OpenTelemetry Distros for Node.js and Python, respectively), the right value is returned. diff --git a/src/lumigo.ts b/src/lumigo.ts index f75c846..43b96e5 100644 --- a/src/lumigo.ts +++ b/src/lumigo.ts @@ -1,9 +1,24 @@ import { dirname, join } from 'path'; import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; import { App, Annotations, IAspect, SecretValue, Stack, Aspects, Tags, TagManager } from 'aws-cdk-lib'; +import { ContainerDefinition, ContainerDependencyCondition, ContainerImage, Ec2Service, FargateService, ITaskDefinitionExtension, TaskDefinition, Volume } from 'aws-cdk-lib/aws-ecs'; +import { + ApplicationLoadBalancedEc2Service, + ApplicationLoadBalancedFargateService, + ApplicationMultipleTargetGroupsEc2Service, + ApplicationMultipleTargetGroupsFargateService, + NetworkLoadBalancedEc2Service, + NetworkLoadBalancedFargateService, + NetworkMultipleTargetGroupsEc2Service, + NetworkMultipleTargetGroupsFargateService, + QueueProcessingEc2Service, + QueueProcessingFargateService, + ScheduledEc2Task, + ScheduledFargateTask, +} from 'aws-cdk-lib/aws-ecs-patterns'; import { Function, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { IConstruct, IValidation } from 'constructs'; +import { Construct, IConstruct, IValidation } from 'constructs'; /* eslint-disable */ const { name, version } = require(join(dirname(__dirname), 'package.json')); @@ -12,13 +27,30 @@ const { name, version } = require(join(dirname(__dirname), 'package.json')); import * as lambdaLayersNodejs from './lambda_layers_nodejs.json'; import * as lambdaLayersPython from './lambda_layers_python.json'; -type SupportedFunction = Function | NodejsFunction | PythonFunction; +type SupportedLambdaFunction = Function | NodejsFunction | PythonFunction; +type SupportedEcsPatternsService = ( + ApplicationLoadBalancedEc2Service | + ApplicationLoadBalancedFargateService | + ApplicationMultipleTargetGroupsEc2Service | + ApplicationMultipleTargetGroupsFargateService | + NetworkLoadBalancedEc2Service | + NetworkLoadBalancedFargateService | + NetworkMultipleTargetGroupsEc2Service | + NetworkMultipleTargetGroupsFargateService | + QueueProcessingEc2Service | + QueueProcessingFargateService | + ScheduledEc2Task | + ScheduledFargateTask +); +type SupportedEcsService = FargateService | Ec2Service | SupportedEcsPatternsService; export interface LumigoProps { readonly lumigoToken: SecretValue; } -export interface LumigoAspectProps { +export interface LumigoTraceProps { + readonly traceLambda?: boolean; + readonly traceEcs?: boolean; readonly lambdaNodejsLayerVersion?: Number; readonly lambdaPythonLayerVersion?: Number; readonly lambdaEnableW3CTraceContext?: Boolean; @@ -30,6 +62,16 @@ export interface TraceLambdaProps { readonly applyAutoTraceTag?: Boolean; } +export interface TraceEcsTaskDefinitionProps { + // TODO Add container version + readonly applyAutoTraceTag?: Boolean; +} + +export interface TraceEcsServiceDefinitionProps { + // TODO Add container version + readonly applyAutoTraceTag?: Boolean; +} + // Layer type to layer name enum LambdaLayerType { NODE = 'lumigo-node-tracer', @@ -52,10 +94,28 @@ const LUMIGO_AUTOTRACE_TAG_VALUE = `${name}@${version}`; const LUMIGO_LAMBDA_PYTHON_HANDLER = 'lumigo_tracer._handler'; -/** - * TODO: Document tracing functions one-by-one - * TODO: Document using as Aspect to trace all functions - */ +const LUMIGO_INJECTOR_CONTAINER_NAME = 'lumigo-injector'; + +const LUMIGO_INJECTOR_VOLUME_NAME = 'lumigo-injector'; + +const LUMIGO_INJECTOR_VOLUME_MOUNT_POINT = '/opt/lumigo'; + +const LUMIGO_INJECTOR_IMAGE_NAME = 'public.ecr.aws/lumigo/lumigo-autotrace:latest'; + +const LUMIGO_INJECTOR_ENV_VAR_NAME = 'LD_PRELOAD'; + +const LUMIGO_INJECTOR_ENV_VAR_VALUE = `${LUMIGO_INJECTOR_VOLUME_MOUNT_POINT}/injector/lumigo_injector.so`; + +const DEFAULT_LUMIGO_TRACE_PROPS: LumigoTraceProps = { + traceLambda: true, + traceEcs: false, // For now it's experimental + lambdaEnableW3CTraceContext: false, +}; + +const DEFAULT_TRACE_ECS_TASK_DEFINITION_PROPS: TraceEcsTaskDefinitionProps = { + applyAutoTraceTag: true, +}; + export class Lumigo { props: LumigoProps; @@ -90,18 +150,35 @@ export class Lumigo { Annotations.of(node).addWarning(message); } - public traceEverything(root: App | Stack, props: LumigoAspectProps = { - lambdaEnableW3CTraceContext: false, - }) { + public traceEverything(root: App | Stack, props: LumigoTraceProps = DEFAULT_LUMIGO_TRACE_PROPS) { Aspects.of(root).add(this.asAspect(props)); } - private asAspect(props: LumigoAspectProps = { - lambdaEnableW3CTraceContext: false, - }): IAspect { + private asAspect(props: LumigoTraceProps = DEFAULT_LUMIGO_TRACE_PROPS): IAspect { const lumigo = this; return { visit: function(construct: IConstruct): void { + function applyAutotraceTagThroughTagManager() { + try { + /** + * To overcome the limitation that (1) tags are implemented as aspects, (2) that + * we are already inside an aspect and (3) an aspect cannot add aspects, we need + * to access the TagManager of the function and add the tag manually. + */ + const scope = construct.node?.scope; + if (!!scope) { + /* eslint-disable */ + const tags = (scope as any)['tags']; + /* eslint-enable */ + if (!!tags && tags instanceof TagManager) { + tags.setTag(LUMIGO_AUTOTRACE_TAG_NAME, LUMIGO_AUTOTRACE_TAG_VALUE, 100, true); + } + } + } catch (e) { + lumigo.warning(construct, `Cannot set the '${LUMIGO_AUTOTRACE_TAG_NAME}' tag to '${LUMIGO_AUTOTRACE_TAG_VALUE}'.`); + } + } + if (construct instanceof Function) { try { const layerType = lumigo.getLayerType(construct); @@ -122,25 +199,7 @@ export class Lumigo { */ applyAutoTraceTag: false, }); - - try { - /** - * To overcome the limitation that (1) tags are implemented as aspects, (2) that - * we are already inside an aspect and (3) an aspect cannot add aspects, we need - * to access the TagManager of the function and add the tag manually. - */ - const scope = construct.node?.scope; - if (!!scope) { - /* eslint-disable */ - const tags = (scope as any)['tags']; - /* eslint-enable */ - if (!!tags && tags instanceof TagManager) { - tags.setTag(LUMIGO_AUTOTRACE_TAG_NAME, LUMIGO_AUTOTRACE_TAG_VALUE, 100, true); - } - } - } catch (e) { - lumigo.warning(construct, `Cannot set the '${LUMIGO_AUTOTRACE_TAG_NAME}' tag to '${LUMIGO_AUTOTRACE_TAG_VALUE}'.`); - } + applyAutotraceTagThroughTagManager(); } catch (e) { if (e instanceof UnsupportedLambdaRuntimeError) { lumigo.info(construct, `The '${e.unsupportedRuntime}' cannot be automatically traced by Lumigo.`); @@ -148,12 +207,141 @@ export class Lumigo { throw e; } } + } else if (!!props.traceEcs) { + if ( + construct instanceof Ec2Service || + construct instanceof FargateService || + construct instanceof ApplicationLoadBalancedEc2Service || + construct instanceof ApplicationLoadBalancedFargateService || + construct instanceof ApplicationMultipleTargetGroupsEc2Service || + construct instanceof ApplicationMultipleTargetGroupsFargateService || + construct instanceof NetworkLoadBalancedEc2Service || + construct instanceof NetworkLoadBalancedFargateService || + construct instanceof NetworkMultipleTargetGroupsEc2Service || + construct instanceof NetworkMultipleTargetGroupsFargateService || + construct instanceof QueueProcessingEc2Service || + construct instanceof QueueProcessingFargateService || + construct instanceof ScheduledEc2Task || + construct instanceof ScheduledFargateTask + ) { + lumigo.traceEcsService(construct, { + applyAutoTraceTag: false, + }); + } else if (construct instanceof TaskDefinition) { + lumigo.traceEcsTaskDefinition(construct, { + applyAutoTraceTag: false, + }); + applyAutotraceTagThroughTagManager(); + } } }, }; } - public traceLambda(lambda: SupportedFunction, props: TraceLambdaProps = { + /** + * @returns A wrapper that invokes `traceTaskDefinition` on the task definition to be extended. + */ + public asEcsExtension(): ITaskDefinitionExtension { + return { + extend: this.traceEcsTaskDefinition.bind(this), + }; + } + + public traceEcsService(service: SupportedEcsService, props: TraceEcsServiceDefinitionProps = { + applyAutoTraceTag: true, + }) { + this.warning(service, 'Autotracing of ECS workloads is experimental; if you find any issues, please let us know at https://support.lumigo.io!'); + + this.doTraceEcsTaskDefinition(service.taskDefinition); + if (!!props.applyAutoTraceTag) { + this.applyAutotraceTag(service); + } + } + + public traceEcsTaskDefinition(taskDefinition: TaskDefinition, props: TraceEcsTaskDefinitionProps = DEFAULT_TRACE_ECS_TASK_DEFINITION_PROPS) { + this.warning(taskDefinition, 'Autotracing of ECS workloads is experimental; if you find any issues, please let us know at https://support.lumigo.io!'); + this.doTraceEcsTaskDefinition(taskDefinition, props); + } + + private doTraceEcsTaskDefinition(taskDefinition: TaskDefinition, props: TraceEcsTaskDefinitionProps = DEFAULT_TRACE_ECS_TASK_DEFINITION_PROPS) { + /* + * This function must be idempotent, as `Lumigo.traceEverything()` will apply to + * both the ECS Service and its ECS TaskDefinition. + */ + if (!getTaskDefinitionVolumes(taskDefinition).find(volume => volume.name === LUMIGO_INJECTOR_VOLUME_NAME)) { + taskDefinition.addVolume({ + name: LUMIGO_INJECTOR_VOLUME_NAME, + }); + } + + // Add injector container + const TARGET_DIRECTORY_PATH = '/target'; + const injectorContainer = getTaskDefinitionContainers(taskDefinition) + .find(container => container.containerName === LUMIGO_INJECTOR_CONTAINER_NAME) + || // We did not find the injector container yet, time to add it + taskDefinition.addContainer(LUMIGO_INJECTOR_CONTAINER_NAME, { + image: ContainerImage.fromRegistry(LUMIGO_INJECTOR_IMAGE_NAME), + containerName: LUMIGO_INJECTOR_CONTAINER_NAME, + environment: { + TARGET_DIRECTORY: TARGET_DIRECTORY_PATH, + }, + essential: false, + }); + + if (!injectorContainer.mountPoints?.find(mountPoint => mountPoint.sourceVolume === LUMIGO_INJECTOR_VOLUME_NAME)) { + injectorContainer.addMountPoints({ + sourceVolume: LUMIGO_INJECTOR_VOLUME_NAME, + containerPath: TARGET_DIRECTORY_PATH, + readOnly: false, + }); + } + + const lumigoToken = this.props.lumigoToken.toString(); + + // We wait to start any other container until the inject has done its work + const otherContainers: ContainerDefinition[] = getTaskDefinitionContainers(taskDefinition) + .filter((it: ContainerDefinition) => it !== injectorContainer); + + otherContainers.forEach(container => { + if (!container.containerDependencies?.find(containerDependency => containerDependency.container === injectorContainer)) { + container.addContainerDependencies({ + container: injectorContainer, + condition: ContainerDependencyCondition.COMPLETE, + }); + } + + if (!container.mountPoints?.find(mountPoint => mountPoint.sourceVolume === LUMIGO_INJECTOR_VOLUME_NAME)) { + container.addMountPoints({ + sourceVolume: LUMIGO_INJECTOR_VOLUME_NAME, + containerPath: LUMIGO_INJECTOR_VOLUME_MOUNT_POINT, + readOnly: true, + }); + } + + // Trigger the injector + // The environment is implemented as a dictionary, no need for idempotency checks + container.addEnvironment(LUMIGO_INJECTOR_ENV_VAR_NAME, LUMIGO_INJECTOR_ENV_VAR_VALUE); + // TODO Create secret instead? + // The environment is implemented as a dictionary, no need for idempotency checks + container.addEnvironment(LUMIGO_TRACER_TOKEN_ENV_VAR_NAME, lumigoToken); + }); + + taskDefinition.node.addValidation(new EcsTaskDefinitionLumigoInjectorVolumeValidation(taskDefinition)); + taskDefinition.node.addValidation(new EcsTaskDefinitionLumigoInjectorContainerValidation(taskDefinition)); + + otherContainers.forEach(container => { + taskDefinition.node.addValidation(new EcsContainerDefinitionLumigoInjectorVolumeMountPointValidation(container)); + taskDefinition.node.addValidation(new EcsContainerDefinitionLumigoInjectorContainerConditionValidation(container, injectorContainer)); + taskDefinition.node.addValidation(new EcsContainerDefinitionHasLumigoInjectorEnvVarValidation(container)); + taskDefinition.node.addValidation(new EcsContainerDefinitionHasLumigoTracerTokenEnvVarValidation(container, lumigoToken)); + }); + + if (!!props.applyAutoTraceTag) { + this.applyAutotraceTag(taskDefinition); + } + } + + public traceLambda(lambda: SupportedLambdaFunction, props: TraceLambdaProps = { enableW3CTraceContext: false, applyAutoTraceTag: true, }) { @@ -193,20 +381,24 @@ export class Lumigo { lambda.node.addValidation(new HasLumigoPythonHandlerInResourceValidation(lambda)); } - if (props.enableW3CTraceContext === true) { + if (!!props.enableW3CTraceContext) { lambda.addEnvironment(LUMIGO_PROPAGATE_W3C_ENV_VAR_NAME, String(true)); lambda.node.addValidation(new HasLumigoPropagateW3CEnvVarValidation(lambda)); } - if (props.applyAutoTraceTag === true) { - Tags.of(lambda).add(LUMIGO_AUTOTRACE_TAG_NAME, LUMIGO_AUTOTRACE_TAG_VALUE); + if (!!props.applyAutoTraceTag) { + this.applyAutotraceTag(lambda); } this.info(lambda, `This function has been modified with Lumigo auto-tracing by the '${LUMIGO_AUTOTRACE_TAG_VALUE}' package.`); } - private getLayerType(lambda: SupportedFunction): LambdaLayerType { + private applyAutotraceTag(construct: Construct): void { + Tags.of(construct).add(LUMIGO_AUTOTRACE_TAG_NAME, LUMIGO_AUTOTRACE_TAG_VALUE); + } + + private getLayerType(lambda: SupportedLambdaFunction): LambdaLayerType { switch (lambda.runtime) { case Runtime.NODEJS_10_X: case Runtime.NODEJS_12_X: @@ -235,11 +427,194 @@ export class Lumigo { } +abstract class TaskDefinitionValidation implements IValidation { + + private readonly taskDefinition: TaskDefinition; + + private readonly issues: string[] = []; + + constructor(taskDefinition: TaskDefinition) { + this.taskDefinition = taskDefinition; + } + + protected addIssue = (issue: string) => this.issues.push(issue); + + public validate(): string[] { + this.validateTaskDefinition(this.taskDefinition); + + return this.issues; + } + + protected abstract validateTaskDefinition(taskDefinition: TaskDefinition): void; + +} + +class EcsTaskDefinitionLumigoInjectorContainerValidation extends TaskDefinitionValidation { + + constructor(taskDefinition: TaskDefinition) { + super(taskDefinition); + } + + protected validateTaskDefinition(taskDefinition: TaskDefinition): void { + var lumigoInjectorContainers = getTaskDefinitionContainers(taskDefinition).filter(container => container.containerName == 'lumigo-injector'); + switch (lumigoInjectorContainers.length) { + case 0: { + this.addIssue(`No container called '${LUMIGO_INJECTOR_CONTAINER_NAME}' found; did you modify the task definition after adding Lumigo tracing to it?`); + return; + } + case 1: { + // TODO Validate contaienr image + break; + } + default: { + this.addIssue(`${lumigoInjectorContainers.length} containers called '${LUMIGO_INJECTOR_CONTAINER_NAME}' found; did you set Lumigo tracing up multiple times for this task definition?`); + return; + } + } + } + +} + +class EcsTaskDefinitionLumigoInjectorVolumeValidation extends TaskDefinitionValidation { + + constructor(taskDefinition: TaskDefinition) { + super(taskDefinition); + } + + protected validateTaskDefinition(taskDefinition: TaskDefinition): void { + var lumigoInjectorVolumes = getTaskDefinitionVolumes(taskDefinition)?.filter(volume => volume.name == 'lumigo-injector'); + + switch (lumigoInjectorVolumes?.length || 0) { + case 0: { + this.addIssue(`No volume called '${LUMIGO_INJECTOR_VOLUME_NAME}' found; did you modify the task definition after adding Lumigo tracing to it?`); + return; + } + case 1: { + break; + } + default: { + this.addIssue(`${lumigoInjectorVolumes.length} volume called '${LUMIGO_INJECTOR_VOLUME_NAME}' found; did you set Lumigo tracing up multiple times for this task definition?`); + return; + } + } + + const lumigoInjectorVolume = lumigoInjectorVolumes[0]; + if (!!lumigoInjectorVolume.dockerVolumeConfiguration) { + this.addIssue(`The '${LUMIGO_INJECTOR_VOLUME_NAME}' volume has 'dockerVolumeConfiguration's attached to it; did you modify the task definition after adding Lumigo tracing to it?`); + } + if (!!lumigoInjectorVolume.efsVolumeConfiguration) { + this.addIssue(`The '${LUMIGO_INJECTOR_VOLUME_NAME}' volume has 'efsVolumeConfiguration's attached to it; did you modify the task definition after adding Lumigo tracing to it?`); + } + } + +} + +abstract class ContainerDefinitionValidation implements IValidation { + + private readonly containerDefinition: ContainerDefinition; + + private readonly issues: string[] = []; + + constructor(containerDefinition: ContainerDefinition) { + this.containerDefinition = containerDefinition; + } + + protected addIssue = (issue: string) => this.issues.push(`Container '${this.containerDefinition.containerName}': ${issue}`); + + public validate(): string[] { + this.validateContainerDefinition(this.containerDefinition); + + return this.issues; + } + + protected abstract validateContainerDefinition(containerDefinition: ContainerDefinition): void; + +} + +class EcsContainerDefinitionLumigoInjectorVolumeMountPointValidation extends ContainerDefinitionValidation { + + constructor(containerDefinition: ContainerDefinition) { + super(containerDefinition); + } + + protected validateContainerDefinition(containerDefinition: ContainerDefinition) { + const injectorVolumeMountPoint = containerDefinition.mountPoints?.find(mountPoint => mountPoint.sourceVolume === LUMIGO_INJECTOR_VOLUME_NAME); + + if (!injectorVolumeMountPoint) { + this.addIssue(`No mount point '${LUMIGO_INJECTOR_VOLUME_NAME}' found`); + } else { + if (!injectorVolumeMountPoint.readOnly) { + this.addIssue(`The mount point for the '${LUMIGO_INJECTOR_VOLUME_NAME}' volume is not set to read-only`); + } + if (injectorVolumeMountPoint.containerPath !== LUMIGO_INJECTOR_VOLUME_MOUNT_POINT) { + this.addIssue(`The container path of the mount point for the '${LUMIGO_INJECTOR_VOLUME_NAME}' volume is not set to '${LUMIGO_INJECTOR_VOLUME_MOUNT_POINT}'`); + } + } + } + +} + +class EcsContainerDefinitionLumigoInjectorContainerConditionValidation extends ContainerDefinitionValidation { + + readonly injectorContainer: ContainerDefinition; + + constructor(containerDefinition: ContainerDefinition, injectorContainer: ContainerDefinition) { + super(containerDefinition); + this.injectorContainer = injectorContainer; + } + + protected validateContainerDefinition(containerDefinition: ContainerDefinition) { + const lumigoInjectorContainerDependency = containerDefinition.containerDependencies + .find(containerDependency => containerDependency.container === this.injectorContainer); + + if (lumigoInjectorContainerDependency?.condition !== ContainerDependencyCondition.COMPLETE) { + this.addIssue(`The container dependency condition of the '${containerDefinition.containerName}' on the '${this.injectorContainer.containerName}' is not set to '${ContainerDependencyCondition.COMPLETE}'`); + } + } + +} + +class EcsContainerDefinitionHasLumigoInjectorEnvVarValidation extends ContainerDefinitionValidation { + + constructor(containerDefinition: ContainerDefinition) { + super(containerDefinition); + } + + protected validateContainerDefinition(containerDefinition: ContainerDefinition) { + const environment: { [key: string]: string } = (containerDefinition as any).environment; + + if (environment[LUMIGO_INJECTOR_ENV_VAR_NAME] !== LUMIGO_INJECTOR_ENV_VAR_VALUE) { + this.addIssue(`Container '${containerDefinition.containerName}': The '${LUMIGO_INJECTOR_ENV_VAR_NAME}' does not have the expected value '${LUMIGO_INJECTOR_ENV_VAR_VALUE}'`); + } + } + +} + +class EcsContainerDefinitionHasLumigoTracerTokenEnvVarValidation extends ContainerDefinitionValidation { + + readonly expectedToken: string; + + constructor(containerDefinition: ContainerDefinition, expectedToken: string) { + super(containerDefinition); + this.expectedToken = expectedToken; + } + + protected validateContainerDefinition(containerDefinition: ContainerDefinition) { + const environment: { [key: string]: string } = (containerDefinition as any).environment; + + if (environment[LUMIGO_TRACER_TOKEN_ENV_VAR_NAME] !== this.expectedToken) { + // Don't print out the token value, who knows where these logs end up + this.addIssue(`The '${LUMIGO_TRACER_TOKEN_ENV_VAR_NAME}' does not have the expected value provided in the SecretValue passed to the Lumigo object`); + } + } + +} + class HasExactlyOneLumigoLayerValidation implements IValidation { - private readonly lambda: SupportedFunction; + private readonly lambda: SupportedLambdaFunction; - constructor(lambda: SupportedFunction) { + constructor(lambda: SupportedLambdaFunction) { this.lambda = lambda; } @@ -267,9 +642,9 @@ class HasExactlyOneLumigoLayerValidation implements IValidation { class HasLumigoTracerEnvVarValidation implements IValidation { - private readonly lambda: SupportedFunction; + private readonly lambda: SupportedLambdaFunction; - constructor(lambda: SupportedFunction) { + constructor(lambda: SupportedLambdaFunction) { this.lambda = lambda; } @@ -301,9 +676,9 @@ class HasLumigoTracerEnvVarValidation implements IValidation { class HasAwsLambdaExecWrapperEnvVarValidation implements IValidation { - private readonly lambda: SupportedFunction; + private readonly lambda: SupportedLambdaFunction; - constructor(lambda: SupportedFunction) { + constructor(lambda: SupportedLambdaFunction) { this.lambda = lambda; } @@ -335,9 +710,9 @@ class HasAwsLambdaExecWrapperEnvVarValidation implements IValidation { class HasAwsLambdaOriginalHandlerEnvVarValidation implements IValidation { - private readonly lambda: SupportedFunction; + private readonly lambda: SupportedLambdaFunction; - constructor(lambda: SupportedFunction) { + constructor(lambda: SupportedLambdaFunction) { this.lambda = lambda; } @@ -369,9 +744,9 @@ class HasAwsLambdaOriginalHandlerEnvVarValidation implements IValidation { class HasLumigoPropagateW3CEnvVarValidation implements IValidation { - private readonly lambda: SupportedFunction; + private readonly lambda: SupportedLambdaFunction; - constructor(lambda: SupportedFunction) { + constructor(lambda: SupportedLambdaFunction) { this.lambda = lambda; } @@ -403,9 +778,9 @@ class HasLumigoPropagateW3CEnvVarValidation implements IValidation { class HasLumigoPythonHandlerInResourceValidation implements IValidation { - private readonly lambda: SupportedFunction; + private readonly lambda: SupportedLambdaFunction; - constructor(lambda: SupportedFunction) { + constructor(lambda: SupportedLambdaFunction) { this.lambda = lambda; } @@ -447,3 +822,11 @@ class UnsupportedLambdaLayerRegion extends Error { } } + +function getTaskDefinitionContainers(taskDefinition: TaskDefinition): ContainerDefinition[] { + return (taskDefinition as any).containers as ContainerDefinition[]; +} + +function getTaskDefinitionVolumes(taskDefinition: TaskDefinition): Volume[] { + return (taskDefinition as any).volumes as Volume[] || []; +} diff --git a/test/lumigo-ecs-tracing.test.ts b/test/lumigo-ecs-tracing.test.ts new file mode 100644 index 0000000..7d2dda2 --- /dev/null +++ b/test/lumigo-ecs-tracing.test.ts @@ -0,0 +1,222 @@ +import './matchers/custom-matchers'; +import { App, SecretValue, Stack, StackProps } from 'aws-cdk-lib'; +import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { Cluster, ContainerDefinition, ContainerDependencyCondition, EcrImage, FargateTaskDefinition, TaskDefinition, Volume } from 'aws-cdk-lib/aws-ecs'; +import { ApplicationLoadBalancedFargateService, QueueProcessingFargateService } from 'aws-cdk-lib/aws-ecs-patterns'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; +import { Construct } from 'constructs'; +import { Lumigo } from '../src'; + +class ApplicationLoadBalancedFargateServiceStack extends Stack { + + readonly taskDefinition: TaskDefinition; + + constructor(scope: Construct, id: string, props: StackProps = {}) { + super(scope, id, props); + + const vpc = new Vpc(this, 'TestVpc', { + vpcName: 'TestVpc', + cidr: '10.0.0.0/16', + maxAzs: 3, // Default is all AZs in region + natGateways: 1, + subnetConfiguration: [ + { + name: 'private-subnet', + subnetType: SubnetType.PRIVATE_WITH_NAT, + cidrMask: 24, + }, + { + name: 'public-subnet', + subnetType: SubnetType.PUBLIC, + cidrMask: 24, + }, + ], + }); + + const cluster = new Cluster(this, 'TestCluster', { + clusterName: 'TestCluster', + vpc, + }); + + this.taskDefinition = new FargateTaskDefinition(this, 'TestDefinition', {}); + this.taskDefinition.addContainer('app', { + image: EcrImage.fromRegistry('docker.io/library/hello-world', {}), + environment: { + OTEL_SERVICE_NAME: 'http-server', // This will be the service name in Lumigo + LUMIGO_DEBUG_SPANDUMP: '/dev/stdout', + }, + portMappings: [{ + containerPort: 8443, + }], + }); + + new ApplicationLoadBalancedFargateService(this, 'TestService', { + cluster, + taskDefinition: this.taskDefinition, + }); + } + +} + +class QueueProcessingFargateServiceStack extends Stack { + + readonly taskDefinition: TaskDefinition; + + constructor(scope: Construct, id: string, props: StackProps = {}) { + super(scope, id, props); + + const vpc = new Vpc(this, 'TestVpc', { + vpcName: 'TestVpc', + cidr: '10.0.0.0/16', + maxAzs: 3, // Default is all AZs in region + natGateways: 1, + subnetConfiguration: [ + { + name: 'private-subnet', + subnetType: SubnetType.PRIVATE_WITH_NAT, + cidrMask: 24, + }, + { + name: 'public-subnet', + subnetType: SubnetType.PUBLIC, + cidrMask: 24, + }, + ], + }); + + const cluster = new Cluster(this, 'TestCluster', { + clusterName: 'TestCluster', + vpc, + }); + + const queue = new Queue(this, 'TestQueue'); + + const service = new QueueProcessingFargateService(this, 'TestService', { + cluster, + image: EcrImage.fromRegistry('docker.io/library/hello-world', {}), + environment: { + OTEL_SERVICE_NAME: 'http-server', // This will be the service name in Lumigo + LUMIGO_DEBUG_SPANDUMP: '/dev/stdout', + }, + queue, + healthCheck: { + command: ['CMD-SHELL', 'pgrep python3'], // Check that the process is still running + }, + }); + + this.taskDefinition = service.taskDefinition; + } + +}; + +describe('ECS tracing injection', () => { + + function checkInjectionOccurred(taskDefinition: TaskDefinition) { + const containers: ContainerDefinition[] = (taskDefinition as any).containers; + + expect(containers).toHaveLength(2); + + const volumes = (taskDefinition as any).volumes as Volume[]; + expect(volumes).toHaveLength(1); + expect(volumes[0].name).toBe('lumigo-injector'); + expect(volumes[0].dockerVolumeConfiguration).toBeUndefined(); + expect(volumes[0].efsVolumeConfiguration).toBeUndefined(); + + const lumigoInjectorContainer = containers.find(container => container.containerName === 'lumigo-injector')!; + expect(lumigoInjectorContainer.imageName).toBe('public.ecr.aws/lumigo/lumigo-autotrace:latest'); + expect(lumigoInjectorContainer.containerDependencies).toHaveLength(0); + expect(lumigoInjectorContainer.mountPoints).toContainEqual({ + sourceVolume: 'lumigo-injector', + containerPath: '/target', + readOnly: false, + }); + + const appContainer = containers.find(container => container.containerName !== 'lumigo-injector')!; + const environment = (appContainer as any).environment as {[key: string]: string}; + expect(environment.LUMIGO_TRACER_TOKEN).toMatch(/^\$\{Token\[.+\]\}$/); + expect(environment.LD_PRELOAD).toBe('/opt/lumigo/injector/lumigo_injector.so'); + + expect(appContainer.mountPoints).toContainEqual({ + sourceVolume: 'lumigo-injector', + containerPath: '/opt/lumigo', + readOnly: true, + }); + + expect(appContainer.containerDependencies!).toHaveLength(1); + const containerDependency = appContainer.containerDependencies![0]!; + expect(containerDependency.condition).toBe(ContainerDependencyCondition.COMPLETE); + expect(containerDependency.container).toBe(lumigoInjectorContainer); + } + + describe('with a QueueProcessingFargateService', () => { + + test('works as intended', () => { + const app = new App(); + + new Lumigo({ lumigoToken: SecretValue.secretsManager('LumigoToken') }).traceEverything(app, { + traceEcs: true, + }); + + const root = new QueueProcessingFargateServiceStack(app, 'TestService'); + + app.synth(); + + const serviceConstruct = root.node.children + .find(construct => construct instanceof QueueProcessingFargateService) as QueueProcessingFargateService; + + checkInjectionOccurred(serviceConstruct.taskDefinition); + }); + + test('is idempotent', () => { + const app = new App(); + + const root = new QueueProcessingFargateServiceStack(app, 'TestService'); + + const lumigo = new Lumigo({ lumigoToken: SecretValue.secretsManager('LumigoToken') }); + lumigo.traceEverything(app, { + traceEcs: true, + }); + lumigo.traceEcsTaskDefinition(root.taskDefinition); + + app.synth(); + + checkInjectionOccurred(root.taskDefinition); + }); + + }); + + describe('with a ApplicationLoadBalancedFargateService', () => { + + test('works as intended', () => { + const app = new App(); + + new Lumigo({ lumigoToken: SecretValue.secretsManager('LumigoToken') }).traceEverything(app, { + traceEcs: true, + }); + + const root = new ApplicationLoadBalancedFargateServiceStack(app, 'TestService'); + + app.synth(); + + checkInjectionOccurred(root.taskDefinition); + }); + + test('is idempotent', () => { + const app = new App(); + + const root = new ApplicationLoadBalancedFargateServiceStack(app, 'TestService'); + + const lumigo = new Lumigo({ lumigoToken: SecretValue.secretsManager('LumigoToken') }); + lumigo.traceEverything(app, { + traceEcs: true, + }); + lumigo.traceEcsTaskDefinition(root.taskDefinition); + + app.synth(); + + checkInjectionOccurred(root.taskDefinition); + }); + + }); + +}); diff --git a/test/lumigo.test.ts b/test/lumigo-lambda-tracing.test.ts similarity index 92% rename from test/lumigo.test.ts rename to test/lumigo-lambda-tracing.test.ts index 9d7c505..357d4db 100644 --- a/test/lumigo.test.ts +++ b/test/lumigo-lambda-tracing.test.ts @@ -1,11 +1,10 @@ import './matchers/custom-matchers'; import { App, SecretValue, Stack, StackProps } from 'aws-cdk-lib'; -import { Alias, Function, InlineCode, LayerVersion, Runtime, SingletonFunction } from 'aws-cdk-lib/aws-lambda'; +import { Alias, Function, InlineCode, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; import { Lumigo } from '../src'; - -export class NodejsTestStack extends Stack { +class NodejsTestStack extends Stack { constructor(scope: Construct, id: string, props: StackProps = {}) { super(scope, id, props); @@ -21,7 +20,7 @@ interface PythonTestStackProps extends StackProps { readonly handler?: string; } -export class PythonTestStack extends Stack { +class PythonTestStack extends Stack { constructor(scope: Construct, id: string, props: PythonTestStackProps = {}) { super(scope, id, props); @@ -33,7 +32,7 @@ export class PythonTestStack extends Stack { } } -export class NodejsAliasTestStack extends Stack { +class NodejsAliasTestStack extends Stack { constructor(scope: Construct, id: string, props: StackProps = {}) { super(scope, id, props); @@ -52,31 +51,11 @@ export class NodejsAliasTestStack extends Stack { } } -export class SingletonFunctionTestStack extends Stack { - constructor(scope: Construct, id: string, props: StackProps = {}) { - super(scope, id, props); - - new SingletonFunction(this, 'MyLambda1', { - uuid: 'af5f3e05-4361-4f78-bb0d-87198da1af99', - code: new InlineCode('foo'), - handler: 'index.handler', - runtime: Runtime.NODEJS_14_X, - }); - - new SingletonFunction(this, 'MyLambda2', { - uuid: 'af5f3e05-4361-4f78-bb0d-87198da1af99', - code: new InlineCode('foo'), - handler: 'index.handler', - runtime: Runtime.NODEJS_14_X, - }); - } -} - interface LumigoStackProps extends StackProps { readonly lumigo: Lumigo; } -export class NodejsTestSingleLambdaStack extends Stack { +class NodejsTestSingleLambdaStack extends Stack { constructor(scope: Construct, id: string, props: LumigoStackProps) { super(scope, id, props); @@ -90,7 +69,7 @@ export class NodejsTestSingleLambdaStack extends Stack { } } -export class PythonTestSingleLambdaStack extends Stack { +class PythonTestSingleLambdaStack extends Stack { constructor(scope: Construct, id: string, props: LumigoStackProps) { super(scope, id, props); @@ -104,7 +83,7 @@ export class PythonTestSingleLambdaStack extends Stack { } } -export class NodejsTestOverrideAwsLambdaExecWrapperEnvVarsStack extends Stack { +class NodejsTestOverrideAwsLambdaExecWrapperEnvVarsStack extends Stack { constructor(scope: Construct, id: string, props: LumigoStackProps) { super(scope, id, props); @@ -120,7 +99,7 @@ export class NodejsTestOverrideAwsLambdaExecWrapperEnvVarsStack extends Stack { } } -export class NodejsTestOverrideLumigoTracerTokenWrapperEnvVarsStack extends Stack { +class NodejsTestOverrideLumigoTracerTokenWrapperEnvVarsStack extends Stack { constructor(scope: Construct, id: string, props: LumigoStackProps) { super(scope, id, props); @@ -136,7 +115,7 @@ export class NodejsTestOverrideLumigoTracerTokenWrapperEnvVarsStack extends Stac } } -export class NodejsTestOverrideLumigoLayerStack extends Stack { +class NodejsTestOverrideLumigoLayerStack extends Stack { constructor(scope: Construct, id: string, props: LumigoStackProps) { super(scope, id, props); @@ -152,7 +131,7 @@ export class NodejsTestOverrideLumigoLayerStack extends Stack { } } -export class NodejsTestSingleLambdaPinnedLayerVersionStack extends Stack { +class NodejsTestSingleLambdaPinnedLayerVersionStack extends Stack { constructor(scope: Construct, id: string, props: LumigoStackProps) { super(scope, id, props); @@ -168,7 +147,7 @@ export class NodejsTestSingleLambdaPinnedLayerVersionStack extends Stack { } } -export class PythonTestSingleLambdaPinnedLayerVersionStack extends Stack { +class PythonTestSingleLambdaPinnedLayerVersionStack extends Stack { constructor(scope: Construct, id: string, props: LumigoStackProps) { super(scope, id, props);