@codedependant/kubernetes-leader-election
Version:
Leader election for kubernetes using leases
239 lines (194 loc) • 7.12 kB
Markdown
## Kubernetes Leader Election
Leader election for kubernetes using leases
### Installation
```bash
npm install @codedpendant/kubernetes-leader-election -- save
```
<!-- vim-markdown-toc GFM -->
* [Exports](#exports)
* [Events](#events)
* [Elector](#elector)
* [Classes](#classes)
* [Elector(`options`)](#electoroptions)
* [Properties](#properties)
* [Methods](#methods)
* [`start`(): Promise<void>](#start-promiseltvoidgt)
* [`stop`(): Promise<void>](#stop-promiseltvoidgt)
* [Usage](#usage)
* [Kubernetes RBAC](#kubernetes-rbac)
* [Service Account](#service-account)
* [Role](#role)
* [RoleBinding](#rolebinding)
* [Example Deployment](#example-deployment)
* [Authors](#authors)
<!-- vim-markdown-toc -->
## Exports
### Events
* ([Object][]) - Named events that are emitted from elector instances
* ([String][]) LEADERSHIP_ACQUIRED Event emitted when an elector claims ownership of a lease
* ([String][]) LEADERSHIP_LOST - Event emitted when an elector recognizes a different elector has claimed ownership
* ([String][]) LEASE_RENEWED - Event emitted when the currently leader renews its lease resource
### [Elector](#electoroptions)
* ([Class][]) Elector - Elector instances configured with the same lease name will form a coordination group in which
one, and only one elector instance can be considered the leader
## Classes
**Extends**: [EventEmitter][]
### Elector(`options`)
**Arguments**
* (optional) `options` ([Object][]): Configuration options for the elector
* (optional) `auto_close` [Boolean][]: If true, the elector will try to stop itself when the `SIGINT` and `SIGTERM` signals are sent to the process
* default: **true**
* (optional) `identity` [String][]: A **unique** name for the elector used to claim leadership. THe name of the kubernetes pod is generally safe.
* default: **elector-{{hostname}}-{{process-id}}**
* (optional) `lease_name` [String][]: The name of the kubernetes lease the electors will manage.
* default: `nodejs-leader-election`
* (optional) `log` [Object][]: A logger instance that implements the [abstract-logging][] interface. [pino][] is recommended.
* (optional) `namespace` [String][]: The namespace the elector should be constrained to
* default: **default**
* (optional) `renew_interval_ms` [Number][]: The frequency at which the leader will attempt to renew its lease
* default: **10000**
* (optional) `lease_duration_sec` [Number][]: The amount of time assigned to the active lease. After which it is considered expired and the election process will start again
* default: **20** (Calculated as (2 * renew_interval_ms) / 1000)
* (optional) `wait_for_leadership` [Boolean][]: If `true`, when the start() method is called, the elector will wait for a leader to be elected.
* default: **false**
#### Properties
* current_leader [Boolean][] - Indicates if the elector instance is currently the leader
* lease_name [String][] - The name of the kubernetes lease resources that is being managed
* identity [String][] - The unique name of the elector instance
* namespace [String][] - the kubernetes namespaces the elector is currently bound to
#### Methods
##### `start`(): [Promise][]<void>
##### `stop`(): [Promise][]<void>
## Usage
The elector instance depends entirely on the kubernetes API. As such, if it cannot determine
that it is running in a kubernetes environment, It will not attempt to interact with the
api and it will assume leadership. Generally useful for development when not running in kubernetes.
When created, the elector will check for the environment variable `KUBERNETES_SERVICE_HOST` to
make this determination
```javascript
const {Elector, EVENTS} = require('@codedependant/kubernetes-leader-election')
const elector = new Elector({
lease_name: 'my-application-lease'
, auto_close: false
, identity: process.env.POD_NAME || `my-application-${process.pid}`
})
elector.on(EVENTS.LEADERSHIP_ACQUIRED, (evt) => {
// handle becoming the leader
importantFunction(evt)
})
elector.on(EVENTS.LEADERSHIP_LOST, () => {
// Handle not being the leader
importantFunction(evt)
})
process.on('SIGINT', onSignal)
process.on('SIGTERM', onSignal)
async importantFunction(...args) {
if (!elector.current_leader) {
console.log(`I'm not the leader. Nothing to do`)
return
}
// Do something important that only the leader can do
console.log('Important work has been done')
}
function onSignal(signal) {
elector.log.info('received signal %s', signal)
elector.stop()
}
elector.start()
```
### Kubernetes RBAC
To allow your application to manage leases, set up the appropriate RBAC configuration in Kubernetes.
This involves creating a ServiceAccount, Role, and a RoleBinding or ClusterRoleBinding
to grant the necessary permissions.
Below is an example of the minimal setup required
#### Service Account
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: kubernetes-election
namespace: default
```
#### Role
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: lease-manager
namespace: default
rules:
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
```
#### RoleBinding
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: lease-manager-binding
namespace: default
subjects:
- kind: ServiceAccount
name: kubernetes-election
namespace: default
roleRef:
kind: Role
name: lease-manager
apiGroup: rbac.authorization.k8s.io
```
#### Example Deployment
Once this is in place, any deployment or pod that makes use of this package should leverage
the previously defined service account. It is highly recommended to make use of the pod name
as the elector instance's identity
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: elector
labels:
app: elector
spec:
selector:
matchLabels:
app: elector
template:
metadata:
labels:
app: elector
spec:
serviceAccountName: kubernetes-election
containers:
- name: application
image: your-node-application:latest
command:
- node
args:
- 'index.js'
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
```
## Authors
* [**Eric Satterwhite**](mailto:esatterwhite@wi.rr.com) <esatterwhite@wi.rr.com>
[String]: https://mdn.io/string
[Object]: https://mdn.io/object
[Class]: https://mdn.io/classes
[Promise]: https://mdn.io/promise
[Boolean]: https://mdn.io/boolean
[Number]: https://mdn.io/number
[abstract-logging]: https://www.npmjs.com/package/abstract-logging
[pino]: https://www.npmjs.com/package/pino
[EventEmitter]: https://nodejs.org/api/events.html#class-eventemitter