Project Scaffolding
Project Scaffolding
Now that Go is installed, let’s scaffold your first provider project. Scaffolding creates the initial project structure with all necessary files and boilerplate code, saving hours of manual setup.
In this chapter, you’ll learn multiple approaches to bootstrap a Terraform provider project and understand what each generated file does.
Why Scaffold?
Starting from scratch is time-consuming and error-prone. Scaffolding provides:
- ✅ Standard structure — Industry-standard directory layout
- ✅ Boilerplate code — Pre-written framework integration
- ✅ Best practices — HashiCorp-recommended patterns
- ✅ Working foundation — Compilable starting point
- ✅ Quick start — Begin development in minutes, not hours
Scaffolding Options
HashiCorp Template (Recommended)
Official template from HashiCorp with Plugin Framework.
GitHub: terraform-provider-scaffolding-framework
Pros:
- ✅ Official and well-maintained
- ✅ Uses modern Plugin Framework
- ✅ Includes examples and documentation
- ✅ GitHub Actions CI/CD included
- ✅ GoReleaser configuration ready
Cons:
- ⚠️ Requires GitHub account
- ⚠️ Less customization out of the box
Using HashiCorp Template
Step 1: Create from Template
- Visit: https://github.com/hashicorp/terraform-provider-scaffolding-framework
- Click “Use this template” button (green button, top right)
- Select “Create a new repository”
- Configure your repository:
- Repository name:
terraform-provider-mycloud - Description: “Terraform provider for MyCloud platform”
- Visibility: Public (for Terraform Registry) or Private
- Repository name:
- Click “Create repository”
Step 2: Clone Your Repository
# Clone your new repository
git clone https://github.com/YOUR_USERNAME/terraform-provider-mycloud.git
# Navigate into directory
cd terraform-provider-mycloud
# Verify structure
ls -la
Expected output:
drwxr-xr-x .github/
-rw-r--r-- .gitignore
-rw-r--r-- .goreleaser.yml
-rw-r--r-- GNUmakefile
-rw-r--r-- LICENSE
-rw-r--r-- README.md
drwxr-xr-x docs/
drwxr-xr-x examples/
-rw-r--r-- go.mod
-rw-r--r-- go.sum
drwxr-xr-x internal/
-rw-r--r-- main.go
drwxr-xr-x tools/
Step 3: Update Module Name
The template uses a placeholder module name. Update it to yours:
# Open go.mod
vim go.mod
Change:
module github.com/hashicorp/terraform-provider-scaffolding-framework
To:
module github.com/YOUR_USERNAME/terraform-provider-mycloud
Step 4: Update Import Paths
Find and replace all import paths:
macOS/Linux:
find . -type f -name "*.go" -exec sed -i '' \
's|github.com/hashicorp/terraform-provider-scaffolding-framework|github.com/YOUR_USERNAME/terraform-provider-mycloud|g' {} +
Linux (GNU sed):
find . -type f -name "*.go" -exec sed -i \
's|github.com/hashicorp/terraform-provider-scaffolding-framework|github.com/YOUR_USERNAME/terraform-provider-mycloud|g' {} +
Or manually edit:
main.gointernal/provider/provider.gointernal/provider/provider_test.go- All resource and data source files
Step 5: Update Provider Address
Edit main.go:
opts := providerserver.ServeOpts{
// Change this line
Address: "registry.terraform.io/YOUR_USERNAME/mycloud",
Debug: debug,
}
Step 6: Download Dependencies
# Tidy dependencies
go mod tidy
# Download all dependencies
go mod download
# Verify
go list -m all
Step 7: Build and Verify
# Build provider
go build -o terraform-provider-mycloud
# Check binary
ls -lh terraform-provider-mycloud
# Try running it
./terraform-provider-mycloud --help
Expected output:
-rwxr-xr-x 23M terraform-provider-mycloud
Usage of ./terraform-provider-mycloud:
-debug
set to true to run the provider with support for debuggers
Understanding Generated Files
Let’s examine each file in the scaffolded project:
main.go
Purpose: Provider entry point
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/YOUR_USERNAME/terraform-provider-mycloud/internal/provider"
)
//go:generate terraform-plugin-docs generate
var version string = "dev"
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run with debugger support")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/YOUR_USERNAME/mycloud",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}
Key elements:
//go:generate— Doc generation directiveversion— Set by GoReleaser during buildproviderserver.Serve()— Starts GRPC serverAddress— Provider namespace in registry
go.mod
Purpose: Go module definition
module github.com/YOUR_USERNAME/terraform-provider-mycloud
go 1.22
require (
github.com/hashicorp/terraform-plugin-framework v1.5.0
github.com/hashicorp/terraform-plugin-go v0.21.0
github.com/hashicorp/terraform-plugin-log v0.9.0
)
Defines:
- Module path
- Go version requirement
- Direct dependencies
internal/provider/provider.go
Purpose: Provider implementation
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
)
var _ provider.Provider = &MyCloudProvider{}
type MyCloudProvider struct {
version string
}
type MyCloudProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
APIKey types.String `tfsdk:"api_key"`
}
func (p *MyCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "mycloud"
resp.Version = p.version
}
func (p *MyCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
Optional: true,
},
"api_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
},
},
}
}
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var data MyCloudProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Initialize API client here
}
func (p *MyCloudProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewExampleResource,
}
}
func (p *MyCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewExampleDataSource,
}
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &MyCloudProvider{
version: version,
}
}
}
GNUmakefile
Purpose: Build automation
default: testacc
# Run acceptance tests
.PHONY: testacc
testacc:
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
# Format code
.PHONY: fmt
fmt:
gofmt -s -w -e .
# Install provider locally
.PHONY: install
install: build
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/YOUR_USERNAME/mycloud/0.1.0/darwin_arm64
mv terraform-provider-mycloud ~/.terraform.d/plugins/registry.terraform.io/YOUR_USERNAME/mycloud/0.1.0/darwin_arm64/
# Build provider
.PHONY: build
build:
go build -o terraform-provider-mycloud
# Run linter
.PHONY: lint
lint:
golangci-lint run
.goreleaser.yml
Purpose: Release automation
version: 2
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}'
flags:
- -trimpath
ldflags:
- '-s -w -X main.version={{.Version}}'
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
ignore:
- goos: windows
goarch: arm64
archives:
- format: zip
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
checksum:
name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
algorithm: sha256
signs:
- artifacts: checksum
args:
- "--batch"
- "--local-user"
- "{{ .Env.GPG_FINGERPRINT }}"
- "--output"
- "${signature}"
- "--detach-sign"
- "${artifact}"
release:
draft: false
changelog:
skip: false
Automates:
- Multi-platform builds
- Binary naming
- GPG signing
- GitHub release creation
.github/workflows/
CI/CD workflows:
test.yml— Run tests on every pushrelease.yml— Build and release on tags
Manual Setup Alternative
If you prefer complete control:
Create Project Structure
# Create project
mkdir terraform-provider-mycloud
cd terraform-provider-mycloud
# Initialize Git
git init
# Initialize Go module
go mod init github.com/YOUR_USERNAME/terraform-provider-mycloud
# Create directory structure
mkdir -p internal/provider
mkdir -p examples
mkdir -p docs
mkdir -p .github/workflows
Create main.go
cat > main.go << 'EOF'
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/YOUR_USERNAME/terraform-provider-mycloud/internal/provider"
)
var version string = "dev"
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "enable debugger")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/YOUR_USERNAME/mycloud",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}
EOF
Install Dependencies
# Install Plugin Framework
go get github.com/hashicorp/terraform-plugin-framework@latest
go get github.com/hashicorp/terraform-plugin-log@latest
# Tidy
go mod tidy
Provider Naming Conventions
Repository Name
terraform-provider-{name}
Examples:
terraform-provider-awsterraform-provider-mycloud
Module Path
github.com/{username}/terraform-provider-{name}
Provider Address (Registry)
registry.terraform.io/{namespace}/{name}
Resource Names
{provider}_{resource}
Examples:
mycloud_environmentmycloud_database
Data Source Names
{provider}_{datasource}
Examples:
mycloud_regionsmycloud_instance_types
Customizing Your Provider
After scaffolding:
1. Update README.md
# Terraform Provider: MyCloud
Terraform provider for MyCloud platform.
## Requirements
- Terraform 1.5+
- Go 1.21+ (for development)
## Using the Provider
terraform {
required_providers {
mycloud = {
source = "YOUR_USERNAME/mycloud"
}
}
}
provider "mycloud" {
endpoint = "https://api.mycloud.com"
api_key = var.mycloud_api_key
}
2. Add LICENSE
Choose a license:
- MIT — Simple and permissive
- Apache 2.0 — Patent protection
- MPL 2.0 — HashiCorp’s choice
3. Configure .gitignore
# Binaries
*.exe
*.dll
*.so
*.dylib
terraform-provider-*
# Terraform
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
# Go
vendor/
go.work
go.work.sum
# IDEs
.idea/
.vscode/
*.swp
# OS
.DS_Store
Verification Checklist
Before proceeding:
- ✅ Repository created and cloned
- ✅ Module name updated in go.mod
- ✅ Import paths updated
- ✅ Dependencies downloaded
- ✅ Provider compiles successfully
- ✅ Makefile targets work
- ✅ .gitignore configured
- ✅ README updated
- ✅ License added
Testing Your Scaffold
# Format code
make fmt
# Build
make build
# Verify binary
file terraform-provider-mycloud
# Check help
./terraform-provider-mycloud --help
Common Issues
Module Path Mismatch
Error:
package github.com/hashicorp/terraform-provider-scaffolding-framework/internal/provider:
cannot find package
Fix: Update all import statements
Dependency Download Fails
Error:
go: github.com/hashicorp/terraform-plugin-framework@latest:
Get "https://proxy.golang.org/...": connection refused
Fix:
export GOPROXY=https://proxy.golang.org,direct
go mod download
Build Permission Error
Error:
-bash: ./terraform-provider-mycloud: Permission denied
Fix:
chmod +x terraform-provider-mycloud
Best Practices
- Use version control from the start
- Document your provider immediately
- Set up CI/CD early
- Follow naming conventions
- Keep dependencies updated
Key Takeaways
- ✅ Use HashiCorp’s template for fastest setup
- ✅ Update module names and import paths
- ✅ Verify build before development
- ✅ Follow naming conventions strictly
- ✅ Customize README and LICENSE
- ✅ Set up automation early
What’s Next?
Now let’s understand the project structure and file organization.
Previous: ← Go Installation
Last updated: June 30, 2026