Project Structure
Project Structure Overview
A well-organized project structure is crucial for maintainability, collaboration, and following community standards. This chapter explains the standard Terraform provider directory layout.
Understanding where files belong and why makes development smoother and helps others navigate your code.
Standard Provider Layout
terraform-provider-mycloud/
├── .github/ # GitHub-specific files
│ ├── workflows/ # GitHub Actions CI/CD
│ │ ├── test.yml
│ │ └── release.yml
│ └── ISSUE_TEMPLATE/ # Issue templates
├── .goreleaser.yml # GoReleaser configuration
├── .gitignore # Git ignore rules
├── LICENSE # Open source license
├── README.md # Project documentation
├── GNUmakefile # Build automation
├── go.mod # Go module definition
├── go.sum # Dependency checksums
├── main.go # Provider entry point
├── docs/ # Generated documentation
│ ├── index.md
│ ├── resources/
│ │ ├── environment.md
│ └── data-sources/
│ └── regions.md
├── examples/ # Usage examples
│ ├── provider/
│ │ └── provider.tf
│ ├── resources/
│ │ └── environment/
│ │ └── resource.tf
│ └── data-sources/
│ └── regions/
│ └── data-source.tf
├── internal/ # Internal packages
│ ├── provider/ # Provider implementation
│ │ ├── provider.go
│ │ ├── provider_test.go
│ │ ├── resource_environment.go
│ │ ├── resource_environment_test.go
│ │ ├── data_source_regions.go
│ │ └── data_source_regions_test.go
│ └── client/ # API client
│ ├── client.go
│ ├── client_test.go
│ ├── environments.go
│ └── regions.go
└── tools/ # Development tools
└── tools.go
Let’s examine each directory and file.
Root Directory Files
main.go
Purpose: Provider entry point
Contents:
- Provider server setup
- GRPC initialization
- Debug mode handling
Typical size: 20-40 lines
Why at root: Go convention for executable main packages
go.mod
Purpose: Go module definition
Contains:
- Module path
- Go version
- Dependencies
Why at root: Required by Go tooling
go.sum
Purpose: Dependency checksums
Auto-generated: By go mod commands
Why at root: Required by Go tooling
GNUmakefile
Purpose: Build automation
Common targets:
build— Compile providerinstall— Install locallytest— Run teststestacc— Run acceptance testsfmt— Format codelint— Lint codedocs— Generate documentation
README.md
Purpose: Project documentation
Should include:
- Provider description
- Installation instructions
- Usage examples
- Development guide
- Contributing guidelines
LICENSE
Purpose: Legal terms for use
Common choices:
- MIT — Simple, permissive
- Apache 2.0 — Patent protection
- MPL 2.0 — HashiCorp standard
.gitignore
Purpose: Exclude files from Git
Typical entries:
# Binaries
*.exe
terraform-provider-*
!terraform-provider-*/
# Terraform
.terraform/
*.tfstate*
.terraform.lock.hcl
# Go
vendor/
go.work
# IDEs
.idea/
.vscode/
# OS
.DS_Store
.goreleaser.yml
Purpose: Release automation configuration
Handles:
- Multi-platform builds
- Binary packaging
- GitHub release creation
- Terraform Registry publishing
internal/ Directory
The internal/ directory contains private packages that cannot be imported by external projects.
Why internal/?
Go’s internal package convention:
- Code in
internal/is private - Cannot be imported by other modules
- Enforces encapsulation
internal/provider/
Purpose: Provider implementation
Files:
provider.go
Main provider struct and methods:
Metadata()— Provider nameSchema()— Configuration schemaConfigure()— InitializationResources()— Resource listDataSources()— Data source list
provider_test.go
Provider unit tests
resource_*.go
Individual resource implementations:
resource_environment.goresource_database.goresource_network.go
Each contains:
- Resource struct
- Schema definition
- CRUD operations (Create, Read, Update, Delete)
resource_*_test.go
Resource tests:
- Unit tests
- Acceptance tests
data_source_*.go
Data source implementations:
data_source_regions.godata_source_instance_types.go
Each contains:
- Data source struct
- Schema definition
- Read operation
data_source_*_test.go
Data source tests
internal/client/
Purpose: API client implementation
Separation benefits:
- Testable independently
- Reusable across resources
- Clean provider code
Files:
client.go
Main API client:
type Client struct {
BaseURL string
APIKey string
HTTPClient *http.Client
}
func NewClient(endpoint, apiKey string) *Client {
return &Client{
BaseURL: endpoint,
APIKey: apiKey,
HTTPClient: &http.Client{Timeout: 30 * time.Second},
}
}
environments.go
Environment-specific API calls:
func (c *Client) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error)
func (c *Client) GetEnvironment(ctx context.Context, id string) (*Environment, error)
func (c *Client) UpdateEnvironment(ctx context.Context, id string, req UpdateEnvironmentRequest) (*Environment, error)
func (c *Client) DeleteEnvironment(ctx context.Context, id string) error
regions.go
Region-specific API calls:
func (c *Client) ListRegions(ctx context.Context) ([]Region, error)
docs/ Directory
Purpose: Auto-generated documentation
Generated by: terraform-plugin-docs
Structure:
docs/
├── index.md # Provider overview
├── resources/
│ ├── environment.md # Resource docs
│ └── database.md
└── data-sources/
├── regions.md # Data source docs
└── instance_types.md
Why generated?
- Consistency across providers
- Auto-includes examples
- Formatted for Terraform Registry
- Extracts schema automatically
Do not edit manually! Docs are regenerated on every build.
examples/ Directory
Purpose: Working usage examples
Structure:
examples/
├── provider/
│ └── provider.tf # Provider configuration
├── resources/
│ ├── environment/
│ │ └── resource.tf # Environment resource example
│ └── database/
│ └── resource.tf
└── data-sources/
├── regions/
│ └── data-source.tf # Regions data source example
└── instance_types/
└── data-source.tf
examples/provider/provider.tf
terraform {
required_providers {
mycloud = {
source = "YOUR_USERNAME/mycloud"
}
}
}
provider "mycloud" {
endpoint = "https://api.mycloud.com"
api_key = var.mycloud_api_key
}
examples/resources/environment/resource.tf
resource "mycloud_environment" "example" {
name = "staging"
region = "us-west-2"
replicas = 3
config = {
memory_gb = 4
cpu_cores = 2
}
}
examples/data-sources/regions/data-source.tf
data "mycloud_regions" "available" {
filter {
name = "availability"
values = ["high"]
}
}
output "regions" {
value = data.mycloud_regions.available.names
}
Purpose of examples:
- Documentation input
- Manual testing
- User reference
.github/ Directory
Purpose: GitHub-specific configuration
.github/workflows/
CI/CD workflow definitions
test.yml
name: Tests
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go test ./...
release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --clean
tools/ Directory
Purpose: Development tool dependencies
tools.go
//go:build tools
package tools
import (
_ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs"
)
Why?
- Pins tool versions in go.mod
- Ensures consistent tooling across team
- Works with
go install
File Naming Conventions
Resources
resource_{name}.go
resource_{name}_test.go
Examples:
resource_environment.goresource_database.go
Data Sources
data_source_{name}.go
data_source_{name}_test.go
Examples:
data_source_regions.godata_source_instance_types.go
Tests
Same name as implementation + _test.go:
provider_test.goresource_environment_test.go
Package Organization
One Package vs Multiple
Option 1: Single provider package (Recommended)
internal/provider/
├── provider.go
├── resource_environment.go
├── resource_database.go
├── data_source_regions.go
Pros:
- Simpler imports
- Easier navigation
- Standard practice
Option 2: Separate packages
internal/
├── provider/
│ └── provider.go
├── resources/
│ ├── environment.go
│ └── database.go
└── datasources/
└── regions.go
Pros:
- Better organization for large providers
- Clearer separation of concerns
When to use: Providers with 20+ resources
Alternative Structures
pkg/ vs internal/
Some providers use pkg/:
pkg/
└── provider/
Difference:
pkg/— Public, can be importedinternal/— Private, cannot be imported
Recommendation: Use internal/ for provider code
Flat Structure (Small Providers)
For very small providers:
terraform-provider-mycloud/
├── main.go
├── provider.go
├── resource_environment.go
└── data_source_regions.go
When appropriate:
- < 5 resources
- Single developer
- Rapid prototyping
Not recommended for production providers
Configuration Files
.editorconfig
Ensure consistent formatting:
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
.golangci.yml
Linter configuration:
run:
timeout: 5m
linters:
enable:
- gofmt
- govet
- staticcheck
- gosimple
- ineffassign
Best Practices
1. Consistent Naming
- Use snake_case for file names
- Match resource names:
mycloud_environment→resource_environment.go
2. Logical Grouping
- Keep related code together
- Group by functionality, not type
3. Test Proximity
- Place tests next to implementation
- Same filename +
_test.go
4. Documentation Close to Code
- Examples in
examples/ - Comments in code for doc generation
5. Separate API Client
- Keep provider logic separate from API calls
- Makes testing easier
Common Mistakes
❌ Mixing public and private
# BAD
pkg/provider/
internal/client/
Choose one: either pkg/ or internal/
❌ Deep nesting
# BAD
internal/provider/resources/compute/environments/environment.go
Keep it shallow
❌ No examples
Always include working examples
❌ Manual documentation
Use terraform-plugin-docs, don’t write docs manually
Verification Checklist
Your structure should have:
- ✅
main.goat root - ✅
internal/provider/directory - ✅ Separate
_test.gofiles - ✅
examples/with working configs - ✅
docs/for generated docs - ✅
.github/workflows/for CI - ✅ Proper
.gitignore - ✅ README and LICENSE
Key Takeaways
- ✅ Use
internal/for provider code - ✅ Separate API client from provider logic
- ✅ Follow naming conventions strictly
- ✅ Keep structure flat and simple
- ✅ Include working examples
- ✅ Generate documentation automatically
- ✅ Co-locate tests with implementation
What’s Next?
Now let’s learn about managing dependencies with Go modules.
Next: Dependencies and Modules →
Previous: ← Project Scaffolding
Last updated: June 30, 2026