Migration from Nginx to Envoy Proxy

Hello, Habr! I present to you a translation of the post: Migration from Nginx to Envoy Proxy.

Envoy is a high-performance distributed proxy server (written in C++) designed for individual services and applications; it also serves as a messaging bus and a "universal data plane" developed for large microservices architectures known as a service mesh. In its creation, solutions to problems encountered while developing servers like NGINX, HAProxy, hardware load balancers, and cloud load balancers were considered. Envoy works alongside every application and abstracts the network, providing common functions regardless of the platform. When all service traffic in the infrastructure flows through the Envoy mesh, it becomes easy to visualize problem areas with consistent observability, tune overall performance, and add core features in a specific location.

Capabilities

  • Out-of-process architecture: Envoy is a standalone, high-performance server that occupies a small amount of memory. It works seamlessly with any application language or framework.
  • Support for http/2 and grpc: Envoy has first-class support for http/2 and grpc for both incoming and outgoing connections. It is a transparent proxy from http/1.1 to http/2.
  • Advanced load balancing: Envoy supports advanced load balancing features, including automatic retries, circuit breaking, global rate limiting, request shadowing, local zone load balancing, etc.
  • Configuration management API: Envoy provides a robust API for dynamically managing its configuration.
  • Observability: Deep observability of L7 traffic, built-in support for distributed tracing, and observability for MongoDB, DynamoDB, and many other applications.

Step 1 — Example of an NGINX config

This scenario uses a specially created file nginx.conf, based on a complete example from NGINX Wiki. You can view the configuration in the editor by opening nginx.conf

The original nginx config

user  www www;
pid /var/run/nginx.pid;
worker_processes  2;

events {
  worker_connections   2000;
}

http {
  gzip on;
  gzip_min_length  1100;
  gzip_buffers     4 8k;
  gzip_types       text/plain;

  log_format main      '$remote_addr - $remote_user [$time_local]  '
    '"$request" $status $bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '"$gzip_ratio"';

  log_format download  '$remote_addr - $remote_user [$time_local]  '
    '"$request" $status $bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '"$http_range" "$sent_http_content_range"';

  upstream targetCluster {
    172.18.0.3:80;
    172.18.0.4:80;
  }

  server {
    listen        8080;
    server_name   one.example.com  www.one.example.com;

    access_log   /var/log/nginx.access_log  main;
    error_log  /var/log/nginx.error_log  info;

    location / {
      proxy_pass         http://targetCluster/;
      proxy_redirect     off;

      proxy_set_header   Host             $host;
      proxy_set_header   X-Real-IP        $remote_addr;
    }
  }
}

NGINX configurations typically consist of three key elements:

  1. The NGINX server configuration, log structure, and Gzip functionality. This is defined globally in all cases.
  2. Configuring NGINX to accept requests on the host one.example.com on port 8080.
  3. Configuring the target location, how to handle traffic for different parts of the URL.

Not all configurations will be applied to Envoy Proxy, and you do not need to configure some parameters. Envoy Proxy has four key types, which support the basic infrastructure provided by NGINX. The core includes:

  • Listeners: They define how Envoy Proxy accepts incoming requests. Currently, Envoy Proxy supports only TCP-based listeners. Once a connection is established, it is passed to a set of filters for processing.
  • Filters: They are part of a pipeline architecture that can process incoming and outgoing data. This functionality includes filters like Gzip, which compresses data before sending it to the client.
  • Routers: They redirect traffic to the required destination defined as a cluster.
  • Clusters: They define the endpoint for the traffic and configuration parameters.

We will use these four components to create the Envoy Proxy configuration to match a specific NGINX configuration. The goal of Envoy is to work with APIs and dynamic configuration. In this case, the basic configuration will utilize static, hard-coded parameters from NGINX.

Step 2 — NGINX Configuration

The first part nginx.conf defines some internal components of NGINX that need to be configured.

Worker Connections

The configuration below defines the number of worker processes and connections. This indicates how NGINX will scale to meet demand.

worker_processes  2;

events {
  worker_connections   2000;
}

Envoy Proxy manages worker processes and connections differently.

Envoy creates a worker thread for each hardware thread in the system. Each worker thread runs a non-blocking event loop that is responsible for

  1. Listening to each listener
  2. Accepting new connections
  3. Creating a set of filters for the connection
  4. Handling all input/output operations for the duration of the connection.

All further processing of the connection is fully handled in the worker thread, including any redirect behavior.

For each worker thread in Envoy, there is a connection in the pool. Thus, HTTP/2 connection pools establish only one connection for each external host at a time; with four worker threads, there will be four HTTP/2 connections for each external host at steady state. Keeping everything in one worker thread allows nearly all code to be written without locks, as if it were single-threaded. If more worker threads are allocated than necessary, it can lead to inefficient memory usage, creating many idle connections, and reducing the number of connections returned to the pool.

For more information, visit Envoy Proxy blog.

HTTP Configuration

The following NGINX configuration block defines HTTP settings such as:

  • Which mime types are supported
  • Default timeouts
  • Gzip configuration

You can configure these aspects using filters in Envoy Proxy, which we will discuss later.

Step 3 — Server Configuration

In the HTTP configuration block, NGINX specifies to listen on port 8080 and respond to incoming requests for the domains one.example.com and www.one.example.com.

 server {
    listen        8080;
    server_name   one.example.com  www.one.example.com;

Inside Envoy, this is managed by Listeners.

Envoy Listeners

The most important aspect of getting started with Envoy Proxy is defining listeners. You need to create a configuration file that describes how you want to run the Envoy instance.

The snippet below will create a new listener and bind it to port 8080. The configuration indicates to Envoy Proxy which ports it should be bound to for incoming requests.

Envoy Proxy uses YAML notation for its configuration. For an introduction to this notation, check here the link.

Copy to Editorstatic_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }

There is no need to specify server_name, as Envoy Proxy filters will handle it.

Step 4 — Location Configuration

When a request comes into NGINX, the location block determines how to handle and where to direct the traffic. In the following snippet, all traffic to the site is routed to an upstream cluster named targetCluster. The upstream cluster defines the nodes that should handle the request. We will discuss this in the next step.

location / {
    proxy_pass         http://targetCluster/;
    proxy_redirect     off;

    proxy_set_header   Host             $host;
    proxy_set_header   X-Real-IP        $remote_addr;
}

In Envoy, this is handled by Filters.

Envoy Filters

For static configuration, filters determine how to handle incoming requests. In this case, we set up filters that correspond to server_names from the previous step. When incoming requests match certain domains and routes, the traffic is directed to the cluster. This is equivalent to the upstream configuration in NGINX.

Copy to Editor    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
                - "one.example.com"
                - "www.one.example.com"
              routes:
              - match:
                  prefix: "\/"
                route:
                  cluster: targetCluster
          http_filters:
          - name: envoy.router

Name envoy.http_connection_manager is a built-in filter in Envoy Proxy. Other filters include Redis, Mongo, TCP. You can find the complete list in the documentation.

For more information on other load balancing policies, visit Envoy Documentation.

Step 5 — Proxy and Upstream Configuration

In NGINX, the upstream configuration defines a set of target servers that will handle the traffic. In this case, two clusters have been designated.

  upstream targetCluster {
    172.18.0.3:80;
    172.18.0.4:80;
  }

In Envoy, this is managed by clusters.

Envoy Clusters

The upstream equivalent is defined as clusters. In this case, hosts have been identified to handle the traffic. The method of accessing the hosts, such as timeout, is defined as the cluster configuration. This allows for finer control over aspects such as timeout and load balancing.

Copy to Editor  clusters:
  - name: targetCluster
    connect_timeout: 0.25s
    type: STRICT_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts: [
      { socket_address: { address: 172.18.0.3, port_value: 80 }},
      { socket_address: { address: 172.18.0.4, port_value: 80 }}
    ]

When using service discovery STRICT_DNS Envoy will continuously and asynchronously resolve the specified DNS targets. Each returned IP address from the DNS will be treated as an explicit host in the upstream cluster. This means that if the query returns two IP addresses, Envoy will assume there are two hosts in the cluster, and both should be load balanced. If a host is removed from the results, Envoy assumes it no longer exists and will select traffic from any existing connection pools.

For more information see the Envoy Proxy documentation.

Step 6 — Access log and errors

Final configuration — logging. Instead of sending error logs to disk, Envoy Proxy uses a cloud approach. All application logs are output to stdout and stderr.

When users make a request, access logs are optional and are disabled by default. To enable access logs for HTTP requests, turn on the configuration access_log for the HTTP connection manager. The path can either be a device like stdout, or a file on disk, depending on your requirements.

The following configuration will redirect all access logs to stdout (translator’s note — stdout is necessary for using Envoy inside Docker. If using outside of Docker, replace /dev/stdout with a path to a regular log file). Copy the snippet into the connection manager configuration section:

Copy to Clipboardaccess_log:
- name: envoy.file_access_log
  config:
    path: "/dev/stdout"

