Multi Stack Multi Environment Best Practices

Overview

The Multi-Stack, Multi-Environment CDK Deployment Pattern provides a scalable and flexible approach to managing many AWS CDK stacks across multiple environments (accounts, regions, stages). This pattern addresses several common challenges in enterprise-level infrastructure as code:

  • Complexity Management: Separate stack implementation from deployment configuration

  • Deployment Flexibility: Deploy individual stacks, groups of stacks, or all stacks as needed

  • Environment Consistency: Ensure consistent stack configurations across environments

  • Resource Isolation: Keep resource definitions clean and focused

  • Credential Management: Safely handle AWS credentials for different environments

  • Testing Granularity: Test infrastructure at various levels of integration

This pattern scales effectively by:

  1. Using lightweight context objects to reference stack configurations without loading full CDK code

  2. Implementing lazy loading for AWS session credentials

  3. Separating stack definition from stack instantiation

  4. Providing reusable deployment utilities at both individual and group levels

  5. Centralizing environment configuration for consistency

Code Architecture

Directory Structure

project/
├── cdk/                           # Deployment scripts
│   ├── app.py                     # Main CDK app entrypoint
│   ├── deploy.py                  # Multi-stack deployment script
│   ├── stack1/                    # Stack1-specific deployment
│   │   ├── stack1_app.py          # Stack1 app entry point
│   │   └── stack1_deploy.py       # Stack1 deployment script
│   └── stack2/                    # Stack2-specific deployment
│       ├── stack2_app.py          # Stack2 app entry point
│       └── stack2_deploy.py       # Stack2 deployment script
├── cdk_mate/                      # Core library
│   ├── stack_ctx.py               # Stack context implementation
│   └── tests/                     # Example implementations
│       ├── stacks.py              # CDK stack definition
│       │   ├── stack1/                             # Stack1-specific definition
│       │   │   ├── iac_define.py                   # Stack1 entry point
│       │   │   └── iac_define_01_everything.py     # Stack1 mixin class
│       │   └── stack2/                             # Stack2-specific definition
│       │       ├── iac_define.py                   # Stack2 entry point
│       │       └── iac_define_01_everything.py     # Stack2 mixin class
│       ├── bsm_enum.py            # AWS session enumerations
│       ├── stack_ctx_enum.py      # Stack context enumerations
│       └── stack_enum.py          # CDK stack initialization
└── tests/                         # Unit tests
    └── test_stack_enum.py         # Tests for stack initialization

Core Components

  1. Stack Implementation: Each stack is implemented in a dedicated module using a mixin pattern to manage complexity

  2. BSM Enum: Defines AWS session configurations for different environments

  3. Stack Context Enum: Defines metadata for each stack instance (stack name, AWS account, region)

  4. IAC Initialization: Creates CDK stack instances using context objects

  5. Deployment Scripts: Provides flexible deployment options

Implementation Details

Reference:

Stack Context

The StackCtx class provides essential metadata for each stack instance.

stack_ctx.py
  1# -*- coding: utf-8 -*-
  2
  3"""
  4Stack Context Management for AWS CDK Deployments
  5
  6This module provides utilities for managing stack contexts across multiple
  7environments, enabling flexible and consistent infrastructure deployment.
  8"""
  9
 10import typing as T
 11import dataclasses
 12
 13import aws_cdk as cdk
 14
 15from boto_session_manager import BotoSesManager
 16from func_args.api import T_KWARGS, REQ, OPT, BaseModel
 17
 18from .utils import to_camel, to_slug
 19
 20from .cli.cli_cmd import Synth, Diff, Deploy, Destroy
 21
 22
 23if T.TYPE_CHECKING:  # pragma: no cover
 24    from pathlib_mate import T_PATH_ARG
 25
 26
 27@dataclasses.dataclass
 28class StackCtx(BaseModel):
 29    """
 30    Represents the configuration and deployment context for an AWS CDK stack.
 31
 32    This dataclass encapsulates all necessary information for deploying
 33    a stack across different environments, providing a flexible and
 34    reusable approach to infrastructure management.
 35
 36    - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk_mate/tests/stack_ctx_enum.py>`_
 37
 38    :param construct_id: Unique identifier for the CDK construct
 39    :param stack_name: Descriptive name of the stack (used in AWS CloudFormation)
 40    :param aws_account_id: AWS Account ID where the stack will be deployed
 41    :param aws_region: AWS Region where the stack will be deployed
 42    :param bsm: Optional Boto Session Manager for AWS credentials
 43    """
 44
 45    construct_id: str = dataclasses.field(default=REQ)
 46    stack_name: str = dataclasses.field(default=REQ)
 47    aws_account_id: str = dataclasses.field(default=REQ)
 48    aws_region: str = dataclasses.field(default=REQ)
 49    bsm: T.Optional["BotoSesManager"] = dataclasses.field(default=None)
 50
 51    @classmethod
 52    def make_stack_ctx_kwargs(
 53        cls,
 54        stack_name: str,
 55        bsm: "BotoSesManager",
 56    ) -> T_KWARGS:
 57        """
 58        Create a new StackCtx instance.
 59        """
 60        return {
 61            "construct_id": to_camel(stack_name),
 62            "stack_name": to_slug(stack_name),
 63            "aws_account_id": bsm.aws_account_id,
 64            "aws_region": bsm.aws_region,
 65            "bsm": bsm,
 66        }
 67
 68    @classmethod
 69    def new(
 70        cls,
 71        stack_name: str,
 72        bsm: "BotoSesManager",
 73    ):
 74        """
 75        Create a new StackCtx instance.
 76        """
 77        return cls(
 78            **cls.make_stack_ctx_kwargs(
 79                stack_name=stack_name,
 80                bsm=bsm,
 81            )
 82        )
 83
 84    def to_stack_kwargs(self) -> dict[str, T.Any]:
 85        """
 86        Generate keyword arguments for CDK stack initialization. Your stack
 87        definition should look like this:
 88
 89        .. code-block:: python
 90
 91            import aws_cdk as cdk
 92            from constructs import Construct
 93
 94            class MyStack(
 95                cdk.Stack,
 96            ):
 97                def __init__(
 98                    self,
 99                    scope: Construct,
100                    id: str,
101                    stack_name: str,
102                    env: cdk.Environment,
103                    ...
104                ):
105                    super().__init__(
106                        scope=scope,
107                        id=id,
108                        stack_name=stack_name,
109                        env=env,
110                        ...
111                    )
112                    ...
113        """
114        return dict(
115            id=self.construct_id,
116            stack_name=self.stack_name,
117            env=cdk.Environment(
118                account=self.aws_account_id,
119                region=self.aws_region,
120            ),
121        )
122
123    @property
124    def stack_console_url(self) -> str:
125        """
126        Generate the AWS CloudFormation console URL for this stack.
127        """
128        return (
129            f"https://{self.aws_region}.console.aws.amazon.com/cloudformation"
130            f"/home?region={self.aws_region}#/stacks?"
131            f"filteringStatus=active&filteringText={self.stack_name}&viewNested=true"
132        )
133
134    def cdk_synth(
135        self,
136        dir_cdk: T.Optional["T_PATH_ARG"] = None,
137        **kwargs: T_KWARGS,
138    ):  # pragma: no cover
139        """
140        Synthesize the stack using AWS CDK CLI.
141
142        - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk/stack1/stack1_deploy.py>`_
143
144        :param dir_cdk: Optional directory path for CDK synthesis context
145        """
146        print("--- Preview in AWS Console ---")
147        print(f"{self.stack_name}: {self.stack_console_url}")
148        cmd = Synth(
149            **kwargs,
150        )
151        return cmd.run(bsm=self.bsm, dir_cdk=dir_cdk)
152
153    def cdk_diff(
154        self,
155        dir_cdk: T.Optional["T_PATH_ARG"] = None,
156        **kwargs: T_KWARGS,
157    ):  # pragma: no cover
158        """
159        Show the differences between the current stack and the deployed stack.
160
161        - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk/stack1/stack1_deploy.py>`_
162
163        :param dir_cdk: Optional directory path for CDK diff context
164        """
165        print("--- Preview in AWS Console ---")
166        print(f"{self.stack_name}: {self.stack_console_url}")
167        cmd = Diff(
168            stacks=[self.construct_id],
169            **kwargs,
170        )
171        return cmd.run(bsm=self.bsm, dir_cdk=dir_cdk)
172
173    def cdk_deploy(
174        self,
175        dir_cdk: T.Optional["T_PATH_ARG"] = None,
176        prompt: bool = False,
177        **kwargs: T_KWARGS,
178    ):  # pragma: no cover
179        """
180        Deploy the stack using AWS CDK CLI.
181
182        - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk/stack1/stack1_deploy.py>`_
183
184        :param dir_cdk: Optional directory path for CDK deployment context
185        :param prompt: Whether to prompt for approval before deployment
186        """
187        print("--- Preview in AWS Console ---")
188        print(f"{self.stack_name}: {self.stack_console_url}")
189        cmd = Deploy(
190            stacks=[self.construct_id],
191            require_approval=OPT if prompt else "never",
192            **kwargs,
193        )
194        return cmd.run(bsm=self.bsm, dir_cdk=dir_cdk)
195
196    def cdk_destroy(
197        self,
198        dir_cdk: T.Optional["T_PATH_ARG"] = None,
199        prompt: bool = False,
200        **kwargs: T_KWARGS,
201    ):  # pragma: no cover
202        """
203        Destroy the stack using AWS CDK CLI.
204
205        - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk/stack1/stack1_deploy.py>`_
206
207        :param dir_cdk: Optional directory path for CDK deployment context
208        :param prompt: Whether to prompt for approval before destruction
209        """
210        print("--- Preview in AWS Console ---")
211        print(f"{self.stack_name}: {self.stack_console_url}")
212        cmd = Destroy(
213            stacks=[self.construct_id],
214            force=OPT if prompt else True,
215            **kwargs,
216        )
217        return cmd.run(bsm=self.bsm, dir_cdk=dir_cdk)
218
219
220def cdk_diff_many(
221    stack_ctx_list: list[StackCtx],
222    dir_cdk: T.Optional["T_PATH_ARG"] = None,
223    **kwargs: T_KWARGS,
224):  # pragma: no cover
225    """
226    Show the differences between multiple stacks and their deployed versions.
227
228    - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk/deploy.py>`_
229
230    :param stack_ctx_list: List of stack contexts to compare
231    :param dir_cdk: Optional directory path for CDK context
232    """
233    print("--- Preview in AWS Console ---")
234    for stack_ctx in stack_ctx_list:
235        print(f"{stack_ctx.stack_name}: {stack_ctx.stack_console_url}")
236    cmd = Diff(
237        stacks=[stack_ctx.construct_id for stack_ctx in stack_ctx_list],
238        **kwargs,
239    )
240    return cmd.run(bsm=stack_ctx_list[0].bsm, dir_cdk=dir_cdk)
241
242
243def cdk_deploy_many(
244    stack_ctx_list: list[StackCtx],
245    dir_cdk: T.Optional["T_PATH_ARG"] = None,
246    prompt: bool = False,
247    **kwargs: T_KWARGS,
248):  # pragma: no cover
249    """
250    Deploy multiple stacks in a single operation.
251
252    - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk/deploy.py>`_
253
254    :param stack_ctx_list: List of stack contexts to deploy
255    :param dir_cdk: Optional directory path for CDK deployment context
256    :param prompt: Whether to prompt for approval before deployment
257    """
258    print("--- Preview in AWS Console ---")
259    for stack_ctx in stack_ctx_list:
260        print(f"{stack_ctx.stack_name}: {stack_ctx.stack_console_url}")
261    cmd = Deploy(
262        stacks=[stack_ctx.construct_id for stack_ctx in stack_ctx_list],
263        require_approval=OPT if prompt else "never",
264        **kwargs,
265    )
266    return cmd.run(bsm=stack_ctx_list[0].bsm, dir_cdk=dir_cdk)
267
268
269def cdk_destroy_many(
270    stack_ctx_list: list[StackCtx],
271    dir_cdk: T.Optional["T_PATH_ARG"] = None,
272    prompt: bool = False,
273    **kwargs: T_KWARGS,
274):  # pragma: no cover
275    """
276    Destroy multiple stacks in a single operation.
277
278    - `Usage Example <https://github.com/MacHu-GWU/cdk_mate-project/blob/main/cdk/deploy.py>`_
279
280    :param stack_ctx_list: List of stack contexts to destroy
281    :param dir_cdk: Optional directory path for CDK deployment context
282    :param prompt: Whether to prompt for approval before destruction
283    """
284    print("--- Preview in AWS Console ---")
285    for stack_ctx in stack_ctx_list:
286        print(f"{stack_ctx.stack_name}: {stack_ctx.stack_console_url}")
287    cmd = Destroy(
288        stacks=[stack_ctx.construct_id for stack_ctx in stack_ctx_list],
289        force=OPT if prompt else True,
290        **kwargs,
291    )
292    return cmd.run(bsm=stack_ctx_list[0].bsm, dir_cdk=dir_cdk)

