Supporting REST and HTML with a gRPC Microservice

[ad_1]

Any microservice can become a gRPC microservice.

gRPC and protobuf work together to bring more structure to building out APIs, even if your service has to work across different clients or support streams of data.

The system generates model and networking code for the protocol — you define the API using a .proto file which is compiled into native code for the different clients.

Although some clients may not be able to take advantage of gRPC, that’s OK because gRPC includes support for classic HTTP and REST as well.

Moreover, gRPC is designed for machines to talk to other machines, and the data packets can be much smaller than corresponding REST and HTML traffic.

gRPC shines in bandwidth-constrained uses cases, such as a service shares readings from hundreds of sensors in an urban farm with a central server that manages the building’s HVAC, or a service that calculates fees for a personal shopper mobile app.

In this tutorial, you’ll work with an app that creates TODOs and learn how to:

  • Build a .proto file so it maps HTTP URLs to gRPC services.
  • Configure an Envoy proxy server to transcode HTTP/JSON to gRPC.
Note: This tutorial makes a few assumptions:

These instructions work for Apple Silicon and Intel-based Macs, as well as x86_64 and AArch64 versions of Linux.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

The starter project contains a Vapor app and a database server for managing TODO items. It uses gRPC to communicate with clients.

Open the envoy folder to find some configuration files. You’ll modify them for the Envoy proxy.

Make sure you have these prereqs installed:

  • protoc to generate gRPC files.
  • Postman, or something similar, to send HTTP requests to the server.
  • Evans, or something similar, to generate gRPC requests.

If you need to install these, step through the first few sections of gRPC and Server Side Swift: Getting Started.

When installing protoc on Linux, be sure to add both the bin and include directories from the extracted archive to your PATH.

Now that your tools are set up, it’s time to start things up with Docker Compose and test things out with Evans.

Open Terminal and navigate to the root directory of the project. Run the following commands to start the vapor app and database servers:


docker compose up db -d
docker compose up app -d

Setting the `-d` option runs the process in the background, which is helpful when you’re viewing console output with Docker Desktop.

If you’re running in CLI, you can omit the `-d` option. You’ll want to use Screen to run the command, or a new terminal window or tab.

Exercising the Server with Evans

Now that both servers are running, you’ll use Evans to add a few TODO items.

Navigate to the root of the project — the directory that contains todo.proto — and type this command:


evans repl --host localhost --port 1234 --proto ./todo.proto

The above command will bring you into the evans repl (Read Evaluate Print Loop) and allow you to call gRPC endpoints defined in todo.proto.

Next, you’ll create new TODO items and generate a list of them with Evans:

  • To create items, you’ll use call createTodo and follow the prompts.
  • To generate a list of TODO items, you’ll use call fetchTodos.
  • To close the app, unsurprisingly, you’ll use exit.

Take a moment to review the animated screenshot. We use Evans to create, complete, delete, and list Todo items.

Using Evans to create a todo list for brewing a latte

Evans uses gRPC to communicate with the servers. It doesn’t support HTTP yet.

To see for yourself, use Postman or another REST client to navigate to http://localhost:1234/. The server will respond with an error code of 415 Unsupported Media Type.

Postman HTTP error 415

If you need to bring down the servers, use Docker desktop or type docker compose down in your Terminal.

Setting Up Envoy Proxy

Lyft designed the Envoy proxy server. Its core features include gRPC, load balancing and HTTP/2. Google Cloud proxy works on top of Envoy. Envoy is built on the learnings of NGINX and HAProxy, and can run in parallel with them providing common features.

The other parts of your application are running in Docker containers, and now it’s time to add one for Envoy.

Creating a Docker Container for Envoy

Find and open docker-compose.yml with a text editor so that you can add an entry. You’re working with yml, so make sure you indent and use whitespace as shown.

At the bottom of the file, add the following entry for Envoy, below the entry for db:


  envoy:  
    image: envoyproxy/envoy:v1.21-latest  
    volumes:  
     - ./envoy/envoy-demo.yml:/etc/envoy/envoy.yaml  
    ports:  
     - '8082:8082'  
     - '9901:9901'

This entry uses a standard build of the Envoy proxy from Docker hub and does the following:

  • The volumes section copies a configuration file into the server.
  • The ports section exposes 8082 for HTTP traffic.
  • Then it exposes port 9901 — the administrative site for Envoy and used only to confirm Envoy is running.

Save the changes to docker-compose.yml. Start the Envoy server by typing docker compose up envoy -d in Terminal.

Confirm that Envoy is running by pointing a browser to 127.0.0.1:9901 to bring up the administrative site. Next, navigate to 127.0.0.1:8082 which will redirect to Envoy’s main website.

You’ve just deployed a configuration example from Envoy’s documentation, and it wasn’t even that hard! Next, you’ll modify it so it can transcode HTTP and gRPC traffic.

Annotating the API Definition

In this section, you’ll annotate the todo.proto file. You’ll also use protoc to generate a file for Envoy to use.

Including Dependencies

Envoy is organized into seperate packages to minimize requirements and improve organization.

The code that makes HTTP annotations work is in a separate annotations.proto file. Standard practice with .proto files differs from Swift Package Manager (SPM).

Although SPM can download dependencies when it runs, for .proto files, you’ll want to download dependencies first to prevent unexpected changes from breaking your application.

Make a new sub-directory (inside the project root) to hold the dependency:


mkdir -p google/api

Download the current version of the annotations.proto from Google using cURL:


curl https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto > google/api/annotations.proto

Download http.proto — it’s a dependency for annotations.proto:


curl https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto > google/api/http.proto

Add an import statement to your todo.proto file.

Open the todo.proto in a text editor and replace the //TODO: Add AnnotationsImport line with the following:


import "google/api/annotations.proto";

Great, now you have your house in order with all the dependencies installed.

Next up, you’ll cover the basic options you have for using HTTP GET verbs.

Transcoding HTTP GET Verbs

In the original todo.proto, the FetchTodos procedure call takes an empty input then returns a list of todos, similar to an HTTP GET request.

Modify the FetchTodos definition, inserting an annotation between the curly braces. Your finished code should look like this:


rpc FetchTodos (Empty) returns (TodoList) {
  option (google.api.http) = {
    get: "/v1/todos"
  };
}

The code tells the Envoy proxy to convert an inbound HTTP GET of http://yourserver/v1/todos to a gRPC call of FetchTodos. It then converts the response of TodoList to a JSON array.

Another available service that is similar to an HTTP GET is CompleteTodo. Unlike FetchTodos, this service has an input parameter. When using HTTP GET, input parameters are usually coded in the URL. The annotations support this pattern.

Find the CompleteTodo service and insert this annotation between the curly braces:


option (google.api.http) = {
  get: "/v1/todos/{todoID}/complete"
};

With this, you tell Envoy to extract a value from the URL and assign it to todoID — capitalization matters here. The todo.proto definition for CompleteTodo expects a message of type TodoID.

Look at the definition for the TodoID message:


message TodoID {
  string todoID = 1;
}

One of the fields is a string type, called todoID. At runtime, Envoy uses the string extracted from the URL to create a TodoID then passes a gRPC call to the server.

Transcoding an HTTP POST Verb

For an HTTP POST request, you must specify what the body of the POST contains.

First, find the entry for CreateTodo, and add this annotation:


option (google.api.http) = {
  post: "/v1/todos"
  body: "*"
};

The body line indicates that the payload will contain keys and values at the root level.

Envoy will attempt to decode them into the needed message. Any fields missing from the request payload will be assigned default values in gRPC.

Still in todo.proto, observe how it defines a Todo. It should look like this:


message Todo {
  optional string todoID = 1;
  string title = 2;
  bool completed = 3;
}

The todoID is optional, and completed is a bool which has a default value of false.

When the client creates the body of the POST, it uses JSON:


{
"title": "Buy Spinach and Olive Oil"
}

Using an asterisk for the body is just one pattern. For this tutorial, you’ll stick to the asterisk.

Note: There are other implementations. A more common alternative is to create a message to hold a request because that request can be appended.

An example of this form is below — it would change the service and create a new message:


