如何设置 Dockerized 应用程序与 Elastic Beanstalk 的持续集成?

How can I set up Continuous Integration of a Dockerized application to Elastic Beanstalk?

我是 Docker 的新手,我之前的经验是将 Java Web 应用程序(Tomcat 容器中的 运行)部署到 Elastic Beanstalk。我习惯的管道是这样的:提交被签入 git,这会触发 Jenkins 作业,该作业构建应用程序 JAR(或 WAR)文件,将其发布到 Artifactory,然后然后使用 eb deploy 将同一个 JAR 部署到 Elastic Beanstalk 中的应用程序。 (抱歉,如果 "pipeline" 是保留词;我只是在概念上使用它。)

顺便说一句,我也打算将 Gitlab 用于 CI/CD 而不是 Jenkins(由于我无法控制的组织原因),但从 Jenkins 到 Gitlab 的跳转对我来说似乎是直截了当的 - - 当然比从直接部署 WAR 到部署 Docker 化容器的跳跃更重要。

进入 Docker 世界,我想管道会是这样的:提交被签入 git,这会触发 Gitlab CI,然后它将构建 JAR 或 WAR 文件,将其发布到 Artifactory,然后使用 Dockerfile 构建 Docker 图像,将该 Docker 图像发布到 Amazon ECR(也许?)。 .. 然后我真的不确定 Elastic Beanstalk 集成将如何从那里进行。我知道它与 Dockerrun.aws.json 文件有关,并且可能需要调用 AWS CLI。

我刚看完来自 Amazon 的名为 Running Microservices and Docker on AWS Elastic Beanstalk 的网络研讨会,其中指出在我的存储库的根目录中应该有一个 Dockerrun.aws.json 文件,该文件基本上定义了与 EB 的集成。但是,似乎 JSON 文件包含一个 link 到 ECR 中的单个 Docker 图像,这让我很失望。每次构建新图像时 link 不会改变吗?我在想象 CI 需要动态更新存储库中的 JSON 文件……这对我来说几乎感觉像是一种反模式。

在我上面 link 编辑的网络研讨会中,主持人创建了他的 Docker 图像并使用 CLI 手动将其推送到 ECR。然后他手动将Dockerrun.aws.json文件上传到EB。但是他不需要上传应用程序,因为它已经包含在 Docker 图片中。这对我来说似乎很奇怪,我质疑我是否正确理解了事情。 Dockerrun.aws.json 文件是否需要在每次构建时更改?还是我想的不对?

自从我 post 回答这个问题后的 8 个月里,我学到了很多东西,我们已经转向了不同的更好的技术。但我会 post 我在回答我原来的问题时学到的东西。

Dockerrun.aws.json文件与ECS任务定义几乎完全一样。使用 Beanstalk 的多 Docker 容器部署版本(而不是单个容器)很重要,即使您只部署一个容器也是如此。 IMO 他们应该摆脱 Beanstalk 的单容器平台,因为它毫无用处。但是假设您将 Beanstalk 设置为 Multi-Container Docker 平台,那么 Dockerrun.aws.json 文件看起来像这样:

{
  "AWSEBDockerrunVersion": 2,
  "containerDefinitions": [
    {
      "name": "my-container-name-this-can-be-whatever-you-want",
      "image": "my.artifactory.com/docker/my-image:latest",
      "environment": [],
      "essential": true,
      "cpu": 10,
      "memory": 2048,
      "mountPoints": [],
      "volumesFrom": [],
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/aws/elasticbeanstalk/my-image/var/log/stdouterr.log",
          "awslogs-region": "us-east-1",
          "awslogs-datetime-format": "%Y-%m-%d %H:%M:%S.%L"
        }
      }
    }
  ]
}

如果您决定在未来将整个事情转换为 ECS 服务而不是使用 Beanstalk,那将变得非常容易,因为上面的示例 JSON 直接转换为 ECS 任务定义提取 "containerDefinitions" 部分。因此等效的 ECS 任务定义可能如下所示:

[
  {
    "name": "my-container-name-this-can-be-whatever-you-want",
    "image": "my.artifactory.com/docker/my-image:latest",
    "environment": [
      {
        "name": "VARIABLE1",
        "value": "value1"
      }
    ],
    "essential": true,
    "cpu": 10,
    "memory": 2048,
    "mountPoints": [],
    "volumesFrom": [],
    "portMappings": [
      {
        "hostPort": 0,
        "containerPort": 80
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/aws/ecs/my-image/var/log/stdouterr.log",
        "awslogs-region": "us-east-1",
        "awslogs-datetime-format": "%Y-%m-%d %H:%M:%S.%L"
      }
    }
  }
]

