Laying paths to microservices


One of the most important tasks when dividing a system into microservices is to provide a reliable mechanism for their replication and discovery and create a set of rules to route incoming requests to the appropriate containers or network nodes. The ideal system should also be able to monitor availability status and exclude unreachable replicas from routing. In this article, we will talk about using a router Kongwhich takes on not only the tasks of smart routing, but also the ability to log and transform requests, access control, request monitoring, and can also be extended using plugins.

The Kong Gateway router exists in a free and commercial version, can be run as a standalone application (for example, through a docker container Kong), or installed in Kubernetes (in which case it appears as a specialized Ingress controller). Kong can be managed using both API requests and the Kong Manager web interfaces, or Konga. Many extensions are preinstalled in the official container, but they can also be found in Kong Plugin Hub. We will consider installation and configuration using the example of a simple application consisting of three microservices (one of which represents a public interface without authorization, for one we will set complex routing rules, and the third will require the mandatory use of a token to access connection points).

First, let’s draw up the topology of our system and select microservices:

  • search service for location and extended information about the city by country and name (using the Chercheville application on Elixir, available on docker hub);

  • service for obtaining information from the profile of an authorized user;

  • service for performing authorization and obtaining a token.

Each service will have its own database (or third-party data source) and all interaction between systems will be done through an API gateway, which we will configure using the Kong API Gateway.

Kong uses a PostgreSQL database to store its configuration, or it can be run with services and bindings defined in a json/yaml file. We will consider the option of launching with a database. To coordinate the launch, we will use Docker Compose, which can be found in the official repository: https://github.com/Kong/docker-kong (path /compose).

docker compose --profile database up -d

After performing the initial database migrations, the main process starts and starts listening on several TCP ports:

  • 8000/8443 – HTTP/HTTPS traffic for routing to servers;

  • 8001/8444 – HTTP/HTTPS ports for sending administrative requests (also used by the Konga web interface for managing resources);

  • 8002/8445 – HTTP/HTTPS connection to Kong Manager (web interface) must be additionally allowed and published via docker-compose.yaml.

Before setting up routes, you need to understand the terms used in Kong:

  • Service – backend service configuration (http/https protocol, host address, port, number of connection attempts and timeouts, client certificate identifier when using https). Plugins for audit/transformation of requests can be connected to the service configuration, as well as valid consumers (consumer) can be defined.

  • Route – a rule for routing incoming requests, includes enumerations of related services (there may be more than one for fault tolerance) and a group of filters (for http protocols) or a set of rules for tcp-stream routing. A request processing plugin can also be added to each route.

    • domain name (Hosts);

    • path prefixes (Paths);

    • HTTP methods (Methods);

    • protocols (http / https);

    • domain check for tcp-stream routing (SNI);

    • request sources for tcp-stream (sources);

    • request recipients for tcp-stream (destinations).

  • Certificate – registered client certificates for connecting to https backend services (a unique id is generated for each, which is used when configuring the service).

  • Upstream – a rule for forwarding http/https traffic to one or more services. After creating and configuring upstream, it becomes possible to add targets (ip / dns addresses of target servers, ports and a weight value that affects the probability of choosing a server when using the none hash rule), as well as Alert to send notifications to administrators by mail or Slack when target server unavailability:

    • Hash on – hashing method for selecting a target service from the pool (may be consumer, ip, cookie, header, none). In the case of none, a sequential selection of services is used for uniform distribution;

    • Hash fallback – an alternative hashing method if the main one did not return a result (for example, there was no cookie or header);

    • Active Health Check – availability check configuration (http method, path, codes of successful and unsuccessful responses, number of check attempts, check interval);

  • Consumer – registered authorized users of the services and their authorization method. For Consumer, a Credentials set is defined – this can be Basic Auth (name and password), API Key, Hash-based message authentication code (HMAC) – name and secret, OAuth2 (application id and secret, redirect uri), JSON Web Token (JWT ).

To manage resources, you can use http requests to administration port 8001 (or 8444 for https requests):

  • http://ip:8001/services – viewing the list/registration of a new service;

  • http://ip:8001/routes – viewing/creating a route;

  • http://ip:8001/consumers – management of authorized users;

  • http://ip:8001/upstreams – change of proxying rules with availability check (upstream);

  • http://ip:8001/certificates – management of client certificates;

  • http://ip:8001 – View information about a running kong instance.

For ease of access, you can create a route to admin api through the main port (8000):

curl -X POST http://127.0.0.1:8001/services \
  --data name=admin-api \
  --data host=127.0.0.1 \
  --data port=8001

curl -X POST http://127.0.0.1:8001/services/admin-api/routes \
  --data paths[]=/admin-api

And then it will be possible to access the administration service through http://localhost:8000/admin-api/. Having an API for administration allows you to self-register the service and route at startup, but now we have access to the API without authorization, which cannot be called a secure solution. Let’s create a new consumer with an APIKey and assign it to access the api route:

curl -i -X POST \
  --url http://localhost:8001/services/admin-api/plugins/ \
  --data 'name=key-auth'

curl -i -X POST \
  --url http://localhost:8000/admin-api/consumers/ \
  --data "username=admin"
  
curl -i -X POST \
  --url http://localhost:8000/admin-api/consumers/admin/key-auth/ \
  --data 'key=6Y6fKCsFWWWSRMXGyUWs8g9q'

Now the API will only be accessed using the apikey header: 6Y6fKCsFWWWSRMXGyUWs8g9q.