rpc CreateTodo(CreateTodoRequest) returns (Todo) {
  option (google.api.http) = {
    post: "/v1/todos"
    body: "todo"
  };
}

message CreateTodoRequest {
  Todo todo = 1;
}

In the example above, the body expects a JSON object that maps to a Todo message. That would require changing the server and client code, which is beyond the scope of this tutorial.

By now, you can see a pattern for annotating gRPC procedure calls with HTTP. There’s still more to learn, so keep reading.

Transcoding Other Verbs

In todo.proto, there is one call left to explore: DeleteTodo. It uses the TodoID, similarly to how CompleteTodo uses it, but there is a different HTTP verb.

Try it out for yourself. Annotate DeleteTodo like this:


option (google.api.http) = {
  delete: "/v1/todos/{todoID}"
};

Similar to CompleteTodo above, you tell Envoy to extract a value from the URL and assign it to todoID.

Additionally, gRPC supports PUT and UPDATE, as well as others. Google’s gRPC Service Configuration Reference for gRPC Transcoding explains the implementation. It also covers how to use URL query values and a few other tricks.

Generating an Annotated Protobuf File

At this point, you’ve annotated todo.proto and put the imports in place, and you’re ready to generate a todo.pb file for Envoy to use.

Save your changes to todo.proto. Make sure your working directory is the root for your project. Execute this command to tell protoc to generate todo.pb:


protoc -I. --include_imports --include_source_info --descriptor_set_out=todo.pb todo.proto

Here’s what you’re doing with that command:

  • -I. tells protoc to look for imports starting in the current directory.
  • --include_source_info and --include_imports work together with --descriptor_set_out to create todo.pb as a self-contained, meaning it needs no dependency references at runtime.

Copy the new todo.pb to the envoy folder so it’s adjacent to the Envoy configuration files.

Before you configure Envoy to do the transcoding, open docker-compose.yml in a text editor and overwrite volumes within the Envoy section with the following:


- ./envoy/grpc-envoy.yml:/etc/envoy/envoy.yaml
- ./envoy/todo.pb:/data/todo.pb:ro

The first line will now copy grpc-envoy.yml into the server, and the second line will copy todo.pb into the server’s container.

Ok, you’re almost to the good part. Keep going! The last step is to configure Envoy to actually do the transcoding.

Configuring Envoy for Transcoding

Open the envoy directory then open grpc-envoy.yml in a text editor. This file is a sample taken from the Envoy documentation and is a basic, bare configuration to support transcoding.

The first entry for admin assigns the administration website to port 9901. In the section for static_resouces there are listeners and clusters.

Envoy uses one listener for one port. Skim through the configuration to take note of a few more attributes:

  • There’s a single listener watching port 8082.
  • There’s an entry for stat_prefix, which is just the prefix that any log entries will have.
  • In the routes section, note that the server is going to match using the “/” prefix, meaning it’ll match everything.
  • You can also see that it’ll send traffic to a cluster named grpc, which is defined further down.
  • And before Envoy routes any traffic, it’ll apply the http_filters.

Adding a Transcoding Filter

The first filter you need to set up is the transcoding filter. Its important keys are name and typed_config, and they signal that your filter is a gRPC to HTTP/JSON transcoder.

Your first step is to tell the filter about your API.

Set the proto_descriptor to the file path of todo.pb. Additionally, set the services to the name of your service in the todo.proto file.

Your finished entry should look like this:


proto_descriptor: "data/todo.pb"
services: ["todos.TodoService"]

Leave the other values in this section as their defaults, but there are a couple of items to note:

  • Scroll down to the definition for clusters. At the end, you’ll find an entry for address: host.docker.internal, which is something you need when running Envoy in Docker as you are right now.
  • Your gRPC server port value is set to 1234, so no need to make changes there.

Running the Servers

If your servers aren’t running, use Docker commands to start them. And even if your Envoy server is running, bring it up again to reload the configuration files you have just modified. Open a new Terminal, navigate to the starter project root directory and enter the following commands:


docker compose up db -d
docker compose up app -d
docker compose up envoy -d

These commands bring up your application containers again, re-reading their configuration files to pick up any changes.

Now that the configurations are set, you should be able to send gRPC or HTTP traffic to port 8082. The requests will get rerouted to your gRPC server.

The next step is to use Postman to send a GET request to localhost:8082/v1/todos. If you created any TODOs earlier they should appear. Otherwise, you’ll recieve an empty JSON array.

The animated screenshot below shows retrieving the Todo list, creating a Todo, completing a Todo, and deleting a Todo with Postman.
Listing, adding, completing, and removing Todos with Postman

If you’ve installed Evans you can now use it with port 8082.

Envoy routes both kinds of traffic — gRPC traffic passes through to the server untouched, and HTTP traffic gets transcoded.

Now do it: To point Evans to the new port, change the command from before:


evans repl --host localhost --port 8082 --proto ./todo.proto

Now Evans knows to use the new port you created.

Other Options with protoc

Swift server code is moving from using Futures based on SwiftNIO to Async/Await. Recently, the grpc-swift team updated the protoc plugins to generate both code patterns.

In the next few sections, you’ll learn how to switch to Async/Await patterns in your concurrency code.

Generating Concurrency Code

In this tutorial, the grpc-swift plugin uses EventLoopFuture but not the Async/Await concurrency pattern. You can ignore part of the documentation at the repo.

In the past either ExperimentalAsyncClient or the ExperimentalAsyncServicer flag could generate experimental Aysnc/Await code, but neither currently work.

In Spring of 2022, Async/Await support was moved to a different branch, but was merged back to main in the Summer.

With these updates to the plugins, you don’t need to provide any special options or flags. They generate Async/Await code and the SwiftNIO style code.

Starting with the release of grpc-swift 1.8 in June 2022, generated .swift files no longer use the same naming convention for Swift compilers 5.6 and above for clients. For servers, the SwiftNIO naming convention is the same.

To take advantage of the Async/Await structure, new implementations are available to you. For example, consider the code generated by the todo.proto:

For Swift 5.5 and older compilers, the service provider is `Todos_TodoServiceProvider`

For Swift 5.6 and newer compilers, the SwiftNIO service provider is `Todos_TodoServiceProvider`. Additionally a `Todos_TodoServiceAsyncProvider` protocol appears in the todo.grpc.swift file.

The signature of the fetchTodos changes from:


func fetchTodos(request: Todos_Empty, context: StatusOnlyCallContext) -> EventLoopFuture<Todos_TodoList>

To this:


func fetchTodos(request: Todos_Empty, context: GRPCAsyncServerCallContext) async throws -> Todos_TodoList

Though this tutorial doesn’t focus on writing Swift Service Clients, there is similar change worthy of note.

For 5.6 and later compilers, any instances of `Todos_TodoServiceClient` will be marked as deprecated and will use the SwiftNIO code.

In order to continue using SwiftNIO style code, you need to change instances of `Todos_TodoServiceClient` to `Todos_TodoServiceNIOClient`.

To switch to Async/Await, update your client code to use `Todos_TodoServiceAsyncClient`.

This part of the grpc-swift project is under active development. Be sure to check the repository and GitHub if you encounter issues or unexpected warnings as you work.

Where to Go from Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of this tutorial.

In this tutorial you learned a few things:

  • How to generate a protocol definition file that mapped HTTP URLs to gRPC services.
  • How to configure an Envoy proxy to transcode HTTP/JSON to gRPC.
  • How to use some of the optional features of the protoc command.

Now you’re well-equipped to enable services to use multiple protocols at the same endpoint. You can upgrade your servers to enjoy more efficient communication with new clients, without forcing your older applications to update. This means that existing REST clients, like web front ends or iOS applications, don’t need to be changed. Your back end applications can now support gRPC clients seamlessly.

Visit the Envoy documentation website to access the full documentation for how to configure Envoy to transcode HTTP/gRPC.

You can also find the main documentation for Envoy on that same site.

Over on Github, the grpc-swift project contains information about support for Async/Await, and it has the full documentation for the Swift plugin and its options.

Finally, Google offers guidance for designing APIs that support gRPC and HTTP transcoding.

We hope you enjoyed this tutorial. Please join the forum discussion below if you have any questions or comments!

[ad_2]

Source link