@aws/ml-container-creator 0.2.1 → 0.2.3

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.
Files changed (36) hide show
  1. package/bin/cli.js +88 -86
  2. package/config/bootstrap-stack.json +211 -0
  3. package/config/parameter-schema.json +88 -0
  4. package/infra/ci-harness/bin/ci-harness.ts +26 -0
  5. package/infra/ci-harness/buildspec.yml +352 -0
  6. package/infra/ci-harness/cdk.json +27 -0
  7. package/infra/ci-harness/lambda/scanner/index.ts +199 -0
  8. package/infra/ci-harness/lib/ci-harness-stack.ts +609 -0
  9. package/infra/ci-harness/package-lock.json +3979 -0
  10. package/infra/ci-harness/package.json +32 -0
  11. package/infra/ci-harness/tsconfig.json +38 -0
  12. package/package.json +13 -3
  13. package/src/app.js +318 -318
  14. package/src/copy-tpl.js +19 -19
  15. package/src/lib/asset-manager.js +74 -74
  16. package/src/lib/aws-profile-parser.js +45 -45
  17. package/src/lib/bootstrap-command-handler.js +560 -547
  18. package/src/lib/bootstrap-config.js +45 -45
  19. package/src/lib/ci-register-helpers.js +19 -19
  20. package/src/lib/ci-report-helpers.js +37 -37
  21. package/src/lib/ci-stage-helpers.js +49 -49
  22. package/src/lib/comment-generator.js +4 -4
  23. package/src/lib/config-manager.js +105 -105
  24. package/src/lib/deployment-config-resolver.js +10 -10
  25. package/src/lib/deployment-registry.js +153 -153
  26. package/src/lib/engine-prefix-resolver.js +8 -8
  27. package/src/lib/key-value-parser.js +6 -6
  28. package/src/lib/manifest-cli.js +108 -108
  29. package/src/lib/prompt-runner.js +224 -224
  30. package/src/lib/prompts.js +121 -121
  31. package/src/lib/registry-command-handler.js +174 -174
  32. package/src/lib/registry-loader.js +52 -52
  33. package/src/lib/sensitive-redactor.js +9 -9
  34. package/src/lib/template-engine.js +1 -1
  35. package/src/lib/template-manager.js +62 -62
  36. package/src/prompt-adapter.js +18 -18
package/bin/cli.js CHANGED
@@ -2,35 +2,35 @@
2
2
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import { createRequire } from 'module'
6
- import path from 'path'
7
- import { program, Option, Help } from 'commander'
8
- import { run } from '../src/app.js'
5
+ import { createRequire } from 'module';
6
+ import path from 'path';
7
+ import { program, Option, Help } from 'commander';
8
+ import { run } from '../src/app.js';
9
9
 
10
- const require = createRequire(import.meta.url)
11
- const { version } = require('../package.json')
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require('../package.json');
12
12
 
13
13
  /**
14
14
  * Collect repeatable options into an array.
15
15
  * Used for --model-env and --server-env which can be specified multiple times.
16
16
  */
17
17
  function collect(value, previous) {
18
- return previous.concat([value])
18
+ return previous.concat([value]);
19
19
  }
20
20
 
21
21
  program
22
22
  .name('ml-container-creator')
23
23
  .version(version)
24
24
  .enablePositionalOptions()
25
- .passThroughOptions()
26
25
  .helpCommand('help [command]', 'Display help for command')
27
- .argument('[project-name]', 'Name for the generated project')
26
+ .argument('[project-name...]', 'Name for the generated project')
28
27
 
29
28
  // --- General ---
30
29
  .addOption(new Option('--skip-prompts', 'Skip interactive prompts and use configuration from other sources'))
31
30
  .addOption(new Option('--config <path>', 'Path to configuration file'))
32
31
  .addOption(new Option('--project-name <name>', 'Project name'))
33
32
  .addOption(new Option('--project-dir <dir>', 'Output directory path'))
33
+ .addOption(new Option('--force', 'Overwrite existing output directory without prompting'))
34
34
 
35
35
  // --- Model & Framework ---
