This repository provides a complete, production-ready, and highly scalable multi-service Go monorepo template. The core philosophy is abstraction and reusability, using custom Starlark macros to encapsulate the project's core workflows so that adding new services is trivial.
- Abstraction: Complex workflows like API code generation and Kubernetes manifest creation are hidden behind simple, reusable Bazel macros (
go_api_library,k8s_environment). - Reusability: Shared code, such as database clients and loggers, is placed in the
/pkgdirectory. The Kubernetes deployment model uses a single, generic Helm chart that is customized for each service and environment. - Trivial Onboarding: Adding a new service is as simple as copying an existing service, updating a few lines of configuration, and running
bazel run //:gazelle.
- Build System: Bazel 8 LTS
- Dependency Management: Bzlmod (
MODULE.bazel) - Language: Go 1.25.4
- API: gRPC, gRPC-Gateway, OpenAPI v2
- Database ORM: GORM (with PostgreSQL driver)
- Containerization: OCI Images (via
rules_oci) - Deployment: Helm + Kustomize (via
rules_helmandrules_kustomize) - Linting:
golangci-lint(viarules_lint)
Before you begin, ensure you have the following installed:
- Bazel: Version 8.0.0 or higher. We recommend using Bazelisk to automatically manage the Bazel version from the
.bazelversionfile. - Go: Version 1.25.4 or higher.
- Container registry access: Needed only to push images with
oci_push.
To get your local development environment and IDE tooling (gopls) in sync, run the following command. This command uses Gazelle to analyze the Bzlmod dependencies in MODULE.bazel and updates the go.mod file accordingly.
bazel run //:gazelle-go-modBuild all services, libraries, and generated code in the entire monorepo.
bazel build //...Run all unit tests and lint checks across the entire monorepo.
bazel test //...To run a service locally (e.g., the user-api), use the bazel run command. This will build the service and its dependencies and execute the resulting binary.
# The user-api will be available on:
# gRPC: localhost:8080
# HTTP: localhost:8081
bazel run //services/user-api- Copy Existing Service: Copy an existing service directory (e.g.,
cp -r services/product-api services/new-service). - Define Protobuf API: Add a new
.protofile in/proto/acme/new-service/v1. - Update BUILD files: Modify the
BUILD.bazelfiles in the new directories to reflect the new service name and dependencies. - Run Gazelle:
bazel run //:gazelleto update the Go build rules. - Add Deployment Config: Add new
values-new-service.yamlfiles to the Kustomize overlays in/k8s/overlays.
You can generate a single, deployable YAML manifest for any environment using the k8s_environment macro.
# Generate the manifest for the 'dev' environment
bazel build //k8s:dev
# The output will be located at: bazel-bin/k8s/dev.yaml
# You can deploy it directly with kubectl:
kubectl apply -f bazel-bin/k8s/dev.yamlIf you want to inspect or diff the raw Kubernetes manifests produced before they are bundled by the k8s_environment macro, you can build each overlay explicitly using the manifests alias target:
# Build raw manifests for dev overlay
bazel build //k8s/overlays/dev:manifests
# Example output file (path depends on rule implementation): bazel-bin/k8s/overlays/dev/kustomization.yaml
# Build raw manifests for staging overlay
bazel build //k8s/overlays/staging:manifests
# Build raw manifests for prod overlay
bazel build //k8s/overlays/prod:manifestsThese overlay outputs are useful for:
- Reviewing environment-specific patches
- Running
kubectl difflocally - Feeding into cluster policy scanners (e.g., conftest, kube-score) prior to full packaging
The k8s_environment macro then combines:
- Helm chart render (parameterized by service values files)
- Kustomize overlay transformations
- Image references built by Bazel
to produce a single deployment manifest per environment (//k8s:dev, //k8s:staging, //k8s:prod).
To build and push a service's OCI image to the configured container registry (harbor.example.com), use the :push target. Images are built with oci_image and published with oci_push.
bazel run //services/user-api:push
bazel run //services/product-api:pushTo push the base Helm chart as an OCI artifact to the registry, use the :push-oci target (unchanged).
bazel run //charts/app:push-ociThis template uses a powerful combination of Helm and Kustomize for managing Kubernetes deployments.
- Base Helm Chart (
/charts/app): A single, generic "App" chart defines the common Kubernetes resources (Deployment,Service,HPA, etc.). It's highly configurable via avalues.yamlfile. - Kustomize Overlays (
/k8s/overlays): Each environment (dev,staging,prod) has its own Kustomize overlay. Thekustomization.yamlfile in each overlay:
- Specifies which services to deploy.
- Points to environment-specific
values-*.yamlfiles to configure each service's replica count, resources, etc. - Can apply strategic patches to the manifests (e.g., adding environment variables or sidecar containers).
This approach provides maximum flexibility and avoids duplicating YAML configuration.
| Use Case | Recommended Target |
|---|---|
| Quick full environment manifest | //k8s:dev (or staging/prod) |
| Inspect raw overlay output | //k8s/overlays/dev:dev_manifests |
| Run policy/lint checks on manifests | Overlay targets (raw) |
| Pre-deploy diff against cluster | Overlay targets + kubectl diff |
| CI artifact for promotion | Environment macro target |
bazel build //k8s/overlays/dev:dev_manifests
conftest test bazel-bin/k8s/overlays/dev/dev_manifests.yamlFor production-grade secrets management, we recommend using the External Secrets Operator (ESO). This operator fetches secrets from an external provider (like AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault) and injects them as native Kubernetes Secret objects.
Workflow:
-
Install ESO: Install the External Secrets Operator in your Kubernetes cluster.
-
Create an
ExternalSecret: You would create a manifest like the one below, which tells ESO to fetch a secret from your cloud provider and create a KubernetesSecretnameddb-credentialswith the keyDATABASE_URL.# Example: external-secret.yaml (This file would live outside this repo) apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: database-credentials spec: secretStoreRef: name: aws-secret-store # Assumes you have a SecretStore configured kind: ClusterSecretStore target: name: db-credentials # This is the name of the K8s Secret to create data: - secretKey: DATABASE_URL remoteRef: key: my-app/database/url # The key of the secret in AWS Secrets Manager
-
Reference the Secret: The Kustomize overlay in
/k8s/overlays/dev/kustomization.yamlalready includes a patch to reference thisdb-credentialssecret and mount it as an environment variable in theuser-apideployment. This keeps your application code clean and your deployment manifests free of sensitive information.