Skip to main content

API Gateway

O projeto NX API Gateway também popularmente conhecido como webhook, gateway ou gw, tem o objetivo de centralizar as integrações dos softwares da Smart NX com os softwares das empresas parceiras disponibilizando acesso à esses recursos através de APIs RESTful.

Principais Funcionalidades​

  • 🦖 Escalável: Ao utilizar os serviços da AWS como o Lambda, RDS e API Gateway possuimos uma infraestrutura elástica e que se adequa ao uso, obtendo todos os benefícios que uma estrutura serverless proporciona.

  • 🛠 Retornos personalizados: A depender da solução precisamos de tratamentos específicos: Alguns lugares só aceitam json, arrays de primeiro nível, etc. Tudo isso pode ser ajustado aqui: Transformando um XML para JSON ou simplificando uma resposta.

  • 🔐 Segurança: É construído uma camada adicional de proteção, gerando tokens próprios da Smart NX e deixando os tokens originais dos parceiros a serem integrados no banco de dados.

  • ⚡️ Rapidez: Semanalmente são geradas novas versões em produção das novas APIs integradas, sendo também disponibilizado um ambiente de homologação onde elas podem rapidamente serem validadas.

Tecnologias e Ferramentas​

img img img img img img img

  • Python: 3.7.7
  • Repositório git: automacao/webhook
  • Jira board: [P] API Gateway
  • RDS: MySQL 8
  • Flask: 1.1.2

Visão Geral​

Neste tópico será abordado uma visão geral do processo de desenvolvimento no API Gateway.

img

Início: a necessidade de negócio​

Em vários projetos e produtos surge a necessidade de fazer integrações via api com outras empresas. Na maioria das vezes o retorno e/ou o formato não é muito fácil de se trabalhar por ser um xml ou por estar em um formato não tão amigável para ser exposto ao usuário final, por exemplo, uma data em formato americano demonstrada em um chatbot.

Nesses casos é adequado ao formato necessário e todo tratamento é feito no API Gateway. A demanda inicia-se com um problema de negócio e a partir do mesmo é criada uma solução. Para o desenvolvimento as tratativas iniciam-se no quadro [P] API Gateway no JIRA onde se é criada a tarefa:

img

Neste exemplo a API retorna um arquivo .pdf e a tratativa a ser feita é fazer o upload deste arquivo no S3 gerando um link e usar um encurtador nesse link para que ele fique link.smartnx.io/seuPdf. O problema ocorre pois em um chatbot pelo canal WhatsApp não é possível enviar arquivos dinamicamente, sendo necessário enviar um link direto.

Controle e versionamento de código​

Com a tarefa em mãos cria-se uma nova branch no repositório git do produto, o mesmo estando localizado dentro do workspace SMARTNX e do projeto automacao, com o nome de webhook.

Dentro do repositório temos duas branches importantes:

  • develop: Onde está o código de homologação. As novas funcionalidades passam pelo processo de merge nesta branch para serem aprovadas.

  • master: Onde está o código de produção. As novas funcionalidades uma vez homologadas e aprovadas na develop vem para cá, em um deploy realizado se necessário sempre no primeiro dia útil da semana após as 18 horas.

O ecossistema AWS​

A master e a develop possuem seus códigos dentro da infraestrutura da Amazon Web Services - AWS rodando em funções lambda e utilizando o RDS como banco de dados.

Nós utilizamos a linguagem python com o microframework Flask e acessados pelo AWS API Gateway.

Setup​

Após o setup do git e bitbucket faça o clone do repositório:

git clone git@bitbucket.org:devsmartnx/webhook.git

Entre na pasta webhook:

cd webhook

Crie a venv para isolar as dependências do projeto:

python -m venv ./venv

Ative a venv:

source ./venv/bin/activate

Instale as dependências:

pip install -r requirements.txt

O próximo passo é selecionar o ambiente que você quer acessar as informações. O GW possui ambientes de produção e homologação. A troca entre os ambientes pode ser facilmente feita por uma variável de sistema:

  • Homologação:
export FLASK_CONFIGURATION=dev
  • Produção:
export FLASK_CONFIGURATION=prod

Caso queira que essa configuração persista tem três arquivos que você deve configurar:

  • ~/.bashrc

Cada instância do terminal do Ubuntu as configurações são lidas deste arquivo. Configure para persistir:

export FLASK_CONFIGURATION=dev
  • ~/.profile

Semelhante ao bashrc só que específico ao seu usuário:

export FLASK_CONFIGURATION=dev
  • /etc/environment

Se você quer que essa variável esteja presente, independente do terminal (e seja possível acessá-la de outros aplicativos) adicione a seguinte linha no fim do arquivo:

FLASK_CONFIGURATION=dev

Uma vez que essa configuração foi finalizada, simplesmente rode no seu terminal:

python run.py

Tudo ocorrendo certinho, você receberá a seguinte mensagem e poderá acessar as rotas localmente:

* Serving Flask app "app" (lazy loading)
* Environment: dev
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 255-506-991

Fluxo de Trabalho​

Iniciando uma integração​

Na pasta app existe um arquivo chamado config.py, dentro dele há uma classe chamada Env que por sua vez tem uma lista chamda SERVICES, adicione a seguinte linha no final da lista:

{'bp': '', 'version': [1], 'ServiceName': ''}

Onde:

  • Key bp é o nome da rota que será usado para chamar o recurso via url, é aconsehavel que ela tenha o nome da integração todo em minúsculo(lower case);

  • Key ServiceName é o nome da classe que será criada para manipular a integração, é aconsehavel que ela tenha o nome da integração todo em CamelCase;

