API, Golang, Microservice, gRPC

How to Setup gRPC Service to Service Communication

Service to service communication is key to any microservice or externally exposed service. It is common to use JSON when you're communicating with other services. However, there is a newer, faster, more efficient solution open sourced out of Google, and that is gRPC. If you're wondering why you would use gRPC, I grabbed a snippet from the gRPC website:

The main usage scenarios:

  • Low latency, highly scalable, distributed systems.
  • Developing mobile clients which are communicating to a cloud server.
  • Designing a new protocol that needs to be accurate, efficient and language independent.
  • Layered design to enable extension eg. authentication, load balancing, * logging and monitoring etc.

Before we get started you can find all the code for this post on Github, here.

Getting Started

Now that that's out of the way let's go over what this post will cover.

  1. Create proto files
  2. Build two services (user and role)
  3. Set up gRPC communication

We're going to build two services, a user service, and a role service. The user service will communicate with the role service to retrieve the users' roles.

Let's start by making two empty folders in the project to differentiate the services, user-microservice/ and roles-microservice/.

Creating Proto files

The first step when using gRPC is to create proto files. We will build one for each service, and put them in a pb/ folder.

user.proto

The user proto file comprises one rpc, and one request and reply. The only thing we need to do is get the user, and we need to specify which user to retrieve. Requests and replies are made up of messages in the protobuf syntax.

syntax = "proto3";

package user;

service Users {
    rpc GetUser(GetUserRequest) returns(UserReply) {}
}

// Requests
message GetUserRequest {
    int32 user_id = 1;
}

// Replys
message User {
    int32 id = 1;
    string name = 2;
    string email= 3;
}

message Role {
    int32 id = 1;
    string name = 2;
}

message UserReply {
    User user = 1;
    repeated Role roles = 2;
}

role.proto

The role proto file needs a bit more work; you can retrieve all the roles, as well as get the roles for a specific user. Both need request and reply models shown below.

syntax = "proto3";

package role;

service Roles {
    rpc GetRoles(EmptyRequest) returns(RolesReply) {}
    rpc GetUserRole(GetUserRoleRequest) returns(UserRoleReply) {}
}

// Requests
message EmptyRequest { }

message GetUserRoleRequest {
    int32 user_id = 1;
}

// Replys
message RolesReply {
    repeated Role roles = 1;
}

message Role {
    int32 id = 2;
    string name = 1;
}

message UserRoleReply {
    int32 user_id = 1;
    repeated Role roles = 2;
}

Once these are built, you can run using the command below to generate role.pb.go and user.pb.go.

protoc --go_out=plugins=grpc:. *.proto

Build Services

Now that we have the generated proto files we can get to the meat of the services. For this post, we are going to build both services in the main.go files.

Let's start with the user service.

User Service

The user service is quite simple, as shown above, we only have one endpoint to get a user. In a real-world system you would likely want to hook into a database, but for this example, we're using a hardcoded set of users.

First things first we need a server struct which implements the generated user server interface from the pb/user.pb.go file. That means we create a GetUser method on the server struct which will do some validation, communicate with the role service, and then return a user with their roles.

We need to create a role client to communicate with the role service. We do this by accessing the generated roles protobuf code and using the NewRolesClient method. We pass in a gRPC connection into that method, and that establishes a connection to the roles service.

To expose the gRPC server we need to open and listen on a port, and then gRPC hooks that into the function.

type Server struct {
    users       []*pb.User
    rolesClient rolesPb.RolesClient
}

func getRolesClient() rolesPb.RolesClient {
    conn, err := grpc.Dial("localhost:6000", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Failed to start gRPC connection: %v", err)
    }

    return rolesPb.NewRolesClient(conn)
}

func (s *Server) GetUser(_ context.Context, req *pb.GetUserRequest) (*pb.UserReply, error) {
    if req.UserId < 0 || req.UserId > int32(len(s.users)) {

        return nil, errors.New("invalid user")
    }
    user := s.users[req.UserId]
    roleReq := &rolesPb.GetUserRoleRequest{
        UserId: req.UserId,
    }
    rolesReply, err := s.rolesClient.GetUserRole(context.Background(), roleReq)
    if err != nil {
        return nil, err
    }

    roles := make([]*pb.Role, 0)
    for _, role := range rolesReply.Roles {
        roles = append(roles, &pb.Role{
            Id:   role.Id,
            Name: role.Name,
        })
    }
    return &pb.UserReply{
        User:  user,
        Roles: roles,
    }, nil
}

func main() {
    users := []*pb.User{
        {
            Id:    1,
            Email: "[email protected]",
            Name:  "Bob",
        },
        {
            Id:    2,
            Email: "[email protected]",
            Name:  "Amy",
        },
        {
            Id:    3,
            Email: "[email protected]",
            Name:  "George",
        },
        {
            Id:    4,
            Email: "[email protected]",
            Name:  "Lily",
        },
        {
            Id:    5,
            Email: "[email protected]",
            Name:  "Jacob",
        },
    }

    lis, err := net.Listen("tcp", "localhost:7000")
    if err != nil {
        log.Fatalf("failed to initializa TCP listen: %v", err)
    }
    defer lis.Close()

    server := grpc.NewServer()
    roleServer := &Server{
        users:       users,
        rolesClient: getRolesClient(),
    }
    pb.RegisterUsersServer(server, roleServer)

    server.Serve(lis)
}

Role Service

The role service is also quite simple; we have two endpoints, one to get all the roles and another to a specific users roles. In a real-world system you would likely want to hook into a database, but for this example, we're using a hardcoded set of roles.

First, we need to implement the service as defined in our proto file. In this case that is GetRoles and GetUserRole. Get Roles returns a list of all the roles, and GetUserRole returns the list of roles which a current user has.

To access the roles and user roles we need to pass them into the server, so they're accessible within the controllers.

You need to set up a basic TCP connection, and gRPC binds into that to be externally available.

type Server struct {
    userRoles map[int32][]*pb.Role
    roles     []*pb.Role
}

func (s *Server) GetRoles(_ context.Context, _ *pb.EmptyRequest) (*pb.RolesReply, error) {
    return &pb.RolesReply{
        Roles: s.roles,
    }, nil
}

func (s *Server) GetUserRole(_ context.Context, req *pb.GetUserRoleRequest) (*pb.UserRoleReply, error) {
    roles, ok := s.userRoles[req.UserId]
    if !ok {
        return nil, errors.New("user not found")
    }
    return &pb.UserRoleReply{
        UserId: req.UserId,
        Roles:  roles,
    }, nil
}

func main() {

    var (
        normal = &pb.Role{
            Id:   1,
            Name: "normal",
        }
        editor = &pb.Role{
            Id:   2,
            Name: "editor",
        }
        admin = &pb.Role{
            Id:   3,
            Name: "admin",
        }
        superUser = &pb.Role{
            Id:   4,
            Name: "super user",
        }
    )

    lis, err := net.Listen("tcp", "localhost:6000")
    if err != nil {
        log.Fatalf("failed to initializa TCP listen: %v", err)
    }
    defer lis.Close()

    server := grpc.NewServer()
    roleServer := &Server{
        userRoles: map[int32][]*pb.Role{
            1: {normal},
            2: {normal, editor},
            3: {normal},
            4: {normal, editor, admin},
            5: {normal, editor, admin, superUser},
        },
        roles: []*pb.Role{normal, editor, admin, superUser},
    }
    pb.RegisterRolesServer(server, roleServer)

    server.Serve(lis)
}

Communication

Now that the two services are ready to compile let's give them a test. Since gRPC uses binary transport, not text we cannot just curl the endpoints. You have to use a tool to communicate with them. I would recommend this one, github.com/ktr0731/evans.

Call the GetUser method with the tool and pass an integer between 1-5. You should get a response with the user data plus the roles which came from the other service! It's that easy.

Conclusion

The example above is a straightforward use of service to service communication, but the principles are the same at any level, you'll likely have databases and more strict validation, but ultimately you send serialized data to and from services like this example. I hope it was clear and easy to understand how to set up this sort of communication easily. I find it much easier and faster than using json requests.

You can find all the code for this post on Github, here.

Author image

About Ryan McCue

Hi, my name is Ryan! I am a Software Developer with experience in many web frameworks and libraries including NodeJS, Django, Golang, and Laravel.