Skip to content

Instantly share code, notes, and snippets.

@thiagomatar
Last active September 3, 2022 12:57
Show Gist options
  • Save thiagomatar/5ddf831bc2f6cf9402e64206df2429f9 to your computer and use it in GitHub Desktop.
Save thiagomatar/5ddf831bc2f6cf9402e64206df2429f9 to your computer and use it in GitHub Desktop.
API Gateway: conceito e aplicação

Ao final deste artigo você será capaz de:

  1. Definir o conceito de um API Gateway.
  2. Conhecer os benefícios da utilização deste componente arquitetural.
  3. Construir seu próprio API Gateway utilizando Spring Cloud.

Conteúdo

  1. O que é um API Gateway?
  2. Motivação
  3. API Gateway
  4. Implementando um API Gateway
  5. Testando
  6. Conclusão

O que é um API Gateway?

Conforme descrito no livro MICROSERVICES FROM DESIGN TO DEPLOYMENT por Chris Richardson e Floyd Smith, 2016:

“API Gateway é um serviço de ponto de entrada único no sistema. É semelhante ao padrão de projetos FACADE em orientação à objetos. O API Gateway encapsula a arquitetura do sistema interno e fornece uma API personalizada para cada cliente. Ainda, pode ter outras responsabilidades como autenticação, monitoramento, balanceamento de carga, cache, manipulação das requisições e resposta”

Motivação

Como sabemos, no modelo arquitetural de micro serviços podem haver dezenas e/ou centenas de serviços, cada um com sua responsabilidade para resolver um determinado problema.

Vamos imaginar que iremos desenvolver uma aplicação mobile para recomendar aluguel de veículos. Nesta aplicação, colocaremos alguns filtros com informações sobre o tipo de veículo que queremos alugar, e depois o sistema fará uma varredura em lojas especializadas em aluguel de veículos, mostrando o resultado com as melhores buscas. Caso algum veículo agrade, então, forneceremos dados pessoais para cadastro e finalizaremos o aluguel utilizando o cartão de crédito. Em seguida, o sistema reserva o veículo.

Imaginando estes cenários podemos destacar algumas responsabilidades que poderiam, hipoteticamente, serem separados em serviços:

  • Serviço de busca de veículos em lojas especializadas em aluguel de veículos.
  • Serviço de cadastro de clientes.
  • Serviço de pagamentos.
  • Serviço de reserva de veículos.

Conforme demonstrado na imagem acima, o app pode realizar requisições para cada serviço individualmente. Porém, essa abordagem é cheia de armadilhas que podem impactar a evolução do produto. Alguns dos principais problemas são:

  • Para cada serviço o APP deve conhecer seu endereço e porta, e assim realizar requisições separadamente, aumentando sua complexidade.
  • Os serviços internos precisam ficar expostos para chamadas externas. Isso pode implicar sérios problemas de segurança.
  • Dificuldade de manutenção e solução de problemas no sistema, correndo o risco de causar indisponibilidade.
  • É custoso refatorar o APP uma vez que ele está muito acoplado no serviços downstream.

API Gateway

Ao invés do APP realizar as requisições diretamente aos serviços downstream, vamos centralizar as requisições em somente um ponto de entrada.

O API Gateway é o serviço responsável, agora, em receber todas as requisições e roteá-las. Essas requisições serão encaminhadas para cada serviço específico, tendo ainda a chance de filtrar essas requisições e respostas podendo manipulá-los, além de adicionar algumas camadas de verificações e monitoramentos.

Como mencionado acima, o principal benefício de utilizar um API Gateway é encapsular a estrutura interna do sistema. Desta forma, ao invés dos clientes acessarem diretamente os serviços downstream, toda requisição é passada pela porta de entrada. Isso facilitará a integração do cliente com o sistema, manterá os serviços internos seguros, e, teremos a chance de manipular as requisições e respostas (enriquecer headers, traduzir protocolos, etc…).

Como todas as escolhas, o API Gateway também tem algumas desvantagens. Por ser a porta de entrada, esse serviço se torna um componente de extrema importância para o sistema e deverá ter alta disponibilidade.

Também existe o risco do API Gateway se tornar um gargalo de desenvolvimento.Por isso, os desenvolvedores devem atualizar o API Gateway para expor os endpoints de cada micro serviço.

Implementando um API Gateway

Existem vários API Gateways disponíveis no mercado, sejam pagos ou open source, que podem ser utilizados.

Os principais fornecedores pagos:

  • Apigee
  • Aws Gateway
  • MuleSoft
  • Axway
  • Young App
  • SnapLogic
  • Akana API Platform
  • Oracle API Platform
  • 3scale
  • Sensedia

