Skip to content

Commit 7f94b36

Browse files
mohamedzeidan2021Mohamed Zeidan
andauthored
fix: set VolumeKmsKeyId on ModelStep runtime repack job (#6537)
ModelBuilder.build() defers source-code repacking to a _RepackModelStep when model data is a pipeline variable. That step built its Compute with only an instance type, so repack_model_step_settings could not set the compute-volume KMS key: unknown settings were forwarded to ModelTrainer rather than the job's Compute. As a result the runtime repack training job could not encrypt its storage volume with a customer-managed key, and the generated request had no ResourceConfig.VolumeKmsKeyId. Thread volume_kms_key_id from repack_model_step_settings into the repack job's Compute (mirroring how instance_type is handled), and default it from the model's KMS key using setdefault so an explicit override wins (mirroring the existing output_data_config logic). Add unit tests covering an explicit key, an omitted key, a user override, and a pipeline-variable key, at both the ModelStep and _RepackModelStep layers. Fixes #6536 Co-authored-by: Mohamed Zeidan <zeidmo@amazon.com>
1 parent 081f57a commit 7f94b36

4 files changed

Lines changed: 202 additions & 1 deletion

File tree

‎sagemaker-mlops/src/sagemaker/mlops/workflow/_utils.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ def __init__(
173173
job will be created without VPC config (default: None).
174174
security_group_ids (list[str]): List of security group ids. If not
175175
specified, the re-packing job will be created without VPC config (default: None).
176-
**kwargs: additional arguments for the repacking job.
176+
**kwargs: additional arguments for the repacking job. ``volume_kms_key_id``
177+
is consumed here and applied to the repack job's ``Compute`` so the
178+
training volume is encrypted with the given KMS key; any remaining
179+
keys are forwarded to the underlying ``ModelTrainer``.
177180
"""
178181
self._model_data = model_data
179182
self.sagemaker_session = sagemaker_session
@@ -207,6 +210,7 @@ def __init__(
207210

208211
compute = Compute(
209212
instance_type=kwargs.pop("instance_type", None) or INSTANCE_TYPE,
213+
volume_kms_key_id=kwargs.pop("volume_kms_key_id", None),
210214
)
211215

212216
networking = None

‎sagemaker-mlops/src/sagemaker/mlops/workflow/model_step.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,15 @@ def _append_repack_model_step(self):
291291
kms_key_id=fields["model_kms_key"],
292292
),
293293
)
294+
# Encrypt the repack job's compute volume with the same customer-managed
295+
# key as the model. This reaches _RepackModelStep via **kwargs, which
296+
# applies it to the job's Compute (ResourceConfig.VolumeKmsKeyId). Without
297+
# it the runtime repack cannot use a configured CMK for its volume (GH
298+
# #6536). setdefault so an explicit repack_model_step_settings override wins.
299+
if fields["model_kms_key"]:
300+
self._repack_model_step_settings.setdefault(
301+
"volume_kms_key_id", fields["model_kms_key"]
302+
)
294303
repack_model_step = _RepackModelStep(
295304
name="{}-{}-{}".format(self.name, _REPACK_MODEL_NAME_BASE, name_base),
296305
sagemaker_session=(

‎sagemaker-mlops/tests/unit/workflow/test_model_step.py‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,137 @@ def test_repack_step_output_config_respects_user_override():
161161
assert kwargs["output_data_config"] is mine
162162

163163

164+
def test_repack_step_gets_volume_kms_key_from_model():
165+
"""GH #6536: the repack job's compute volume must be encrypted with the model's KMS
166+
key. This reaches _RepackModelStep via volume_kms_key_id, which applies it to the
167+
job's Compute (ResourceConfig.VolumeKmsKeyId). Without it the runtime repack cannot
168+
use a configured CMK for its volume."""
169+
from sagemaker.serve.model_builder import ModelBuilder
170+
from sagemaker.mlops.workflow import model_step as ms
171+
172+
ps = _pipeline_session()
173+
builder = Mock(spec=ModelBuilder)
174+
builder.sagemaker_session = ps
175+
builder.model_name = "my-model"
176+
builder.role_arn = "arn:aws:iam::111122223333:role/R"
177+
builder.s3_model_data_url = "s3://orig/model.tar.gz"
178+
builder.entry_point = "inference.py"
179+
builder.source_dir = "/code"
180+
builder.source_code = Mock(requirements="requirements.txt")
181+
builder.vpc_config = None
182+
builder.model_kms_key = "arn:aws:kms:us-west-2:111122223333:key/abc"
183+
184+
step_args = _FakeModelStepArgs(builder, {id(builder)})
185+
fake_repack = Mock()
186+
fake_repack.properties.ModelArtifacts.S3ModelArtifacts = "s3://repacked/model.tar.gz"
187+
188+
with patch("sagemaker.core.workflow.utilities.validate_step_args_input"):
189+
with patch.object(ms, "_RepackModelStep", return_value=fake_repack) as mock_repack:
190+
ms.ModelStep(name="step", step_args=step_args)
191+
192+
_, kwargs = mock_repack.call_args
193+
assert kwargs["volume_kms_key_id"] == "arn:aws:kms:us-west-2:111122223333:key/abc"
194+
195+
196+
def test_repack_step_omits_volume_kms_key_when_model_has_none():
197+
"""With no model KMS key, volume_kms_key_id must not be sent, leaving the repack job's
198+
ResourceConfig.VolumeKmsKeyId unset (legacy behavior preserved)."""
199+
from sagemaker.serve.model_builder import ModelBuilder
200+
from sagemaker.mlops.workflow import model_step as ms
201+
202+
ps = _pipeline_session()
203+
builder = Mock(spec=ModelBuilder)
204+
builder.sagemaker_session = ps
205+
builder.model_name = "my-model"
206+
builder.role_arn = "arn:aws:iam::111122223333:role/R"
207+
builder.s3_model_data_url = "s3://orig/model.tar.gz"
208+
builder.entry_point = "inference.py"
209+
builder.source_dir = "/code"
210+
builder.source_code = Mock(requirements="requirements.txt")
211+
builder.vpc_config = None
212+
builder.model_kms_key = None
213+
214+
step_args = _FakeModelStepArgs(builder, {id(builder)})
215+
fake_repack = Mock()
216+
fake_repack.properties.ModelArtifacts.S3ModelArtifacts = "s3://repacked/model.tar.gz"
217+
218+
with patch("sagemaker.core.workflow.utilities.validate_step_args_input"):
219+
with patch.object(ms, "_RepackModelStep", return_value=fake_repack) as mock_repack:
220+
ms.ModelStep(name="step", step_args=step_args)
221+
222+
_, kwargs = mock_repack.call_args
223+
assert "volume_kms_key_id" not in kwargs
224+
225+
226+
def test_repack_step_volume_kms_key_respects_user_override():
227+
"""An explicit volume_kms_key_id in repack_model_step_settings must win over the model
228+
KMS key default."""
229+
from sagemaker.serve.model_builder import ModelBuilder
230+
from sagemaker.mlops.workflow import model_step as ms
231+
232+
ps = _pipeline_session()
233+
builder = Mock(spec=ModelBuilder)
234+
builder.sagemaker_session = ps
235+
builder.model_name = "my-model"
236+
builder.role_arn = "arn:aws:iam::111122223333:role/R"
237+
builder.s3_model_data_url = "s3://orig/model.tar.gz"
238+
builder.entry_point = "inference.py"
239+
builder.source_dir = "/code"
240+
builder.source_code = Mock(requirements="requirements.txt")
241+
builder.vpc_config = None
242+
builder.model_kms_key = "arn:aws:kms:us-west-2:111122223333:key/abc"
243+
244+
step_args = _FakeModelStepArgs(builder, {id(builder)})
245+
fake_repack = Mock()
246+
fake_repack.properties.ModelArtifacts.S3ModelArtifacts = "s3://repacked/model.tar.gz"
247+
248+
with patch("sagemaker.core.workflow.utilities.validate_step_args_input"):
249+
with patch.object(ms, "_RepackModelStep", return_value=fake_repack) as mock_repack:
250+
ms.ModelStep(
251+
name="step",
252+
step_args=step_args,
253+
repack_model_step_settings={"volume_kms_key_id": "my-volume-key"},
254+
)
255+
256+
_, kwargs = mock_repack.call_args
257+
assert kwargs["volume_kms_key_id"] == "my-volume-key"
258+
259+
260+
def test_repack_step_volume_kms_key_accepts_pipeline_variable():
261+
"""A pipeline variable supplied as volume_kms_key_id must be forwarded unchanged."""
262+
from sagemaker.serve.model_builder import ModelBuilder
263+
from sagemaker.core.workflow import ParameterString
264+
from sagemaker.mlops.workflow import model_step as ms
265+
266+
ps = _pipeline_session()
267+
builder = Mock(spec=ModelBuilder)
268+
builder.sagemaker_session = ps
269+
builder.model_name = "my-model"
270+
builder.role_arn = "arn:aws:iam::111122223333:role/R"
271+
builder.s3_model_data_url = "s3://orig/model.tar.gz"
272+
builder.entry_point = "inference.py"
273+
builder.source_dir = "/code"
274+
builder.source_code = Mock(requirements="requirements.txt")
275+
builder.vpc_config = None
276+
builder.model_kms_key = None
277+
278+
volume_key = ParameterString(name="volume-kms")
279+
step_args = _FakeModelStepArgs(builder, {id(builder)})
280+
fake_repack = Mock()
281+
fake_repack.properties.ModelArtifacts.S3ModelArtifacts = "s3://repacked/model.tar.gz"
282+
283+
with patch("sagemaker.core.workflow.utilities.validate_step_args_input"):
284+
with patch.object(ms, "_RepackModelStep", return_value=fake_repack) as mock_repack:
285+
ms.ModelStep(
286+
name="step",
287+
step_args=step_args,
288+
repack_model_step_settings={"volume_kms_key_id": volume_key},
289+
)
290+
291+
_, kwargs = mock_repack.call_args
292+
assert kwargs["volume_kms_key_id"] is volume_key
293+
294+
164295
def test_no_repack_step_for_unrecognized_model_type():
165296
"""An object that is neither a core Model nor a ModelBuilder yields no repack step."""
166297
from sagemaker.mlops.workflow import model_step as ms

‎sagemaker-mlops/tests/unit/workflow/test_utils.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,63 @@ def test_init_with_custom_instance_type(self, mock_session, temp_entry_point):
207207
call_kwargs = mock_retrieve.call_args[1]
208208
assert call_kwargs["instance_type"] == "ml.p3.2xlarge"
209209

210+
def test_init_threads_volume_kms_key_id_to_compute(self, mock_session, temp_entry_point):
211+
"""GH #6536: volume_kms_key_id must be applied to the repack job's Compute
212+
(ResourceConfig.VolumeKmsKeyId) and consumed here rather than forwarded to
213+
ModelTrainer."""
214+
with patch("sagemaker.mlops.workflow._utils.image_uris.retrieve") as mock_retrieve:
215+
with patch("sagemaker.train.ModelTrainer") as mock_trainer:
216+
with patch("sagemaker.mlops.workflow._utils.TrainingStep.__init__") as mock_super:
217+
mock_retrieve.return_value = "sklearn-image:latest"
218+
mock_trainer_instance = Mock()
219+
mock_trainer_instance.train = Mock(return_value=Mock())
220+
mock_trainer.return_value = mock_trainer_instance
221+
mock_super.return_value = None
222+
223+
_RepackModelStep(
224+
name="repack-step",
225+
sagemaker_session=mock_session,
226+
role="arn:aws:iam::123456789012:role/SageMakerRole",
227+
model_data="s3://bucket/model.tar.gz",
228+
entry_point=temp_entry_point,
229+
volume_kms_key_id="arn:aws:kms:us-west-2:111122223333:key/abc",
230+
)
231+
232+
mock_trainer.assert_called_once()
233+
call_kwargs = mock_trainer.call_args[1]
234+
assert (
235+
call_kwargs["compute"].volume_kms_key_id
236+
== "arn:aws:kms:us-west-2:111122223333:key/abc"
237+
)
238+
# Consumed by Compute, not forwarded to ModelTrainer as a stray kwarg.
239+
assert "volume_kms_key_id" not in call_kwargs
240+
241+
def test_init_without_volume_kms_key_id_leaves_compute_unset(
242+
self, mock_session, temp_entry_point
243+
):
244+
"""Omitting volume_kms_key_id must leave Compute.volume_kms_key_id unset so the
245+
repack job's ResourceConfig carries no VolumeKmsKeyId (legacy behavior)."""
246+
with patch("sagemaker.mlops.workflow._utils.image_uris.retrieve") as mock_retrieve:
247+
with patch("sagemaker.train.ModelTrainer") as mock_trainer:
248+
with patch("sagemaker.mlops.workflow._utils.TrainingStep.__init__") as mock_super:
249+
mock_retrieve.return_value = "sklearn-image:latest"
250+
mock_trainer_instance = Mock()
251+
mock_trainer_instance.train = Mock(return_value=Mock())
252+
mock_trainer.return_value = mock_trainer_instance
253+
mock_super.return_value = None
254+
255+
_RepackModelStep(
256+
name="repack-step",
257+
sagemaker_session=mock_session,
258+
role="arn:aws:iam::123456789012:role/SageMakerRole",
259+
model_data="s3://bucket/model.tar.gz",
260+
entry_point=temp_entry_point,
261+
)
262+
263+
mock_trainer.assert_called_once()
264+
compute = mock_trainer.call_args[1]["compute"]
265+
assert compute.volume_kms_key_id is None
266+
210267
def test_init_with_depends_on(self, mock_session, temp_entry_point):
211268
"""Test _RepackModelStep initialization with depends_on."""
212269
with patch("sagemaker.mlops.workflow._utils.image_uris.retrieve") as mock_retrieve:

0 commit comments

Comments
 (0)