36
36
  .addOption(new Option('--deployment-config <config>', 'Deployment configuration (e.g. http-flask, transformers-vllm, triton-fil)'))
@@ -104,7 +104,7 @@ program
104
104
  .addOption(new Option('--validate-with-docker', 'Enable Docker introspection validation (opt-in)'))
105
105
  .addOption(new Option('--offline', 'Disable HuggingFace API lookups'))
106
106
 
107
- .action(run)
107
+ .action((projectNameArgs, options) => run(projectNameArgs?.[0] || null, options));
108
108
 
109
109
  // Custom help formatting — group options into logical sections (root command only)
110
110
  program.configureHelp({
@@ -112,28 +112,28 @@ program.configureHelp({
112
112
  // Only apply custom grouping to the root command
113
113
  if (cmd !== program) {
114
114
  // Fall back to default Commander formatting for subcommands
115
- return Help.prototype.formatHelp.call(this, cmd, helper)
115
+ return Help.prototype.formatHelp.call(this, cmd, helper);
116
116
  }
117
117
 
118
- const termWidth = helper.padWidth(cmd, helper)
118
+ const termWidth = helper.padWidth(cmd, helper);
119
119
 
120
120
  function callFormatItem(term, description) {
121
- return helper.formatItem(term, termWidth, description, helper)
121
+ return helper.formatItem(term, termWidth, description, helper);
122
122
  }
123
123
 
124
124
  function formatSection(title, options) {
125
- if (options.length === 0) return []
125
+ if (options.length === 0) return [];
126
126
  const lines = options.map(opt => {
127
127
  return callFormatItem(
128
128
  helper.styleOptionTerm(helper.optionTerm(opt)),
129
129
  helper.styleOptionDescription(helper.optionDescription(opt))
130
- )
131
- })
132
- return [helper.styleTitle(`${title}:`), ...lines, '']
130
+ );
131
+ });
132
+ return [helper.styleTitle(`${title}:`), ...lines, ''];
133
133
  }
134
134
 
135
135
  // Collect all visible options
136
- const allOptions = helper.visibleOptions(cmd)
136
+ const allOptions = helper.visibleOptions(cmd);
137
137
 
138
138
  // Partition options into groups by flag prefix/purpose
139
139
  const groups = {
@@ -150,38 +150,38 @@ program.configureHelp({
150
150
  features: [],
151
151
  mcp: [],
152
152
  validation: []
153
- }
153
+ };
154
154
 
155
155
  for (const opt of allOptions) {
156
- const long = opt.long || ''
157
- if (['--skip-prompts', '--config', '--project-name', '--project-dir', '--version', '--help'].includes(long)) {
158
- groups.general.push(opt)
156
+ const long = opt.long || '';
157
+ if (['--skip-prompts', '--config', '--project-name', '--project-dir', '--force', '--version', '--help'].includes(long)) {
158
+ groups.general.push(opt);
159
159
  } else if (['--deployment-config', '--framework', '--model-format', '--model-name', '--model-server', '--base-image'].includes(long)) {
160
- groups.model.push(opt)
160
+ groups.model.push(opt);
161
161
  } else if (['--deployment-target', '--instance-type', '--region', '--role-arn', '--build-target', '--codebuild-compute-type'].includes(long)) {
162
- groups.infra.push(opt)
162
+ groups.infra.push(opt);
163
163
  } else if (long.startsWith('--endpoint-')) {
164
- groups.endpoint.push(opt)
164
+ groups.endpoint.push(opt);
165
165
  } else if (long.startsWith('--ic-')) {
166
- groups.ic.push(opt)
166
+ groups.ic.push(opt);
167
167
  } else if (long.startsWith('--async-')) {
168
- groups.async.push(opt)
168
+ groups.async.push(opt);
169
169
  } else if (long.startsWith('--batch-')) {
170
- groups.batch.push(opt)
170
+ groups.batch.push(opt);
171
171
  } else if (long.startsWith('--hyperpod-') || long === '--fsx-volume-handle') {
172
- groups.hyperpod.push(opt)
172
+ groups.hyperpod.push(opt);
173
173
  } else if (['--model-env', '--server-env'].includes(long)) {
174
- groups.env.push(opt)
174
+ groups.env.push(opt);
175
175
  } else if (['--hf-token'].includes(long)) {
176
- groups.auth.push(opt)
176
+ groups.auth.push(opt);
177
177
  } else if (['--include-sample', '--include-testing', '--test-types'].includes(long)) {
178
- groups.features.push(opt)
178
+ groups.features.push(opt);
179
179
  } else if (['--smart', '--discover'].includes(long)) {
180
- groups.mcp.push(opt)
180
+ groups.mcp.push(opt);
181
181
  } else if (['--validate-env-vars', '--validate-with-docker', '--offline'].includes(long)) {
182
- groups.validation.push(opt)
182
+ groups.validation.push(opt);
183
183
  } else {
184
- groups.general.push(opt)
184
+ groups.general.push(opt);
185
185
  }
186
186
  }
187
187
 
@@ -189,56 +189,57 @@ program.configureHelp({
189
189
  let output = [
190
190
  `${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`,
191
191
  ''
192
- ]
192
+ ];
193
193
 
194
194
  // Arguments
195
- const args = helper.visibleArguments(cmd)
195
+ const args = helper.visibleArguments(cmd);
196
196
  if (args.length > 0) {
197
197
  const argList = args.map(arg => {
198
198
  return callFormatItem(
199
199
  helper.styleArgumentTerm(helper.argumentTerm(arg)),
200
200
  helper.styleArgumentDescription(helper.argumentDescription(arg))
201
- )
202
- })
203
- output = output.concat([helper.styleTitle('Arguments:'), ...argList, ''])
201
+ );
202
+ });
203
+ output = output.concat([helper.styleTitle('Arguments:'), ...argList, '']);
204
204
  }
205
205
 
206
206
  // Option sections
207
- output = output.concat(formatSection('General', groups.general))
208
- output = output.concat(formatSection('Model & Framework', groups.model))
209
- output = output.concat(formatSection('Build & Infrastructure', groups.infra))
210
- output = output.concat(formatSection('Endpoint (Real-Time Inference)', groups.endpoint))
211
- output = output.concat(formatSection('Inference Component', groups.ic))
212
- output = output.concat(formatSection('Async Inference', groups.async))
213
- output = output.concat(formatSection('Batch Transform', groups.batch))
214
- output = output.concat(formatSection('HyperPod (EKS)', groups.hyperpod))
215
- output = output.concat(formatSection('Environment Variables', groups.env))
216
- output = output.concat(formatSection('Authentication', groups.auth))
217
- output = output.concat(formatSection('Optional Features', groups.features))
218
- output = output.concat(formatSection('MCP & Discovery', groups.mcp))
219
- output = output.concat(formatSection('Validation', groups.validation))
207
+ output = output.concat(formatSection('General', groups.general));
208
+ output = output.concat(formatSection('Model & Framework', groups.model));
209
+ output = output.concat(formatSection('Build & Infrastructure', groups.infra));
210
+ output = output.concat(formatSection('Endpoint (Real-Time Inference)', groups.endpoint));
211
+ output = output.concat(formatSection('Inference Component', groups.ic));
212
+ output = output.concat(formatSection('Async Inference', groups.async));
213
+ output = output.concat(formatSection('Batch Transform', groups.batch));
214
+ output = output.concat(formatSection('HyperPod (EKS)', groups.hyperpod));
215
+ output = output.concat(formatSection('Environment Variables', groups.env));
216
+ output = output.concat(formatSection('Authentication', groups.auth));
217
+ output = output.concat(formatSection('Optional Features', groups.features));
218
+ output = output.concat(formatSection('MCP & Discovery', groups.mcp));
219
+ output = output.concat(formatSection('Validation', groups.validation));
220
220
 
221
221
  // Commands
222
- const cmds = helper.visibleCommands(cmd)
222
+ const cmds = helper.visibleCommands(cmd);
223
223
  if (cmds.length > 0) {
224
224
  const cmdList = cmds.map(sub => {
225
225
  return callFormatItem(
226
226
  helper.styleSubcommandTerm(helper.subcommandTerm(sub)),
227
227
  helper.styleSubcommandDescription(helper.subcommandDescription(sub))
228
- )
229
- })
230
- output = output.concat([helper.styleTitle('Commands:'), ...cmdList, ''])
228
+ );
229
+ });
230
+ output = output.concat([helper.styleTitle('Commands:'), ...cmdList, '']);
231
231
  }
232
232
 
233
- return output.join('\n')
233
+ return output.join('\n');
234
234
  }
235
- })
235
+ });
236
236
 
237
237
  // Sub-commands — wired to actual handlers
238
238
 
239
239
  program
240
240
  .command('bootstrap')
241
241
  .description('Set up AWS infrastructure (IAM role, ECR repo, S3 buckets)')
242
+ .passThroughOptions()
242
243
  .argument('[action]', 'Bootstrap action (status, use, list, remove, scan, prune, update)')
243
244
  .argument('[args...]', 'Additional arguments')
244
245
  .option('--profile <profile>', 'AWS profile name')
@@ -249,15 +250,16 @@ program
249
250
  .option('--verify', 'Verify resources exist (for status)')
250
251
  .option('--delete-stack', 'Delete CloudFormation stack on remove')
251
252
  .action(async (action, args, options) => {
252
- const { default: BootstrapCommandHandler } = await import('../src/lib/bootstrap-command-handler.js')
253
- const handler = new BootstrapCommandHandler()
254
- const allArgs = action ? [action, ...args] : []
255
- await handler.handle(allArgs, options)
256
- })
253
+ const { default: BootstrapCommandHandler } = await import('../src/lib/bootstrap-command-handler.js');
254
+ const handler = new BootstrapCommandHandler();
255
+ const allArgs = action ? [action, ...args] : [];
256
+ await handler.handle(allArgs, options);
257
+ });
257
258
 
