@@ -36,6 +36,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
3636 poll_memory_size = int (self .node .try_get_context ("pollMemorySize" ) or 512 )
3737 poll_timeout_seconds = int (self .node .try_get_context ("pollTimeoutSeconds" ) or 30 )
3838 poll_lookback_ms = int (self .node .try_get_context ("pollLookbackMs" ) or 3600000 )
39+ monthly_spend_limit_usd = float (self .node .try_get_context ("monthlySpendLimit" ) or 100 )
3940
4041 task_cpu = int (self .node .try_get_context ("taskCpu" ) or 4096 )
4142 task_memory_mib = int (self .node .try_get_context ("taskMemoryMiB" ) or 30720 )
@@ -243,6 +244,31 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
243244 max_attempts = 2 ,
244245 )
245246
247+ cost_check_code = lambda_ .DockerImageCode .from_image_asset (
248+ str (project_root ),
249+ file = "aws_lambda/Dockerfile" ,
250+ cmd = ["compose_runner.aws_lambda.cost_check_handler.handler" ],
251+ build_args = build_args ,
252+ )
253+
254+ cost_check_function = lambda_ .DockerImageFunction (
255+ self ,
256+ "ComposeRunnerCostCheck" ,
257+ code = cost_check_code ,
258+ memory_size = 256 ,
259+ timeout = Duration .seconds (15 ),
260+ environment = {
261+ "COST_LIMIT_USD" : str (monthly_spend_limit_usd ),
262+ },
263+ description = "Blocks executions when monthly spend exceeds the configured limit." ,
264+ )
265+ cost_check_function .add_to_role_policy (
266+ iam .PolicyStatement (
267+ actions = ["ce:GetCostAndUsage" ],
268+ resources = ["*" ],
269+ )
270+ )
271+
246272 run_output = sfn .Pass (
247273 self ,
248274 "ComposeRunnerOutput" ,
@@ -256,7 +282,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
256282 },
257283 )
258284
259- definition_chain = sfn .Choice (
285+ task_selection = sfn .Choice (
260286 self ,
261287 "SelectFargateTask" ,
262288 ).when (
@@ -266,6 +292,28 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
266292 run_task_standard .next (run_output )
267293 )
268294
295+ cost_limit_exceeded = sfn .Fail (
296+ self ,
297+ "CostLimitExceeded" ,
298+ cause = "Monthly spend limit exceeded." ,
299+ error = "CostLimitExceeded" ,
300+ )
301+
302+ enforce_cost_limit = sfn .Choice (self , "EnforceMonthlyCostLimit" ).when (
303+ sfn .Condition .boolean_equals ("$.cost_check.Payload.allowed" , False ),
304+ cost_limit_exceeded ,
305+ ).otherwise (task_selection )
306+
307+ cost_check_step = tasks .LambdaInvoke (
308+ self ,
309+ "CheckMonthlyCost" ,
310+ lambda_function = cost_check_function ,
311+ payload = sfn .TaskInput .from_object ({"stateInput.$" : "$" }),
312+ result_path = "$.cost_check" ,
313+ )
314+
315+ definition_chain = cost_check_step .next (enforce_cost_limit )
316+
269317 state_machine = sfn .StateMachine (
270318 self ,
271319 "ComposeRunnerStateMachine" ,
0 commit comments