Hands-On Lab
Lab Overview
Now it’s time to put everything together! In this hands-on lab, you’ll build a complete (albeit simple) Terraform provider from scratch, install it locally, and test it with real Terraform configurations.
This lab consolidates everything you’ve learned about Go, project structure, and dependencies.
Lab Objectives
By the end of this lab, you will:
- ✅ Create a working provider
- ✅ Build the provider binary
- ✅ Install it locally for Terraform
- ✅ Write Terraform configurations
- ✅ Execute terraform init/plan/apply
- ✅ Verify provider functionality
Estimated time: 45-60 minutes
Prerequisites
Before starting:
- Go 1.21+ installed
- Terraform 1.5+ installed
- Git installed
- Text editor ready (VS Code recommended)
- Terminal access
Verify:
go version # Should show 1.21+
terraform version # Should show 1.5+
git --version
Lab Architecture
You’ll build terraform-provider-time, a simple provider that creates time-based resources.
Why this provider?
- No external API required
- Demonstrates core concepts
- Easy to test and verify
- Actually useful!
What it does:
- Creates a resource that stores a timestamp
- Supports create, read, and delete operations
- Uses standard Plugin Framework patterns
Step 1: Create Project
1.1 Create Directory
# Create project directory
mkdir -p ~/terraform-providers/terraform-provider-time
cd ~/terraform-providers/terraform-provider-time
1.2 Initialize Git
git init
echo "# Terraform Provider: Time" > README.md
git add README.md
git commit -m "Initial commit"
1.3 Initialize Go Module
go mod init github.com/YOUR_USERNAME/terraform-provider-time
Replace YOUR_USERNAME with your actual GitHub username (or use any valid module path).
Expected output:
go: creating new go.mod: module github.com/YOUR_USERNAME/terraform-provider-time
1.4 Create Directory Structure
mkdir -p internal/provider
mkdir -p examples/provider
mkdir -p examples/resources/time_static
Verify:
tree -L 2
Expected:
.
├── README.md
├── go.mod
├── examples/
│ ├── provider/
│ └── resources/
└── internal/
└── provider/
Step 2: Add Dependencies
# Add Plugin Framework
go get github.com/hashicorp/terraform-plugin-framework@v1.5.0
# Add Plugin Go
go get github.com/hashicorp/terraform-plugin-go@v0.21.0
# Add Plugin Log
go get github.com/hashicorp/terraform-plugin-log@v0.9.0
# Tidy up
go mod tidy
Verify go.mod:
cat go.mod
Should look like:
module github.com/YOUR_USERNAME/terraform-provider-time
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
)
Step 3: Create main.go
Create main.go in the project root:
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/YOUR_USERNAME/terraform-provider-time/internal/provider"
)
var (
version string = "dev"
)
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/YOUR_USERNAME/time",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}
Remember: Replace YOUR_USERNAME with your actual username!
Step 4: Create Provider Implementation
Create internal/provider/provider.go:
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"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ provider.Provider = &TimeProvider{}
// TimeProvider defines the provider implementation.
type TimeProvider struct {
version string
}
// TimeProviderModel describes the provider data model.
type TimeProviderModel struct {}
func (p *TimeProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "time"
resp.Version = p.version
}
func (p *TimeProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "The time provider is used to create time-based resources.",
}
}
func (p *TimeProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var data TimeProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
}
func (p *TimeProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewTimeStaticResource,
}
}
func (p *TimeProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{}
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &TimeProvider{
version: version,
}
}
}
Step 5: Create Resource
Create internal/provider/resource_time_static.go:
package provider
import (
"context"
"fmt"
"time"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &TimeStaticResource{}
func NewTimeStaticResource() resource.Resource {
return &TimeStaticResource{}
}
// TimeStaticResource defines the resource implementation.
type TimeStaticResource struct{}
// TimeStaticResourceModel describes the resource data model.
type TimeStaticResourceModel struct {
ID types.String `tfsdk:"id"`
Timestamp types.String `tfsdk:"timestamp"`
}
func (r *TimeStaticResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_static"
}
func (r *TimeStaticResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Provides a static timestamp resource.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Resource identifier",
},
"timestamp": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Static timestamp in RFC3339 format",
},
},
}
}
func (r *TimeStaticResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data TimeStaticResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Generate timestamp
timestamp := time.Now().UTC().Format(time.RFC3339)
data.Timestamp = types.StringValue(timestamp)
data.ID = types.StringValue(timestamp)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *TimeStaticResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data TimeStaticResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Timestamp is static, no updates needed
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *TimeStaticResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data TimeStaticResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *TimeStaticResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Nothing to do - timestamp just disappears from state
}
Step 6: Build Provider
# Build the provider
go build -o terraform-provider-time
# Verify binary exists
ls -lh terraform-provider-time
Expected output:
-rwxr-xr-x 1 user staff 23M Nov 15 10:30 terraform-provider-time
Step 7: Install Provider Locally
Terraform looks for providers in specific directories. Let’s install there.
7.1 Determine Installation Path
macOS/Linux ARM64 (M1/M2):
PROVIDER_PATH=~/.terraform.d/plugins/registry.terraform.io/YOUR_USERNAME/time/0.1.0/darwin_arm64
macOS/Linux AMD64:
PROVIDER_PATH=~/.terraform.d/plugins/registry.terraform.io/YOUR_USERNAME/time/0.1.0/darwin_amd64
Linux AMD64:
PROVIDER_PATH=~/.terraform.d/plugins/registry.terraform.io/YOUR_USERNAME/time/0.1.0/linux_amd64
Windows AMD64:
$PROVIDER_PATH="$env:APPDATA\terraform.d\plugins\registry.terraform.io\YOUR_USERNAME\time\0.1.0\windows_amd64"
Replace YOUR_USERNAME with your actual username!
7.2 Create Directory and Install
# Create directory
mkdir -p $PROVIDER_PATH
# Copy binary
cp terraform-provider-time $PROVIDER_PATH/
# Verify
ls -lh $PROVIDER_PATH/
Step 8: Create Terraform Configuration
Create a test directory:
mkdir -p ~/terraform-test
cd ~/terraform-test
Create main.tf:
terraform {
required_providers {
time = {
source = "registry.terraform.io/YOUR_USERNAME/time"
version = "0.1.0"
}
}
}
provider "time" {}
resource "time_static" "example" {}
output "timestamp" {
value = time_static.example.timestamp
}
Remember: Replace YOUR_USERNAME!
Step 9: Initialize Terraform
terraform init
Expected output:
Initializing the backend...
Initializing provider plugins...
- Finding registry.terraform.io/YOUR_USERNAME/time versions matching "0.1.0"...
- Installing registry.terraform.io/YOUR_USERNAME/time v0.1.0...
- Installed registry.terraform.io/YOUR_USERNAME/time v0.1.0 (unauthenticated)
Terraform has been successfully initialized!
Step 10: Plan and Apply
10.1 Create Plan
terraform plan
Expected output:
Terraform will perform the following actions:
# time_static.example will be created
+ resource "time_static" "example" {
+ id = (known after apply)
+ timestamp = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ timestamp = (known after apply)
10.2 Apply Configuration
terraform apply -auto-approve
Expected output:
time_static.example: Creating...
time_static.example: Creation complete after 0s [id=2024-11-15T10:30:45Z]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
timestamp = "2024-11-15T10:30:45Z"
10.3 Verify State
terraform show
Expected output:
# time_static.example:
resource "time_static" "example" {
id = "2024-11-15T10:30:45Z"
timestamp = "2024-11-15T10:30:45Z"
}
Outputs:
timestamp = "2024-11-15T10:30:45Z"
Step 11: Test Modifications
11.1 Run Apply Again
terraform apply -auto-approve
Expected: No changes (timestamp is static)
time_static.example: Refreshing state... [id=2024-11-15T10:30:45Z]
No changes. Your infrastructure matches the configuration.
11.2 Destroy Resource
terraform destroy -auto-approve
Expected output:
time_static.example: Destroying... [id=2024-11-15T10:30:45Z]
time_static.example: Destruction complete after 0s
Destroy complete! Resources: 1 destroyed.
Success Criteria
You’ve successfully completed the lab if:
- ✅ Provider compiles without errors
- ✅
terraform initsucceeds - ✅
terraform planshows resource creation - ✅
terraform applycreates resource - ✅ Output shows valid RFC3339 timestamp
- ✅
terraform destroyremoves resource
Lab Enhancements (Optional)
Want to go further? Try these:
Enhancement 1: Add Configuration
Modify provider to accept a custom prefix:
type TimeProviderModel struct {
Prefix types.String `tfsdk:"prefix"`
}
// In Schema():
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"prefix": schema.StringAttribute{
Optional: true,
},
},
}
Enhancement 2: Add Validation
Require timestamps to be after a certain date:
"timestamp": schema.StringAttribute{
Computed: true,
Validators: []validator.String{
// Add custom validator
},
}
Enhancement 3: Add Data Source
Create a data source that returns current time:
func NewTimeCurrentDataSource() datasource.DataSource {
return &TimeCurrentDataSource{}
}
Common Lab Issues
Issue: Provider Not Found
Error:
Error: Failed to query available provider packages
Solutions:
- Check installation path matches your OS/architecture
- Verify username matches in all files
- Run
terraform initagain
Issue: Build Fails
Error:
undefined: provider.New
Solutions:
- Verify all imports are correct
- Run
go mod tidy - Check
internal/provider/provider.goexportsNew()function
Issue: Import Errors
Error:
no required module provides package github.com/YOUR_USERNAME/terraform-provider-time/internal/provider
Solution:
go mod tidy
go build
Issue: Wrong Architecture
Error:
incompatible architecture
Solution: Rebuild for correct architecture:
# For ARM64 Mac
GOARCH=arm64 GOOS=darwin go build
# For AMD64 Mac
GOARCH=amd64 GOOS=darwin go build
# For Linux
GOARCH=amd64 GOOS=linux go build
Debugging Tips
Enable Debug Logging
TF_LOG=DEBUG terraform apply
Check Provider Path
# macOS/Linux
ls -R ~/.terraform.d/plugins/
# Windows
dir /s %APPDATA%\terraform.d\plugins\
Verify Binary
./terraform-provider-time --help
Should show:
Usage of ./terraform-provider-time:
-debug
set to true to run the provider with support for debuggers
Clean Up
After completing the lab:
# Remove test directory
cd ~
rm -rf ~/terraform-test
# Keep provider for future use, or remove:
rm -rf ~/.terraform.d/plugins/registry.terraform.io/YOUR_USERNAME/time
Key Takeaways
- ✅ Provider development follows standard Go project structure
- ✅ Local installation enables rapid testing
- ✅ Terraform init resolves providers from local cache
- ✅ Plugin Framework simplifies resource implementation
- ✅ Hands-on practice solidifies concepts
What’s Next?
Test your knowledge with practice questions!
Previous: ← Dependencies and Modules
Last updated: June 30, 2026