← All posts

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

ResourceData SourceFunction
Has stateyesnono
Reads from backendyes (on refresh)yesno
Called in expressionsnonoyes
Side effectsyesnono

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

MethodPurpose
get_schemaDeclares parameter types and return type for Terraform
get_parametersReturns typed FunctionParameter list (used internally)
get_return_typeReturns typed FunctionReturnType (used internally)
callThe 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: