Monday, October 9, 2017

grpc for python and golang

GRPC

GRPC in Python

Install grpc for python

pip install grpcio-tools

GRPC in Python Server

Start From Proto

// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package cameramap;

// The greeting service definition.
service Cameramap {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayGoodBye (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

Directory

Before you start to generate grpc files, you must set up .proto as you wish to request and response.

├── cameramap
│   └── cameramap.proto
└── cameramap_server
    ├── 

where the directory cameramap put the proto file and we start to program server from cameramap_server.

Recompile Proto

Into directory cameramap_server.

python -m grpc_tools.protoc -I ../cameramap/ --python_out=. --grpc_python_out=. ../cameramap/cameramap.proto

It will generate cameramap_pb2_grpc.py and cameramap_pb2_grpc.py automatically.

Let's see the directory changes

├── cameramap
│   └── cameramap.proto
└── cameramap_server
    ├── cameramap_pb2_grpc.py
    ├── cameramap_pb2_grpc.pyc
    ├── cameramap_pb2.py
    ├── cameramap_pb2.pyc
    └── server.py

server.py

from concurrent import futures
import time
import math

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.name
        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.name)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    cameramap_pb2_grpc.add_CameramapServicer_to_server(CameramapServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

Run the python server

python server.py

GRPC in Python Client

In cameramap_client directory

python -m grpc_tools.protoc -I ../cameramap/ --python_out=. --grpc_python_out=. ../cameramap/cameramap.proto

Let's see the directory first

root@ubuntu:~/grpc# tree
.
├── cameramap
│   └── cameramap.proto
├── cameramap_client
│   ├── cameramap_pb2_grpc.py
│   └── cameramap_pb2.py
└── cameramap_server
    ├── cameramap_pb2_grpc.py
    ├── cameramap_pb2_grpc.pyc
    ├── cameramap_pb2.py
    ├── cameramap_pb2.pyc
    └── server.py

where client.py in directory cameramap_client

from __future__ import print_function

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)

    response = stub.SayHello(cameramap_pb2.HelloRequest(name='you'))
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

Result

root@ubuntu:~/grpc/cameramap_client# python client.py
Greeter client received: Hello, you!

GRPC in Golang

You can prepare the .proto file in directory cameramap as before.

protoc -I cameramap/ cameramap/cameramap.proto --go_out=plugins=grpc:cameramap

Directory Tree

root@golang17:~/golang/projects/wru/src/github.com/jonah/rpcmap# tree
.
├── cameramap
│   ├── cameramap.pb.go
│   └── cameramap.proto
├── cameramap_client
│   └── main.go
├── cameramap_server
│   └── main.go
└── rebuild.sh

It will generate cameramap.pb.go in directory cameramap for your used.

Server Code

package main

import (
    "log"
    "net"

    pb "github.com/jonah/rpcmap/cameramap"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    //pb "google.golang.org/grpc/examples/helloworld/helloworld"
    "fmt"
    "google.golang.org/grpc/reflection"
)

const (
    port = ":50051"
)

var newpara int

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    fmt.Println(port)
    fmt.Println(newpara)
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

/* here we put camera map location with k-nn */
func modifypara() {
    newpara = 3
}

func main() {

    modifypara()

    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    //  pb.RegisterGreeterServer(s, &server{})
    pb.RegisterCameramapServer(s, &server{})
    // Register reflection service on gRPC server.
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Client Code

package main

import (
    "log"
    "os"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    //  pb "google.golang.org/grpc/examples/helloworld/helloworld"
    pb "github.com/jonah/rpcmap/cameramap"
    //pb "helloworld/helloworld"
)

const (
    //  address     = "localhost:50051"
    address     = "192.168.51.129:50051"
    defaultName = "world"
)

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    //  c := pb.NewGreeterClient(conn)
    c := pb.NewCameramapClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}

Result

You can use one .proto file, and generate it for different programming language used.

How to use Dict in Grpc in Python

.Proto File

where .proto file

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package cameramap;

// The greeting service definition.
service Cameramap {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayGoodBye (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  //string name = 1;
  map<string, string> mapfield = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

where we can see the attribute mapfild is a map type.

Client Code

from __future__ import print_function

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)
    dictt = {'Name': 'Zara', 'Age': '7', 'Class': 'First'}
    response = stub.SayHello(cameramap_pb2.HelloRequest(mapfield=dictt))
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

We can directly send a dict to server.

Server Code

from concurrent import futures
import time
import math

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.mapfield
        print request.mapfield['Name']
        #return cameramap_pb2.HelloReply(message='Hello, %s!' % request.name)
        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.mapfield)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    cameramap_pb2_grpc.add_CameramapServicer_to_server(CameramapServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

You can directly parse request.mapfield['Name'] as dict in python.

Note:

You cannot use repeated map in GRPC, since gprc not support it.

Complex Request

We would like to send a complex request composed a list, such as followed

{"a":"b", "c":["1","2","3"], "d":"e"}

Client Code

from __future__ import print_function

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)
    dictt = {'Name': 'Zara', 'Age': '7', 'Class': 'First'}
    listt = ["1","2","3"]
    # changing list to a string
    dictt["list"] = str(listt)
    response = stub.SayHello(cameramap_pb2.HelloRequest(mapfield=dictt))
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

Use str() function forces list to a string.

Server Code

from concurrent import futures
import time
import math

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.mapfield
        print request.mapfield['Name']
        #return cameramap_pb2.HelloReply(message='Hello, %s!' % request.name)
        listt =  eval(request.mapfield['list'])
        # now it become a list
        for v in listt:
            print v
        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.mapfield)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    cameramap_pb2_grpc.add_CameramapServicer_to_server(CameramapServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

Use eval() function to force string to list.

List Requst

message HelloRequest {
  //repeated Datatest datatest = 1;
  repeated string datatest = 1;
}

Client Code

from __future__ import print_function

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)
    #dictt = {'Name': 'Zara', 'Age': '7', 'Class': 'First'}
    listt = ["1","2","3"]
    # changing list to a string
    #dictt["list"] = str(listt)
    response = stub.SayHello(cameramap_pb2.HelloRequest(datatest=listt))
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

Server Code

from concurrent import futures
import time
import math

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.datatest
        #return cameramap_pb2.HelloReply(message='Hello, %s!' % request.name)
        for v in request.datatest:
            print v
        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.datatest)



def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    cameramap_pb2_grpc.add_CameramapServicer_to_server(CameramapServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

Complex Define

In .proto

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

In Client

def guide_list_features(stub):
  rectangle = route_guide_pb2.Rectangle(
      lo=route_guide_pb2.Point(latitude=400000000, longitude=-750000000),
      hi=route_guide_pb2.Point(latitude=420000000, longitude=-730000000))
  print("Looking for features between 40, -75 and 42, -73")

More Complicate Data

It's not allow for grpc to use the folowing request or response.

[{"a":"b"}, {"c":"d"}, {"e":"f"}]

Unfortunately, we usually see the result but it is not strict. So grpc not allow above requst.

The better way is like this

{"alarms" : [{"a":"b"}, {"c":"d"}, {"e":"f"}] }

We shoud note what the request for, now it's alarms. So it's still a map but with list inside.

.Proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package cameramap;

// The greeting service definition.
service Cameramap {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayGoodBye (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.

message HelloRequest {
  map<string, string> mapfield = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

Client Code

from __future__ import print_function

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)
    data =[]
    data.append({'Name': 'Zara', 'Age': '7', 'Class': 'First'})
    data.append({'Name': 'Zara1', 'Age': '71', 'Class': 'First1'})
    dictt = {"data":str(data)}

    #a = [{'Name': 'Zara', 'Age': '7', 'Class': 'First'}, {'Name': 'Zara1', 'Age': '71', 'Class': 'First1'}]

    # changing list to a string
    #dictt["list"] = str(listt)
    response = stub.SayHello(cameramap_pb2.HelloRequest(mapfield=dictt))
    #response = stub.SayHello(bb)
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

Server Code

from concurrent import futures
import time
import math

import grpc

import cameramap_pb2
import cameramap_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.mapfield
        #return cameramap_pb2.HelloReply(message='Hello, %s!' % request.name)
        res = eval(request.mapfield["data"])
        print res
        for re in res:
            print re
            print re["Name"]
        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.mapfield)



def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    cameramap_pb2_grpc.add_CameramapServicer_to_server(CameramapServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

Repeated

Not working on map, but add() is must to understand

  channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)
    #dictt = {'Name': 'Zara', 'Age': '7', 'Class': 'First'}
    s = cameramap_pb2.Datatest(mapfield={'Name': 'Zara', 'Age': '7', 'Class': 'First'})
    b = cameramap_pb2.HelloRequest()
    bb = b.datatest.add()
    bb.mapfield={'Name': 'Zara', 'Age': '7', 'Class': 'First'}

Real Cases

We would like to request a message as followed,

{
    "api": "douban_movie",
    "domain": "movie",
    "res": [
            {
                "name": "卧虎藏龙",
                "rating": 7.8,
                "year": 2000
            }
          ],
    "time": "163ms"
}

Client Code

from __future__ import print_function

import grpc

import cameramap_pb2
import cameramap_pb2_grpc




def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)
    dictt = {'api': 'douban_movie', 'domain': 'movie', 'time': '163ms'}
    listt = [{"name":"dragon", "rating":7.8, "year":"2000"}]
    dictt["res"] = str(listt)
    response = stub.SayHello(cameramap_pb2.HelloRequest(mapfield=dictt))
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

Remember, GRPC is strictly type, so you have to trasfer the generic type to string.

Server Code

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.mapfield

        listt =  eval(request.mapfield['res'])
        # now it become a list
        print listt[0]['rating'], type(listt[0]['rating'])
        for v in listt:
            print v
        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.mapfield)

