Build your first Terraform provider in Python! This guide walks you through creating a simple but functional provider in about 5 minutes.
🤖 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.
We'll create a LocalFile Provider that can:
- Create text files on your local filesystem
- Read file contents as data sources
- Update files when content changes
- Delete files when resources are destroyed
#!/usr/bin/env python3"""Local File Provider - A simple Terraform provider for managing local files."""frompathlibimportPathimporthashlibimportattrsfrompyvider.providersimportregister_provider,BaseProvider,ProviderMetadatafrompyvider.resourcesimportregister_resource,BaseResource,ResourceContextfrompyvider.schemaimports_provider,s_resource,a_str,a_num,PvsSchema# ============================================# PROVIDER DEFINITION# ============================================@register_provider("local")classLocalProvider(BaseProvider):"""Provider for managing local files."""def__init__(self):super().__init__(metadata=ProviderMetadata(name="local",version="0.1.0",protocol_version="6"))def_build_schema(self)->PvsSchema:"""Define provider schema (no configuration needed for this simple example)."""returns_provider({})# ============================================# FILE RESOURCE# ============================================@attrs.defineclassFileConfig:"""File resource configuration."""path:strcontent:str@attrs.defineclassFileState:"""File resource state."""id:strpath:strcontent:strchecksum:strsize:int@register_resource("file")classFile(BaseResource):"""Manages a local text file."""config_class=FileConfigstate_class=FileState@classmethoddefget_schema(cls)->PvsSchema:"""Define resource schema."""returns_resource({# Configuration attributes (user inputs)"path":a_str(required=True,description="Path to the file"),"content":a_str(required=True,description="Content to write"),# Computed attributes (provider outputs)"id":a_str(computed=True,description="File identifier"),"checksum":a_str(computed=True,description="SHA256 checksum"),"size":a_num(computed=True,description="File size in bytes"),})asyncdef_create_apply(self,ctx:ResourceContext)->tuple[FileState|None,None]:"""Create a new file."""ifnotctx.config:returnNone,None# Create file pathfile_path=Path(ctx.config.path)file_path.parent.mkdir(parents=True,exist_ok=True)# Write contentfile_path.write_text(ctx.config.content)# Compute checksumchecksum=hashlib.sha256(ctx.config.content.encode()).hexdigest()# Return statereturnFileState(id=str(file_path.absolute()),path=str(file_path.absolute()),content=ctx.config.content,checksum=checksum,size=len(ctx.config.content)),Noneasyncdefread(self,ctx:ResourceContext)->FileState|None:"""Read current file state."""ifnotctx.state:returnNonefile_path=Path(ctx.state.path)ifnotfile_path.exists():returnNone# File deleted outside Terraformcontent=file_path.read_text()checksum=hashlib.sha256(content.encode()).hexdigest()returnFileState(id=ctx.state.id,path=ctx.state.path,content=content,checksum=checksum,size=len(content))asyncdef_update_apply(self,ctx:ResourceContext)->tuple[FileState|None,None]:"""Update file content."""ifnotctx.configornotctx.state:returnNone,Nonefile_path=Path(ctx.state.path)file_path.write_text(ctx.config.content)checksum=hashlib.sha256(ctx.config.content.encode()).hexdigest()returnFileState(id=ctx.state.id,path=ctx.state.path,content=ctx.config.content,checksum=checksum,size=len(ctx.config.content)),Noneasyncdef_delete_apply(self,ctx:ResourceContext)->None:"""Delete the file."""ifnotctx.state:returnfile_path=Path(ctx.state.path)iffile_path.exists():file_path.unlink()# ============================================# MAIN ENTRY POINT# ============================================if__name__=="__main__":frompyvider.cliimportmainmain()
terraform{required_providers{local={source="example.com/tutorial/local"version="0.1.0"}}}provider"local"{ # No configuration needed for this simple example}resource"local_file""config"{path="managed_files/app.conf"content=<<-EOT # Application Configuration app_name = "MyApp" version = "1.0.0" EOT}resource"local_file""readme"{path="managed_files/README.md"content="# My Managed Files\n\nThis directory is managed by Terraform."}output"config_checksum"{value=local_file.config.checksum}output"readme_size"{value=local_file.readme.size}
# Make sure you're in a directory with your provider code# and have pyvider installed in your virtual environment# Install the provider for Terraformpyviderinstall
# Now run Terraform normally - it will find your providerterraforminit
terraformplan
terraformapply
# Check the created filesls-lamanaged_files/
catmanaged_files/app.conf
catmanaged_files/README.md
How This Works
pyvider install creates a wrapper script in Terraform's plugin directory that:
Activates your virtual environment
Runs your provider with the correct Python interpreter
Allows Terraform to communicate with your provider via the plugin protocol
This is the recommended approach for development and testing.
# Set the Terraform magic cookie (Terraform normally does this)exportTF_PLUGIN_MAGIC_COOKIE="d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4e95f7aa66ead0"# Run the providerpythonlocal_provider.pyprovide
# In another terminal with the same environment variable,# run Terraform commands
Direct Execution Limitations
This approach is useful for debugging but not recommended for normal development.
The provider must be launched by Terraform to work correctly in production scenarios.
@classmethoddefget_schema(cls)->PvsSchema:returns_resource({"path":a_str(required=True,description="Path to the file"),"content":a_str(required=True,description="Content to write"),"checksum":a_str(computed=True,description="SHA256 checksum"),})
The schema defines:
- User inputs: path and content (required)
- Provider outputs: checksum and size (computed)
The ResourceContext provides:
- ctx.config - User configuration (FileConfig)
- ctx.state - Previous state (FileState) for updates
- ctx.planned_state - Planned state from terraform plan
- ctx.private_state - Encrypted private state (if needed)
asyncdef_validate_config(self,config:FileConfig)->list[str]:"""Validate configuration."""errors=[]if".."inconfig.path:errors.append("Path cannot contain '..'")iflen(config.content)>1_000_000:errors.append("Content too large (max 1MB)")returnerrors