Skip to content
This repository has been archived by the owner on Jul 29, 2020. It is now read-only.

Commit

Permalink
repo: Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
joa committed Nov 28, 2018
0 parents commit bc04bcf
Show file tree
Hide file tree
Showing 8 changed files with 609 additions and 0 deletions.
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM golang:alpine as builder

ENV GO111MODULE on
RUN apk update && apk add git && apk add ca-certificates
RUN adduser -D -g '' unprivileged
COPY . $GOPATH/src/github.com/joa/jawlb/
WORKDIR $GOPATH/src/github.com/joa/jawlb/
RUN go get -d -v
RUN go generate main.go
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/app

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /go/bin/app /go/bin/app
USER unprivileged
EXPOSE 8000
ENTRYPOINT ["/go/bin/app"]
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
### Jawlb
Jawlb (pronounced jolp) is an unsophisticated grpclb implementation for things running in Kubernetes talking to gRPC
services within that same Kubernetes cluster.

This load balancer performs service discovery via the Kubernetes API and announces any changes it sees
via the grpclb protocol to its clients.

### Building
You'll want to push the result of `docker build -f Dockerfile .` into your registry.

### grpclb
The grpclb Go implementation (others maybe too) requires you to define a SRV record for the loadbalancer.
The easy solution is to define a Kubernetes service with a port named `grpclb` and you're all set.

### Example
The example shows a load balancer setup that'll forward requests to `myservice:grpc`.
We assume that RBAC isn't required.

#### Load Balancer Deployment
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myservice-lb
spec:
selector:
matchLabels:
app: jawlb
service: myservice
replicas: 1
template:
metadata:
labels:
app: jawlb
service: myservice
spec:
restartPolicy: Always
containers:
- image: "your.regist.ry/jawlb:yolo"
name: jawlb
ports:
- containerPort: 8000
name: grpclb
env:
# The name of the upstream service we want
# to balance
- name: JAWLB_SERVICE
value: "myservice"
# The name of the port exposed by this service
# which we want to forward to
- name: JAWLB_TARGETPORT
value: "grpc"
# The namespace in which jawlb performs the lookup
# and we use the current one simply
- name: JAWLB_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
```
#### Load Balancer Service
```yaml
apiVersion: v1
kind: Service
metadata:
name: myservice-lb
labels:
app: jawlb
service: myservice
spec:
# Make this a headless service because we'll have to use
# the dns:// resolver in the Go client
clusterIP: None
ports:
# The port MUST be named grpclb in order to create
# the proper DNS SRV entry
- name: grpclb
port: 8000
targetPort: grpclb
selector:
app: jawlb
service: myservice
```
#### gRPC Client
```go
import _ "google.golang.org/grpc/balancer/grpclb"

// When dialing, gRPC's DNS resolver will issue a SRV lookup and
// because we're so nice to provide the grpclb entry, everything
// works as expected

conn, err := grpc.Dial(
"dns:///myservice-lb:8000", // must use the dns resolver and port exposed at the balancer
grpc.WithBalancerName("grpclb"), // select the grpclb balancer strat
grpc.WithInsecure())