We use eval function to seperate the list, and accessing the rating to understand its type, float. Since python supports generic types.

Seperate Json By String

Proto Code

message HelloRequest {
  //string name = 1;
  map<string, string> mapfield = 1;
  repeated string res = 2;
}

Client Code

    stub = cameramap_pb2_grpc.CameramapStub(channel)
    dictt = {'api': 'douban_movie', 'domain': 'movie', 'time': '163ms'}
    listt = {"name":"dragon", "rating": 7.8, "year":"2000"}
    listts = []
    listts.append(json.dumps(listt))
    listt1 = {"name":"dragon1", "rating": 2.8, "year":"2001"}
    listts.append(json.dumps(listt1))
    response = stub.SayHello(cameramap_pb2.HelloRequest(mapfield=dictt, res=listts))

Servr Code

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.mapfield
        mapdata = request.mapfield
        print mapdata['api']
        listts =  request.res
        # now it become a list
        print listts
        for listt in listts:
            print listt, type(listt)
            d = json.loads(listt)
            print d['rating']

        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.mapfield)

Seperate Json By Multiple Definition

In Proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package cameramap;

// The greeting service definition.
service Cameramap {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayGoodBye (HelloRequest) returns (HelloReply) {}
}

message ResData {
   string name = 1;
   float rating = 2;
   string year = 3;
}

