TT Lab
Get started
Learn Learning paths Courses

Envoy Internals

Split Four Virtual Hosts and Five Match Kinds

Continue in TT Lab

Goal

Confirm that the virtual host is decided first by the Host header, and add the kinds of matches and the kinds of destinations one at a time to see where the same request goes.

Why it matters

To a human reader, routing configuration looks like a "list of rules", but it is really two stages of selection. And the two stages run on different rules — the first by specificity, the second by order. If you do not know this difference, you waste time moving rules up and down. On top of that, if you confirm by hand that weighted_clusters is a probability, not a guarantee, that you can finish things with direct_response without touching the app, and that headers are added separately in three layers, you can read someone else's route table in a few minutes.

Steps

  1. In /root/envd-route/route.yaml, put four virtual hosts — exact (shop.envd.test), suffix (*.envd.test), prefixw (shop.*) and anyhost (*). Each returns vh=exact, vh=suffix, vh=prefixw and vh=any respectively at / (admin 9931, listener 127.0.0.1:10031). After you start it, request /zzz with four different Host headers and write the results to /root/envd-route/01-vhosts.txt as four lines, exact=, suffix=, prefixw= and any= (the Host values to request are, in order, shop.envd.test, www.envd.test, shop.other.test and nowhere.example).
  2. Copy /root/envd-route/route.yaml to /root/envd-route/route-dup.yaml, then create one more virtual host with domains: ["*"] (give it a name that does not collide). Check it with envoy --mode validate and save the output and exit code to /root/envd-route/02-onestar.txt (the last line is rc=1).
  3. Add two routes above the / route of the exact virtual host — path: "/exact" returns m=path, and safe_regex with ^/id/[0-9]+$ returns m=regex. In /root/envd-route/03-match.txt, write three lines: path= (the /exact request), regex= (the /id/42 request) and regex_miss= (the /id/abc request). The Host is always shop.envd.test.
  4. Add two more routes for /api (below the regex routes, above the / route). One uses a headers condition and returns m=header when x-canary: yes, and the other uses a query_parameters condition and returns m=query when debug=1. In /root/envd-route/04-cond.txt, write three lines: header=, query= and plain= (just /api, without conditions).
  5. Start two upstreams (8082, 8083), create clusters blue and green, and make the /split route send traffic split 75 to 25 through weighted_clusters. Start it with --concurrency 1, request 40 times, and in /root/envd-route/05-weighted.txt write three lines, blue=, green= and total= (the number each upstream received, and the sum).
  6. Add two routes — path: "/healthz" sends 200 and alive through direct_response, and prefix: "/old" sends a 301 to /new through redirect. In /root/envd-route/06-direct.txt, write three lines: health= (the response body), redirect_code= (the HTTP code) and redirect_url= (the Location header value).
  7. Add the response header x-level in each of the three layers — route on the route, virtualhost on the virtual host, and routeconfig on the route table. Request /zzz on shop.envd.test, write all the x-level headers that come back on one line, levels=, in /root/envd-route/07-headers.txt, joined by spaces with no commas, and write the count on the count= line.
  8. In /root/envd-route/08-report.md, write four lines — vhost_order= (the virtual host specificity order in the format exact,suffix,prefix,star), star_limit= (the number of * virtual hosts you can put in one route table), blue_share= (the share blue received in step 5, as an integer percentage) and redirect_code= (the code from step 6) — and below them write what you learned in at least four lines.

Notes

The virtual host is decided before the route table

In /root/envd-route/route.yaml, put four virtual hosts — exact (shop.envd.test), suffix (*.envd.test), prefixw (shop.*) and anyhost (*). Each returns vh=exact, vh=suffix, vh=prefixw and vh=any respectively at / (admin 9931, listener 127.0.0.1:10031). After you start it, request /zzz with four different Host headers and write the results to /root/envd-route/01-vhosts.txt as four lines, exact=, suffix=, prefixw= and any= (the Host values to request are, in order, shop.envd.test, www.envd.test, shop.other.test and nowhere.example).

Before it looks at the route table, the virtual host is decided first by the Host (or :authority) header. So half of "I definitely wrote the route but it does not match" is choosing the virtual host wrongly. There are two kinds of wildcards, suffix (*.foo.com) and prefix (foo.*), and neither matches an empty string. Send the request with curl -H "Host: 이름" http://127.0.0.1:포트/zzz (the placeholders are the name and the port).

Only one * can exist in a whole route table

Copy /root/envd-route/route.yaml to /root/envd-route/route-dup.yaml, then create one more virtual host with domains: ["*"] (give it a name that does not collide). Check it with envoy --mode validate and save the output and exit code to /root/envd-route/02-onestar.txt (the last line is rc=1).

Virtual host selection runs on "the most specific one wins", and with two * there is no way to decide which is more specific. So Envoy does not leave this state confusing at run time; it rejects it when it reads the configuration. The rejection message includes the name of the route table, so copy that line as it is.

Match by exact path and by regular expression

Add two routes above the / route of the exact virtual host — path: "/exact" returns m=path, and safe_regex with ^/id/[0-9]+$ returns m=regex. In /root/envd-route/03-match.txt, write three lines: path= (the /exact request), regex= (the /id/42 request) and regex_miss= (the /id/abc request). The Host is always shop.envd.test.

There are three kinds of matches — prefix (if the beginning is the same), path (must be exactly the same) and safe_regex (regular expression). The first two are fast, so use regular expressions only when you really need them. When the regular expression does not match, that route is skipped and it keeps going down — the important point is that it is not a 404; the next route accepts it. Write safe_regex in the form { regex: "..." }.

Split the same path by header and by query string

Add two more routes for /api (below the regex routes, above the / route). One uses a headers condition and returns m=header when x-canary: yes, and the other uses a query_parameters condition and returns m=query when debug=1. In /root/envd-route/04-cond.txt, write three lines: header=, query= and plain= (just /api, without conditions).

There are many requirements in practice that cannot be split by path alone — only internal testers get the new version, only requests with a debug query go to a different backend. So a match can carry headers and query_parameters conditions as well as the path, and all the conditions inside one match must be satisfied. If two routes use the same prefix, the one on top is judged first, so put the one with conditions on top.

One route splits traffic across two clusters

Start two upstreams (8082, 8083), create clusters blue and green, and make the /split route send traffic split 75 to 25 through weighted_clusters. Start it with --concurrency 1, request 40 times, and in /root/envd-route/05-weighted.txt write three lines, blue=, green= and total= (the number each upstream received, and the sum).

A weight is a ratio, not a guarantee. And because the state is separate for each worker thread, if you count with the default concurrency (the number of cores), the numbers differ every time — that is why this step must be started with one worker. Start the upstreams with python3 /opt/lab/envoy/upstream.py <포트> ok (the placeholder is the port), and since the response body contains the port, you can count with sort | uniq -c.

Respond without an upstream, and move an old path

Add two routes — path: "/healthz" sends 200 and alive through direct_response, and prefix: "/old" sends a 301 to /new through redirect. In /root/envd-route/06-direct.txt, write three lines: health= (the response body), redirect_code= (the HTTP code) and redirect_url= (the Location header value).

direct_response answers directly from Envoy without going to an upstream — you can finish a health check path or a maintenance notice page at the proxy without putting it in the app. redirect defaults to 302, so for a permanent move you must write response_code: MOVED_PERMANENTLY. You can get both values at once with curl -o /dev/null -w '%{http_code} %{redirect_url}'.

There are three places to attach headers

Add the response header x-level in each of the three layers — route on the route, virtualhost on the virtual host, and routeconfig on the route table. Request /zzz on shop.envd.test, write all the x-level headers that come back on one line, levels=, in /root/envd-route/07-headers.txt, joined by spaces with no commas, and write the count on the count= line.

Header manipulation can be used in three places: the route, the virtual host and the route table. They are not overwritten; each one is added — so several headers with the same name can remain, and a client may see them as one joined by commas. They are applied from the inside (the route) to the outside (the route table). Get only the headers with curl -sI and grep -i x-level.

Summarize it as rules for reading a routing table

In /root/envd-route/08-report.md, write four lines — vhost_order= (the virtual host specificity order in the format exact,suffix,prefix,star), star_limit= (the number of * virtual hosts you can put in one route table), blue_share= (the share blue received in step 5, as an integer percentage) and redirect_code= (the code from step 6) — and below them write what you learned in at least four lines.

Calculate the ratio yourself — it comes from how many of the 40 requests it was. In the explanation lines, write sentences that will rescue you next time, such as "a weight is a ratio, not a guarantee".