Os principais fornecedores open source:

  • Kong Gateway
  • Tyk
  • KrakenD
  • Gravitee.io
  • Gloo Edge
  • Goku API Gateway
  • WSO2 API Microgateway
  • Fusio
  • Apiman
  • API Umbrella

Neste artigo iremos desenvolver o API Gateway utilizando o Spring Cloud Gateway, que tem como objetivo fornecer uma maneira simples, mas eficaz de rotear para APIs e propiciar formas muito eficientes de implementar segurança, monitoramento e métricas.

Primeiramente vamos criar os serviços. O intuito principal é o desenvolvimento do Api Gateway, portanto os serviços serão extremamente simples, com respostas estáticas para efeito de testes.

Vamos criar os 2 dos 4 serviços descritos acima para exemplificar: Vehicle Search Service, Customer Registration Service

Através do site: https://start.spring.io iremos criar os dois projetos adicionando somente a dependência Spring WEB.

Descompacte e importe o projeto em sua IDE favorita, em seguida, vamos mudar a porta de cada serviço e criar os recursos.

Para o Vehicle Search Service vamos utilizar a porta 9091 e para o Customer Registration Service a porta 9092, fique a vontade para escolher a porta. Porém, lembre-se que elas precisam ser diferentes, e, que no API Gateway, devemos configurar conforme as portas que escolhemos.

Vamos começar pelo serviço do Vehicle Search Engine:

Abra o arquivo application.properties e defina a porta:

# para Vehicle Search Service
server.port=9091

Logo, vamos criar a classe que vai representar o veículo:

import java.math.BigDecimal;
import java.time.LocalDate;

public class Vehicle {
    private final String color;
    private final String model;
    private final LocalDate availableIn;
    private final BigDecimal price;

    public Vehicle(String color, String model, LocalDate availableIn, BigDecimal price) {
        this.color = color;
        this.model = model;
        this.availableIn = availableIn;
        this.price = price;
    }

    public String getColor() {
        return color;
    }

    public String getModel() {
        return model;
    }

    public LocalDate getAvailableIn() {
        return availableIn;
    }

    public BigDecimal getPrice() {
        return price;
    }
}

Assim como os payloads VehicleSearchRequest e VehicleSearchResponse

public class VehicleServiceRequest {

    private String color;
    private String model;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    @Override
    public String toString() {
        return "VehicleServiceRequest{" +
                "color='" + color + '\'' +
                ", model='" + model + '\'' +
                '}';
    }
}
package br.com.tqi.vss.resources.payload;

import java.util.List;

public class VehicleSearchResponse {

    private final List<Vehicle> vehicle;
    private final boolean match;

    public VehicleSearchResponse(List<Vehicle> vehicle, boolean match) {
        this.vehicle = vehicle;
        this.match = match;
    }

    public List<Vehicle> getVehicle() {
        return vehicle;
    }

    public boolean isMatch() {
        return match;
    }
}

Por fim, a camada de serviço e o controller:

import br.com.tqi.vss.resources.payload.Vehicle;
import br.com.tqi.vss.resources.payload.VehicleSearchResponse;
import br.com.tqi.vss.resources.payload.VehicleServiceRequest;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class VehicleSearchService {

    public VehicleSearchResponse search(VehicleServiceRequest request) {

        List<Vehicle> filtered = new ArrayList<>();
        List<Vehicle> allVehicles = getVehicleList();


        if(request.getColor() != null && request.getModel() != null){
            filtered.addAll(
                    allVehicles.stream()
                            .filter(vehicle -> isFilteredByColorAndModel(request, vehicle))
                            .collect(Collectors.toList()));
        } else if (request.getColor() == null && request.getModel() != null){
            filtered.addAll(
                    allVehicles.stream()
                            .filter(vehicle -> isFilteredByModel(request, vehicle))
                            .collect(Collectors.toList()));
        }else if (request.getColor() != null && request.getModel() == null) {
            filtered.addAll(
                    allVehicles.stream()
                            .filter(vehicle -> isFilteredByColor(request, vehicle))
                            .collect(Collectors.toList()));
        }

        if (filtered.isEmpty()) {
            return new VehicleSearchResponse(allVehicles, false);
        } else {
            return new VehicleSearchResponse(filtered, true);
        }
    }

    private boolean isFilteredByModel(VehicleServiceRequest request, Vehicle vehicle) {
        return vehicle.getModel().equalsIgnoreCase(request.getModel());
    }


    private boolean isFilteredByColor(VehicleServiceRequest request, Vehicle vehicle) {
        return vehicle.getColor().equalsIgnoreCase(request.getColor());
    }

    private boolean isFilteredByColorAndModel(VehicleServiceRequest request, Vehicle vehicle) {
        return isFilteredByColor(request, vehicle)
                && isFilteredByModel(request, vehicle);
    }

    private List<Vehicle> getVehicleList() {
        return List.of(
                new Vehicle("Blue", "Sedan", LocalDate.now().plusMonths(1), BigDecimal.valueOf(243.23)),
                new Vehicle("Black", "Sedan", LocalDate.now().plusMonths(1).plusDays(2), BigDecimal.valueOf(212.32)),
                new Vehicle("Grey", "Sedan", LocalDate.now().plusMonths(2).plusDays(1), BigDecimal.valueOf(322.42)),
                new Vehicle("Blue", "SUV", LocalDate.now(), BigDecimal.valueOf(294.29)),
                new Vehicle("Black", "SUV", LocalDate.now().plusMonths(1).plusDays(2), BigDecimal.valueOf(210.82)),
                new Vehicle("Grey", "SUV", LocalDate.now().plusMonths(2).plusDays(1), BigDecimal.valueOf(335.02)),
                new Vehicle("Blue", "Pickup", LocalDate.now().plusMonths(3), BigDecimal.valueOf(443.92)),
                new Vehicle("Black", "Pickup", LocalDate.now().plusMonths(1).plusDays(2), BigDecimal.valueOf(359.82)),
                new Vehicle("Grey", "Pickup", LocalDate.now().plusDays(5), BigDecimal.valueOf(322.72))
        );
    }

}
package br.com.tqi.vss.resources;


