Project Scaffolding

6 minute read

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

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

  1. Visit: https://github.com/hashicorp/terraform-provider-scaffolding-framework
  2. Click “Use this template” button (green button, top right)
  3. Select “Create a new repository”
  4. Configure your repository:
    • Repository name: terraform-provider-mycloud
    • Description: “Terraform provider for MyCloud platform”
    • Visibility: Public (for Terraform Registry) or Private
  5. 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.go
  • internal/provider/provider.go
  • internal/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 directive
  • version — Set by GoReleaser during build
  • providerserver.Serve() — Starts GRPC server
  • Address — 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 push
  • release.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-aws
  • terraform-provider-mycloud

Module Path

github.com/{username}/terraform-provider-{name}

Provider Address (Registry)

registry.terraform.io/{namespace}/{name}

Resource Names

{provider}_{resource}

Examples:

  • mycloud_environment
  • mycloud_database

Data Source Names

{provider}_{datasource}

Examples:

  • mycloud_regions
  • mycloud_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

  1. Use version control from the start
  2. Document your provider immediately
  3. Set up CI/CD early
  4. Follow naming conventions
  5. 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.

Next: Project Structure →


Previous: ← Go Installation

Last updated: June 30, 2026

Updated: