Read OSS

Navigating the Kubernetes Monorepo: Architecture Overview and Code Map

Intermediate

Prerequisites

  • Basic Go proficiency (packages, interfaces, modules)
  • Familiarity with Kubernetes concepts (Pods, Deployments, Services, Nodes)

Navigating the Kubernetes Monorepo: Architecture Overview and Code Map

Kubernetes is one of the largest open-source Go projects ever built. At over four million lines of code, its repository is vast — but it's also remarkably well-organized. The codebase follows consistent patterns that, once understood, make navigating even the most complex subsystems straightforward. This article establishes the mental map you'll need for every subsequent deep dive in this series.

Repository Layout: The Top-Level Directory Map

Let's start with orientation. The root of the repository contains a handful of critical directories, each with a well-defined purpose:

Directory Purpose
cmd/ Entry points for all Kubernetes binaries (one subdirectory per binary)
pkg/ Internal implementation packages — the heart of each component
staging/ In-tree development of independently publishable Go modules
plugin/ Built-in admission control plugins
api/ OpenAPI specifications and generated Swagger docs
build/ Build infrastructure (container images, cross-compilation)
hack/ Developer scripts (code generation, verification, testing)
test/ Integration and end-to-end tests
vendor/ Vendored dependencies (committed to the repository)
third_party/ Forked third-party code

The root go.mod declares the module as k8s.io/kubernetes and targets Go 1.26:

module k8s.io/kubernetes

go 1.26.0

Tip: The pkg/ directory is not a public API surface. Despite the Go convention, nothing in pkg/ is intended for external import — external consumers use the independently published modules from staging/.

The Staging Mechanism: 33 Modules in a Monorepo

This is arguably the most important architectural decision in the entire codebase. Kubernetes develops 33 Go modules inside the monorepo under staging/src/k8s.io/, but publishes each one as an independent module that external projects can import (e.g., k8s.io/client-go, k8s.io/apimachinery, k8s.io/api).

The go.work file is the glue. It uses Go's workspace feature to redirect all 33 staging modules to their local paths:

use (
	.
	./staging/src/k8s.io/api
	./staging/src/k8s.io/apiextensions-apiserver
	./staging/src/k8s.io/apimachinery
	./staging/src/k8s.io/apiserver
	./staging/src/k8s.io/client-go
	// ... 28 more
)

This means developers edit code in staging/src/k8s.io/client-go/ and it immediately takes effect across the entire monorepo — no need to publish a release and update dependencies.

graph TD
    ROOT["k8s.io/kubernetes<br/>(root module)"]
    API["k8s.io/api"]
    AM["k8s.io/apimachinery"]
    AS["k8s.io/apiserver"]
    CG["k8s.io/client-go"]
    CB["k8s.io/component-base"]
    
    ROOT -->|imports| AS
    ROOT -->|imports| CG
    ROOT -->|imports| CB
    AS -->|imports| AM
    AS -->|imports| CG
    CG -->|imports| API
    CG -->|imports| AM
    API -->|imports| AM
    CB -->|imports| AM
    
    style ROOT fill:#f9f,stroke:#333,stroke-width:2px
    style AM fill:#bbf,stroke:#333

The dependency hierarchy is strict. k8s.io/apimachinery sits at the bottom — it defines the type system and has no dependency on any other Kubernetes module. k8s.io/api adds the concrete API types. k8s.io/client-go builds on both to provide the client library. k8s.io/apiserver brings the server framework. The root k8s.io/kubernetes module imports everything.

Each staging module has its own go.mod. For example, staging/src/k8s.io/client-go/go.mod declares module k8s.io/client-go and lists its dependencies on other k8s.io/* modules. A publishing bot periodically syncs these staging directories to their respective standalone GitHub repositories (e.g., github.com/kubernetes/client-go).

Tip: If you're building a Kubernetes controller or operator, you'll never import k8s.io/kubernetes directly. You'll use the published staging modules: k8s.io/client-go, k8s.io/api, and k8s.io/apimachinery.

The Five Core Binaries and Their Shared Boot Pattern

Every major Kubernetes binary lives under cmd/ and follows an identical bootstrap pattern. The entry point is minimal — typically under 10 lines of actual logic:

flowchart LR
    A["main()"] --> B["app.NewXxxCommand()"]
    B --> C["cobra.Command"]
    C --> D["cli.Run(command)"]
    D --> E["os.Exit(code)"]

Look at the kube-apiserver entry point:

func main() {
	command := app.NewAPIServerCommand()
	code := cli.Run(command)
	os.Exit(code)
}

Now compare the kube-controller-manager:

func main() {
	command := app.NewControllerManagerCommand()
	code := cli.Run(command)
	os.Exit(code)
}

And the kube-scheduler:

func main() {
	command := app.NewSchedulerCommand()
	code := cli.Run(command)
	os.Exit(code)
}

The pattern is identical: create a cobra command, run it through cli.Run(), exit. The cli.Run function from k8s.io/component-base/cli handles signal trapping, structured logging initialization, and clean shutdown.

The one exception is the kubelet, which passes an explicit context.Background():

func main() {
	command := app.NewKubeletCommand(context.Background())
	code := cli.Run(command)
	os.Exit(code)
}

This is because the kubelet needs to manage its own context lifecycle independently — it may need to outlive the cobra command's context for graceful shutdown of running containers.

All binaries share these side-effect imports for observability:

  • k8s.io/component-base/logs/json/register — JSON log format
  • k8s.io/component-base/metrics/prometheus/clientgo — Prometheus client metrics
  • k8s.io/component-base/metrics/prometheus/version — version metrics

The Options → Config → Complete Pattern

Every Kubernetes component follows a universal three-phase configuration pipeline. This pattern enforces a strict separation between user-provided flags, computed configuration, and validated-ready-to-run state.

flowchart TD
    A["Options struct<br/>(raw CLI flags)"] -->|".Complete()"| B["Config struct<br/>(defaults applied)"]
    B -->|".Complete()"| C["completedConfig<br/>(unexported, validated)"]
    C -->|"CompletedConfig wrapper"| D["Ready to Run"]
    
    style A fill:#ffd,stroke:#333
    style B fill:#dfd,stroke:#333
    style C fill:#ddf,stroke:#333
    style D fill:#fdf,stroke:#333

The kube-apiserver demonstrates this beautifully. In cmd/kube-apiserver/app/server.go, the Run() function chains:

func Run(ctx context.Context, opts options.CompletedOptions) error {
	config, err := NewConfig(opts)      // Options → Config
	completed, err := config.Complete() // Config → CompletedConfig
	server, err := CreateServerChain(completed)
	prepared, err := server.PrepareRun()
	return prepared.Run(ctx)
}

The trick is in pkg/controlplane/instance.go. The completedConfig struct is unexported, wrapped in a public CompletedConfig:

type completedConfig struct {
	ControlPlane controlplaneapiserver.CompletedConfig
	*Extra
}

type CompletedConfig struct {
	*completedConfig
}

Because completedConfig is unexported, no external package can construct a CompletedConfig directly. The only way to obtain one is through the Complete() method, which guarantees all defaults have been applied and validation has passed. This is a clever use of Go's visibility rules to encode invariants in the type system.

Component Architecture: How the Pieces Fit Together

Now that we understand the code organization, let's see how the components interact at runtime:

flowchart TD
    subgraph CP["Control Plane"]
        ETCD[(etcd)]
        API["kube-apiserver"]
        CM["kube-controller-manager<br/>(40+ controllers)"]
        SCHED["kube-scheduler"]
    end
    
    subgraph NODE["Node"]
        KL["kubelet"]
        KP["kube-proxy"]
        CRI["Container Runtime<br/>(containerd)"]
    end
    
    KUBECTL["kubectl"] -->|"REST/HTTP"| API
    API -->|"read/write"| ETCD
    CM -->|"watch + update"| API
    SCHED -->|"watch pods<br/>+ bind"| API
    KL -->|"watch + status"| API
    KL -->|"gRPC (CRI)"| CRI
    KP -->|"watch services"| API

The key insight is that etcd is the single source of truth, and the API server is the single point of access. No component talks directly to etcd except the API server. Every other component communicates exclusively through the API server's REST interface. This design provides:

  1. Uniform access control — authentication, authorization, and admission apply to all mutations
  2. Watch-based consistency — components use the watch API to stay synchronized
  3. Loose coupling — components can be restarted independently

The flow from kubectl apply to running containers traverses every major component:

  1. kubectl serializes the resource and sends a POST/PUT to the API server
  2. kube-apiserver authenticates, authorizes, runs admission, persists to etcd
  3. kube-scheduler watches for unbound pods, scores nodes, writes a binding
  4. kubelet on the chosen node watches for pods bound to it, calls the container runtime via CRI
  5. kube-controller-manager ensures desired state matches actual state (replica counts, rolling updates, etc.)

What's Next

With this map in hand, we're ready to go deeper. In Part 2, we'll explore the API machinery type system — the foundational layer that defines how Kubernetes represents every API object in Go. Understanding Scheme, GroupVersionKind, and the hub-and-spoke versioning pattern is essential for comprehending everything that follows.