258
259
  program
259
260
  .command('mcp')
260
261
  .description('Manage MCP servers (add, list, get, remove, init)')
262
+ .passThroughOptions()
261
263
  .argument('<action>', 'MCP action (add, list, get, remove, init)')
262
264
  .argument('[args...]', 'Additional arguments')
263
265
  .option('-e <env>', 'Environment variable in KEY=VALUE format (for add)')
@@ -265,25 +267,26 @@ program
265
267
  .option('--limit <n>', 'Result limit for MCP server (for add)')
266
268
  .option('--bundled', 'Use a bundled server from servers/ directory')
267
269
  .action(async (action, args, options) => {
268
- const { default: McpCommandHandler } = await import('../src/lib/mcp-command-handler.js')
269
- const { runPrompts } = await import('../src/prompt-adapter.js')
270
+ const { default: McpCommandHandler } = await import('../src/lib/mcp-command-handler.js');
271
+ const { runPrompts } = await import('../src/prompt-adapter.js');
270
272
  // McpCommandHandler expects a generator-like object with destinationPath() and prompt()
271
273
  const generatorAdapter = {
272
274
  destinationPath(...segments) {
273
- if (segments.length === 0) return process.cwd()
274
- return path.join(process.cwd(), ...segments)
275
+ if (segments.length === 0) return process.cwd();
276
+ return path.join(process.cwd(), ...segments);
275
277
  },
276
278
  async prompt(prompts) {
277
- return runPrompts(prompts)
279
+ return runPrompts(prompts);
278
280
  }
279
- }
280
- const handler = new McpCommandHandler(generatorAdapter)
281
- await handler.handle([action, ...args], options)
282
- })
281
+ };
282
+ const handler = new McpCommandHandler(generatorAdapter);
283
+ await handler.handle([action, ...args], options);
284
+ });
283
285
 
284
286
  program
285
287
  .command('registry')
286
288
  .description('Registry operations (list, get, remove, replay, export, import, search) — experimental, may be reconciled with do/register')
289
+ .passThroughOptions()
287
290
  .argument('<action>', 'Registry action (log, list, get, remove, replay, export, import, search)')
