我在生产中(在AWS上)有一项服务,该服务遵循不可变的服务器模式。其部署如下所示:


使用Packer创建新的AMI

创建新的CloudFormation堆栈,从大小为1的自动缩放组开始。 br />
当我看到新版本很好时,我可以增加实例数量并最终关闭旧实例,最后从以前的版本中删除CloudFormation堆栈。

在我的在部署的初始版本中,我仅使用了一个堆栈,并对其进行了适当的更新。对于正常版本,这意味着CloudFormation修改了自动伸缩组,以指向新的AMI。然后,我必须杀死一个现有实例或增加自动伸缩组,以使该实例运行该发行版。

每个发行版都使用一个新堆栈会使我的过程更简单,因为回滚更容易并且使向部分用户发布版本更加容易。与不变服务器模式类似,我避免就地更新,而只创建新资源(在这种情况下为堆栈)。

在我工作的公司中,现在更常见的是使用Terraform而不是CloudFormation。我想知道是否有可能熟练运用Terraform进行草图绘制。我不介意使用其他工具,我的主要观点是我希望保留以下基本概念:


允许在不影响稳定版本的情况下部署新版本
(就地)更新设置,仅创建新资源并杀死旧资源。

到目前为止,我仅与Terraform进行过短暂合作,并仅使用它来管理我们基础架构的一小部分。根据建议,我将状态保存在S3存储桶中,例如:

# (from main.tf)
terraform {
  required_version = ">= 0.9.4"
  backend "s3" {
    bucket = "example-company-terraform-state"
    key    = "/foo-service/terraform.tfstate"
    region = "eu-central-1"
  }
}


此处,密钥始终是固定的。因此,Terraform将更新所有内容。我假设您可以为新版本使用新密钥,以产生与创建新CloudFormation堆栈类似的效果。

我没有弄清楚如何将流量转移到新实例。所有实例都应位于负载均衡器(ELB)的后面。我认为,您可以为ELB使用单独的设置,该设置将流量分配给旧版本和新版本的实例。

因此,如果推出新版本,将会有三个不同的Terraform状态:


ELB的地形设置
旧版本的地形设置(更具体地说,它的自动缩放组)
新版本的地形设置发布

问题:


对Terraform状态使用不同的S3键的方法是否与使用单独的CloudFormation堆栈具有相同的效果? (我正在寻找一种具有相互不干扰的多种Terraform设置的方法。)
您是否看到了将所有实例(从旧版本和新版本中删除)的问题的解决方案共享的ELB?
我读到可以在Terraform中导出资源。您是否认为,我可以为ELB创建Terraform设置,将ELB导出为资源,并在实例(加上自动缩放组)的Terraform设置中使用它,以使它们附加到共享的ELB? >

(注意:对于我使用CloudFormation的其他服务,我们不使用ELB,因此严格来说这是一个不同的问题。我仅提及它是为了解释我发现对部署以及为什么要考虑将某些想法应用于其他服务。)

#1 楼

有几种不同的方法可以实现这种目标,每种方法都有一些不同的权衡。我将在下面描述最常见的方法。


最简单的方法是将Terraform的create_before_destroy机制与自动缩放组配合使用。 aws_launch_configuration文档中包含此模式的示例。在这种情况下,更改AMI ID会导致重新创建启动配置。由于create_before_destroy,首先创建了新配置,然后创建了新的自动伸缩组,并将新实例添加到连接的ELB中。 min_elb_capacityaws_autoscaling_group参数可用于确保在考虑要创建的自动伸缩组之前,已连接的ELB中存在给定数量的实例,并且运行状况良好,从而将旧的自动伸缩组的破坏和启动配置延迟到新的自动伸缩组被破坏为止。服务请求。

这种方法的缺点是缺乏控制。由于Terraform将整个更改集视为一次运行,因此在创建新实例后可以暂停以允许执行其他检查,然后再销毁旧实例是不可能的。因此,ELB运行状况检查是确定新版本是否“良好”的唯一输入,一旦旧资源被销毁就无法回滚。


第二个常见问题方法是采用一种“蓝色/绿色部署”模式,对两个集群进行显式更改。这是通过将所有每个释放资源放入子模块中,然后使用不同的参数实例化该模块两次来完成的。在顶级模块中,它类似于以下内容:

resource "aws_elb" "example" {
  instances = "${concat(module.blue.ec2_instance_ids, module.green.ec2_instance_ids)}"

  # ...
}

module "blue" {
  source = "./app"

  ami_id = "ami-1234"
  count  = 10
}

module "green" {
  source = "./app"

  ami_id = "ami-5678"
  count  = 0
}


此处的工作原理是,在“稳定状态”(未进行部署)中,这些模块中只有一个模块的计数为非零,而另一个模块的计数为零。在部署期间,它们都设置为相同的非零计数,但具有不同的ami_id值。每个部署交换其中一个模块为“活动”模块,而两个模块在部署期间均处于活动状态。

