1. Введение
Kong — это шлюз API с открытым исходным кодом и уровень управления микросервисами.
Основанная на Nginx и модуле lua-nginx (в частности , OpenResty ), подключаемая архитектура Kong делает его гибким и мощным.
2. Ключевые концепции
Прежде чем мы углубимся в примеры кода, давайте взглянем на ключевые концепции Kong:
- Объект API — упаковывает свойства любой конечной точки HTTP (s), которая выполняет определенную задачу или предоставляет некоторую услугу. Конфигурации включают в себя методы HTTP, URI конечной точки, восходящий URL-адрес, который указывает на наши серверы API и будет использоваться для проксирования запросов, максимальное количество отказов, ограничения скорости, тайм-ауты и т. д.
- Потребительский объект — оборачивает свойства любого, кто использует наши конечные точки API. Он будет использоваться для отслеживания, контроля доступа и многого другого.
- Upstream Object — описывает, как входящие запросы будут проксироваться или балансироваться по нагрузке, представленному именем виртуального хоста.
- Целевой объект — представляет службы, которые реализованы и обслуживаются, идентифицируемые по имени хоста (или IP-адресу) и порту. Обратите внимание, что цели каждого восходящего потока можно только добавлять или отключать. История целевых изменений поддерживается вышестоящим
- Plugin Object — подключаемые функции для расширения функциональных возможностей нашего приложения в течение жизненного цикла запросов и ответов. Например, функции аутентификации API и ограничения скорости можно добавить, включив соответствующие плагины. Kong предоставляет очень мощные плагины в своей галерее плагинов.
- Admin API — конечные точки RESTful API, используемые для управления конфигурациями Kong, конечными точками, потребителями, плагинами и т. д.
На рисунке ниже показано, чем Kong отличается от устаревшей архитектуры, что может помочь нам понять, почему он ввел эти концепции:
(источник: https://getkong.org/)
3. Настройка
Официальная документация содержит подробные инструкции для различных сред.
4. Управление API
После локальной настройки Kong давайте оценим мощные функции Kong, проксировав нашу простую конечную точку запроса акций:
@RestController
@RequestMapping("/stock")
public class QueryController {
@GetMapping("/{code}")
public String getStockPrice(@PathVariable String code){
return "BTC".equalsIgnoreCase(code) ? "10000" : "0";
}
}
4.1. Добавление API
Далее давайте добавим наш API запросов в Kong.
API-интерфейсы администратора доступны через http://localhost:8001
, поэтому все наши операции по управлению API будут выполняться с этим базовым URI:
APIObject stockAPI = new APIObject(
"stock-api", "stock.api", "http://localhost:8080", "/");
HttpEntity<APIObject> apiEntity = new HttpEntity<>(stockAPI);
ResponseEntity<String> addAPIResp = restTemplate.postForEntity(
"http://localhost:8001/apis", apiEntity, String.class);
assertEquals(HttpStatus.CREATED, addAPIResp.getStatusCode());
Здесь мы добавили API со следующей конфигурацией:
{
"name": "stock-api",
"hosts": "stock.api",
"upstream_url": "http://localhost:8080",
"uris": "/"
}
«имя»
— это идентификатор API, используемый при управлении его поведением.«hosts»
будет использоваться для маршрутизации входящих запросов на заданный«upstream_url»
путем сопоставления заголовка«Host» .
- Относительные пути будут сопоставляться с настроенным «uris»
Если мы хотим объявить API устаревшим или конфигурация неверна, мы можем просто удалить его:
restTemplate.delete("http://localhost:8001/apis/stock-api");
После добавления API они будут доступны для использования через http://localhost:8000
:
String apiListResp = restTemplate.getForObject(
"http://localhost:8001/apis/", String.class);
assertTrue(apiListResp.contains("stock-api"));
HttpHeaders headers = new HttpHeaders();
headers.set("Host", "stock.api");
RequestEntity<String> requestEntity = new RequestEntity<>(
headers, HttpMethod.GET, new URI("http://localhost:8000/stock/btc"));
ResponseEntity<String> stockPriceResp
= restTemplate.exchange(requestEntity, String.class);
assertEquals("10000", stockPriceResp.getBody());
В приведенном выше примере кода мы пытаемся запросить цену акций через API, который мы только что добавили в Kong.
Запрашивая http://localhost:8000/stock/btc
, мы получаем тот же сервис, что и запрос напрямую с http://localhost:8080/stock/btc
.
4.2. Добавление потребителя API
Давайте теперь поговорим о безопасности, а точнее об аутентификации пользователей, обращающихся к нашему API.
Давайте добавим потребителя в наш стандартный API запросов, чтобы позже мы могли включить функцию аутентификации.
Добавить потребителя для API так же просто, как добавить API. Имя потребителя (или идентификатор) является единственным обязательным полем всех свойств потребителя:
ConsumerObject consumer = new ConsumerObject("foreach");
HttpEntity<ConsumerObject> addConsumerEntity = new HttpEntity<>(consumer);
ResponseEntity<String> addConsumerResp = restTemplate.postForEntity(
"http://localhost:8001/consumers/", addConsumerEntity, String.class);
assertEquals(HttpStatus.CREATED, addConsumerResp.getStatusCode());
Здесь мы добавили «foreach» в качестве нового потребителя:
{
"username": "foreach"
}
4.3. Включение аутентификации
А вот и самая мощная функция Kong — плагины.
Теперь мы собираемся применить плагин авторизации к нашему API запроса акций через прокси:
PluginObject authPlugin = new PluginObject("key-auth");
ResponseEntity<String> enableAuthResp = restTemplate.postForEntity(
"http://localhost:8001/apis/stock-api/plugins",
new HttpEntity<>(authPlugin),
String.class);
assertEquals(HttpStatus.CREATED, enableAuthResp.getStatusCode());
Если мы попытаемся запросить цену акции через прокси-URI, запрос будет отклонен:
HttpHeaders headers = new HttpHeaders();
headers.set("Host", "stock.api");
RequestEntity<String> requestEntity = new RequestEntity<>(
headers, HttpMethod.GET, new URI("http://localhost:8000/stock/btc"));
ResponseEntity<String> stockPriceResp = restTemplate
.exchange(requestEntity, String.class);
assertEquals(HttpStatus.UNAUTHORIZED, stockPriceResp.getStatusCode());
Помните, что ForEach
является одним из наших потребителей API, поэтому мы должны разрешить ему использовать этот API, добавив ключ аутентификации:
String consumerKey = "foreach.pass";
KeyAuthObject keyAuth = new KeyAuthObject(consumerKey);
ResponseEntity<String> keyAuthResp = restTemplate.postForEntity(
"http://localhost:8001/consumers/foreach/key-auth",
new HttpEntity<>(keyAuth),
String.class);
assertTrue(HttpStatus.CREATED == keyAuthResp.getStatusCode());
Затем ForEach
может использовать этот API, как и раньше:
HttpHeaders headers = new HttpHeaders();
headers.set("Host", "stock.api");
headers.set("apikey", consumerKey);
RequestEntity<String> requestEntity = new RequestEntity<>(
headers,
HttpMethod.GET,
new URI("http://localhost:8000/stock/btc"));
ResponseEntity<String> stockPriceResp = restTemplate
.exchange(requestEntity, String.class);
assertEquals("10000", stockPriceResp.getBody());
5. Расширенные функции
Помимо базового API-прокси и управления, Kong также поддерживает балансировку нагрузки API, кластеризацию, проверку работоспособности, мониторинг и т. д.
В этом разделе мы рассмотрим, как сбалансировать запросы с помощью Kong и как защитить API-интерфейсы администратора.
5.1. Балансировки нагрузки
Kong предлагает две стратегии балансировки нагрузки запросов к серверным службам: динамический балансировщик колец и простой метод на основе DNS. Для простоты мы будем использовать балансировщик колец .
Как мы упоминали ранее, восходящие потоки используются для балансировки нагрузки, и каждый восходящий поток может иметь несколько целей.
Kong поддерживает алгоритмы балансировки как взвешенного кругового перебора, так и алгоритмы балансировки на основе хэшей. По умолчанию используется схема взвешенного циклического перебора, когда запросы доставляются каждой цели в соответствии с их весом.
Во-первых, давайте подготовим восходящий поток:
UpstreamObject upstream = new UpstreamObject("stock.api.service");
ResponseEntity<String> addUpstreamResp = restTemplate.postForEntity(
"http://localhost:8001/upstreams",
new HttpEntity<>(upstream),
String.class);
assertEquals(HttpStatus.CREATED, addUpstreamResp.getStatusCode());
Затем добавьте две цели для основной ветки: тестовую версию с весом=10
и релизную версию с весом=40
:
TargetObject testTarget = new TargetObject("localhost:8080", 10);
ResponseEntity<String> addTargetResp = restTemplate.postForEntity(
"http://localhost:8001/upstreams/stock.api.service/targets",
new HttpEntity<>(testTarget),
String.class);
assertEquals(HttpStatus.CREATED, ddTargetResp.getStatusCode());
TargetObject releaseTarget = new TargetObject("localhost:9090",40);
addTargetResp = restTemplate.postForEntity(
"http://localhost:8001/upstreams/stock.api.service/targets",
new HttpEntity<>(releaseTarget),
String.class);
assertEquals(HttpStatus.CREATED, addTargetResp.getStatusCode());
С приведенной выше конфигурацией мы можем предположить, что 1/5 запросов пойдет на тестовую версию, а 4/5 — на релизную:
APIObject stockAPI = new APIObject(
"balanced-stock-api",
"balanced.stock.api",
"http://stock.api.service",
"/");
HttpEntity<APIObject> apiEntity = new HttpEntity<>(stockAPI);
ResponseEntity<String> addAPIResp = restTemplate.postForEntity(
"http://localhost:8001/apis", apiEntity, String.class);
assertEquals(HttpStatus.CREATED, addAPIResp.getStatusCode());
HttpHeaders headers = new HttpHeaders();
headers.set("Host", "balanced.stock.api");
for(int i = 0; i < 1000; i++) {
RequestEntity<String> requestEntity = new RequestEntity<>(
headers, HttpMethod.GET, new URI("http://localhost:8000/stock/btc"));
ResponseEntity<String> stockPriceResp
= restTemplate.exchange(requestEntity, String.class);
assertEquals("10000", stockPriceResp.getBody());
}
int releaseCount = restTemplate.getForObject(
"http://localhost:9090/stock/reqcount", Integer.class);
int testCount = restTemplate.getForObject(
"http://localhost:8080/stock/reqcount", Integer.class);
assertTrue(Math.round(releaseCount * 1.0 / testCount) == 4);
Обратите внимание, что схема взвешенного циклического перебора уравновешивает запросы к серверным службам приблизительно в соответствии с соотношением весов, поэтому можно проверить только приблизительное соотношение, отраженное в последней строке приведенного выше кода.
5.2. Защита API администратора
По умолчанию Kong принимает административные запросы только от локального интерфейса, что в большинстве случаев является достаточным ограничением. Но если мы хотим управлять им через другие сетевые интерфейсы, мы можем изменить значение admin_listen
в kong.conf
и настроить правила брандмауэра.
Или мы можем заставить Kong служить прокси для самого Admin API. Допустим, мы хотим управлять API с помощью пути «/admin-api», мы можем добавить такой API:
APIObject stockAPI = new APIObject(
"admin-api",
"admin.api",
"http://localhost:8001",
"/admin-api");
HttpEntity<APIObject> apiEntity = new HttpEntity<>(stockAPI);
ResponseEntity<String> addAPIResp = restTemplate.postForEntity(
"http://localhost:8001/apis",
apiEntity,
String.class);
assertEquals(HttpStatus.CREATED, addAPIResp.getStatusCode());
Теперь мы можем использовать прокси-API администратора для управления API:
HttpHeaders headers = new HttpHeaders();
headers.set("Host", "admin.api");
APIObject foreachAPI = new APIObject(
"foreach-api",
"foreach.com",
"http://ww.foreach.com",
"/");
RequestEntity<APIObject> requestEntity = new RequestEntity<>(
foreachAPI,
headers,
HttpMethod.POST,
new URI("http://localhost:8000/admin-api/apis"));
ResponseEntity<String> addAPIResp = restTemplate
.exchange(requestEntity, String.class);
assertEquals(HttpStatus.CREATED, addAPIResp.getStatusCode());
Конечно, мы хотим, чтобы проксируемый API был защищен. Этого можно легко добиться, включив плагин аутентификации для проксируемого API администратора.
6. Резюме
В этой статье мы представили Kong — платформу для шлюза API микросервисов и сосредоточились на ее основных функциях — управлении API и маршрутизации запросов к вышестоящим серверам, а также на некоторых более продвинутых функциях, таких как балансировка нагрузки.
Тем не менее, есть еще много полезных функций, которые мы можем изучить, и мы можем разработать собственные плагины, если нам это нужно — вы можете продолжить изучение официальной документации здесь .
Как всегда, полную реализацию можно найти на Github .