CloudFormation Macro - Example Random MD5 Macro#

lambda_function.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4这个 Lambda Function 实现了一个 AWS CloudFormation Macro, 用来生成一个随机的 MD5 字符串.
 5"""
 6
 7import typing as T
 8import uuid
 9import traceback
10import dataclasses
11
12T_DATA = T.Dict[str, T.Any]
13
14
15@dataclasses.dataclass
16class Base:
17    @classmethod
18    def from_dict(cls, dct: T_DATA):
19        kwargs = {}
20        for field in dataclasses.fields(cls):
21            if field.name in dct:
22                kwargs[field.name] = dct[field.name]
23        return cls(**kwargs)
24
25    def to_dict(self) -> T_DATA:
26        return dataclasses.asdict(self)
27
28
29@dataclasses.dataclass
30class Request(Base):
31    region: str = dataclasses.field(default=None)
32    accountId: str = dataclasses.field(default=None)
33    fragment: dict = dataclasses.field(default=None)
34    transformId: str = dataclasses.field(default=None)
35    params: str = dataclasses.field(default=None)
36    requestId: str = dataclasses.field(default=None)
37    templateParameterValues: dict = dataclasses.field(default=None)
38
39
40SUCCESS = "success"
41
42
43@dataclasses.dataclass
44class Response(Base):
45    requestId: str = dataclasses.field(default=None)
46    status: str = dataclasses.field(default=None)
47    fragment: dict = dataclasses.field(default=None)
48    errorMessage: str = dataclasses.field(default=None)
49
50
51def lambda_handler(event, context):
52    print("========== Start CloudFormation Macro Lambda Function ==========")
53    print("event: " + str(event))
54
55    request = Request.from_dict(event)
56    try:
57        response = process(request)
58    except Exception as e:
59        traceback.print_exc()
60        response = Response(
61            requestId=request.requestId,
62            status="failure",
63            errorMessage=str(e),
64        )
65
66    return response.to_dict()
67
68
69def process(request: Request) -> Response:
70    return Response(
71        requestId=request.requestId,
72        status=SUCCESS,
73        # --- 方法 1. 返回一个简单的字符串本身
74        # fragment=uuid.uuid4().hex,
75        # --- 方法 2. 返回一个 CloudFormation JSON 中的一个 object 对象, 这个例子中是一个简单的
76        # intrinsic function Fn::Sub, 只是用来演示. 实际上你可以返回任何复杂的 JSON 对象.
77        # 甚至定义一个 Resource, 或是 nested stack 都可以
78        fragment={
79            "Fn::Sub": [
80                "${value}",
81                {"value": uuid.uuid4().hex},
82            ],
83        },
84    )
s1_deploy_macro.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4在这个 CloudFormation Template 中, 我们定义了一个 CloudFormation Macro 和其所需的
 5Lambda Function, 其功能是生成一个随机的 MD5 字符串.
 6"""
 7
 8import json
 9from shared import dir_here, bsm, s3dir
10import aws_cloudformation as aws_cf
11
12# ------------------------------------------------------------------------------
13# Put your settings here
14# ------------------------------------------------------------------------------
15iam_role_arn = "arn:aws:iam::{aws_account_id}:role/lambda-power-user-role"
16name_prefix = "cfn_macro_poc_01_deploy_macro"
17
18# ------------------------------------------------------------------------------
19# Don't change the code below
20# ------------------------------------------------------------------------------
21iam_role_arn = iam_role_arn.format(aws_account_id=bsm.aws_account_id)
22
23path_lbd_func = dir_here / "lambda_function.py"
24path_lbd_deploy_package = dir_here / "lambda.zip"
25path_lbd_func.make_zip_archive(path_lbd_deploy_package, overwrite=True)
26
27s3path_lbd_deploy_package = s3dir / f"{path_lbd_func.md5}.zip"
28s3path_lbd_deploy_package.upload_file(path_lbd_deploy_package, overwrite=True)
29
30
31template = {
32    "AWSTemplateFormatVersion": "2010-09-09",
33    "Resources": {
34        "CfnMacroMd5LambdaFunction": {
35            "Type": "AWS::Lambda::Function",
36            "Properties": {
37                "FunctionName": "cfn_macro-md5",
38                "Handler": "lambda_function.lambda_handler",
39                "Role": iam_role_arn,
40                "Code": {
41                    "S3Bucket": s3path_lbd_deploy_package.bucket,
42                    "S3Key": s3path_lbd_deploy_package.key,
43                },
44                "Runtime": "python3.11",
45                "MemorySize": 128,
46                "Timeout": 30,
47                "Environment": {
48                    "Variables": {"CODE_MD5": str(hash(path_lbd_func.read_text()))}
49                },
50            },
51        },
52        "CfnMacroMd5": {
53            "Type": "AWS::CloudFormation::Macro",
54            "Properties": {
55                "Name": "Md5",
56                "Description": "Generate random md5",
57                "FunctionName": {"Fn::GetAtt": ["CfnMacroMd5LambdaFunction", "Arn"]},
58            },
59        },
60    },
61}
62stack_name = name_prefix.replace("_", "-")
63
64
65def deploy_stack():
66    aws_cf.deploy_stack(
67        bsm=bsm,
68        stack_name=stack_name,
69        template=json.dumps(template),
70        include_named_iam=True,
71        skip_prompt=True,
72    )
73
74
75def delete_stack():
76    aws_cf.remove_stack(
77        bsm=bsm,
78        stack_name=stack_name,
79        skip_prompt=True,
80    )
81
82
83if __name__ == "__main__":
84    # deploy_stack()
85    delete_stack()
86    # pass
s2_test_macro.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4测试 s1_deploy_macro.py 中部署的 Macro.
 5"""
 6
 7import json
 8from shared import bsm
 9import aws_cloudformation as aws_cf
10
11# ------------------------------------------------------------------------------
12# Put your settings here
13# ------------------------------------------------------------------------------
14name_prefix = "cfn_macro_poc_01_test_macro"
15
16# ------------------------------------------------------------------------------
17# Don't change the code below
18# ------------------------------------------------------------------------------
19template = {
20    "AWSTemplateFormatVersion": "2010-09-09",
21    "Resources": {
22        "IamGroup": {
23            "Type": "AWS::IAM::Group",
24            "Properties": {
25                "GroupName": {
26                    "Fn::Transform": {
27                        # this value has to match the value at
28                        # s1_deploy_macro.py -> CfnMacroMd5 -> Properties -> Name
29                        "Name": "Md5",
30                        "Parameters": {},
31                    }
32                },
33            },
34        },
35    },
36}
37stack_name = name_prefix.replace("_", "-")
38
39
40def deploy_stack():
41    aws_cf.deploy_stack(
42        bsm=bsm,
43        stack_name=stack_name,
44        template=json.dumps(template),
45        include_named_iam=True,
46        skip_prompt=True,
47    )
48
49
50def delete_stack():
51    aws_cf.remove_stack(
52        bsm=bsm,
53        stack_name=stack_name,
54        skip_prompt=True,
55    )
56
57
58if __name__ == "__main__":
59    # deploy_stack()
60    delete_stack()
61    # pass
s3_try_def_and_use_in_one_stack.py
  1# -*- coding: utf-8 -*-
  2
  3"""
  4有了 deploy_macro.py 和 test_macro.py 两个成功例子, 我们来尝试将 Macro 的定义和 Macro
  5的使用放在一个 CloudFormation 中可不可以 (**结论, 不可以直接放**). 在 AWS 官方文档中我们已经
  6知道可以将 Macro 的定义和 Macro 的使用分别放在两个不同的 CloudFormation Stack 中,
  7然后使用 Nested Stack 的方式将两个 Stack 放在一个 Stack 中, 并且定义依赖关系, 这个是确定
  8可行的. 但我们还是想要动手尝试一下, 看看可不可以直接放在一个 CloudFormation 中.
  9"""
 10
 11import json
 12from shared import dir_here, bsm, s3dir
 13import aws_cloudformation as aws_cf
 14
 15# ------------------------------------------------------------------------------
 16# Put your settings here
 17# ------------------------------------------------------------------------------
 18iam_role_arn = "arn:aws:iam::{aws_account_id}:role/lambda-power-user-role"
 19name_prefix = "cfn_macro_poc_01_try_def_and_use_in_one_stack"
 20
 21# ------------------------------------------------------------------------------
 22# Don't change the code below
 23# ------------------------------------------------------------------------------
 24iam_role_arn = iam_role_arn.format(aws_account_id=bsm.aws_account_id)
 25
 26path_lbd_func = dir_here / "lambda_function.py"
 27path_lbd_deploy_package = dir_here / "lambda.zip"
 28path_lbd_func.make_zip_archive(path_lbd_deploy_package, overwrite=True)
 29
 30s3path_lbd_deploy_package = s3dir / f"{path_lbd_func.md5}.zip"
 31s3path_lbd_deploy_package.upload_file(path_lbd_deploy_package, overwrite=True)
 32
 33
 34template = {
 35    "AWSTemplateFormatVersion": "2010-09-09",
 36    "Resources": {
 37        "CfnMacroMd5LambdaFunction": {
 38            "Type": "AWS::Lambda::Function",
 39            "Properties": {
 40                "FunctionName": "cfn_macro-md5",
 41                "Handler": "lambda_function.lambda_handler",
 42                "Role": iam_role_arn,
 43                "Code": {
 44                    "S3Bucket": s3path_lbd_deploy_package.bucket,
 45                    "S3Key": s3path_lbd_deploy_package.key,
 46                },
 47                "Runtime": "python3.11",
 48                "MemorySize": 128,
 49                "Timeout": 30,
 50                "Environment": {
 51                    "Variables": {"CODE_MD5": str(hash(path_lbd_func.read_text()))}
 52                },
 53            },
 54        },
 55        "CfnMacroMd5": {
 56            "Type": "AWS::CloudFormation::Macro",
 57            "Properties": {
 58                "Name": "Md5",
 59                "Description": "Generate random md5",
 60                "FunctionName": {
 61                    "Fn::GetAtt": ["CfnMacroMd5LambdaFunction", "Arn"],
 62                },
 63            },
 64        },
 65        "IamGroup": {
 66            "Type": "AWS::IAM::Group",
 67            "Properties": {
 68                "GroupName": {
 69                    "Fn::Transform": {
 70                        "Name": "Md5",
 71                        "Parameters": {},
 72                    }
 73                },
 74            },
 75            "DependsOn": "CfnMacroMd5",
 76        },
 77    },
 78}
 79stack_name = name_prefix.replace("_", "-")
 80
 81
 82def deploy_stack():
 83    aws_cf.deploy_stack(
 84        bsm=bsm,
 85        stack_name=stack_name,
 86        template=json.dumps(template),
 87        include_named_iam=True,
 88        skip_prompt=True,
 89    )
 90
 91
 92def delete_stack():
 93    aws_cf.remove_stack(
 94        bsm=bsm,
 95        stack_name=stack_name,
 96        skip_prompt=True,
 97    )
 98
 99
100if __name__ == "__main__":
101    deploy_stack()
102    # delete_stack()
103    # pass
s4_try_def_and_use_in_one_stack_the_right_way.py
  1# -*- coding: utf-8 -*-
  2
  3"""
  4有了 deploy_macro.py 和 test_macro.py 两个成功例子, 我们来尝试将 Macro 的定义和 Macro
  5的使用放在一个 CloudFormation 中可不可以 (**结论, 不可以直接放**). 在 AWS 官方文档中我们已经
  6知道可以将 Macro 的定义和 Macro 的使用分别放在两个不同的 CloudFormation Stack 中,
  7然后使用 Nested Stack 的方式将两个 Stack 放在一个 Stack 中, 并且定义依赖关系, 这个是确定
  8可行的. 但我们还是想要动手尝试一下, 看看可不可以直接放在一个 CloudFormation 中.
  9"""
 10
 11import json
 12from shared import dir_here, bsm, s3dir
 13import aws_cloudformation as aws_cf
 14
 15# ------------------------------------------------------------------------------
 16# Put your settings here
 17# ------------------------------------------------------------------------------
 18iam_role_arn = "arn:aws:iam::{aws_account_id}:role/lambda-power-user-role"
 19name_prefix = "cfn_macro_poc_01_one_stack_two_nested_stack"
 20
 21# ------------------------------------------------------------------------------
 22# Don't change the code below
 23# ------------------------------------------------------------------------------
 24iam_role_arn = iam_role_arn.format(aws_account_id=bsm.aws_account_id)
 25
 26path_lbd_func = dir_here / "lambda_function.py"
 27path_lbd_deploy_package = dir_here / "lambda.zip"
 28path_lbd_func.make_zip_archive(path_lbd_deploy_package, overwrite=True)
 29
 30s3path_lbd_deploy_package = s3dir / f"{path_lbd_func.md5}.zip"
 31s3path_lbd_deploy_package.upload_file(path_lbd_deploy_package, overwrite=True)
 32
 33macro_template = {
 34    "AWSTemplateFormatVersion": "2010-09-09",
 35    "Resources": {
 36        "CfnMacroMd5LambdaFunction": {
 37            "Type": "AWS::Lambda::Function",
 38            "Metadata": {
 39                "Description": f"This Lambda Function allow CloudFormation to generate random md5 string, You can exam the source code at {s3path_lbd_deploy_package.get_regional_console_url(bsm.aws_region)}",
 40                "PreviewLambdaFunctionSourceCode": s3path_lbd_deploy_package.get_regional_console_url(bsm.aws_region),
 41            },
 42            "Properties": {
 43                "FunctionName": "cfn_macro-md5",
 44                "Handler": "lambda_function.lambda_handler",
 45                "Role": iam_role_arn,
 46                "Code": {
 47                    "S3Bucket": s3path_lbd_deploy_package.bucket,
 48                    "S3Key": s3path_lbd_deploy_package.key,
 49                },
 50                "Runtime": "python3.11",
 51                "MemorySize": 128,
 52                "Timeout": 30,
 53                "Environment": {
 54                    "Variables": {"CODE_MD5": str(hash(path_lbd_func.read_text()))}
 55                },
 56            },
 57        },
 58        "CfnMacroMd5": {
 59            "Type": "AWS::CloudFormation::Macro",
 60            "Properties": {
 61                "Name": "Md5",
 62                "Description": "Generate random md5",
 63                "FunctionName": {
 64                    "Fn::GetAtt": ["CfnMacroMd5LambdaFunction", "Arn"],
 65                },
 66            },
 67        },
 68    },
 69}
 70test_template = {
 71    "AWSTemplateFormatVersion": "2010-09-09",
 72    "Resources": {
 73        "IamGroup": {
 74            "Type": "AWS::IAM::Group",
 75            "Properties": {
 76                "GroupName": {
 77                    "Fn::Transform": {
 78                        "Name": "Md5",
 79                        "Parameters": {},
 80                    }
 81                },
 82            },
 83        },
 84    },
 85}
 86
 87macro_template_json = json.dumps(macro_template)
 88s3path_macro_template = s3dir / f"{abs(hash(macro_template_json))}_macro.json"
 89s3path_macro_template.write_text(macro_template_json, content_type="application/json")
 90
 91test_template_json = json.dumps(test_template)
 92s3path_test_template = s3dir / f"{abs(hash(test_template_json))}_macro.json"
 93s3path_test_template.write_text(test_template_json, content_type="application/json")
 94
 95
 96def get_object_url(s3path) -> str:
 97    return f"https://{s3path.bucket}.s3.amazonaws.com/{s3path.key}"
 98
 99
100master_template = {
101    "AWSTemplateFormatVersion": "2010-09-09",
102    "Resources": {
103        # NOTE, you have to deploy MacroTemplate first in one API run
104        # then you can deploy TestTemplate in another API run
105        # You CANNOT deploy both in one API run, even with "DependsOn".
106        "MacroTemplate": {
107            "Type": "AWS::CloudFormation::Stack",
108            "Properties": {
109                "TemplateURL": get_object_url(s3path_macro_template),
110            },
111        },
112        "TestTemplate": {
113            "Type": "AWS::CloudFormation::Stack",
114            "Properties": {
115                "TemplateURL": get_object_url(s3path_test_template),
116            },
117            "DependsOn": "MacroTemplate",
118        },
119    },
120}
121stack_name = name_prefix.replace("_", "-")
122
123
124def deploy_stack():
125    aws_cf.deploy_stack(
126        bsm=bsm,
127        stack_name=stack_name,
128        template=json.dumps(master_template),
129        include_named_iam=True,
130        include_macro=True,
131        skip_prompt=True,
132    )
133
134
135def delete_stack():
136    aws_cf.remove_stack(
137        bsm=bsm,
138        stack_name=stack_name,
139        skip_prompt=True,
140    )
141
142
143if __name__ == "__main__":
144    deploy_stack()
145    # delete_stack()
146    # pass