The results should look like this:

      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          access_log:
          - name: envoy.file_access_log
            config:
              path: "/dev/stdout"
          route_config:

By default, Envoy has a format string that includes details of the HTTP request:

[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"

The result of this format string:

[2018-11-23T04:51:00.281Z] "GET / HTTP/1.1" 200 - 0 58 4 1 "-" "curl/7.47.0" "f21ebd42-6770-4aa5-88d4-e56118165a7d" "one.example.com" "172.18.0.4:80"

The output content can be configured by setting the format field. For example:

access_log:
- name: envoy.file_access_log
  config:
    path: "/dev/stdout"
    format: "[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"
"

The log line can also be output in JSON format by setting the json_format. For example:

access_log:
- name: envoy.file_access_log
  config:
    path: "/dev/stdout"
    json_format: {"protocol": "%PROTOCOL%", "duration": "%DURATION%", "request_method": "%REQ(:METHOD)%"}

For more information on the logging methodology of the envoy, visit

https://www.envoyproxy.io/docs/envoy/latest/configuration/access_log#config-access-log-format-dictionaries

Logging is not the only way to gain insights into the workings of Envoy Proxy. It has built-in tracing and metrics capabilities. You can learn more in the tracing documentation or through The interactive tracing script.

Step 7 — Starting

You have now translated the configuration from NGINX to Envoy Proxy. The final step is to run an instance of Envoy Proxy for testing.

Running as a user

At the top of the NGINX configuration, the line user www www; indicates that NGINX runs as a low-privilege user for enhanced security.

Envoy Proxy adopts a cloud-native approach to managing who owns the process. When we run Envoy Proxy through a container, we can specify a low-privilege user.

Starting Envoy Proxy

The command below will run Envoy Proxy through a Docker container on the host. This command allows Envoy to listen for incoming requests on port 80. However, as specified in the listener configuration, Envoy Proxy listens for incoming traffic on port 8080. This allows the process to run as a low-privilege user.

docker run --name proxy1 -p 80:8080 --user 1000:1000 -v /root/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy

Testing

With the proxy running, tests can now be performed and processed. The following cURL command sends a request with the host header set in the proxy configuration.

curl -H "Host: one.example.com" localhost -i

The HTTP request will result in an error 503This is because upstream connections are not working and are unavailable. Thus, Envoy Proxy has no available targets for the request. The following command will launch a series of HTTP services that match the configuration defined for Envoy.

docker run -d katacoda/docker-http-server; docker run -d katacoda/docker-http-server;

With available services, Envoy can successfully proxy traffic to the destination.

curl -H "Host: one.example.com" localhost -i

You should see a response indicating which Docker container handled the request. In the Envoy Proxy logs, you should also see the access log output.

Additional HTTP Response Headers

In the headers of a valid request response, you will see additional HTTP headers. The header shows the time the upstream host spent processing the request, expressed in milliseconds. This is useful if the client wants to determine the service time compared to network latency.

x-envoy-upstream-service-time: 0
server: envoy

Final Config

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
                - "one.example.com"
                - "www.one.example.com"
              routes:
              - match:
                  prefix: "\/"
                route:
                  cluster: targetCluster
          http_filters:
          - name: envoy.router
          clusters:
  - name: targetCluster
    connect_timeout: 0.25s
    type: STRICT_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts: [
      { socket_address: { address: 172.18.0.3, port_value: 80 }},
      { socket_address: { address: 172.18.0.4, port_value: 80 }}
    ]

admin:
  access_log_path: \/tmp\/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9090 }

Additional information from the translator

You can find installation instructions for Envoy Proxy on the website https://www.getenvoy.io/

By default, the rpm lacks a systemd service config.

Add the systemd service config to /etc/systemd/system/envoy.service:

[Unit]
Description=Envoy Proxy
Documentation=https://www.envoyproxy.io/
After=network-online.target
Requires=envoy-auth-server.service
Wants=nginx.service

[Service]
User=root
Restart=on-failure
ExecStart=\/usr\/bin\/envoy --config-path \/etc\/envoy\/config.yaml
[Install]
WantedBy=multi-user.target

You need to create the directory /etc/envoy/ and place the config.yaml file there.

There is a Telegram chat about envoy proxy: https://t.me/envoyproxy_ru

Envoy Proxy does not support serving static content. So, please vote for this feature: https://github.com/envoyproxy/envoy/issues/378

Only registered users can participate in the survey. Please log in, please.

Did this post encourage you to install and test envoy proxy?

  • yes

  • none

75 users voted. 18 users abstained.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster