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:
Using lightweight context objects to reference stack configurations without loading full CDK code
Implementing lazy loading for AWS session credentials
Separating stack definition from stack instantiation
Providing reusable deployment utilities at both individual and group levels
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¶
Stack Implementation: Each stack is implemented in a dedicated module using a mixin pattern to manage complexity
BSM Enum: Defines AWS session configurations for different environments
Stack Context Enum: Defines metadata for each stack instance (stack name, AWS account, region)
IAC Initialization: Creates CDK stack instances using context objects
Deployment Scripts: Provides flexible deployment options
Implementation Details¶
Reference:
Stack definition source code: cdk_mate/tests
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¶
Define Stack Implementation: Create a new module in the stacks (cdk_mate/tests/stacks) module to implement your CDK stack.
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.
Update Initialization: Add the new stack to the stack_enum.py module (cdk_mate/tests/stack_enum.py) module.
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¶
Keep Stack Implementation Clean: Focus on the CDK resources in stack implementation modules.
Use Context Objects: Always use the context objects for stack configuration.
Lazy Loading: Use lazy loading for AWS sessions to improve performance.
Deployment Automation: Use the deployment utilities instead of raw CDK CLI commands.
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.