介绍
在 ClickHouse,我们致力于对 ClickHouse Cloud 的开发采用 API 优先的方法。用户通过用户界面可以执行的每个操作也应该可以通过脚本语言来实现,从而可以被其他系统利用。这意味着我们最近发布的 Cloud API 也是一个产品,它具有关于其行为方式的契约(通过 swagger),我们的用户可以依靠它。虽然我们现有的用户非常期待这个 API 的发布,以满足诸如自动化配置和取消配置、计划缩放和灵活配置管理等需求,但它也让我们能够开始与工具集成:从 Terraform 开始。
在这篇博文中,我们探讨了我们新的 Terraform 提供程序 以及如何使用它来满足一个常见的要求:需要针对 ClickHouse 实例进行测试的系统的 CI/CD。在我们的示例中,我们了解了如何将我们的 go 客户端测试从单体 ClickHouse Cloud 服务迁移到使用 Terraform 和仅在测试期间配置临时服务。这不仅让我们能够降低成本,还可以在客户端和调用之间隔离我们的测试。我们希望其他人能够从这种模式中受益,并将其带入他们的测试基础设施,从而节省成本并简化流程!
Terraform
Terraform 是 HashiCorp 创建的一个开源基础设施即代码软件工具,它允许用户使用称为 HashiCorp 配置语言 (HCL) 或可选的 JSON 的声明性配置语言来定义基础设施。
基础设施即代码是指通过机器可读的定义文件而不是物理硬件配置或交互式配置工具来管理和配置计算资源的过程。这种方法已经几乎普遍被接受,成为管理云计算资源的手段。Terraform 已经获得了广泛的用户群和广泛的采用,成为一个以声明性方式实现此过程的工具。
为了与 Terraform 集成并允许用户配置 ClickHouse Cloud 服务,必须实现一个 提供程序插件,并且理想情况下通过 Hashicorp 注册中心 提供。
身份验证
由于 ClickHouse 提供程序依赖于 ClickHouse API,因此需要一个身份验证密钥来配置和管理服务。用户可以通过 ClickHouse Cloud 界面创建一个令牌,以及一个秘密。以下显示了这个简单的过程。
用户还应记录他们的组织 ID,如上所示。
使用提供程序
创建令牌和秘密后,用户可以创建一个 .tf
文件并声明提供程序的使用。为了避免将凭据放在主文件中,token_key
、token_secret
和 organization_id
被替换为 Terraform 变量。这些变量反过来可以在 secret.tfvars
文件中指定,该文件不应提交到源代码控制。
main.tf
terraform {
required_providers {
clickhouse = {
source = "ClickHouse/clickhouse"
version = "0.0.2"
}
}
}
variable "organization_id" {
type = string
}
variable "token_key" {
type = string
}
variable "token_secret" {
type = string
}
provider clickhouse {
environment = "production"
organization_id = var.organization_id
token_key = var.token_key
token_secret = var.token_secret
}
secret.tfvars
token_key = "<token_key>"
token_secret = "<token_secret>"
organization_id = "<organization_id>"
假设用户已经 安装了 Terraform,则可以使用 terraform init
安装提供程序。
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding clickhouse/clickhouse versions matching "0.0.2"...
- Installing clickhouse/clickhouse v0.0.2...
- Installed clickhouse/clickhouse v0.0.2 (self-signed, key ID D7089EE5C6A92ED1)
Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
配置好提供程序后,我们可以通过在上面的文件中添加几行 HCL 代码来部署 ClickHouse Cloud 服务。
variable "service_password" {
type = string
}
resource "clickhouse_service" "service" {
name = "example-service"
cloud_provider = "aws"
region = "us-east-2"
tier = "development"
idle_scaling = true
password = var.service_password
ip_access = [
{
source = "0.0.0.0/0"
description = "Anywhere"
}
]
}
output "CLICKHOUSE_HOST" {
value = clickhouse_service.service.endpoints.0.host
}
这里我们指定了所需的云提供程序、区域和层级。层级可以是开发或生产。开发层级代表 ClickHouse Cloud 中的入门产品,适用于较小的工作负载和入门项目。对于上面的示例,我们启用了空闲,这样我们的服务在未使用时就不会消耗成本。
我们还必须指定一个服务名称和可以访问此服务的 IP 地址列表(在我们的示例中为任何地方),以及一个服务密码。我们再次将其抽象为一个变量,指向我们的秘密文件。启用
idle_scaling
是开发层级实例的唯一有效值,即它不能被禁用。提供程序的未来版本将验证此设置。
我们的输出声明在 CLICKHOUSE_HOST
输出变量中捕获了我们的服务的端点,确保在服务就绪后获得连接详细信息非常简单。完整的示例 main.tf 文件可以在 此处 找到。
配置此服务只需要一个命令,terraform apply
,并使用选项 -var-file
传递我们的秘密。
terraform apply -var-file=secrets.tfvars
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# clickhouse_service.service will be created
+ resource "clickhouse_service" "service" {
+ cloud_provider = "aws"
+ endpoints = (known after apply)
+ id = (known after apply)
+ idle_scaling = true
+ ip_access = [
+ {
+ description = "Anywhere"
+ source = "0.0.0.0/0"
},
]
+ last_updated = (known after apply)
+ name = "example-service"
+ password = (sensitive value)
+ region = "us-east-2"
+ tier = "development"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
clickhouse_service.service: Creating...
clickhouse_service.service: Still creating... [10s elapsed]
clickhouse_service.service: Still creating... [20s elapsed]
clickhouse_service.service: Still creating... [30s elapsed]
clickhouse_service.service: Still creating... [40s elapsed]
clickhouse_service.service: Still creating... [50s elapsed]
clickhouse_service.service: Still creating... [1m0s elapsed]
clickhouse_service.service: Still creating... [1m10s elapsed]
clickhouse_service.service: Creation complete after 1m12s [id=fd72178b-931e-4571-a0d8-6fb1302cfd4f]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
CLICKHOUSE_HOST = "gx75qb62bi.us-east-2.aws.clickhouse.cloud"
如上所示,Terraform 基于定义构建了一个计划,然后配置了服务。由于我们之前的输出配置,我们的服务分配的 hostname 也会被打印出来。可以使用 terraform destroy
删除上述服务。
为了让 Terraform 对一组资源应用更改,它需要一种方法来获取其当前状态,包括已配置的资源及其配置。这在“状态”中描述,其中包含对资源的完整描述。这允许随着时间的推移对资源进行更改,每个命令都能够确定要采取的适当操作。在我们的简单情况下,我们将在运行上述命令的文件夹中本地保存此状态。然而,状态管理是一个 更复杂的话题,有许多方法可以维护它,以适合现实环境,包括使用 HashiCorp 的云产品。当预计多个个人或系统在任何给定时间对状态进行操作,并且需要并发控制时,这一点尤其重要。
CI/CD - 一个实际的例子
将 Terraform 添加到 Github Actions
针对 ClickHouse Cloud 进行测试对于向我们的用户提供高质量的客户端至关重要。在 Terraform 提供程序可用之前,我们的 ClickHouse 客户端针对 ClickHouse Cloud 中的单个服务进行测试,测试由 Github Actions 编排。这个实例是在我们的客户端之间共享的,每个客户端在 PR 或提交到存储库时都会创建自己的数据库和表。虽然这已经足够了,但它也存在一些局限性。
- 单点故障。这个服务的任何问题,例如由于区域可用性,都会导致所有测试失败。
- 冲突资源。虽然通过确保所有资源(例如,表)都遵循使用客户端名称和时间戳的命名约定来避免这种情况,但这仍然存在一些后果(见下文)。
- 资源增长与测试复杂性。确保测试能够并发运行意味着确保特定测试使用的表、数据库和用户是唯一的,以避免冲突 - 这需要在所有客户端中保持一致的样板代码。当与客户端需要 大量测试 以确保 ClickHouse 功能覆盖率相结合时,这意味着可能创建数百个表。需要进一步的测试编排来确保每个客户端在完成时都删除这些表,以避免表数量激增 - 也许并不意外,ClickHouse 不是为 10,000 个表而设计的!
- 成本低效 - 虽然上述测试的查询负载并不大,但我们的服务实际上一直处于活动状态,并且由于大量并发 DDL 操作,可能会导致很高的 ZooKeeper 负载。这意味着我们使用了生产服务。此外,我们的测试需要对空闲状态具有鲁棒性,以防服务能够关闭。
- 可观察性复杂性 - 随着客户端数量的增加以及多个测试同时运行,使用服务器日志调试测试失败变得更加复杂。
Terraform 提供程序承诺为这些问题提供一个简单的解决方案,每个客户端只需在测试开始时创建一个服务,运行其测试套件,并在完成时销毁服务。因此,我们的测试服务成为短暂的。
这种方法具有以下优点
- 测试隔离 - 虽然测试仍然容易受到某个区域中 ClickHouse 云不可用的影响,但它们对服务问题已变得健壮,例如,客户端触发 ClickHouse 错误导致服务范围内的错误,或者客户端测试进行服务范围内的配置更改。我们客户端的测试立即被隔离。
- 没有资源增长,测试更简单 - 我们的服务只在测试运行期间存在。客户端开发人员现在只需要考虑由于自己的测试并发导致的资源冲突。他们还可以对整个服务进行配置更改,从而简化测试。
- 成本低效 - 可以创建更小的(开发)服务,并且仅存在几分钟(大多数情况下少于 10 分钟),从而最大程度地降低成本。
- 简单的可观察性 - 虽然我们在测试完成后会销毁服务,但会记录服务 ID。如果需要,这可以用于在我们的可观察性系统中检索服务器日志。
现有工作流程
对于我们的第一个客户端,我们选择了 Clickhouse Go,它使用简单的 Github 操作 以及封装在代码测试套件中的许多测试复杂性。
Github 操作提供了一个简单的基于工作流的 CI/CD 平台。通过与 Github 的紧密集成,用户只需在
.github/workflow
目录下以声明方式在 yml 文件中创建工作流,每个工作流都包含要运行的作业。这些作业由步骤组成,可以配置为 按计划运行或针对特定事件运行,例如 PR。
现有的云测试包含一个配置为针对上述单片服务运行的作业。测试套件已经支持通过环境变量 CLICKHOUSE_HOST
和 CLICKHOUSE_PASSWORD
指定应执行测试的 ClickHouse 实例。这些通过 Github 密钥填充。这还需要将环境变量 CLICKHOUSE_USE_DOCKER
设置为 false 以禁用现有的基于 Docker 的测试。
除了这些特定更改之外,云测试类似于基于 Docker 的单节点测试 - 使用矩阵来测试客户端与不同的 Go 版本,以及在运行测试之前签出代码并安装 Go 的步骤。
integration-tests-cloud:
runs-on: ubuntu-latest
strategy:
max-parallel: 1
fail-fast: true
matrix:
go:
- "1.19"
- "1.20"
steps:
- uses: actions/checkout@main
- name: Install Go ${{ matrix.go }}
uses: actions/[email protected]
with:
stable: false
go-version: ${{ matrix.go }}
- name: Run tests
env:
CLICKHOUSE_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST }}
CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD }}
CLICKHOUSE_USE_DOCKER: false
CLICKHOUSE_USE_SSL: true
run: |
CLICKHOUSE_DIAL_TIMEOUT=20 CLICKHOUSE_TEST_TIMEOUT=600s CLICKHOUSE_QUORUM_INSERT=3 make test
新工作流程
在迁移工作流之前,我们需要一个简单的 Clickhouse 服务的 Terraform 资源定义。以下内容基于之前相同的示例,在开发层中创建服务,但引入了用于 organization_id
、token_key
、token_secret
、service_name
和 service_password
的变量。我们还输出服务 ID 以帮助以后调试,并允许我们的服务从任何地方访问 - 短暂的性质意味着安全风险很低。以下 main.tf
文件存储在 clickhouse-go
客户端的根目录 中。
terraform {
required_providers {
clickhouse = {
source = "ClickHouse/clickhouse"
version = "0.0.2"
}
}
}
variable "organization_id" {
type = string
}
variable "token_key" {
type = string
}
variable "token_secret" {
type = string
}
variable "service_name" {
type = string
}
variable "service_password" {
type = string
}
provider clickhouse {
environment = "production"
organization_id = var.organization_id
token_key = var.token_key
token_secret = var.token_secret
}
resource "clickhouse_service" "service" {
name = var.service_name
cloud_provider = "aws"
region = "us-east-2"
tier = "development"
idle_scaling = true
password = var.service_password
ip_access = [
{
source = "0.0.0.0/0"
description = "Anywhere"
}
]
}
output "CLICKHOUSE_HOST" {
value = clickhouse_service.service.endpoints.0.host
}
output "SERVICE_ID" {
value = clickhouse_service.service.id
}
Terraform 支持通过以 TF_VAR_
为前缀的环境变量指定变量值。例如,要填充组织 ID,我们只需设置 TF_VAR_organization_id
。
与我们之前的工作流程类似,这些环境变量的值可以使用 Github 加密密钥 填充。在我们的例子中,我们在组织级别创建这些密钥,以便它们可以在创建于同一 ClickHouse 云帐户中的所有客户端和服务之间共享,从而实现简单的管理。
注意:我们这里没有服务名称的值。除了不敏感之外,我们还希望确保这些名称在测试运行中是唯一的,以便我们可以识别服务的来源和创建时间。
为了使 Terraform 在运行程序上可用,我们使用 hashicorp/setup-terraform
操作。这将在 Github 操作 CLI 运行程序上安装 Terraform 并公开其 CLI,以便我们可以像从终端进行调用一样进行调用。
我们的最终工作流如下所示
integration-tests-cloud:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
strategy:
max-parallel: 1
fail-fast: true
matrix:
go:
- "1.19"
- "1.20"
steps:
- name: Check Out Code
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/[email protected]
with:
terraform_version: 1.3.4
terraform_wrapper: false
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Set Service Name
run: echo "TF_VAR_service_name=go_client_tests_$(date +'%Y_%m_%d_%H_%M_%S')" >> $GITHUB_ENV
- name: Terraform Apply
id: apply
run: terraform apply -no-color -auto-approve
env:
TF_VAR_organization_id: ${{ secrets.INTEGRATIONS_TEAM_TESTS_ORGANIZATION_ID }}
TF_VAR_token_key: ${{ secrets.INTEGRATIONS_TEAM_TESTS_TOKEN_KEY }}
TF_VAR_token_secret: ${{ secrets.INTEGRATIONS_TEAM_TESTS_TOKEN_SECRET }}
TF_VAR_service_password: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD }}
- name: Set Host
run: echo "CLICKHOUSE_HOST=$(terraform output -raw CLICKHOUSE_HOST)" >> $GITHUB_ENV
- name: Service Id
run: terraform output -raw SERVICE_ID
- name: Install Go ${{ matrix.go }}
uses: actions/[email protected]
with:
stable: false
go-version: ${{ matrix.go }}
- name: Run tests
env:
CLICKHOUSE_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD }}
CLICKHOUSE_USE_DOCKER: false
CLICKHOUSE_USE_SSL: true
run: |
CLICKHOUSE_DIAL_TIMEOUT=20 CLICKHOUSE_TEST_TIMEOUT=600s CLICKHOUSE_QUORUM_INSERT=2 make test
- name: Cleanup
if: always()
run: terraform destroy -no-color -auto-approve
env:
TF_VAR_organization_id: ${{ secrets.INTEGRATIONS_TEAM_TESTS_ORGANIZATION_ID }}
TF_VAR_token_key: ${{ secrets.INTEGRATIONS_TEAM_TESTS_TOKEN_KEY }}
TF_VAR_token_secret: ${{ secrets.INTEGRATIONS_TEAM_TESTS_TOKEN_SECRET }}
TF_VAR_service_password: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD }}
总而言之,此工作流包含以下步骤
- 通过
uses: actions/checkout@v3
将现有代码签出到运行程序。 - 通过
uses: hashicorp/[email protected]
在运行程序上安装 Terraform。 - 调用
terraform init
来安装 ClickHouse 提供程序。 - 通过
terraform validate
命令验证签出代码根目录中的 Terraform 资源定义文件。 - 将环境变量
TF_VAR_service_name
设置为以go_client_tests_
为前缀的日期字符串。这确保了我们的服务在所有客户端和测试运行中具有唯一的名称,并有助于调试。 - 运行
terraform apply
来创建一个具有指定密码的云服务,组织 ID、令牌和密钥通过环境变量传递。 - 将 CLICKHOUSE_HOST 环境变量设置为上一步应用步骤输出的值。
- 捕获服务 ID 以用于调试目的。
- 根据当前矩阵版本安装 Go。
- 运行测试 - 注意
CLICKHOUSE_HOST
已在上面设置。敏锐的读者会注意到,我们像之前的工作流一样将环境变量传递给make test
,以增加超时时间。但是,我们将CLICKHOUSE_QUORUM_INSERT
降低到2
。这是必需的,因为某些测试需要数据在所有节点上存在才能进行查询。虽然我们之前的单片服务有三个节点,但我们较小的开发服务只有两个节点。 - 通过
terraform destroy
命令销毁服务,无论工作流的成功与否(if: always()
)。
这些更改现在已经生效!每当对存储库发布 PR 或提交时,更改都将在短暂的 ClickHouse 云集群上进行测试!
目前,这些测试不会针对从 fork 提交的 PR 和分支运行(这需要 ClickHouse 组织的成员)。这是 Github 的标准策略,适用于 pull_request 事件,因为它可能会导致密钥泄露。我们计划在未来的增强功能中解决这个问题。
结论
在这篇博文中,我们使用了 ClickHouse 云的新 Terraform 提供程序来构建 Github 操作中的 CI/CD 工作流,该工作流为测试提供短暂的集群。我们使用这种方法来降低使用 ClickHouse 云进行客户端测试的成本和复杂性。