├── .gitignore ├── Makefile ├── README.md ├── app ├── Dockerfile ├── nginx.conf └── services │ ├── authentication.conf │ └── inbox.conf ├── docker-compose.yml └── spec ├── app ├── .rspec ├── Dockerfile ├── Gemfile ├── Gemfile.lock └── spec │ ├── app_spec.rb │ ├── expected_routes.yml │ └── spec_helper.rb └── dummy_service ├── Dockerfile ├── Gemfile ├── Gemfile.lock └── config.ru /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | docker-compose kill 3 | docker-compose build 4 | docker-compose up -d gateway 5 | docker-compose run tester rspec spec 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NGINX API Gateway 2 | ================= 3 | 4 | This repo is an example of how to create an NGINX proxy as a gateway to your 5 | micro services. 6 | 7 | There's a [detailed blog post](https://engineering.nanit.com/testing-nginx-as-your-micro-services-gateway-7e8c71fcdfa6) about it. 8 | 9 | The important thing is that you can run tests to verify that the gateway routing 10 | is done correctly, meaning a request is routed to a specified service with a 11 | specified path. 12 | 13 | For example, you can verify that an HTTP call to http://api.mycompany.com/login is 14 | routed to your authentication service with the /login path. 15 | 16 | Running The Test 17 | ---------------- 18 | 19 | Make sure docker and docker-compose are installed on your machine. 20 | 21 | Clone this repository: 22 | 23 | ```bash 24 | $ git clone git@github.com:nanit/api-gateway-example.git 25 | ``` 26 | 27 | And run the tests: 28 | 29 | ```bash 30 | $ cd api-gateway-example && make test 31 | ``` 32 | 33 | All tests should pass which means that: 34 | 35 | 1. A request to http://your.gateway.com/login is routed to the authentication service 36 | with the /login path 37 | 2. A request to http://your.gateway.com/messages is routed to the inbox 38 | service with the /messages path 39 | 40 | Project Structure 41 | ----------------- 42 | 43 | 1. [spec/app/spec/expected_routes.yml](https://github.com/nanit/api-gateway-example/blob/master/spec/app/spec/expected_routes.yml) - the route specs. Each route has an expected destination service name and an expected destination path. 44 | 45 | 2. `app/services/.conf` - per-service routes configuration. 46 | 47 | 3. [app/nginx.conf](https://github.com/nanit/api-gateway-example/blob/master/app/nginx.conf) - the main nginx configuration file. 48 | 49 | 4. [docker-compose.yml](https://github.com/nanit/api-gateway-example/blob/master/docker-compose.yml) - add new services here. 50 | 51 | You should only change these 4 to fit your specific needs. The rest of the 52 | files may remain untouched. 53 | 54 | 55 | Adding a New Route 56 | ------------------ 57 | 58 | 1. Add the route to [spec/app/spec/expected_routes.yml](https://github.com/nanit/api-gateway-example/blob/master/spec/app/spec/expected_routes.yml) 59 | 60 | 2. Run `make test` in the root directory and see the tests fail. 61 | 62 | 3. Add the proper NGINX configuration. 63 | 64 | 4. Run `make test` again in the root directory and (hopefully) see the tests pass. 65 | 66 | Adding a New Micro Service 67 | -------------------------- 68 | 69 | 1. Add the service to [docker-compose.yml](https://github.com/nanit/api-gateway-example/blob/master/docker-compose.yml). Make sure the SERVICE_NAME environment variable is set properly, and that the gateway is linked to it. 70 | 71 | 2. Add your service specs to [spec/app/spec/expected_routes.yml](https://github.com/nanit/api-gateway-example/blob/master/spec/app/spec/expected_routes.yml) 72 | 73 | 3. Run `make test` in the root directory to see the specs fail. 74 | 75 | 4. Add `app/services/new_service.conf` with the proper routing to your new service. 76 | 77 | 5. Run `make test` in the root directory again and (hopefully again) see the 78 | tests pass. 79 | 80 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY nginx.conf /etc/nginx/nginx.conf 3 | RUN rm -rf /etc/nginx/conf.d/* 4 | COPY services/* /etc/nginx/conf.d/ 5 | EXPOSE 80 6 | -------------------------------------------------------------------------------- /app/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | 7 | access_log /var/log/nginx/access.log; 8 | error_log /var/log/nginx/error.log; 9 | 10 | server { 11 | listen 80; 12 | server_name _; 13 | 14 | include /etc/nginx/conf.d/*.conf; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/services/authentication.conf: -------------------------------------------------------------------------------- 1 | location /login { 2 | proxy_set_header Host $http_host; 3 | proxy_redirect off; 4 | proxy_pass http://authentication; 5 | } 6 | 7 | location /register { 8 | proxy_set_header Host $http_host; 9 | proxy_redirect off; 10 | proxy_pass http://authentication; 11 | } 12 | -------------------------------------------------------------------------------- /app/services/inbox.conf: -------------------------------------------------------------------------------- 1 | location /messages { 2 | proxy_set_header Host $http_host; 3 | proxy_redirect off; 4 | proxy_pass http://inbox; 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | authentication: 2 | build: spec/dummy_service 3 | environment: 4 | SERVICE_NAME: authentication 5 | inbox: 6 | build: spec/dummy_service 7 | environment: 8 | SERVICE_NAME: inbox 9 | 10 | gateway: 11 | build: app 12 | links: 13 | - authentication 14 | - inbox 15 | ports: 16 | - 80 17 | 18 | tester: 19 | build: spec/app 20 | links: 21 | - gateway 22 | -------------------------------------------------------------------------------- /spec/app/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /spec/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.3.0-onbuild 2 | 3 | CMD ["rspec", "spec"] 4 | -------------------------------------------------------------------------------- /spec/app/Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gem 'rspec' 5 | gem 'httparty' 6 | -------------------------------------------------------------------------------- /spec/app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.5.0) 5 | httparty (0.21.0) 6 | mini_mime (>= 1.0.0) 7 | multi_xml (>= 0.5.2) 8 | mini_mime (1.1.2) 9 | multi_xml (0.6.0) 10 | rspec (3.12.0) 11 | rspec-core (~> 3.12.0) 12 | rspec-expectations (~> 3.12.0) 13 | rspec-mocks (~> 3.12.0) 14 | rspec-core (3.12.1) 15 | rspec-support (~> 3.12.0) 16 | rspec-expectations (3.12.2) 17 | diff-lcs (>= 1.2.0, < 2.0) 18 | rspec-support (~> 3.12.0) 19 | rspec-mocks (3.12.3) 20 | diff-lcs (>= 1.2.0, < 2.0) 21 | rspec-support (~> 3.12.0) 22 | rspec-support (3.12.0) 23 | 24 | PLATFORMS 25 | ruby 26 | 27 | DEPENDENCIES 28 | httparty 29 | rspec 30 | 31 | BUNDLED WITH 32 | 1.17.2 33 | -------------------------------------------------------------------------------- /spec/app/spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'yaml' 3 | 4 | expected_routes = YAML::load_file(File.join(File.dirname(File.expand_path(__FILE__)), 'expected_routes.yml')) 5 | 6 | expected_routes.each do |route, expected| 7 | RSpec.describe route do 8 | it {is_expected.to route_to_service(expected["service"]).with_path(expected["path"])} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/app/spec/expected_routes.yml: -------------------------------------------------------------------------------- 1 | /login: 2 | service: authentication 3 | path: /login 4 | /messages: 5 | service: inbox 6 | path: /messages 7 | -------------------------------------------------------------------------------- /spec/app/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | require 'json' 3 | RSpec.configure do |config| 4 | config.expect_with :rspec do |expectations| 5 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 6 | end 7 | config.mock_with :rspec do |mocks| 8 | mocks.verify_partial_doubles = true 9 | end 10 | end 11 | 12 | RSpec::Matchers.define :route_to_service do |service| 13 | match do |route| 14 | resp = HTTParty.get("http://gateway#{route}") 15 | return false if resp.code != 200 16 | actual = JSON.parse(resp.body) 17 | return actual["service"] == service && actual["path"] == path 18 | end 19 | 20 | chain :with_path, :path 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy_service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.3.0-onbuild 2 | 3 | EXPOSE 80 4 | 5 | CMD ["rackup", "-p", "80", "-o", "0.0.0.0"] 6 | -------------------------------------------------------------------------------- /spec/dummy_service/Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gem "rack" 5 | -------------------------------------------------------------------------------- /spec/dummy_service/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rack (3.0.6.1) 5 | 6 | PLATFORMS 7 | ruby 8 | 9 | DEPENDENCIES 10 | rack 11 | 12 | BUNDLED WITH 13 | 1.17.2 14 | -------------------------------------------------------------------------------- /spec/dummy_service/config.ru: -------------------------------------------------------------------------------- 1 | require 'json' 2 | run Proc.new { |env| 3 | ['200', {'Content-Type' => 'application/json'}, 4 | [{service: ENV["SERVICE_NAME"], 5 | path: env["REQUEST_URI"].match(/http:\/\/[^\/]+(.*)$/)[1]}.to_json]] 6 | } 7 | --------------------------------------------------------------------------------