In addition to authorization plugins, you can add the following extensions:

  • session – session tracking between requests (most often using cookies);

  • bot detection – detection of bot activity;

  • cors – cors policy management;

  • ip restriction – restriction of access according to white and black lists;

  • acme – integration with letsencrypt for automatic renewal of certificates;

  • rate limiting – limiting the number of requests per second;

  • response ratelimiting – limiting the number of responses per second;

  • request size limiting – limiting the size of the request;

  • proxy cache – response caching;

  • pre function – run lua-function before request processing;

  • post function – run lua-function after request processing;

  • aws lambda – run an external function with AWS when processing a rule;

  • azure functions – run an Azure function when processing a rule;

  • datadog – send query metrics to datadog;

  • prometheus – send metrics according to the rules to prometheus;

  • zipkin – sending labels for processing rules for distributed zipkin tracing;

  • request transformer – changing the request before passing it to the upstream server;

  • response transformer – changing the response of the upstream server;

  • correlation ID – tracking communication in the chain of shipments;

  • tcp/udp/http log – sending information about the request and response to the logging server via tcp/udp/http;

  • file log – writing information about the request/response to a file;

  • syslog – sending a request and response to the syslog system process;

  • statsd – sending statistics on request processing to statsd;

  • loggly – forwarding request/response information to loggly.

If you need to create your own extensions, you can use Python Develop Kit.

For management, you can also install konga to configure all resources through the web interface:

docker network create konga
docker rm -f konga-db
docker run -d --network=konga --name konga-db -v /data/mongo:/data/db mongo
docker rm -f konga2
docker run -d --network=konga --name konga -e NODE_ENV=production -e "TOKEN_STRING=fdoER#2sa" -e DB_ADAPTER=mongo -e DB_HOST=konga-db -e DB_DATABASE=konga --network=konga -p 443:1337 -v /data/konga:/data -e SSL_KEY_PATH=/data/cert.key -e SSL_CRT_PATH=/data/cert.crt pantsel/konga

When making the first connection, you will need to configure the Kong API address and then you can connect to https and perform similar settings through the web interface:

Kong interface
Kong interface

Now let’s start our routing service and bind its methods to the /place prefix. For this we clone repository and run docker compose up -d. After launch, it will be necessary to import data by country:

docker exec -ti chercheville-app-1 sh
./bin/chercheville rpc 'ChercheVille.SeedData.import_data(["FR"])'

To check the availability of the service, execute the request http://localhost:5000/cities/?q=Paris. Now let’s bind the service and route via konga:

Here Host is the external address of the server (alternatively, you can use the DNS name on the internal network or through the Service Discovery used, for example Consul). Next, you need to configure the binding of the service to an external route:

Strip Path allows you to transform the address and remove the /places prefix when forwarding a request. Now the service will be available through the address http://localhost:8000/places/ and it will be possible to make a request between microservices or from an external client through the API Gateway.

The next step is to self-register the authorization service in kong, which will also manage the Consumer to access the profile service:

import requests
import socket
from flask import Flask

gateway = "http://localhost:8000"
kong_api = f"{gateway}/admin-api"
headers = {
    "apikey": "6Y6fKCsFWWWSRMXGyUWs8g9q"
}

hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)

# саморегистрация сервиса
requests.post(f"{kong_api}/services", headers=headers, data={
    "name": "authorizer",
    "url": "http://{local_ip}:10080"
})
requests.post(f"{kong_api}/services/authorizer/routes", data={
    "paths[]=/auth"
})

app = Flask(__name__)


@app.route("/login")
def login():
    # extract and check login/password
    login = "login"
    requests.post(f"{kong_api}/consumers", data={"username": login})  # регистрируем consumer


@app.route("/logout")
def logout():
    requests.delete(f"{kong_api}/consumers/{login}")


app.run(port=10080)

When registering the profile service, you need to add the key-auth plugin to check the token when accessing profile addresses. Upon authorization, the X-Consumer-ID header will be added to store the ID that corresponds to the presented token.

import requests
import socket
import json
import flask

gateway = "http://localhost:8000"
kong_api = f"{gateway}/admin-api"
headers = {
    "apikey": "6Y6fKCsFWWWSRMXGyUWs8g9q"
}

hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)

# саморегистрация сервиса
requests.post(f"{kong_api}/services", headers=headers, data={
    "name": "profile",
    "url": "http://{local_ip}:9080"
})
requests.post(f"{kong_api}/services/profile/routes", data={
    "paths[]=/profile"
})
requests.post(f"{kong_api}/services/profile/plugins", data={
    "name=key-auth"
})
app = flask.Flask(__name__)


@app.route("/profile")
def login():
  consumers = json.loads(requests.get(f"{kong_api}/consumers"))
  # определение имени (идентификатора) пользователя
  # список содержит id (consumer id) и имя пользователя (name)
  id = flask.request.headers.get("X-Consumer-ID")
  username = None
  for c in consumers:
    if c["id"]==id:
       username = c["username"]
  if not username:
    flask.abort(403, "Not authorized")
  return f"Logged user: {username}"


app.run(port=9080)

Similarly, you can configure the removal of services and routes when the application ends. Thus, using the Kong API gateway, you can dynamically change the binding of routes to service instances, as well as solve typical tasks of access control and obtaining metrics by request execution time.


I would like to invite everyone who is interested in the topic of microservices to a free lesson on the topic: “Authorization and authentication in microservice architecture”. Registration is available via the link below.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *