Project Structure

6 minute read

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 provider
  • install — Install locally
  • test — Run tests
  • testacc — Run acceptance tests
  • fmt — Format code
  • lint — Lint code
  • docs — 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 name
  • Schema() — Configuration schema
  • Configure() — Initialization
  • Resources() — Resource list
  • DataSources() — Data source list

provider_test.go

Provider unit tests

resource_*.go

Individual resource implementations:

  • resource_environment.go
  • resource_database.go
  • resource_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.go
  • data_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.go
  • resource_database.go

Data Sources

data_source_{name}.go
data_source_{name}_test.go

Examples:

  • data_source_regions.go
  • data_source_instance_types.go

Tests

Same name as implementation + _test.go:

  • provider_test.go
  • resource_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 imported
  • internal/ — 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.go at root
  • ✅ internal/provider/ directory
  • ✅ Separate _test.go files
  • ✅ 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

Updated: