博客 / 工程

使用 ClickHouse Cloud 和 Terraform 进行 CI/CD

author avatar
Dale McDiarmid
2023年7月13日 - 17 分钟阅读

简介

在 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 界面创建一个令牌以及一个密钥。下面显示了这个简单的过程

Markdown Image

用户还应记录其组织 ID,如图所示。

使用提供程序

创建令牌和密钥后,用户可以创建一个 .tf 文件并声明提供程序的使用。为了避免将凭据放在主文件中,token_keytoken_secretorganization_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 的入门级产品,适用于较小的工作负载和入门项目。在上面的示例中,我们启用了空闲,这样我们的服务在未使用时不会消耗成本。

启用 idle_scaling 是开发层级实例的唯一有效值,即它无法禁用。未来版本的提供程序将验证此设置。

我们还必须指定服务名称和 IP 地址列表,服务将从这些 IP 地址访问(在我们的示例中为任何地方),以及服务的密码。我们再次将其抽象为我们 secrets 文件中的一个变量。

我们的输出声明将服务的端点捕获在 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 在配置服务之前,会根据定义构建一个计划。分配给我们服务的主机名也会打印出来,这要归功于我们之前的输出配置。terraform destroy 可以用来删除上述服务。

为了使 Terraform 将更改应用于一组资源,它需要一种获取其当前状态的方法,包括已配置的资源及其配置。这在“状态”中描述,其中包含资源的完整描述。这允许随着时间的推移对资源进行更改,每个命令都能够确定要采取的适当操作。在我们的简单示例中,我们将此状态本地保存在运行上述命令的文件夹中。然而,状态管理是一个更为复杂的主题,有许多方法可以维护它,使其适用于实际环境,包括使用 HashiCorp 的云产品。当期望多个个人或系统在任何给定时间对状态进行操作并且需要并发控制时,这一点尤其重要。

CI/CD - 一个实际示例

将 Terraform 添加到 Github Actions

针对 ClickHouse Cloud 进行测试对于为我们的用户提供高质量的客户端至关重要。在 Terraform 提供程序可用之前,我们的 ClickHouse 客户端是针对 ClickHouse Cloud 中的单个服务进行测试的,测试由 Github Actions 编排。此实例在我们的客户端之间共享,每个客户端在对存储库进行 PR 或提交时都会创建自己的数据库和表。虽然这已经足够了,但它存在一些限制

  • 中心故障点。此服务的任何问题,例如由于区域可用性问题,都将导致所有测试失败。
  • 资源冲突。虽然通过确保所有资源(例如表)都遵循使用客户端名称和时间戳的命名约定来避免这种情况,但这会产生一些后果(见下文)。
  • 资源增长和测试复杂性。确保测试可以并发运行意味着确保特定测试使用的表、数据库和用户是唯一的,以避免冲突 - 这需要在客户端之间保持一致的样板代码。当与客户端需要大量测试以确保 ClickHouse 中的功能覆盖率相结合时,这意味着可能需要创建数百个表。还需要进一步的测试编排,以确保每个客户端在完成后删除这些表,以避免表爆炸 - 也许不足为奇的是,ClickHouse 并非为 1 万个表而设计的!
  • 成本效率低下 - 虽然上述测试的查询负载不大,但我们的服务实际上始终处于活动状态,并且由于大量并发 DDL 操作而可能受到较高的 zookeeper 负载。这意味着我们使用了生产服务。此外,我们的测试需要对空闲状态具有鲁棒性,以防服务能够关闭。
  • 可观察性复杂性 - 随着客户端数量的增多以及多个测试的运行,使用服务器日志调试测试失败变得更加复杂。

Terraform 提供程序承诺为这些问题提供一个简单的解决方案,每个客户端只需在测试开始时创建一个服务,运行其测试套件,并在完成后销毁该服务。因此,我们的测试服务变成了临时的。

github_actions_architectures.png

