GitOps 作为一种工作流程,非常适合应用程序交付,主要用于 Kubernetes 环境,但也可以用于基础设施。在一个典型的 GitOps 场景中,您可能希望考虑像 Crossplane 这样 Kubernetes 原生的替代方案,而大多数传统基础设施仍然使用 CI/CD 管道。使用 Kubernetes 作为基础创建部署平台有几个好处,但也意味着更多的人需要具备特定的技能。像 Terraform 这样的基础设施即代码工具的优势之一是它易于学习,并且不需要太多的专业知识。
当我的团队构建我们的平台服务时,我们希望每个人都能做出贡献。我们的大部分工程师(如果不是全部的话)每天都在使用 Terraform。他们知道如何创建可以在多种场景和为多个客户使用的 Terraform 模块。虽然有很多自动化 Terraform 的方法,但我们希望尽可能地利用合适的 GitOps 工作流程。
Terraform 控制器如何工作
在寻找使用 Kubernetes 运行 Terraform 的替代方案时,我发现了几种控制器和运算符,但没有一种让我觉得像 Weaveworks 的 tf-controller 那样具有潜力。我们已经在使用 Flux 作为我们的 GitOps 工具。tf-controller 通过利用 Flux 的一些核心功能工作,并且具有 Terraform 部署的自定义资源。源控制器负责获取我们的模块,Kustomize 控制器应用 Terraform 资源,然后控制器启动静态 Pod(称为 runner)来运行您的 Terraform 命令。
Terraform 资源看起来像这样
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: helloworld
namespace: flux-system
spec:
interval: 1m
approvePlan: auto
path: ./terraform/module
sourceRef:
kind: GitRepository
name: helloworld
namespace: flux-system
这里有一些关于规范需要注意的地方。spec 中的 interval 控制控制器启动 runner pod 的频率。然后,它对您的根模块执行 terraform plan
,该根模块由 path 参数定义。
这个特定的资源被设置为自动批准计划。这意味着如果计划与目标系统的当前状态之间存在差异,则会运行一个新的 runner 来自动应用更改。这使得流程尽可能地“GitOps”,但是您可以禁用此功能。如果禁用它,您必须手动批准计划。您可以使用 Terraform Controller CLI 或通过使用对应该应用的提交的引用更新您的清单来做到这一点。有关更多详细信息,请参阅有关手动批准的文档。
tf-controller 利用 Flux 的源控制器。sourceRef
属性用于定义您想要使用的源资源,就像 Flux Kustomization 资源一样。
高级部署
虽然上面的示例有效,但它不是我的团队通常会做的部署类型。当不定义后端存储时,状态将存储在集群中,这对于测试和开发来说是可以的。但是对于生产,我更喜欢将状态文件存储在集群外部的某个地方。我不希望直接在根模块中定义这一点,因为我想在多个部署中重用我们的根模块。这意味着我必须在我们的 Terraform 资源中定义我们的后端。
这是一个我如何设置自定义后端配置的例子。您可以在 Terraform 文档中找到所有可用的后端
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: helloworld
namespace: flux-system
spec:
backendConfig:
customConfiguration: |
backend "azurerm" {
resource_group_name = "rg-terraform-mgmt"
storage_account_name = "stgextfstate"
container_name = "tfstate"
key = "helloworld.tfstate"
}
...
将状态文件存储在集群之外意味着我可以重新部署我们的集群。但是这样就没有存储依赖性。不需要备份或状态迁移。一旦新的集群启动,它就会针对相同的状态运行命令,并且我恢复了正常运行。
另一个高级举措是模块之间的依赖关系。有时我们将部署设计为像一个两级火箭,一个部署设置某些资源,下一个部署使用这些资源。在这些场景中,我们需要确保我们的 Terraform 以这样一种方式编写,以便我们输出作为第二个模块输入所需的任何数据,并确保第一个模块首先成功运行。
这两个例子来自演示依赖关系时使用的代码,所有的代码都可以在我的 GitHub 上找到。为了简洁起见,省略了一些代码
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: shared-resources
namespace: flux-system
spec:
...
writeOutputsToSecret:
name: shared-resources-output
...
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: workload01
namespace: flux-system
spec:
...
dependsOn:
- name: shared-resources
...
varsFrom:
- kind: Secret
name: shared-resources-output
...
在我称之为 shared-resources 的部署中,您会看到我定义了一个 secret,用于存储部署的输出。在本例中,输出如下
output "subnet_id" {
value = azurerm_virtual_network.base.subnet.*.id[0]
}
output "resource_group_name" {
value = azurerm_resource_group.base.name
}
在 workload01 部署中,我首先使用 dependsOn
属性定义我们的依赖关系,这确保了 shared-resources 在调度 workload01 之前成功运行。来自 shared-resources 的输出然后被用作 workload01 的输入,这就是我想等待的原因。
为什么不使用管道或 Terraform Cloud
自动化 Terraform 最常见的方法是使用 CI/CD 管道或 Terraform Cloud。使用管道进行 Terraform 工作得很好,但通常最终需要一遍又一遍地复制管道定义。有一些解决方案可以解决这个问题,但是通过使用 tf-controller,您可以采用更具声明性的方法来定义您希望部署看起来是什么样子,而不是以命令式的方式定义步骤。
Terraform Cloud 已经引入了许多与使用 GitOps 工作流程重叠的功能,但是使用 tf-controller 不会阻止您使用 Terraform Cloud。您可以将 Terraform Cloud 用作部署的后端,只通过 tf-controller 自动化运行。
我的团队使用这种方法的原因是,我们已经使用 GitOps 部署应用程序,并且我们可以更灵活地提供这些功能作为服务。我们可以通过 API 控制我们的实现,使我们的运营商和最终用户更容易访问自助服务。关于我们的平台方法的细节是一个很大的话题,我们将在另一篇文章中再讨论这些。
本文最初发表在作者的博客上,并已获得许可转载。
评论已关闭。