Skip to content

Commit ccc0398

Browse files
authored
chore(layers): bundle assets from source when testing (#1710)
1 parent 02aae6d commit ccc0398

12 files changed

+200
-65
lines changed

.github/scripts/setup_tmp_layer_files.sh

Lines changed: 0 additions & 13 deletions
This file was deleted.

.github/workflows/publish_layer.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ jobs:
4242
node-version: "18"
4343
- name: Setup dependencies
4444
uses: ./.github/actions/cached-node-modules
45-
- name: Create layer files
46-
run: |
47-
export VERSION=${{ inputs.latest_published_version }}
48-
bash .github/scripts/setup_tmp_layer_files.sh
4945
- name: CDK build
5046
run: npm run cdk -w layers -- synth --context PowertoolsPackageVersion=${{ inputs.latest_published_version }} -o cdk.out
5147
- name: Zip output

.github/workflows/reusable-run-linting-check-and-unit-tests.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@ jobs:
6969
uses: ./.github/actions/cached-node-modules
7070
- name: Run linting
7171
run: npm run lint -w layers
72-
- name: Create layer files
73-
run: |
74-
export VERSION=latest
75-
bash .github/scripts/setup_tmp_layer_files.sh
7672
- name: Run tests
7773
run: npm run test:unit -w layers
7874
check-docs-snippets:

.github/workflows/run-e2e-tests.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@ jobs:
5555
role-to-assume: ${{ secrets.AWS_ROLE_ARN_TO_ASSUME }}
5656
aws-region: eu-west-1
5757
mask-aws-account-id: true
58-
- name: Create layer files
59-
if: ${{ matrix.package == 'layers' }}
60-
run: |
61-
export VERSION=latest
62-
bash .github/scripts/setup_tmp_layer_files.sh
6358
- name: Run integration tests on utils
6459
env:
6560
RUNTIME: nodejs${{ matrix.version }}x

layers/README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ RUNTIME=node12.x VERSION=0.9.0 npm run test:e2e
4444
```shell
4545
cdk bootstrap aws://AWS_ACCOUNT/NEW_REGION
4646
```
47-
* Build the layer folder from the project root directory
48-
```shell
49-
bash ./.github/scripts/setup_tmp_layer_files.sh
50-
```
5147
* Deploy the first layer version to the new region, make sure to set the NEW_REGION in your AWS CLI configuration correctly, otherwise you will deploy to the wrong region
5248
```shell
5349
npm run cdk -w layers -- deploy --app cdk.out --context region=NEW_REGION 'LayerPublisherStack' --require-approval never --verbose

layers/bin/layers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const app = new App();
1010

1111
new LayerPublisherStack(app, 'LayerPublisherStack', {
1212
powertoolsPackageVersion: app.node.tryGetContext('PowertoolsPackageVersion'),
13+
buildFromLocal: app.node.tryGetContext('BuildFromLocal') || false,
1314
layerName: 'AWSLambdaPowertoolsTypeScript',
1415
ssmParameterLayerArn: SSM_PARAM_LAYER_ARN,
1516
});

layers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .",
1515
"lint-fix": "eslint --fix --ext .ts,.js --fix --no-error-on-unmatched-pattern .",
1616
"test:unit": "jest --group=unit",
17-
"test:e2e": "jest --group=e2e"
17+
"test:e2e": "jest --group=e2e",
18+
"createLayerFolder": "cdk synth --context BuildFromLocal=true"
1819
},
1920
"lint-staged": {
2021
"*.{js,ts}": "npm run lint-fix"

layers/src/canary-stack.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { CustomResource, Duration, Stack, StackProps } from 'aws-cdk-lib';
22
import { Construct } from 'constructs';
3-
import { LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda';
3+
import { LayerVersion, Runtime, Tracing } from 'aws-cdk-lib/aws-lambda';
44
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
55
import { randomUUID } from 'node:crypto';
66
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
77
import { Provider } from 'aws-cdk-lib/custom-resources';
88
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
9-
import path from 'path';
9+
import path from 'node:path';
1010
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
1111

1212
export interface CanaryStackProps extends StackProps {
@@ -54,13 +54,15 @@ export class CanaryStack extends Stack {
5454
],
5555
},
5656
environment: {
57+
LAYERS_PATH: '/opt/nodejs/node_modules',
5758
POWERTOOLS_SERVICE_NAME: 'canary',
5859
POWERTOOLS_PACKAGE_VERSION: powertoolsPackageVersion,
5960
POWERTOOLS_LAYER_NAME: layerName,
6061
SSM_PARAMETER_LAYER_ARN: props.ssmParameterLayerArn,
6162
},
6263
layers: layer,
6364
logRetention: RetentionDays.ONE_DAY,
65+
tracing: Tracing.ACTIVE,
6466
});
6567

6668
canaryFunction.addToRolePolicy(

layers/src/layer-publisher-stack.ts

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
2-
import { Construct } from 'constructs';
32
import {
4-
LayerVersion,
3+
CfnLayerVersionPermission,
54
Code,
5+
LayerVersion,
66
Runtime,
7-
CfnLayerVersionPermission,
87
} from 'aws-cdk-lib/aws-lambda';
98
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
10-
import { resolve } from 'node:path';
9+
import { Construct } from 'constructs';
10+
import { execSync } from 'node:child_process';
11+
import { randomUUID } from 'node:crypto';
12+
import { join, resolve, sep } from 'node:path';
1113

1214
export interface LayerPublisherStackProps extends StackProps {
1315
readonly layerName?: string;
1416
readonly powertoolsPackageVersion?: string;
1517
readonly ssmParameterLayerArn: string;
18+
readonly buildFromLocal?: boolean;
1619
}
1720

1821
export class LayerPublisherStack extends Stack {
@@ -24,7 +27,7 @@ export class LayerPublisherStack extends Stack {
2427
) {
2528
super(scope, id, props);
2629

27-
const { layerName, powertoolsPackageVersion } = props;
30+
const { layerName, powertoolsPackageVersion, buildFromLocal } = props;
2831

2932
console.log(
3033
`publishing layer ${layerName} version : ${powertoolsPackageVersion}`
@@ -41,7 +44,112 @@ export class LayerPublisherStack extends Stack {
4144
license: 'MIT-0',
4245
// This is needed because the following regions do not support the compatibleArchitectures property #1400
4346
// ...(![ 'eu-south-2', 'eu-central-2', 'ap-southeast-4' ].includes(Stack.of(this).region) ? { compatibleArchitectures: [Architecture.X86_64] } : {}),
44-
code: Code.fromAsset(resolve(__dirname, '..', '..', 'tmp')),
47+
code: Code.fromAsset(resolve(__dirname), {
48+
bundling: {
49+
// This is here only because is required by CDK, however it is not used since the bundling is done locally
50+
image: Runtime.NODEJS_18_X.bundlingImage,
51+
// We need to run a command to generate a random UUID to force the bundling to run every time
52+
command: [`echo "${randomUUID()}"`],
53+
local: {
54+
tryBundle(outputDir: string) {
55+
// This folder are relative to the layers folder
56+
const tmpBuildPath = resolve(__dirname, '..', 'tmp');
57+
const tmpBuildDir = join(tmpBuildPath, 'nodejs');
58+
// This folder is the project root, relative to the current file
59+
const projectRoot = resolve(__dirname, '..', '..');
60+
61+
// This is the list of packages that we need include in the Lambda Layer
62+
// the name is the same as the npm workspace name
63+
const utilities = ['commons', 'logger', 'metrics', 'tracer'];
64+
65+
// These files are relative to the tmp folder
66+
const filesToRemove = [
67+
'node_modules/@types',
68+
'package.json',
69+
'package-lock.json',
70+
'node_modules/**/README.md',
71+
'node_modules/.bin/semver',
72+
'node_modules/async-hook-jl/test',
73+
'node_modules/shimmer/test',
74+
'node_modules/jmespath/artifacts',
75+
// We remove the type definitions since they can't be used in the Lambda Layer
76+
'node_modules/@aws-lambda-powertools/*/lib/*.d.ts',
77+
'node_modules/@aws-lambda-powertools/*/lib/*.d.ts.map',
78+
];
79+
const buildCommands: string[] = [];
80+
const modulesToInstall: string[] = [];
81+
82+
if (buildFromLocal) {
83+
for (const util of utilities) {
84+
// Build latest version of the package
85+
buildCommands.push(`npm run build -w packages/${util}`);
86+
// Pack the package to a .tgz file
87+
buildCommands.push(`npm pack -w packages/${util}`);
88+
// Move the .tgz file to the tmp folder
89+
buildCommands.push(
90+
`mv aws-lambda-powertools-${util}-*.tgz ${tmpBuildDir}`
91+
);
92+
}
93+
modulesToInstall.push(
94+
...utilities.map((util) =>
95+
join(tmpBuildDir, `aws-lambda-powertools-${util}-*.tgz`)
96+
)
97+
);
98+
filesToRemove.push(
99+
...utilities.map((util) =>
100+
join(`aws-lambda-powertools-${util}-*.tgz`)
101+
)
102+
);
103+
} else {
104+
// Dependencies to install in the Lambda Layer
105+
modulesToInstall.push(
106+
...utilities.map(
107+
(util) =>
108+
`@aws-lambda-powertools/${util}@${powertoolsPackageVersion}`
109+
)
110+
);
111+
}
112+
113+
// Phase 1: Cleanup & create tmp folder
114+
execSync(
115+
[
116+
// Clean up existing tmp folder from previous builds
117+
`rm -rf ${tmpBuildDir}`,
118+
// Create tmp folder again
119+
`mkdir -p ${tmpBuildDir}`,
120+
].join(' && ')
121+
);
122+
123+
// Phase 2: (Optional) Build packages & pack them
124+
buildFromLocal &&
125+
execSync(buildCommands.join(' && '), { cwd: projectRoot });
126+
127+
// Phase 3: Install dependencies to tmp folder
128+
execSync(
129+
`npm i --prefix ${tmpBuildDir} ${modulesToInstall.join(' ')}`
130+
);
131+
132+
// Phase 4: Remove unnecessary files
133+
execSync(
134+
`rm -rf ${filesToRemove
135+
.map((filePath) => `${tmpBuildDir}/${filePath}`)
136+
.join(' ')}`
137+
);
138+
139+
// Phase 5: Copy files from tmp folder to cdk.out asset folder (the folder is created by CDK)
140+
execSync(`cp -R ${tmpBuildPath}${sep}* ${outputDir}`);
141+
142+
// Phase 6: (Optional) Restore changes to the project root made by the build
143+
buildFromLocal &&
144+
execSync('git restore packages/*/package.json', {
145+
cwd: projectRoot,
146+
});
147+
148+
return true;
149+
},
150+
},
151+
},
152+
}),
45153
});
46154

47155
const layerPermission = new CfnLayerVersionPermission(
Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { readFileSync } from 'node:fs';
1+
import { join } from 'node:path';
2+
import { readFile } from 'node:fs/promises';
23
import { Logger } from '@aws-lambda-powertools/logger';
34
import { Metrics } from '@aws-lambda-powertools/metrics';
45
import { Tracer } from '@aws-lambda-powertools/tracer';
@@ -9,36 +10,63 @@ const logger = new Logger({
910
const metrics = new Metrics();
1011
const tracer = new Tracer();
1112

12-
export const handler = (): void => {
13-
// Check that the packages version matches the expected one
14-
const packageJSON = JSON.parse(
15-
readFileSync(
16-
'/opt/nodejs/node_modules/@aws-lambda-powertools/logger/package.json',
17-
{
18-
encoding: 'utf8',
19-
flag: 'r',
20-
}
21-
)
13+
const layerPath = process.env.LAYERS_PATH || '/opt/nodejs/node_modules';
14+
const expectedVersion = process.env.POWERTOOLS_PACKAGE_VERSION || '0.0.0';
15+
16+
const getVersionFromModule = async (moduleName: string): Promise<string> => {
17+
const manifestPath = join(
18+
layerPath,
19+
'@aws-lambda-powertools',
20+
moduleName,
21+
'package.json'
2222
);
2323

24-
if (packageJSON.version != process.env.POWERTOOLS_PACKAGE_VERSION) {
25-
throw new Error(
26-
`Package version mismatch: ${packageJSON.version} != ${process.env.POWERTOOLS_PACKAGE_VERSION}`
27-
);
24+
let manifest: string;
25+
try {
26+
manifest = await readFile(manifestPath, { encoding: 'utf8' });
27+
} catch (error) {
28+
console.log(error);
29+
throw new Error(`Unable to read/find package.json file at ${manifestPath}`);
2830
}
2931

30-
// Check that the logger is working
31-
logger.debug('Hello World!');
32+
let moduleVersion: string;
33+
try {
34+
const { version } = JSON.parse(manifest);
35+
moduleVersion = version;
36+
} catch (error) {
37+
console.log(error);
38+
throw new Error(`Unable to parse package.json file at ${manifestPath}`);
39+
}
40+
41+
return moduleVersion;
42+
};
43+
44+
export const handler = async (): Promise<void> => {
45+
// Check that the packages version matches the expected one
46+
for (const moduleName of ['commons', 'logger', 'metrics', 'tracer']) {
47+
const moduleVersion = await getVersionFromModule(moduleName);
48+
if (moduleVersion != expectedVersion) {
49+
throw new Error(
50+
`Package version mismatch (${moduleName}): ${moduleVersion} != ${expectedVersion}`
51+
);
52+
}
53+
}
3254

3355
// Check that the metrics is working
3456
metrics.captureColdStartMetric();
3557

3658
// Check that the tracer is working
37-
const segment = tracer.getSegment();
38-
if (!segment) throw new Error('Segment not found');
39-
const handlerSegment = segment.addNewSubsegment('### index.handler');
40-
tracer.setSegment(handlerSegment);
59+
const subsegment = tracer.getSegment()?.addNewSubsegment('### index.handler');
60+
if (!subsegment) {
61+
throw new Error('Unable to create subsegment, check the Tracer');
62+
}
63+
tracer.setSegment(subsegment);
4164
tracer.annotateColdStart();
42-
handlerSegment.close();
43-
tracer.setSegment(segment);
65+
subsegment.close();
66+
tracer.setSegment(subsegment.parent);
67+
68+
// Check that logger & tracer are both working
69+
// the presence of a log will indicate that the logger is working
70+
// while the content of the log will indicate that the tracer is working
71+
logger.debug('subsegment', { subsegment: subsegment.format() });
4472
};

0 commit comments

Comments
 (0)