Когда писать свой
Хороший вопрос «нужен ли мне свой провайдер».
Да:
- У тебя свой 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
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
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
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:
provider_installation { dev_overrides {"example/mysite" = "/home/student/go/bin"
}
direct {}}
go install .
cd /path/to/test-config
terraform plan # использует binary из ~/go/bin
Terraform скачает provider, посмотрит на dev_override, найдёт локальный binary, использует. Iterate цикл, компилировать, plan'ать.
Тесты
Terraform даёт terraform-plugin-testing:
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"),),
},
},
})
}
Запуск:
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.