を用いたインフラストラクチャの展開


I earlier this year to the team responsible for our on-prem deployment and cloud offering!


SourceGraphでは、“コードのGoogle”を構築し、コードを誰よりもアクセスできるように目指しています.多くの企業は、開発者が容易にできるように、ソースを活用しているsearch code , automate code changes at scale , and track code changes , もっと!SourceGraphは顧客の2つの展開オプション、クラウドマルチテナントの提供と事前に提供しています.しかし、我々は本当にハードテナント製品であなたのプライベートコードを安全に保つしようとするが、多くの企業の顧客はまだ高い分離を選択し、プリムのインストールで行くことを選択します.悲しいことに、生産ソースグラフインスタンスの展開と維持は些細な仕事ではありません.さらに、すべての企業が、別のシステムを維持するために必要なリソースを持っているわけではありません.
Hackathonの会社では、Google Cloud Platform(GCP)上の共有インフラストラクチャの中で、完全に管理された単一テナントSourcegraphインスタンスを展開することを可能にする「マジックインスタンスメーカー」を構築することを決めた.ユーザーは私たちに名前を提供する必要があります、そして、我々は魔法的に「インスタンスを作ります」、そして、新鮮なsourcegraph展開の魔法URLを返します.

建築


他の生産のWebベースのシステムと同じように、完全に機能的なソースの展開を提供するために多くの可動部分があります.あなたは、リソース、ストレージリソース、DNS、HTTP(TLS証明書)、および大いに多くを計算する必要があります.
Kubernetesは我々のうちの1つですsupported installation methods 大規模な展開のために、Kubernetesは様々なインフラストラクチャオートメーションのための驚くべき生態系を持っています.我々は、我々の実験を使用して共有Google KubernetesエンジンHelm chart . データストアでは、クラウド・SQLやGoogleクラウド・ストレージ(GCS)などのGCP上でマネージド・サービスを利用する.DNSとTLSのために、我々はほとんどCloudFlareに頼っています.どのように我々はそんなに多くのものを自動化するのですか?地形(duh).terraformでは、すべての種類のリソースを提供することができます(プロバイダによって)、それは無料で状態管理が付属します.

問題


私たちは皆、コード(IAC)として地形やインフラストラクチャが好きです.これは、宣言のインフラストラクチャを管理するための素晴らしいツールであり、(うまくいけば)Clickopsとは異なり、再現性があります.しかし、Terraform(HCL)は静的です、そして、我々は通常ちょうど1でHCLファイルをコミットしますgit リポジトリ.我々のユースケースでは、人間の介入なしに動的にリソースを供給する必要がある.残念なことに、Terraformは新しいモジュールを作成して、変更を適用するために、どんな箱の解決も提供しません.
あなたの大好きなプログラミング言語のいずれかでterraformモジュールを宣言するのは良いことではないでしょうか?さらに、生成されたリソースに対してより多くの制御を行いますが、プレーンのterraformではHCL言語の制約に縛られます.CDK for Terraform ( CDKTF )はこの問題を解決する実験的試みです.
我々は、プロジェクトを実装するためにGOでCDKTFを使用しました.なぜ行くと地形?GoはSourcegraphの言語への試みです、そして、terraformは我々が日常的に使う何かですpulumi ).

どうやって動くの?


For a complete tutorial, you should check out the Hashicorp's official tutorial. Code snippets below definitely won't compile.


まず、作成する必要がありますcdktf.json 設定ファイル.設定に使用されます.providers and 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 , これは動的にプロバイダーの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),
  })
上記のコードは大まかに翻訳します.
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 . 待って、何もしない?The app.Synth() 呼び出しは、実際にそれを適用することなくJSONファイルにGoで定義されたモジュールを合成します.HCLについての大きなことは、それがほとんどJSONと交換可能であるということです.実際、あなたならばcdtempDir , 通常通り走ることができるterraform init and terraform apply terraformモジュールを適用するコマンド.これはまさにcdktf deploy コマンドはシーンの背後にあり、それは単にterraform あなたのコマンド.残念なことに、それはterraformモジュールを適用することになるとき、我々はまだterraform CLIを使用することに戻る必要があります.
この素敵なラッパーを利用しますhashicorp/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 そして、コマンドはまだ終了するのに十分な時間を取る.これはあらゆる実行時に多くのプロバイダーのコードをコンパイルすることと関係があるかもしれません.以下は非科学的なベンチマークです.
マウンドアウトM 1マックス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
マックのためのDocker
[cdktf-builder 6/6] RUN --mount=type=cache,target=/tmp/terraform-plugin-cache cdktf get  854.4s
私たちもcaching providers , しかし、それは助けませんでした.おそらく、これは大幅にHashicorppre-built providers 行くか、我々自身のレジストリを維持することができますか?

ビルド時のパフォーマンス問題


Disclaimer: by no means I am an expert in Go and there must be some optimization you can do to the compiler


我々のGoプログラムでCDKTFを導入する前に、それはどこかに15秒程度かかります.CDKTFを追加した後、それをビルドする2分以上かかります.
また、ビルドする場合linux/amd64 イメージ、それはコンパイルする15分以上かかります!もちろん、これはほとんどのパフォーマンスが悪いためですqemu エミュレーション.
より速いターンアラウンドは開発速度と開発者の幸福の鍵である

囲碁におけるCDKTFの非直感的使用


あなたの使用に気づくかもしれないjsii モジュール/スタックのどこにも定義されていて、囲碁ライブラリが基礎と対話するのが必要ですnode 実行時

サポート不足


Goの例についてはあまりドキュメントがありません.我々は主にモジュールを書くために生成されたプロバイダのコードのタイプスクリプトの例とautocompleteを読むことに頼っています.再び、GOはCDKTFサポートされている言語の一つですpre-built providers .

最終語


Just write TypeScript!


CDKTFはまだ初期のプロジェクトであり、Goのサポートはまだ実験的です.アイデアは素晴らしいですが、私はまだ実際のプロジェクトでそれを使用して快適ではないだろう.go portは間違いなくより多くの愛に値します.
以前に述べたように、HCLとJSONはほとんど交換可能であるので、我々はまた、任意のプログラミング言語を使用してterraformモジュールを表現したり、HCL parser .