Tutorial¶
In this tutorial, we’ll walk through using tf to create a simple math provider.
We’ll use uv, a popular package manager.
Project Setup¶
We’re going to name our provider Python package mathprovider. We’ll create a directory for it right in our home directory – mkdir ~/mathprovider && cd ~/mathprovider.
From now on, we’ll assume you’re in that directory.
First, let’s start a package with uv:
$ uv init --package
Initialized project `mathprovider`
This will give us a couple of files we can work with, namely pyproject.toml and the src/ directory.
Let’s add tf as a dependency.
$ uv add tf
Resolved 8 packages in 114ms
Built mathprovider @ file://~/mathprovider
Prepared 1 package in 4ms
Installed 8 packages in 2ms
+ cffi==1.17.1
+ cryptography==45.0.6
+ grpcio==1.74.0
+ mathprovider==0.1.0 (from file://~/mathprovider)
+ msgpack==1.1.1
+ protobuf==5.29.5
+ pycparser==2.22
+ tf==1.1.0
Finally, we’re going to have a main function in our package that needs to be the entrypoint.
We’ll create a src/mathprovider/main.py file and a main function in it.
import sys
from tf.runner import run_provider
def main():
provider = None
run_provider(provider, sys.argv)
Let’s create a console script in pyproject.toml that points to our main function.
These need to have a specific name (terraform-provider-<PROVIDER_NAME>).
We add this to a new [project.scripts] section of pyproject.toml.
...
[project.scripts]
terraform-provider-math = "mathprovider.main:main"
Finally, we have uv create this for us with uv sync.
You will now find a .venv/ directory in your project along with a .venv/bin/terraform-provider-math script.
If we run it, we’ll find a bunch of garbage output and our program hanging:
$ uv run terraform-provider-math
1|6|unix|/tmp/tmp5hf5fmoy/py-tf-plugin.sock|grpc|XXX...
(hang)
What’s going on here? Our provider entrypoint is speaking the Go Plugin Protocol, and it’s waiting for Terraform to connect to it.
Let’s ctrl-c out of it.
We now have the basic scaffolding of a provider, but it doesn’t do anything yet.
Creating the Provider¶
Let’s sketch out the basic provider class in our main.py.
First, for simplicity let’s import everything we’ll need later.
import sys
from typing import Optional, Type
from tf import runner
from tf import types as t
from tf.iface import (
Config,
DataSource,
ReadDataContext,
Resource,
State,
)
from tf.provider import Diagnostics, Provider
from tf.schema import Attribute, Schema
Now, we can add our MathProvider class that implements the Provider protocol.
class MathProvider(Provider):
def get_model_prefix(self) -> str:
return "math_"
def get_provider_schema(self, diags: Diagnostics) -> Schema:
return Schema(attributes=[])
def full_name(self) -> str:
return "test.terraform.io/test/math"
def validate_config(self, diags: Diagnostics, config: Config):
pass
def configure_provider(self, diags: Diagnostics, config: Config):
pass
def get_data_sources(self) -> list[Type[DataSource]]:
return []
def get_resources(self) -> list[Type[Resource]]:
return []
While we’ll leave most of these empty for now, there are a few ones worth noting:
get_model_prefixreturns a prefix that will be used for all attributes in this provider. Tofu decides which resources map to which provider by using their type name prefix. We’ll usemath_here, so our resources will be named similarly to`math_divider.full_namereturns the full name of the provider, which is used in Tofu configuration files. This should be in the format<NAMESPACE>/<PROVIDER_NAME>. If you ever upload your provider to the Terraform Registry, this should match the name you use there. That provider name following the last slash should align with the model prefix (e.g.mathandmath_).get_data_sourcesandget_resourcesreturn lists of data source and resource classes that this provider implements. We’ll leave these empty for now, but we’ll add to them later.
Finally, we need to plug this provider class into our main function.
We can do that by instantiating it and passing it to run_provider.
def main():
provider = MathProvider()
runner.run_provider(provider, sys.argv)
Tofu Environment¶
To easily get started using our provider, we’re going to create an example directory
for our .tf files and a tofu.rc file
Let’s create a example/ directory in our project root and tofu.rc and main.tf files in it.
$ mkdir example && cd example
$ touch tofu.rc main.tf
The tofu.rc file is a configuration file for Tofu itself.
As we are developing our provider, we want Tofu to find it in our project’s virtual environment’s bin directory.
Uv has helpfully created a .venv/ directory in our project root. The tofu.rc file needs to point to the absolute path of our .venv/bin directory.
You’ll need to change /home/hunter/mathprovider to the absolute path of your own project directory.
provider_installation {
dev_overrides {
"test.terraform.io/test/math" = "/home/hunter/mathprovider/.venv/bin"
}
direct {}
}
Let’s also fill in our main.tf file to use our provider.
Right now we’ll just specify the provider and configure it with no arguments.
terraform {
required_providers {
math = {
source = "test.terraform.io/test/math"
}
}
}
provider "math" {}
Finally, in our shell we’ll need to have tofu use our custom tofu.rc file.
We can do this by setting the TF_CLI_CONFIG_FILE environment variable to point to our tofu.rc file.
$ export TF_CLI_CONFIG_FILE=/home/hunter/mathprovider/example/tofu.rc
Now we can run `tofu plan while we are in the example/ directory. Our main.tf isn’t doing much yet, but we should see our provider being started up.
$ tofu plan
│ Warning: Provider development overrides are in effect
│
│ The following provider development overrides are set in the CLI configuration:
│ - test.terraform.io/test/math in /home/hunter/mathprovider/.venv/bin
│
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become
│ incompatible with published releases.
╵
No changes. Your infrastructure matches the configuration.
OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
Adding a Data Source¶
In this tutorial we’ll only implement a single data source, math_divider, which will take two numbers and return their quotient.
Let’s add another class to our main.py file that implements the DataSource protocol.
We’ll also add some basic validation to ensure the divisor is not zero.
class Divider(DataSource):
@classmethod
def get_name(cls) -> str:
return "divider"
@classmethod
def get_schema(cls) -> Schema:
return Schema(
attributes=[
Attribute("dividend", t.Number(), required=True),
Attribute("divisor", t.Number(), required=True),
Attribute("quotient", t.Number(), computed=True),
]
)
def validate(self, diags: Diagnostics, type_name: str, config: Config):
super().validate(diags, type_name, config)
if config["divisor"] == 0:
diags.add_error(
"Invalid divisor",
"The 'divisor' attribute cannot be zero.",
)
def read(self, ctx: ReadDataContext, config: Config) -> Optional[State]:
return {
"dividend": config["dividend"],
"divisor": config["divisor"],
"quotient": config["dividend"] / config["divisor"],
}
def __init__(self, provider):
pass
Then we need to add this class to our provider’s get_data_sources method.
...
class MathProvider:
...
def get_data_sources(self) -> list[Type[DataSource]]:
return [Divider]
Finally, let’s use our new data source in our main.tf file.
We’ll add an output block so we can see the result of our division.
terraform {
required_providers {
math = {
source = "test.terraform.io/test/math"
}
}
}
provider "math" {}
data "math_divider" "example" {
dividend = 10
divisor = 2
}
output "result" {
value = data.math_divider.example.quotient
}
Now if we run tofu plan again, we should see our data source being read and the output being computed.
$ tofu plan
│ Warning: Provider development overrides are in effect
│
│ The following provider development overrides are set in the CLI configuration:
│ - test.terraform.io/test/math in /home/hunter/mathprovider/.venv/bin
│
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become
│ incompatible with published releases.
╵
data.math_divider.example: Reading...
data.math_divider.example: Read complete after 0s
Changes to Outputs:
+ result = 5
You can apply this plan to save these new output values to the OpenTofu state, without changing any real infrastructure.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly these actions if you run
"tofu apply" now.
Congratulations! You’ve created a simple Tofu provider with a data source! Now you can experiment with adding more data sources and resources to your provider.