James Thomas

Notes on JavaScript

Location-Based Cloud Foundry Applications Using Nginx and Docker

Routing application traffic based upon the geographic location of incoming requests can be used for a number of scenarios…

  • Restricting access to your application outside defined geographic regions.
  • Load-balancing traffic to the closest region for improved performance.
  • Providing custom applications for different countries.

IBM Bluemix allows deploying applications to different geographic regions through hosting instances of the Cloud Foundry platform in multiple locations.

Cloud Foundry supports simple HTTP routing rules for deployed applications. Organisations can register domains and routes for applications. Routes can be bound to one or more deployed applications. Incoming HTTP traffic is load-balanced, using the Round-Robin policy, between the application instances bound to a route.

However, the platform does not currently support traffic routing based upon the geographic location of incoming requests or sharing domains and routes between regions.

So, say we want to deploy custom versions of an application to different regions and automatically forward users to the correct version based upon their location. How can we achieve this?

Let’s find out…

Deploying Application To Different Regions

IBM Bluemix currently provides Cloud Foundry in two regions for deploying applications.

  • US South (api.ng.bluemix.net)
  • Europe (api.eu-gb.bluemix.net)

Moving between regions is as simple as providing the different region endpoint during the authentication command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[16:25:47 ~]$ cf login -a api.ng.bluemix.net -u james.thomas@uk.ibm.com -s dev
API endpoint: api.ng.bluemix.net

Password>
Authenticating...
OK

Targeted org james.thomas@uk.ibm.com

Targeted space dev

API endpoint:   https://api.ng.bluemix.net (API version: 2.27.0)
User:           james.thomas@uk.ibm.com
Org:            james.thomas@uk.ibm.com
Space:          dev
[16:26:44 ~]$

We’re now authenticated against the US South region.

Let’s start by deploying our sample application, which displays a web page showing the application URL, to this region.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[16:44:24 ~/code/sample]$ cf api
API endpoint: https://api.ng.bluemix.net (API version: 2.27.0)
[16:44:32 ~/code/sample]$ cf push sample-demo-app
Using manifest file /Users/james/code/sample/manifest.yml

Updating app sample-demo-app in org james.thomas@uk.ibm.com / space dev as james.thomas@uk.ibm.com...
OK

...

Showing health and status for app sample-demo-app in org james.thomas@uk.ibm.com / space dev as james.thomas@uk.ibm.com...
OK

requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: sample-demo-app.mybluemix.net
last uploaded: Fri Sep 11 15:45:04 UTC 2015
stack: lucid64
buildpack: SDK for Node.js(TM) (node.js-4.0.0)

     state     since                    cpu    memory          disk        details
#0   running   2015-09-11 04:46:00 PM   0.0%   67.1M of 256M   59M of 1G
[16:45:14 ~/code/sample]$ 

Once that has finished, we can move over to the European region and deploy our application there.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[16:52:33 ~/code/sample]$ cf login -a api.eu-gb.bluemix.net -u james.thomas@uk.ibm.com -s dev
[16:52:58 ~/code/sample]$ cf push sample-demo-app
Using manifest file /Users/james/code/sample/manifest.yml

Updating app sample-demo-app in org james.thomas@uk.ibm.com / space dev as james.thomas@uk.ibm.com...
OK

...

Showing health and status for app sample-demo-app in org james.thomas@uk.ibm.com / space dev as james.thomas@uk.ibm.com...
OK

requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: sample-demo-app.eu-gb.mybluemix.net
last uploaded: Fri Sep 11 15:53:31 UTC 2015
stack: lucid64
buildpack: SDK for Node.js(TM) (node.js-4.0.0)

     state     since                    cpu    memory          disk        details
#0   running   2015-09-11 04:54:17 PM   0.0%   67.4M of 256M   59M of 1G
[16:54:25 ~/code/bluemix/sample]$

With the second deployment completed, there are now instances of the same application running in separate regions.

Each instance is available through a separate URL.

Now we need to set up traffic forwarding from the relevant locations to the correct region.

Reverse Proxy with Region Traffic Forwarding

Due to the platform not supporting multi-region traffic routing, we need to set up a custom reverse proxy. This server will receive requests from our external application domain and transparently forward them onto the correct region application.

We’re going to use Nginx.

Nginx (pronounced engine-x) is a free, open-source, high-performance HTTP server and reverse proxy, as well as an IMAP/POP3 proxy server

Nginx comes with a module for looking up locations associated with IP address using the MaxMind GeoIP library. The module can resolve incoming request addresses into continents, countries and even cities. Using the variables defined by the module, we can write traffic forwarding rules to send requests to the correct region.

Nginx Configuration

Nginx defines two configuration directives, geoip_country and geoip_city, to specify locations for the MaxMind GeoIP database files.

http { 
    ...
    geoip_country /usr/share/GeoIP/GeoIP.dat;
    geoip_city /etc/nginx/geoip/GeoLiteCity.dat;
    ...
}

When configured, Nginx will expose a series of variables for each request with geographical information.

  • $geoip_country_code - two-letter country code, for example, “RU”, “US”.
  • $geoip_country_name - country name, for example, “Russian Federation”, “United States”.
  • $geoip_city_continent_code - two-letter continent code, for example, “EU”, “NA”.
  • $geoip_city - city name, for example, “Moscow”, “Washington”.

Starting with the default nginx configuration, there are only a few modifications needed to set up a reverse proxy based upon location.

For each request, we check the $geoip_city_continent_code against our list of regions. If the request is valid, setting the proxy_pass directive forwards the request onto the correct region. We also overwrite the Host: HTTP header with the region URL. IBM Bluemix uses this header to internally route incoming requests to the correct application host.

Requests coming from outside these locations will be sent to a custom error page.

Due to a known issue with IBM Containers, we must use IP addresses rather than the host names with the proxy_pass directive.

Here is the full configuration for the enabled-site/default file.

server {
  listen 80 default_server;
  listen [::]:80 default_server ipv6only=on;

  root /usr/share/nginx/html;
  index index.html index.htm;
  error_page 404 /404.html;

# Make site accessible from http://localhost/
  server_name localhost;

  location = /404.html {
    internal;
  }

  location / {
    set $host_header "unknown";

    if ($geoip_city_continent_code = "EU") { 
      proxy_pass http://5.10.124.141;
      set $host_header "sample-demo-app.eu-gb.mybluemix.net";
    }

    if ($geoip_city_continent_code = "NA") { 
      proxy_pass http://75.126.81.66;
      set $host_header "sample-demo-app.mybluemix.net";
    }

    if ($host_header = "unknown") {
      return 404;
    }

    proxy_set_header Host $host_header;
  }
}

With the reverse proxy server configured, we need to provision a new production server, install Linux and Nginx, configure networking, security updates and backup services…

…or we can use Docker.

Running Nginx using Docker

There are thousands of repositories on Docker Hub providing Nginx, including the official image. Unfortunately, the official image provides a version of Nginx that is not built with the geo_ip module.

Ubuntu’s default package repository for Nginx does provide a build including the geo_ip module. By modifying the Dockerfile for the official image, we can build a new image from Ubuntu with the required version of Nginx and include our custom configuration files.

FROM ubuntu
RUN apt-get -y install nginx

# copy custom configuration
COPY nginx.conf /etc/nginx/nginx.conf
COPY default /etc/nginx/sites-available/
COPY geoip /etc/nginx/geoip
COPY 404.html /usr/share/nginx/html/

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log
RUN ln -sf /dev/stderr /var/log/nginx/error.log

# expose HTTP and HTTP ports
EXPOSE 80 443

CMD ["nginx", "-g", "daemon off;"]

Building and running this container locally, we can test that Nginx is configured correctly. The repository containing the Dockerfile and build artificats is located here.

1
2
3
4
5
6
7
8
9
10
[16:58:40 ~/code/final]$ docker build -t geo_ip .
Sending build context to Docker daemon 15.88 MB
Step 0 : FROM ubuntu
 ---> 91e54dfb1179
...
Step 9 : CMD nginx -g daemon off;
 ---> Using cache
 ---> 7bb6dbaafe3e
Successfully built 7bb6dbaafe3e
[16:58:50 ~/code/final]$ docker run -Pti geo_ip

With the custom image ready, we just need to deploy it somewhere…

Running Nginx on IBM Containers

IBM Bluemix supports deploying Docker containers alongside Cloud Foundry applications, allowing us to use the same cloud platform for running our custom region applications as providing the reverse proxy

Pushing pre-built images to the IBM Containers service is really as simple as creating a new tag and typing docker push.

Please read and follow the documentation about installing the command-line container management tools and authenticating with the remote service before attempting the commands below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[14:10:52 ~]$ docker tag geo_ip registry.ng.bluemix.net/jthomas/geo_ip
[14:10:59 ~]$ docker images
REPOSITORY                               TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
geo_ip                                   latest              7bb6dbaafe3e        3 days ago          222.3 MB
registry.ng.bluemix.net/jthomas/geo_ip   latest              7bb6dbaafe3e        3 days ago          222.3 MB
[14:11:07 ~]$ cf ic login
** Retrieving client certificates from IBM Containers
** Storing client certificates in /Users/james/.ice/certs
Successfully retrieved client certificates
** Authenticating with registry at registry.eu-gb.bluemix.net
Successfully authenticated with registry
[14:24:25 ~]$ docker push registry.ng.bluemix.net/jthomas/geo_ip
The push refers to a repository [registry.ng.bluemix.net/jthomas/geo_ip] (len: 1)
Sending image list
Pushing repository registry.ng.bluemix.net/jthomas/geo_ip (1 tags)
...
Pushing tag for rev [7bb6dbaafe3e] on {https://registry.ng.bluemix.net/v1/repositories/jthomas/geo_ip/tags/latest}
[14:25:39 ~]$ cf ic images
REPOSITORY                                        TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
registry.ng.bluemix.net/jthomas/geo_ip            latest              7b1865be-778        About a minute ago   0 B
registry.ng.bluemix.net/ibmliberty                latest              2209a9732f35        3 weeks ago          263.6 MB
registry.ng.bluemix.net/ibmnode                   latest              8f962f6afc9a        3 weeks ago          178.9 MB
registry.ng.bluemix.net/ibm-mobilefirst-starter   latest              97513e56aaa7        3 weeks ago          464.9 MB
[14:26:43 ~]$ 

We can now use the IBM Bluemix dashboard to start a new container from our custom image, binding a public IP address and exposing ports.

Once the container starts, accessing the bound IP address shows the web page coming back with the region-specific application route.

Using DNS A records, we can now map our external URL to the IP address of the container. Users visiting this URL will be sent to the reverse proxy server which will then forward the request onto the correct region application.

Testing it all out…

Testing out the forwarding rules requires us to send HTTP requests from multiple regions. GeoWebView will run web browsers located in different geographies and show you the rendered page output.

Running the tool with our application’s web address, shows the following rendered page images.

We can see the browsers from the United States and Europe are sent to the correct region. The browser from South Africa is shown the custom error page.

Using Nginx we’ve configured a reverse proxy to route users, based upon their location, to applications running in different IBM Bluemix regions. We’re hosting the service on the same platform as our applications, using Docker. Most importantly, the whole process is transparent to the user, they aren’t forced to visit country-specific URLs.

Success!

Comments