building a blog part 2: deploying the envoy proxy

2019.07.07

My original motivation for creating this site was just to have a real use-case for Envoy. But while looking for ways to shoehorn envoy in to the stack, I actually found a lot of really useful things that I could use it for. I’m excited to FINALLY get around to deploying it so let’s get moving!

If you're asking yourself "what's envoy" or "who are you and why am I reading this", go back and check out the pre-reading for this post:

Today we're going to deploy envoy as a proxy server sitting in front of this site. Right now this site is a collection of static files, generated by Hugo and served by nginx. This is what the request flow looks like when you click on a link:

Fortunately, it’s very simple. There is really just one thing going on here and nginx is doing all the work. Nginx is:

  • Listening on port 80 for TCP connections
  • Processing any HTTP requests that arrive on those connections and mapping it to the resources it has available
  • Talking to the file system to retrieve files that were requested. The site's files are sitting on the host in /var/www/markchur.ch/ arranged according to the structure of the URLs (defined by the nginx config).
  • Packaging up those files in an HTTP response and sending it back to the client

This probably raises a question about envoy - why shouldn’t we keep nginx as our frontend web server? We certainly could, but the final architecture will actually use both. To understand why, let’s look at the differences between web proxies and web servers.

Web Proxies vs Web Servers

In most client-server architectures, there is a request flow that looks something like the following. A client sends a request over a network protocol. The server then retrieves files or data from some persistent place and serves them back over the internet.

This is known as a web server, app server, or sometimes just “sever”. At a fundamental level, web servers serve and generate content to be packaged in to network packets. They often do a lot more, including talking to different backend services to compile a response, executing application logic, or generating dynamic content on the fly. Many applications such as Websphere and .NET IIS are themselves web servers tightly integrated with application logic.

Web proxies (also known as proxies, reverse proxies, proxy servers, and sometimes load balancers) are very similar to web servers, but play a different role. Proxies serve as an intermediary between the client and the server. They rarely generate or serve local content themselves and are rarely the last hop in the request flow. The purpose of a proxy is to offload network functionality from the origin server, such as security, monitoring, and rate limiting and also add other functionality, such as load balancing, that is best applied in the network than at the end of a connection.

  • Some common web servers are nginx, Apache, Msft IIS, and Node.js
  • Some common web proxies are HAproxy, F5, Citrix ADC, and Envoy

Note that these lists overlap heavily. Many web servers (such as nginx) can also act as fully featured proxies, though their feature sets generally tend to lean in one direction or the other. Envoy, for example, is purely designed to be a hop in the network and is not able to serve static content. Thus, we could not use envoy as a standalone web server. This is why I have both envoy and nginx.

Nginx will serve static content (generated by Hugo) and Envoy will proxy the requests to apply network services such as load balancing, traffic management, TLS, and monitoring. Especially in architectures where the network is shared infrastructure and applications are independently owned, it makes a lot of sense to separate applications from application networking.

Site Architecture

Now let’s get to the good stuff. This is the architecture we’re shooting for. Nginx will serve internally on port 8080, a port that is not accessible outside the private network. Envoy will listen on port 80 through a public interface. This is a simplified diagram, but in the future we’ll use the same envoy architecture for multiple redundant app backends.

Deploying Envoy

Envoy can be configured dynamically through its API and/or through a configuration file. For the sake of simplicity I’m using a static config file, but will switch to the API in the future.

The relevant pieces of this config are:

  • Listener - tells envoy which interface and port to bind to for incoming packets
  • Filter - protocol-specific modules that process the incoming traffic
  • Routes - rules that govern where traffic should be sent
  • Cluster - a group of backend endpoints that envoy will send traffic to

Here our listener is configured, setting the front door to port 80.

 listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 80

This is the HTTP filter. Within this HTTP filter there is a route_config, which define the rules for how envoy routes HTTP host and path to the correct backend. Here we are blindly routing all hosts ( * ) and HTTP paths ( / ) to the hugo-cluster.

filters:
- name: envoy.http_connection_manager
  config:
    route_config:
      virtual_hosts:
      - name: hugo-virtual-host
        domains: "*"
        routes:
        - route:
            cluster: hugo-cluster
          match:
            prefix: "/"

Here is a statically configured cluster. Typically this would be a dynamic list but right now it’s just a single endpoint, localhost and 8080 because nginx and envoy are running on the same host. In the future we'll turn this into an multi-host HA architecture with dynamic service discovery.

clusters:
- name: hugo-cluster
  hosts:
  - socket_address:
      address: localhost
      port_value: 8080
lb_policy: round_robin

Once I have my config in place, I deploy the envoy proxy using Docker, mounting the config file and also an access log file inside the container so that they can be persisted. Peeking at the container logs shows all clusters initialized which means that envoy should be up and running with our config.

$ docker run \
   -v /etc/envoy/envoy.yaml:/etc/envoy/envoy.yaml \
   -v /var/log/envoy.log:/var/log/envoy.log \
   -itd --rm --network host --name envoy \
   envoyproxy/envoy:v1.10.0 \
   envoy -c /etc/envoy/envoy.json

$ docker logs envoy
…
[2019-06-02 22:23:36.391][1][info][config] [source/server/configuration_impl.cc:56] loading 1 cluster(s)
[2019-06-02 22:23:36.392][1][info][config] [source/server/configuration_impl.cc:60] loading 1 listener(s)
[2019-06-02 22:23:36.407][1][info][main] [source/server/server.cc:478] starting main dispatch loop
[2019-06-02 22:23:36.413][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:137] cm init: all clusters initialized

Running on envoy

It's the moment of truth. We hit the URL and huzzah - server: envoy. Success!

$ curl -I markchur.ch
HTTP/1.1 200 OK
server: envoy
date: Sun, 30 Jun 2019 20:30:20 GMT
content-type: text/html
content-length: 4515
last-modified: Mon, 03 Jun 2019 02:13:25 GMT
etag: "5cf48245-11a3"
accept-ranges: bytes
x-envoy-upstream-service-time: 0

In the next posts we'll take a look at security and monitoring with envoy. These topics will have us well on our way to a production-ready site!