Share
Embed
Implementing OS in Go
Implementing operating system in Go - what it means, toolchain, builds, fun facts, some developer notes.
Andrey Smirnov
March 23, 2019
Transcript
89 slides
-
Как написать операционную систему на Go Андрей Смирнов Implementing OS
in Go (Andrey Smirnov) -
Why another OS for Kubernetes? Linux distribution Kubernetes We want
this We have to support this -
Configuration drift Linux distribution Kubernetes Linux distribution Kubernetes Linux distribution
Kubernetes Linux distribution Kubernetes -
Configuration drift Linux distribution Kubernetes Linux distribution Kubernetes Linux distribution
Kubernetes Linux distribution Kubernetes -
Kubernetes in the cloud Linux distribution Kubernetes Linux distribution Kubernetes
Linux distribution Kubernetes Linux distribution Kubernetes -
Kubernetes in the cloud Linux distribution Kubernetes Linux distribution Kubernetes
Linux distribution Kubernetes Linux distribution Kubernetes -
Why another OS for Kubernetes? Package management issues, configuration drift
across nodes Inconsistent Kubernetes experience across clouds and bare-metal Security/compliance issues due to extra bloat in the OS Requires automation tools to manage the cluster Hard to keep up with Kubernetes release schedule -
What is Talos? Open-source modern OS designed for Kubernetes (only)
Linux kernel + user-space in Go Talos Kubernetes Linux kernel User-space (Go) -
Talos goals Minimal: no bloat Fast: fast to boot Hardened:
security best practices applied by default Secure: PKI infrastructure for auth Immutable: read-only rootfs -
Talos Architecture Linux kernel init machined services containerd ntpd networkd
udevd osd trustd proxyd kubelet kubeadm etcd kube-apiserver customer workloads initramfs rootfs/ squashfs KSPP mounts userdata Talos K8s -
Talos Architecture Linux kernel init machined services initramfs rootfs/ squashfs
KSPP mounts userdata Talos K8s -
Talos Architecture Linux kernel init machined services containerd ntpd networkd
udevd osd trustd proxyd initramfs rootfs/ squashfs KSPP mounts userdata Talos K8s -
Talos Architecture Linux kernel init machined services containerd ntpd networkd
udevd osd trustd proxyd kubelet kubeadm initramfs rootfs/ squashfs KSPP mounts userdata Talos K8s -
Talos Architecture Linux kernel init machined services containerd ntpd networkd
udevd osd trustd proxyd kubelet kubeadm etcd kube-apiserver customer workloads initramfs rootfs/ squashfs KSPP mounts userdata Talos K8s -
Why doing OS in Go is so cool? Easy to
reason about the code -
Why doing OS in Go is so cool? Easy to
reason about the code Static linking -
Why doing OS in Go is so cool? Easy to
reason about the code Static linking Great concurrency primitives -
Why doing OS in Go is so cool? Easy to
reason about the code Static linking Great concurrency primitives Wrappers for low-level OS primitives (e.g. netlink) -
Why doing OS in Go is so cool? Easy to
reason about the code Static linking Great concurrency primitives Wrappers for low-level OS primitives (e.g. netlink) Easy integration with other services in Go -
Service Startup containerd networkd kubeadm kubelet ntpd kubeadm-flags.env osd udevd
proxyd ca.crt trustd -
Service A way to run the service (Runner) Service state,
health Service logs Service dependencies Service control (stop, start, restart) Service pre-run hooks (prepare) -
Service Runner Process Container via containerd API (oci) Container via
CRI Goroutine Automatic restarts -
Runner API - v.0 type Runner interface { Run() error
} Run() returns when runnable object terminates -
Runner API - v.1 type Runner interface { Run(ctx context.Context)
error } type Runner interface { Run() error Stop() } Run() returns when runnable object terminates -
Runner API v.2 type EventSink func(event Event) type Runner interface
{ Run(eventSink EventSink) error Stop() } [Waiting]: Waiting for service "containerd" to be "up", service "kubeadm" to be "up", file "/var/lib/kubelet/kubeadm-flags.env" to exist (1m28s ago) [Waiting]: Waiting for service "kubeadm" to be "up", file "/var/lib/kubelet/kubeadm-flags.env" to exist (1m26s ago) [Waiting]: Waiting for file "/var/lib/kubelet/kubeadm-flags.env" to exist (52s ago) [Preparing]: Running pre state (36s ago) [Preparing]: Creating service runner (36s ago) [Running]: Started task kubelet (PID 551) for container kubelet (36s ago) [Running]: Health check failed: Get http://127.0.0.1:10248/healthz: dial tcp 127.0.0.1:10248: connect: connection refused (34s ago) [Running]: Health check successful (29s ago) -
Auto-restarting runner type Restarter struct { wrapped Runner } func
(rr *Restarter) Run(eventSink EventSink) error { for { err := rr.wrapped.Run(eventSink) eventSink(Event(“restarting due to %v”, err)) time.Sleep(time.Second) } } -
Auto-restarting runner type Restarter struct { wrapped Runner } func
(rr *Restarter) Run(eventSink EventSink) error { for { err := rr.wrapped.Run(eventSink) eventSink(Event(“restarting due to %v”, err)) time.Sleep(time.Second) } } -
Auto-restarting runner type Restarter struct { wrapped Runner } func
(rr *Restarter) Run(eventSink EventSink) error { for { err := rr.wrapped.Run(eventSink) eventSink(Event(“restarting due to %v”, err)) time.Sleep(time.Second) } } -
Auto-restarting runner type Restarter struct { wrapped Runner } func
(rr *Restarter) Run(eventSink EventSink) error { for { err := rr.wrapped.Run(eventSink) eventSink(Event(“restarting due to %v”, err)) time.Sleep(time.Second) } } -
Auto-restarting runner: with stop type Restarter struct { wrapped Runner
stop chan struct{} } func (rr *Restarter) Stop() { close(rr.stop) } -
Auto-restarting runner: with stop type Restarter struct { wrapped Runner
stop chan struct{} } func (rr *Restarter) Stop() { close(rr.stop) } -
Auto-restarting runner: with stop for { errCh := make(chan error)
go func() { errCh <- rr.wrapped.Run(eventSink) } select { case <-rr.stop: rr.wrapped.Stop() return <-errCh case err := <- errCh: eventSink(Event(“restarting due to %v”, err)) } time.Sleep(time.Second) } -
Auto-restarting runner: with stop for { errCh := make(chan error)
go func() { errCh <- rr.wrapped.Run(eventSink) } select { case <-rr.stop: rr.wrapped.Stop() return <-errCh case err := <- errCh: eventSink(Event(“restarting due to %v”, err)) } time.Sleep(time.Second) } -
Auto-restarting runner: with stop for { errCh := make(chan error)
go func() { errCh <- rr.wrapped.Run(eventSink) } select { case <-rr.stop: rr.wrapped.Stop() return <-errCh case err := <- errCh: eventSink(Event(“restarting due to %v”, err)) } time.Sleep(time.Second) } -
Auto-restarting runner: with stop for { errCh := make(chan error)
go func() { errCh <- rr.wrapped.Run(eventSink) } select { case <-rr.stop: rr.wrapped.Stop() return <-errCh case err := <- errCh: eventSink(Event(“restarting due to %v”, err)) } time.Sleep(time.Second) } -
Auto-restarting runner: with stop for { errCh := make(chan error)
go func() { errCh <- rr.wrapped.Run(eventSink) } select { case <-rr.stop: rr.wrapped.Stop() return <-errCh case err := <- errCh: eventSink(Event(“restarting due to %v”, err)) } time.Sleep(time.Second) } -
Auto-restarting runner: with stop for { errCh := make(chan error)
go func() { errCh <- rr.wrapped.Run(eventSink) } select { case <-rr.stop: rr.wrapped.Stop() return <-errCh case err := <- errCh: eventSink(Event(“restarting due to %v”, err)) } time.Sleep(time.Second) } -
Service type Service interface { Id() string Runner() Runner Dependencies()
[]string PreCondition() Condition } -
Condition type Condition interface { fmt.Stringer Wait(ctx context.Context) error }
Condition represents anything which can be waited for Conditions are composable [Waiting]: Waiting for service "containerd" to be "up", service "kubeadm" to be "up", file "/var/lib/kubelet/kubeadm-flags.env" to exist (1m28s ago) [Waiting]: Waiting for service "kubeadm" to be "up", file "/var/lib/kubelet/kubeadm-flags.env" to exist (1m26s ago) [Waiting]: Waiting for file "/var/lib/kubelet/kubeadm-flags.env" to exist (52s ago) -
Condition composition func FileExists(path string) Condition func ServiceRunning(id string) Condition
func WaitForAll(conditions ...Condition) Condition -
Condition composition func FileExists(path string) Condition func ServiceRunning(id string) Condition
func WaitForAll(conditions ...Condition) Condition condition := WaitForAll(ServiceRunning("trustd"), FileExists("/var/lib/kubelet/kubeadm-flags.env")) -
Condition composition type waitAll struct{ conditions []Condition } func WaitForAll(conditions
...Condition) Condition { return &waitAll{conditions: conditions} } -
Condition composition func (w *waitAll) Wait(ctx context.Context) error { errCh
:= make(chan error) for _, cond := range w.conditions { go func(cond Condition) { errCh <- cond.Wait(ctx) }(cond) } var multiErr multierror.Error for range w.conditions { multiErr := multierror.Append(multiErr, <-errCh) } return multiErr.ErrorOrNil() } -
Service: getting things together for _, svc := range services
{ go func(svc Service) { cnd := WaitForAll(svc.PreCondition(), ServiceRunning(svc.Dependencies()...)) cnd.Wait(ctx) svc.Runner().Run(eventSink) }(svc) } -
Service: getting things together for _, svc := range services
{ go func(svc Service) { cnd := WaitForAll(svc.PreCondition(), ServiceRunning(svc.Dependencies()...)) cnd.Wait(ctx) svc.Runner().Run(eventSink) }(svc) } -
Service: getting things together for _, svc := range services
{ go func(svc Service) { cnd := WaitForAll(svc.PreCondition(), ServiceRunning(svc.Dependencies()...)) cnd.Wait(ctx) svc.Runner().Run(eventSink) }(svc) } -
Service: getting things together for _, svc := range services
{ go func(svc Service) { cnd := WaitForAll(svc.PreCondition(), ServiceRunning(svc.Dependencies()...)) cnd.Wait(ctx) svc.Runner().Run(eventSink) }(svc) } -
Service: getting things together for _, svc := range services
{ go func(svc Service) { cnd := WaitForAll(svc.PreCondition(), ServiceRunning(svc.Dependencies()...)) cnd.Wait(ctx) svc.Runner().Run(eventSink) }(svc) } -
Building OS in 3 minutes Parallel builds (stages, go parallelization
over packages) Efficient caching (Go build cache, buildkitd caching) Go compiler is blazingly fast! -
Build Graph toolchain kernel tools Go sources init container images
initramfs rootfs OS -
Build Graph toolchain kernel tools Go sources init container images
initramfs rootfs OS -
Building with buildkit tl;dr: better docker build Environment-agnostic reproducible builds
Efficient caching Internal parallelization Shared cache -
Slow Go modules download COPY ./ ./ RUN go mod
download RUN go mod verify RUN go build ./... -
Slow Go modules download COPY ./ ./ RUN go mod
download RUN go mod verify RUN go build ./... SLOOOOW! Any source code change invalidates build cache -
Caching Go modules download COPY ./go.mod ./ COPY ./go.sum ./
RUN go mod download RUN go mod verify -
Caching Go modules download COPY ./go.mod ./ COPY ./go.sum ./
RUN go mod download RUN go mod verify COPY ./cmd ./cmd COPY ./pkg ./pkg RUN go build ./... -
Caching Go modules download COPY ./go.mod ./ COPY ./go.sum ./
RUN go mod download RUN go mod verify COPY ./cmd ./cmd COPY ./pkg ./pkg RUN go build ./... cached until Go modules requirements change cache invalidates with source code changes -
Caching Go modules go.{mod,sum} $ go mod download $ go
mod verify ./src $ go build go.{mod,sum} $ go mod download $ go mod verify ./src $ go build -
Mounting Go build cache Mounted volume doesn’t affect build cache
and persists across builds # syntax = docker/dockerfile-upstream:1.1.2-experimental RUN --mount=type=cache,target=/root/.cache/go-build go build ./... -
Caching tools COPY ./src . RUN go build ./... RUN
curl -sfL .../golangci-lint.sh | bash -s -- -b /bin v1.16.0 RUN golangci-lint run -
Caching tools FROM base AS tools RUN curl -sfL .../golangci-lint.sh
| bash -s -- -b /bin v1.16.0 -
Caching tools FROM base AS tools RUN curl -sfL .../golangci-lint.sh
| bash -s -- -b /bin v1.16.0 FROM tools AS src COPY ./src . -
Caching tools FROM base AS tools RUN curl -sfL .../golangci-lint.sh
| bash -s -- -b /bin v1.16.0 FROM tools AS src COPY ./src . FROM src AS build RUN go build ./... FROM src AS lint RUN golangci-lint run -
Testing OS • unit-tests: ◦ algorithms, concurrent code (race detector!),
hard to hit conditions. • integration: ◦ boot Talos in local mode as set of docker containers. ◦ integration test as part of the build. • Cluster API: ◦ boot inception cluster, create more clusters on different cloud providers via CAPI. • conformance: ◦ run conformance tests against cluster. • upgrade tests: ◦ upgrade existing Talos cluster. • bare-metal tests: ◦ hardware, networking, various configurations. -
Unit-tests in buildkit ‘Unit-tests’ are not quite ‘unit’ Some tests
require containerd Should be same containerd as OS: requires rootfs Run tests in buildkit! Russian doll: buildkit - runc - test - containerd - runc - container -
Running steps in insecure mode # syntax = docker/dockerfile-upstream:1.1.2-experimental RUN
--security=insecure go test ./... Equivalent to “docker run --privileged” Contributed back to buildkit Russian doll now possible! -
Integration tests 1. Bring up Talos cluster (3 masters +
1 worker, HA control plane) as Docker containers 2. Wait for kubeadm to boot up Kubernetes cluster 3. Deploy Kubernetes addons (CNI, DNS, …) 4. Wait for all the nodes to report ‘Ready’ Overall time: ~5 mins -
Summary Talos OS - currently in beta, certified K8s installer
Go: concurrency, correctness, integration, build speed Fast: seconds to boot Minimal: OS image < 150 MB Immutable: read-only filesystem, easy upgrades Hardened & Secure: KSPP, no ssh, extra bloat, auth via certs (API) -
Thanks! GitHub: https://github.com/talos-systems/talos Project: https://www.talos-systems.com Contributions are welcome! me: @smira
Go gopher by Renee French, licensed under Creative Commons 3.0 Attributions license.