CDK CLI in Python¶
Overview¶
The CDK CLI in Python module provides a Pythonic interface to AWS Cloud Development Kit (CDK) commands, offering significant improvements over directly using the CLI. This module addresses several common challenges in managing CDK deployments programmatically.
Key Features¶
Pythonic Interface to CDK CLI: Convert CLI options into Python function parameters for better code readability, IDE support, and type checking.
AWS Credential Management: Integration with Boto Session Manager (BSM) to ensure CDK commands use specific AWS credentials, overcoming the limited credential management of the native CDK CLI.
Directory Context Management: Automatically locate and use the right
cdk.jsonfile without changing directories, enabling CDK operations from any location.
CLI Command Modules¶
The CLI commands are implemented in cdk_mate.cli.cli_cmd, providing function wrappers for common CDK operations:
cdk_mate.cli.cli_cmd.Bootstrap- Prepare an AWS environment for CDK deploymentscdk_mate.cli.cli_cmd.Synth- Synthesize CloudFormation templatescdk_mate.cli.cli_cmd.Diff- Compare deployed stacks with current statecdk_mate.cli.cli_cmd.Deploy- Deploy stacks to AWScdk_mate.cli.cli_cmd.Destroy- Remove stacks from AWS
cli_cmd.py
1# -*- coding: utf-8 -*-
2
3"""
4AWS CDK CLI Command Wrapper Classes
5
6This module provides a class-based approach to AWS CDK CLI commands (bootstrap, synth,
7deploy, destroy, etc.) with comprehensive option handling and flexible execution support.
8
9.. code-block:: python
10
11 # Deploy a stack with options
12 Deploy(
13 stacks=["MyStack"],
14 profile="my_aws_profile",
15 require_approval="never"
16 ).run()
17
18 # Destroy a stack with confirmation bypass
19 Destroy(
20 stacks=["MyStack"],
21 force=True
22 ).run()
23"""
24
25import typing as T
26import dataclasses
27
28from func_args.api import REQ, OPT, BaseModel
29
30from .cli_utils import (
31 pos_arg,
32 value_arg,
33 bool_arg,
34 kv_arg,
35 array_arg,
36 count_arg,
37 run_cdk_command,
38)
39
40if T.TYPE_CHECKING: # pragma: no cover
41 from pathlib_mate import T_PATH_ARG
42 from boto_session_manager import BotoSesManager
43
44
45@dataclasses.dataclass
46class BaseCommand(BaseModel):
47 """
48 Base class for all CDK CLI commands.
49
50 Implements common functionality for command execution:
51
52 - Parameter validation
53 - Argument processing and conversion
54 - Command execution with AWS session integration
55
56 All CDK commands inherit global options from this class, such as:
57
58 - AWS profile and credentials management
59 - Output formatting
60 - Debug and verbose options
61 - And many other global AWS CDK CLI options
62
63 The class uses a metadata-driven approach to process different argument types,
64 allowing for a clean, declarative command definition.
65 """
66
67 # fmt: off
68 app: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
69 asset_metadata: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
70 builder: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
71 ca_bundle_path: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
72 ci: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
73 context: dict[str, str] = dataclasses.field(default=OPT, metadata={"t": kv_arg})
74 debug: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
75 ec2creds: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
76 help: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
77 ignore_errors: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
78 json: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
79 lookups: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
80 no_color: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
81 notices: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
82 output: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
83 path_metadata: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
84 plugin: list[str] = dataclasses.field(default=OPT, metadata={"t": array_arg})
85 profile: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
86 proxy: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
87 role_arn: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
88 staging: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
89 strict: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
90 trace: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
91 verbose: int = dataclasses.field(default=OPT, metadata={"t": count_arg})
92 version: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
93 version_reporting: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
94 # fmt: on
95
96 def _cdk_cmd(self) -> list[str]: # pragma: no cover
97 """
98 Return the base CDK command to be executed.
99 """
100 raise NotImplementedError
101
102 def _process(
103 self,
104 args: list[str],
105 field: dataclasses.Field,
106 ):
107 """
108 Process a field based on its metadata type.
109 """
110 name = field.name.replace("_", "-")
111 if name.endswith("_"):
112 name = name[:-1]
113 field.metadata["t"].process(
114 name=name,
115 value=getattr(self, field.name),
116 args=args,
117 )
118
119 def to_args(self) -> list[str]:
120 """
121 Convert the command object to a list of CLI arguments.
122 """
123 args = self._cdk_cmd()
124
125 global_fields: dict[str, dataclasses.Field] = {
126 field.name: field for field in dataclasses.fields(BaseCommand)
127 }
128
129 command_fields: dict[str, dataclasses.Field] = {
130 field.name: field for field in dataclasses.fields(self.__class__)
131 }
132
133 # process command-specific fields first
134 for name in command_fields:
135 if name not in global_fields:
136 field = command_fields[name]
137 # print(f"{field = }") # for debug only
138 self._process(args, field)
139
140 # then process global fields
141 for field in global_fields.values():
142 # print(f"{field = }") # for debug only
143 self._process(args, field)
144
145 return args
146
147 def run(
148 self,
149 bsm: T.Optional["BotoSesManager"] = None,
150 dir_cdk: T.Optional["T_PATH_ARG"] = None,
151 ): # pragma: no cover
152 """
153 Execute the CDK command with the configured parameters.
154
155 :param bsm: Optional Boto Session Manager for AWS credentials and context
156 :param dir_cdk: Optional directory path for executing the CDK command
157
158 :return: CompletedProcess instance with command execution results
159 :raises subprocess.CalledProcessError: If the command execution fails
160 """
161 return run_cdk_command(
162 args=self.to_args(),
163 bsm=bsm,
164 dir_cdk=dir_cdk,
165 )
166
167
168@dataclasses.dataclass
169class Acknowledge(BaseCommand):
170 """
171 Acknowledge a notice by issue number and hide it from displaying again.
172
173 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-ack.html
174 """
175
176 # fmt: off
177 notice_id: str = dataclasses.field(default=OPT, metadata={"t": pos_arg})
178 # fmt: on
179
180 def _cdk_cmd(self) -> list[str]: # pragma: no cover
181 return ["cdk", "acknowledge"]
182
183
184@dataclasses.dataclass
185class Bootstrap(BaseCommand):
186 """
187 Prepare an AWS environment for CDK deployments by deploying the CDK bootstrap stack,
188 named ``CDKToolkit``, into the AWS environment.
189
190 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-bootstrap.html
191 """
192
193 # fmt: off
194 aws_environment: str = dataclasses.field(default=OPT, metadata={"t": pos_arg})
195 bootstrap_bucket_name: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
196 bootstrap_customer_key: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
197 bootstrap_kms_key_id: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
198 cloudformation_execution_policies: list[str] = dataclasses.field(default=OPT, metadata={"t": array_arg})
199 custom_permissions_boundary: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
200 example_permissions_boundary: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
201 execute: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
202 force: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
203 previous_parameters: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
204 public_access_block_configuration: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
205 qualifier: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
206 show_template: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
207 tags: dict[str, str] = dataclasses.field(default=OPT, metadata={"t": kv_arg})
208 template: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
209 termination_protection: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
210 toolkit_stack_name: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
211 trust: list[str] = dataclasses.field(default=OPT, metadata={"t": array_arg})
212 trust_for_lookup: list[str] = dataclasses.field(default=OPT, metadata={"t": array_arg})
213 # fmt: on
214
215 def _cdk_cmd(self) -> list[str]: # pragma: no cover
216 return ["cdk", "bootstrap"]
217
218
219@dataclasses.dataclass
220class Context(BaseCommand):
221 """
222 Manage cached context values for your AWS CDK application.
223
224 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-context.html
225 """
226
227 # fmt: off
228 clear: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
229 force: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
230 reset: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
231
232 # fmt: on
233
234 def _cdk_cmd(self) -> list[str]: # pragma: no cover
235 return ["cdk", "context"]
236
237
238@dataclasses.dataclass
239class Deploy(BaseCommand):
240 """
241 Deploy AWS CDK stacks to AWS infrastructure with granular control over deployment parameters.
242
243 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-deploy.html
244 """
245
246 # fmt: off
247 stacks: list[str] = dataclasses.field(default=OPT, metadata={"t": pos_arg})
248 all: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
249 asset_parallelism: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
250 asset_prebuild: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
251 build_exclude: list[str] = dataclasses.field(default=OPT, metadata={"t": array_arg})
252 change_set_name: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
253 concurrency: int = dataclasses.field(default=OPT, metadata={"t": value_arg})
254 exclusively: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
255 force: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
256 hotswap: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
257 hotswap_fallback: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
258 ignore_no_stacks: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
259 import_existing_resources: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
260 logs: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
261 method: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
262 notification_arns: list[str] = dataclasses.field(default=OPT, metadata={"t": array_arg})
263 outputs_file: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
264 parameters: dict[str, str] = dataclasses.field(default=OPT, metadata={"t": kv_arg})
265 previous_parameters: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
266 progress: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
267 require_approval: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
268 rollback: T.Optional[bool] = dataclasses.field(default=None, metadata={"t": bool_arg})
269 toolkit_stack_name: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
270 watch: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
271 # fmt: on
272
273 def _cdk_cmd(self) -> list[str]: # pragma: no cover
274 return ["cdk", "deploy"]
275
276
277@dataclasses.dataclass
278class Destroy(BaseCommand):
279 """
280 Safely remove AWS CDK stacks from infrastructure with flexible destruction options.
281
282 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-deploy.html
283 """
284
285 # fmt: off
286 stacks: list[str] = dataclasses.field(default=OPT, metadata={"t": pos_arg})
287 all: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
288 exclusively: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
289 force: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
290 # fmt: on
291
292 def _cdk_cmd(self) -> list[str]: # pragma: no cover
293 return ["cdk", "destroy"]
294
295
296@dataclasses.dataclass
297class Diff(BaseCommand):
298 """
299 Compare deployed stacks with current state or a specific CloudFormation template.
300
301 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-diff.html
302 """
303
304 # fmt: off
305 stacks: list[str] = dataclasses.field(default=OPT, metadata={"t": pos_arg})
306 change_set: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
307 context_lines: int = dataclasses.field(default=OPT, metadata={"t": value_arg})
308 exclusively: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
309 fail: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
310 processed: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
311 quiet: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
312 security_only: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
313 strict: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
314 template: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
315 # fmt: on
316
317 def _cdk_cmd(self) -> list[str]: # pragma: no cover
318 return ["cdk", "diff"]
319
320
321@dataclasses.dataclass
322class GC(BaseCommand):
323 """
324 Perform garbage collection on unused assets stored in the resources of your bootstrap stack.
325
326 Note: This command is still in development and requires the --unstable=gc option.
327
328 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-gc.html
329 """
330
331 # fmt: off
332 aws_environment: list[str] = dataclasses.field(default=OPT, metadata={"t": pos_arg})
333 action: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
334 bootstrap_stack_name: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
335 confirm: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
336 created_buffer_days: int = dataclasses.field(default=OPT, metadata={"t": value_arg})
337 rollback_buffer_days: int = dataclasses.field(default=OPT, metadata={"t": value_arg})
338 type: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
339 unstable: list[str] = dataclasses.field(default=OPT, metadata={"t": array_arg})
340
341 # fmt: on
342
343 def _cdk_cmd(self) -> list[str]: # pragma: no cover
344 return ["cdk", "gc"]
345
346
347@dataclasses.dataclass
348class Import(BaseCommand):
349 """
350 Import existing AWS resources into a CDK stack.
351
352 This command allows you to take existing resources that were created using
353 other methods and start managing them using the AWS CDK.
354
355 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-import.html
356 """
357
358 # fmt: off
359 stacks: list[str] = dataclasses.field(default=OPT, metadata={"t": pos_arg})
360 change_set_name: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
361 execute: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
362 force: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
363 record_resource_mapping: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
364 resource_mapping: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
365 rollback: T.Optional[bool] = dataclasses.field(default=None, metadata={"t": bool_arg})
366 toolkit_stack_name: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
367 # fmt: on
368
369 def _cdk_cmd(self) -> list[str]: # pragma: no cover
370 return ["cdk", "import"]
371
372
373@dataclasses.dataclass
374class Init(BaseCommand):
375 """
376 Create a new AWS CDK project from a template.
377
378 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-init.html
379 """
380
381 # fmt: off
382 template_type: str = dataclasses.field(default=OPT, metadata={"t": pos_arg})
383 generate_only: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
384 language: str = dataclasses.field(default=OPT, metadata={"t": value_arg})
385 list_: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
386
387 # fmt: on
388
389 def _cdk_cmd(self) -> list[str]: # pragma: no cover
390 return ["cdk", "init"]
391
392
393@dataclasses.dataclass
394class Synth(BaseCommand):
395 """
396 Synthesize AWS CDK stacks into CloudFormation templates with comprehensive configuration options.
397
398 Ref: https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-synth.html
399 """
400
401 # fmt: off
402 exclusively: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
403 quiet: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
404 validation: bool = dataclasses.field(default=OPT, metadata={"t": bool_arg})
405 # fmt: on
406
407 def _cdk_cmd(self) -> list[str]:
408 return ["cdk", "synth"]
Credential Management¶
The CLI wrapper integrates with Boto Session Manager to provide enhanced credential management:
Context Manager for Credentials: When a BSM object is provided, the wrapper uses its
awscli()context manager to set the appropriate environment variables before running the CDK command, ensuring the command uses the specified AWS credentials.Explicit Account Selection: This approach guarantees deployment to the correct AWS account by overriding the default credential chain, providing more reliability than profile-based selection.
Example:
from boto_session_manager import BotoSesManager
import cdk_mate.api as cdk_mate
# Create a session for a specific AWS account and region
bsm = BotoSesManager(
profile_name="dev",
region_name="us-east-1"
)
# Deploy using this session
cdk_mate.cli.Deploy(
stacks=["MyStack"],
require_approval="never"
).run(bsm=bsm)
Directory Context Management¶
The CLI wrapper handles directory context automatically:
Automatic Directory Location: The
dir_cdkparameter allows specifying the location of thecdk.jsonfile, and the wrapper handles changing to that directory before executing commands.Path Resolution: If a file path is provided, the wrapper uses its parent directory, ensuring flexibility in how paths are specified.
Example:
import cdk_mate.api as cdk_mate
# Synthesize from any location
cdk_mate.cli.Synth().run(dir_cdk="/path/to/my/cdk/project")
# Using a file path (will use the file's directory)
cdk_mate.cli.Synth().run(dir_cdk="/path/to/my/cdk/project/stack1_app.py")
Usage Examples¶
Bootstrapping an AWS Environment¶
Prepare an AWS environment for CDK deployments:
from boto_session_manager import BotoSesManager
import cdk_mate.cli as cdk_mate
bsm = BotoSesManager(
profile_name="dev",
region_name="us-east-1"
)
cdk_mate.cli.Bootstrap(
aws_environment="123456789012/us-east-1",
bootstrap_bucket_name="my-cdk-bootstrap-bucket",
qualifier="hnb659fds" # Custom qualifier
).run(bsm=bsm)
Basic Synthesis¶
Synthesize a CDK application without changing directories:
import cdk_mate.api as cdk_mate
cdk_mate.cli.Synth().run(dir_cdk="/path/to/cdk/project")
Comparing Stacks with Diff¶
Compare local CDK stacks with deployed versions:
import cdk_mate.cli as cdk_mate
# Basic diff
cdk_mate.cli.Diff(
stacks=["MyStack"]
).run(dir_cdk="/path/to/cdk/project")
# Advanced diff options
cdk_mate.cli.Diff(
stacks=["MyStack"],
quiet=True,
security_only=True,
change_set=False # Faster but less accurate diff
).run(dir_cdk="/path/to/cdk/project")
Deploying with Specific Credentials¶
Deploy a stack using explicit AWS credentials:
from boto_session_manager import BotoSesManager
import cdk_mate.api as cdk_mate
bsm = BotoSesManager(
profile_name="dev",
region_name="us-east-1"
)
cdk_mate.cli.Deploy(
stacks=["MyStack"],
require_approval="never"
).run(
bsm=bsm,
dir_cdk="/path/to/cdk/project",
)
Destroying Multiple Stacks¶
Remove multiple stacks with forced deletion:
import cdk_mate.api as cdk_mate
cdk_mate.cli.Destroy(
stacks=["Stack1", "Stack2"],
force=True
).run(
bsm=bsm,
dir_cdk="/path/to/cdk/project",
)
Conclusion¶
The CDK CLI in Python module provides a robust Pythonic interface to AWS CDK operations, addressing key limitations of the native CLI. By combining credential management, directory context handling, and a comprehensive parameter interface, it enables more reliable and maintainable infrastructure deployment automation.