亚马逊AWS官方博客
实现跨账号 ECS on Fargate 服务发布的 CI/CD Pipeline
背景
AWS 多账号架构为企业提供了一种资源分配和管理的策略,这种策略可以更好地实现业务资源的隔离。在开发和部署应用程序或服务时,往往也会使用不同的账号来进行代码管理和应用部署,从而更好地实现资源隔离,简化权限管理和优化应用程序部署流程。在应用开发和部署方面,AWS 提供了一系列与持续集成和持续交付(CI/CD)相关的工具和服务,以帮助开发团队更轻松地构建、部署和管理应用程序。其中包括基础服务即设施的 CloudFormation,用户存储和版本控制的的 Git 存储库服务 CodeCommit,可以提供编译,测试和打包应用程序的 CodeBuild,自动化应用部署服务的 CodeDeploy 以及构建多阶段流水线的 CodePipeline。在代码管理和应用部署分布在不同账号的条件下,如何构建一套完整的 CI/CD Pipeline 是本篇博客的主要内容。
本篇博客以一个代码实例 webapp-sample 为例,主要介绍如何通过结合 CodeCommit, CodeBuild,CodePipeline,CloudFormation 和 ECR(Elastic Container Registry)等 AWS 服务来实现应用服务的跨账号自动发布。您可从这里获取 github 中的 Webapp-sample 的实例代码。
webapp-sample 是以 Golang 开发的一个简单的 webapp,下面是 webapp-sample 的主要的文件组织结构。其中的 builspec.yml 是我们后面 CodeBuild project 所有执行的文件,deployment 文件夹下是我们有关部署的一些相关资源定义,会在后面的介绍中说明。
解决方案概览
在解决方案中,我们假设两个账号分别为源账号 Account A 和目的账号 Account B,Account A 主要用来作为代码和镜像生成的管理,通过 ECR 的镜像 replication 规则来实时同步镜像到 Account B 中;然后,在 Account B 中创建 CodePipeline 来完成基础设施的创建和 ECS on Fargate 的服务发布。
整体方案的工作流程主要分为以下 7 个步骤,后面会详细介绍每个步骤。其中的 Step 1 – Step 3 是在源账号 Account A 中操作,Step 5 – Step 7 是在目的账号 Account B 中来完成。
Step 1:在 CodeCommit 中创建 repository webapp-sample
Step 2:创建 CodeBuild project 打包镜像并推送到 ECR
Step 3:配置 EventBridge 规则来实现 CodeBuild project 的自动触发
Step 4:创建 ECR 复制规则来实现 ECR 的跨账号复制
Step 5:准备 Account B 的网络环境和 ECS Fargate Cluster
Step 6:准备 CodePipeline 的 S3 source providers
Step 7:使用 CodePipeline 来创建 Pipeline
前提准备
在开始之前,我们需要做好以下工作:
- AWS Account A 和 AWS Account B;
- 一个工作环境,这个工作环境中具有一些基本的工具并配置了 AWS 命令行和 git 环境,并已经配置好 CodeCommit 的访问环境。对于 CodeCommit 的配置可以参考 CodeCommit 配置官方文档。
详细步骤
Step 1:在 CodeCommit 中创建 repository webapp-sample
在工作环境中运行下面的代码来获取到 webapp-sample 的代码包:
再运行下面的代码在 CodeCommit 中创建 webapp-sample repository,并将代码提交到 CodeCommit;注意请设置 $REGION 变量或直接替换 $REGION 为 CodeCommit 所在的 region;
Step 2:创建 CodeBuild project 打包镜像并推送到 ECR
在创建 CodeBuild project 之前,我们需要在 ECR 中先准备一个 repository,我们命名该 ECR repository 为 webapp-sample,它将是 CodeBuild project 推送镜像的 repository。详细的创建步骤请参考 Creating a private repository 官方文档。
其次,在 CodeBuild project 运行过程中,它除了自身可管理的基本的权限还需要 ECR 的权限来完成推送镜像的操作。我们可以在创建 project 时,让 CodeBuild project 帮助我们完成 Role 的创建,然后在创建完成后进行权限的调整,也可以在创建 CodeBuild project 之前先为 CodeBuild project 创建具有 ECR 权限的 Role 以供创建 project 时使用,并允许 CodeBuild project 调整这个 Role 以安装它本身所需的权限。我们这里采用了后一种方式,先来为 CodeBuild project 创建一个 Role,命名改 Role 为 codebuild-webapp-sample-service-role。该 Role 的 Trust entity 为 CodeBuild Service,为了简化起见,我们这里给出的 Policy 是范例 Policy,在具体的实现过程中,您需要根据实际情况,进行权限最小化的设计。范例 Policy 如下:
接下来,我们来创建 CodeBuild project。进入 CodeBuild 控制台,点击右侧的创建项目按钮,在弹出的 CodeBuild 页面中输入项目名称 webapp-sample,在 Source 部分,选择 CodeCommit 作为我们的 source provider,并选择 webapp-sample 作为 repository,在 Reference type 部分,选择 branch,并选定 master 作为 repository 的 branch。
在环境部分,我们选择 amazonlinux2-x86_64-standard:5.0 作为镜像来作为 CodeBuild 的运行环境,需要注意的是在 privileged 部分,勾选它来获取相应的权限提升,因为我们需要在 CodeBuild 环境中进行 docker 镜像的制作。
在 Environment variables 部分, 我们添加一个环境变量来指定 ECR image 的 repository 名称,这个变量会在 buildspec.yml 被引用。
对于 Service Role 部分,我们选择 Existing service role,并选择我们上面刚创建好的 Role。同时启用 Allow AWS CodeBuild to modify this service role so it can be used with this build project,来让 CodeBuild project 来自行添加它自身所需的一些权限到我们的 Role。
在 Buildspec 部分,我们保持默认选项,选择使用 webapp-sample 根目录下的 buildspec.yml 文件。buildspec.yml 主要是完成 docker 镜像的创建并以 latest 和 commit id 作为镜像的 tag。在镜像创建完成后,会被推送到 Account A 的 ECR repository 中。需要指出的是 latest tag 是一个动态的 tag,它也是我们在后续的 CodePipeline 中 ECR source provider 指定使用的 image tag。这样,在每次生成新镜像时,latest 也会跟着变化,从而触发 CodePipeline 执行。下面是 buildspec.yml 的内容:
最后点击 Create build project 来完成 CodeBuild project 的创建。
Step 3:配置 EventBridge 规则来实现 CodeBuild 项目的自动触发
EventBridge 可以监控 AWS CodeCommit 中的事件,我们可以通过在 EventBridge 中添加规则来触发 CodeBuild project 来执行打包镜像。
在 Amazon EventBridge 页面下,选择左侧栏 Buses下的 Rules,在右侧的主区域选择 Create Rule,填写 EventBridge rule 的名字和选择 Rule type 为 Rule with an event pattern。
在 Build event pattern 页面,编辑 Event pattern,这里我们选择 Custom pattern 来指定当 webapp-sample repository 中的 branch master 中有状态变更时,会触发事件响应。Event pattern 的 JSON 如下:
在 Select target(s)界面,我们填入我们上面创建的 Project ARN 来完成事件响应目标的设定。
继续跟随向导步骤,完成 EventBridge rule 的设置。此时,当我们在 CodeCommit master branch 提交代码时,CodeBuild project 就会自动触发。
Step 4:创建 ECR 复制规则来实现 ECR 的跨账号复制
ECR replication 规则支持跨区域和跨账号的镜像复制,跨账号的复制规则需要在 Account A 和 Account B 中分别配置。
在 Account A 的 ECR 界面上,选择左侧 Private registry 下的 replication 菜单,在右侧部分选择 Add Rule 来添加 ECR 复制规则;在弹出的 Destination types 界面下选择 enable cross-account replication,选择 Next 继续。
在 Cross-account replication 下,我们需要填写我们的部署账号 Account B,以及部署目标区域,这里以 ap-southeat-2 为例。
ECR 复制规则还提供了 filters 的可选功能,支持前缀的过滤功能。用户可以通过添加 filter 的方式来限定复制的 repository,我们这里添加 webapp-sample 来限定只有前缀为 webapp-sample 的 repository 中会被复制到 Account B。
到这里,我们完成 Account A 中 ECR replication 规则的配置。另外,从 step2 – step 4 中目前在 Account A 中的所有操作,您也可以选择使用 webapp-sample 代码中的路径为 deployment/AccountA/main.template.yaml 的 CloudFormation template 来生成。
除了配置 Account A 中的 replication 规则,我们还需要在 Account B 中赋予 Account A 响应的权限来允许 Account A 向 Account B 中复制镜像。
选择 Account B 中的 ECR 页面下,选择 Private registry 下的 Permissions,点击 Edit JSON 来填写下面的规则来完成规则的设定;Principal 下的 AWS 账号是 Account A 的账号 ID,Resource 内的 AWS 账号是当前的 Account B 的账号 ID。
对于 Account B 中的 ECR repository,我们可以自己创建,也可以不创建而选择在第一次触发镜像复制的时候由系统自动来生成。这里需要在 ECR repository 生成以后确保 repository 的 Image scan settings 中的 Scan on push 启用,一方面是增强镜像的安全性,另一方面在后面创建 Pipeline 的时候,当镜像复制完成的时候,我们需要配置事件来触发 Pipeline。但由于当前在镜像复制时在目标账号中不会有 Event 产生,所以我们可以通过配置 EventBridge 中 Image Scan 事件来触发 Pipeline。对 Account B 的 ECR 的操作,也可以通过执行代码路径中的 deployment/AccountB/ecr.yaml CloudFormation template 来实现。
到这里,我们完成了 CodeCommit repository 的创建,配置了当 CodeCommit 中的 master branch 进行提交时,会直接触发 CodeBuild 项目来生成容器镜像并推送到 Account A 中的 ECR,而镜像也会自动复制到到 Account B 中的 ECR 为下一步的自动部署做准备。另外,需要注意的是接下来的步骤都会在 Account B 中完成。
Step 5:准备 Account B 的网络环境和 ECS Fargate Cluster
在创建 Pipeline 之前,我们先要准备好 Account B 中的网络环境和 ECS Fargate Cluster。由于我们会在随后的 CodePipeline 中使用 CloudFormation 的方式来创建我们最终应用程序的基础设施 Application Load Balancer(ALB)和 Elastic Container Service (ECS)service,所以在创建 VPC 和 ECS Fargate Cluster 的时候需要做一些特殊的设置。需要按照一定的要求来进行添加 tag 的操作,后续会需要使用 tag 来获取到 VPC 内的资源并引用。当然,你也可以对已有的 VPC 和 ECS Fargate Cluster 进行添加 tag 的操作来配置环境。下面是需要做的一些特殊设置:
- 对 VPC 添加 key为”ecs/vpc”的 tag。
- 至少两个 public subnets,用来部署 ALB,并添加 key 为”ecs/alb”的 tag,需要注意的是 alb 需要指定至少两个 subnets。
- 两个 private subnets,用来部署 ECS service,并添加“ecs/service”的 tag,为了满足 ECS service 从 ECR 拉取镜像的需求,需配置 private subnets 可以访问公网。
- 对 ECS Fargate Cluster 添加“ecs/cluster”的 tag。
另外,对于网络环境,您也可以使用 CloudFormation 来执行 webapp-sample 代码仓库中 deployment/AccountB/vpc.yaml 来创建新的 VPC,subnets 和 ECS Fargate Cluster。
Step 6:准备 CodePipeline 的 S3 source providers
在解决方案概览中,我们提到会使用 S3 和 ECR 来作为 CodePipeline 中的 Source Stage 中的 Source Provider。当然 ECR 我们已经可以在上一步骤中生成,现在我们来准备 S3 source provider。
在 webapp-sample 代码库中,deployment 文件夹下有两个文件:
- ecs-service.yaml:基础设施和应用部署要用到的 CloudFormation template,会在 deploy 阶段被执行
- buildspec.yml:CodeBuild project 的 yml 主要为 CodePipeline 的 deploy 阶段生成 CloudFormation 的参数配置文件
下面是 ecs-service.yaml 中 ECS TaskDefinition 的片段,是我们主要关心的部分。可以看到在 TaskDefinition 中 ContainerDefinitions 定义的部分有个 Image 的配置,这个配置的值是在执行 CloudFormation template 时由配置参数指定,而配置参数是由后面介绍的 CodeBuild project 执行时从 CodePipeline Source stage 的 ECR 镜像来提供。从而每当有新的镜像生成触发 CodePipeline 时,就会被部署到 ECS Fargate service 完成应用发布。
接下来,在 Account B 中创建一个 S3 bucket 作为 CodePipeline 的一个 source provider,并启用 Bucket 的 version 功能。运行下面的代码将代码库中的 deployment/AccountB/PipelineSource 文件夹下的文件压缩打包上传到该 bucket 中。请替换以下代码中的 $REGION 为 S3 Bucket 所在的 region,替换 $BUCKET_NAME 为您刚刚新创建的 bucket。
Step 7:使用 CodePipeline 来创建 Pipeline
在创建 Pipeline 之前,我们需要先准备好两个 Role,分别是后边 build 阶段 CodeBuild project 需要的 Role 和 Deploy 阶段 CloudFormation 需要的 Role。与 Step 2 中相似,为了简化起见,我们这里给出的 Policy 是范例 Policy,在具体的实现过程中,您需要根据实际情况,进行最小化权限的设计。
CodeBuild project 的 Role 命名为 codebuild-webapp-sample-ppl-build-service-role,Trust entity 为 AWS service 的 CodeBuild,范例 Permission Policy 如下:
Deploy 阶段的 Role 命名为 deploy-webapp-sample-ppl-ecsdeploy-service-role,Trust entity 为 AWS service 的 CloudFormation,范例 Permission Policy 如下:
下面我们开始在 CodePipeline 中创建 Pipeline。
在 CodePipeline 页面上,点击右上角的 Create Pipeline 来执行创建 CodePipeline 的操作,在 Choose pipeline settings 步骤中,填写所要创建的 Pipeline 的名称,Pipeline type V1 和 V2 都可以,它对于我们使用场景没有不同,其他的我们都选择默认选项,让 CodePipeline 来帮助我们创建 CodePipeline 的 Role 和 Artifacts S3 Bucket。
在 Add source stage 阶段我们选择 S3 作为 source provider,并分别在 Bucket 部分和 S3 object 填写我们在上一步骤中创建的 Bucket 和压缩文件 webapp-sample-deployment.zip。
在下面 Add build stage 页面中,选择 AWS CodeBuild 作为 Build provider,并点击 Create project 按钮来创建 CodeBuild project,需要指出的是,以 CodePipeline 为 Source Provider 的 CodeBuild project 目前只能通过在 CodePipeline 创建/编辑阶段进行添加。
在弹出的 Create build project 页面中,添加 Project name,配置 Environment 部分,继续使用 aws/codebuild/amazonlinux2-x86_64-standard:5.0 镜像。对于 Service Role,我们采用“Existing service role”,指定为我们刚刚为 CodeBuild 创建的 Role,并启用 Allow AWS CodeBuild to modify this service role so it can be used with this build project,这样 CodeBuild 会自动向我们创建的 Role 调整运行所需要的权限。其他的选项都选择默认,在完成 CodeBuild 的配置后,返回 Add build stage 页面,点击页面中的 Continue to CodePipeline 返回 CodePipeline 的 Add build stage 阶段继续。
CodeBuild project 会使用在上一步骤中 S3 的 webapp-sample-deployment.zip 中的 buildspec.yml 文件执行 build 步骤。在 build 过程中,我们会通过 aws cli 和上一步中定义的 tag 来获取 VPC,subnets 和 ECS Fargate Cluster 的资源 ID,通过对后面添加的 secondary source ECRSource 来获取到与 git commit id 对应的镜像 tag,从而为 ecs-service.yaml 生成参数配置文件。
buildspec.yml 的文件内容如下:
我们来看一下 buildspec.yml 的定义,可以看到最终形成了两个文件,packaged.yaml 其实是我们的 ecs-service.yaml,这个 CloudFormation template 定义了我们要生成的 ECS Service 和 ECS Task,并通过参数的方式指定了最终要部署的镜像,这样我们在以后镜像改变的时候也会引起 CloudFormation 的更新完成新镜像的部署。
点击 Next 按钮,进入 Add deploy stage。在 Add deploy stage 页面中,选择 AWS CloudFormation 作为 Deploy provider,在随后弹出的 Action mode 中,选取 Create or replace a stack。然后在 Stack Name 文本框中输入 webapp-sample-ecsservice,忽略弹出的 ValidationError,在 Change set name 文本框中输入 webapp-sample-ecsservice-changeset;在 Template 部分,指定 BuildArtifact 为 Artifact name,File name 为在 CodeBuild 中生成的 artifacts files 中的 packaged.yaml, 启用 Use configuration file 在 configuration file 部分中同样指定 BuildArtifact 为 Artifact name,File name 为在 CodeBuild 中生成的 artifacts files中的 packaged.configuration.json。在 Capabilities 部分,我们添加 CAPABILITY_IAM, CAPABILITY_NAMED_IAM 和 CAPABILITY_AUTO_EXPAND 来允许 CloudFormation 操作 IAM 权限。在 Role name 部分,选择我们刚刚为 deploy stage 创建的 Role。
然后选择 Next,并在随后的 Review 页面上点击 Create pipeline 来生成 Pipeline。
您可能在此时看到执行失败的 Pipeline,这是因为此时的 Pipeline 还是个不完整的 Pipeline,我们需要继续对 Pipeline 进行编辑。点击 Pipeline 列表中的 webapp-sample-pipeline 进入到刚创建好的 webapp-sample-pipeline 中,点击右上角的 Edit 后,Pipeline 进入到编辑阶段。
首先我们来添加一个 secondary source provider,把 ECR webapp-sample 在 Source 阶段,选择右上角的 Edit Stage,进入到 Source 的编辑阶段。再选择右侧的 Add action 进入到添加 Action 的页面,如下图所示,输入 ECRSource 作为 Action name,选择我们 Amazon ECR 作为 Action provider,选择 ECR repository webapp-sample 作为 Repository name,并指定 latest 作为 image tag。在 Variable namespace 和 Output artifacts 中分别输入 ECRSourceVariables 和 ECRSourceArtifact,这样 source stage 的资源可以在后面的 stage 中被引用。
在 Source stage 添加完 ECRSource 以后,CodePipeline 创建一个 EventBridge 规则,这条规则时当对 webapp-sample:latest 上发生 PUSH 事件时,就会触发 Pipeline。上一篇博客提到,镜像复制规则目前并不产生 PUSH 事件,所以我们新建一个 EventBridge 规则,使用 ECR Image Scan 事件来触发 Pipeline。Event Pattern 的规则如下:
回到 CodePipeline 的 Edit 页面上,我们点击 Build 阶段右侧的 Edit Stage 按钮,进入到 Build 阶段的编辑页面,再点击下图中标识的编辑按钮进入到 Build Stage 中 Action 的编辑页面。
在 Action 的页面中,在 Input artifacts 部分,我们将刚刚添加的 ECRSource 的 Output Artifact ECRSourceArtifact 添加进去,并选择 SourceArtifact 作为 Primary source。这样 CodeBuild project 就可以在运行时分别访问这两个 SourceArtifact 和 ECRSourceArtifact。
接着,还是回到在 CodePipeline 的 Edit 界面,我们点击 Deploy 阶段右侧的 Edit stage 按钮,进入到 Deploy 阶段的编辑页面,再点击弹出的 Add Action Group 按钮来添加 CloudFormation 的执行动作。详细配置如下图所示,其中的 Stack name 和 Change set name 要和我们 Deploy 阶段的 Deploy action 中所定义的相符合。
至此,我们完成了 CodePipeline 的创建,整个创建 Pipeline 的过程也可以通过执行代码中的 deployment/AccountB/pipeline.yaml CloudFormation template 来实现。此时,当有代码提交引起镜像复制到 Account B 后,就会引起 Pipeline 的执行,执行后的 Pipeline 的整体界面如下图所示:
总结
在这篇博客中,我们详细介绍了如何构建跨账号代码管理和 ECS on Fargate 服务发布的方案。在源账号进行代码管理和镜像生成,并自动复制镜像到目标账号,一方面镜像分布在两个账号中增强了数据灾备的能力,另一方面简化了代码或镜像跨账号访问权限配置的复杂性。在目的账号,我们采用了 CloudFormation 来进行所属服务配套的基础设施和服务引用的部署,加快了部署速度,提高了复用率。