将新映像推送到 ECR 存储库时如何自动部署到 ECS Fargate

Posted

技术标签:

【中文标题】将新映像推送到 ECR 存储库时如何自动部署到 ECS Fargate【英文标题】:How to automate deployment to ECS Fargate when new image is pushed to ECR repository 【发布时间】:2021-10-11 00:37:30 【问题描述】:

首先,这是特定于 CDK 的 - 我知道围绕这个主题有很多问题/答案,但没有一个是特定于 CDK 的。

鉴于最佳实践规定 Fargate 部署不应在 ECR 存储库中查找“最新”标签,那么在使用 ECR 作为源时如何设置 CDK 管道?

在每个服务都在自己的存储库中的多存储库应用程序中(这些存储库将有自己的 CDK CodeBuild 部署来设置构建和推送到 ECR),基础设施 CDK 管道如何知道新图像正在推送到 ECR 存储库并能够将该新映像部署到 ECS Fargate 服务?

由于任务定义必须指定图像标签(否则它将查找可能不存在的“最新”),这似乎是不可能的。

作为一个具体的例子,假设我有以下 2 个存储库:

CdkInfra 将为每位客户创建其中一个存储库,以便为其应用程序创建完整的环境 一些服务 实际应用代码 只有一个存储库应该存在并被多个 CdkInfra 项目重复使用 cdk 定义 CodeBuild 项目的目录,因此当检测到推送到 master 时,会构建服务并将映像推送到 ECR

预期的工作流程如下:

    SomeService 存储库已更新,因此将新映像推送到 ECR CdkInfra 管道应检测到跟踪的 ECR 存储库有新图像 CdkInfra 管道更新 Fargate 任务定义以引用新图像的标签 Fargate 服务提取并部署新映像

我知道由于 CFN 不支持 ECS 部署,目前 CodeDeploy 存在限制,但似乎 CodePipelineActions 有能力设置一个 EcrSourceAction 可能能够实现这一点,但是我一直无法让这个工作到目前为止。

这是否可能,还是我一直等到 CFN 支持 ECS CodeDeploy 功能?

【问题讨论】:

使用 SDK 从您的 CDK 代码中获取 SSM 参数的值? 【参考方案1】:

您可以将最新标签的名称存储在AWS Systems Manager (SSM)parameter(参见列表here)中,并在您将新映像部署到 ECR 时动态更新它。

然后,您可以在 CDK 部署期间使用 AWS SDK 获取参数的值,然后将该值传递给您的 Fargate 部署。

以下用 Python 编写的 CDK 堆栈使用 YourSSMParameterName 参数的值(在我的 AWS 账户中)作为 S3 存储桶的名称:

from aws_cdk import (
    core as cdk
    aws_s3 as s3
)

import boto3

class MyStack(cdk.Stack):
    def __init__(self, scope, construct_id, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

        ssm = boto3.client('ssm')
    
        res = ssm.get_parameter(Name='YourSSMParameterName')
        name = res['Parameter']['Value']

        s3.Bucket(
            self, '...',
            bucket_name=name,
        )

我对此进行了测试,效果很好。

【讨论】:

如果我更新应用程序存储库以在推送图像时添加/更新参数,并更新主堆栈以从存储中提取值,我将如何设置触发器来识别参数值变了,需要部署吗? @AndreiSevtsenko 我认为在目前的情况下,你能做的最好的就是使用 lambdas。【参考方案2】:

我对这种情况的看法是,如果您使用 CDK(实际上是 CloudFormation)从 ECR 部署最新的图像是非常困难的。

我最终将所有 Docker 映像构建和 CDK 部署作为一个构建脚本

在我的例子中,是一个 Java 应用程序,我构建了 war 文件并在 /docker 目录中准备了 DockerFile

FROM tomcat:8.0
COPY deploy.war /usr/local/tomcat/webapps/

然后让 CDK 脚本在运行时获取并构建映像。

    const taskDefinition = new ecs.FargateTaskDefinition(this, 'taskDefinition', 
      cpu: 256,
      memoryLimitMiB: 1024
    );
   
    const container = taskDefinition.addContainer('web', 
      image: ecs.ContainerImage.fromDockerImageAsset(
        new DockerImageAsset(this, "image", 
          directory: "docker"
        )
      )
    );    

这会将映像放入特定的 CDK ECR 存储库并进行部署。

因此,我不依赖 ECR 来保存不同版本的构建。每次我需要部署或回滚时,只需直接从构建脚本中执行即可。

【讨论】:

【参考方案3】:

好吧,经过一番骇客之后,我设法做到了。

首先,服务本身(在本例中是一个 Spring Boot 项目)在其根目录中获得一个 cdk 目录。这基本上只是设置了 CI/CD 管道的 CI 部分:

const appName: string = this.node.tryGetContext('app-name');

const ecrRepo = new ecr.Repository(this, `$appNameRepository`, 
    repositoryName: appName,
    imageScanOnPush: true,
    removalPolicy: cdk.RemovalPolicy.DESTROY,
);

const bbSource = codebuild.Source.bitBucket(
    // BitBucket account
    owner: 'mycompany',
    // Name of the repository this project belongs to
    repo: 'reponame',
    // Enable webhook
    webhook: true,
    // Configure so webhook only fires when the master branch has an update to any code other than this CDK project (e.g. Spring source only)
    webhookFilters: [codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH).andBranchIs('master').andFilePathIsNot('./cdk/*')],
);

