@rlyle1179/serverless-aws-batch 1.1.19 → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +5 -0
- package/LICENSE +21 -21
- package/README.md +362 -362
- package/index.js +66 -66
- package/lib/awscli.js +107 -107
- package/lib/batchenvironment.js +418 -418
- package/lib/batchtask.js +390 -390
- package/lib/docker.js +139 -139
- package/lib/ecr.js +29 -29
- package/lib/generateCoreTemplate.js +120 -120
- package/lib/schedule.js +90 -90
- package/package.json +35 -35
package/lib/batchtask.js
CHANGED
|
@@ -1,390 +1,390 @@
|
|
|
1
|
-
|
|
2
|
-
const BbPromise = require('bluebird');
|
|
3
|
-
const _ = require('lodash');
|
|
4
|
-
const util = require('util');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const JSZip = require("jszip");
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param functionName
|
|
11
|
-
* @returns {string} The logical ID of the Job Definition resource created for the function
|
|
12
|
-
*/
|
|
13
|
-
function getJobDefinitionLogicalId(functionName) {
|
|
14
|
-
let logicalId = `JobDefinition${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`;
|
|
15
|
-
return logicalId.length > 64
|
|
16
|
-
? logicalId.substring(0, 64)
|
|
17
|
-
: logicalId;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @returns {string} "IamRoleBatchJobExecution"
|
|
22
|
-
*/
|
|
23
|
-
function getBatchJobExecutionRoleLogicalId(functionName) {
|
|
24
|
-
let logicalId = `IamRoleBatchJobExecution${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`;
|
|
25
|
-
return logicalId.length > 64
|
|
26
|
-
? logicalId.substring(0, 64)
|
|
27
|
-
: logicalId;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let dependsOnFunction = undefined;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Transforms a function object into a "JobDefinition" object that can be used to run this function inside a Batch task
|
|
34
|
-
*/
|
|
35
|
-
function compileBatchTask(functionName) {
|
|
36
|
-
const functionObject = this.serverless.service.getFunction(functionName);
|
|
37
|
-
|
|
38
|
-
// If this isn't a batch function, just skip it
|
|
39
|
-
if (! functionObject.hasOwnProperty("batch")) {
|
|
40
|
-
return BbPromise.resolve();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
functionObject.getJobDefinitionName = function() {
|
|
44
|
-
return `${this.provider.serverless.service.service}-${this.provider.getStage()}-${functionName}`
|
|
45
|
-
}.bind(this);
|
|
46
|
-
|
|
47
|
-
// Setup our new function
|
|
48
|
-
const newFunction = {
|
|
49
|
-
"Type": "AWS::Batch::JobDefinition",
|
|
50
|
-
"Properties": {
|
|
51
|
-
"JobDefinitionName": functionObject.getJobDefinitionName(),
|
|
52
|
-
"Type": "container"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
updateContainerProperties.bind(this)(newFunction, functionObject, functionName);
|
|
56
|
-
updateRetryStrategy.bind(this)(newFunction, functionObject);
|
|
57
|
-
updateTimeout.bind(this)(newFunction, functionObject);
|
|
58
|
-
|
|
59
|
-
const logicalId = getJobDefinitionLogicalId(functionName);
|
|
60
|
-
|
|
61
|
-
// generate role for a batchjob execution based on custom iam statements
|
|
62
|
-
const newBatchJobExecutionRoleObject = generateBatchJobExecutionRole.bind(this)(functionName, functionObject)
|
|
63
|
-
|
|
64
|
-
// FIX: CloudFormation is blowing up trying to create all the JobDefinitions in parallel,
|
|
65
|
-
// so to fix we make each JobDefinition depend on the prior one being created to solve this issue.
|
|
66
|
-
if ( dependsOnFunction !== undefined ) {
|
|
67
|
-
for(let k in newBatchJobExecutionRoleObject) {
|
|
68
|
-
newBatchJobExecutionRoleObject[k]["DependsOn"] = dependsOnFunction;
|
|
69
|
-
}
|
|
70
|
-
newFunction["DependsOn"] = dependsOnFunction;
|
|
71
|
-
}
|
|
72
|
-
dependsOnFunction = logicalId;
|
|
73
|
-
|
|
74
|
-
// Add it to our compiled cloud formation templates
|
|
75
|
-
_.merge(
|
|
76
|
-
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
|
|
77
|
-
{
|
|
78
|
-
[logicalId]: newFunction
|
|
79
|
-
},
|
|
80
|
-
newBatchJobExecutionRoleObject
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// Now replace the original lambda function with code to invoke this batch task
|
|
84
|
-
generateLambdaScheduleArtifact.bind(this)(functionName);
|
|
85
|
-
|
|
86
|
-
_.merge(
|
|
87
|
-
this.serverless.service.functions,
|
|
88
|
-
{
|
|
89
|
-
[functionName]: {
|
|
90
|
-
handler: 'handler.schedule',
|
|
91
|
-
name: functionObject.name,
|
|
92
|
-
memorySize: 128,
|
|
93
|
-
timeout: 6,
|
|
94
|
-
runtime: _.get(this.serverless.service,"custom.runtime", "nodejs18.x" ),
|
|
95
|
-
package: {
|
|
96
|
-
artifact: `${path.join(this.serverless.config.servicePath, '.serverless', functionName)}.zip`
|
|
97
|
-
},
|
|
98
|
-
environment: {
|
|
99
|
-
EVENT_LOGGING_ENABLED: functionObject.batch.hasOwnProperty('scheduleLoggingEnabled') && functionObject.batch.scheduleLoggingEnabled,
|
|
100
|
-
FUNCTION_NAME: functionName,
|
|
101
|
-
JOB_DEFINITION_ARN: {
|
|
102
|
-
'Ref': this.provider.naming.getJobDefinitionLogicalId(functionName)
|
|
103
|
-
},
|
|
104
|
-
JOB_QUEUE_ARN: {
|
|
105
|
-
'Ref': this.provider.naming.getBatchJobQueueLogicalId()
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
role: {
|
|
109
|
-
"Fn::GetAtt": [
|
|
110
|
-
this.provider.naming.getLambdaScheduleExecutionRoleLogicalId(),
|
|
111
|
-
"Arn"
|
|
112
|
-
]
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
_.unset(this.serverless.service.functions, `${functionName}.iamRoleStatements`)
|
|
118
|
-
return BbPromise.resolve();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Generates a container properties configuration and adds it to the newFunction object
|
|
123
|
-
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-jobdefinition-containerproperties.html
|
|
124
|
-
*/
|
|
125
|
-
function updateContainerProperties(newFunction, functionObject, functionName) {
|
|
126
|
-
newFunction.Properties.ContainerProperties = _.merge(
|
|
127
|
-
{},
|
|
128
|
-
functionObject.batch.ContainerProperties
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Memory')) {
|
|
132
|
-
const memorySize = getMemorySize(functionObject);
|
|
133
|
-
newFunction.Properties.ContainerProperties.Memory = memorySize;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Command')) {
|
|
137
|
-
if (! functionObject.handler) {
|
|
138
|
-
throw new this.serverless.classes.Error(`Could not find handler for batch task ${functionObject.name}`);
|
|
139
|
-
}
|
|
140
|
-
newFunction.Properties.ContainerProperties.Command = [
|
|
141
|
-
`${functionObject.handler}`,
|
|
142
|
-
"Ref::event"
|
|
143
|
-
]
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Image')) {
|
|
147
|
-
newFunction.Properties.ContainerProperties.Image = this.provider.naming.getDockerImageName();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Vcpus')) {
|
|
151
|
-
newFunction.Properties.ContainerProperties.Vcpus = 1;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('JobRoleArn')) {
|
|
155
|
-
newFunction.Properties.ContainerProperties.JobRoleArn = {
|
|
156
|
-
Ref: this.provider.naming.getBatchJobExecutionRoleLogicalId(functionName)
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Setup the required environment variables and any included in the serverless configuration
|
|
161
|
-
const environment = _.merge(
|
|
162
|
-
{
|
|
163
|
-
'AWS_LAMBDA_FUNCTION_TIMEOUT': getTimeout(functionObject),
|
|
164
|
-
'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': getMemorySize(functionObject),
|
|
165
|
-
'AWS_REGION': this.serverless.service.provider.region || 'us-east-1',
|
|
166
|
-
'AWS_SECRET_ACCESS_KEY': process.env.AWS_SECRET_ACCESS_KEY,
|
|
167
|
-
'AWS_ACCESS_KEY_ID': process.env.AWS_ACCESS_KEY_ID
|
|
168
|
-
},
|
|
169
|
-
this.serverless.service.provider.environment,
|
|
170
|
-
functionObject.environment,
|
|
171
|
-
functionObject.batch.ContainerProperties.Environment
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
let invalidEnvVar = null;
|
|
175
|
-
_.forEach(
|
|
176
|
-
_.keys(environment),
|
|
177
|
-
key => { // eslint-disable-line consistent-return
|
|
178
|
-
// taken from the bash man pages
|
|
179
|
-
if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) {
|
|
180
|
-
invalidEnvVar = `Invalid characters in environment variable ${key}`;
|
|
181
|
-
return false; // break loop with lodash
|
|
182
|
-
}
|
|
183
|
-
const value = environment[key];
|
|
184
|
-
if (_.isObject(value)) {
|
|
185
|
-
const isCFRef = _.isObject(value) &&
|
|
186
|
-
!_.some(value, (v, k) => k !== 'Ref' && !_.startsWith(k, 'Fn::'));
|
|
187
|
-
if (!isCFRef) {
|
|
188
|
-
invalidEnvVar = `Environment variable ${key} must contain string`;
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
if (invalidEnvVar) {
|
|
196
|
-
throw new Error(invalidEnvVar);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
newFunction.Properties.ContainerProperties.Environment = [];
|
|
200
|
-
_.forEach(
|
|
201
|
-
_.keys(environment),
|
|
202
|
-
key => {
|
|
203
|
-
newFunction.Properties.ContainerProperties.Environment.push({
|
|
204
|
-
"Name": key,
|
|
205
|
-
"Value": environment[key]
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const logGroupName = _.get(this.serverless.service,"custom.awsBatch.logGroupName");
|
|
211
|
-
if ( logGroupName ) {
|
|
212
|
-
newFunction.Properties.ContainerProperties.LogConfiguration = {
|
|
213
|
-
LogDriver: 'awslogs',
|
|
214
|
-
Options: {
|
|
215
|
-
"awslogs-group": logGroupName,
|
|
216
|
-
"awslogs-stream-prefix": "batch"
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Generates a retry strategy configuration and adds it to the newFunction object
|
|
224
|
-
*/
|
|
225
|
-
function updateRetryStrategy(newFunction, functionObject) {
|
|
226
|
-
newFunction.Properties.RetryStrategy = _.merge(
|
|
227
|
-
{},
|
|
228
|
-
functionObject.batch.RetryStrategy
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
if (! newFunction.Properties.RetryStrategy.hasOwnProperty('Attempts')) {
|
|
232
|
-
newFunction.Properties.RetryStrategy.Attempts = 1;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Generates a timeout configuration and adds it to the newFunction object
|
|
238
|
-
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-jobdefinition-timeout.html
|
|
239
|
-
*/
|
|
240
|
-
function updateTimeout(newFunction, functionObject) {
|
|
241
|
-
newFunction.Properties.Timeout = _.merge(
|
|
242
|
-
{},
|
|
243
|
-
functionObject.batch.Timeout
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
if (! newFunction.Properties.Timeout.hasOwnProperty('AttemptDurationSeconds')) {
|
|
247
|
-
const timeout = getTimeout(functionObject);
|
|
248
|
-
newFunction.Properties.Timeout.AttemptDurationSeconds = timeout;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Retrieves the memory to use for this function if it's set on the functionObject.
|
|
254
|
-
* Otherwise defaults to 2gb.
|
|
255
|
-
*/
|
|
256
|
-
function getMemorySize(functionObject) {
|
|
257
|
-
return Number(functionObject.batch.memory)
|
|
258
|
-
|| Number(functionObject.memory)
|
|
259
|
-
|| 2048;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Retrieves the timeout to use for this function if it's set on the functionObject.
|
|
264
|
-
* Otherwise defaults to 300s
|
|
265
|
-
*/
|
|
266
|
-
function getTimeout(functionObject) {
|
|
267
|
-
return Number(functionObject.batch.Timeout ? functionObject.batch.Timeout.AttemptDurationSeconds: undefined)
|
|
268
|
-
|| Number(functionObject.timeout)
|
|
269
|
-
|| 300;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Handles copying the "schedule.js" file into a zip deployment artifact for the specific batch function
|
|
275
|
-
*/
|
|
276
|
-
async function generateLambdaScheduleArtifact(functionName) {
|
|
277
|
-
this.serverless.cli.log(`Building lambda schedule artifact for: "${functionName}"...`);
|
|
278
|
-
|
|
279
|
-
const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(functionName);
|
|
280
|
-
const artifactFilePath = path.join(this.serverless.config.servicePath, '.serverless', functionArtifactFileName);
|
|
281
|
-
|
|
282
|
-
const zip = new JSZip();
|
|
283
|
-
|
|
284
|
-
// Add a top-level, arbitrary text file with contents
|
|
285
|
-
const contents = fs.readFileSync(path.join(__dirname, 'schedule.js'));
|
|
286
|
-
zip.file("handler.js", contents);
|
|
287
|
-
|
|
288
|
-
// JSZip can generate Buffers so you can do the following
|
|
289
|
-
const response = new Promise((resolve, reject) => {
|
|
290
|
-
zip.generateNodeStream({type:'nodebuffer',streamFiles:true})
|
|
291
|
-
.pipe(fs.createWriteStream(artifactFilePath))
|
|
292
|
-
.on('finish', function () {
|
|
293
|
-
resolve()
|
|
294
|
-
})
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
return await response;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Generate Batch job execution role per batch job
|
|
301
|
-
*/
|
|
302
|
-
function generateBatchJobExecutionRole(functionName, functionObject) {
|
|
303
|
-
const batchJobExecutionRoleName = `BatchJobRole-${functionName}-${this.provider.getRegion()}-${this.provider.getStage()}-${this.provider.serverless.service.service}`.substring(0, 64);
|
|
304
|
-
const batchJobExecutionRoleTemplate = `
|
|
305
|
-
{
|
|
306
|
-
"Type": "AWS::IAM::Role",
|
|
307
|
-
"Properties": {
|
|
308
|
-
"RoleName": "${batchJobExecutionRoleName}",
|
|
309
|
-
"Path": "/",
|
|
310
|
-
"AssumeRolePolicyDocument": {
|
|
311
|
-
"Version": "2012-10-17",
|
|
312
|
-
"Statement": [
|
|
313
|
-
{
|
|
314
|
-
"Sid": "",
|
|
315
|
-
"Effect": "Allow",
|
|
316
|
-
"Principal": {
|
|
317
|
-
"Service": "ecs-tasks.amazonaws.com"
|
|
318
|
-
},
|
|
319
|
-
"Action": "sts:AssumeRole"
|
|
320
|
-
}
|
|
321
|
-
]
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
`;
|
|
326
|
-
|
|
327
|
-
// Merge our iamRoleStatements into this role
|
|
328
|
-
const batchJobExecutionRole = JSON.parse(batchJobExecutionRoleTemplate);
|
|
329
|
-
if (this.serverless.service.provider.hasOwnProperty('iamRoleStatements')) {
|
|
330
|
-
_.merge(
|
|
331
|
-
batchJobExecutionRole.Properties,
|
|
332
|
-
{
|
|
333
|
-
"Policies": [
|
|
334
|
-
{
|
|
335
|
-
"PolicyName": "batch-job-execution-policies",
|
|
336
|
-
"PolicyDocument": {
|
|
337
|
-
"Version": "2012-10-17",
|
|
338
|
-
"Statement": this.serverless.service.provider.iamRoleStatements
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
]
|
|
342
|
-
}
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Merge in custom iamRoleStatements from lambda function definition
|
|
347
|
-
if (functionObject.hasOwnProperty('iamRoleStatements')) {
|
|
348
|
-
_.mergeWith(
|
|
349
|
-
batchJobExecutionRole.Properties,
|
|
350
|
-
{
|
|
351
|
-
"Policies": [
|
|
352
|
-
{
|
|
353
|
-
"PolicyName": "batch-job-custom-policies",
|
|
354
|
-
"PolicyDocument": {
|
|
355
|
-
"Version": "2012-10-17",
|
|
356
|
-
"Statement": functionObject.iamRoleStatements
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
]
|
|
360
|
-
},
|
|
361
|
-
(objValue, srcValue) => {
|
|
362
|
-
if (_.isArray(objValue)) {
|
|
363
|
-
return objValue.concat(srcValue);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Setup the JobQueue to push tasks to
|
|
370
|
-
return {
|
|
371
|
-
[this.provider.naming.getBatchJobExecutionRoleLogicalId(functionName)]: batchJobExecutionRole
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Iterates through all of our functions, starting the compile to JobDefinition if needed
|
|
377
|
-
*/
|
|
378
|
-
function compileBatchTasks() {
|
|
379
|
-
const allFunctions = this.serverless.service.getAllFunctions();
|
|
380
|
-
return BbPromise.each(
|
|
381
|
-
allFunctions,
|
|
382
|
-
functionName => compileBatchTask.bind(this)(functionName)
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
module.exports = {
|
|
387
|
-
compileBatchTasks,
|
|
388
|
-
getJobDefinitionLogicalId,
|
|
389
|
-
getBatchJobExecutionRoleLogicalId
|
|
390
|
-
};
|
|
1
|
+
|
|
2
|
+
const BbPromise = require('bluebird');
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const util = require('util');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const JSZip = require("jszip");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param functionName
|
|
11
|
+
* @returns {string} The logical ID of the Job Definition resource created for the function
|
|
12
|
+
*/
|
|
13
|
+
function getJobDefinitionLogicalId(functionName) {
|
|
14
|
+
let logicalId = `JobDefinition${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`;
|
|
15
|
+
return logicalId.length > 64
|
|
16
|
+
? logicalId.substring(0, 64)
|
|
17
|
+
: logicalId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {string} "IamRoleBatchJobExecution"
|
|
22
|
+
*/
|
|
23
|
+
function getBatchJobExecutionRoleLogicalId(functionName) {
|
|
24
|
+
let logicalId = `IamRoleBatchJobExecution${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`;
|
|
25
|
+
return logicalId.length > 64
|
|
26
|
+
? logicalId.substring(0, 64)
|
|
27
|
+
: logicalId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let dependsOnFunction = undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Transforms a function object into a "JobDefinition" object that can be used to run this function inside a Batch task
|
|
34
|
+
*/
|
|
35
|
+
function compileBatchTask(functionName) {
|
|
36
|
+
const functionObject = this.serverless.service.getFunction(functionName);
|
|
37
|
+
|
|
38
|
+
// If this isn't a batch function, just skip it
|
|
39
|
+
if (! functionObject.hasOwnProperty("batch")) {
|
|
40
|
+
return BbPromise.resolve();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
functionObject.getJobDefinitionName = function() {
|
|
44
|
+
return `${this.provider.serverless.service.service}-${this.provider.getStage()}-${functionName}`
|
|
45
|
+
}.bind(this);
|
|
46
|
+
|
|
47
|
+
// Setup our new function
|
|
48
|
+
const newFunction = {
|
|
49
|
+
"Type": "AWS::Batch::JobDefinition",
|
|
50
|
+
"Properties": {
|
|
51
|
+
"JobDefinitionName": functionObject.getJobDefinitionName(),
|
|
52
|
+
"Type": "container"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
updateContainerProperties.bind(this)(newFunction, functionObject, functionName);
|
|
56
|
+
updateRetryStrategy.bind(this)(newFunction, functionObject);
|
|
57
|
+
updateTimeout.bind(this)(newFunction, functionObject);
|
|
58
|
+
|
|
59
|
+
const logicalId = getJobDefinitionLogicalId(functionName);
|
|
60
|
+
|
|
61
|
+
// generate role for a batchjob execution based on custom iam statements
|
|
62
|
+
const newBatchJobExecutionRoleObject = generateBatchJobExecutionRole.bind(this)(functionName, functionObject)
|
|
63
|
+
|
|
64
|
+
// FIX: CloudFormation is blowing up trying to create all the JobDefinitions in parallel,
|
|
65
|
+
// so to fix we make each JobDefinition depend on the prior one being created to solve this issue.
|
|
66
|
+
if ( dependsOnFunction !== undefined ) {
|
|
67
|
+
for(let k in newBatchJobExecutionRoleObject) {
|
|
68
|
+
newBatchJobExecutionRoleObject[k]["DependsOn"] = dependsOnFunction;
|
|
69
|
+
}
|
|
70
|
+
newFunction["DependsOn"] = dependsOnFunction;
|
|
71
|
+
}
|
|
72
|
+
dependsOnFunction = logicalId;
|
|
73
|
+
|
|
74
|
+
// Add it to our compiled cloud formation templates
|
|
75
|
+
_.merge(
|
|
76
|
+
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
|
|
77
|
+
{
|
|
78
|
+
[logicalId]: newFunction
|
|
79
|
+
},
|
|
80
|
+
newBatchJobExecutionRoleObject
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Now replace the original lambda function with code to invoke this batch task
|
|
84
|
+
generateLambdaScheduleArtifact.bind(this)(functionName);
|
|
85
|
+
|
|
86
|
+
_.merge(
|
|
87
|
+
this.serverless.service.functions,
|
|
88
|
+
{
|
|
89
|
+
[functionName]: {
|
|
90
|
+
handler: 'handler.schedule',
|
|
91
|
+
name: functionObject.name,
|
|
92
|
+
memorySize: 128,
|
|
93
|
+
timeout: 6,
|
|
94
|
+
runtime: _.get(this.serverless.service,"custom.awsBatch.runtime", "nodejs18.x" ),
|
|
95
|
+
package: {
|
|
96
|
+
artifact: `${path.join(this.serverless.config.servicePath, '.serverless', functionName)}.zip`
|
|
97
|
+
},
|
|
98
|
+
environment: {
|
|
99
|
+
EVENT_LOGGING_ENABLED: functionObject.batch.hasOwnProperty('scheduleLoggingEnabled') && functionObject.batch.scheduleLoggingEnabled,
|
|
100
|
+
FUNCTION_NAME: functionName,
|
|
101
|
+
JOB_DEFINITION_ARN: {
|
|
102
|
+
'Ref': this.provider.naming.getJobDefinitionLogicalId(functionName)
|
|
103
|
+
},
|
|
104
|
+
JOB_QUEUE_ARN: {
|
|
105
|
+
'Ref': this.provider.naming.getBatchJobQueueLogicalId()
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
role: {
|
|
109
|
+
"Fn::GetAtt": [
|
|
110
|
+
this.provider.naming.getLambdaScheduleExecutionRoleLogicalId(),
|
|
111
|
+
"Arn"
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
_.unset(this.serverless.service.functions, `${functionName}.iamRoleStatements`)
|
|
118
|
+
return BbPromise.resolve();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generates a container properties configuration and adds it to the newFunction object
|
|
123
|
+
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-jobdefinition-containerproperties.html
|
|
124
|
+
*/
|
|
125
|
+
function updateContainerProperties(newFunction, functionObject, functionName) {
|
|
126
|
+
newFunction.Properties.ContainerProperties = _.merge(
|
|
127
|
+
{},
|
|
128
|
+
functionObject.batch.ContainerProperties
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Memory')) {
|
|
132
|
+
const memorySize = getMemorySize(functionObject);
|
|
133
|
+
newFunction.Properties.ContainerProperties.Memory = memorySize;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Command')) {
|
|
137
|
+
if (! functionObject.handler) {
|
|
138
|
+
throw new this.serverless.classes.Error(`Could not find handler for batch task ${functionObject.name}`);
|
|
139
|
+
}
|
|
140
|
+
newFunction.Properties.ContainerProperties.Command = [
|
|
141
|
+
`${functionObject.handler}`,
|
|
142
|
+
"Ref::event"
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Image')) {
|
|
147
|
+
newFunction.Properties.ContainerProperties.Image = this.provider.naming.getDockerImageName();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('Vcpus')) {
|
|
151
|
+
newFunction.Properties.ContainerProperties.Vcpus = 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (! newFunction.Properties.ContainerProperties.hasOwnProperty('JobRoleArn')) {
|
|
155
|
+
newFunction.Properties.ContainerProperties.JobRoleArn = {
|
|
156
|
+
Ref: this.provider.naming.getBatchJobExecutionRoleLogicalId(functionName)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Setup the required environment variables and any included in the serverless configuration
|
|
161
|
+
const environment = _.merge(
|
|
162
|
+
{
|
|
163
|
+
'AWS_LAMBDA_FUNCTION_TIMEOUT': getTimeout(functionObject),
|
|
164
|
+
'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': getMemorySize(functionObject),
|
|
165
|
+
'AWS_REGION': this.serverless.service.provider.region || 'us-east-1',
|
|
166
|
+
'AWS_SECRET_ACCESS_KEY': process.env.AWS_SECRET_ACCESS_KEY,
|
|
167
|
+
'AWS_ACCESS_KEY_ID': process.env.AWS_ACCESS_KEY_ID
|
|
168
|
+
},
|
|
169
|
+
this.serverless.service.provider.environment,
|
|
170
|
+
functionObject.environment,
|
|
171
|
+
functionObject.batch.ContainerProperties.Environment
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let invalidEnvVar = null;
|
|
175
|
+
_.forEach(
|
|
176
|
+
_.keys(environment),
|
|
177
|
+
key => { // eslint-disable-line consistent-return
|
|
178
|
+
// taken from the bash man pages
|
|
179
|
+
if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) {
|
|
180
|
+
invalidEnvVar = `Invalid characters in environment variable ${key}`;
|
|
181
|
+
return false; // break loop with lodash
|
|
182
|
+
}
|
|
183
|
+
const value = environment[key];
|
|
184
|
+
if (_.isObject(value)) {
|
|
185
|
+
const isCFRef = _.isObject(value) &&
|
|
186
|
+
!_.some(value, (v, k) => k !== 'Ref' && !_.startsWith(k, 'Fn::'));
|
|
187
|
+
if (!isCFRef) {
|
|
188
|
+
invalidEnvVar = `Environment variable ${key} must contain string`;
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (invalidEnvVar) {
|
|
196
|
+
throw new Error(invalidEnvVar);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
newFunction.Properties.ContainerProperties.Environment = [];
|
|
200
|
+
_.forEach(
|
|
201
|
+
_.keys(environment),
|
|
202
|
+
key => {
|
|
203
|
+
newFunction.Properties.ContainerProperties.Environment.push({
|
|
204
|
+
"Name": key,
|
|
205
|
+
"Value": environment[key]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const logGroupName = _.get(this.serverless.service,"custom.awsBatch.logGroupName");
|
|
211
|
+
if ( logGroupName ) {
|
|
212
|
+
newFunction.Properties.ContainerProperties.LogConfiguration = {
|
|
213
|
+
LogDriver: 'awslogs',
|
|
214
|
+
Options: {
|
|
215
|
+
"awslogs-group": logGroupName,
|
|
216
|
+
"awslogs-stream-prefix": "batch"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Generates a retry strategy configuration and adds it to the newFunction object
|
|
224
|
+
*/
|
|
225
|
+
function updateRetryStrategy(newFunction, functionObject) {
|
|
226
|
+
newFunction.Properties.RetryStrategy = _.merge(
|
|
227
|
+
{},
|
|
228
|
+
functionObject.batch.RetryStrategy
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (! newFunction.Properties.RetryStrategy.hasOwnProperty('Attempts')) {
|
|
232
|
+
newFunction.Properties.RetryStrategy.Attempts = 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generates a timeout configuration and adds it to the newFunction object
|
|
238
|
+
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-jobdefinition-timeout.html
|
|
239
|
+
*/
|
|
240
|
+
function updateTimeout(newFunction, functionObject) {
|
|
241
|
+
newFunction.Properties.Timeout = _.merge(
|
|
242
|
+
{},
|
|
243
|
+
functionObject.batch.Timeout
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (! newFunction.Properties.Timeout.hasOwnProperty('AttemptDurationSeconds')) {
|
|
247
|
+
const timeout = getTimeout(functionObject);
|
|
248
|
+
newFunction.Properties.Timeout.AttemptDurationSeconds = timeout;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Retrieves the memory to use for this function if it's set on the functionObject.
|
|
254
|
+
* Otherwise defaults to 2gb.
|
|
255
|
+
*/
|
|
256
|
+
function getMemorySize(functionObject) {
|
|
257
|
+
return Number(functionObject.batch.memory)
|
|
258
|
+
|| Number(functionObject.memory)
|
|
259
|
+
|| 2048;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Retrieves the timeout to use for this function if it's set on the functionObject.
|
|
264
|
+
* Otherwise defaults to 300s
|
|
265
|
+
*/
|
|
266
|
+
function getTimeout(functionObject) {
|
|
267
|
+
return Number(functionObject.batch.Timeout ? functionObject.batch.Timeout.AttemptDurationSeconds: undefined)
|
|
268
|
+
|| Number(functionObject.timeout)
|
|
269
|
+
|| 300;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handles copying the "schedule.js" file into a zip deployment artifact for the specific batch function
|
|
275
|
+
*/
|
|
276
|
+
async function generateLambdaScheduleArtifact(functionName) {
|
|
277
|
+
this.serverless.cli.log(`Building lambda schedule artifact for: "${functionName}"...`);
|
|
278
|
+
|
|
279
|
+
const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(functionName);
|
|
280
|
+
const artifactFilePath = path.join(this.serverless.config.servicePath, '.serverless', functionArtifactFileName);
|
|
281
|
+
|
|
282
|
+
const zip = new JSZip();
|
|
283
|
+
|
|
284
|
+
// Add a top-level, arbitrary text file with contents
|
|
285
|
+
const contents = fs.readFileSync(path.join(__dirname, 'schedule.js'));
|
|
286
|
+
zip.file("handler.js", contents);
|
|
287
|
+
|
|
288
|
+
// JSZip can generate Buffers so you can do the following
|
|
289
|
+
const response = new Promise((resolve, reject) => {
|
|
290
|
+
zip.generateNodeStream({type:'nodebuffer',streamFiles:true})
|
|
291
|
+
.pipe(fs.createWriteStream(artifactFilePath))
|
|
292
|
+
.on('finish', function () {
|
|
293
|
+
resolve()
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return await response;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Generate Batch job execution role per batch job
|
|
301
|
+
*/
|
|
302
|
+
function generateBatchJobExecutionRole(functionName, functionObject) {
|
|
303
|
+
const batchJobExecutionRoleName = `BatchJobRole-${functionName}-${this.provider.getRegion()}-${this.provider.getStage()}-${this.provider.serverless.service.service}`.substring(0, 64);
|
|
304
|
+
const batchJobExecutionRoleTemplate = `
|
|
305
|
+
{
|
|
306
|
+
"Type": "AWS::IAM::Role",
|
|
307
|
+
"Properties": {
|
|
308
|
+
"RoleName": "${batchJobExecutionRoleName}",
|
|
309
|
+
"Path": "/",
|
|
310
|
+
"AssumeRolePolicyDocument": {
|
|
311
|
+
"Version": "2012-10-17",
|
|
312
|
+
"Statement": [
|
|
313
|
+
{
|
|
314
|
+
"Sid": "",
|
|
315
|
+
"Effect": "Allow",
|
|
316
|
+
"Principal": {
|
|
317
|
+
"Service": "ecs-tasks.amazonaws.com"
|
|
318
|
+
},
|
|
319
|
+
"Action": "sts:AssumeRole"
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
`;
|
|
326
|
+
|
|
327
|
+
// Merge our iamRoleStatements into this role
|
|
328
|
+
const batchJobExecutionRole = JSON.parse(batchJobExecutionRoleTemplate);
|
|
329
|
+
if (this.serverless.service.provider.hasOwnProperty('iamRoleStatements')) {
|
|
330
|
+
_.merge(
|
|
331
|
+
batchJobExecutionRole.Properties,
|
|
332
|
+
{
|
|
333
|
+
"Policies": [
|
|
334
|
+
{
|
|
335
|
+
"PolicyName": "batch-job-execution-policies",
|
|
336
|
+
"PolicyDocument": {
|
|
337
|
+
"Version": "2012-10-17",
|
|
338
|
+
"Statement": this.serverless.service.provider.iamRoleStatements
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Merge in custom iamRoleStatements from lambda function definition
|
|
347
|
+
if (functionObject.hasOwnProperty('iamRoleStatements')) {
|
|
348
|
+
_.mergeWith(
|
|
349
|
+
batchJobExecutionRole.Properties,
|
|
350
|
+
{
|
|
351
|
+
"Policies": [
|
|
352
|
+
{
|
|
353
|
+
"PolicyName": "batch-job-custom-policies",
|
|
354
|
+
"PolicyDocument": {
|
|
355
|
+
"Version": "2012-10-17",
|
|
356
|
+
"Statement": functionObject.iamRoleStatements
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
},
|
|
361
|
+
(objValue, srcValue) => {
|
|
362
|
+
if (_.isArray(objValue)) {
|
|
363
|
+
return objValue.concat(srcValue);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Setup the JobQueue to push tasks to
|
|
370
|
+
return {
|
|
371
|
+
[this.provider.naming.getBatchJobExecutionRoleLogicalId(functionName)]: batchJobExecutionRole
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Iterates through all of our functions, starting the compile to JobDefinition if needed
|
|
377
|
+
*/
|
|
378
|
+
function compileBatchTasks() {
|
|
379
|
+
const allFunctions = this.serverless.service.getAllFunctions();
|
|
380
|
+
return BbPromise.each(
|
|
381
|
+
allFunctions,
|
|
382
|
+
functionName => compileBatchTask.bind(this)(functionName)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
compileBatchTasks,
|
|
388
|
+
getJobDefinitionLogicalId,
|
|
389
|
+
getBatchJobExecutionRoleLogicalId
|
|
390
|
+
};
|