使用 CDK for Terraform 和 Go 部署基础架构
我今年早些时候加入了 Sourcegraph的团队,负责我们的本地部署和云产品!
在 Sourcegraph,我们的目标是构建“Google for Code”,让每个人都更容易访问代码。许多公司利用 Sourcegraph,因此开发人员可以轻松搜索代码、自动大规模更改代码和跟踪代码更改等等! Sourcegraph 为客户提供两种部署选项,云多租户产品和本地部署。尽管我们非常努力地确保您的私有代码在多租户产品中的安全,但许多企业客户仍然更喜欢高度隔离并选择本地安装。可悲的是,部署和维护生产 Sourcegraph 实例并非易事。此外,并非每家公司都有必要的资源来维护另一个系统(尤其是在您维护其他人的系统时)。
在一次公司黑客马拉松中,我们一群人决定构建一个“神奇的实例制造商”,让任何人都可以在 Google Cloud Platform (GCP) 上的共享基础架构中一键部署完全托管的单租户 Sourcegraph 实例。用户只需向我们提供一个名称,我们将神奇地“制作”一个实例并返回新 Sourcegraph 部署的神奇 URL。
架构
就像任何其他基于 Web 的生产系统一样,提供功能齐全的 Sourcegraph 部署有很多移动部件。您需要计算资源、存储资源、DNS、HTTP(TLS 证书)等等。
Kubernetes 是我们支持的安装方法之一用于大规模部署,Kubernetes 拥有一个惊人的生态系统,用于各种基础设施自动化。我们使用我们的实验性Helm 图表在共享的 Google Kubernetes Engine (GKE) 集群上部署 Sourcegraph。对于数据存储,我们尽可能使用 GCP 上的托管服务,例如 Cloud SQL 和 Google Cloud Storage (GCS)。对于 DNS 和 TLS,我们主要依赖 Cloudflare。我们如何自动化这么多东西? Terraform(呃)。使用 Terraform,我们可以(通过提供者)提供各种资源,并且免费提供状态管理。
问题
我们都喜欢 Terraform 或基础设施即代码 (IaC)。与 ClickOps 不同,它是声明式管理基础架构的绝佳工具,并且(希望)它是可重现的。但是,Terraform (HCL) 是静态的,我们通常只是将 HCL 文件提交到git
存储库中。在我们的用例中,我们需要在没有人工干预的情况下动态配置资源。不幸的是,Terraform 没有提供任何开箱即用的解决方案来以编程方式创建新模块并应用更改。
用您最喜欢的编程语言之一声明 terraform 模块不是很好吗?此外,您将对生成的资源有更多的控制权,而使用普通的 Terraform,您会受到 HCL 语言约束的约束。 TerraformCDK(cdktf) 是解决此问题的实验性尝试。
我们在 Go 中使用 cdktf 来实现该项目。为什么 Go 和 Terraform? Go 是 Sourcegraph 的首选语言,而 Terraform 是我们每天都在使用的东西(因此我们没有使用pulumi之类的东西)。
它是如何工作的?
如需完整教程,请查看 Hashicorp 官方教程。下面的代码片段肯定不会编译。
首先需要创建一个cdktf.json配置文件。用于配置providers
和modules
。
{
"language": "go",
"app": "go run main.go",
"terraformProviders": [
{
"name": "google",
"source": "hashicorp/google",
"version": "~> 4.15.0"
},
{
"name": "cloudflare",
"source": "cloudflare/cloudflare",
"version": "~> 3.11.0"
}
]
// ...
}
进入全屏模式 退出全屏模式
它相当于HCL中的以下内容,
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.15.0"
}
}
}
进入全屏模式 退出全屏模式
然后您需要运行cdktf get
,它将为cdktf.json
中的提供程序动态生成 go 包。您稍后可以在代码中导入这些包以声明您的 Terraform 模块。
我的模块是什么样的?
import (
"fmt"
"github.com/aws/constructs-go/constructs/v10"
jsii "github.com/aws/jsii-runtime-go"
"github.com/hashicorp/terraform-cdk-go/cdktf"
"github.com/sourcegraph/magic-instance-maker/generated/google"
"github.com/sourcegraph/magic-instance-maker/generated/cloudflare"
"github.com/sourcegraph/magic-instance-maker/generated/helm"
)
func NewStack(scope constructs.Construct, id string) cdktf.TerraformStack {
stack := cdktf.NewTerraformStack(scope, &id)
// Configure remote backend to store terraform state
cdktf.NewGcsBackend(stack, &cdktf.GcsBackendProps{
Bucket: jsii.String("gcs-bucket-name"),
Prefix: jsii.String(fmt.Sprintf("tenants/%s", id)),
})
// Configure gcp provide, this is equivalent to the `provider` block
google.NewGoogleProvider(stack, jsii.String("google"), &google.GoogleProviderConfig{
Zone: jsii.String("region-name"),
Project: jsii.String("project-id"),
})
// This is equivalent to the data source block `data "google_sql_database_instance" "cloud-sql-instance" {}`
cloudSqlDatabaseInstance := google.NewDataGoogleSqlDatabaseInstance(stack, jsii.String("cloud-sql-instance"), &google.DataGoogleSqlDatabaseInstanceConfig{
Project: jsii.String("project-id"),
Name: &cloudSqlInstanceId,
})
sqlUser := google.NewSqlUser(stack, jsii.String("sql-user"), &google.SqlUserConfig{
Project: jsii.String(projectId),
Name: jsii.String(fmt.Sprintf("%s-admin", id)),
Password: cloudSqlAdminPassword.Result(),
Instance: cloudSqlDatabaseInstance.Name(),
Type: jsii.String("BUILT_IN"),
})
cloudsqlPgsqlDbDependencies := []cdktf.ITerraformDependable{sqlUser}
cloudSqlPgsqlDb := google.NewSqlDatabase(stack, jsii.String("pgsql"), &google.SqlDatabaseConfig{
Project: jsii.String(projectId),
Name: jsii.String(fmt.Sprintf("%s-pgsql", id)),
Instance: cloudSqlDatabaseInstance.Name(),
DependsOn: &cloudsqlPgsqlDbDependencies,
})
helm.NewHelmProvider(stack, jsii.String("helm"), &helm.HelmProviderConfig{
Kubernetes: &helm.HelmProviderKubernetes{
ConfigPath: jsii.String("KUBECONFIGPATH"),
ConfigContext: jsii.String("CLUSTERNAME"),
},
})
// We provision Sourcegraph deployment using our experimental helm chart
// https://docs.sourcegraph.com/admin/install/kubernetes/helm
helm.NewRelease(stack, jsii.String("release"), &helm.ReleaseConfig{
Repository: jsii.String("https://sourcegraph.github.io/deploy-sourcegraph-helm/"),
Chart: jsii.String("sourcegraph"),
Name: jsii.String(id),
Namespace: jsii.String(id),
CreateNamespace: jsii.Bool(true),
Values: jsii.Strings("values-file-a-yaml-string"),
})
// Configure cloudflare provide, this is equivalent to the `provider` block
cloudflare.NewCloudflareProvider(stack, jsii.String("cloudflare"), &cloudflare.CloudflareProviderConfig{
ApiToken: jsii.String("cloudflare-api-token"),
})
// This is equivalent to `resource "cloudflare_record" "magic-example-com" {}`
cloudflare.NewRecord(stack, jsii.String("magic-example-com"), &cloudflare.RecordConfig{
ZoneId: jsii.String("cloudflare-zone-id"),
Name: jsii.String(fmt.Sprintf("magic-%s.example.com", id)),
Type: jsii.String("A"),
Value: nginxIngressIpAddress.Address(),
Proxied: jsii.Bool(true),
})
进入全屏模式 退出全屏模式
上面的 Go 代码大致翻译为,
variable "tenant_id" { type = string }
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.15.0"
}
}
}
terraform {
backend "gcs" {
bucket = "gcs-bucket-name"
# this actually won't work in terraform
# backend block doesn't allow interpolations
prefix = "tenants/${var.tenant_id}"
}
}
provider "google" { }
data "google_cloud_sql_instance" "cloud-sql-instance" {
name = ""
}
resource "cloudflare_record" "magic-example-com" {
name = "magic-${var.tenant_id}.example.com"
# ...
}
进入全屏模式 退出全屏模式
你如何实际应用你在 go 中定义的模块或堆栈?
您可以使用cdktf-cli
执行此操作,然后只运行cdktf deploy
。但是我们需要动态配置堆栈,并且我们不想“脱壳”,
func main() {
tempDir, err := os.MkdirTemp("", "magic-instance-maker-")
if err != nil {
return nil, err
}
defer os.RemoveAll(tempDir)
app := cdktf.NewApp(&cdktf.AppOptions{Outdir: jsii.String(tempDir)})
sharedtenant.NewStack(app, name, cluster)
app.Synth()
}
进入全屏模式 退出全屏模式
现在让我们尝试运行go run main.go
。等等,它什么也没做?app.Synth()
调用仅将 Go 中定义的模块合成到 JSON 文件中,而不实际应用它。 HCL 的伟大之处在于它大部分都可以与 JSON 互换。实际上,如果您将cd
转换为tempDir
,则可以运行通常的terraform init
和terraform apply
命令来应用您的 terraform 模块。这正是cdktf deploy
命令在幕后所做的,它只是为您运行terraform
命令。不幸的是,在应用 terraform 模块时,我们仍然不得不使用 terraform CLI。
我们将利用这个漂亮的包装器hashcorp/terraform-exec来应用来自 Go 的合成 Terraform 模块。
import (
"context"
"log"
"os"
"path/filepath"
jsii "github.com/aws/jsii-runtime-go"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
"github.com/hashicorp/terraform-cdk-go/cdktf"
"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
)
func main() {
tempDir, err := os.MkdirTemp("", "magic-instance-maker-")
if err != nil {
return nil, err
}
defer os.RemoveAll(tempDir)
app := cdktf.NewApp(&cdktf.AppOptions{Outdir: jsii.String(tempDir)})
NewStack(app, name, cluster)
app.Synth()
installer := &releases.ExactVersion{
Product: product.Terraform,
Version: version.Must(version.NewVersion("1.1.4")),
}
execPath, err := installer.Install(context.Background())
if err != nil {
log.Fatalf("error installing Terraform: %s", err)
}
workingDir := filepath.Join(tempDir, "stacks", name)
tf, err := tfexec.NewTerraform(workingDir, execPath)
if err != nil {
log.Fatalf("error running NewTerraform: %s", err)
}
err = tf.Init(context.Background(), tfexec.Upgrade(true))
if err != nil {
log.Fatalf("error running Init: %s", err)
}
err = tf.Apply(context.Background())
if err != nil {
log.Fatalf("error running Apply: %s", err)
}
}
进入全屏模式 退出全屏模式
再次运行go run main.go
,您的所有资源都应该处于活动状态。
想法
cdktf 是一个非常酷的项目,它允许我们对基础设施进行实际“编码”,它提供了一种更方便的方式来以编程方式与 Terraform 交互。您可以使用典型的流控制或您已经熟悉的任何约定,而不是受到 Terraform 自己的 DSL (HCL) 的限制。
现在,有什么问题?
cdktf get
的性能问题
我们在cdktf.json
中只有几个提供程序,并且该命令仍然需要很长时间才能完成。这可能与每次运行时编译大量提供程序的代码有关。以下是一些不科学的基准:
在满负荷的 M1 Max MacBook 上,
Generated go constructs in the output directory: generated
________________________________________________________
Executed in 75.17 secs fish external
usr time 92.10 secs 63.00 micros 92.10 secs
sys time 12.05 secs 863.00 micros 12.05 secs
进入全屏模式 退出全屏模式
适用于 Mac 的 Docker,
[cdktf-builder 6/6] RUN --mount=type=cache,target=/tmp/terraform-plugin-cache cdktf get 854.4s
进入全屏模式 退出全屏模式
我们还尝试了缓存提供程序,但没有帮助。也许当 Hashicorp 开始为 Go 发布预建提供程序时,这可以大大改善,或者我们是否可以维护自己的注册表?
构建时的性能问题
免责声明:我绝不是 Go 专家,您必须对编译器进行一些优化
在我们的 Go 程序中引入 cdktf 之前,在 docker 映像构建期间构建二进制文件大约需要 15 秒。添加cdktf后,构建需要两分钟多。
而且,如果要构建一个linux/amd64
的镜像,编译需要15分钟以上!当然,这主要是由于qemu
仿真性能不佳所致。
更快的周转是开发速度和开发人员幸福的关键:(
cdktf 在 Go 中的不直观用法
您可能会注意到我们之前定义的模块/堆栈中的任何地方都使用了jsii,并且 Go 库需要与底层node
运行时交互。
缺乏 Go 支持
Go 示例的文档并不多。我们主要依靠阅读 TypeScript 示例和生成的提供程序代码的自动完成来编写模块。同样,Go 是缺少个预构建提供程序的 cdktf 支持的语言之一。
最后的话
只需写TypeScript!
cdktf 仍是一个早期项目,对 Go 的支持仍处于试验阶段。这个想法很棒,但我还不习惯在实际项目中使用它。 Go 端口绝对值得更多的爱 :)
正如我之前提到的,HCL 和 JSON 大多是可互换的,因此我们也可以使用任何编程语言或使用HCL 解析器手工制作 JSON 来表示 Terraform 模块。
更多推荐
所有评论(0)