1
1
import json
2
+ import logging
2
3
import sys
3
4
from abc import ABC , abstractmethod
4
5
from enum import Enum
18
19
19
20
PYTHON_RUNTIME_VERSION = f"V{ '' .join (map (str , sys .version_info [:2 ]))} "
20
21
22
+ logger = logging .getLogger (__name__ )
23
+
21
24
22
25
class BaseInfrastructureStack (ABC ):
23
26
@abstractmethod
@@ -36,19 +39,19 @@ class PythonVersion(Enum):
36
39
37
40
38
41
class BaseInfrastructureV2 (ABC ):
39
- STACKS_OUTPUT : dict = {}
40
-
41
42
def __init__ (self , feature_name : str , handlers_dir : Path , layer_arn : str = "" ) -> None :
42
43
self .feature_name = feature_name
43
44
self .stack_name = f"test-{ feature_name } -{ uuid4 ()} "
44
45
self .handlers_dir = handlers_dir
46
+ self .layer_arn = layer_arn
45
47
self .stack_outputs : Dict [str , str ] = {}
48
+
49
+ # NOTE: Investigate why cdk.Environment in Stack
50
+ # changes synthesized asset (no object_key in asset manifest)
46
51
self .app = App ()
47
- # NOTE: Investigate why env changes synthesized asset (no object_key in asset manifest)
48
52
self .stack = Stack (self .app , self .stack_name )
49
53
self .session = boto3 .Session ()
50
54
self .cfn : CloudFormationClient = self .session .client ("cloudformation" )
51
- self .layer_arn = layer_arn
52
55
53
56
# NOTE: CDK stack account and region are tokens, we need to resolve earlier
54
57
self .account_id = self .session .client ("sts" ).get_caller_identity ()["Account" ]
@@ -86,6 +89,7 @@ def create_lambda_functions(self, function_props: Optional[Dict] = None):
86
89
handlers = list (self .handlers_dir .rglob ("*.py" ))
87
90
source = Code .from_asset (f"{ self .handlers_dir } " )
88
91
props_override = function_props or {}
92
+ logger .debug (f"Creating functions for handlers: { handlers } " )
89
93
if not self .layer_arn :
90
94
raise ValueError (
91
95
"""Lambda Layer ARN cannot be empty when creating Lambda functions.
@@ -96,6 +100,8 @@ def create_lambda_functions(self, function_props: Optional[Dict] = None):
96
100
97
101
for fn in handlers :
98
102
fn_name = fn .stem
103
+ fn_name_pascal_case = fn_name .title ().replace ("_" , "" ) # basic_handler -> BasicHandler
104
+ logger .debug (f"Creating function: { fn_name_pascal_case } " )
99
105
function_settings = {
100
106
"id" : f"{ fn_name } -lambda" ,
101
107
"code" : source ,
@@ -116,9 +122,8 @@ def create_lambda_functions(self, function_props: Optional[Dict] = None):
116
122
removal_policy = RemovalPolicy .DESTROY ,
117
123
)
118
124
119
- # CFN Outputs only support hyphen
120
- fn_name_pascal_case = fn_name .title ().replace ("_" , "" ) # basic_handler -> BasicHandler
121
- self ._add_resource_output (
125
+ # CFN Outputs only support hyphen hence pascal case
126
+ self .add_cfn_output (
122
127
name = fn_name_pascal_case , value = function_python .function_name , arn = function_python .function_arn
123
128
)
124
129
@@ -133,14 +138,12 @@ def deploy(self) -> Dict[str, str]:
133
138
template , asset_manifest_file = self ._synthesize ()
134
139
assets = Assets (asset_manifest = asset_manifest_file , account_id = self .account_id , region = self .region )
135
140
assets .upload ()
136
- outputs = self ._deploy_stack (self .stack_name , template )
137
- # NOTE: hydrate map of stack resolved outputs for future access
138
- self .STACKS_OUTPUT [self .feature_name ] = outputs
139
-
140
- return outputs
141
+ self .stack_outputs = self ._deploy_stack (self .stack_name , template )
142
+ return self .stack_outputs
141
143
142
144
def delete (self ) -> None :
143
145
"""Delete CloudFormation Stack"""
146
+ logger .debug (f"Deleting stack: { self .stack_name } " )
144
147
self .cfn .delete_stack (StackName = self .stack_name )
145
148
146
149
@abstractmethod
@@ -157,7 +160,7 @@ def created_resources(self):
157
160
s3 = s3.Bucket(self.stack, "MyBucket")
158
161
159
162
# This will create MyBucket and MyBucketArn CloudFormation Output
160
- self._add_resource_output (name="MyBucket", value=s3.bucket_name, arn_value=bucket.bucket_arn)
163
+ self.add_cfn_output (name="MyBucket", value=s3.bucket_name, arn_value=bucket.bucket_arn)
161
164
```
162
165
163
166
Creating Lambda functions available in the handlers directory
@@ -170,7 +173,9 @@ def created_resources(self):
170
173
...
171
174
172
175
def _synthesize (self ) -> Tuple [Dict , Path ]:
176
+ logger .debug ("Creating CDK Stack resources" )
173
177
self .create_resources ()
178
+ logger .debug ("Synthesizing CDK Stack into raw CloudFormation template" )
174
179
cloud_assembly = self .app .synth ()
175
180
cf_template : Dict = cloud_assembly .get_stack_by_name (self .stack_name ).template
176
181
cloud_assembly_assets_manifest_path : str = (
@@ -179,6 +184,7 @@ def _synthesize(self) -> Tuple[Dict, Path]:
179
184
return cf_template , Path (cloud_assembly_assets_manifest_path )
180
185
181
186
def _deploy_stack (self , stack_name : str , template : Dict ) -> Dict [str , str ]:
187
+ logger .debug (f"Creating CloudFormation Stack: { stack_name } " )
182
188
self .cfn .create_stack (
183
189
StackName = stack_name ,
184
190
TemplateBody = yaml .dump (template ),
@@ -191,16 +197,10 @@ def _deploy_stack(self, stack_name: str, template: Dict) -> Dict[str, str]:
191
197
192
198
stack_details = self .cfn .describe_stacks (StackName = stack_name )
193
199
stack_outputs = stack_details ["Stacks" ][0 ]["Outputs" ]
194
- self .stack_outputs = {
195
- output ["OutputKey" ]: output ["OutputValue" ] for output in stack_outputs if output ["OutputKey" ]
196
- }
197
-
198
- return self .stack_outputs
199
-
200
- def _add_resource_output (self , name : str , value : str , arn : str ):
201
- """Add both resource value and ARN as Outputs to facilitate tests.
200
+ return {output ["OutputKey" ]: output ["OutputValue" ] for output in stack_outputs if output ["OutputKey" ]}
202
201
203
- This will create two outputs: {Name} and {Name}Arn
202
+ def add_cfn_output (self , name : str , value : str , arn : str = "" ):
203
+ """Create {Name} and optionally {Name}Arn CloudFormation Outputs.
204
204
205
205
Parameters
206
206
----------
@@ -212,7 +212,8 @@ def _add_resource_output(self, name: str, value: str, arn: str):
212
212
CloudFormation Output Value for ARN
213
213
"""
214
214
CfnOutput (self .stack , f"{ name } " , value = value )
215
- CfnOutput (self .stack , f"{ name } Arn" , value = arn )
215
+ if arn :
216
+ CfnOutput (self .stack , f"{ name } Arn" , value = arn )
216
217
217
218
218
219
def deploy_once (
@@ -241,13 +242,7 @@ def deploy_once(
241
242
Generator[Dict[str, str], None, None]
242
243
stack CloudFormation outputs
243
244
"""
244
- try :
245
- handlers_dir = f"{ request .path .parent } /handlers"
246
- except AttributeError :
247
- # session fixture has a slightly different object
248
- # luckily it only runs Lambda Layer Stack which doesn't deploy Lambda fns
249
- handlers_dir = f"{ request .node .path .parent } /handlers"
250
-
245
+ handlers_dir = f"{ request .node .path .parent } /handlers"
251
246
stack = stack (handlers_dir = Path (handlers_dir ), layer_arn = layer_arn )
252
247
253
248
try :
@@ -257,10 +252,6 @@ def deploy_once(
257
252
else :
258
253
# tmp dir shared by all workers
259
254
root_tmp_dir = tmp_path_factory .getbasetemp ().parent
260
-
261
- # cache and lock must be unique per stack
262
- # otherwise separate processes deploy the first stack collected only
263
- # since the original lock was based on parallel workers cache tmp dir
264
255
cache = root_tmp_dir / "cache.json"
265
256
266
257
with FileLock (f"{ cache } .lock" ):
@@ -288,6 +279,7 @@ def create_resources(self):
288
279
CfnOutput (self .stack , "LayerArn" , value = layer )
289
280
290
281
def _create_layer (self ) -> str :
282
+ logger .debug ("Creating Lambda Layer with latest source code available" )
291
283
output_dir = Path (str (AssetStaging .BUNDLING_OUTPUT_DIR ), "python" )
292
284
input_dir = Path (str (AssetStaging .BUNDLING_INPUT_DIR ), "aws_lambda_powertools" )
293
285
build_commands = [f"pip install . -t { output_dir } " , f"cp -R { input_dir } { output_dir } " ]
0 commit comments