Agora dentro da pasta app/views existe basicamente 2 pastas: v1 e v2, a sua integração só ra na pasta v2 caso ja exista na v1 uma versão mais desatualizada. Para este tutorial vamos levar em consideração que vamos usar pasta v1. Dentro desta pasta existe o arquivo "init.py" e lá dentro faça a importação da Blueprint.

Crie uma pasta com o nome da integração em minúsculo, fazendo referência da lista de serviços incluida anteriormente. Dentro desta pasta crie "init.py" e a pasta "controllers"

  • init.py
# __init__.py
from flask.blueprints import Blueprint
v1_mupay = Blueprint('v1_[nome da pasta mãe]', __name__)
from .controllers import *

Dentro da pasta controllers inclua arquivos em lowercase dentro deles estarão descritos as rotas ex: "persons.py" o modelo deste arquivo fica assim:

from app.inc import RequestFactory, token_authorization_required
#onde integracao é no nome da pasta e o v1 é a Blue print
from ...integracao import v1_integracao


@v1_mupay.route('/nome deste arquivo /<parametro opcional>', methods=["GET"])
@token_authorization_required
def getCardByPersonID("parametro opcional"):
response, status = RequestFactory.run(
# getCardByPersonID sera incluido em uma tabela explicada mais adiante o nome é a junção do método, nete caso é o get mas o nome da função descrita duas linhas antes.
'getCardByPersonID',
# a key variables só sera preenchido com os aprametros opcionais
{'variables': {
'person_id': person_id
}})
return response, status

Model da integração​

  • Dentro da pasta inc: init.py
from app.inc.[nome do arquivo da integração] import [nome da classe da integração]

Arquivo que será criado dentro da app/inc:

from json import loads

import requests
from app.exceptions import BaseError, GenericError, NotAuthorizedError, IntegrationError
from app.inc import BaseResponse, UsefulFunctions
from app.model.Integration import Integration
from app.repository import BaseResource, IntegrationService


class Integration(object):

def __init__(self, company_id):
self.header = {
'Content-Type':'application/json'
}
self.__set(company_id)

def __set(self, company_id):
response = IntegrationService.get(company_id, 'Itspay')

if response['status'] is not 200:
raise NotAuthorizedError('Company has no integration to use this method.')

integration = response['response'][0]
self.url = integration['url']
self.header.update({
'authorization': integration['user']
})

def sendRequest(self, route_format, params):
try:
send_header = self.header
if 'auth' in route_format and route_format['auth'] is False:
send_header.pop('Authorization')
if 'variables' in params:
route_format['path'] = UsefulFunctions.interpretRouteParam(route_format['path'], params)
params.pop('variables')

response = requests.request(route_format['method'], '{}{}'.format(self.url, route_format['path']),headers=send_header, **params)

except requests.exceptions.RequestException as e:
raise GenericError(502, 'Bad Gateway')

try:
json_data = loads(response.text)
except:
json_data = ''

if response.ok is not True:
raise IntegrationError(response.status_code, response.reason, json_data)

return BaseResponse.success(response.status_code, response.reason, json_data), response.status_code

Banco de dados​

Tabela service​

insert into services (name,url,price) values ("","","");

Onde:

  • name: Nome da integração
  • url: É a base da url da API da integração
  • price: Só é usado em casos especiais, default 0

Tabela route_switch​

insert into route_switch (tag,class_name,method,path,reponse_type,auth) values ("","","","","","");

Onde:

  • tag: nome da função escrito no arquivo de rota ex: "getCardByPersonID"
  • class_name: Nome da classe da integração que fica em app/inc
  • method: Método da requisição da API
  • path: Continuidade da url da API (url+path = url da API completa)
  • reponse_type: Tipo de resposta (file, text, json, ...)
  • auth: parâmetro opcional caso seja necessário enviar alguma configuração extra.

Integration​

insert into integration (user,password,active,company,service_id,config) values ("","","","","","");

Onde:

  • user: usuário da API da integração (Cada cliente tem uma)
  • password: senha da API da integração (Cada cliente tem uma)
  • active: 0 para desativada, 1 para ativada
  • company: Id da empresa no Gateway
  • service_id: Id do serviço (aquele da tabela de serviços incluida anteriormente)
  • config: parâmetros opcionais em json

Project Patterns​

Lista de algums design patterns que nós do time de automação utilizamos.

"Tá de boa eu nao me importo mais hehehe" Wreiners

commits​

Usamos uma variação do conventional commits que referencia as tarefas do Jira.

tipo:(id da tarefa) o que foi feito | onde foi feito

exemplo de commits

pre-commits​

É um gerenciador de pacotes para hooks pre-commit. Link para documentação docs.

Depois de instalar os requirements, você precisará iniciar o pre-commit no repositório com o comando pre-commit install.

repos:
- repo: local
hooks:
- id: requirements
name: requirements
entry: bash -c 'venv/bin/pip3 freeze > requirements.txt; git add requirements.txt'
language: system
pass_filenames: false
stages: [commit]
Exemplo de configuração pre-commit

code style​

Usamos o formatador de código Black. Antes de cada commit o black verifica os arquivos e automaticamente formata do código. Depois de formatado é necessário dar stage nos arquivos e rodar o commit mais uma vez. Também podemos formatar fora do commit utilizando o comando black <file>.

Onboarding​