// ... magic 🧙‍♀️
```

### Configuration
Everything is passed via environment variables.

- `JAWLB_NAMESPACE` in which namespace service lookup is performed, default `"default"`
- `JAWLB_SERVICE` the name of the Kubernetes service to balance, required
- `JAWLB_TARGETPORT` the name(!) of the target port on that service, default `"grpc"`
- `JAWLB_LABELSELECTOR` an additional label selector, default `""`
- `JAWLB_HOST` the hostname to listen on, default`""`
- `JAWLB_PORT` port to listen on, default `8000`
- `JAWLB_SHUTDOWNGRACEPERIOD` Grace period for open connections during shutdown, default `"25s"`

### What's missing
- Potentially some actual load balancing
- Implementation of [LoadReporter](https://github.com/grpc/grpc/blob/master/src/proto/grpc/lb/v1/load_reporter.proto) service
- Readiness Probe
- Health Check

### Resources
- [grpclb spec](https://github.com/grpc/grpc/tree/master/src/proto/grpc/lb/v1)
- [Load Balancing in gRPC](https://github.com/grpc/grpc/blob/master/doc/load-balancing.md)
63 changes: 63 additions & 0 deletions broadcast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"context"
)

type Listener chan<- ServerList

type broadcast struct {
ctx context.Context
src <-chan ServerList
state ServerList
tgts map[Listener]bool
add chan Listener
rem chan Listener
}

func newBroadcast(ctx context.Context, src <-chan ServerList) *broadcast {
b := &broadcast{
ctx: ctx,
src: src,
tgts: make(map[Listener]bool),
add: make(chan Listener),
rem: make(chan Listener),
}

go b.run()

return b
}

func (b *broadcast) addListener(listener Listener) {
b.add <- listener

// send initial state if present once on registration
if len(b.state) > 0 {
go func() { listener <- b.state }()
}
}

func (b *broadcast) remListener(listener Listener) {
b.rem <- listener
}

func (b *broadcast) run() {
for {
select {
case <-b.ctx.Done():
close(b.add)
close(b.rem)
return
case l := <-b.add:
b.tgts[l] = true
case l := <-b.rem:
delete(b.tgts, l)
case state := <-b.src:
b.state = state
for tgt := range b.tgts {
go func() { tgt <- state }()
}
}
}
}
26 changes: 26 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module github.com/joa/jawlb

require (
github.com/gogo/protobuf v1.1.1 // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/json-iterator/go v1.1.5 // indirect
github.com/kelseyhightower/envconfig v1.3.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/spf13/pflag v1.0.3 // indirect
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/grpc v1.16.0
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
k8s.io/api v0.0.0-20181121071145-b7bd5f2d334c
k8s.io/apimachinery v0.0.0-20181126191516-4a9a8137c0a1
k8s.io/client-go v9.0.0+incompatible
k8s.io/klog v0.1.0 // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
)
69 changes: 69 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM=
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20181121071145-b7bd5f2d334c h1:aSW17ws1n3Y/gxcAggEFSs+UJlzpE3+stTPLQSiVEno=
k8s.io/api v0.0.0-20181121071145-b7bd5f2d334c/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apimachinery v0.0.0-20181126191516-4a9a8137c0a1 h1:u/v3rSGNjiTxclqUNHYgSrCIotyczPebwV1FPXtdKRQ=
k8s.io/apimachinery v0.0.0-20181126191516-4a9a8137c0a1/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v9.0.0+incompatible h1:2kqW3X2xQ9SbFvWZjGEHBLlWc1LG9JIJNXWkuqwdZ3A=
k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk=
k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
60 changes: 60 additions & 0 deletions lb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
grpclb "google.golang.org/grpc/balancer/grpclb/grpc_lb_v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type lb struct {
b *broadcast
}

func (l *lb) BalanceLoad(req grpclb.LoadBalancer_BalanceLoadServer) error {
if in, err := req.Recv(); err != nil {
return err
} else if init := in.GetInitialRequest(); init != nil {
err := req.Send(&grpclb.LoadBalanceResponse{
LoadBalanceResponseType: &grpclb.LoadBalanceResponse_InitialResponse{
InitialResponse: &grpclb.InitialLoadBalanceResponse{},
},
})

if err != nil {
return err
}
} else {
return status.Error(codes.InvalidArgument, "expected initial request")
}

ch := make(chan ServerList)
defer close(ch)

l.b.addListener(ch)
defer l.b.remListener(ch)

for {
select {
case <-req.Context().Done():
return nil
case msg := <-ch:
var servers []*grpclb.Server

for _, server := range msg {
servers = append(servers, &grpclb.Server{IpAddress: server.IP, Port: server.Port})
}

err := req.Send(&grpclb.LoadBalanceResponse{
LoadBalanceResponseType: &grpclb.LoadBalanceResponse_ServerList{
ServerList: &grpclb.ServerList{
Servers: servers,
},
},
})

if err != nil {
return err
}
}
}
}
Loading

0 comments on commit bc04bcf

Please sign in to comment.