Skip to content

Latest commit

 

History

History

stack-training

README.md

stack-training

SageMaker training infrastructure for the image classification sample: data and model buckets, the SageMaker pipeline (validation, preprocessing, three trainers, evaluation, ensemble, fairness check, clinical quality gate, registration), the Model Registry, a Model Card and cost controls.

What It Does

  1. Creates a KMS key with rotation for the stack's buckets, logs and SageMaker jobs
  2. Creates 6 S3 buckets (raw data, processed data, scripts, model artifacts, inference results, monitoring), plus an SBOM bucket when enable_sbom_bucket = true (default) and a CloudTrail bucket when enable_cloudtrail = true (default false). force_destroy is false on all of them
  3. Creates the SageMaker execution role (carrying the workload permissions boundary when permissions_boundary_arn is set)
  4. Creates the Model Package Group and, with enable_model_card = true (default), a Model Card
  5. Creates the SageMaker pipeline from pipeline.json.tftpl: 9 top-level steps (ValidateDataset, PreprocessData, three training steps, EvaluateAllModels, CreateEnsembleModel, BiasCheck, and the Condition step), whose branches are RegisterEnsembleModel or Fail
  6. Creates an EventBridge rule that starts the pipeline with RetrainingReason=data_upload when the .batch_complete marker is written to the raw data bucket
  7. With enable_upload_quarantine = true (default), creates the upload quarantine Lambda (see Upload quarantine)
  8. Creates CloudWatch log groups, metric filters and a training dashboard, and an AWS Budgets budget (monthly_budget_usd, default 200)
  9. Optionally creates a managed MLflow tracking server (enable_mlflow, default false) and a multi-region CloudTrail trail (enable_cloudtrail, default false)

Upload quarantine