import br.com.tqi.vss.resources.payload.VehicleSearchResponse;
import br.com.tqi.vss.resources.payload.VehicleServiceRequest;
import br.com.tqi.vss.service.VehicleSearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RequestMapping("/vehicle")
@RestController
public class VehicleSearchResource {

    private static final Logger log = LoggerFactory.getLogger(VehicleSearchResource.class);
    private final VehicleSearchService service;

    public VehicleSearchResource(VehicleSearchService service) {
        this.service = service;
    }

    @GetMapping
    public VehicleSearchResponse search(VehicleServiceRequest request){
        log.info(request.toString());
        return service.search(request);
    }

}

Agora vamos configurar o serviço de registro de usuários:

# para Customer Registration Service
server.port=9092

Para simplificar, para este serviço, criaremos somente o controller:

package br.com.tqi.csr.resources;

import br.com.tqi.csr.resources.payload.CustomerRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/customer")
public class CustomerRegistrationResource {

    @PostMapping
    public String register(@RequestBody CustomerRequest request){

        return "Customer " + request.getName() + " created successfully";
    }


}

E finalmente, vamos criar o nosso API Gateway, e para isso, vamos voltar até o site: https://start.spring.io e criar o nosso projeto.

Ótimo! Agora com o projeto criado, vamos começar a elaboração das rotas, tendo em vista que o spring permite fazer isso de duas formas: estática e dinamicamente. A maior diferença é que para cada nova rota criada no gateway é necessário realizar uma nova entrega do API Gateway. Assim, tratando da forma dinâmica é possível colocar esses arquivos de configurações em um servidor de configurações, e quando incluir uma nova rota, só precisamos recarregar o contexto da aplicação e as novas rotas estarão disponíveis para utilização. No nosso exemplo iremos utilizar a forma dinâmica, para isso vamos adicionar as rotas no arquivo de configuração do spring.

O Spring permite que façamos as configurações do projeto através do arquivo application.properties, mas, como alternativa, podemos utilizá-lo como arquivo YAML. Basta renomear o arquivo para application.yaml, o que vai facilitar muito as configurações.

Em /src/main/resources/ altere o arquivo application.properties para application.yaml.

Vamos começar adicionando as rotas dos nossos serviços no arquivo application.yaml:

server:
  port: 9090
spring:
  main:
    banner-mode: off
  cloud:
    gateway:
      routes:

        - id: vehicle-search-service
          uri: http://localhost:9091
          predicates:
            - Path=/vehicle/search
            - Method=GET
          filters:
            - RewritePath=/vehicle/search, /vehicle

        - id: customer-registration-service
          uri: http://localhost:9092
          predicates:
            - Path=/customer
            - Method=POST

Podemos observar que a configuração de rotas está estruturada em 3 seções, são elas:

Route: É o bloco de construção básico do gateway. Ele é definido por um ID, um URI de destino, uma lista de predicados e uma lista de filtros. Assim, uma rota é correspondida se o predicado agregado for verdadeiro.

Predicate: Este é um predicado da função do Java 8. O tipo de entrada é um Spring Framework ServerWebExchange. Isso permite que você corresponda a qualquer coisa da solicitação HTTP, como cabeçalhos ou parâmetros.

Filter: Filtros são instâncias do Spring Framework GatewayFilter. Aqui, você pode modificar requisições e respostas antes ou depois de enviar a requisição para os serviços downstream.

O Spring fornece vários filtros que podem ser utilizados na declaração das rotas. Mais filtros podem ser encontrados clicando aqui.

Como exemplo, utilizamos o rewritepath, que altera o path da uri antes de enviar a requisição para o sistema downstream.

Também é possível customizar filtros para atender demandas específicas do sistema, como por exemplo: validar se o usuário está autenticado, criar logs e até a rastreabilidade das requisições.

Os filtros podem ser configurados de forma específica, ou seja, para cada rota conforme exemplificado acima, como também de modo global, onde o filtro será aplicado para todas as rotas. Como exemplo, vamos criar um filtro global e adicionar na configuração do gateway:

package br.com.tqi.gateway.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClientResponse;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

@Component
public class GatewayLogFilter extends Config implements GatewayFilterFactory<Config> {

    private static final Logger log = LoggerFactory.getLogger(GatewayLogFilter.class);

    @Override
    public GatewayFilter apply(Config config) {

        return (exchange, chain) -> {
            logHttpRequest(exchange);
            return chain.filter(exchange).then(logHttpResponse(exchange));
        };
    }

    @Override
    public Class<Config> getConfigClass() {
        return Config.class;
    }

    private void logHttpRequest(ServerWebExchange exchange) {
        log.info("Http method={} to={} route={} headers={}",
                exchange.getRequest().getMethod(),
                exchange.getRequest().getURI(),
                exchange.getRequest().getPath(),
                exchange.getRequest().getHeaders()
        );
    }

    private Mono<Void> logHttpResponse(ServerWebExchange exchange) {
        return Mono.fromRunnable(() -> {
            if (isThereAnHttpError(exchange)) {

                final var route = (Route) exchange.getAttributes().get(GATEWAY_ROUTE_ATTR);
                final var clientResponse = (HttpClientResponse) exchange.getAttributes().get(CLIENT_RESPONSE_ATTR);

                log.error("Http routeId={} statusCode={} from={} to={} headers={}",
                        route.getId(),
                        exchange.getResponse().getStatusCode(),
                        exchange.getRequest().getURI(),
                        route.getUri() + clientResponse.uri(),
                        exchange.getResponse().getHeaders()
                );
            }
        });
    }

    private boolean isThereAnHttpError(ServerWebExchange exchange) {
        if (exchange.getResponse().getStatusCode() != null) {
            return HttpStatus.Series.SUCCESSFUL != exchange.getResponse().getStatusCode().series();
        } else {
            return true;
        }
    }
}

Vamos adicionar o filtro no application.yaml:

server:
  port: 9090
spring:
  main:
    banner-mode: off
  cloud:
    gateway:
      default-filters:
        - GatewayLogFilter
      routes:

        - id: vehicle-search-service
          uri: http://localhost:9091
          predicates:
            - Path=/vehicle/search
            - Method=GET
          filters:
            - RewritePath=/vehicle/search, /vehicle

        - id: customer-registration-service
          uri: http://localhost:9092
          predicates:
            - Path=/customer
            - Method=POST

Agora toda requisição encaminhada para o API Gateway será logada, implicitamente e de forma global:

INFO 29978 --- [or-http-epoll-2] b.c.tqi.gateway.filter.GatewayLogFilter  : Http method=GET to=http://localhost:9090/vehicle/search route=/vehicle/search headers=[User-Agent:"PostmanRuntime/7.26.8", Accept:"*/*", Postman-Token:"6ff0be20-cfdd-4c9c-ba2a-aec81cc1d97f", Host:"localhost:9090", Accept-Encoding:"gzip, deflate, br", Connection:"keep-alive"]

Testando

Vamos subir os três projetos e executar as seguintes requisições:

curl --location --request GET 'localhost:9090/vehicle/search?color=blue&model=suv'
curl --location --request GET 'localhost:9091/vehicle'
curl --location --request POST 'localhost:9090/customer' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Marcleisson"
}'

Conclusão

Parabéns se você chegou até aqui! Pois isso significa que provavelmente caso não conhecesse este componente arquitetural, agora com certeza conseguirá definir, exemplificar e implementar um API Gateway.

Todo o código fonte pode ser encontrado no github.

Obrigado pela leitura e até a próxima! =)

Thiago Queiroz Matar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment