Building Your First Provider Function
This is part 3 of a four-part series. Start with Part 1 — Your First Resource and Part 2 — Your First Data Source. Part 4 adds an ephemeral resource.
In parts 1 and 2 we built a resource that manages servers and a data source that queries them. Now we’ll add a provider function — a pure Python callable that Terraform can invoke directly in expressions, with no state involved.
We’ll use it to generate a consistent server name from a prefix and environment, then feed that name into the resource from part 1.
What Makes a Function Different
| Resource | Data Source | Function | |
|---|---|---|---|
| Has state | yes | no | no |
| Reads from backend | yes (on refresh) | yes | no |
| Called in expressions | no | no | yes |
| Side effects | yes | no | no |
Functions are pure: same inputs always produce the same output.
Add the Function
Create my_provider/names.py:
from pyvider.cty import CtyString
from pyvider.functions import (
BaseFunction,
FunctionParameter,
FunctionReturnType,
register_function,
)
from pyvider.schema import PvsSchema, a_str, s_function
@register_function("generate_name")
class GenerateNameFunction(BaseFunction):
"""Generate a standardized server name from a prefix and environment."""
@classmethod
def get_schema(cls) -> PvsSchema:
return s_function(
parameters=[
a_str(description="Name prefix — e.g. 'web', 'db', 'app'"),
a_str(description="Environment — e.g. 'prod', 'staging', 'dev'"),
],
return_type=a_str(description="Generated name"),
)
def get_parameters(self) -> list[FunctionParameter]:
return [
FunctionParameter(name="prefix", type=CtyString()),
FunctionParameter(name="env", type=CtyString()),
]
def get_return_type(self) -> FunctionReturnType:
return FunctionReturnType(type=CtyString())
async def call(self, prefix: str, env: str) -> str:
return f"{prefix}-{env}"
Register It
Add the import to my_provider/__init__.py:
from pyvider.providers import BaseProvider, ProviderMetadata, register_provider
from pyvider.schema import PvsSchema, s_provider
import my_provider.server # registers mycloud_server
import my_provider.server_info # registers mycloud_server_info
import my_provider.names # registers mycloud::generate_name
@register_provider("mycloud")
class MyCloudProvider(BaseProvider):
def __init__(self) -> None:
super().__init__(
metadata=ProviderMetadata(
name="mycloud",
version="0.1.0",
protocol_version="6",
)
)
@classmethod
def get_schema(cls) -> PvsSchema:
return s_provider({})
The Complete Terraform Configuration
Now the full main.tf — using the function to name the server, then querying it back with the data source:
terraform {
required_providers {
mycloud = {
source = "example.com/tutorial/mycloud"
version = "0.1.0"
}
}
}
provider "mycloud" {}
locals {
server_name = provider::mycloud::generate_name("web", "prod")
# evaluates to: "web-prod"
}
resource "mycloud_server" "web" {
name = local.server_name
}
data "mycloud_server_info" "web" {
server_id = mycloud_server.web.id
}
output "server_name" { value = local.server_name }
output "server_id" { value = mycloud_server.web.id }
output "server_status" { value = data.mycloud_server_info.web.status }
Run it:
pyvider install
terraform init
terraform apply
Expected outputs:
server_name = "web-prod"
server_id = "srv-001"
server_status = "running"
Required Methods on Every Function
| Method | Purpose |
|---|---|
get_schema | Declares parameter types and return type for Terraform |
get_parameters | Returns typed FunctionParameter list (used internally) |
get_return_type | Returns typed FunctionReturnType (used internally) |
call | The actual logic — receives native Python values, returns a native value |
What’s in the Complete Provider
After all three parts, my_provider/ looks like this:
my_provider/
├── __init__.py # provider class + component imports
├── server.py # mycloud_server resource
├── server_info.py # mycloud_server_info data source
└── names.py # provider::mycloud::generate_name function
Each file is independent. Pyvider discovers all registered components automatically at startup via the decorator registry.
Next: Part 4 — Adding an Ephemeral Resource for short-lived credentials that never touch .tfstate.
Or jump to:
- Full documentation — complete API reference
- Demo provider — working example with all four component types
- pyvider-components — production-ready components you can study or reuse