pyvider is in pre-release. This tutorial covers stable functionality. Some APIs may change during the pre-release series.
See project status for details.
🤖 AI-Generated Content
This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.
Welcome! In this tutorial, you'll build your first Terraform resource using pyvider. By the end, you'll have a working file resource that creates, reads, updates, and deletes local files through Terraform.
A resource in Terraform represents a piece of infrastructure that can be managed (created, updated, deleted). Think of resources as the things you want to manage:
A file on disk
A server in the cloud
A database record
An API object
Resources have a lifecycle:
Create - Make something new
Read - Check current state
Update - Modify existing thing
Delete - Remove it
Terraform handles this lifecycle automatically. You just implement the operations!
# Runtime configuration class (Python type safety)@attrs.defineclassFileConfig:"""What the user configures."""path:str# Where to create the filecontent:str# What to write in the filemode:str="644"# File permissions (optional, defaults to 644)# Runtime state class (Python type safety)@attrs.defineclassFileState:"""What Terraform tracks about the file."""id:str# Unique identifierpath:str# File pathcontent:str# Current contentmode:str# Current permissionssize:int# File size in bytes (computed by us)
@register_resource("file")classFile(BaseResource):"""Manages a local file."""# Link our runtime typesconfig_class=FileConfigstate_class=FileState@classmethoddefget_schema(cls)->PvsSchema:"""Define what Terraform users see."""returns_resource({# User inputs"path":a_str(required=True,description="File path"),"content":a_str(required=True,description="File content"),"mode":a_str(default="644",description="File permissions"),# Provider outputs (we compute these)"id":a_str(computed=True,description="File ID"),"size":a_num(computed=True,description="File size in bytes"),})
What's happening here?
@register_resource("file") - Registers this as a Terraform resource
asyncdefread(self,ctx:ResourceContext)->FileState|None:"""Refresh state from filesystem."""# If no existing state, nothing to readifnotctx.state:returnNonefile_path=Path(ctx.state.path)# Check if file still existsifnotfile_path.exists():returnNone# File was deleted outside Terraform# File exists! Return current statecontent=file_path.read_text()returnFileState(id=ctx.state.id,path=str(file_path),content=content,mode=ctx.state.mode,size=len(content),)
asyncdef_create_apply(self,ctx:ResourceContext)->tuple[FileState|None,None]:"""Create file."""ifnotctx.config:returnNone,Nonefile_path=Path(ctx.config.path)# Write the filefile_path.write_text(ctx.config.content)# Return new statereturnFileState(id=str(file_path.absolute()),path=str(file_path),content=ctx.config.content,mode=ctx.config.mode,size=len(ctx.config.content),),None
Return value explained:
First element: New state to track
Second element: Private data (advanced, we don't need it)
asyncdef_delete_apply(self,ctx:ResourceContext)->None:"""Delete file."""ifnotctx.state:returnfile_path=Path(ctx.state.path)# Delete file if it existsiffile_path.exists():file_path.unlink()
Step 8: Add Validation (Optional but Recommended)¶
Let's add configuration validation to prevent bad inputs:
"""Complete file resource example with validation.This snippet demonstrates a full-featured resource implementation including:- Runtime type definitions with attrs- Schema definition with validation- All CRUD operations (create, read, update, delete)- Configuration validationUsed in: tutorials, guides"""frompathlibimportPathimportattrsfrompyvider.resourcesimportBaseResource,register_resourcefrompyvider.resources.contextimportResourceContextfrompyvider.schemaimportPvsSchema,a_num,a_str,s_resource# Runtime configuration class (Python type safety)@attrs.defineclassFileConfig:"""What the user configures."""path:str# Where to create the filecontent:str# What to write in the filemode:str="644"# File permissions (optional, defaults to 644)# Runtime state class (Python type safety)@attrs.defineclassFileState:"""What Terraform tracks about the file."""id:str# Unique identifierpath:str# File pathcontent:str# Current contentmode:str# Current permissionssize:int# File size in bytes (computed by us)@register_resource("file")classFile(BaseResource):"""Manages a local file."""# Link our runtime typesconfig_class=FileConfigstate_class=FileState@classmethoddefget_schema(cls)->PvsSchema:"""Define what Terraform users see."""returns_resource({# User inputs"path":a_str(required=True,description="File path"),"content":a_str(required=True,description="File content"),"mode":a_str(default="644",description="File permissions"),# Provider outputs (we compute these)"id":a_str(computed=True,description="File ID"),"size":a_num(computed=True,description="File size in bytes"),})asyncdef_validate_config(self,config:FileConfig)->list[str]:"""Validate configuration."""errors=[]# Prevent path traversal attacksif".."inconfig.path:errors.append("Path cannot contain '..'")# Validate file mode formatifnotconfig.mode.isdigit()orlen(config.mode)!=3:errors.append("Mode must be 3 digits (e.g., '644')")returnerrorsasyncdefread(self,ctx:ResourceContext)->FileState|None:"""Refresh state from filesystem."""# If no existing state, nothing to readifnotctx.state:returnNonefile_path=Path(ctx.state.path)# Check if file still existsifnotfile_path.exists():returnNone# File was deleted outside Terraform# File exists! Return current statecontent=file_path.read_text()returnFileState(id=ctx.state.id,path=str(file_path),content=content,mode=ctx.state.mode,size=len(content),)asyncdef_create_apply(self,ctx:ResourceContext)->tuple[FileState|None,None]:"""Create file."""ifnotctx.config:returnNone,Nonefile_path=Path(ctx.config.path)# Write the filefile_path.write_text(ctx.config.content)# Return new statereturnFileState(id=str(file_path.absolute()),path=str(file_path),content=ctx.config.content,mode=ctx.config.mode,size=len(ctx.config.content),),Noneasyncdef_update_apply(self,ctx:ResourceContext)->tuple[FileState|None,None]:"""Update file."""ifnotctx.configornotctx.state:returnNone,Nonefile_path=Path(ctx.state.path)# Update the file contentfile_path.write_text(ctx.config.content)# Return updated statereturnFileState(id=ctx.state.id,path=ctx.state.path,content=ctx.config.content,mode=ctx.config.mode,size=len(ctx.config.content),),Noneasyncdef_delete_apply(self,ctx:ResourceContext)->None:"""Delete file."""ifnotctx.state:returnfile_path=Path(ctx.state.path)# Delete file if it existsiffile_path.exists():file_path.unlink()
terraform{required_providers{local={source="mycompany/local"}}}resource"local_file""greeting"{path="hello.txt"content="Hello from Terraform!"mode="644"}output"file_size"{value=local_file.greeting.size}
Congratulations! You've built your first pyvider resource. You now understand:
✅ Resource Lifecycle - Create, read, update, delete operations
✅ Schema Definition - Defining what users configure
✅ Runtime Types - Separating config from state
✅ Validation - Preventing bad configurations
✅ Testing - Using Terraform to test your resource