如何避免AWS CodePipeline在ECS镜像未变时部署服务并持久化缓存?
AWS Copilot多服务流水线部署优化问题
使用AWS Copilot执行copilot init pipeline和copilot pipeline deploy命令,基于AWS CodePipeline与CodeBuild部署多服务,相关配置如下:
配置片段
流水线清单
name: ****** version: 1 source: provider: GitHub properties: branch: ******* repository: *********** connection_name: ******** stages: - name: *********** deployments: - service1 - service2 - service3: depends_on: [service1, service2]
Buildspec.yaml
# Buildspec runs in the build stage of your pipeline. version: 0.2 phases: install: commands: - echo "cd into $CODEBUILD_SRC_DIR" - cd $CODEBUILD_SRC_DIR # Download the copilot linux binary. - wget -q https://ecs-cli-v2-release.s3.amazonaws.com/copilot-linux-v1.34.0 -O copilot-linux - chmod +x ./copilot-linux build: commands: - echo "Building base image..." - docker build -t base-image -f Dockerfile . post_build: commands: - ls -la - export COLOR="false" - export CI="true" - pipeline=$(cat $CODEBUILD_SRC_DIR/copilot/pipelines/production/manifest.yml | ruby -ryaml -rjson -e 'puts JSON.pretty_generate(YAML.load(ARGF))') - pl_envs=$(echo $pipeline | jq -r '.stages[].name') # Find all the local services in the workspace. - svc_ls_result=$(./copilot-linux svc ls --local --json) - svc_list=$(echo $svc_ls_result | jq '.services') - > if [ ! "$svc_list" = null ]; then svcs=$(echo $svc_ls_result | jq -r '.services[].name'); fi # Find all the local jobs in the workspace. - job_ls_result=$(./copilot-linux job ls --local --json) - job_list=$(echo $job_ls_result | jq '.jobs') - > if [ ! "$job_list" = null ]; then jobs=$(echo $job_ls_result | jq -r '.jobs[].name'); fi # Raise error if no services or jobs are found. - > if [ "$svc_list" = null ] && [ "$job_list" = null ]; then echo "No services or jobs found for the pipeline to deploy. Please create at least one service or job and push the manifest to the remote." 1>&2; exit 1; fi # Generate the cloudformation templates. # The tag is the build ID but we replaced the colon ':' with a dash '-'. # We truncate the tag (from the front) to 128 characters, the limit for Docker tags # (https://docs.docker.com/engine/reference/commandline/tag/) # Check if the `svc package` commanded exited with a non-zero status. If so, echo error msg and exit. - > for env in $pl_envs; do tag=$(echo ${CODEBUILD_BUILD_ID##*:}-$env | sed 's/:/-/g' | rev | cut -c 1-128 | rev) for svc in $svcs; do ./copilot-linux svc package -n $svc -e $env --output-dir './infrastructure' --tag $tag --upload-assets; if [ $? -ne 0 ]; then echo "Cloudformation stack and config files were not generated. Please check build logs to see if there was a manifest validation error." 1>&2; exit 1; fi done; for job in $jobs; do ./copilot-linux job package -n $job -e $env --output-dir './infrastructure' --tag $tag --upload-assets; if [ $? -ne 0 ]; then echo "Cloudformation stack and config files were not generated. Please check build logs to see if there was a manifest validation error." 1>&2; exit 1; fi done; done; - ls -lah ./infrastructure artifacts: files: - "infrastructure/*"
服务Dockerfile
FROM base-image ARG micro_service=service1 WORKDIR /usr/src/app ADD ./$micro_service/ ./$micro_service EXPOSE 8000 WORKDIR /usr/src/app/$micro_service # Build Service RUN npm run build RUN chmod +x start.sh CMD ["./start.sh"]
注:base image包含所有服务的通用代码,目前仅在构建阶段临时缓存,未推送到ECR。
当前问题
流水线运行时会构建Docker镜像并推送到ECR,但即使镜像内容未变更(依赖缓存复用),ECS任务定义仍会被更新,导致所有服务被重新部署。
具体疑问与解决方案
1. 镜像未变更时阻止ECS任务更新
当前每次构建都会用CODEBUILD_BUILD_ID生成新标签,即使镜像内容一致,新标签也会触发任务定义更新。解决思路:
- 基于镜像内容生成唯一标签:用Docker镜像的摘要(digest)作为标签,或者计算服务代码+base镜像的哈希值作为标签,确保内容不变则标签不变。修改Buildspec中生成tag的逻辑:
# 替代原tag生成命令,先构建服务镜像并获取摘要,提取前16位作为标签 tag=$(docker build -t temp-svc-$svc --build-arg micro_service=$svc . | grep "Successfully built" | awk '{print $3}' | cut -c 1-16)-$env - 跳过重复资产上传:在执行
copilot svc package前,检查ECR中是否已存在该标签的镜像,若存在则去掉--upload-assets参数,避免生成新的任务定义。
2. 仅部署变更的服务
当前流水线会遍历所有服务执行package,导致全量部署。优化方案:
- 通过Git diff识别变更服务:在Buildspec的build阶段前,添加Git命令对比当前提交与上一次构建的提交,找出变更的服务目录:
# 获取上一次构建的commit ID(存储在S3中) last_commit=$(aws s3 cp s3://your-bucket/last-commit.txt - || echo "") # 对比变更文件所属服务 changed_svcs=$(git diff --name-only $last_commit HEAD | grep -E "^(service1|service2|service3)/" | cut -d'/' -f1 | sort -u) # 若无变更服务,直接退出构建 if [ -z "$changed_svcs" ]; then echo "No services changed, skipping build"; exit 0; fi # 更新存储的last_commit echo $CODEBUILD_RESOLVED_SOURCE_VERSION > last-commit.txt && aws s3 cp last-commit.txt s3://your-bucket/ - 修改循环逻辑仅处理变更服务:将post_build中的
for svc in $svcs替换为for svc in $changed_svcs,只对变更的服务执行package和部署。
3. 持久化base image缓存
当前base image仅在单次构建中缓存,无法跨构建复用。解决方法:
- 将base image推送到ECR:在Buildspec的build阶段,构建base image后推送到ECR仓库,后续构建先拉取该镜像作为缓存:
# 构建并推送base image到ECR base_image_tag=$(docker build -t base-image . | grep "Successfully built" | awk '{print $3}') aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin your-ecr-account-id.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com docker tag base-image:latest your-ecr-account-id.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/base-image:$base_image_tag docker tag base-image:latest your-ecr-account-id.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/base-image:latest docker push your-ecr-account-id.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/base-image --all-tags - 修改服务Dockerfile引用ECR中的base image:将
FROM base-image改为FROM your-ecr-account-id.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/base-image:latest,构建服务镜像时优先拉取该镜像作为缓存层。 - 启用CodeBuild Docker层缓存:在CodeBuild项目配置中开启
Docker layer caching,复用之前构建的镜像层,进一步减少构建时间。
内容的提问来源于stack exchange,提问作者Ziaf Nadeem
相关产品推荐
相关产品推荐

