@@ -5,24 +5,37 @@ import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
55import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks' ;
66import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch' ;
77import { Construct } from 'constructs' ;
8+ import * as path from 'path' ;
9+
10+ export interface ImgedaStackProps extends cdk . StackProps {
11+ /** Auto-delete bucket contents on stack destroy. Use for dev/test only. */
12+ readonly autoCleanup ?: boolean ;
13+ }
814
915export class ImgedaStack extends cdk . Stack {
1016 public readonly inputBucket : s3 . Bucket ;
1117 public readonly outputBucket : s3 . Bucket ;
1218 public readonly stateMachine : sfn . StateMachine ;
1319
14- constructor ( scope : Construct , id : string , props ?: cdk . StackProps ) {
20+ constructor ( scope : Construct , id : string , props ?: ImgedaStackProps ) {
1521 super ( scope , id , props ) ;
1622
23+ const autoCleanup = props ?. autoCleanup ?? false ;
24+ const removalPolicy = autoCleanup
25+ ? cdk . RemovalPolicy . DESTROY
26+ : cdk . RemovalPolicy . RETAIN ;
27+
1728 // --- S3 Buckets ---
1829 this . inputBucket = new s3 . Bucket ( this , 'InputBucket' , {
19- removalPolicy : cdk . RemovalPolicy . RETAIN ,
30+ removalPolicy,
31+ autoDeleteObjects : autoCleanup ,
2032 encryption : s3 . BucketEncryption . S3_MANAGED ,
2133 blockPublicAccess : s3 . BlockPublicAccess . BLOCK_ALL ,
2234 } ) ;
2335
2436 this . outputBucket = new s3 . Bucket ( this , 'OutputBucket' , {
25- removalPolicy : cdk . RemovalPolicy . RETAIN ,
37+ removalPolicy,
38+ autoDeleteObjects : autoCleanup ,
2639 encryption : s3 . BucketEncryption . S3_MANAGED ,
2740 blockPublicAccess : s3 . BlockPublicAccess . BLOCK_ALL ,
2841 lifecycleRules : [
@@ -33,74 +46,68 @@ export class ImgedaStack extends cdk.Stack {
3346 ] ,
3447 } ) ;
3548
36- // --- Lambda Layer ---
37- const imgedaLayer = new lambda . LayerVersion ( this , 'ImgedaLayer' , {
38- code : lambda . Code . fromAsset ( '../' , {
39- bundling : {
40- image : lambda . Runtime . PYTHON_3_12 . bundlingImage ,
41- command : [
42- 'bash' , '-c' ,
43- 'pip install . -t /asset-output/python && cp -r src/imgeda /asset-output/python/' ,
44- ] ,
45- } ,
46- } ) ,
47- compatibleRuntimes : [ lambda . Runtime . PYTHON_3_12 ] ,
48- description : 'imgeda package and dependencies' ,
49+ // --- Docker Image Lambda ---
50+ // All 5 Lambda functions share one Docker image built from lambda/Dockerfile.
51+ // Each function sets an ACTION env var to route to the correct handler.
52+ const projectRoot = path . resolve ( __dirname , '../..' ) ;
53+ const imageCode = lambda . DockerImageCode . fromImageAsset ( projectRoot , {
54+ file : 'lambda/Dockerfile' ,
55+ exclude : [
56+ 'cdk' ,
57+ 'tests' ,
58+ '.venv' ,
59+ '.git' ,
60+ 'node_modules' ,
61+ '.github' ,
62+ '.claude' ,
63+ 'dist' ,
64+ 'build' ,
65+ '*.egg-info' ,
66+ '.ruff_cache' ,
67+ '.mypy_cache' ,
68+ '.pytest_cache' ,
69+ 'htmlcov' ,
70+ ] ,
4971 } ) ;
5072
51- // --- Lambda Functions ---
52- const commonLambdaProps : Partial < lambda . FunctionProps > = {
53- runtime : lambda . Runtime . PYTHON_3_12 ,
73+ const listImagesFn = new lambda . DockerImageFunction ( this , 'ListImagesFn' , {
74+ code : imageCode ,
5475 memorySize : 1024 ,
5576 timeout : cdk . Duration . minutes ( 5 ) ,
56- layers : [ imgedaLayer ] ,
57- handler : 'imgeda.lambda_handler.handler.handler' ,
58- } ;
59-
60- const listImagesFn = new lambda . Function ( this , 'ListImagesFn' , {
61- ...commonLambdaProps ,
62- code : lambda . Code . fromInline ( '# handler in layer' ) ,
63- handler : 'imgeda.lambda_handler.handler.handler' ,
6477 description : 'List images in S3 bucket and split into batches' ,
65- timeout : cdk . Duration . minutes ( 5 ) ,
6678 environment : { ACTION : 'list_images' } ,
6779 } ) ;
6880
69- const analyzeBatchFn = new lambda . Function ( this , 'AnalyzeBatchFn' , {
70- ...commonLambdaProps ,
71- code : lambda . Code . fromInline ( '# handler in layer' ) ,
72- handler : 'imgeda.lambda_handler.handler.handler' ,
73- description : 'Analyze a batch of images' ,
81+ const analyzeBatchFn = new lambda . DockerImageFunction ( this , 'AnalyzeBatchFn' , {
82+ code : imageCode ,
7483 memorySize : 2048 ,
7584 timeout : cdk . Duration . minutes ( 10 ) ,
7685 ephemeralStorageSize : cdk . Size . gibibytes ( 2 ) ,
86+ description : 'Analyze a batch of images' ,
7787 environment : { ACTION : 'analyze_batch' } ,
7888 } ) ;
7989
80- const mergeManifestsFn = new lambda . Function ( this , 'MergeManifestsFn' , {
81- ...commonLambdaProps ,
82- code : lambda . Code . fromInline ( '# handler in layer' ) ,
83- handler : 'imgeda.lambda_handler.handler.handler' ,
84- description : 'Merge partial manifests into final JSONL' ,
90+ const mergeManifestsFn = new lambda . DockerImageFunction ( this , 'MergeManifestsFn' , {
91+ code : imageCode ,
92+ memorySize : 1024 ,
8593 timeout : cdk . Duration . minutes ( 10 ) ,
94+ description : 'Merge partial manifests into final JSONL' ,
8695 environment : { ACTION : 'merge_manifests' } ,
8796 } ) ;
8897
89- const aggregateFn = new lambda . Function ( this , 'AggregateFn' , {
90- ... commonLambdaProps ,
91- code : lambda . Code . fromInline ( '# handler in layer' ) ,
92- handler : 'imgeda.lambda_handler.handler.handler' ,
98+ const aggregateFn = new lambda . DockerImageFunction ( this , 'AggregateFn' , {
99+ code : imageCode ,
100+ memorySize : 1024 ,
101+ timeout : cdk . Duration . minutes ( 5 ) ,
93102 description : 'Compute aggregate statistics from manifest' ,
94103 environment : { ACTION : 'aggregate' } ,
95104 } ) ;
96105
97- const generatePlotsFn = new lambda . Function ( this , 'GeneratePlotsFn' , {
98- ...commonLambdaProps ,
99- code : lambda . Code . fromInline ( '# handler in layer' ) ,
100- handler : 'imgeda.lambda_handler.handler.handler' ,
101- description : 'Generate visualization plots from manifest' ,
106+ const generatePlotsFn = new lambda . DockerImageFunction ( this , 'GeneratePlotsFn' , {
107+ code : imageCode ,
102108 memorySize : 2048 ,
103109 timeout : cdk . Duration . minutes ( 10 ) ,
110+ description : 'Generate visualization plots from manifest' ,
104111 environment : { ACTION : 'generate_plots' } ,
105112 } ) ;
106113
@@ -114,36 +121,79 @@ export class ImgedaStack extends cdk.Stack {
114121 this . outputBucket . grantReadWrite ( generatePlotsFn ) ;
115122
116123 // --- Step Functions Workflow ---
124+ //
125+ // Execution input schema:
126+ // {
127+ // "input_bucket": "<bucket-with-images>",
128+ // "prefix": "images/",
129+ // "output_bucket": "<bucket-for-results>"
130+ // }
131+ //
132+ // Each task uses `resultPath` to preserve the full state across steps,
133+ // and `payload` to construct the handler-specific input from state fields.
134+
117135 const listImagesTask = new tasks . LambdaInvoke ( this , 'ListImages' , {
118136 lambdaFunction : listImagesFn ,
119- outputPath : '$.Payload' ,
137+ payload : sfn . TaskInput . fromObject ( {
138+ 'bucket.$' : '$.input_bucket' ,
139+ 'prefix.$' : '$.prefix' ,
140+ } ) ,
141+ resultPath : '$.list_result' ,
142+ payloadResponseOnly : true ,
120143 } ) ;
121144
122145 const analyzeBatchTask = new tasks . LambdaInvoke ( this , 'AnalyzeBatch' , {
123146 lambdaFunction : analyzeBatchFn ,
124- outputPath : '$.Payload' ,
147+ payloadResponseOnly : true ,
125148 } ) ;
126149
127150 const analyzeMap = new sfn . Map ( this , 'AnalyzeBatches' , {
128- itemsPath : '$.batches' ,
151+ itemsPath : '$.list_result. batches' ,
129152 maxConcurrency : 10 ,
130153 resultPath : '$.analyze_results' ,
154+ itemSelector : {
155+ 'source_bucket.$' : '$.input_bucket' ,
156+ 'keys.$' : '$$.Map.Item.Value' ,
157+ 'output_bucket.$' : '$.output_bucket' ,
158+ 'output_key.$' :
159+ "States.Format('partials/batch-{}.jsonl', $$.Map.Item.Index)" ,
160+ } ,
131161 } ) ;
132162 analyzeMap . itemProcessor ( analyzeBatchTask ) ;
133163
134164 const mergeTask = new tasks . LambdaInvoke ( this , 'MergeManifests' , {
135165 lambdaFunction : mergeManifestsFn ,
136- outputPath : '$.Payload' ,
166+ payload : sfn . TaskInput . fromObject ( {
167+ 'bucket.$' : '$.output_bucket' ,
168+ 'analyze_results.$' : '$.analyze_results' ,
169+ 'output_key' : 'manifests/manifest.jsonl' ,
170+ 'input_dir.$' :
171+ "States.Format('s3://{}/{}', $.input_bucket, $.prefix)" ,
172+ } ) ,
173+ resultPath : '$.merge_result' ,
174+ payloadResponseOnly : true ,
137175 } ) ;
138176
139177 const aggregateTask = new tasks . LambdaInvoke ( this , 'Aggregate' , {
140178 lambdaFunction : aggregateFn ,
141- outputPath : '$.Payload' ,
179+ payload : sfn . TaskInput . fromObject ( {
180+ 'bucket.$' : '$.output_bucket' ,
181+ 'manifest_key.$' : '$.merge_result.output_key' ,
182+ 'output_key' : 'summary/summary.json' ,
183+ } ) ,
184+ resultPath : '$.aggregate_result' ,
185+ payloadResponseOnly : true ,
142186 } ) ;
143187
144188 const generatePlotsTask = new tasks . LambdaInvoke ( this , 'GeneratePlots' , {
145189 lambdaFunction : generatePlotsFn ,
146- outputPath : '$.Payload' ,
190+ payload : sfn . TaskInput . fromObject ( {
191+ 'bucket.$' : '$.output_bucket' ,
192+ 'manifest_key.$' : '$.merge_result.output_key' ,
193+ 'output_prefix' : 'plots/' ,
194+ } ) ,
195+ resultPath : '$.plots_result' ,
196+ payloadResponseOnly : true ,
147197 } ) ;
148198
149199 const definition = listImagesTask
0 commit comments