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

How to automate deployment to ECS Fargate when new image is pushed to ECR repository

首先,这是 CDK 特有的 - 我知道有很多 questions/answers 围绕这个主题,但其中 none 是 CDK 特有的。

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

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

由于任务定义必须指定图像标签(否则它会寻找可能不存在的 'latest'),这似乎是不可能的。

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

预期的工作流程如下:

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

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

这有可能吗,还是我一直在等待 CFN 支持 ECS CodeDeploy 功能?

您可以将最新标签的名称存储在 AWS Systems Manager (SSM) parameter (see the list 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,
        )

我测试了一下,效果很好。

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

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

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

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 来保留不同版本的构建。每次我需要部署或回滚时,直接从构建脚本中执行即可。

好吧,经过一些黑客攻击后,我设法做到了这一点。

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

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

const ecrRepo = new ecr.Repository(this, `${appName}Repository`, {
    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, `${appName}BuildProject`, {
    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 存储库具有有效的 'latest' 映像(否则将无法正确创建 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 完全支持它时那样干净,但它工作得很好。