Развертывание инфраструктуры с помощью CDK для Terraform с Go

Я присоединился к Sourcegraph в начале этого года в команду, отвечающую за наше развертывание на местах и облачные предложения!

В Sourcegraph мы стремимся создать «Google для кода» и сделать код более доступным для всех. Многие компании используют Sourcegraph, чтобы разработчики могли легко искать код, автоматизировать изменения кода в масштабе, отслеживать изменения кода и многое другое! Sourcegraph предлагает клиентам два варианта развертывания: облачное многопользовательское предложение и локальное. Хотя мы очень стараемся обеспечить безопасность вашего частного кода в многопользовательских предложениях, многие корпоративные клиенты все же предпочитают высокую степень изоляции и выбирают установку на месте. К сожалению, развертывание и поддержка производственного экземпляра Sourcegraph не является тривиальной задачей. Кроме того, не у каждой компании есть необходимые ресурсы для обслуживания еще одной системы (особенно если вы обслуживаете чужую систему).

В ходе хакатона, организованного компанией, мы решили создать «волшебный экземпляр», который позволит любому человеку одним щелчком мыши развернуть полностью управляемый однопользовательский экземпляр Sourcegraph в общей инфраструктуре на Google Cloud Platform (GCP). Пользователям нужно только сообщить нам имя, и мы волшебным образом «создадим» экземпляр и вернем волшебный URL-адрес свежего развертывания Sourcegraph.

Архитектура

Для обеспечения полнофункционального развертывания Sourcegraph, как и любой другой производственной веб-системы, необходимо множество движущихся частей. Вам нужны вычислительные ресурсы, ресурсы хранения, DNS, HTTP (сертификаты TLS) и многое другое.

Kubernetes — один из поддерживаемых нами методов установки для крупномасштабного развертывания, а Kubernetes обладает удивительной экосистемой для автоматизации различных инфраструктур. Мы развертываем Sourcegraph на общем кластере Google Kubernetes Engine (GKE), используя нашу экспериментальную схему Helm. Для хранилищ данных мы по возможности используем управляемые сервисы на GCP, такие как Cloud SQL и Google Cloud Storage (GCS). Для DNS и TLS мы в основном полагаемся на Cloudflare. Как мы автоматизируем так много вещей? Terraform (да). С помощью Terraform мы можем предоставлять все виды ресурсов (по провайдерам), а управление состоянием предоставляется бесплатно.

Проблема

Нам всем нравится Terraform или Infrastructure as Code (IaC). Это отличный инструмент для декларативного управления инфраструктурой, и (надеюсь) он воспроизводим, в отличие от ClickOps. Однако Terraform (HCL) статичен, и мы обычно просто фиксируем файлы HCL в репозитории git. В нашем случае нам необходимо динамически предоставлять ресурсы без вмешательства человека. К сожалению, Terraform не предоставляет никакого готового решения для программного создания нового модуля и применения изменений.

Разве не было бы здорово объявлять модули Terraform с помощью одного из ваших любимых языков программирования? Кроме того, у вас будет гораздо больше контроля над генерируемыми ресурсами, в то время как в обычном Terraform вы связаны ограничениями языка HCL. CDK for Terraform (cdktf) — это экспериментальная попытка решить эту проблему.

Для реализации проекта мы использовали cdktf на языке Go. Почему Go и Terraform? Go — это основной язык в Sourcegraph, а Terraform — это то, что мы используем каждый день (поэтому мы не стали использовать такие вещи, как pulumi).

Как это работает?

Для получения полного руководства вам следует ознакомиться с официальным руководством Hashicorp. Приведенные ниже фрагменты кода определенно не будут компилироваться.

Во-первых, вам нужно создать конфигурационный файл cdktf.json. Он используется для настройки провайдеров и модулей.

{
  "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, который динамически сгенерирует go-пакеты для провайдеров в cdktf.json. Позже вы сможете импортировать эти пакеты в свой код, чтобы объявить свой модуль 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.

Мы воспользуемся этой красивой оберткой hashicorp/terraform-exec для применения синтезированного модуля Terraform из Go.

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. Вы можете использовать типичный контроль потока или любые другие соглашения, с которыми вы уже знакомы, вместо того, чтобы быть ограниченным собственным DSL (HCL) Terraform.

Итак, в чем же загвоздка?

Проблемы производительности с cdktf get

У нас всего несколько провайдеров в cdktf.json, а команда все еще требует значительного времени для завершения. Это может быть связано с компиляцией большого количества кода провайдеров при каждом запуске. Ниже приведен ненаучный бенчмарк:

На максимальном MacBook M1 Max,

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
Вход в полноэкранный режим Выход из полноэкранного режима

Docker для Mac,

[cdktf-builder 6/6] RUN --mount=type=cache,target=/tmp/terraform-plugin-cache cdktf get  854.4s
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы также пробовали кэшировать провайдеры, но это не помогло. Возможно, это можно будет значительно улучшить, когда Hashicorp начнет публиковать готовые провайдеры для Go, или если мы сможем вести собственный реестр?

Проблемы с производительностью во время сборки

Оговорка: я ни в коем случае не являюсь экспертом в Go, и наверняка есть какая-то оптимизация, которую можно сделать в компиляторе.

До внедрения cdktf в нашу Go-программу сборка бинарного файла во время сборки образа докера занимала около 15 секунд. После добавления cdktf сборка занимает более двух минут.

Более того, если вы хотите собрать образ linux/amd64, то на компиляцию уйдет более 15 минут! Конечно, в основном это связано с низкой производительностью эмуляции qemu.

Более быстрые сроки — ключ к скорости разработки и счастью разработчиков 🙁

Неинтуитивное использование cdktf в Go

Вы можете заметить использование jsii везде в модуле/стеке, который мы определили ранее, и он необходим для взаимодействия библиотеки Go с базовой средой выполнения node.

Отсутствие поддержки Go

Существует не так много документации для примера Go. Для написания модуля мы в основном полагаемся на чтение примеров TypeScript и автозаполнение кода сгенерированных провайдеров. Опять же, Go — один из поддерживаемых cdktf языков, для которого не хватает готовых провайдеров.

Заключительные слова

Просто пишите на TypeScript!

cdktf — все еще ранний проект, и поддержка Go все еще экспериментальная. Идея отличная, но я бы пока не стал использовать ее в реальном проекте. Порт Go определенно заслуживает большей любви 🙂

Как я уже говорил, HCL и JSON в основном взаимозаменяемы, поэтому мы также можем вручную создать JSON для представления модуля Terraform с помощью любого языка программирования или использовать парсер HCL.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *