# Свой Terraform-провайдер, Plugin Framework _Advanced · TerraformLab Knowledge Base_ **TL;DR:** Свой provider, Go через terraform-plugin-framework. Модель: Provider → Resource (CRUD) или DataSource (R). Каждый ресурс, Schema + методы Create/Read/Update/Delete. Core общается с provider по gRPC. Деплой: `go install` + dev_overrides, или Terraform Registry. Нужен когда нет готового provider для твоего API. ## Когда писать свой Хороший вопрос «нужен ли мне свой провайдер». **Да:** - У тебя свой API (внутренний SaaS, отдельная компания). - Существующий community-провайдер заброшен. - Нужны provider-specific фичи которых нет (custom diff-logic). **Нет:** - REST API простой и используется в одном проекте, `data "http"` или `terraform_data` + local-exec справятся. - Можно через `null_resource` с CLI, не идеально, но работает. Свой provider, обязательство. Версионирование, документация, community-issues. Думай дважды. ## Plugin Framework vs Plugin SDK v2 Два пути: | | Plugin Framework | Plugin SDK v2 | |---|---|---| | Год | 2022+ | 2014+ | | Статус | Активная разработка | Maintenance only | | Type-safety | Лучше | Хуже | | Schema | Декларативная | Map-based | | Recommended | Да, для нового | Только legacy-migration | Для новых провайдеров, Plugin Framework. SDK v2, старый, но многие крупные провайдеры (включая `hashicorp/aws`) ещё на нём. ## Минимальный provider ### main.go ```go package main import ( "context" "log" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/example/terraform-provider-mysite/internal/provider" ) func main() { err := providerserver.Serve(context.Background(), provider.New("dev"), providerserver.ServeOpts{Address: "registry.terraform.io/example/mysite"}, ) if err != nil { log.Fatal(err.Error()) } } ``` ### internal/provider/provider.go ```go package provider import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" ) type MySiteProvider struct { version string } func New(version string) func() provider.Provider { return func() provider.Provider { return &MySiteProvider{version: version} } } func (p *MySiteProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { resp.TypeName = "mysite" resp.Version = p.version } func (p *MySiteProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "api_token": schema.StringAttribute{ Required: true, Sensitive: true, }, "endpoint": schema.StringAttribute{ Optional: true, }, }, } } func (p *MySiteProvider) Configure(_ context.Context, _ provider.ConfigureRequest, _ *provider.ConfigureResponse) { // парсим конфиг, инициализируем HTTP-клиент } func (p *MySiteProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ NewPostResource, } } func (p *MySiteProvider) DataSources(_ context.Context) []func() datasource.DataSource { return nil } ``` ### internal/provider/post_resource.go ```go package provider import ( "context" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) type PostResource struct { // HTTP-клиент к API } func NewPostResource() resource.Resource { return &PostResource{} } type PostResourceModel struct { ID types.String `tfsdk:"id"` Title types.String `tfsdk:"title"` Body types.String `tfsdk:"body"` } func (r *PostResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_post" } func (r *PostResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{Computed: true}, "title": schema.StringAttribute{Required: true}, "body": schema.StringAttribute{Required: true}, }, } } func (r *PostResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan PostResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } // POST /posts // plan.ID = ответ API plan.ID = types.StringValue("mocked-id-123") resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } func (r *PostResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // GET /posts/{id}, обновляем state } func (r *PostResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // PUT /posts/{id} } func (r *PostResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // DELETE /posts/{id} } ``` ## Локальная разработка, dev_overrides Не хочешь пушить в Registry каждый запуск. `~/.terraformrc`: ```hcl provider_installation { dev_overrides { "example/mysite" = "/home/student/go/bin" } direct {} } ``` ```bash go install . cd /path/to/test-config terraform plan # использует binary из ~/go/bin ``` Terraform скачает provider, посмотрит на dev_override, найдёт локальный binary, использует. Iterate цикл, компилировать, plan'ать. ## Тесты Terraform даёт `terraform-plugin-testing`: ```go package provider_test import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) func TestAccPostResource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testAccPostResourceConfig("title-1", "body-1"), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("mysite_post.test", "title", "title-1"), ), }, }, }) } ``` Запуск: ```bash TF_ACC=1 go test ./... -v ``` `TF_ACC=1` нужен, без него acceptance-tests пропускаются. ## Регистрация в Registry Terraform Registry индексирует репо `terraform-provider-X` под org/user. Требования: - GitHub release с правильно подписанными ZIP'ами (Goreleaser). - `docs/` сгенерированные через `tfplugindocs`. - `LICENSE` (MPL-2.0 или MIT). Полный сетап, отдельная статья; в Hashicorp есть детальный walkthrough. ## Подводные камни - **Schema-changes, breaking.** Удалить required-поле = всем пользователям нужен upgrade-path (`moved` или `removed`). Думай про схему сразу. - **`Read` важен.** Если не реализуешь, drift detection не работает, Terraform не видит ушедшие из state ресурсы. Самый частый bug. - **Idempotency на стороне API.** `Create` дважды не должен создавать дубль. Если API не idempotent, fallback в `Read`-before-`Create`. - **Sensitive поля.** Если атрибут содержит секрет, `Sensitive: true` в schema. См. [tf-sensitive](/terraform/kb/tf-sensitive.md). - **gRPC-protocol версии.** Plugin Framework на protocol 6. Старые провайдеры на 5. Mix-mode возможен, но скоро 5 deprecate'нется. - **Versioning.** Semver обязателен. Breaking-changes, major. Любое изменение schema без backward-compat, major bump. - **Документация, собственная инфра.** `tfplugindocs` генерирует markdown из Schema-description'ов. Без хороших description'ов docs бесполезные. - **Тестирование требует API.** Acceptance-тесты реально дёргают API. Mock-сервер или sandbox-окружение нужны для CI. ## Команды ```bash go install . ``` Скомпилировать provider в $GOPATH/bin. Использует dev_overrides из .terraformrc. ```bash TF_ACC=1 go test ./... -v ``` Acceptance-тесты. Реально гоняют terraform plan/apply. ```bash tfplugindocs generate ``` Сгенерировать docs/ для Registry из Schema-description'ов. ```bash goreleaser release --clean ``` Релиз ZIP-ами для всех платформ + GPG-подпись. Для Registry. ## См. также - [DAG в Terraform, как строится граф зависимостей](/terraform/kb/tf-dag-internals.md) - [CDKTF, Terraform на TypeScript/Python](/terraform/kb/tf-cdktf.md)