Skip to content

Commit cc70f6a

Browse files
committed
feat: add --express flag to sam deploy and sam sync for CloudFormation Express mode
CloudFormation Express mode completes stack operations ~4x faster by returning once resource configuration is applied rather than waiting for full stabilization. This adds --express/--no-express to both commands: - sam deploy --express (opt-in, default false) - sam sync --express (opt-in by default, default true) When enabled, passes DeploymentConfig={"Mode":"EXPRESS","DisableRollback":...} to CreateChangeSet/CreateStack/UpdateStack. The DisableRollback value inside DeploymentConfig is controlled by the existing --disable-rollback flag. Requires boto3/botocore >= 1.43.38 (PR #9084).
1 parent cb8abd8 commit cc70f6a

13 files changed

Lines changed: 250 additions & 13 deletions

File tree

samcli/commands/deploy/command.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@
143143
type=int,
144144
help="Maximum duration in minutes to wait for the deployment to complete.",
145145
)
146+
@click.option(
147+
"--express/--no-express",
148+
default=False,
149+
required=False,
150+
is_flag=True,
151+
help="Use CloudFormation Express mode to speed up deployments by completing once resource "
152+
"configuration is applied, without waiting for full stabilization.",
153+
)
146154
@stack_name_option(callback=guided_deploy_stack_name) # pylint: disable=E1120
147155
@s3_bucket_option(disable_callback=True) # pylint: disable=E1120
148156
@image_repository_option
@@ -204,6 +212,7 @@ def cli(
204212
disable_rollback,
205213
on_failure,
206214
max_wait_duration,
215+
express,
207216
):
208217
"""
209218
`sam deploy` command entry point
@@ -241,6 +250,7 @@ def cli(
241250
disable_rollback,
242251
on_failure,
243252
max_wait_duration,
253+
express,
244254
) # pragma: no cover
245255

246256

@@ -276,6 +286,7 @@ def do_cli(
276286
disable_rollback,
277287
on_failure,
278288
max_wait_duration,
289+
express,
279290
):
280291
"""
281292
Implementation of the ``cli`` method
@@ -387,5 +398,6 @@ def do_cli(
387398
on_failure=on_failure,
388399
max_wait_duration=max_wait_duration,
389400
language_extensions=language_extensions,
401+
express=express,
390402
) as deploy_context:
391403
deploy_context.run()

samcli/commands/deploy/core/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"on_failure",
3939
"force_upload",
4040
"max_wait_duration",
41+
"express",
4142
]
4243

4344
CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS

samcli/commands/deploy/deploy_context.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(
7777
on_failure,
7878
max_wait_duration,
7979
language_extensions: Optional[bool] = None,
80+
express: bool = False,
8081
):
8182
self.template_file = template_file
8283
self.stack_name = stack_name
@@ -111,6 +112,7 @@ def __init__(
111112
self._max_template_size = 51200
112113
self.max_wait_duration = max_wait_duration
113114
self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions)
115+
self.express = express
114116

115117
def __enter__(self):
116118
return self
@@ -255,6 +257,11 @@ def deploy(
255257
click.secho(f"{resource} has no authentication.", fg="yellow")
256258

257259
assert self.deployer is not None
260+
if self.express:
261+
deployment_config = {"Mode": "EXPRESS", "DisableRollback": disable_rollback}
262+
else:
263+
deployment_config = None
264+
258265
if use_changeset:
259266
try:
260267
result, changeset_type = self.deployer.create_and_wait_for_changeset(
@@ -266,6 +273,7 @@ def deploy(
266273
notification_arns=notification_arns,
267274
s3_uploader=s3_uploader,
268275
tags=tags,
276+
deployment_config=deployment_config,
269277
)
270278
click.echo(self.MSG_SHOWCASE_CHANGESET.format(changeset_id=result["Id"]))
271279

@@ -279,7 +287,7 @@ def deploy(
279287
return
280288

281289
marker_time = self.deployer.get_last_event_time(stack_name, 0)
282-
self.deployer.execute_changeset(result["Id"], stack_name, disable_rollback)
290+
self.deployer.execute_changeset(result["Id"], stack_name, disable_rollback, express=self.express)
283291
self.deployer.wait_for_execute(
284292
stack_name, changeset_type, disable_rollback, self.on_failure, marker_time, self.max_wait_duration
285293
)
@@ -307,6 +315,7 @@ def deploy(
307315
s3_uploader=s3_uploader,
308316
tags=tags, # type: ignore[arg-type]
309317
on_failure=self.on_failure,
318+
deployment_config=deployment_config,
310319
)
311320
LOG.debug(result)
312321

samcli/commands/sync/command.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@
161161
help="This option will skip the initial infrastructure deployment if it is not required"
162162
" by comparing the local template with the template deployed in cloud.",
163163
)
164+
@click.option(
165+
"--express/--no-express",
166+
default=True,
167+
required=False,
168+
is_flag=True,
169+
help="Use CloudFormation Express mode to speed up infrastructure deployments by completing once resource "
170+
"configuration is applied, without waiting for full stabilization. Enabled by default.",
171+
)
164172
@container_env_var_file_option(cls=ContainerOptions)
165173
@watch_exclude_option
166174
@stack_name_option(required=True) # pylint: disable=E1120
@@ -216,6 +224,7 @@ def cli(
216224
metadata: dict,
217225
use_container: bool,
218226
container_env_var_file: Optional[str],
227+
express: bool,
219228
save_params: bool,
220229
config_file: str,
221230
config_env: str,
@@ -262,6 +271,7 @@ def cli(
262271
build_in_source,
263272
watch_exclude,
264273
language_extensions,
274+
express,
265275
) # pragma: no cover
266276

267277

@@ -297,6 +307,7 @@ def do_cli(
297307
build_in_source: Optional[bool],
298308
watch_exclude: Optional[Dict[str, List[str]]],
299309
language_extensions: Optional[bool],
310+
express: bool = True,
300311
) -> None:
301312
"""
302313
Implementation of the ``cli`` method
@@ -408,6 +419,7 @@ def do_cli(
408419
on_failure=None,
409420
max_wait_duration=60,
410421
language_extensions=language_extensions,
422+
express=express,
411423
) as deploy_context:
412424
with SyncContext(
413425
dependency_layer,

samcli/lib/deploy/deployer.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,16 @@ def has_stack(self, stack_name):
178178
raise e
179179

180180
def create_changeset(
181-
self, stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags
181+
self,
182+
stack_name,
183+
cfn_template,
184+
parameter_values,
185+
capabilities,
186+
role_arn,
187+
notification_arns,
188+
s3_uploader,
189+
tags,
190+
deployment_config=None,
182191
):
183192
"""
184193
Call Cloudformation to create a changeset and wait for it to complete
@@ -222,6 +231,9 @@ def create_changeset(
222231
"Tags": tags,
223232
}
224233

234+
if deployment_config:
235+
kwargs["DeploymentConfig"] = deployment_config
236+
225237
kwargs = self._process_kwargs(kwargs, s3_uploader, capabilities, role_arn, notification_arns)
226238
return self._create_change_set(stack_name=stack_name, changeset_type=changeset_type, **kwargs)
227239

@@ -369,18 +381,20 @@ def wait_for_changeset(self, changeset_id, stack_name):
369381

370382
raise ChangeSetError(stack_name=stack_name, msg=f"ex: {ex} Status: {status}. Reason: {reason}") from ex
371383

372-
def execute_changeset(self, changeset_id, stack_name, disable_rollback):
384+
def execute_changeset(self, changeset_id, stack_name, disable_rollback, express=False):
373385
"""
374386
Calls CloudFormation to execute changeset
375387
376388
:param changeset_id: ID of the changeset
377389
:param stack_name: Name or ID of the stack
378390
:param disable_rollback: Preserve the state of previously provisioned resources when an operation fails.
391+
:param express: If True, skip DisableRollback (managed by DeploymentConfig on the changeset).
379392
"""
380393
try:
381-
self._client.execute_change_set(
382-
ChangeSetName=changeset_id, StackName=stack_name, DisableRollback=disable_rollback
383-
)
394+
kwargs = {"ChangeSetName": changeset_id, "StackName": stack_name}
395+
if not express:
396+
kwargs["DisableRollback"] = disable_rollback
397+
self._client.execute_change_set(**kwargs)
384398
except botocore.exceptions.ClientError as ex:
385399
raise DeployFailedError(stack_name=stack_name, msg=str(ex)) from ex
386400

@@ -595,11 +609,28 @@ def wait_for_execute(
595609
raise ex
596610

597611
def create_and_wait_for_changeset(
598-
self, stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags
612+
self,
613+
stack_name,
614+
cfn_template,
615+
parameter_values,
616+
capabilities,
617+
role_arn,
618+
notification_arns,
619+
s3_uploader,
620+
tags,
621+
deployment_config=None,
599622
):
600623
try:
601624
result, changeset_type = self.create_changeset(
602-
stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags
625+
stack_name,
626+
cfn_template,
627+
parameter_values,
628+
capabilities,
629+
role_arn,
630+
notification_arns,
631+
s3_uploader,
632+
tags,
633+
deployment_config=deployment_config,
603634
)
604635
self.wait_for_changeset(result["Id"], stack_name)
605636
self.describe_changeset(result["Id"], stack_name)
@@ -646,6 +677,7 @@ def sync(
646677
s3_uploader: Optional[S3Uploader],
647678
tags: Optional[Dict],
648679
on_failure: FailureMode,
680+
deployment_config=None,
649681
):
650682
"""
651683
Call the sync command to directly update stack or create stack
@@ -686,6 +718,9 @@ def sync(
686718
"Tags": tags,
687719
}
688720

721+
if deployment_config:
722+
kwargs["DeploymentConfig"] = deployment_config
723+
689724
kwargs = self._process_kwargs(kwargs, s3_uploader, capabilities, role_arn, notification_arns)
690725

691726
try:
@@ -695,7 +730,8 @@ def sync(
695730
msg = ""
696731

697732
if exists:
698-
kwargs["DisableRollback"] = disable_rollback # type: ignore
733+
if not deployment_config:
734+
kwargs["DisableRollback"] = disable_rollback # type: ignore
699735
# get the latest stack event, and use 0 in case if the stack does not exist
700736
marker_time = self.get_last_event_time(stack_name, 0)
701737
result = self.update_stack(**kwargs)
@@ -704,8 +740,8 @@ def sync(
704740
)
705741
msg = "\nStack update succeeded. Sync infra completed.\n"
706742
else:
707-
# Pass string representation of enum
708-
kwargs["OnFailure"] = str(on_failure)
743+
if not deployment_config:
744+
kwargs["OnFailure"] = str(on_failure)
709745

710746
result = self.create_stack(**kwargs)
711747
self.wait_for_execute(stack_name, "CREATE", disable_rollback, on_failure=on_failure)

0 commit comments

Comments
 (0)