An EventBridge rule on the raw-data bucket's Object Created events under training_data_path invokes a Lambda once per uploaded object. EventBridge is the same event source the pipeline trigger uses: S3 allows one notification configuration per bucket, and the bucket's sends everything to EventBridge. The Lambda:

  • keeps an object that has an allowed extension (.jpg, .jpeg, .png, the extensions validation and preprocessing read), opens with Pillow as the format that extension names (verified and fully decoded, so truncated files fail), and is at least quarantine_min_image_size_px (112, the validation step's hard minimum) on both sides;
  • otherwise copies it to <quarantine_prefix><original key> (default quarantine/), writes <quarantine_prefix><original key>.reason.json with the reason, ETag, size and time, deletes the original, and adds 1 to the QuarantinedImages metric (namespace <project_name>/DataQuality, dimension Bucket);
  • ignores the .batch_complete marker, keys outside training_data_path and anything already under quarantine_prefix.

quarantine_prefix must sit outside training_data_path (a variable validation checks it), so neither the pipeline trigger nor the validation and preprocessing inputs read quarantined files. The bucket is versioned, so the deleted original stays recoverable as a noncurrent version.

The function uses the terraform-aws-lambda module: KMS-encrypted environment and log group (log_retention_days), active X-Ray tracing, an SQS dead-letter queue encrypted with the project key plus a queue-depth alarm, reserved concurrency (upload_quarantine_reserved_concurrency, 10) and the workload permissions boundary. Its role can read and delete only under training_data_path, write only under quarantine_prefix, and publish metrics only to its namespace. Pillow comes from a layer built from the same zip and script as the inference Lambda's layer (ops-scripts/build_lambda_layer.sh, Pillow 12.3.0); make layer builds it, and the apply builds it when missing.

The checks run as files arrive, while the marker starts the pipeline as soon as the upload script writes it, so a large upload can start the pipeline before the Lambda has reached every file. The validation step checks every image again and fails the run on a bad one, so nothing unchecked reaches training. For a clean first run, wait for the Lambda to catch up (its CloudWatch Invocations metric flattens) before writing the marker, or rerun the pipeline.

Pipeline behaviour

  • Patient-grouped split. Preprocessing resizes images to preprocessing_target_size (512) and splits train, validation and test by BreakHis patient id, so one patient's images never cross splits.
  • Thresholds tuned on validation. Evaluation and the ensemble tune their decision thresholds on the validation split; the clinical gate is scored on the test split at that threshold.
  • Fairness gate on the ensemble. BiasCheck runs Fairlearn over the ensemble's own test predictions at its tuned threshold, per fairness_gate.sensitive_feature (magnification). Fewer than two subgroups is "not evaluable" and fails the gate unless allow_not_evaluable = true (default false; meant for small demo datasets).
  • Gate. The Condition step requires clinical_quality_gate (accuracy 0.85, recall 0.95, precision 0.80, AUC 0.90) and fairness_gate.max_disparity (0.10). A pass registers the ensemble as PendingManualApproval; a miss ends in the Fail step.
  • Per-execution artifacts. Evaluation, ensemble and bias outputs go to <prefix>/<pipeline-execution-id>/ in the model artifacts bucket, and the registered package's ModelDataUrl points at that execution's model.tar.gz.
  • Lineage. RetrainingReason, DatasetVersion and CodeCommitSha are pipeline parameters recorded on the model package. The Terraform variables only set their defaults; the upload rule passes data_upload and the drift rule in stack-inference passes drift_detected.
  • Network isolation. enable_network_isolation = true (default) runs the training jobs without outbound network access; they read ImageNet weights from s3://<scripts-bucket>/pretrained-weights/. Processing jobs are not isolated because they install packages at start-up. vpc_config optionally places the jobs in your subnets.
  • Instance types. The shipped terraform.tfvars trains on ml.c5.2xlarge (CPU) with 2 epochs, sized for a smoke test. Instance types starting ml.g or ml.p use the GPU training image; they need a SageMaker training quota.

Prerequisites

  1. stack-backend-setup applied and backend.hcl written (make backend-config)
  2. Terraform ~> 1.15 and AWS CLI v2
  3. After apply: the scripts uploaded (make upload-scripts), the ImageNet weights uploaded when network isolation is on, and training data uploaded with scripts/data_uploader.sh
  4. training_data_path set to the prefix the uploader writes to (medical_image_data/, the shipped value), and auto_trigger_marker_key to the marker it writes (medical_image_data/.batch_complete)

Usage

From the repository root:

make deploy-training     # terraform init -backend-config=backend.hcl, plan -out, apply
make upload-scripts      # pipeline scripts to the scripts bucket

# Network isolation (default): upload ImageNet weights once (no-op when isolation is off)
make weights

# Upload a dataset (breast_benign/ and breast_malignant/ folders); the marker starts the pipeline
./scripts/data_uploader.sh data/breakhis

make deploy-training passes state_bucket_name, state_bucket_region and permissions_boundary_arn from the bootstrap outputs as TF_VAR_*.

Requirements

Name Version
terraform ~> 1.15
archive ~> 2.7
aws ~> 6.0
random ~> 3.8

Providers

Name Version
aws ~> 6.0
random ~> 3.8
terraform n/a

Resources

Name Type
aws_budgets_budget.monthly resource
aws_cloudtrail.audit resource
aws_cloudwatch_event_rule.new_data_uploaded resource
aws_cloudwatch_event_rule.upload_quarantine resource
aws_cloudwatch_event_target.pipeline_trigger resource
aws_cloudwatch_event_target.upload_quarantine resource
aws_cloudwatch_log_group.cloudtrail resource
aws_cloudwatch_metric_alarm.upload_quarantine_dlq resource
aws_iam_role.cloudtrail_cwl resource
aws_iam_role.eventbridge_sagemaker_role resource
aws_iam_role_policy.cloudtrail_cwl resource
aws_iam_role_policy.eventbridge_sagemaker_policy resource
aws_lambda_layer_version.quarantine_pillow resource
aws_s3_bucket_policy.cloudtrail resource
aws_sagemaker_mlflow_tracking_server.mlflow resource
aws_sagemaker_model_card.medical_image resource
aws_sagemaker_model_package_group.medical_image_models resource
aws_sagemaker_pipeline.medical_image_pipeline resource
aws_sns_topic.cloudtrail_notifications resource
aws_sns_topic_policy.cloudtrail_notifications resource
aws_sqs_queue.upload_quarantine_dlq resource
random_id.bucket_suffix resource
random_id.cloudtrail_suffix resource
random_id.sbom_suffix resource
terraform_data.quarantine_layer_build resource

Inputs

Name Description Type Default Required
aws_region AWS region string n/a yes
environment Environment name string n/a yes
models Configuration for all training models
map(object({
script_name = string
tar_file = string
step_name = string
hyperparameters = map(string)
volume_size = number
instance_count = number
max_runtime = number
instance_type = string
enable_cache = bool
cache_expiry = string
}))
n/a yes
pipeline_steps Configuration for pipeline processing steps
map(object({
instance_type = string
instance_count = number
volume_size = number
script_name = string
step_name = string
step_type = string
enable_cache = bool
cache_expiry = string
}))
n/a yes
project_name Name of the project, used as the prefix of every resource name. Lowercase letters, digits and hyphens. Together with environment it is capped so the longest derived name (the inference-results bucket) stays within the 63-character S3 limit. string n/a yes
allow_not_evaluable Let the fairness gate pass when the test split has fewer than two subgroups of fairness_gate.sensitive_feature, so disparity cannot be measured (status not_evaluable). Default false fails the pipeline instead; set true only for small demo datasets. bool false no
auto_trigger_marker_key Object key prefix in the raw-data bucket whose creation starts the pipeline. scripts/data_uploader.sh writes this marker after a batch upload completes. string "medical_image_data/.batch_complete" no
bucket_defaults Default configuration for S3 buckets
object({
force_destroy = bool
enable_versioning = bool
block_public_access = bool
})
{
"block_public_access": true,
"enable_versioning": true,
"force_destroy": false
}
no
budget_alert_emails Email addresses that receive AWS Budgets alerts at 80% actual and 100% forecasted thresholds. list(string) [] no
budget_start_date Start of the AWS Budgets period, in the format YYYY-MM-DD_HH:MM (UTC). string "2026-01-01_00:00" no
clinical_quality_gate Clinical quality gate the ensemble must clear before it can be registered. Recall is highest because a missed malignant case (false negative) is the costly error. The evaluation and ensemble steps receive the same values (CLINICAL_QUALITY_GATE), so the threshold search targets this recall.
object({
accuracy = number
recall = number
precision = number
auc_roc = number
})
{
"accuracy": 0.85,
"auc_roc": 0.9,
"precision": 0.8,
"recall": 0.95
}
no
cloudtrail_retention_days Days to retain CloudTrail logs in S3 before lifecycle deletion. HIPAA requires 6 years of audit retention - typically achieved by shipping to a SIEM, not keeping everything in S3. number 400 no
code_commit_sha Default of the CodeCommitSha pipeline parameter: the Git commit that produced the model, recorded on the model package for audit lineage. Override per execution from CI. string "local" no
dashboard_config Dashboard configuration JSON string "" no
dashboard_name CloudWatch dashboard name string null no
data_config Data processing configuration
object({
content_type = string
compression_type = string
s3_data_type = string
s3_input_mode = string
s3_upload_mode = string
})
{
"compression_type": "None",
"content_type": "application/x-image",
"s3_data_type": "S3Prefix",
"s3_input_mode": "File",
"s3_upload_mode": "EndOfJob"
}
no
dataset_version Default of the DatasetVersion pipeline parameter (for example a DVC hash such as dataset-v2.3-sha256:...), recorded on the registered model package for audit lineage. Override per execution from CI. string "unversioned" no
debugger_rule_image Region-specific SageMaker Debugger rule-evaluator image. us-east-1 default; see https://docs.aws.amazon.com/sagemaker/latest/dg/debugger-docker-images-rules.html string "503895931360.dkr.ecr.us-east-1.amazonaws.com/sagemaker-debugger-rules:latest" no
enable_auto_trigger Enable auto-trigger of pipeline on new data upload bool true no
enable_cloudtrail Create a multi-region CloudTrail trail with log-file integrity validation for this account. Leave false when an organization trail already records this account's API activity. bool false no
enable_cloudtrail_sns Attach an SNS delivery-notification topic to the CloudTrail. Off by default. The topic is encrypted with the project CMK, whose key policy then grants the CloudTrail service principal (CloudTrail cannot publish to a topic on the AWS-managed alias/aws/sns key). The trail and S3/CloudWatch delivery work without it. bool false no
enable_debugger Attach SageMaker Debugger built-in rules (Overfit, LossNotDecreasing) to the training steps (Part 2). Off by default; the CloudWatch training metrics (and MLflow when enable_mlflow = true) cover the same need. bool false no
enable_experiments Associate each training step with a SageMaker Experiment trial component via ExperimentConfig (Part 2). bool true no
enable_kms_key_rotation Enable KMS key rotation bool true no
enable_managed_spot_training Use EC2 Spot for all training steps. Requires checkpoint support in trainers. bool true no
enable_mlflow Create a managed SageMaker MLflow tracking server (Small) for experiment tracking. Off by default because it is billed for every hour it runs. bool false no
enable_model_card Create a SageMaker Model Card documenting intended use, risk rating, and clinical context for the model package group (Part 2/4). bool true no
enable_network_isolation Run the training jobs with network isolation (no outbound network access from the training container). The trainers then read ImageNet weights from s3:///pretrained-weights/, so run scripts/download_pretrained_weights.py once before the first pipeline execution. Processing jobs are not isolated because they install Python dependencies at start-up. bool true no
enable_sbom_bucket Create a dedicated S3 bucket to receive Syft-generated CycloneDX SBOMs from the patched-image CodeBuild. Low monthly cost; safe to leave on even if you don't wire the patched image yet. bool true no
enable_training_monitoring Enable training monitoring bool true no
enable_upload_quarantine Check every object uploaded under training_data_path in the raw-data bucket with a Lambda (allowed extension, opens with Pillow as that format, minimum size) and move failing files to quarantine_prefix with a .reason.json next to each. The pipeline validation step still checks every image. bool true no
fairness_gate Fairness gate the ensemble must clear before registration (Part 4). max_disparity bounds the larger of demographic-parity difference and equalized-odds difference, computed by scripts/bias/compute_bias.py with Fairlearn. Must mirror DEFAULT_THRESHOLD in that script. sensitive_feature is reported in the bias report; the public datasets used here carry no demographic metadata, so magnification is an honest subgroup proxy - supply a real attribute for clinical use.
object({
max_disparity = number
sensitive_feature = string
})
{
"max_disparity": 0.1,
"sensitive_feature": "magnification"
}
no
kms_deletion_window_days KMS key deletion window (days) number 7 no
log_group_names Log group names for metric filters map(string) {} no
log_groups Map of log groups to create
map(object({
name = string
}))
{} no
log_retention_days CloudWatch log retention days number 30 no
metric_namespaces Metric namespaces for CloudWatch metrics map(string) {} no
min_images_per_class Minimum images required per class (benign/malignant) for the validation step to pass. Production default is 100; lower it for small test datasets. number 100 no
model_card_risk_rating Risk rating recorded on the Model Card. High for clinical decision-support models. string "High" no
monthly_budget_usd Monthly spending budget in USD (project-scoped via tag filter). Set to 0 to disable the AWS Budgets resource entirely. number 200 no
permissions_boundary_arn ARN of the permissions boundary attached to every IAM role this stack creates (stack-backend-setup output workload_boundary_arn). Required when CI/CD CodeBuild applies the stack; null leaves the roles unbounded. string null no
pipeline_max_parallel_steps Max concurrent steps a pipeline execution can run. Lets the three model trainers run in parallel. number 4 no
preprocessing_target_size Target square resolution (pixels) the preprocessing job resizes every image to before training. 512 preserves fine diagnostic features like microcalcifications; the trainers downsample to their own input_size from there. number 512 no
quarantine_min_image_size_px Uploads whose width or height is below this many pixels are quarantined. Matches the validation step's MIN_RESOLUTION default (112). number 112 no
quarantine_prefix Key prefix in the raw-data bucket that failing uploads are moved to. Must end with / and sit outside training_data_path so neither the pipeline trigger nor the preprocessing input reads it. string "quarantine/" no
retraining_reason Default of the RetrainingReason pipeline parameter (manual, data_upload, drift_detected), recorded on the registered model package. The upload trigger passes data_upload and the drift alarm passes drift_detected per execution. string "manual" no
sagemaker_images SageMaker container image configurations
object({
tensorflow_gpu_tag = string
tensorflow_cpu_tag = string
tensorflow_inference_tag = string
})
{
"tensorflow_cpu_tag": "2.19.0-cpu-py312-ubuntu22.04-sagemaker",
"tensorflow_gpu_tag": "2.19.0-gpu-py312-cu125-ubuntu22.04-sagemaker",
"tensorflow_inference_tag": "2.19.0-cpu-py312-ubuntu22.04-sagemaker"
}
no
sbom_retention_days How long SBOM JSON files are kept before S3 expires them. number 730 no
spot_max_wait_buffer_seconds Extra wait time (seconds) SageMaker holds for Spot capacity on top of MaxRuntime number 1800 no
training_data_path Training data S3 path string "medical_image_data/" no
training_input_mode SageMaker training input mode string "FastFile" no
training_keep_alive_seconds KeepAlivePeriodInSeconds for warm pools. Set to 0 to disable. number 1800 no
upload_quarantine_reserved_concurrency Reserved concurrency for the upload quarantine Lambda. A full dataset upload sends one event per image; the cap keeps it from using the account's whole Lambda concurrency, and throttled events are retried by Lambda's asynchronous queue. number 10 no
vpc_config Optional VPC for the training and processing jobs. Null runs them in the SageMaker service network. The subnets need a route to Amazon S3 (gateway endpoint), SageMaker API, CloudWatch Logs and ECR (interface endpoints or NAT); processing jobs also pip install packages, which needs a route to PyPI or a mirror.
object({
subnet_ids = list(string)
security_group_ids = list(string)
})
null no

Outputs

Name Description
auto_trigger_eventbridge_rule Name of the EventBridge rule for auto-triggering pipeline
endpoint_name Name of the SageMaker endpoint
inference_results_bucket Name of the inference results S3 bucket
kms_key_arn ARN of the KMS key
kms_key_id ID of the KMS key
mlflow_tracking_server_arn ARN of the MLflow tracking server. Null when enable_mlflow = false.
mlflow_tracking_server_url URL of the MLflow tracking server. Null when enable_mlflow = false.
model_artifacts_bucket Name of the model artifacts S3 bucket
model_package_group_name Name of the model package group
monitoring_bucket Name of the monitoring S3 bucket
network_isolation_enabled Whether the training jobs run with network isolation. When true, the ImageNet weights must be in s3:///pretrained-weights/ (make weights).
pipeline_log_groups Pipeline step log group names
processed_data_bucket Name of the processed data S3 bucket
raw_data_bucket Name of the raw data S3 bucket
sagemaker_execution_role_arn ARN of the SageMaker execution role
sagemaker_image_uris SageMaker Docker image URIs fetched dynamically from AWS
sagemaker_pipeline_arn ARN of the SageMaker pipeline
sagemaker_pipeline_name Name of the SageMaker pipeline
sbom_bucket Name of the SBOM S3 bucket. Null when enable_sbom_bucket = false.
sbom_bucket_arn ARN of the SBOM S3 bucket. Null when enable_sbom_bucket = false.
scripts_bucket Name of the scripts S3 bucket
training_dashboard_url Training monitoring dashboard URL
training_log_groups Training metrics log group names
upload_quarantine_function_name Lambda that checks each uploaded training image (null when enable_upload_quarantine = false)
upload_quarantine_location Where failing uploads are moved, with a .reason.json next to each (null when enable_upload_quarantine = false)

File Structure

stack-training/
|-- auto_trigger.tf           # EventBridge rule, target and role that start the pipeline on upload
|-- backend.tf                # Partial S3 backend (values come from backend.hcl)
|-- backend.hcl.example       # Template for backend.hcl
|-- cloudtrail_budgets.tf     # Optional CloudTrail trail and bucket, AWS Budgets budget
|-- data.tf                   # Caller identity and SageMaker prebuilt image lookups
|-- lambda/upload_quarantine/ # Upload quarantine Lambda source
|-- main.tf                   # KMS, S3 buckets, execution role, registry, Model Card, MLflow, pipeline
|-- monitor.tf                # CloudWatch log groups, metric filters and training dashboard
|-- outputs.tf                # Buckets, pipeline, registry, IAM, KMS, MLflow, SBOM outputs
|-- pipeline.json.tftpl       # SageMaker pipeline definition (rendered by templatefile)
|-- providers.tf              # AWS provider with default_tags
|-- quarantine.tf             # Upload quarantine Lambda, rule, role, layer and DLQ
|-- sbom.tf                   # SBOM bucket for the patched inference image builds
|-- terraform.tfvars          # Values for this environment
|-- terraform.tfvars.example  # Annotated example values
|-- variables.tf              # Input variables
`-- versions.tf               # Terraform and provider version constraints