const buildSpec = 
    version: '0.2',
    phases: 
        pre_build: 
            // Get the git commit hash that triggered this build
            commands: ['env', 'export TAG=$CODEBUILD_RESOLVED_SOURCE_VERSION'],
        ,
        build: 
            commands: [
                // Build Java project
                './mvnw clean install -Dskiptests',
                // Log in to ECR repository that contains the Corretto image
                'aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 489478819445.dkr.ecr.us-west-2.amazonaws.com',
                // Build docker images and tag them with the commit hash as well as 'latest'
                'docker build -t $ECR_REPO_URI:$TAG -t $ECR_REPO_URI:latest .',
                // Log in to our own ECR repository to push
                '$(aws ecr get-login --no-include-email)',
                // Push docker images to ECR repository defined above
                'docker push $ECR_REPO_URI:$TAG',
                'docker push $ECR_REPO_URI:latest',
            ],
        ,
        post_build: 
            commands: [
                // Prepare the image definitions artifact file
                'printf \'["name":"servicename","imageUri":"%s"]\' $ECR_REPO_URI:$TAG > imagedefinitions.json',
                'pwd; ls -al; cat imagedefinitions.json',
            ],
        ,
    ,
    // Define the image definitions artifact - is required for deployments by other CDK projects
    artifacts: 
        files: ['imagedefinitions.json'],
    ,
;

const buildProject = new codebuild.Project(this, `$appNameBuildProject`, 
    projectName: appName,
    source: bbSource,
    environment: 
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
        privileged: true,
        environmentVariables: 
            // Required for tagging/pushing image
            ECR_REPO_URI:  value: ecrRepo.repositoryUri ,
        ,
    ,
    buildSpec: codebuild.BuildSpec.fromObject(buildSpec),
);

!!buildProject.role &&
    buildProject.role.addToPrincipalPolicy(
        new iam.PolicyStatement(
            effect: iam.Effect.ALLOW,
            actions: ['ecr:*'],
            resources: ['*'],
        ),
    );

设置完成后,必须手动构建一次 CodeBuild 项目,以便 ECR 存储库具有有效的“最新”映像(否则无法正确创建 ECS 服务)。

现在在单独的基础架构代码库中,您可以照常创建 ECS 集群和服务,从查找中获取 ECR 存储库:

const repo = ecr.Repository.fromRepositoryName(this, 'SomeRepository', 'reponame'); // reponame here has to match what you defined in the bbSource previously

const cluster = new ecs.Cluster(this, `Cluster`,  vpc );

const service = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'Service', 
    cluster,
    serviceName: 'servicename',
    taskImageOptions: 
        image: ecs.ContainerImage.fromEcrRepository(repo, 'latest'),
        containerName: repo.repositoryName,
        containerPort: 8080,
    ,
);

最后创建一个监听 ECR 事件的部署结构,手动将生成的 imageDetail.json 文件转换为有效的 imagedefinitions.json 文件,然后部署到现有服务。

const sourceOutput = new cp.Artifact();
const ecrAction = new cpa.EcrSourceAction(
    actionName: 'ECR-action',
    output: sourceOutput,
    repository: repo, // this is the same repo from where the service was originally defined
);

const buildProject = new codebuild.Project(this, 'BuildProject', 
    environment: 
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
        privileged: true,
    ,
    buildSpec: codebuild.BuildSpec.fromObject(
        version: '0.2',
        phases: 
            build: 
                commands: [
                    'cat imageDetail.json | jq "[. | name: .RepositoryName, imageUri: .ImageURI]" > imagedefinitions.json',
                    'cat imagedefinitions.json',
                ],
            ,
        ,
        artifacts: 
            files: ['imagedefinitions.json'],
        ,
    ),
);

const convertOutput = new cp.Artifact();
const convertAction = new cpa.CodeBuildAction(
    actionName: 'Convert-Action',
    input: sourceOutput,
    outputs: [convertOutput],
    project: buildProject,
);

const deployAction = new cpa.EcsDeployAction(
    actionName: 'Deploy-Action',
    service: service.service,
    input: convertOutput,
);

new cp.Pipeline(this, 'Pipeline', 
    stages: [
         stageName: 'Source', actions: [ecrAction] ,
         stageName: 'Convert', actions: [convertAction] ,
         stageName: 'Deploy', actions: [deployAction] ,
    ],
);

显然,一旦 CloudFormation 完全支持这一点,它就不像其他情况那样干净,但它工作得很好。

【讨论】:

以上是关于将新映像推送到 ECR 存储库时如何自动部署到 ECS Fargate的主要内容,如果未能解决你的问题,请参考以下文章

新 ECS 任务定义的 CI/CD

将映像从 aws ecr 部署到 aws elastic beanstalk

AWS ECR GetAuthorizationToken 问题

推送到 git 存储库时避免自动工作项链接

Docker 推送到 AWS ECR 问题

如何使用 Jenkins 将 Docker 容器从 Amazon ECR 自动部署到 Kubernetes