← back to billwear

GoBear — learning Go by building a bare-metal provisioner

A public, release-early, release-often journey. The goal: learn Go while shaping a small, real network service that could grow into a bare-metal provisioner.


Project shape

Scope

Start as a general-purpose network service with solid bones: structured logging, Prometheus metrics, health endpoints, clean shutdown, and a minimal API. Grow toward PXE/TFTP helpers and a small state store.

Why this order

Production-shaped scaffolding first means every experiment runs inside sane boundaries. Learning Go and ops habits at the same time pays long-term dividends.

Milestone 0 — a production-shaped starter

Bootstrap a single binary that can log, expose metrics, respond to health checks, and shut down gracefully on SIGINT/SIGTERM.

go.mod

module github.com/yourname/gobear

go 1.22

require (
    github.com/prometheus/client_golang v1.18.0
)

cmd/gobear/main.go

package main

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	"github.com/prometheus/client_golang/prometheus/promhttp"
)

// abbreviated for brevity...
func main() {{
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{{Level: slog.LevelInfo}}))
	slog.SetDefault(logger)
	logger.Info("starting gobear")
	// mux, handlers, graceful shutdown, etc.
}}

Run it

go mod tidy
go run ./cmd/gobear
# in another shell:
curl -s localhost:8080/healthz
curl -s localhost:8080/
curl -s localhost:8080/metrics | head

What we have now

A clean, single binary with structured logs, metrics, health checks, and graceful shutdown — the core of any network service.

Milestone 1 — configuration & layout

  • Project layout: cmd/gobear/ for the main, internal/ for private packages, pkg/ for public ones later.
  • Env-first config: environment variables with sane defaults; support YAML/TOML later if needed.
  • Build metadata: inject version, commit, date via -ldflags to print at startup.

Version injection

// inside main.go
var (
    version = "dev"
    commit  = "none"
    date    = "unknown"
)
logger.Info("build", "version", version, "commit", commit, "date", date)
# build with metadata
go build -ldflags "-X main.version=$(git describe --tags --always) \
 -X main.commit=$(git rev-parse --short HEAD) \
 -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" ./cmd/gobear

Milestone 2 — a tiny API for provisioning

Start with a simulated provisioning workflow that returns a token. No PXE yet — just shape the flow.

// add to mux in main()
mux.HandleFunc("/api/v1/provision", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        w.WriteHeader(http.StatusMethodNotAllowed); return
    }
    token := fmt.Sprintf("pxe_%d", time.Now().Unix())
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"token":"%s"}`, token)
})
# try it
curl -s -X POST localhost:8080/api/v1/provision

Why a fake first?

Design the request/response and logging paths before touching PXE/TFTP. It keeps learning focused and increments small.

Milestone 3 — health, readiness, and metrics

  • /healthz: quick “am I alive?”
  • /readyz: add checks for dependencies (e.g., can we read a state file?)
  • /metrics: Prometheus handler already mounted
// example readiness check
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
    if _, err := os.Stat("state.db"); err == nil {
        w.Write([]byte("ready")); return
    }
    w.WriteHeader(http.StatusServiceUnavailable)
    w.Write([]byte("not ready"))
})

Milestone 4 — next steps toward a provisioner

  • PXE notes: A minimal PXE flow needs DHCP options (66/67), a TFTP server, and kernel/initrd with a preseed or cloud-init data source.
  • State: Start with a tiny BoltDB/SQLite or JSON file for requests and tokens.
  • Templates: Render cloud-init or iPXE templates from Go to hand to clients.
  • CLI: Add --addr flags using the standard flag package.

Release early, release often

# quick quality loop
go test ./...
golangci-lint run   # if you add it
go build ./cmd/gobear
./gobear

Commit small, runnable changes. Keep a CHANGELOG. Tag “walkable” points even if features are thin. The habit matters more than the scope in the early days.