Hobby project with microservices
Jul 18, 2018 · 5 minute read · CommentsThis post was inspired by awesome series of tutorials from Ewan Valentine. He built a small microservice oriented web application to select an appropriate ship for every consignment. Technology - Golang, PostgreSQL, MongoDB, gRPC, go-micro framework, React, Kubernetes. I followed through these tutorials. But made some additional “changes”. You can find them below.
General Overview:
-
Consignment Service - “main” service, connected to
user
(authentication) andvessel
(availabe vessels) services. Responsible for creation and displaying consignments. MongoDB is used. -
UI Service - simple React app. It is connected to
user
(authentication) andconsignment
(consignment managment) services. -
User Service - authentication and autorization. It uses PostgreSQL.
-
Vessel Service - serving creation and finding available vesselss. MongoDB is used.
-
Infrastructure. Tested locally on minikube.
- db-chart - Helm chart for creating MongoDB and PostgreSQL clusters
- monitoring-chart - Helm chart for creating Prometheus and Grafana
- ingress - Ingress creating for UI and monitoring. Use as:
minikube start minikube addons enable ingress ... helm upgrade ... echo "$(minikube ip) test" | sudo tee -a /etc/hosts
Notes:
- Originally the author uses a lot of advantages go-micro framework. It is definitely an interesting topic but a little bit out of my scope. It encapsulates the complexity of gRPC ecosystem but I try to do “by hand” as much work as possible. Also, I have to change Ewan’s source code instead of copying it, that is certainly good for education purpose. One of this framework substitution is implementing grpc-gateway - generates a reverse-proxy server which translates a RESTful JSON API into gRPC. Also, I added a simple swagger output.
httpMux.HandleFunc("/swagger/", func(w http.ResponseWriter, r *http.Request) {
dir := "./proto/auth"
if !strings.HasSuffix(r.URL.Path, ".swagger.json") {
log.Printf("Swagger Not Found: %s", r.URL.Path)
http.NotFound(w, r)
return
}
log.Printf("Serving Swagger %s", r.URL.Path)
p := strings.TrimPrefix(r.URL.Path, "/swagger/")
p = path.Join(dir, p)
fmt.Println(p)
http.ServeFile(w, r, p)
})
- I got my hands dirty with CORS. Testing UI on the local machine requires specifying two different IP (one for user service and one consignment service). Of course, during CORS it won’t work. The browser doesn’t allow using different addresses. Luckily, grpc-gateway repository has good documentation that helps to fix it.
// allowCORS allows Cross Origin Resoruce Sharing from any origin.
// Don't do this without consideration in production systems.
func allowCORS(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" {
preflightHandler(w, r)
return
}
}
h.ServeHTTP(w, r)
})
}
func preflightHandler(w http.ResponseWriter, r *http.Request) {
headers := []string{"Content-Type", "Accept", "Authorization"}
w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"}
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
log.Printf("CORS preflight request for %s \n", r.URL.Path)
}
- Added a simple health check endpoint for Kubernetes.
httpMux := http.NewServeMux()
httpMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
if err := db.DB().Ping(); err != nil {
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
- The general goal of this project is education, so I try to figure out how to work with Consul and implement simple service discovery. Of course, this project runs on Kubernetes and Consul’s service discovery is not necessary. But anyway I implemented registering and retrieving service attributes from Consul.
//Register in Consul
defer func() {
cErr := consul.Agent().ServiceDeregister(serviceID)
if cErr != nil {
log.Println("Can't remove service from Consul ", cErr)
return
}
log.Println("Remove from Consul ", serviceID)
}()
err = consul.Agent().ServiceRegister(&consulapi.AgentServiceRegistration{
ID: serviceID,
Name: "user-service",
Port: 50054,
Address: "host",
Check: &consulapi.AgentServiceCheck{
CheckID: "health_check",
Name: "User-Service health status",
Interval: "10s",
GRPC: "host:50054",
},
})
if err != nil {
log.Println("Couldn't add service to Consul, ", err)
}
log.Println("Add to Consul, ", serviceID)
//Get IP
ip, err := net.InterfaceAddrs()
if err != nil {
log.Println("Couldn't get IP address")
}
for _, a := range ip {
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
fmt.Println("MY IP: ", ipnet.IP)
}
}
}
//Get User-service from Consul
health, _, err := consul.Health().Service("user-service", "", true, nil)
if err != nil {
log.Println("Cant get live services")
}
fmt.Println("HEALTH: ", len(health))
for _, item := range health {
log.Println("Service: ", item.Service.ID, item.Service.Address, item.Service.Port)
}
- Moreover, I found out that gRPC has in-built support of Consul health checks, but implementation is a little bit weird, the package name has to be
grpc_health_v1
, otherwise consul could’t use it.
// Check implements Consul health checking
func (srv *service) Check(ctx context.Context, in *pb.HealthCheckRequest) (*pb.HealthCheckResponse, error) {
if err := srv.repo.ping(); err != nil {
return &pb.HealthCheckResponse{
Status: pb.HealthCheckResponse_NOT_SERVING,
}, err
}
return &pb.HealthCheckResponse{Status: pb.HealthCheckResponse_SERVING}, nil
}
-
Added support of go-grpc-prometheus - allow you to monitor your gRPC methods. It requires adding Prometheus and Grafana to the cluster.
-
Other changes were pretty small. I changed Docker build to a fancier one, and
dep
package management, adding Helm support for more convenient deploy to cluster, logging branch name during build etc. -
In the end, I couldn’t find out “straight” way of autoincrement
consignment id
. It could be generated on the consignment service side but might lead to collisions in case of multiple instances, that is highly possible for microservices architecture. Instead of it, after every insert, theid
field is updated. It doesn’t look good and if you know a better way I’ll be grateful if you let me know.
func (repo *ConsignmentRepository) Create(consignment *pb.Consignment) error {
repo.collection().Insert(consignment)
cons := []test{}
repo.collection().Find(bson.M{"id": ""}).All(&cons)
for i, v := range cons {
fmt.Println(i, v.ID.Hex())
consignment.Id = v.ID.Hex()
repo.collection().Update(bson.M{"id": ""}, bson.M{"$set": bson.M{"id": v.ID.Hex()}})
}
return nil
}