从CodeBuildStep传变量至Stage栈时CDK流水线无限循环问题
问题背景
我基于aws-cdk-lib 2.99.1开发了一套基础CDK流水线,预期实现以下流程:
- 从pom文件提取制品版本
- 构建Lambda(输出为zip文件)
- 将制品部署至CodeArtifact
- 更新pom文件版本(尚未实现)
- 使用pom中的制品版本将zip文件部署为Lambda新版本
流水线实现结构
- CodeBuildStep的synth步骤:
- 导出
LAMBDA_SNAPSHOT_VERSION_ID - 执行
mvn install -Passembly-zip - 执行
mvn deploy:deploy-file将制品部署至快照仓库 - 进入
${CODEBUILD_SRC_DIR}/cdk目录 - 执行
cdk synth -c LAMBDA_SNAPSHOT_VERSION_ID=${LAMBDA_SNAPSHOT_VERSION_ID}
- 导出
- Stage阶段:
- dev-deploy(
LambdaPipelineStage继承自Stage)- LambdaStack:输入参数为
functionName、env、version(即LAMBDA_SNAPSHOT_VERSION_ID)
- LambdaStack:输入参数为
- dev-deploy(
LambdaStack会创建Lambda函数及版本,版本描述为LAMBDA_SNAPSHOT_VERSION_ID。单独执行cdk deploy无问题,但部署该流水线后,流水线陷入Source -> Build -> UpdatePipeline的无限循环;移除版本值传递代码后,流水线可正常执行:Source -> Build -> UpdatePipeline -> Assets -> lambda-dev-deploy(prepare->deploy)。
相关代码如下:
CodeBuildStep synthStep = new CodeBuildStep( codeBuildName, CodeBuildStepProps .builder() .projectName(codeBuildName) .cache(codebuildCache) .input( CodePipelineSource.codeCommit( codeCommitRepository, "master", CodeCommitSourceOptions .builder() .eventRole(codePipelineEventRole) .actionName("source-change") .build() ) ) .partialBuildSpec(getPartialBuildSpec()) .installCommands(getInstallCommands()) .commands(getBuildCommands()) .primaryOutputDirectory("${CODEBUILD_SRC_DIR}/cdk/cdk.out") .buildEnvironment(buildEnvironment) .actionRole(codePipelineRole) .role(codeBuildRole) .rolePolicyStatements(List.of(policyStatement)) .build() ); String codePipeline = "cdk-codepipeline-" + repoName; CodePipeline pipeline = new CodePipeline( this, codePipeline, CodePipelineProps.builder() .pipelineName(codePipeline) .selfMutation(Boolean.TRUE) .role(codePipelineRole) .synth(synthStep) .crossAccountKeys(Boolean.TRUE) .artifactBucket(getArtifactBucket()) .build() ); String functionName = "dev-" + repoName; pipeline.addStage( new LambdaPipelineStage( this, repoName + "-dev-deploy", StageProps .builder() .stageName(repoName + "-dev-deploy") .build(), functionName, Constants.DEVELOPMENT_ENV ) ); public class LambdaPipelineStage extends Stage { public LambdaPipelineStage(final Construct scope, final String id, final StageProps props, final String functionName, final String environment ) { super(scope, id, props); LambdaStack stack = new LambdaStack( this, functionName, StackProps .builder() .stackName(functionName) .build(), functionName, environment, getValueFromContext(scope, Constants.LAMBDA_SNAPSHOT_VERSION_ID) ); } private String getValueFromContext(final Construct scope, final String variableKey) { return (String) scope.getNode().tryGetContext(variableKey); } } public class LambdaStack extends Stack { public LambdaStack(final Construct scope, final String id, final StackProps props, final String functionName, final String environment, final String version) { super(scope, id, props); initStack(functionName, environment, version); } private void initStack(final String functionName, final String environment, final String version) { String artifactId = Constants.LAMBDA_ARTIFACT_ID_VALUE; String handler = Constants.LAMBDA_HANDLER; Code code = Code.fromAsset( "../lambda/target/" + artifactId + "-" + version + ".zip" ); IRole lambdaRole = getLambdaRole(); IFunction lambdaFunction = Function.Builder.create(this, functionName) .functionName(functionName) .runtime(Constants.LAMBDA_RUNTIME) .code(code) .handler(handler) .role(lambdaRole) .environment(getEnvironmentVariables(functionName, environment)) .memorySize(Constants.LAMBDA_MEMORY_SIZE) .architecture(Constants.LAMBDA_ARCHITECTURE) .ephemeralStorageSize(Size.mebibytes(Constants.LAMBDA_MEMORY_SIZE)) .timeout(Duration.seconds(Constants.LAMBDA_TIMEOUT_SECONDS)) .layers(getLayers(functionName)) .build(); // Create a Lambda version using the Maven version Version lambdaVersion = new Version( this, functionName + "-version", VersionProps.builder() .lambda(lambdaFunction) .description(version) .build() ); } }
问题原因
无限循环的核心是流水线自突变(selfMutation)机制被不必要触发。每次流水线运行时,你通过上下文参数传递的版本号是动态变化的,这会导致生成的Cloud Assembly(cdk.out)内容每次都不一样——比如Lambda版本描述、Asset包路径都依赖这个版本号。CDK会认为流水线定义本身需要更新,于是触发UpdatePipeline步骤,而UpdatePipeline完成后又会重新启动整个流水线,形成循环。
解决方案
要解决这个问题,必须把动态版本相关的部署逻辑从流水线的synth阶段分离,让流水线的核心定义保持稳定,不随版本号变化而改变。具体操作如下:
1. 调整synth阶段职责
synth阶段只负责生成稳定的流水线结构和基础Lambda函数定义,不再传递动态版本的上下文参数。修改后的synth阶段命令示例:
# 导出版本号 export LAMBDA_SNAPSHOT_VERSION_ID=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) # 构建Lambda包 mvn install -Passembly-zip # 部署到CodeArtifact mvn deploy:deploy-file ... # 进入CDK目录执行synth(不再传递版本上下文参数) cd ${CODEBUILD_SRC_DIR}/cdk cdk synth
2. 添加独立的Lambda版本部署步骤
在流水线的dev-deploy阶段之后,新增一个CodeBuildStep,用AWS CLI直接发布Lambda版本,避免通过CDK Stack定义动态资源:
CodeBuildStep deployLambdaVersionStep = new CodeBuildStep( "deploy-lambda-version", CodeBuildStepProps.builder() .commands(List.of( // 从pom文件提取版本号 "export LAMBDA_SNAPSHOT_VERSION_ID=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout -f ../lambda/pom.xml)", // 发布Lambda版本 "aws lambda publish-version --function-name " + functionName + " --description $LAMBDA_SNAPSHOT_VERSION_ID" )) .buildEnvironment(BuildEnvironment.builder() .buildImage(LinuxBuildImage.STANDARD_7_0) .build()) .rolePolicyStatements(List.of( PolicyStatement.Builder.create() .actions(List.of("lambda:PublishVersion")) .resources(List.of("arn:aws:lambda:" + region + ":" + account + ":function:" + functionName)) .build() )) .build() ); // 将步骤添加到流水线的dev-deploy阶段之后 pipeline.addStage(new LambdaPipelineStage(...)) .addPost(deployLambdaVersionStep);
3. 修改LambdaStack,移除动态版本定义
让LambdaStack只负责创建基础的Lambda函数,把版本创建逻辑移到上述的CodeBuild步骤中:
private void initStack(final String functionName, final String environment, final String version) { String artifactId = Constants.LAMBDA_ARTIFACT_ID_VALUE; String handler = Constants.LAMBDA_HANDLER; Code code = Code.fromAsset( "../lambda/target/" + artifactId + "-" + version + ".zip" ); IRole lambdaRole = getLambdaRole(); IFunction lambdaFunction = Function.Builder.create(this, functionName) .functionName(functionName) .runtime(Constants.LAMBDA_RUNTIME) .code(code) .handler(handler) .role(lambdaRole) .environment(getEnvironmentVariables(functionName, environment)) .memorySize(Constants.LAMBDA_MEMORY_SIZE) .architecture(Constants.LAMBDA_ARCHITECTURE) .ephemeralStorageSize(Size.mebibytes(Constants.LAMBDA_MEMORY_SIZE)) .timeout(Duration.seconds(Constants.LAMBDA_TIMEOUT_SECONDS)) .layers(getLayers(functionName)) .build(); // 移除这里的Version创建代码,改用后续的CodeBuild步骤处理 }
核心原理
CDK流水线的自突变机制会对比每次synth生成的Cloud Assembly与当前流水线的定义,一旦有差异就会更新流水线。当你的Stack定义依赖动态变化的参数时,每次synth都会生成不同的模板,导致流水线不断自我更新。通过将动态部署逻辑从CDK基础设施定义中分离,改用脚本化步骤处理,就能让流水线结构保持稳定,彻底避免无限循环。
内容的提问来源于stack exchange,提问作者Supun Athukorala

