Skip to content

Commit 30c507a

Browse files
mohamedzeidan2021Mohamed Zeidan
andauthored
fix: apply output KMS key to ModelTrainer source-code S3 uploads (#5956) (#6339)
V2 Estimator encrypted staged user code in S3 with output_kms_key; v3 ModelTrainer uploaded source code and driver files to S3 with no KMS encryption, so it could not be used in environments whose S3 policies enforce SSE-KMS. create_input_data_channel now passes S3 ExtraArgs (ServerSideEncryption=aws:kms, SSEKMSKeyId=...) on local uploads, using an explicit new kms_key argument when given and otherwise falling back to output_data_config.kms_key_id (mirroring V2, where the output KMS key also encrypted staged code). Behavior is unchanged when no KMS key is configured (extra_args stays None). Co-authored-by: Mohamed Zeidan <zeidmo@amazon.com>
1 parent 06bbcd5 commit 30c507a

2 files changed

Lines changed: 83 additions & 5 deletions

File tree

‎sagemaker-train/src/sagemaker/train/model_trainer.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,7 @@ def create_input_data_channel(
10371037
key_prefix: Optional[str] = None,
10381038
ignore_patterns: Optional[List[str]] = None,
10391039
instance_group_names: Optional[List[str]] = None,
1040+
kms_key: Optional[str] = None,
10401041
) -> Channel:
10411042
"""Create an input data channel for the training job.
10421043
@@ -1060,9 +1061,16 @@ def create_input_data_channel(
10601061
channel's data should be assigned to. Only applied when the channel is
10611062
built from a URI/local-path data source (not a caller-supplied
10621063
``S3DataSource``/``FileSystemDataSource``, which the caller controls).
1064+
kms_key (Optional[str]): The Amazon Web Services KMS key id (or ARN/alias) to use for
1065+
server-side encryption when uploading local data to S3. If not specified, the
1066+
``kms_key_id`` of this trainer's ``output_data_config`` is used (mirroring the V2
1067+
``Estimator`` behavior of encrypting staged user code with the output KMS key).
1068+
Only applied when ``data_source`` is a local file path.
10631069
"""
10641070
from sagemaker.core.helper.pipeline_variable import PipelineVariable
10651071

1072+
upload_extra_args = self._resolve_upload_extra_args(kms_key)
1073+
10661074
# Pass the field only when provided, so it stays unset (Unassigned) by default.
10671075
instance_group_kwargs = (
10681076
{"instance_group_names": instance_group_names}
@@ -1138,12 +1146,14 @@ def create_input_data_channel(
11381146
path=copied_path,
11391147
bucket=staging_bucket,
11401148
key_prefix=effective_prefix,
1149+
extra_args=upload_extra_args,
11411150
)
11421151
else:
11431152
s3_uri = self.sagemaker_session.upload_data(
11441153
path=data_source,
11451154
bucket=staging_bucket,
11461155
key_prefix=effective_prefix,
1156+
extra_args=upload_extra_args,
11471157
)
11481158
channel = Channel(
11491159
channel_name=channel_name,
@@ -1172,6 +1182,22 @@ def create_input_data_channel(
11721182
raise ValueError(f"Unsupported data_source type: {type(data_source)}")
11731183
return channel
11741184

1185+
def _resolve_upload_extra_args(self, kms_key: Optional[str] = None) -> Optional[dict]:
1186+
"""Build S3 ``ExtraArgs`` for encrypting local source/data uploads with KMS.
1187+
1188+
Uses the explicit ``kms_key`` if given, otherwise falls back to the
1189+
``kms_key_id`` on this trainer's ``output_data_config`` (V2 ``Estimator`` parity:
1190+
staged user code is encrypted with the output KMS key). Returns ``None`` when no
1191+
usable string key is configured, so behavior is unchanged by default (GH #5956).
1192+
A pipeline-variable key is ignored here because uploads happen at SDK compile time.
1193+
"""
1194+
resolved_key = kms_key
1195+
if resolved_key is None and self.output_data_config is not None:
1196+
resolved_key = getattr(self.output_data_config, "kms_key_id", None)
1197+
if isinstance(resolved_key, str) and resolved_key:
1198+
return {"ServerSideEncryption": "aws:kms", "SSEKMSKeyId": resolved_key}
1199+
return None
1200+
11751201
def _get_input_data_config(
11761202
self,
11771203
input_data_channels: Optional[List[Union[Channel, InputData]]],

‎sagemaker-train/tests/unit/train/test_model_trainer.py‎

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,58 @@ def test_create_input_data_channel(mock_default_bucket, mock_upload_data, model_
517517
assert channel.data_source.s3_data_source.s3_uri == expected_s3_uri
518518

519519

520+
@patch("sagemaker.train.model_trainer.Session.upload_data")
521+
@patch("sagemaker.train.model_trainer.Session.default_bucket")
522+
def test_create_input_data_channel_encrypts_uploads_with_output_kms_key(
523+
mock_default_bucket, mock_upload_data
524+
):
525+
"""GH #5956: local source uploads are encrypted with output_data_config.kms_key_id."""
526+
mock_default_bucket.return_value = DEFAULT_BUCKET
527+
mock_upload_data.return_value = f"s3://{DEFAULT_BUCKET}/code"
528+
trainer = ModelTrainer(
529+
training_image=DEFAULT_IMAGE,
530+
role=DEFAULT_ROLE,
531+
compute=DEFAULT_COMPUTE_CONFIG,
532+
stopping_condition=DEFAULT_STOPPING_CONDITION,
533+
output_data_config=OutputDataConfig(
534+
s3_output_path=f"s3://{DEFAULT_BUCKET}/out",
535+
kms_key_id="my-kms-key",
536+
),
537+
)
538+
trainer.create_input_data_channel("code", DEFAULT_SOURCE_DIR)
539+
assert mock_upload_data.call_args.kwargs["extra_args"] == {
540+
"ServerSideEncryption": "aws:kms",
541+
"SSEKMSKeyId": "my-kms-key",
542+
}
543+
544+
545+
@patch("sagemaker.train.model_trainer.Session.upload_data")
546+
@patch("sagemaker.train.model_trainer.Session.default_bucket")
547+
def test_create_input_data_channel_explicit_kms_key_overrides(
548+
mock_default_bucket, mock_upload_data, model_trainer
549+
):
550+
"""An explicit kms_key argument takes precedence over output_data_config."""
551+
mock_default_bucket.return_value = DEFAULT_BUCKET
552+
mock_upload_data.return_value = f"s3://{DEFAULT_BUCKET}/code"
553+
model_trainer.create_input_data_channel("code", DEFAULT_SOURCE_DIR, kms_key="explicit-key")
554+
assert mock_upload_data.call_args.kwargs["extra_args"] == {
555+
"ServerSideEncryption": "aws:kms",
556+
"SSEKMSKeyId": "explicit-key",
557+
}
558+
559+
560+
@patch("sagemaker.train.model_trainer.Session.upload_data")
561+
@patch("sagemaker.train.model_trainer.Session.default_bucket")
562+
def test_create_input_data_channel_no_kms_by_default(
563+
mock_default_bucket, mock_upload_data, model_trainer
564+
):
565+
"""Default (no kms_key_id) leaves uploads unencrypted -> extra_args None (unchanged)."""
566+
mock_default_bucket.return_value = DEFAULT_BUCKET
567+
mock_upload_data.return_value = f"s3://{DEFAULT_BUCKET}/code"
568+
model_trainer.create_input_data_channel("code", DEFAULT_SOURCE_DIR)
569+
assert mock_upload_data.call_args.kwargs["extra_args"] is None
570+
571+
520572
def test_create_input_data_channel_with_instance_group_names(model_trainer):
521573
"""instance_group_names is propagated onto the channel's S3DataSource."""
522574
channel = model_trainer.create_input_data_channel(
@@ -917,7 +969,7 @@ def test_remote_debug_config(mock_training_job, modules_session):
917969
@patch("sagemaker.train.model_trainer._get_unique_name")
918970
@patch("sagemaker.train.model_trainer.TrainingJob")
919971
def test_model_trainer_full_init(mock_training_job, mock_unique_name, modules_session):
920-
def mock_upload_data(path, bucket, key_prefix):
972+
def mock_upload_data(path, bucket, key_prefix, extra_args=None):
921973
return f"s3://{bucket}/{key_prefix}"
922974

923975
modules_session.upload_data.side_effect = mock_upload_data
@@ -1195,7 +1247,7 @@ def mock_upload_data(path, bucket, key_prefix):
11951247
# def test_model_trainer_local_full_init(
11961248
# mock_download_folder, mock_unique_name, mock_local_container, modules_session
11971249
# ):
1198-
# def mock_upload_data(path, bucket, key_prefix):
1250+
# def mock_upload_data(path, bucket, key_prefix, extra_args=None):
11991251
# return f"s3://{bucket}/{key_prefix}"
12001252

12011253
# modules_session.upload_data.side_effect = mock_upload_data
@@ -1449,7 +1501,7 @@ def test_hyperparameters_invalid(mock_exists, modules_session):
14491501
@patch("sagemaker.train.model_trainer._get_unique_name")
14501502
@patch("sagemaker.train.model_trainer.TrainingJob")
14511503
def test_model_trainer_default_paths(mock_training_job, mock_unique_name, modules_session):
1452-
def mock_upload_data(path, bucket, key_prefix):
1504+
def mock_upload_data(path, bucket, key_prefix, extra_args=None):
14531505
return f"s3://{bucket}/{key_prefix}"
14541506

14551507
unique_name = "base-job-0123456789"
@@ -1553,7 +1605,7 @@ def test_metric_definitions(mock_training_job, modules_session):
15531605
@patch("sagemaker.train.model_trainer._get_unique_name")
15541606
@patch("sagemaker.core.resources.TrainingJob")
15551607
def test_nova_recipe(mock_training_job, mock_unique_name, modules_session):
1556-
def mock_upload_data(path, bucket, key_prefix):
1608+
def mock_upload_data(path, bucket, key_prefix, extra_args=None):
15571609
if os.path.isfile(path):
15581610
file_name = os.path.basename(path)
15591611
return f"s3://{bucket}/{key_prefix}/{file_name}"
@@ -1825,7 +1877,7 @@ def test_nova_recipe_model_package_config_only_mpg_from_recipe(modules_session):
18251877
@patch("sagemaker.train.model_trainer._get_unique_name")
18261878
@patch("sagemaker.train.model_trainer.TrainingJob")
18271879
def test_llmft_recipe(mock_training_job, mock_unique_name, modules_session):
1828-
def mock_upload_data(path, bucket, key_prefix):
1880+
def mock_upload_data(path, bucket, key_prefix, extra_args=None):
18291881
if os.path.isfile(path):
18301882
file_name = os.path.basename(path)
18311883
return f"s3://{bucket}/{key_prefix}/{file_name}"

0 commit comments

Comments
 (0)