Deploying Your Provider as a Binary
This is part 5 of a five-part series. The earlier parts cover a resource, a data source, a function, and an ephemeral resource.
Get the code: all five parts live in provide-io/pyvider-tutorial. Clone and follow along:
git clone https://github.com/provide-io/pyvider-tutorial.git
cd pyvider-tutorial/part5-deploy
uv sync && uv run flavor pack && tofu init && tofu apply
Everything we’ve built so far has run through pyvider install — a wrapper script that activates a local venv and invokes Python. That’s great for iteration, but it’s not how you ship a provider to other people. They don’t have your venv. They don’t want to uv sync. They want a binary.
This is what Flavorpack does: it bundles your provider code, its Python dependencies, and a Python runtime into a single self-contained executable. The user drops the binary into ~/.terraform.d/plugins/ and it just works.
Dev Install vs Binary Deploy
pyvider install |
flavor pack |
|
|---|---|---|
| Output | Wrapper shell script | Single executable (~90 MB) |
| Requires Python on target | yes | no |
| Requires your venv | yes | no |
| Startup time | instant (local Python) | slower first run, fast after |
| Best for | local development | distribution to others |
Same provider code, two different delivery mechanisms. You pick at build time.
Configure Flavorpack
Flavorpack reads pyproject.toml. Add a [tool.flavor] section to the provider we built in part 4:
[project]
name = "terraform-provider-mycloud"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["pyvider>=0.3.33", "flavorpack"]
[project.scripts]
terraform-provider-mycloud = "my_provider:main"
[project.entry-points."pyvider"]
mycloud = "my_provider"
[tool.pyvider]
provider_name = "mycloud"
[tool.flavor]
entry_point = "pyvider.cli:main"
output_path = "dist/terraform-provider-mycloud"
Three things changed:
flavorpackadded todependenciessouv run flavorworks without a global install.entry_pointpoints at the samepyvider.cli:mainthatpyvider installuses. The binary launches that.output_pathis where the.pspfile goes — Flavorpack appends.pspautomatically.
Build
uv sync
uv run flavor pack
The first run takes a minute: Flavorpack downloads a pinned CPython build, resolves your dependencies, creates a slot tarball, and writes the whole thing into a PSPF (Progressive Secure Package Format) file with an Ed25519 signature.
🚀 Packaging application...
📋 Building PSPF package structure...
🔐 Setting up package signing keys...
✍️ Writing PSPF package file...
🔍 Verifying .../dist/terraform-provider-mycloud.psp...
✅ Package integrity verified
✅ Successfully built 1 package(s)
The result is one file: dist/terraform-provider-mycloud.psp. Run it directly and it extracts itself on first launch, caches the extraction, and then re-uses that cache on subsequent runs.
Install to Terraform’s Plugin Directory
Terraform discovers local providers under a platform-specific directory tree:
~/.terraform.d/plugins/
└── <source>/
└── <version>/
└── <platform>/
└── terraform-provider-<name>
So for our mycloud provider at version 0.1.0 on macOS ARM64:
PLUGIN_DIR="$HOME/.terraform.d/plugins/local/providers/mycloud/0.1.0/darwin_arm64"
mkdir -p "$PLUGIN_DIR"
mv dist/terraform-provider-mycloud.psp "$PLUGIN_DIR/terraform-provider-mycloud"
chmod +x "$PLUGIN_DIR/terraform-provider-mycloud"
Two details to notice:
- Strip the
.pspextension. Terraform expects the binary name to matchterraform-provider-<name>exactly. - Match the platform string.
darwin_arm64,linux_amd64,windows_amd64, etc. Flavorpack builds for the platform it runs on; cross-compilation is a separate topic.
Use It
Your main.tf doesn’t change. The same provider block works against either the dev wrapper or the binary:
terraform {
required_providers {
mycloud = {
source = "local/providers/mycloud"
version = "0.1.0"
}
}
}
provider "mycloud" {}
resource "mycloud_server" "web" {
name = "web-01"
}
Run it:
tofu init
tofu apply -auto-approve
tofu output
On the first tofu apply, the binary extracts its embedded Python + dependencies to a cache directory. You’ll notice a pause. Every run after that reuses the cache and starts quickly — warm-start time is typically a few seconds.
What’s Actually in the Binary
A .psp file is a polyglot: valid executable at the front, structured package at the back. If you inspect one with a hex editor or run flavor inspect, you’ll find:
| Section | Contents |
|---|---|
| Launcher | Native Rust binary that handles extraction and exec |
| Metadata | Manifest: entry point, version, environment passthroughs |
| Python slot | CPython interpreter for the target platform |
| App slot | Your package + all Python dependencies (wheels) |
| Signature | Ed25519 signature of the index and metadata |
| Trailer | Offsets and checksums for O(1) lookup |
The launcher verifies the signature, checks the integrity seal, extracts the app and Python slots into a cache keyed by package checksum, and then execs into your entry point. All of that happens before pyvider.cli:main sees control.
Signing and Trust
By default, flavor pack generates ephemeral signing keys per build. That’s fine for local testing. For distribution, generate a persistent keypair once and reuse it:
uv run flavor keygen --out-dir keys
Reference the keys in [tool.flavor.signing]:
[tool.flavor.signing]
private_key_path = "keys/provider-private.key"
public_key_path = "keys/provider-public.key"
Commit the public key. Keep the private key in a secret manager and inject it at release time. Anyone running your binary can verify it was built with your private key and hasn’t been tampered with.
What’s in the Complete Provider
part5-deploy/
├── pyproject.toml # now includes [tool.flavor]
├── main.tf
└── 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)
└── session_token.py # mycloud_session_token (ephemeral resource)
The Python code is identical to part 4. The only thing that changed is how you ship it.
That’s the series. You’ve built a provider with a resource, a data source, a function, and an ephemeral — and packaged it as a distributable binary.
Where to go next:
- Full documentation — complete API reference
- Flavorpack documentation — cross-platform builds, signing workflows, CI recipes
- terraform-provider-pyvider — a production provider built the same way, with a multi-platform release workflow you can copy