288
291
  .argument('[args...]', 'Additional arguments')
289
292
  .option('--backend <backend>', 'Filter by backend')
@@ -306,20 +309,19 @@ program
306
309
  .option('--parameters <json>', 'Parameters JSON string')
307
310
  .option('--generator-version <version>', 'Generator version')
308
311
  .action(async (action, args, options) => {
309
- const { default: RegistryCommandHandler } = await import('../src/lib/registry-command-handler.js')
310
- const handler = new RegistryCommandHandler()
311
- await handler.handle([action, ...args], options)
312
- })
312
+ const { default: RegistryCommandHandler } = await import('../src/lib/registry-command-handler.js');
313
+ const handler = new RegistryCommandHandler();
314
+ await handler.handle([action, ...args], options);
315
+ });
313
316
 
314
317
  program
315
318
  .command('configure')
316
319
  .description('Interactive configuration setup (experimental)')
317
320
  .action(async () => {
318
- const { runPrompts } = await import('../src/prompt-adapter.js')
319
- const { default: ConfigurationExporter } = await import('../src/lib/configuration-exporter.js')
321
+ const { runPrompts } = await import('../src/prompt-adapter.js');
320
322
 
321
- console.log('\n🔧 ML Container Creator Configuration (experimental)')
322
- console.log('\nThis will help you set up configuration files for your project.\n')
323
+ console.log('\n🔧 ML Container Creator Configuration (experimental)');
324
+ console.log('\nThis will help you set up configuration files for your project.\n');
323
325
 
324
326
  const answers = await runPrompts([
325
327
  {
@@ -331,7 +333,7 @@ program
331
333
  { name: 'Show environment variable examples', value: 'env' }
332
334
  ]
333
335
  }
334
- ])
336
+ ]);
335
337
 
336
338
  if (answers.configType === 'cli') {
337
339
  console.log(`
@@ -347,7 +349,7 @@ program
347
349
 
348
350
  # Using a config file
349
351
  ml-container-creator --config=my-config.json --skip-prompts
350
- `)
352
+ `);
351
353
  } else if (answers.configType === 'env') {
352
354
  console.log(`
353
355
  🌍 Environment Variables:
@@ -358,8 +360,8 @@ program
358
360
  export HF_TOKEN="hf_..."
359
361
 
360
362
  Then run: ml-container-creator --deployment-config=http-flask --skip-prompts
361
- `)
363
+ `);
362
364
  }
363
- })
365
+ });
364
366
 
365
- program.parse()
367
+ program.parse();
@@ -0,0 +1,211 @@
1
+ {
2
+ "AWSTemplateFormatVersion": "2010-09-09",
3
+ "Description": "ML Container Creator — shared bootstrap infrastructure (IAM role, ECR repository, optional S3 buckets). Re-run bootstrap to apply updates from new versions.",
4
+
5
+ "Parameters": {
6
+ "CreateS3Buckets": {
7
+ "Type": "String",
8
+ "Default": "false",
9
+ "AllowedValues": ["true", "false"],
10
+ "Description": "Whether to create S3 buckets for async inference and batch transform"
11
+ },
12
+ "UseExistingRoleArn": {
13
+ "Type": "String",
14
+ "Default": "",
15
+ "Description": "ARN of an existing IAM role to use instead of creating one. Leave empty to create a new role."
16
+ }
17
+ },
18
+
19
+ "Conditions": {
20
+ "ShouldCreateS3Buckets": { "Fn::Equals": [{ "Ref": "CreateS3Buckets" }, "true"] },
21
+ "ShouldCreateRole": { "Fn::Equals": [{ "Ref": "UseExistingRoleArn" }, ""] }
22
+ },
23
+
24
+ "Resources": {
25
+ "SageMakerExecutionRole": {
26
+ "Type": "AWS::IAM::Role",
27
+ "Condition": "ShouldCreateRole",
28
+ "Properties": {
29
+ "RoleName": "mlcc-sagemaker-execution-role",
30
+ "AssumeRolePolicyDocument": {
31
+ "Version": "2012-10-17",
32
+ "Statement": [
33
+ {
34
+ "Effect": "Allow",
35
+ "Principal": { "Service": "sagemaker.amazonaws.com" },
36
+ "Action": "sts:AssumeRole"
37
+ }
38
+ ]
39
+ },
40
+ "Policies": [
41
+ {
42
+ "PolicyName": "mlcc-execution-policy",
43
+ "PolicyDocument": {
44
+ "Version": "2012-10-17",
45
+ "Statement": [
46
+ {
47
+ "Sid": "SageMakerEndpoints",
48
+ "Effect": "Allow",
49
+ "Action": [
50
+ "sagemaker:CreateEndpoint",
51
+ "sagemaker:CreateEndpointConfig",
52
+ "sagemaker:CreateModel",
53
+ "sagemaker:CreateInferenceComponent",
54
+ "sagemaker:UpdateEndpoint",
55
+ "sagemaker:UpdateEndpointWeightsAndCapacities",
56
+ "sagemaker:UpdateInferenceComponent",
57
+ "sagemaker:DeleteEndpoint",
58
+ "sagemaker:DeleteEndpointConfig",
59
+ "sagemaker:DeleteModel",
60
+ "sagemaker:DeleteInferenceComponent",
61
+ "sagemaker:DescribeEndpoint",
62
+ "sagemaker:DescribeEndpointConfig",
63
+ "sagemaker:DescribeModel",
64
+ "sagemaker:DescribeInferenceComponent",
65
+ "sagemaker:InvokeEndpoint",
66
+ "sagemaker:InvokeEndpointAsync"
67
+ ],
68
+ "Resource": "*"
69
+ },
70
+ {
71
+ "Sid": "ECRPull",
72
+ "Effect": "Allow",
73
+ "Action": [
74
+ "ecr:GetAuthorizationToken",
75
+ "ecr:BatchCheckLayerAvailability",
76
+ "ecr:GetDownloadUrlForLayer",
77
+ "ecr:BatchGetImage"
78
+ ],
79
+ "Resource": { "Fn::Sub": "arn:aws:ecr:*:${AWS::AccountId}:repository/ml-container-creator" }
80
+ },
81
+ {
82
+ "Sid": "ECRAuth",
83
+ "Effect": "Allow",
84
+ "Action": "ecr:GetAuthorizationToken",
85
+ "Resource": "*"
86
+ },
87
+ {
88
+ "Sid": "CloudWatchLogs",
89
+ "Effect": "Allow",
90
+ "Action": [
91
+ "logs:CreateLogGroup",
92
+ "logs:CreateLogStream",
93
+ "logs:PutLogEvents"
94
+ ],
95
+ "Resource": "arn:aws:logs:*:*:*"
96
+ },
97
+ {
98
+ "Sid": "S3ModelRead",
99
+ "Effect": "Allow",
100
+ "Action": [
101
+ "s3:GetObject",
102
+ "s3:ListBucket"
103
+ ],
104
+ "Resource": [
105
+ "arn:aws:s3:::ml-container-creator-*",
106
+ "arn:aws:s3:::ml-container-creator-*/*"
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ }
112
+ ],
113
+ "Tags": [
114
+ { "Key": "mlcc:managed-by", "Value": "ml-container-creator" },
115
+ { "Key": "mlcc:created-by", "Value": "bootstrap" }
116
+ ]
117
+ }
118
+ },
119
+
120
+ "EcrRepository": {
121
+ "Type": "AWS::ECR::Repository",
122
+ "Properties": {
123
+ "RepositoryName": "ml-container-creator",
124
+ "ImageScanningConfiguration": { "ScanOnPush": true },
125
+ "EncryptionConfiguration": { "EncryptionType": "AES256" },
126
+ "LifecyclePolicy": {
127
+ "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"description\":\"Expire untagged images after 30 days\",\"selection\":{\"tagStatus\":\"untagged\",\"countType\":\"sinceImagePushed\",\"countUnit\":\"days\",\"countNumber\":30},\"action\":{\"type\":\"expire\"}}]}"
128
+ },
129
+ "Tags": [
130
+ { "Key": "mlcc:managed-by", "Value": "ml-container-creator" },
131
+ { "Key": "mlcc:created-by", "Value": "bootstrap" }
132
+ ]
133
+ }
134
+ },
135
+
136
+ "AsyncS3Bucket": {
137
+ "Type": "AWS::S3::Bucket",
138
+ "Condition": "ShouldCreateS3Buckets",
139
+ "DeletionPolicy": "Retain",
140
+ "UpdateReplacePolicy": "Retain",
141
+ "Properties": {
142
+ "BucketName": { "Fn::Sub": "${AWS::AccountId}-${AWS::Region}-ml-container-creator-async" },
143
+ "VersioningConfiguration": { "Status": "Enabled" },
144
+ "BucketEncryption": {
145
+ "ServerSideEncryptionConfiguration": [
146
+ { "ServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } }
147
+ ]
148
+ },
149
+ "Tags": [
150
+ { "Key": "mlcc:managed-by", "Value": "ml-container-creator" },
151
+ { "Key": "mlcc:created-by", "Value": "bootstrap" }
152
+ ]
153
+ }
154
+ },
155
+
156
+ "BatchS3Bucket": {
157
+ "Type": "AWS::S3::Bucket",
158
+ "Condition": "ShouldCreateS3Buckets",
159
+ "DeletionPolicy": "Retain",
160
+ "UpdateReplacePolicy": "Retain",
161
+ "Properties": {
162
+ "BucketName": { "Fn::Sub": "${AWS::AccountId}-${AWS::Region}-ml-container-creator-batch" },
163
+ "VersioningConfiguration": { "Status": "Enabled" },
164
+ "BucketEncryption": {
165
+ "ServerSideEncryptionConfiguration": [
166
+ { "ServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } }
167
+ ]
168
+ },
169
+ "Tags": [
170
+ { "Key": "mlcc:managed-by", "Value": "ml-container-creator" },
171
+ { "Key": "mlcc:created-by", "Value": "bootstrap" }
172
+ ]
173
+ }
174
+ }
175
+ },
176
+
177
+ "Outputs": {
178
+ "RoleArn": {
179
+ "Description": "SageMaker execution role ARN",
180
+ "Value": {
181
+ "Fn::If": [
182
+ "ShouldCreateRole",
183
+ { "Fn::GetAtt": ["SageMakerExecutionRole", "Arn"] },
184
+ { "Ref": "UseExistingRoleArn" }
185
+ ]
186
+ }
187
+ },
188
+ "EcrRepositoryName": {
189
+ "Description": "ECR repository name",
190
+ "Value": { "Ref": "EcrRepository" }
191
+ },
192
+ "EcrRepositoryUri": {
193
+ "Description": "ECR repository URI",
194
+ "Value": { "Fn::GetAtt": ["EcrRepository", "RepositoryUri"] }
195
+ },
196
+ "AsyncS3BucketName": {
197
+ "Condition": "ShouldCreateS3Buckets",
198
+ "Description": "S3 bucket for async inference output",
199
+ "Value": { "Ref": "AsyncS3Bucket" }
200
+ },
201
+ "BatchS3BucketName": {
202
+ "Condition": "ShouldCreateS3Buckets",
203
+ "Description": "S3 bucket for batch transform I/O",
204
+ "Value": { "Ref": "BatchS3Bucket" }
205
+ },
206
+ "StackVersion": {
207
+ "Description": "Bootstrap stack template version for forward compatibility tracking",
208
+ "Value": "2026-05-04"
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "deploymentTargets": {
4
+ "managed-inference": {
5
+ "endpoint": {
6
+ "initialInstanceCount": {
7
+ "type": "integer",
8
+ "min": 1,
9
+ "max": 100,
10
+ "default": 1,
11
+ "description": "Number of instances for the endpoint",
12
+ "apiReference": "CreateEndpointConfig.ProductionVariants.InitialInstanceCount"
13
+ },
14
+ "dataCapturePercent": {
15
+ "type": "integer",
16
+ "min": 0,
17
+ "max": 100,
18
+ "default": 0,
19
+ "description": "Percentage of requests to capture",
20
+ "apiReference": "CreateEndpointConfig.DataCaptureConfig.InitialSamplingPercentage"
21
+ },
22
+ "variantName": {
23
+ "type": "string",
24
+ "pattern": "^[a-zA-Z0-9]([\\w-]{0,62}[a-zA-Z0-9])?$",
25
+ "default": "AllTraffic",
26
+ "description": "Name of the production variant",
27
+ "apiReference": "CreateEndpointConfig.ProductionVariants.VariantName"
28
+ },
29
+ "volumeSize": {
30
+ "type": "integer",
31
+ "min": 1,
32
+ "max": 16384,
33
+ "default": null,
34
+ "description": "Size of the ML storage volume in GB",
35
+ "apiReference": "CreateEndpointConfig.ProductionVariants.VolumeSizeInGB"
36
+ }
37
+ },
38
+ "inferenceComponent": {
39
+ "cpuCount": {
40
+ "type": "number",
41
+ "min": 0.25,
42
+ "max": 768,
43
+ "default": null,
44
+ "description": "Number of vCPUs allocated",
45
+ "apiReference": "CreateInferenceComponent.Specification.ComputeResourceRequirements.NumberOfCpuCoresRequired"
46
+ },
47
+ "memorySize": {
48
+ "type": "integer",
49
+ "min": 128,
50
+ "max": 3145728,
51
+ "default": null,
52
+ "description": "Memory allocation in MB",
53
+ "apiReference": "CreateInferenceComponent.Specification.ComputeResourceRequirements.MinMemoryRequiredInMb"
54
+ },
55
+ "gpuCount": {
56
+ "type": "integer",
57
+ "min": 0,
58
+ "max": 8,
59
+ "default": null,
60
+ "description": "Number of GPUs allocated",
61
+ "apiReference": "CreateInferenceComponent.Specification.ComputeResourceRequirements.NumberOfAcceleratorDevicesRequired"
62
+ },
63
+ "copyCount": {
64
+ "type": "integer",
65
+ "min": 0,
66
+ "max": 100,
67
+ "default": 1,
68
+ "description": "Number of inference component copies",
69
+ "apiReference": "CreateInferenceComponent.RuntimeConfig.CopyCount"
70
+ },
71
+ "modelWeight": {
72
+ "type": "number",
73
+ "min": 0,
74
+ "max": 1,
75
+ "default": 1.0,
76
+ "description": "Traffic routing weight for the model",
77
+ "apiReference": "UpdateEndpointWeightsAndCapacities.DesiredWeightsAndCapacities.DesiredWeight"
78
+ }
79
+ }
80
+ },
81
+ "eks": {},
82
+ "async": {},
83
+ "batch": {}
84
+ },
85
+ "extensionPoints": {
86
+ "engines": {}
87
+ }
88
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import 'source-map-support/register';
3
+ import * as cdk from 'aws-cdk-lib';
4
+ import { MlccCiHarnessStack } from '../lib/ci-harness-stack';
5
+
6
+ const app = new cdk.App();
7
+
8
+ // Region and account can be configured via:
9
+ // 1. CDK context: -c region=us-east-1 -c account=123456789012
10
+ // 2. Environment variables: CDK_DEFAULT_REGION, CDK_DEFAULT_ACCOUNT
11
+ // 3. AWS CLI profile (automatic via CDK)
12
+ const region = app.node.tryGetContext('region')
13
+ || process.env.CDK_DEFAULT_REGION
14
+ || process.env.AWS_REGION;
15
+
16
+ const account = app.node.tryGetContext('account')
17
+ || process.env.CDK_DEFAULT_ACCOUNT
18
+ || process.env.AWS_ACCOUNT_ID;
19
+
20
+ new MlccCiHarnessStack(app, 'MlccCiHarnessStack', {
21
+ env: {
22
+ region,
23
+ account,
24
+ },
25
+ description: 'ML Container Creator CI Integration Harness - automated lifecycle testing infrastructure',
26
+ });