这种方法有许多优点

  • 测试隔离 - 虽然测试仍然容易受到某个区域中 ClickHouse Cloud 不可用的影响,但它们已经变得对服务问题具有鲁棒性,例如,客户端触发 ClickHouse 错误导致服务范围问题,或者客户端测试进行服务范围配置更改。我们客户端的测试立即被隔离。
  • 无资源增长和更简单的测试 - 我们的服务仅在测试运行的生命周期内存在。客户端开发人员现在只需要考虑由于他们自己的测试并发性而可能导致的资源冲突。他们还可以对整个服务进行配置更改,从而可能简化测试。
  • 成本效率低下 - 可以创建更小的(开发)服务,并且仅存在几分钟(在大多数情况下 < 10 分钟),从而最大限度地降低成本。
  • 简单的可观察性 - 虽然我们在测试完成后销毁服务,但服务 ID 会被记录下来。如果需要,这可以用于在我们的可观察性系统中检索服务器日志。

现有工作流程

对于我们的第一个客户端,我们选择了 Clickhouse Go,它具有简单的 Github Actions,并且许多测试复杂性都封装在代码的测试套件中。

Github Actions 提供了一个基于工作流程的简单 CI/CD 平台。通过与 Github 的紧密集成,用户只需在 .github/workflow 目录下的 yml 文件中以声明方式创建工作流程,每个工作流程都包含要运行的作业。这些作业由步骤组成,可以配置为在计划时间或特定事件(例如 PR)时运行。

现有的 Cloud 测试由一个配置为针对上述单体服务运行的作业组成。测试套件已经支持通过环境变量 CLICKHOUSE_HOSTCLICKHOUSE_PASSWORD 指定应该在其上执行测试的 ClickHouse 实例。这些环境变量通过 Github Secrets 填充。这也需要将环境变量 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_idtoken_keytoken_secretservice_nameservice_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 Cloud 账户中创建的客户端和服务之间共享,以实现简单的管理。

repo_secrets.png

注意:我们这里没有服务名称的值。除了不敏感之外,我们还想确保这些名称对于测试运行是唯一的,以便我们可以识别服务的来源和创建时间。

为了使 Terraform 在 runner 上可用,我们使用了 hashicorp/setup-terraform action。这会在 Github Actions CLI runner 上安装 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 }}

总而言之,此工作流程包含以下步骤

  1. 通过 uses: actions/checkout@v3 将现有代码检出到 runner。
  2. 通过 uses: hashicorp/[email protected] 在 runner 上安装 Terraform。
  3. 调用 terraform init 以安装 ClickHouse 提供程序。
  4. 通过terraform validate 命令验证检出代码根目录中的 Terraform 资源定义文件。
  5. 将环境变量 TF_VAR_service_name 设置为以 go_client_tests_ 为前缀的日期字符串。这确保了我们的服务在客户端和测试运行中具有唯一的名称,并有助于调试。
  6. 运行 terraform apply 以创建一个具有指定密码的 Cloud 服务,组织 ID、令牌和密钥通过环境变量传递。
  7. CLICKHOUSE_HOST 环境变量设置为上一个 apply 步骤输出的值。
  8. 捕获服务 ID 以用于调试。
  9. 根据当前矩阵版本安装 Go。
  10. 运行测试 - 请注意,上面已设置了 CLICKHOUSE_HOST。细心的读者会注意到,我们像之前的工作流程一样将环境变量传递给 make test,以增加超时时间。但是,我们将 CLICKHOUSE_QUORUM_INSERT 降低到 2。这是必需的,因为某些测试需要在查询之前在所有节点上都存在数据。虽然我们之前的单体服务有三个节点,但我们较小的开发服务只有两个节点。
  11. 通过 terraform destroy 命令销毁服务,无论工作流程是否成功(if: always())。

这些更改现已生效!每当向存储库发出 PR 或提交时,都会针对临时 ClickHouse Cloud 集群测试更改!

git_actions.png

目前,这些测试不适用于从 fork 和仅分支提出的 PR(这需要 ClickHouse 组织的成员)。这是 pull_request 事件的标准 Github 策略,因为它可能会泄漏密钥。我们计划在未来的增强功能中解决此问题。

结论

在这篇博文中,我们使用了新的 ClickHouse Cloud Terraform 提供程序在 Github Actions 中构建了一个 CI/CD 工作流程,该工作流程配置了用于测试的临时集群。我们使用这种方法来降低客户端使用 ClickHouse Cloud 进行测试的成本和复杂性。

分享此文章

订阅我们的新闻通讯

随时了解功能发布、产品路线图、支持和云产品!
正在加载表单...
关注我们
X imageSlack imageGitHub image
Telegram imageMeetup imageRss image