使用此方法时,每个步骤都是不同的Terraform操作:


将非活动模块的更改计数设置为非零,并设置其AMI ID。
使用Terraform进行更改,从而激活新模块
验证新版本是否良好
更改计数的旧模块设置为零
应用Terraform进行更改,从而停用旧的模块

尽管这具有更多的步骤,但是它允许在步骤3中进行任意验证并花费任意时间

由于旧群集和新群集都存在于同一配置中,因此存在错误使用此模式的风险,并且过早破坏活动集群。可以通过仔细查看Terraform的计划以确保它不影响旧集群来缓解这种情况,但是Terraform本身不能保证这一点。

此外,由于两个集群都使用相同的子模块配置,因此在保留蓝色/绿色分隔的同时对配置进行更新可能很棘手。如果进行了需要Terraform替换运行实例的更改,则必须在磁盘上临时拥有模块代码的两个副本,使source参数指向单独的副本,然后仅对非活动模块使用的副本进行更改。


我将介绍的最后一种方法是最极端和最手动的方法,但是它在满足您的要求和保持控制方面做得最好。实际上,这是对您当前CloudFormation工作流程的最直白的解释,并且是您在问题中谈到的方法的更具体版本。

在这种方法中,有两种完全分开的方法Terraform配置,我将其称为“与版本无关”(必须在版本之间生存的事物,例如ELB)和“特定于版本”(为每个新版本重新创建的资源)。

与版本无关的配置将包含ELB,并且您怀疑会导出其ID,以供特定于版本的配置使用:

terraform {
  required_version = ">= 0.9.4"
  backend "s3" {
    bucket = "example-company-terraform-state"
    key    = "exampleapp/version-agnostic"
    region = "eu-central-1"
  }
}

resource "aws_elb" "example" {
  # ...
}

output "elb_id" {
  value = "${aws_elb.example.id}"
}


此配置可以照常进行初始化,计划和应用,从而创建一个没有附加实例即可启动的ELB。

版本特定的配置将类似于先前方法中的“ app”子模块,但这时间作为顶层模块。此模块的后端配置将省略S3密钥,因为对于每个新发行版,此密钥都会按您的预期进行更改:

terraform {
  required_version = ">= 0.9.4"
  backend "s3" {
    bucket = "example-company-terraform-state"
    region = "eu-central-1"
  }
}


然后可以设置特定的密钥(或重新设置)在运行terraform init时:

$ terraform init -reconfigure -backend-config="key=exampleapp/20170808-1"


这里我选择使用“当前日期,发布索引”元组作为发布的标识符。通过使用该参数的新值运行terraform init,可以创建一个完全独立的状态,与上一个状态无关。使用-reconfigure告诉Terraform您不希望将旧状态迁移到新状态,而只是直接切换到新状态路径,可能会在此过程中创建新状态。

您可以然后运行terraform show确认状态确实为空(因此操作不会影响现有资源),然后像往常一样运行计划/应用周期。

对新版本感到满意后,您可以切换回先前的版本并销毁它。

特定于版本的配置将需要与版本无关的配置中的ELB ID,以便按顺序进行填充load_balancersaws_autoscaling_group属性。要访问此数据,我们可以使用terraform_remote_state数据源从S3中的状态读取值:

data "terraform_remote_state" "version_agnostic" {
  backend = "s3"
  config {
    bucket = "example-company-terraform-state"
    key    = "exampleapp/version-agnostic"
    region = "eu-central-1"
  }
}

resource "aws_autoscaling_group" "example" {
  # ...

  load_balancers = ["${data.terraform_remote_state.version_agnostic.elb_id}"]
}


对于这种复杂的系统,最好通过某种包装脚本或业务流程来运行Terraform,以减轻发布过程的负担。例如,这样的脚本可能会自动生成新的版本号,以避免操作员误输入日期或意外地与现有版本冲突的风险。在指南“在自动化中运行Terraform”中,有一些有关通过脚本运行Terraform的建议和警告。尽管第三个选项是CloudFormation方法中最直接的映射,但第二个选项则更多之所以常用,是因为它在控制和工作流开销之间达成了合理的折衷。

评论


谢谢,这是一个很好的总结。我将不得不对此进行更多考虑,但这对我理解问题和可能的解决方案很有帮助。在当前情况下,第二种方法可能最有意义。

– PhilippClaßen
17年8月8日在23:23

我要说的是,使用诸如Ansible之类的工具进行编排对于第二个和第三个选项至关重要,这样可以避免自动部署在很大程度上成为手动部署,从而失去了您所指出的Terraform的某些优势。值得考虑在此解决方案中使用多少Terraform-Ansible可以调用Terraform模块,但如果它们与部署事件相关,它也可以运行AWS操作(直接或通过CLI),而不是需要Terraform管理状态。

– RichVel
20年1月8日在15:25