linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/Advanced/tf-provider-development

kb/advanced ── Advanced ── advanced

Свой Terraform-провайдер, Plugin Framework

Свой 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.

view as markdownaka: terraform-custom-provider, terraform-plugin-framework, hashicorp-plugin-sdk

Когда писать свой

Хороший вопрос «нужен ли мне свой провайдер».

Да:

  • У тебя свой 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 FrameworkPlugin 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.

  • 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.

§ см. также

  • tf-dag-internalsDAG в Terraform, как строится граф зависимостейTerraform строит DAG (directed acyclic graph) из ресурсов и их зависимостей. На plan/apply граф «обходится» в топологическом порядке, параллельно где можно (limit, `-parallelism`, дефолт 10). Implicit dependencies, через interpolation; explicit, `depends_on`. Цикл = `Cycle: ...` ошибка. Понимание DAG объясняет почему apply иногда «застревает» и как ускорить большой граф.
  • tf-cdktfCDKTF, Terraform на TypeScript/PythonCDKTF, Terraform на TypeScript/Python/Go/Java/C#. На выходе стандартный HCL + tfstate. Плюсы: типы, autocomplete, циклы и классы. Минусы: ещё одна абстракция, debug сложнее, не все фичи Terraform покрыты 1:1. Оправдан когда нужна программируемая генерация HCL (N stack'ов из CSV, например).
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки