如何通过CDK Pipeline无冗余构建部署S3静态网站至多环境
优化AWS CDK Pipeline多环境静态网站部署方案
现有实现方案
1. 多环境重复构建
在Pipeline的Synth阶段,为每个环境单独设置环境变量并执行构建:
from aws_cdk.pipelines import ShellStep pipeline = CodePipeline(self, "Pipeline", synth=ShellStep("Synth", input=cdk_source, commands=[ # 测试环境构建 "export REACT_APP_BACKEND_URL=https://api.test.example.com", "npm install", "npm run build", "cp -r build ../test-build", # 预发布环境构建 "export REACT_APP_BACKEND_URL=https://api.stage.example.com", "npm run build", "cp -r build ../stage-build", # 生产环境构建 "export REACT_APP_BACKEND_URL=https://api.prod.example.com", "npm run build", "cp -r build ../prod-build", ] ) )
2. 分环境S3部署
通过自定义Stage类,将对应环境的构建产物部署到专属S3存储桶:
from aws_cdk import aws_s3 as s3, aws_s3_deployment as s3deploy class MVPPipelineStage(cdk.Stage): def __init__( self, scope: Construct, construct_id: str, stage_name: str, **kwargs ) -> None: super().__init__(scope, construct_id, **kwargs) build_path = f"../{stage_name}-build" website_bucket = s3.Bucket( self, f"WebsiteBucket-{stage_name}", public_read_access=True ) s3deploy.BucketDeployment( self, f"DeployWebsite-{stage_name}", sources=[s3deploy.Source.asset(build_path)], destination_bucket=website_bucket, )
现存问题
当前方案需要为每个环境重复执行完整的构建流程,导致冗余操作,显著增加了Pipeline的整体运行时长。
需求目标
- 仅执行一次前端构建,生成通用构建产物
- 将同一构建产物部署至多环境(测试、预发布、生产)
- 在部署阶段或运行时动态注入环境专属配置(如后端API URL)
优化方案与最佳实践
方案一:部署时动态替换环境配置
核心思路:构建一次通用产物(包含配置占位符),在部署每个环境时,通过脚本替换占位符为对应环境的实际值。
步骤1:调整前端构建逻辑
在前端项目的public目录下创建config.json,使用占位符标记需要动态替换的配置:
{ "backendUrl": "{{BACKEND_URL}}" }
修改前端代码,从该配置文件读取环境变量:
// 前端初始化逻辑 fetch('/config.json') .then(res => res.json()) .then(config => { // 使用config.backendUrl作为后端API地址 window.APP_CONFIG = config; });
步骤2:修改Pipeline的Synth阶段
仅执行一次构建,保存通用构建产物:
pipeline = CodePipeline(self, "Pipeline", synth=ShellStep("Synth", input=cdk_source, commands=[ "npm install", "npm run build", # 保存通用构建产物 "cp -r build ../shared-build", # 执行CDK synth生成部署模板 "npm run cdk synth" ] ) )
步骤3:修改Stage类,添加部署时配置替换
在每个环境的部署阶段,通过Shell脚本替换config.json中的占位符,再同步到S3:
from aws_cdk import iam class MVPPipelineStage(cdk.Stage): def __init__( self, scope: Construct, construct_id: str, stage_name: str, backend_url: str, **kwargs ) -> None: super().__init__(scope, construct_id, **kwargs) website_bucket = s3.Bucket( self, f"WebsiteBucket-{stage_name}", public_read_access=True ) # 自定义部署步骤:复制通用产物并替换配置 deploy_config_step = ShellStep(f"DeployAndConfigure-{stage_name}", commands=[ # 复制通用构建产物到临时目录 "cp -r ../shared-build ./temp-build", # 替换占位符为当前环境的API地址 f'sed -i "s|{{{{BACKEND_URL}}}}|{backend_url}|g" ./temp-build/config.json', # 同步到目标S3桶 f"aws s3 sync ./temp-build s3://{website_bucket.bucket_name}" ], # 授予S3操作权限 role_policy_statements=[ iam.PolicyStatement( actions=["s3:*"], resources=[website_bucket.bucket_arn, f"{website_bucket.bucket_arn}/*"] ) ] ) # 将部署步骤添加到流水线的Stage中 pipeline.add_stage(self, post=[deploy_config_step])
方案二:运行时自动识别环境配置
核心思路:前端根据当前访问域名自动推断环境,无需构建或部署阶段的额外操作。
修改前端代码,通过域名判断后端API地址:
const getBackendUrl = () => { const hostname = window.location.hostname; if (hostname.includes('test')) return 'https://api.test.example.com'; if (hostname.includes('stage')) return 'https://api.stage.example.com'; if (hostname.includes('prod')) return 'https://api.prod.example.com'; // 本地开发默认地址 return 'http://localhost:3001'; }; // 初始化应用时使用该地址 window.APP_CONFIG = { backendUrl: getBackendUrl() };
这种方案无需修改CDK Pipeline,只需一次构建即可部署到所有环境,但缺点是API地址变更时需要重新构建前端,灵活性稍弱。
方案三:S3对象覆盖实现环境配置
核心思路:先部署通用构建产物,再单独上传对应环境的config.json覆盖S3中的默认文件。
修改Stage类:
import tempfile import json class MVPPipelineStage(cdk.Stage): def __init__( self, scope: Construct, construct_id: str, stage_name: str, backend_url: str, **kwargs ) -> None: super().__init__(scope, construct_id, **kwargs) website_bucket = s3.Bucket( self, f"WebsiteBucket-{stage_name}", public_read_access=True ) # 第一步:部署通用构建产物 s3deploy.BucketDeployment( self, f"DeployBaseAssets-{stage_name}", sources=[s3deploy.Source.asset("../shared-build")], destination_bucket=website_bucket, ) # 第二步:生成并上传环境专属config.json with tempfile.TemporaryDirectory() as tmp_dir: config_file_path = f"{tmp_dir}/config.json" with open(config_file_path, 'w') as f: json.dump({"backendUrl": backend_url}, f) s3deploy.BucketDeployment( self, f"DeployEnvConfig-{stage_name}", sources=[s3deploy.Source.asset(tmp_dir)], destination_bucket=website_bucket, # 确保覆盖根目录的config.json destination_key_prefix="/" )
最佳实践总结
推荐使用方案一(部署时动态替换配置),它兼顾了构建效率和配置灵活性:
- 仅需一次前端构建,大幅缩短Pipeline运行时间
- 环境配置与代码解耦,修改API地址无需重新构建前端
- 部署阶段的替换操作轻量且可控
内容的提问来源于stack exchange,提问作者Codeformer
相关产品推荐
相关产品推荐

