title | sub_title | author | event | location | date | options | theme | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Let's build your own OpenTofu provider |
Introduction to OpenTofu / Terraform Provider Development |
Konstantin Ignatov |
Gophercamp.cz 2025 |
Clubco, Brno, Czech Republic |
Friday, April 25, 2025 |
|
|
This repository contains the materials for the workshop on creating OpenTofu (Terraform) providers.
In this workshop, I’ll guide you through the fundamentals of provider development, culminating in building a minimalistic but functional provider with custom logic. I’ll highlight various features of the relevant SDKs.
Each .md
file is a presentation, and the one you’re reading now is the
introduction to the topic.
The presentations are built for
presenterm
and are meant to be run
in a modern terminal such as Ghostty
or
kitty
.
If you’ve scanned the QR code with the repo URL, you might want to skip to Getting started.
Each exercise has a separate presentation file, and they are numbered.
If you get stuck, feel free to check out the solutions provided in the same
repo. You’ll find the folders NN-solution
, where NN
is the exercise number.
Good luck!
- Scenario: Working on a payment system
- Multiple payment providers
- Various payment routes (through the providers)
- Different rules when to use which provider
- Rules scattered across files, DBs, and switches in external services
- Ensuring complete coverage (all currencies, card types) is complex
- Rules depend on each other: easy to create conflicts
Guess what happened with the system over time, and how everything was working in reality?
%%{
init: {
'themeVariables': {
'fontFamily': 'JetBrainsMonoNL NFM Bold, Fira Code Bold, monospace',
}
}
}%%
sequenceDiagram
actor User
participant App
participant API as Remote service
User ->>+ App: Change things for me
loop Until states match
App ->>+ API: What's the current state?
API ->>- App: Here's the current state
Note over App: Comparing states,<br />What changes are needed?
App ->>+ API: Make these changes
Note over API: Applying changes
API ->>- App: Changes applied
end
App ->>- User: All done! #10004;
This manual loop is often slow and error-prone.
- What happens when someone changes a resource outside your system? (Drift!)
- What happens when it fails halfway through? (Inconsistent state!)
- How do you handle dependencies between resources? (You didn't expect you'd need graph algorithms and topological sort, didn't you?)
%%{
init: {
'themeVariables': {
'fontFamily': 'JetBrainsMonoNL NFM Bold, Fira Code Bold, monospace',
}
}
}%%
sequenceDiagram
actor Developer
participant TF as Terraform/OpenTofu
participant API as Cloud Provider API
Developer ->> TF: tofu init
TF ->> Developer: Providers downloaded
Developer ->> TF: tofu plan
TF ->> API: Check current state
API ->> TF: Calculate changes
TF ->> Developer: Show execution plan
No changes yet!
Use the plan:
%%{
init: {
'themeVariables': {
'fontFamily': 'JetBrainsMonoNL NFM Bold, Fira Code Bold, monospace',
}
}
}%%
sequenceDiagram
actor Developer
participant TF as Terraform/OpenTofu
participant API as Cloud Provider API
participant Infra as Infrastructure
Developer ->> TF: tofu apply
TF ->> API: Create/Update resources
API ->> Infra: Provision resources
Infra ->> API: Confirm changes
API ->> TF: Return results
TF ->> Developer: Show completion summary
No loops!
Feature | Build OpenTofu Provider | Without OpenTofu / Terraform |
---|---|---|
Go Framework | terraform-plugin- {go,sdk } |
✅ Anything you like! |
Error Handling | ||
Error Reporting | ✅ Framework diagnostics |
❌ Implementation effort |
Object Diff | ✅ HCL / SDK ↔ Go helpers | ❌ Implementation effort |
Dependencies | ✅ Handled by OpenTofu | ❌ Implementation effort |
Testing Harness | ✅ Testing utilities | ❌ Custom test harness |
Timeouts | ✅ Built-in | ❌ Implementation effort |
Locking (state) | ✅ Built-in | ❓ Can be done |
CI/CD | ✅ Supported | ❌ Implementation effort |
Visual edit / GUI | ❌ No, only file editing | ✅ Can be done |
Visual diff | ✅ Built-in | ❌ Implementation effort |
Idempotency | ✅ Built-in | ❓ Can be done |
Drift detection | ✅ Built-in | ❌ Implementation effort |
Partial updates | ⭕️ Supported | ❌ Manual work |
State history | ✅ Pluggable | ❓ Can be done |
Transactions | ❌ No, Unsupported | ❓ Can be done. Maybe |
Rollback | ❌ Implementation effort |
- Complex State & Dependencies: managing resources with intricate states or interdependencies. Leverage TF Core's graph management.
- Collaboration: Multiple users manage config. Benefit from built-in state locking and drift detection.
- Standard IaC Practices: Reuse existing Infrastructure as Code workflows, tooling, and CI/CD pipelines.
- Declarative Approach: Defining the desired end state is natural and idempotency is crucial. The framework encourages this.
- Very Simple State Model: Managing items with minimal dependencies or complexity.
- Specific UI/UX Needs: Interactions beyond config files are necessary, like a dedicated GUI.
- Atomic Transactions: Changes must succeed or fail as a single, indivisible unit. OpenTofu doesn't offer this.
- Inherently Imperative Tasks: The core logic involves sequential steps or procedures (e.g., complex data migrations).
- Overriding Tech Constraints: Must use non-Go languages/frameworks or the plugin architecture is unsuitable.
OpenTofu is a community-driven fork of Terraform.
OpenTofu | Terraform | |
---|---|---|
Licensing | MPL 2.0 | BUSL-1.1 |
Copyright | The OpenTofu Authors | HashiCorp, Inc. |
Maintaining organization | Linux Foundation | HashiCorp, Inc. |
Governance | Community-driven | Corporate-driven |
Provider Development Frameworks (as of now, all MPL and HashiCorp):
Repo / Import | |
---|---|
High-level (modern) | github.com/hashicorp/terraform-plugin-framework |
High-level (previous) | github.com/hashicorp/terraform-plugin-sdk |
Low-level (active) | github.com/hashicorp/terraform-plugin-go |
A Provider is the plugin that teaches OpenTofu how to interact with a specific target API or platform (e.g., AWS, Kubernetes, a custom service).
Under the hood:
- Provider is an executable binary
- It's usually written in Go (e.g., using
terraform-provider-framework
). - Distributed via OpenTofu Registry or Terraform Registry.
- OpenTofu runs it and communicates with it via gRPC.
- OpenTofu tells the provider what to create, update and delete
- THe provider calls relevant APIs.
Key Responsibilities:
- Define Schema: Resources, data sources, functions, and the attributes.
- Authenticate: Manages credentials for the target API.
- Manage Resources: Implements the Create, Read, Update, Delete logic.
Core | Provider |
---|---|
Configuration parsing/interpolation | API client implementation |
Dependency graph construction | Resource schema definition |
State file management | CRUD operations |
Plan/apply workflow execution | Authentication handling |
Plugin RPC communication | Error translation |
flowchart LR
Core[OpenTofu Core]
Core -->|RPC| Provider
Core --> State[State Management]
Core --> Plan[Execution Plans]
Provider --> API[API Calls]
Provider --> Resource[Resource CRUD]
HashCorp Configuration Language:
- Human-readable configuration language
- Declarative syntax
- Key-value pairs, blocks, lists
- Used to define infrastructure resources
HCL vs JSON (and alike):
- Comments
- Interpolation
- Functions
- Variables
- References
- Expressions
HCL with a schema can be mapped to JSON and back, with a couple of twists:
- A block in HCL can be either an object or a list of objects, or a set of objects.
- Schema (per provider and global — for
.tf
files themselves) is needed to distinguish that
OpenTofu JSON Configuration Syntax
JSON:
{
"output": {
"bar": {
"value": "${aws_instance.foo}"
}
}
}
Terraform:
output "bar" {
value = aws_instance.foo
}
So, it's not straightforward (e.g., note $
sign in the JSON variant),
but it's a way to think about HCL, and you can use JSON syntax to generate
resource terraform definitions.
mindmap
root((HCL))
Blocks
Provider
::icon(fa fa-plug)
Resource
::icon(fa fa-cubes)
Data
::icon(fa fa-database)
Module
Variable
Output
Locals
Terraform
Moved
Import
Check
Backend
Required_Providers
Expressions
Literals
References
Functions
Operators
Conditionals
For Expressions
Dynamic Blocks
Splat Expressions
Indexing/Slicing
Heredoc Syntax
Attributes
Types
String
Number
Bool
List
Map
Object
Set
Null
Any
Meta-Arguments
count
for_each
depends_on
provider
lifecycle
prevent_destroy
create_before_destroy
ignore_changes
replace_triggered_by
provisioner
connection
["alias (provider)"]
Once you have your provider developed, you can use the full power of HCL and
Terraform
.
For provider, implement only:
- Resource Blocks
- Data Blocks
- Provider Block
Provider
provider "foo" {
provider_attribute = "value"
provider_block {
nested_attribute = 1 + 2
}
}
Data Source
// "foo" is the provider name
data "foo_bar" "hello" {
data_attribute = true
data_source_block {
attribute = "${expression}"
}
}
Provider Function
// assign to some attribute:
x = provider::foo::myfunc(1, 2)
Resource
resource "foo_baz" "yep" {
ref = data.foo_bar.computed_v
all = [0, 1, 2]
}
import {
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/resource"
}
type PlaygroundProvider struct {}
type PlaygroundDataSource struct {}
type PlaygroundResource struct {}
type PlaygroundFunction struct {}
var _ provider.Provider = (*PlaygroundProvider)(nil)
var _ datasource.DataSource = (*PlaygroundDataSource)(nil)
var _ provider.Resource = (*PlaygroundResource)(nil)
var _ function.Function = (*PlaygroundFunction)(nil)
…and you just follow compiler errors telling you what is still missing
Feature | Provider | Datasource | Resource | Function |
---|---|---|---|---|
Schema | ✅ | ✅ | ✅ | ✅ Definition + Return |
Configure | ✅ | ✅ | ✅ | ❌ OpenTofu only, low-level |
Read | ❌ | ✅ | ✅ | ✅ Stateless Run() |
Create | ❌ | ❌ | ✅ | ❌ |
Update | ❌ | ❌ | ✅ | ❌ |
Delete | ❌ | ❌ | ✅ | ❌ |
Import | ❌ | ❌ | ✅ | ❌ |
Alternatives to start learning:
With low-level SDK:
Now that you have an overview of OpenTofu/Terraform provider concepts, it's time to dive into the hands-on exercises!
- Navigate to the First Exercise: The workshop modules are in numbered
markdown files (
NN-*.md
). Begin with the first one. - Follow the Sequence: Proceed through the exercises in numerical order
(
01-functions.md
,02-flags-and-attrs.md
, etc.). - Check Solutions: If you get stuck or want to compare your code,
solutions for each exercise
NN
are available in the correspondingNN-solution/
directory.
Let's get coding!
█ ▄▄▄▄▄ ██▀▄██▀▄██▄ █▄▄ ▀█ ▄▄▄▄▄ █
█ █ █ █▄▀█▄▀▀█▄██▀ █▀███ █ █ █
█ █▄▄▄█ ██▄▀▀ ▄▀▀▄█▀ █▀█ ▄█ █▄▄▄█ █
█▄▄▄▄▄▄▄█ █▄▀▄█▄▀▄▀▄▀▄▀▄█ █▄▄▄▄▄▄▄█
█▄▄▄▀▄ ▄▄ ▀█▀ ▀██▀▄██▀ ▀█ ██▀▄█▀█
█ ▄ ██ ▄▀▀▀ ▄ ▀ █▀▀ ▀▀▀▄█▀█ █▀ █ ▄█
█▄▄ ▀▄▀ █ ██ ▄ █▄▄▄▄▄ ▀█ ▀█ █
█▄▀█▄ █▄██ ▀█▄█▀▄▀█▀▀ ▀▀▀▄▀▀█▀█ ▄█
██▀▄█▄ ▄█ █▄▀ ▀█▄ █▄ ▀▄▄ ███▀▄ █
█ █▀ ▄█▀▀█▄ ▀ ██▀▀█▄▀█▀▀▄█▄▄ █ ▄█
██▄▀▄█ ▄ ▀█ ██ █▀ ▀█▀ ██ ▀ ▄██▀▄ █
█▄▀▀█ ▄▄ ▀█▀▀█▄███ ▄█▀▀▀▀ ▄▀▀ ▄█ ▄█
█▄█▄█▄▄▄▄▀▀▄▀ ▀█▄ ▄▄▄ ▀▀ ▄▄▄ ██▀▀█
█ ▄▄▄▄▄ █ █ ▄ ▀ █▄▀▀█ ▄▄ █▄█ ▄█ █
█ █ █ █▄▀█ ██ █▀ ▀█ ▀█ ▄▄ ▀▀█▀█
█ █▄▄▄█ █ ▀ ▀█▄██▀ ▀▀█ ▄ ▀▀█ ▄▄▄▀▄█
█▄▄▄▄▄▄▄█▄█▄█▄██▄▄▄██▄▄█▄▄▄█▄▄▄█▄▄█