Geo-Routing with Apache APISIX – Dev Community

Apache APISIX, the Apache-led API Gateway, comes out of the box with a number of plugins to implement your use case. However, sometimes, the plugin you are looking for is not available. It is always possible to make your own, sometimes it is necessary. Today, I’ll show you how to route users according to their location without writing a single line of Lua code.

Why Geo-Routing?

Geo-routing is the forwarding of HTTP requests based on a user’s physical location, which is inferred from their IP. There are many reasons to do this, and here are some of them.

Note that I will be using country as the location-dependent factor, but any scale small or large works. This is the scale I’m most familiar with – and probably the most useful.

Firstly, most applications are not meant to be geo-dependent. The app your team just developed may only make sense in one country, if not in one region. In this case, geo-routing will never be a problem.

However, some apps grow along with the business. When this happens, the need for internationalization and localization appears. It is the responsibility of the app to handle such geo-dependent factors. i18n should be handled seamlessly by the tech stack, For example:in java. l10n is more For this But there shouldn’t be any problem either.

Problems arise when business rules differ from country to country, mainly because of laws. Other reasons include a partnership. Imagine an e-commerce shop that has branches in many countries. You can choose a delivery partner, but depending on the country, the available partners vary. While having a single codebase is always a wise choice, even the best design can slow the chaos from multiple business rules. At one point, splitting the God app into multiple country-dependent apps is inevitable.

Sometimes, you don’t even have a choice. A country decides that you have to store your database in their region, so you can’t share it anymore and both the storage and the app have to be split. I first saw this with Russia in 2015: We had to deploy a custom version of our e-commerce application just for Russia.

Lastly, you may also want to deploy a new app version for a single country only. In this case, you should be monitoring (not only) technical metrics but business metrics over time. You will then decide whether to expand the new version to other countries based on them or to do more work on the latest version before implementing further.

Setting up Apache APISIX for Ground Routes

Although I am a developer by trade (and passion!), I am pragmatic. I believe that every line of code I don’t write is a line I don’t need to maintain. Apache APISIX does not offer geo-route, but it is built on top of Nginx. The latter provides a geo-route feature, though not by default.

The following instructions are based on Docker to allow everyone to follow them, regardless of their platform.

To set up geo-routing on Apache APISIX we need several steps:

  1. Create a Custom Docker Image
    • Add required library modules
    • add its dependency
  2. Configure Apache APISIX
  3. enjoy!

Nginx geo-route required ngx_http_geoip_module Modulus. But if we try to install it through package manager, it also gets installed nginxwho struggles with nginx Embedded example in Apache APISIX. Since we only need the library, we can get it from the corresponding Docker image:

FROM nginx:1.21.4 as geoiplib

FROM apache/apisix:2.15.0-debian

COPY --from=geoiplib /usr/lib/nginx/modules/ngx_http_geoip_module.so \      #1
                     /usr/local/apisix/modules/ngx_http_geoip_module.so
enter fullscreen mode

exit fullscreen mode

  1. copy library from nginx image to apache/apisix One

The regular package install installs all the dependencies, even the ones we don’t want. Because we only copy the library, we need to install the dependencies manually. It’s straightforward:

RUN apt-get update \
 && apt-get install -y libgeoip1
enter fullscreen mode

exit fullscreen mode

Nginx provides two ways to activate modules: via command line or dynamically. nginx.conf configuration file. The first is impossible because we are not in control, so the latter is our only option. To update the Nginx config file with modules at startup time, Apache APISIX provides a hook in its config file:

nginx_config:
  main_configuration_snippet: |
    load_module     "modules/ngx_http_geoip_module.so";
enter fullscreen mode

exit fullscreen mode

The above will generate the following:

# Configuration File - Nginx Server Configs
# This is a read-only file, do not try to modify it.
master_process on;

worker_processes auto;
worker_cpu_affinity auto;

# main configuration snippet starts
load_module     "modules/ngx_http_geoip_module.so";

...
enter fullscreen mode

exit fullscreen mode

GeoIP module depends on Maxmind GeoIP database. We installed it implicitly in the previous step; We have to configure the module to point to it:

nginx_config:
  http_configuration_snippet: |
    geoip_country   /usr/share/GeoIP/GeoIP.dat;
enter fullscreen mode

exit fullscreen mode

From this point on, every request that passes through Apache APISIX is geo-located. This translates as Nginx adding additional variables. According to the documentation:

