Skip to content

Commit 066b50b

Browse files
authored
feat: trigger a one-time baseline generation upon deployment (#393)
* feat: trigger a one-time baseline generation upon deployment * chore: update unit tests
1 parent 2341f2f commit 066b50b

4 files changed

Lines changed: 101 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### **Changed**
1313

14+
- `sagemaker-model-monitoring` now triggers a one-time baseline generation upon deployment
15+
- update seedfarmer version to 8.0.0
1416
- fixed duplicate principal error in `sagemaker-templates` module when dev, pre-prod, and prod account IDs resolve to the same AWS account
1517
- fixed `sagemaker-templates` Model Deploy seed code incorrectly granting S3 permissions to a ManagedPolicy instead of a Role, which caused deployment failures with CDK 2.174.0+
1618
- update qs to 6.14.1 via npm override to address security vulnerability
1719
- pin @cdklabs/generative-ai-cdk-constructs to 0.1.311 to fix build compatibility
1820
- update starlette to 0.50.0 and fastapi to 0.128.0 to address security vulnerabilities
1921

22+
2023
## v3.1.0
2124

2225
### **Added**

modules/sagemaker/sagemaker-model-monitoring/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ When you provide training data, the module will:
5858
3. Generate baseline statistics and constraints files
5959
4. Store the baseline artifacts in your specified S3 location
6060
5. Schedule automatic baseline regeneration (default: daily at 2 AM UTC)
61+
6. Trigger a one-time baseline generation immediately upon deployment
6162

6263
## Inputs/Outputs
6364

modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/baselining_construct.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import json
12
from typing import Any, List
23

3-
from aws_cdk import Duration
4+
from aws_cdk import CustomResource, Duration
45
from aws_cdk import aws_events as events
56
from aws_cdk import aws_events_targets as targets
67
from aws_cdk import aws_iam as iam
@@ -167,6 +168,100 @@ def __init__(
167168
)
168169
)
169170

171+
# Trigger baseline immediately on deployment (fire-and-forget)
172+
trigger_lambda = lambda_.Function(
173+
self,
174+
"TriggerBaselineLambda",
175+
runtime=lambda_.Runtime.PYTHON_3_13,
176+
handler="index.handler",
177+
code=lambda_.Code.from_inline("""
178+
import boto3
179+
import json
180+
import urllib3
181+
import logging
182+
183+
logger = logging.getLogger()
184+
logger.setLevel(logging.INFO)
185+
186+
sfn = boto3.client('stepfunctions')
187+
http = urllib3.PoolManager()
188+
189+
def send_response(event, context, status, reason=None):
190+
response_body = {
191+
'Status': status,
192+
'Reason': reason or f'See CloudWatch Log Stream: {context.log_stream_name}',
193+
'PhysicalResourceId': 'baseline-trigger',
194+
'StackId': event['StackId'],
195+
'RequestId': event['RequestId'],
196+
'LogicalResourceId': event['LogicalResourceId'],
197+
}
198+
199+
json_response = json.dumps(response_body)
200+
headers = {'content-type': '', 'content-length': str(len(json_response))}
201+
202+
try:
203+
http.request('PUT', event['ResponseURL'], body=json_response, headers=headers)
204+
except Exception as e:
205+
logger.error(f"Failed to send response: {e}")
206+
207+
def handler(event, context):
208+
try:
209+
logger.info(f"Event: {json.dumps(event)}")
210+
request_type = event['RequestType']
211+
212+
if request_type in ['Create', 'Update']:
213+
state_machine_arn = event['ResourceProperties']['StateMachineArn']
214+
monitor_types = json.loads(event['ResourceProperties']['MonitorTypes'])
215+
216+
for monitor_type in monitor_types:
217+
sfn.start_execution(
218+
stateMachineArn=state_machine_arn,
219+
input=json.dumps({'monitor_type': monitor_type})
220+
)
221+
222+
send_response(event, context, 'SUCCESS', 'Baseline generation triggered')
223+
elif request_type == 'Delete':
224+
send_response(event, context, 'SUCCESS', 'Nothing to delete')
225+
else:
226+
send_response(event, context, 'SUCCESS', f'No action for {request_type}')
227+
228+
except Exception as e:
229+
logger.error(f"Error: {e}")
230+
send_response(event, context, 'FAILED', str(e))
231+
"""),
232+
timeout=Duration.seconds(30),
233+
)
234+
235+
trigger_lambda.add_to_role_policy(
236+
iam.PolicyStatement(
237+
actions=["states:StartExecution"],
238+
resources=[state_machine.state_machine_arn],
239+
)
240+
)
241+
242+
CustomResource(
243+
self,
244+
"TriggerBaselineResource",
245+
service_token=trigger_lambda.function_arn,
246+
properties={
247+
"StateMachineArn": state_machine.state_machine_arn,
248+
"MonitorTypes": json.dumps(enabled_monitors),
249+
},
250+
)
251+
252+
# Add CDK-nag suppressions
253+
if trigger_lambda.role:
254+
NagSuppressions.add_resource_suppressions(
255+
trigger_lambda.role,
256+
[
257+
{
258+
"id": "AwsSolutions-IAM4",
259+
"reason": "Lambda function uses AWS managed policy for basic execution role",
260+
},
261+
],
262+
apply_to_children=True,
263+
)
264+
170265
# Add CDK-nag suppressions
171266
if baselining_lambda.role:
172267
NagSuppressions.add_resource_suppressions(

modules/sagemaker/sagemaker-model-monitoring/tests/test_stack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def test_baseline_generation_resources(stack_defaults: None) -> None:
102102
template = Template.from_stack(stack)
103103

104104
# Check for Lambda function
105-
template.resource_count_is("AWS::Lambda::Function", 1)
105+
template.resource_count_is("AWS::Lambda::Function", 2)
106106

107107
# Check for Step Functions state machine
108108
template.resource_count_is("AWS::StepFunctions::StateMachine", 1)

0 commit comments

Comments
 (0)