Deploy CDK Stack To Multiple Account#
Keywords: Amazon, AWS, Cloud Development, Kit, CDK
Overview#
在使用 AWS CDK 进行基础设施即代码部署时, 我们经常会遇到这样一个场景: 我们有一个 CloudFormation Stack 的定义, 需要将它部署到多个不同的 AWS 账户中. 每个账户中的 Stack 可能会有不同的参数配置, 需要使用不同的 AWS Credentials. 那么, 如何才能优雅地实现只定义一次 Stack, 并用一个 Python 脚本就能管理多个账户的部署呢?本文将为您详细介绍这个解决方案.
核心挑战#
在实现这个需求的过程中, 我们面临着两个主要的技术挑战:
1. CDK 工具的限制#
虽然 CDK 支持使用 Python 编写基础设施代码, 但它本身是基于 TypeScript 开发的工具. 它并不提供可以在 Python 代码中直接调用的部署接口. 有人可能会想到先用 CDK 生成 CloudFormation 模板, 然后用其他工具部署, 但这种方式是有风险的. 因为 CDK CLI 在部署过程中会执行许多重要的步骤, 包括:
完整性检查
依赖关系验证
Artifacts 打包
其他安全性校验
如果绕过 CDK CLI 直接部署 CloudFormation 模板, 就会失去这些重要的保障.
2. 参数传递和 Boto Session 管理的挑战#
CDK 命令行工具存在两个主要限制:
CDK CLI 不支持向 Python 的
app.py
文件传递参数动态修改
app.py
的内容是不安全的实践
此外, 在处理 AWS 会话时也存在挑战:
CDK 支持通过 profile 指定权限
但当使用临时 credentials (如 assume role 获取的 token) 时, 无法预先配置到 AWS CLI 配置文件中
解决方案#
针对以上挑战, 我们设计了一个优雅的解决方案:
1. 参数传递机制#
在 lib.py
中, 我们实现了一个参数传递机制:
lib.py
1# -*- coding: utf-8 -*-
2
3import typing as T
4import json
5import subprocess
6from pathlib_mate import Path
7from boto_session_manager import BotoSesManager
8
9dir_here = Path.dir_here(__file__)
10path_params = dir_here / "params.json"
11
12
13def deploy_cdk(
14 params: dict[str, T.Any],
15 bsm: BotoSesManager,
16):
17 print(f"Deploying CDK stack to {bsm.aws_account_id} {bsm.aws_region} ...")
18 path_params.write_text(json.dumps(params))
19 args = [
20 "cdk",
21 "deploy",
22 "--all",
23 "--require-approval",
24 "never",
25 ]
26 with dir_here.temp_cwd():
27 with bsm.awscli():
28 subprocess.run(args)
29
30
31def destroy_cdk(
32 params: dict[str, T.Any],
33 bsm: BotoSesManager,
34):
35 print(f"Delete CDK stack from {bsm.aws_account_id} {bsm.aws_region} ...")
36 path_params.write_text(json.dumps(params))
37 args = [
38 "cdk",
39 "destroy",
40 "--all",
41 "--force",
42 ]
43 with dir_here.temp_cwd():
44 with bsm.awscli():
45 subprocess.run(args)
46
47
48params_dev = {"acc_name": "app_dev"}
49bsm_dev = BotoSesManager(profile_name="bmt_app_dev_us_east_1")
50
51params_test = {"acc_name": "app_test"}
52bsm_test = BotoSesManager(profile_name="bmt_app_test_us_east_1")
部署前将特定账户的参数序列化为 JSON 文件
在
app.py
中读取该 JSON 文件使用参数创建对应的 Stack 实例
为了确保参数的时效性, 我们在 app.py
中增加了文件时间戳检查机制:
app.py
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4# ------------------------------------------------------------------------------
5# Read parameters
6# ------------------------------------------------------------------------------
7import json
8from pathlib import Path
9from datetime import datetime
10
11path_params = Path(__file__).absolute().parent.joinpath("params.json")
12# Ensure that this path_params file is created recently
13ts_mtime = path_params.stat().st_mtime_ns / 1000000000
14ts_now = datetime.now().timestamp()
15elapsed = ts_now - ts_mtime
16if elapsed > 3:
17 raise SystemExit("The params.json file is too old.")
18params = json.loads(path_params.read_text())
19
20# ------------------------------------------------------------------------------
21# Synth the Stack
22# ------------------------------------------------------------------------------
23import aws_cdk as cdk
24import aws_cdk.aws_iam as iam
25from constructs import Construct
26
27
28class Stack(cdk.Stack):
29 def __init__(
30 self,
31 scope: Construct,
32 construct_id: str,
33 acc_name: str,
34 **kwargs,
35 ) -> None:
36 super().__init__(scope, construct_id, **kwargs)
37
38 iam.CfnGroup(
39 self,
40 "IamGroup",
41 group_name=f"deploy-cdk-stack-to-multiple-account-test-stack-{acc_name}",
42 )
43
44
45app = cdk.App()
46
47# You can comment them in and out to deploy part of the stacks
48Stack(
49 app,
50 "deploy-cdk-stack-to-multiple-account-test-stack",
51 acc_name=params["acc_name"],
52)
53
54# create CloudFormation template
55app.synth()
ts_mtime = path_params.stat().st_mtime_ns / 1000000000
ts_now = datetime.now().timestamp()
elapsed = ts_now - ts_mtime
if elapsed > 3:
raise SystemExit("The params.json file is too old.")
这样就能确保每次部署都使用最新的参数文件.
2. AWS 会话管理#
我们使用了 boto_session_manager
库来解决会话管理的问题:
with bsm.awscli():
subprocess.run(args)
这个方案的优点在于:
支持 in-memory 的临时权限管理
可以使用 assume role 或者 profile
Context Manager 自动管理环境变量的生命周期
确保 CDK CLI 使用正确的权限
实际使用#
有了这些基础设施, 我们就可以像 run_cdk_deploy.py
和 run_cdk_destroy.py
中展示的那样:
run_cdk_deploy.py
1# -*- coding: utf-8 -*-
2
3from lib import params_dev, bsm_dev, params_test, bsm_test, deploy_cdk
4
5deploy_cdk(params_dev, bsm_dev)
6deploy_cdk(params_test, bsm_test)
run_cdk_destroy.py
1# -*- coding: utf-8 -*-
2
3from lib import params_dev, bsm_dev, params_test, bsm_test, destroy_cdk
4
5destroy_cdk(params_dev, bsm_dev)
6destroy_cdk(params_test, bsm_test)
可以针对单个账户进行部署
也可以批量部署到多个账户
支持灵活的部署策略
这样的设计既保证了部署的安全性, 又提供了足够的灵活性.
总结#
通过这个解决方案, 我们成功解决了 CDK 多账户部署的几个关键问题:
参数管理的安全性和时效性
AWS 权限的灵活管理
部署流程的可控性和可扩展性
这个方案既保持了 CDK 的优势, 又克服了它的局限性, 为多账户部署提供了一个优雅的解决方案.