The following variables are available when using this database:

$geoip_country_code
A two-letter country code, for example, "RU", "US",
$geoip_country_code3
For example, a three-letter country code, "RUS", "USA",
$geoip_country_name
Country name, for example, "Russian Federation", "United States",

– module ngx_http_geoip_module

ground test

You might assume that the above works – and it does, but I want to prove it.

I have created a dedicated project whose architecture is simple:

  • Apache APISIX is configured as above
  • Two upstream, one in English and one in French
upstreams:
  - id: 1
    type: roundrobin
    nodes:
      "english:8082": 1
  - id: 2
    type: roundrobin
    nodes:
      "french:8081": 1
routes:
  - uri: /
    upstream_id: 1
  - uri: /
    upstream_id: 2
#END
enter fullscreen mode

exit fullscreen mode

With this snippet, each user uses English upstream. I intend to direct the users based in France to the French upstream and the rest to the English ones. For this, we need to configure another route:

routes:
  - uri: /
    upstream_id: 2
    vars: [["geoip_country_code", "==", "FR"]]   #1
    priority: 5                                  #2
enter fullscreen mode

exit fullscreen mode

  1. The magic happens here; see below.
  2. By default, route matching rules are evaluated in an arbitrary order. We need to evaluate this rule first. So we increase the priority – the default is 10.

Most Apache APISIX users are accustomed to matching on routes, methods, and domains, but there’s more to it. One can match on Nginx variables as shown above. In our case, the route matches if geoip_country_code variable is equal to "FR",

note that vars Values ​​readability over power. Use filter_func(vars) attribute if you need more complex logic.

We still can’t test our feature at this point, as we will need to change our IP address. Fortunately, cheating is possible (a little bit), and cheating is helpful in other scenarios. Imagine that Apache APISIX is not directly exposed to the internet but sits behind a reverse proxy. There could be several reasons for this: “history”, a single RP pointing to multiple gateways under the responsibility of different teams, etc.

In this case, the client IP will be the proxy of the RP. To propagate the native client IP, the agreed-on method has to be added: X-Forwarded-For Request HTTP headers:

The X-Forwarded-For (XFF) request header is a de facto standard header for identifying the base IP address of a client connecting to a web server through a proxy server.

When a client connects directly to the server, the client’s IP address is sent to the server (and often written to the server access log). But if a client connection passes through a forward or reverse proxy, the server only sees the IP address of the last proxy, which is often of little use. This is especially true if the final proxy is a load balancer that is part of the same installation as the server. Therefore, to provide the server with a more useful client IP address, X-Forwarded-For The request header is used.

–x-forwarded-for

The nginx module provides this configuration but limits it to the IP range. For testing, we configure this any IP; In production, we should set it to RP IP.

nginx_config:
  http:
    geoip_proxy     0.0.0.0/0;
enter fullscreen mode

exit fullscreen mode

We can finally test the setup:

curl localhost:9080
enter fullscreen mode

exit fullscreen mode

{
  "lang": "en",
  "message": "Welcome to Apache APISIX"
}
enter fullscreen mode

exit fullscreen mode

curl -H "X-Forwarded-For: 212.27.48.10" localhost:9080    #1
enter fullscreen mode

exit fullscreen mode

  1. 212.27.48.10 have a french ip address
{
  "lang": "fr",
  "message": "Bienvenue à Apache APISIX"
}
enter fullscreen mode

exit fullscreen mode

Bonus: Logs and Monitoring

Using the new variable in the Episix log is straightforward. I would recommend it for two reasons:

  • in the beginning to make sure everything is ok
  • In the long run, to monitor traffic, For example:send it to elasticsearch and display it on the kibana dashboard

Just configure it accordingly:

nginx_config:
  http:
    access_log_format: "$remote_addr - $remote_user [$time_local][$geoip_country_code] $http_host \"$request\" $status $body_bytes_sent $request_time \"$http_referer\" \"$http_user_agent\" $upstream_addr $upstream_status $upstream_response_time" #1
enter fullscreen mode

exit fullscreen mode

  1. keep default log variable and add country code

conclusion

Geo-routing is a necessity for successful apps and businesses. Apache APISIX does not provide this out-of-the-box. In this post, I showed how it can still be straightforward to install it using the power of Nginx.

You can find the source code for this post on GitHub:

To go further:

Originally published in A Java Geek on November 6th2022

Leave a Comment