[编者的话] 这篇文章解释了如何更细粒度地控制 Terraform, 将基于 Ruby on Rails,Node.js 和 Scala 构建的服务迁移到业务流程平台 (例如 AWS ECS) 以及当服务建立起来时, 如何有效地控制 Terraform 的执行以满足部署需求, CI/CD 需求, 并且能够有效地应用于有状态服务场景.
作者: Payam Moghaddam, 软件作家, 忍者战士
前言
很容易理解如何使用 Terraform https://www.terraform.io/ 设置容器编排平台, 例如 AWS Elastic Container Service(ECS) https://aws.amazon.com/ecs/ . 基本上, 你只需要阅读文档, 理解所涉及的部分, 并像乐高一样把它们整合在一起即可.
Terraform 支持设置所有与 ECS 相关的关键资源. 你只需将各个部分组织在一起.
但不清楚的是, 一旦你的服务建立起来, 如何部署更新后的 Docker 镜像, 特别是管理自己模式的有状态服务. 此外, 如何将其作为持续交付(CD) https://continuousdelivery.com/ 管道的一部分? 例如, 在下面的管道中, 我们如何在部署步骤中使用 Terraform 以自动方式部署构建步骤中构建的最新 Docker 映像, 并且零停机时间, 即使新映像需要模式更新? 这部分并不是那么清楚.
在代码提交 (Source) 上, 构建一个新的 Docker 镜像 (Build), 并将其部署到 ECS(Deploy) 上, 同时处理任何潜在的架构更新.
当我们计划将基于 Ruby on Rails,Node.js 和 Scala 构建的服务迁移到业务流程平台 (例如 AWS ECS) 时, 我们留下了一系列问题, 无法找到明确的答案:
1, 我们如何在持续交付 (CD) 管道中使用 Terraform?
2, 如果我们使用 Terraform 来配置我们的服务, 我们将如何部署包含数据库架构更新的后续更新?
3, 我们如何才能在我们的 CD 中限制 Terraform 所需的权限才能进行代码更新, 而不是像数据库配置更新那样?
4, 我们如何在保持蓝绿部署 https://medium.com/build-acl/modern-cloud-infrastructure-concepts-definitions-841291511c47 的同时实现这一目标?
我们开始想也许 Terraform 无法做到这些, 也许 Terraform 不是这项工作的合适工具. 但这很难接受, Terraform 让我们如此接近我们的目标! 它可以在 ECS 上启动和运行我们的整个服务, 所有这些都在一个单一的 terraform 应用中, 但对于增量代码和模式更新, 我们不得不使用不同的工具? 这不太现实, 对吧?
所以我们进一步挖掘, 并且用一点聪明才智, 我们想出了如何有效地控制 Terraform 的执行以满足我们的需求, 并且能够解决我们的问题. 在这篇文章中, 我想和大家分享我们是如何做到的, 以及我们在这一过程中学到了什么.
本文假设你对 Terraform 有一个基本的了解. 虽然示例是使用 AWS ECS(因为我们在 ACL https://build.acl.com/ 上使用 ECS), 但核心知识与 ECS 无关, 并且通常可以与 Terraform 一起使用.
AWS 弹性容器服务(ECS)
你可能知道 Terraform 是什么, 但 AWS ECS 是什么呢? 它是一个容器编排平台, 你可以在其中声明要运行的 Docker 容器, ECS 会为你找出运行它们的最佳方法. 它与 Terraform 相似, 因为你声明了你想要的东西, ECS 会负责实现它. 虽然 ECS 还有很多内容, 但就本文而言, 我们需要了解的是 ECS 允许我们定义我们想要运行的容器 (即 ECS 任务) 以及如何运行它们(即 ECS 服务). 如果你正在使用 Kubernetes, 你可以通过对比 Kubernetes pods 和服务来理解这篇文章.
命令式与声明式
在大多数情况下, 可以很容易地使用 Terraform 和 ECS 来启动和运行. 有大量的文档和示例可供复制. 然而, 当你想要更细粒度地控制基础设施和容器是如何形成的, 这就变得具有挑战性了. 在某些情况下, 你不仅仅想声明你的需求, 而是希望指定如何满足你的需求. 这对于有状态服务尤为重要. 例如:
1, 在配置数据库时, 你可能希望运行 "种子" 脚本来设置初始表定义, 存储过程等, 以便数据库从一开始就为应用程序的逻辑做好准备.
2, 部署代码更新时, 你可能需要事先执行 "架构更新".
3, 使用持续部署 (CD) 管道应用更新时, 你可能希望将 CD 的影响范围限制为仅更新容器.
4, 最后, 你希望以特定顺序完成所有这些操作, 这样如果发生任何故障, 部署将停止, 以便你可以在继续之前进行调试.
如何使用 Terraform 实现这一点并非易事, 需要一些技巧. 特别是, 它需要使用 Terraform 的 null_resource 资源, 使用 depends_on 显式地定义依赖关系, 以及用目标确定 plan/ apply 执行的范围.
null_resource - 在 Terraform 中执行任意逻辑
尽管 Terraform 提供了大量的程序, 但在某些情况下你需要执行自己的逻辑. 这是 null_resource 派上用场的地方. 直接从文档 https://www.terraform.io/docs/provisioners/null_resource.html 中获取:
null_resource 的行为与任何其他资源完全相同, 因此你可以像配置其他资源一样配置提供程序 https://www.terraform.io/docs/provisioners/index.html , 连接详细信息 https://www.terraform.io/docs/provisioners/connection.html 和其他元参数.
例如, 如果要配置种子数据库, 可以在创建数据库后创建一个 null_resource 执行种子逻辑:
- ```
- variable "password" { default = "password123" }
- resource "aws_db_instance" "example" {
- allocated_storage = 10
- storage_type = "gp2"
- engine = "postgres"
- instance_class = "db.t2.micro"
- name = "example"
- username = "user"
- password = "${var.password}"
- }
- resource "null_resource" "seed" {
- provisioner "local-exec" {
- command = "PGPASSWORD=${var.password} psql --host=${aws_db_instance.example.address} --port=${aws_db_instance.example.port} --username=${aws_db_instance.example.username} --dbname=${aws_db_instance.example.name} < seed.sql"
- }
- }
- ```
在这个简单的示例中, 在创建数据库之后, null_resource 的 local-exec 供应程序将在运行 Terraform 的本地环境中执行. 它将执行 psql 以使用 seed.sql 脚本为数据库设定种子. 在实践中, 你可以在必要时使用其他配置程序和更复杂的脚本来实现相同的功能, 但关键点仍然是相同的: 使用 null_resource, 你可以执行任意逻辑作为常规 Terraform 执行的一部分.
实际上, 使用 null_resource, 你可以在 Terraform 执行的任何时刻注入任意逻辑来控制发生的事情. 这为你提供了极大的自由度和自定义 Terraform 执行的能力, 而无需自定义提供程序. 但 Terraform 如何知道在数据库创建后执行该逻辑的呢? 要理解这一点, 你需要了解 Terraform 的工作原理.
控制资源图
Terraform 执行的一个重要特征是如何创建 "资源图" 来执行其工作. 引言来自:
Terraform 构建所有资源的图形, 并并行化任何非依赖资源的创建和修改. 因此, Terraform 尽可能高效地构建基础架构, 运营商可以深入了解基础架构中的依赖关系.
这个图表允许我们简单地声明我们想要配置的内容并让 Terraform 处理它. 默认情况下, Terraform 会尽可能地并行化, 除非有依赖性阻止它这样做. 因此, 为了控制 Terraform 的执行顺序, 我们需要考虑底层的依赖关系图. 通过适当的依赖关系设置, 我们可以并行或按顺序执行任务. 要显式地定义顺序依赖关系, 可以使用所有 Terraform 资源可用的 depends_on 属性. 例如, 默认情况下, 将并行创建以下三个资源:
- ```
- resource "null_resource" "first" {
- provisioner "local-exec" {
- command = "echo'first'"
- }
- }
- resource "null_resource" "second" {
- provisioner "local-exec" {
- command = "echo'second'"
- }
- }
- resource "null_resource" "third" {
- provisioner "local-exec" {
- command = "echo'third'"
- }
- }
- ```
底层依赖图显示并行执行(简化视图)
但是, 如果我们正确配置 depends_on 属性, 我们可以按顺序创建它们:
- ```
- resource "null_resource" "first" {
- provisioner "local-exec" {
- command = "echo'first'"
- }
- }
- resource "null_resource" "second" {
- depends_on = ["null_resource.first"]
- provisioner "local-exec" {
- command = "echo'second'"
- }
- }
- resource "null_resource" "third" {
- depends_on = ["null_resource.second"]
- provisioner "local-exec" {
- command = "echo'third'"
- }
- }
- ```
底层依赖图显示顺序执行(简化视图)
现在你可能已经开始看到我们如何使用 Terraform 将更新部署到有状态服务的解决方案了: 使用 null_resources, 并使用适当的 depends_on 依赖关系对关键步骤进行顺序执行. 尤其是:
1, 定义要部署的新容器(即更新
aws_ecs_task_definition
资源)
2, 如果需要, 更新数据库架构(例如, 使用模式更新脚本运行 null_resource, 在第 1 步的
aws_ecs_task_definition
上使用 depends_on)
3, 部署新的容器(例如, 从第 1 步更新 aws_ecs_service 以使用新容器, 并在第 2 步中依赖 null_resource)
这可以确保在部署新代码之前我们的数据库架构是最新的. 简直棒极了!
我们离目标越来越近了, 但缺少了一些东西. 对于相当复杂的服务, 你不希望在代码部署期间对所有内容执行 terraform apply. 例如, 你的 Terraform 逻辑可能设置 DNS 条目, 设置 Redis, 设置负载均衡器等, 这些都是你在代码部署期间不希望触及的. 一次更改太多, 它会将不经常更改的资源 (例如 DNS) 与频繁更改的资源 (例如应用程序逻辑) 混合在一起, 并且它需要你的 CD 拥有大量权限. 为了缩小范围, 我们需要使用另一个特性: Terraform 的 - target 执行能力.
有针对性的改变
随着你的基础架构变得越来越复杂, Terraform 资源的数量不断增加, 让 Terraform 以自动化的方式简单地 apply 所有内容变得很危险. 你想减少 apply 的范围. 幸运的是, Terraform 通过 - target 在执行期间 https://www.terraform.io/docs/commands/apply.html#target-resource 为你提供特定资源的选项来解决此问题.
该 - target 选项可用于将 Terraform 的注意力仅集中在一部分资源上.
当我们想要针对单一资源时, 我们很早就使用了这个. 但是, 当我们需要同时部署资源组时(例如 ECS 任务, ECS 服务, null_resource 等), 这是不可行的. 幸运的是, 我们意识到我们可以通过组合 null_resource, 聚合将多个目标聚合为一个 depends_on. 例如:
- resource "null_resource" "deployment" {
- triggers {
- revision = "${var.git_revision}"
- }
- depends_on = [
- "aws_ecs_service.application",
- "aws_ecs_task_definition.application",
- "null_resource.migrate"
- ]
- }
因此, 当我们可以执行
terraform apply -target=null_resource.deployment
时, 由于与此资源相关的资源图, 它也会更新所有关联的部署资源. 在本例中, 它确保只更新我们的
aws_ecs_task_definition
,aws_ecs_service 以及通过
null_resource.migrate
运行任何架构更新. 使用目标资源, 我们可以放心地将 CD 发送到 Terraform apply, 而不会有任何意外的变化.
有了这三个关键部分, 我现在可以与你分享 ACL 如何使用 Terraform 设置其部署的.
把它们放在一起 - 使用 Terraform 进行部署
在 ACL 中, 我们使用 AWS CodePipeline 进行部署. 虽然我将使用 CodePipeline 解释我们的解决方案, 但可以在任何持续交付工具 (例如 Jenkins) 上使用此解决方案. 以下是我们在管道中使用 Terraform 部署的示例服务:
1, 构建: 我们创建服务的 Docker 镜像并推送到 AWS ECR.
2, 准备: 我们使用 Terraform 与目标
null_resource.prepare
为我们的服务创建新的 ECS 任务, 但尚未更新相关的 ECS 服务以使用它们.
3, 批准: 我们准备好所有东西, 但等待明确的审批步骤进行部署. 这使开发团队能够控制, 我们的合规团队可以清楚地了解授权部署的人员.
4, 部署: 我们使用 Terraform 与目标
null_resource.deploy
来运行我们的架构更新脚本(如果有的话), 并更新我们的 ECS 服务以使用新的 ECS 任务.
正如管道演示的那样, 我们使用各种 null_resources 来控制 Terraform 的部署执行. 我们的管道执行环境是我们纯粹为部署目的而构建的 Docker 镜像. 它安装了 Terraform, 有了所需的其他语言和工具, 我们 null_resources 的 local-exec 提供程序能够成功地执行. 例如, 我们在 Ruby 中编写了一个脚本来调度一次性 ECS 任务, 我们用它来运行我们的架构更新脚本. 以下是
null_resource.migrate
使用它并作为部署的一部分运行的资源.
- resource "null_resource" "migrate" {
- triggers {
- revision = "${var.git_revision}"
- database = "${aws_db_instance.application.address}"
- }
- depends_on = ["aws_ecs_task_definition.application"]
- provisioner "local-exec" {
- command = <<EOS
- ruby ./ecs_task_runner.rb
- -f ${aws_ecs_task_definition.application.family}
- -v ${aws_ecs_task_definition.application.revision}
- -e .ecs/db_migrate.sh
- EOS
- }
- }
这种混合自定义逻辑并在正确的时间执行它们的能力, 使我们能够对如何完成部署有很大的控制权. 这也是为什么我们不觉得有必要寻找单独的部署工具, 因为我们可以根据我们的需求定制 Terraform.
额外的知识
在此过程中, 我们了解了一些额外的细节, 我想我将与大家分享, 以防你们走这条路:
1, 非脚本语言 (如 Scala) 可能需要单独的 ECS 任务才能运行迁移脚本. 我们使用 Play 框架 https://www.playframework.com/ , 运行应用程序的 ECS 任务没有执行模式迁移的代码, 因此我们必须创建一个单独的 ECS 任务来运行迁移脚本.
2, 我们在 local-exec 提供程序内部运行 ECS 任务的脚本最初是使用 AWS CLI https://docs.aws.amazon.com/cli/latest/reference/ecs/run-task.html 编写的; 遗憾的是, 我们了解到 AWS CLI 将在 10 分钟后超时. 我们不得不在 Ruby 中重写它们, 以支持更高的限制.
3, 我们有意识地决定将持续集成管道 (CI) 与持续交付管道 (CD) 区分开来. CI 是在专用于 CI 的独立第三方平台上完成的, 团队可以快速轻松地运行其测试套件, 而 CD 则在 AWS CodePipeline 上完成的, 我们的基础架构团队使用它以安全, 合规的方式将代码部署到生产环境中.
结束语
好了, 无需单独的工具, 你就可以使用 Terraform 以蓝绿色方式将你的服务部署到具有高级控制权的 ECS 上了. 这并不意味着 Terraform 应该是所有场景的部署工具. 相反, 它意味着 Terraform 可以成为部署工具, 直到真正需要一个单独的工具.
事实上, 通过保持简单性并在 AWS CodePipeline 中使用 Terraform, 我们可以依赖 AWS 的安全和合规服务, 而不是依赖安全性较低的第三方 CI/CD 服务. 这大大缩短了我们的交付时间, 并开放了我们的基础设施团队的能力, 以专注于其他挑战. AWS 为我们管理的越多越好!
我希望这篇文章解释了如何更细粒度地控制 Terraform. 如果你有任何问题或建议, 请在评论中分享. 感谢你的阅读和评论!
来源: https://juejin.im/entry/5b9101845188255c6003f827