此处的主要区别在于,对于 Beanstalk 版本,您需要将端口 80 映射到端口 80,因为 Beanstalk 上的 运行 Docker 的限制是您无法在同一实例上复制容器,而在 ECS 中则可以。这意味着在 ECS 中,您可以将容器端口映射到主机端口 "zero,",这实际上只是告诉 ECS 在短暂范围内选择一个随机端口,从而允许您在单个实例上堆叠容器的多个副本。其次ECS如果要传入环境变量,需要直接注入Task DefinitionJSON。在 Beanstalk 世界中,您不需要将环境变量放在 Dockerrun.aws.json 文件中,因为 Beanstalk 有一个单独的工具用于在控制台中管理环境变量。

事实上,Dockerrun.aws.json 文件应该只是被认为是一个模板。因为 Beanstalk 上的 Docker 在后台使用 ECS,所以它只是将您的 Dockerrun.aws.json 作为模板并使用它来生成自己的任务定义 JSON,它将托管环境变量注入到最后的 "environment" 属性 JSON.

我第一次问这个问题时遇到的一个大问题是每次部署时是否都必须更新这个 Dockerrun.aws.json 文件。我发现,这归结为您希望如何部署事物的选择。你可以,但你不必。如果您编写 Dockerrun.aws.json 文件以便 "image" 属性 引用 :latest Docker 图像,则无需更新该文件。您需要做的就是反弹 Beanstalk 实例(即重新启动环境),它将从 Artifactory(或 ECR,或您发布图像的任何其他地方)中提取任何可用的 :latest Docker 图像。因此,构建管道需要做的就是将 :latest Docker 图像发布到您的 Docker 存储库,然后使用 awscli 触发 Beanstalk 环境的重启,命令如下这个:

$ aws elasticbeanstalk restart-app-server --region=us-east-1 --environment-name=myapp

但是,这种方法有很多缺点。如果您有一个 dev/unstable 分支将 :latest 图像发布到同一个存储库,那么如果环境碰巧自行重新启动,您将面临部署该不稳定分支的风险。因此,我建议对 Docker 标签进行版本控制,并且只部署版本标签。因此,不是指向 my-image:latest,而是指向 my-image:1.2.3 之类的东西。这确实意味着您的构建过程必须在每次构建时更新 Dockerrun.aws.json 文件。然后你还需要做的不仅仅是一个简单的重启应用程序服务器。

在这种情况下,我写了一些 bash 脚本,利用 jq utility 以编程方式更新 JSON 中的 "image" 属性,用当前构建版本替换字符串 "latest" 。然后我必须调用 awsebcli 工具(请注意,这是一个不同于普通 awscli 工具的包)来更新环境,如下所示:

$ eb deploy myapp --label 1.2.3 --timeout 1 || true

我在这里做了一些骇人听闻的事情:不幸的是,eb deploy 命令需要永远。 (这是我们切换到纯 ECS 的另一个原因;Beanstalk 慢得令人难以置信。)该命令在整个部署时间内挂起,在我们的例子中可能需要 30 分钟或更长时间。这对于构建过程来说是完全不合理的,所以我强制该过程在 1 分钟后超时(它实际上继续部署;它只是断开我的 CLI 客户端和 returns 一个失败代码给我,即使它随后可能会成功)。 || true 是一种 hack,它有效地告诉 Gitlab 忽略失败退出代码,并假装它成功了。这显然是有问题的,因为没有办法判断 Elastic Beanstalk 部署是否真的失败了;我们假设它永远不会。

关于使用 eb deploy 的另一件事:默认情况下,此工具会自动尝试压缩构建目录中的所有内容,并将整个 ZIP 文件上传到 Beanstalk。你不需要那个;您只需要更新 Dockerrun.aws.json。为了做到这一点,我的构建步骤是这样的:

  • 使用 jq 更新 Dockerrun.aws.json 带有最新版本标签的文件
  • 使用zip创建一个名为deploy.zip的新ZIP文件并将Dockerrun.aws.json放入其中
  • 确保名为 .elasticbeanstalk/config.yml 的文件已就位(如下所述)
  • 运行 eb deploy ... 命令

然后您需要在 .elasticbeanstalk/config.yml 的构建目录中创建一个如下所示的文件:

deploy:
  artifact: deploy.zip
global:
  application_name: myapp
  default_region: us-east-1
  workspace_type: Application

awsebcli 知道在您调用 eb deploy 时自动查找此文件。这个特定文件所说的是寻找一个名为 deploy.zip 的文件,而不是尝试压缩整个目录本身。

所以 :latest 部署方法是有问题的,因为你可能会部署一些不稳定的东西;部署的版本化方法是有问题的,因为部署脚本更复杂,并且因为除非你希望你的构建管道花费 30 多分钟,否则部署有可能不会成功并且真的没有办法告诉(除了自己监控每个部署)。

无论如何,设置起来要多一些工作,但我建议您尽可能迁移到 ECS。 (最好还是迁移到 EKS,尽管这需要做更多的工作。)Beanstalk 有很多问题。