Skip to content

Commit 2fa62cd

Browse files
authored
fix!: use CodeBuild instead of Lambda (#4)
* use CodeBuild for build environment * allow to set extractPath * allow to select nodejs version BREAKING CHANGE: the paths where assets are extracted have changed. If you use multiple assets, you may see some errors. Use new `extractPath` property to fix them. closes #2 #3
1 parent 545d7f9 commit 2fa62cd

File tree

15 files changed

+2420
-7355
lines changed

15 files changed

+2420
-7355
lines changed

API.md

+33-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ Then, the extracted directories will be located as the following:
6666
└── module1 # extracted module1 assets
6767
```
6868

69+
You can also override the path where assets are extracted by `extractPath` property for each asset.
70+
6971
Please also check [the example directory](./example/) for a complete example.
7072

7173
## Motivation - why do we need this construct?

example/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## How to Deploy
22
```sh
33
# Assume current directory is the example directory
4-
cd ../src/lambda/nodejs-build
4+
cd ../lambda/nodejs-build
55
npm ci
66
npm run build
77
cd -

example/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class TestStack extends Stack {
6565
buildEnvironment: {
6666
VITE_API_ENDPOINT: api.url,
6767
},
68+
nodejsVersion: 18,
6869
});
6970

7071
new CfnOutput(this, 'DistributionUrl', {

imgs/architecture.png

6.33 KB
Loading

lambda/nodejs-build/index.ts

+79-74
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
import * as fs from 'fs';
2-
import * as path from 'path';
3-
import AdmZip from 'adm-zip';
4-
import extract from 'extract-zip';
5-
import { execSync } from 'child_process';
6-
import { Readable } from 'stream';
7-
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
1+
import { BatchGetBuildsCommand, CodeBuildClient, StartBuildCommand } from '@aws-sdk/client-codebuild';
82
import type { ResourceProperties } from '../../src/types';
93

10-
const s3 = new S3Client({});
4+
const cb = new CodeBuildClient({});
115

126
type Event = {
137
RequestType: 'Create' | 'Update' | 'Delete';
@@ -20,53 +14,98 @@ type Event = {
2014
};
2115

2216
export const handler = async (event: Event, context: any) => {
23-
let rootDir = '';
2417
console.log(JSON.stringify(event));
2518

2619
try {
2720
if (event.RequestType == 'Create' || event.RequestType == 'Update') {
2821
const props = event.ResourceProperties;
29-
rootDir = fs.mkdtempSync('/tmp/extract');
3022

31-
// set npm cache directory under /tmp since other directories are read-only in Lambda env
32-
process.env.NPM_CONFIG_CACHE = '/tmp/.npm';
23+
// start code build project
24+
const build = await cb.send(
25+
new StartBuildCommand({
26+
projectName: props.codeBuildProjectName,
27+
environmentVariablesOverride: [
28+
{
29+
name: 'input',
30+
value: JSON.stringify(
31+
props.sources.map((source) => ({
32+
assetUrl: `s3://${source.sourceBucketName}/${source.sourceObjectKey}`,
33+
extractPath: source.extractPath,
34+
commands: (source.commands ?? []).join(' && '),
35+
}))
36+
),
37+
},
38+
{
39+
name: 'buildCommands',
40+
value: props.buildCommands.join(' && '),
41+
},
42+
{
43+
name: 'destinationBucketName',
44+
value: props.destinationBucketName,
45+
},
46+
{
47+
name: 'destinationObjectKey',
48+
value: props.destinationObjectKey,
49+
},
50+
{
51+
name: 'workingDirectory',
52+
value: props.workingDirectory,
53+
},
54+
{
55+
name: 'outputSourceDirectory',
56+
value: props.outputSourceDirectory,
57+
},
58+
{
59+
name: 'projectName',
60+
value: props.codeBuildProjectName,
61+
},
62+
{
63+
name: 'responseURL',
64+
value: event.ResponseURL,
65+
},
66+
{
67+
name: 'stackId',
68+
value: event.StackId,
69+
},
70+
{
71+
name: 'requestId',
72+
value: event.RequestId,
73+
},
74+
{
75+
name: 'logicalResourceId',
76+
value: event.LogicalResourceId,
77+
},
78+
...Object.entries(props.environment ?? {}).map(([name, value]) => ({
79+
name,
80+
value,
81+
})),
82+
],
83+
})
84+
);
3385

34-
// inject environment variables from resource properties
35-
Object.entries(props.environment ?? {}).forEach(([key, value]) => (process.env[key] = value));
86+
// Sometimes CodeBuild build fails before running buildspec, without calling the CFn callback.
87+
// We can poll the status of a build for a few minutes and sendStatus if such errors are detected.
88+
// if (build.build?.id == null) {
89+
// throw new Error('build id is null');
90+
// }
3691

37-
// Download and extract each asset, and execute commands if any specified.
38-
const promises = props.sources.map(async (p) => {
39-
const dir = await extractZip(rootDir, p.directoryName, p.sourceObjectKey, p.sourceBucketName);
40-
if (p.commands != null) {
41-
execSync(p.commands.join(' && '), { cwd: dir, stdio: 'inherit' });
42-
}
43-
});
92+
// for (let i=0; i< 20; i++) {
93+
// const res = await cb.send(new BatchGetBuildsCommand({ ids: [build.build.id] }));
94+
// const status = res.builds?.[0].buildStatus;
95+
// if (status == null) {
96+
// throw new Error('build status is null');
97+
// }
4498

45-
await Promise.all(promises);
46-
47-
console.log(JSON.stringify(process.env));
48-
const wd = path.join(rootDir, props.workingDirectory);
49-
execSync(props.buildCommands.join(' && '), {
50-
cwd: wd,
51-
stdio: 'inherit',
52-
});
53-
54-
// zip the artifact directory and upload it to a S3 bucket.
55-
const srcPath = path.join(rootDir, props.outputSourceDirectory);
56-
await uploadDistDirectory(srcPath, props.destinationBucketName, props.destinationObjectKey);
99+
// await new Promise((resolve) => setTimeout(resolve, 5000));
100+
// }
57101
} else {
58-
// how do we process 'Delete' event?
102+
// Do nothing on a DELETE event.
103+
await sendStatus('SUCCESS', event, context);
59104
}
60-
await sendStatus('SUCCESS', event, context);
61105
} catch (e) {
62106
console.log(e);
63107
const err = e as Error;
64108
await sendStatus('FAILED', event, context, err.message);
65-
} finally {
66-
if (rootDir != '') {
67-
// remove the working directory to prevent storage leakage
68-
fs.rmSync(rootDir, { recursive: true, force: true });
69-
}
70109
}
71110
};
72111

@@ -91,37 +130,3 @@ const sendStatus = async (status: 'SUCCESS' | 'FAILED', event: Event, context: a
91130
},
92131
});
93132
};
94-
95-
const uploadDistDirectory = async (srcPath: string, dstBucket: string, dstKey: string) => {
96-
const zip = new AdmZip();
97-
zip.addLocalFolder(srcPath);
98-
await s3.send(
99-
new PutObjectCommand({
100-
Body: zip.toBuffer(),
101-
Bucket: dstBucket,
102-
Key: dstKey,
103-
})
104-
);
105-
};
106-
107-
const extractZip = async (rootDir: string, dirName: string, key: string, bucket: string) => {
108-
const dir = path.join(rootDir, dirName);
109-
fs.mkdirSync(dir, { recursive: true });
110-
const zipPath = path.join(dir, 'temp.zip');
111-
await downloadS3File(key, bucket, zipPath);
112-
await extract(zipPath, { dir });
113-
console.log(`extracted to ${dir}`);
114-
return dir;
115-
};
116-
117-
const downloadS3File = async (key: string, bucket: string, dst: string) => {
118-
const data = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
119-
await new Promise((resolve, reject) => {
120-
if (data.Body instanceof Readable) {
121-
data.Body
122-
.pipe(fs.createWriteStream(dst))
123-
.on('error', (err) => reject(err))
124-
.on('close', () => resolve(1));
125-
}
126-
});
127-
};

0 commit comments

Comments
 (0)