// The request message containing the user's name.
message HelloRequest {
  //string name = 1;
  map<string, string> mapfield = 1;
  repeated ResData res = 2;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

Client Code

from __future__ import print_function

import grpc

import cameramap_pb2
import cameramap_pb2_grpc
import json




def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = cameramap_pb2_grpc.CameramapStub(channel)
    dictt = {'api': 'douban_movie', 'domain': 'movie', 'time': '163ms'}

    s1 = cameramap_pb2.ResData(name ="haha", rating = 9.9, year = "2017")
    s2 = cameramap_pb2.ResData(name ="haha1", rating = 9.99, year = "2017")
    # it is allowed for the following way
    tmp={}
    tmp["name"]="haha2"
    tmp["rating"] = 9.342
    tmp["year"]="2017"

    s = []
    s.append(s1)
    s.append(s2)
    s.append(tmp)


    hq = cameramap_pb2.HelloRequest(mapfield=dictt, res=s)
    response = stub.SayHello(hq)
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

Server Code

from concurrent import futures
import time
import math

import grpc

import cameramap_pb2
import cameramap_pb2_grpc
import json

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class CameramapServicer(cameramap_pb2_grpc.CameramapServicer):
    def SayHello(self, request, context):
        print request.mapfield
        mapdata = request.mapfield
        print mapdata['api']
        listts =  request.res
        # now it become a list
        print listts
        print '***************************'
        for listt in listts:
            print listt
            print '-------------------'
        return cameramap_pb2.HelloReply(message='Hello, %s!' % request.mapfield)



def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    cameramap_pb2_grpc.add_CameramapServicer_to_server(CameramapServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

Retries Setting

    proxyOpts := []grpc.DialOption{}
    // since it's exponetially backoff, max delay time
    var DefaultBackoffConfig = grpc.BackoffConfig{
        MaxDelay: 12 * time.Second,
    }
    proxyOpts = append(proxyOpts, grpc.WithBackoffConfig(DefaultBackoffConfig))
    // reconnect time out
    proxyOpts = append(proxyOpts, grpc.WithTimeout(6000*time.Second))
    // it must have, to block the initiation before established
    proxyOpts = append(proxyOpts, grpc.WithBlock())
    proxyOpts = append(proxyOpts, grpc.WithInsecure())
    
    
    
    //conn, err := grpc.Dial(address, grpc.WithInsecure())
    //  conn, err := grpc.Dial(address, proxyOpts...)
    conn, err := grpc.Dial(address, proxyOpts...)
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }

Consul DNS Testing

ConsulDNS server in 192.168.51.129.

consul agent -dev -ui -server -node=consul-dev -client=192.168.51.129 -dns-port=53 -config-dir=/root/dnsconfig -domain=localdomain

In Other server, we need to setup nameserver.

root@ubuntu:~# cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 192.168.51.129
search localdomain

Adding Endpoints to Consul DNS Server.

root@ubuntu:~# cat dnsconfig/service.json
{

         "services": [{

         "id":"dotnetcoresample",

         "name":"dotnetcoresample",

         "tags":["dotnetcoresample"],

         "address": "192.168.51.129",

         "port": 50051

       }]

     }

However, port will not include the dns routing only IP takes effect.

In Any Server, try it.


root@ubuntu:~# ping dotnetcoresample.service.dc1.consul
PING dotnetcoresample.service.dc1.consul.localdomain (192.168.51.129) 56(84) bytes of data.
64 bytes from 192.168.51.129: icmp_seq=1 ttl=64 time=0.011 ms
64 bytes from 192.168.51.129: icmp_seq=2 ttl=64 time=0.039 ms
64 bytes from 192.168.51.129: icmp_seq=3 ttl=64 time=0.033 ms

or

root@ubuntu:~# ping dotnetcoresample.service
PING dotnetcoresample.service.localdomain (192.168.51.129) 56(84) bytes of data.
64 bytes from 192.168.51.129: icmp_seq=1 ttl=64 time=0.012 ms
64 bytes from 192.168.51.129: icmp_seq=2 ttl=64 time=0.040 ms

GRPC-LB for Aware Client

It only supports for grpc-go client, that will connect and record to etcd and consul as a metadata stored.

https://github.com/liyue201/grpc-lb

So for general purpose, such as consul-dns, might be a good solution.

No comments:

Post a Comment