Environment Management

The BSM (Boto Session Manager) enum provides lazy-loaded AWS session objects:

bsm_enum.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4This module provides an enumeration of pre-configured Boto Session Manager
 5instances for different AWS environments and accounts.
 6"""
 7
 8from functools import cached_property
 9from boto_session_manager import BotoSesManager
10
11from .runtime import IS_CI
12
13
14class BsmEnum:
15    """
16    Use lazy loading to create enum values.
17    """
18    def _get_bsm(self, profile: str) -> BotoSesManager:
19        if IS_CI:
20            return BotoSesManager(region_name="us-east-1")
21        else:
22            return BotoSesManager(profile_name=profile)
23
24    @cached_property
25    def dev(self):
26        return self._get_bsm("esc_app_dev_us_east_1")
27
28    @cached_property
29    def test(self):
30        return self._get_bsm("esc_app_test_us_east_1")
31
32
33bsm_enum = BsmEnum()

Stack Context Enumeration

The StackCtxEnum class provides lightweight stack configuration objects:

stack_ctx_enum.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4This module provides an enumeration of pre-configured cloudformation stack
 5context information for different stacks.
 6"""
 7
 8from functools import cached_property
 9
10from ..stack_ctx import StackCtx
11from .bsm_enum import bsm_enum
12
13
14class StackCtxEnum:
15    """
16    Use lazy loading to create enum values.
17    """
18    @cached_property
19    def stack1_dev(self):
20        return StackCtx.new(
21            stack_name="cdk-mate-stack1-dev",
22            bsm=bsm_enum.dev,
23        )
24
25    @cached_property
26    def stack1_test(self):
27        return StackCtx.new(
28            stack_name="cdk-mate-stack1-test",
29            bsm=bsm_enum.test,
30        )
31
32    @cached_property
33    def stack2_dev(self):
34        return StackCtx.new(
35            stack_name="cdk-mate-stack2-dev",
36            bsm=bsm_enum.dev,
37        )
38
39    @cached_property
40    def stack2_test(self):
41        return StackCtx.new(
42            stack_name="cdk-mate-stack2-test",
43            bsm=bsm_enum.test,
44        )
45
46
47stack_ctx_enum = StackCtxEnum()

Stack Initialization

The stack initialization module creates CDK stack instances using the context objects:

stack_enum.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4Stack Initialization for Multi-Account AWS CDK Deployment
 5"""
 6
 7import dataclasses
 8from functools import cached_property
 9
10import aws_cdk as cdk
11
12from .stacks.stack1.iac_define import Stack1
13from .stacks.stack2.iac_define import Stack2
14
15from .stack_ctx_enum import stack_ctx_enum
16
17
18@dataclasses.dataclass
19class StackEnum:
20    """
21    Enumeration of CDK stacks for different environments.
22    """
23
24    app: cdk.App = dataclasses.field()
25
26    @cached_property
27    def stack1_dev(self):
28        return Stack1(
29            scope=self.app,
30            **stack_ctx_enum.stack1_dev.to_stack_kwargs(),
31        )
32
33    @cached_property
34    def stack1_test(self):
35        return Stack1(
36            scope=self.app,
37            **stack_ctx_enum.stack1_test.to_stack_kwargs(),
38        )
39
40    @cached_property
41    def stack2_dev(self):
42        return Stack2(
43            scope=self.app,
44            **stack_ctx_enum.stack2_dev.to_stack_kwargs(),
45        )
46
47    @cached_property
48    def stack2_test(self):
49        return Stack2(
50            scope=self.app,
51            **stack_ctx_enum.stack2_test.to_stack_kwargs(),
52        )
53
54
55# Create the global stack enumeration instance
56app = cdk.App()
57
58stack_enum = StackEnum(app=app)

Deployment Options

This pattern supports three flexible deployment approaches:

1. Single Stack, Multiple Environments

Use stack-specific deployment scripts to deploy a single stack to one or more environments:

cdk.json
 1{
 2  "app": "python3 stack1_app.py",
 3  "watch": {
 4    "include": [
 5      "**"
 6    ],
 7    "exclude": [
 8      "README.md",
 9      "cdk*.json",
10      "requirements*.txt",
11      "source.bat",
12      "**/__init__.py",
13      "python/__pycache__",
14      "tests"
15    ]
16  },
17  "context": {
18    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
19    "@aws-cdk/core:checkSecretUsage": true,
20    "@aws-cdk/core:target-partitions": [
21      "aws",
22      "aws-cn"
23    ],
24    "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25    "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26    "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
27    "@aws-cdk/aws-iam:minimizePolicies": true,
28    "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
29    "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
30    "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
31    "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
32    "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
33    "@aws-cdk/core:enablePartitionLiterals": true,
34    "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
35    "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
36    "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
37    "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
38    "@aws-cdk/aws-route53-patters:useCertificate": true,
39    "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
40    "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
41    "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
42    "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
43    "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
44    "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
45    "@aws-cdk/aws-redshift:columnId": true,
46    "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
47    "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
48    "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
49    "@aws-cdk/aws-kms:aliasNameRef": true,
50    "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
51    "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
52    "@aws-cdk/aws-efs:denyAnonymousAccess": true,
53    "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
54    "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
55    "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
56    "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
57    "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
58    "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
59    "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
60    "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
61    "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
62    "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
63    "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
64    "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
65    "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
66    "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
67    "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
68    "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
69    "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
70    "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
71    "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
72    "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
73    "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
74    "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
75    "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
76    "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
77    "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
78    "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
79    "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
80    "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
81    "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
82    "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
83    "@aws-cdk/core:enableAdditionalMetadataCollection": true,
84    "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false,
85    "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
86    "@aws-cdk/aws-events:requireEventBusPolicySid": true,
87    "@aws-cdk/aws-dynamodb:retainTableReplica": true
88  }
89}
stack1_app.py
1#!/usr/bin/env python3
2
3from cdk_mate.tests.stack_enum import stack_enum
4
5_ = stack_enum.stack1_dev
6_ = stack_enum.stack1_test
7
8stack_enum.app.synth()
stack1_deploy.py
 1# -*- coding: utf-8 -*-
 2
 3from cdk_mate.tests.stack_ctx_enum import stack_ctx_enum
 4
 5# --- 📦 cdk synth
 6# stack_ctx_enum.stack1_dev.cdk_synth(dir_cdk=__file__)
 7# stack_ctx_enum.stack1_test.cdk_synth(dir_cdk=__file__)
 8
 9# --- 🔍 cdk diff
10# stack_ctx_enum.stack1_dev.cdk_diff(dir_cdk=__file__)
11# stack_ctx_enum.stack1_test.cdk_diff(dir_cdk=__file__)
12
13# --- 🚀 cdk deploy
14# stack_ctx_enum.stack1_dev.cdk_deploy(dir_cdk=__file__, prompt=False)
15# stack_ctx_enum.stack1_test.cdk_deploy(dir_cdk=__file__, prompt=False)
16
17# --- 💥 cdk destroy
18# stack_ctx_enum.stack1_dev.cdk_destroy(dir_cdk=__file__, prompt=False)
19# stack_ctx_enum.stack1_test.cdk_destroy(dir_cdk=__file__, prompt=False)

2. Multiple Stacks, Same Environment

Deploy multiple stacks to the same environment:

cdk.json
 1{
 2  "app": "python3 app.py",
 3  "watch": {
 4    "include": [
 5      "**"
 6    ],
 7    "exclude": [
 8      "README.md",
 9      "cdk*.json",
10      "requirements*.txt",
11      "source.bat",
12      "**/__init__.py",
13      "python/__pycache__",
14      "tests"
15    ]
16  },
17  "context": {
18    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
19    "@aws-cdk/core:checkSecretUsage": true,
20    "@aws-cdk/core:target-partitions": [
21      "aws",
22      "aws-cn"
23    ],
24    "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25    "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26    "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
27    "@aws-cdk/aws-iam:minimizePolicies": true,
28    "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
29    "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
30    "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
31    "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
32    "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
33    "@aws-cdk/core:enablePartitionLiterals": true,
34    "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
35    "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
36    "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
37    "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
38    "@aws-cdk/aws-route53-patters:useCertificate": true,
39    "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
40    "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
41    "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
42    "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
43    "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
44    "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
45    "@aws-cdk/aws-redshift:columnId": true,
46    "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
47    "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
48    "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
49    "@aws-cdk/aws-kms:aliasNameRef": true,
50    "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
51    "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
52    "@aws-cdk/aws-efs:denyAnonymousAccess": true,
53    "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
54    "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
55    "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
56    "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
57    "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
58    "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
59    "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
60    "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
61    "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
62    "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
63    "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
64    "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
65    "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
66    "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
67    "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
68    "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
69    "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
70    "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
71    "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
72    "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
73    "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
74    "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
75    "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
76    "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
77    "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
78    "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
79    "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
80    "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
81    "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
82    "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
83    "@aws-cdk/core:enableAdditionalMetadataCollection": true,
84    "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false,
85    "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
86    "@aws-cdk/aws-events:requireEventBusPolicySid": true,
87    "@aws-cdk/aws-dynamodb:retainTableReplica": true
88  }
89}
app.py
 1#!/usr/bin/env python3
 2
 3from cdk_mate.tests.stack_enum import stack_enum
 4
 5_ = stack_enum.stack1_dev
 6_ = stack_enum.stack1_test
 7_ = stack_enum.stack2_dev
 8_ = stack_enum.stack2_test
 9
10stack_enum.app.synth()
deploy.py
 1# -*- coding: utf-8 -*-
 2
 3import cdk_mate.api as cdk_mate
 4from cdk_mate.tests.stack_ctx_enum import stack_ctx_enum
 5
 6# --- 🗂️ group stacks by aws account / environment
 7# --- 🟢 creation order
 8create_dev_stack_ctx_list = [
 9    stack_ctx_enum.stack1_dev,
10    stack_ctx_enum.stack2_dev,
11]
12
13create_test_stack_ctx_list = [
14    stack_ctx_enum.stack1_test,
15    stack_ctx_enum.stack2_test,
16]
17
18# --- 🔴 deletion order
19delete_dev_stack_ctx_list = [
20    stack_ctx_enum.stack1_dev,
21    stack_ctx_enum.stack2_dev,
22]
23
24delete_test_stack_ctx_list = [
25    stack_ctx_enum.stack1_test,
26    stack_ctx_enum.stack2_test,
27]
28
29# --- 📦 cdk synth
30cdk_mate.cli.Synth().run(dir_cdk=__file__)
31
32# --- 🔍 cdk diff
33# cdk_mate.cdk_diff_many(create_dev_stack_ctx_list, dir_cdk=__file__)
34# cdk_mate.cdk_diff_many(create_test_stack_ctx_list, dir_cdk=__file__)
35
36# --- 🚀 cdk deploy
37cdk_mate.cdk_deploy_many(create_dev_stack_ctx_list, dir_cdk=__file__, prompt=False)
38cdk_mate.cdk_deploy_many(create_test_stack_ctx_list, dir_cdk=__file__, prompt=False)
39
40# --- 💥 cdk destroy
41cdk_mate.cdk_destroy_many(delete_dev_stack_ctx_list, dir_cdk=__file__, prompt=False)
42cdk_mate.cdk_destroy_many(delete_test_stack_ctx_list, dir_cdk=__file__, prompt=False)

Testing

The pattern includes a testing approach that synthesizes stacks without deployment (tests/test_stack_enum.py):

test_stack_enum.py
 1# -*- coding: utf-8 -*-
 2
 3import os
 4import pytest
 5
 6from cdk_mate.tests.mock_aws import BaseMockAwsTest
 7
 8
 9class TestStackEnum(BaseMockAwsTest):
10    def test_synth(self):
11        from cdk_mate.tests.stack_enum import stack_enum
12
13        _ = stack_enum.stack1_dev
14        _ = stack_enum.stack1_test
15        _ = stack_enum.stack2_dev
16        _ = stack_enum.stack2_test
17
18        stack_enum.app.synth()
19
20
21if __name__ == "__main__":
22    from cdk_mate.tests import run_cov_test
23
24    run_cov_test(
25        __file__,
26        "cdk_mate",
27        preview=False,
28    )

Usage Guide

Setting Up New Stacks

  1. Define Stack Implementation: Create a new module in the stacks (cdk_mate/tests/stacks) module to implement your CDK stack.

  2. Add Stack Context: Add new stack context entries to the stack_ctx_enum.py (cdk_mate/tests/stack_ctx_enum.py) module for each environment.

  3. Update Initialization: Add the new stack to the stack_enum.py module (cdk_mate/tests/stack_enum.py) module.

  4. Create Deployment Scripts: Add deployment scripts for the new stack in cdk.

Deployment Workflows

Individual Stack Development

First ensure that you enabled the stack you want to deploy in the xyz_app.py script like this:

stack1_app.py
1#!/usr/bin/env python3
2
3from cdk_mate.tests.stack_enum import stack_enum
4
5_ = stack_enum.stack1_dev
6_ = stack_enum.stack1_test
7
8stack_enum.app.synth()

When working on a specific stack, use the stack-specific deployment script xyz_deploy.py, comment in and out the right line like this:

stack1_deploy.py
 1# -*- coding: utf-8 -*-
 2
 3from cdk_mate.tests.stack_ctx_enum import stack_ctx_enum
 4
 5# --- 📦 cdk synth
 6# stack_ctx_enum.stack1_dev.cdk_synth(dir_cdk=__file__)
 7# stack_ctx_enum.stack1_test.cdk_synth(dir_cdk=__file__)
 8
 9# --- 🔍 cdk diff
10# stack_ctx_enum.stack1_dev.cdk_diff(dir_cdk=__file__)
11# stack_ctx_enum.stack1_test.cdk_diff(dir_cdk=__file__)
12
13# --- 🚀 cdk deploy
14# stack_ctx_enum.stack1_dev.cdk_deploy(dir_cdk=__file__, prompt=False)
15# stack_ctx_enum.stack1_test.cdk_deploy(dir_cdk=__file__, prompt=False)
16
17# --- 💥 cdk destroy
18# stack_ctx_enum.stack1_dev.cdk_destroy(dir_cdk=__file__, prompt=False)
19# stack_ctx_enum.stack1_test.cdk_destroy(dir_cdk=__file__, prompt=False)

Then run the deployment script from the command line:

# you don't have to cd to it
python /path/to/cdk/stack1/stack1_deploy.py

Environment Deployment

To deploy all stacks for a specific environment:

# you don't have to cd to it
python /path/to/cdk/deploy.py

Best Practices

  1. Keep Stack Implementation Clean: Focus on the CDK resources in stack implementation modules.

  2. Use Context Objects: Always use the context objects for stack configuration.

  3. Lazy Loading: Use lazy loading for AWS sessions to improve performance.

  4. Deployment Automation: Use the deployment utilities instead of raw CDK CLI commands.

  5. Testing: Always include tests that synthesize stacks before deployment.

Conclusion

The Multi-Stack, Multi-Environment CDK Deployment Pattern provides a scalable and flexible approach to managing complex infrastructure as code across multiple environments. By separating stack implementation from deployment configuration and providing reusable utilities, this pattern enables teams to efficiently manage infrastructure at scale.