SEANK.H.LIAO

k8s leaderelection

follow the leader

leader election

So you want to run a high availability service, but your code can only really handle having a single instance do the critical work. What do you do? You run multiple replicas, choosing one to be the leader to do the critical sections.

Now you could implement raft, paxos, or something else yourself, but if you're running in kubernetes, there's a ready made Lease resource, and leaderelection libraries to use.

manifests

We need a service account to attach permissions to

1apiVersion: v1
2kind: ServiceAccount
3metadata:
4  name: test-leader

The leaderelection library will create and update the Leases on demand

 1apiVersion: rbac.authorization.k8s.io/v1
 2kind: Role
 3metadata:
 4  name: test-leader
 5rules:
 6  - apiGroups:
 7      - coordination.k8s.io
 8    resources:
 9      - leases
10    verbs:
11      - "*"
12---
13apiVersion: rbac.authorization.k8s.io/v1
14kind: RoleBinding
15metadata:
16  name: test-leader
17roleRef:
18  apiGroup: rbac.authorization.k8s.io
19  kind: Role
20  name: test-leader
21subjects:
22  - kind: ServiceAccount
23    name: test-leader

We need to run our pods. 3 replicas so we actually see the effects of leader election. We inject POD_NAME as a unique identity

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: test-leader
 5spec:
 6  replicas: 3
 7  selector:
 8    matchLabels:
 9      app.kubernetes.io/name: test-leader
10  template:
11    metadata:
12      labels:
13        app.kubernetes.io/name: test-leader
14    spec:
15      serviceAccountName: test-leader
16      containers:
17        - name: leader
18          image: go.seankhliao.com/test-leaderelection/cmd/leader

code

Now for actual code:

 1package main
 2
 3import (
 4        "context"
 5        "os"
 6        "time"
 7
 8        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 9        "k8s.io/client-go/kubernetes"
10        "k8s.io/client-go/rest"
11        "k8s.io/client-go/tools/leaderelection"
12        "k8s.io/client-go/tools/leaderelection/resourcelock"
13        "k8s.io/klog/v2"
14)
15
16func main() {
17        // setup client to talk to api server using service account credentials
18        restConfig, err := rest.InClusterConfig()
19        if err != nil {
20                klog.ErrorS(err, "get rest config")
21                os.Exit(1)
22        }
23        client, err := kubernetes.NewForConfig(restConfig)
24        if err != nil {
25                klog.ErrorS(err, "get rest client")
26                os.Exit(1)
27        }
28
29        // get a unique identity for ourselves
30        hostname, err := os.Hostname()
31        if err != nil {
32                klog.ErrorS(err, "get hostname")
33                os.Exit(1)
34        }
35
36        ctx := context.Background()
37        ctx, cancel := context.WithCancel(ctx)
38
39
40        // runs in a leader election loop
41        // panics on failure
42        leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
43        // this is the lease we create/update if we win the leader
44                Lock:            &resourcelock.LeaseLock{
45                LeaseMeta: metav1.ObjectMeta{
46                        Namespace: "default",
47                        Name:      "test-lease",
48                },
49                Client: client.CoordinationV1(),
50                LockConfig: resourcelock.ResourceLockConfig{
51                        Identity: hostname,
52                },
53        },
54                // recommended defaults
55                LeaseDuration:   15 * time.Second,
56                RenewDeadline:   10 * time.Second,
57                RetryPeriod:     2 * time.Second,
58                // TODO, ensure exit from critical work before canceling context
59                ReleaseOnCancel: true,
60                Callbacks: leaderelection.LeaderCallbacks{
61                        // main work should happen here
62                        OnStartedLeading: func(ctx context.Context) {
63                                dur := 5 * time.Second
64                                klog.InfoS("leading", "tick_dur", dur)
65                                tick := time.NewTicker(dur)
66                                defer tick.Stop()
67                        leadLoop:
68                                for {
69                                        select {
70                                        case <-tick.C:
71                                                klog.InfoS("still leading")
72                                        case <-ctx.Done():
73                                                klog.InfoS("leading cancelled")
74                                                break leadLoop
75                                        }
76                                }
77                        },
78                        OnStoppedLeading: func() {
79                        // TODO: ensure work loop exit before canceling leaderelection ctx
80                                cancel()
81                                klog.InfoS("stopped leading")
82                        },
83                        OnNewLeader: func(identity string) {
84                        // just notifications
85                                klog.InfoS("new leader", "id", identity)
86                        },
87                },
88        })
89}

deploy

And a simple skaffold config to deploy into a kind cluster

 1apiVersion: skaffold/v4beta1
 2kind: Config
 3metadata:
 4  name: test-leader
 5build:
 6  artifacts:
 7    - image: go.seankhliao.com/test-leaderelection/cmd/leader
 8      ko:
 9        main: ./cmd/leader
10  local:
11    push: false
12  tagPolicy:
13    sha256: {}
14manifests:
15  rawYaml:
16    - deploy/manifests/*
17deploy:
18  kubectl: {}
19  kubeContext: kind-kind
20  logs:
21    prefix: podAndContainer

run logs

 1$ skaffold run --tail
 2Generating tags...
 3 - go.seankhliao.com/test-leaderelection/cmd/leader -> go.seankhliao.com/test-leaderelection/cmd/leader:latest
 4Checking cache...
 5 - go.seankhliao.com/test-leaderelection/cmd/leader: Not found. Building
 6Starting build...
 7Found [kind-kind] context, using local docker daemon.
 8Building [go.seankhliao.com/test-leaderelection/cmd/leader]...
 9Target platforms: [linux/amd64]
10Using base gcr.io/distroless/static:nonroot@sha256:bce851161342b0c9d19d0d56e3d37b4787cc6b53a164ec21432e0e1755d08e17 for go.seankhliao.com/test-leaderelection/cmd/leader
11Using build config go.seankhliao.com/test-leaderelection/cmd/leader for go.seankhliao.com/test-leaderelection/cmd/leader
12Building go.seankhliao.com/test-leaderelection/cmd/leader for linux/amd64
13Loading go.seankhliao.com/test-leaderelection/cmd/leader:266dddc70cc019db6657b05dd884b16d59882f98947c1e2833dc5274cca5ecac
14Loaded go.seankhliao.com/test-leaderelection/cmd/leader:266dddc70cc019db6657b05dd884b16d59882f98947c1e2833dc5274cca5ecac
15Adding tag latest
16Added tag latest
17Build [go.seankhliao.com/test-leaderelection/cmd/leader] succeeded
18Starting test...
19Tags used in deployment:
20 - go.seankhliao.com/test-leaderelection/cmd/leader -> go.seankhliao.com/test-leaderelection/cmd/leader:5963bd657b4fabc4e4dcb69b57ef08569e25a3b8df132c8c67339801cac25d9c
21Starting deploy...
22Loading images into kind cluster nodes...
23 - go.seankhliao.com/test-leaderelection/cmd/leader:5963bd657b4fabc4e4dcb69b57ef08569e25a3b8df132c8c67339801cac25d9c -> Loaded
24Images loaded in 1.455 second
25 - deployment.apps/test-leader configured
26 - role.rbac.authorization.k8s.io/test-leader configured
27 - rolebinding.rbac.authorization.k8s.io/test-leader unchanged
28 - serviceaccount/test-leader unchanged
29Waiting for deployments to stabilize...
30 - deployment/test-leader is ready.
31Deployments stabilized in 3.068 seconds
32Press Ctrl+C to exit
33[test-leader-6d7766bbcc-8sddv leader] I0107 12:50:38.714950       1 leaderelection.go:248] attempting to acquire leader lease default/test-lease...
34[test-leader-6d7766bbcc-8sddv leader] I0107 12:50:38.718334       1 main.go:76] "new leader" id="test-leader-7d76b8f7df-7m2x8"
35[test-leader-6d7766bbcc-pdp5j leader] I0107 12:50:37.666586       1 leaderelection.go:248] attempting to acquire leader lease default/test-lease...
36[test-leader-6d7766bbcc-pdp5j leader] I0107 12:50:37.671308       1 main.go:76] "new leader" id="test-leader-7d76b8f7df-7m2x8"
37[test-leader-6d7766bbcc-r7qq8 leader] I0107 12:50:36.889879       1 leaderelection.go:248] attempting to acquire leader lease default/test-lease...
38[test-leader-6d7766bbcc-r7qq8 leader] I0107 12:50:36.894878       1 main.go:76] "new leader" id="test-leader-7d76b8f7df-7m2x8"
39[test-leader-6d7766bbcc-pdp5j leader] I0107 12:50:52.958867       1 leaderelection.go:258] successfully acquired lease default/test-lease
40[test-leader-6d7766bbcc-pdp5j leader] I0107 12:50:52.958957       1 main.go:76] "new leader" id="test-leader-6d7766bbcc-pdp5j"
41[test-leader-6d7766bbcc-pdp5j leader] I0107 12:50:52.958985       1 main.go:57] "leading" tick_dur="5s"
42[test-leader-6d7766bbcc-8sddv leader] I0107 12:50:54.270478       1 main.go:76] "new leader" id="test-leader-6d7766bbcc-pdp5j"
43[test-leader-6d7766bbcc-r7qq8 leader] I0107 12:50:54.749086       1 main.go:76] "new leader" id="test-leader-6d7766bbcc-pdp5j"
44[test-leader-6d7766bbcc-pdp5j leader] I0107 12:50:57.959756       1 main.go:64] "still leading"
45[test-leader-6d7766bbcc-pdp5j leader] I0107 12:51:02.960722       1 main.go:64] "still leading"
46[test-leader-6d7766bbcc-pdp5j leader] I0107 12:51:07.959616       1 main.go:64] "still leading"
47[test-leader-6d7766bbcc-pdp5j leader] I0107 12:51:12.959551       1 main.go:64] "still leading"
48[test-leader-6d7766bbcc-pdp5j leader] I0107 12:51:17.959866       1 main.go:64] "still leading"