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

  1. Pythonic Interface to CDK CLI: Convert CLI options into Python function parameters for better code readability, IDE support, and type checking.

  2. 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.

  3. Directory Context Management: Automatically locate and use the right cdk.json file 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:

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:

  1. 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.

  2. 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:

  1. Automatic Directory Location: The dir_cdk parameter allows specifying the location of the cdk.json file, and the wrapper handles changing to that directory before executing commands.

  2. 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.