Quem nunca usou um encurtador de URL como bit.ly ou o falecido goo.gl ? este recurso é excelente quando você precisa se comunicar com seus clientes e usuários em cenários onde existe limitação de caracteres como por exemplo um SMS ou um post no twitter.
E agora, porque não desenvolver seu próprio encurtador de URL utilizando seu próprio domínio e com controle total dentro de sua infraestrutura sem limitações de conta ? Então vamos em frente e de quebra vamos passar por diversos serviços da amazon AWS, para quem não conhece, é uma excelente porta de entrada.
DynamoDB
Para converter nossa URL longa em uma URL curta, precisamos guardar essa informação em algum local, para esta solução vamos utilizar um banco de de dados, o DynamoDB da AWS.
O DynamoDB é um banco de dados NoSQL disponível na AWS, ele é altamente escalável e gratuito até 25 GB por mês de armazenamento. O nível gratuito também suporta até 200 milhões de requisições por mês.
Acesse o serviço do dynamoDB na AWS e clique em Create Table. Crie a tabela urlshort com a chave primaria codepath do tipo string.
Toda URL curta que vamos gerar, possui um código único. que será incorporado no final da URL. Exemplo: https://s.exemplo.com/gt7hd4
O código único, gerado de forma aleatória para a URL curta, será a chave primária. Clique em Create para finalizar.
Apos criar a tabela você será direcionado para uma tela apresentando a tabela com os dados vazios. Vamos criar um registro apenas para visualizar como serão armazenadas as informações no banco. Clique em Create Item na tela abaixo
Informe um código na chave primaria com 8 posições, este código será parte da nossa url curta. Informe também em originalURL a url original e uma data de expiração em expirationDate.
Pronto, os registros foram incluídos no DB. Este foi apenas um registro de teste para ver os dados inseridos no dynamoDB. Agora que temos o banco configurado vamos iniciar a construção da nossa API.
API GATEWAY
Para construir um encurtador de URL precisamos criar dois serviços:
- Um serviço que receba uma URL longa e gere um código aleatório para ser utilizado na URL curta.
- Um serviço que redireciona a URL curta para a URL original.
Para as duas situações vamos utilizar um serviço da amazon AWS chamado API Gateway, que permite criar uma API de serviços REST, sem necessidade de um web server e de instanciar uma maquina EC2. Utilizando o API Gateway em conjunto com o Lambda (veremos mais a frente) teremos uma estrutura 100% serverless.
Acesse o serviço API Gateway da AWS e clique em Get Started, neste momento, será exibida uma tela informando que foi feita uma importação automática de uma aplicação de exemplo, neste caso você pode confirmar, mas não vamos utiliza-la, vamos criar uma API do zero. Então sua configuração deve ficar conforme abaixo
Após clicar em Create API, você será direcionado para a tela inicial do API Gateway exibindo somente a raiz da url, pois nenhum método foi criado ainda.
Se você estivesse construindo uma API REST para o seu backend, agora iriamos criar o(s) resource(s). Exemplo: http://dominio.com.br/resource, onde resource poderia ser qualquer entidade do seu negocio, como usuário, endereço, gamefication, log, cliente e por ai vai. Dentro de cada resource você criaria os métodos para cadastrar (POST), alterar (PUT), excluir (DELETE) e buscar (GET) a entidade daquele resource.
Como não estamos falando da construção de seu backend e sim de um encurtador de URL, estamos falando de um microserviço com somente esta função especifica que faz parte de uma unica API isolada do seu backend. Como queremos que o encurtador de URL gere a URL mais curta possível para o seu domínio, não vamos criar nenhum resource, vamos criar os métodos direto na raiz da API e utilizar um subdomínio como “s.[seudominio]”.
Vamos criar o método POST para encurtar a URL. Clique em Actions -> Create Method e selecione POST.
Ao criar o método e seleciona-lo, vemos a tela acima. Agora é necessário informar ao API Gateway, o que ele deve fazer ao receber uma requisição nesse método. Em integration type você percebe que tem 5 opções, o que nós vamos utilizar aqui é a integração com o lambda, que ainda não foi construído, então vamos fazer uma parada no API Gateway e pular para a criação e codificação do encurtador no lambda.
AWS LAMBDA FUNCTIONS
Lambda function é um dos recursos de cloud mais interessantes para desenvolvedores que surgiu nos últimos anos. É o que chamamos de serverless (sem servidor), ou seja, você codifica direto na cloud ou localmente integrado a ela e você realiza o deploy do seu código numa estrutura já preparada para executa-lo, sem necessidade de administrar uma maquina e subir um apache, tomcat, nginx, jboss e etc …
O recurso de serverless está disponível também em outras clouds como IBM Cloud (cloud functions), Google Cloud Functions e no Azure.
Você pode escolher dentre N linguagens suportadas pela cloud, para o nosso encurtador vamos desenvolver em nodeJS.
Dentro da AWS, escolha o serviço Lambda e clique na opção Create Function
Ao clicar, assim como o API Gateway, podemos partir de um exemplo. Para facilitar, vamos selecionar um modelo que vai gerar o código de boa parte do que precisamos.
Selecione a opção blueprint e pesquise por microservice
selecione então a opção microservice-http-endpoint que exibe a linguagem nodeJS no rodapé da caixa e clique em configure.
Em configure teremos 3 blocos para preencher. No primeiro bloco informe o nome da função (url-shrink) e selecione uma role já existente na AWS que é um padrão para funções lambda (lambda_basic_execution)
no segundo bloco temos a trigger que vai acionar a função lambda, vamos configurar esse bloco depois, então clique em remove e siga para o ultimo bloco, que apenas informa o código da função que está sendo gerada como modelo. Clique em create function para finalizar
pronto sua função está criada!
Agora vamos retornar para o API Gateway e terminar de configurar o nosso método POST apontando para a função lambda url-shrink.
Agora temos o nome da nossa função para configurar o método e devemos marcar a opção Use Lambda Proxy integration para que o request seja encaminhado ao handler da função lambda. Ao clicar em save, será exibida uma mensagem de confirmação de permissão para acessar a função, clique em OK.
O proximo passo é realizar o deploy da API, pois ela ainda não esta disponível em nenhum ambiente pra teste ou execução. Clique em Actions -> Deploy API e informe os dados abaixo:
Ao clicar em deploy, temos configurado o ambiente produção e a API esta disponível. Veja a tela pós deploy.
A API está publicada na URL
https://4unvlzjev4.execute-api.sa-east-1.amazonaws.com/producao
você já pode realizar um post para esta URL e testar, veja abaixo:
Acima utilizamos o POSTMAN para fazer uma requisição POST para a URL da nossa API. Passamos como parâmetro um json com o nome da tabela e o item para inclusão. A execução chegou até a API, mas retornou um erro:
User: arn:aws:sts::643129401010:assumed-role/lambda_basic_execution/url-shrink is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:sa-east-1:643129401010:table/urlshort
O erro é bem claro, a role que utilizamos como padrão não está autorizada a realizar o comando dynamodb/PutItem na tabela urlShort. Para corrigir esse bloqueio de permissão, é necessário acessar o serviço IAM da AWS e selecionar a role lambda_basic_execution que atribuímos a função lambda .
O serviço de IAM da AWS é responsável por gerenciar qualquer tipo de acesso dentro do ambiente da amazon, seja uma autenticação de usuario ou uma comunicação entre serviços internos. Tudo isso é feito aplicando politicas a usuarios, grupos e roles.
Clique no nome da role desejada (ou crie uma nova para associar a função lambda caso prefira) e em seguida clique em Attach Polices e adicione a police AmazonDynamoDBFullAccess.
pronto, agora a sua função lambda já consegue gravar os dados no dynamoDB.
Até agora temos uma API configurada pra receber a requisição de um encurtamento de URL, temos um banco de dados configurado para receber essas informações, temos uma função lambda que recebe a requisição da API, mas que ainda não possui o código que gera a url curta. Vamos então ao codigo da nossa função lambda que vai gravar as informações no banco:
[cc lang=”javascript”]
console.log(‘Loading function’);
const doc = require(‘dynamodb-doc’);
const dynamo = new doc.DynamoDB();
exports.handler = (event, context, callback) => {
//console.log(‘Received event:’, JSON.stringify(event, null, 2));
var codepath = null;
var dataExpiracao = new Date();
var numberOfDaysExpiration = 7;
var bodyRetorno = null;
const done = (err, res) => callback(null, {
statusCode: err ? ‘400’ : ‘200’,
body: err ? err.message : JSON.stringify(bodyRetorno),
headers: {
‘Content-Type’: ‘application/json’,
},
});
switch (event.httpMethod) {
case ‘DELETE’:
dynamo.deleteItem(JSON.parse(event.body), done);
break;
case ‘GET’:
dynamo.scan({ TableName: event.queryStringParameters.TableName }, done);
break;
case ‘POST’:
//gerando o codigo da URL temporaria codepath = randomString(6, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
//gerando a data de expiração dataExpiracao.setDate(dataExpiracao.getDate() + numberOfDaysExpiration); var dd = dataExpiracao.getDate(); var mm = dataExpiracao.getMonth() + 1; var y = dataExpiracao.getFullYear(); var dataExpiracaoFormatted = dd + '/'+ mm + '/'+ y; var parsedJson = JSON.parse(event.body); console.log('expirationDate:', dataExpiracaoFormatted); console.log('codepath:', codepath); parsedJson.Item.expirationDate = dataExpiracaoFormatted; parsedJson.Item.codepath = codepath; bodyRetorno = { "codepath": codepath, }; dynamo.putItem(parsedJson, done); break;
default:
done(new Error(`Unsupported method "${event.httpMethod}"`));}
};function randomString(length, chars) {
var result = '';
for (var i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
[/cc]O código acima deve ser informado na edição da função lambda e depois clicar no botão Save. Não é o foco detalhar a construção de uma função lambda e todas os recursos que ela dispõe na AWS, mas em breve vou criar um tutorial completo deste recurso e atualizo aqui o link.
Sobre o código é importante entender o que ele faz:
- Caso a requisição seja do tipo POST, é gerado um código alfanumérico aleatório com 6 posições. Este código será incorporado em nossa URL curta e serve também como chave do registro no DynamoDB
- É criado também uma data de expiração 7 dias apos a data corrente. Vamos utilizar essa data para excluir registros com mais de 7 dias.
- Ao utilizar a função dynamo.putItem(parsedJson, done); estamos incluindo o registro no banco do dynamoDB.
- O parsedJSON é o json no formato que deve ser utilizado para gravar os dados no dynamoDB, este formato é conforme abaixo:
- É obrigatório o atributo "TableName" com o nome da tabela e o atributo "Item" contendo os campos que fazem parte do registro que será incluido.
- Como em nosso código obtemos o json do body da requisição, Este json deve ser enviado no body da requisição. (var parsedJson = JSON.parse(event.body);)
- O json recebido no body é manipulado para incluir os campos de data de expiração e o codepath, entao o json enviado na API, deve conter somente a url original
REQUEST
{
"TableName" : "urlshort",
"Item" : {
"originalURL" : "https://epocanegocios.globo.com/Tecnologia/noticia/2018/12/tudo-que-sabemos-sobre-boring-company-empresa-de-musk-que-quer-acabar-com-o-transito.html"
}
}
RESPONSE
{
"codepath": "AXq5E6"
}
concluído este passo, estamos gravando o mapeamento da url curta para url original no banco, está tudo ok neste ponto, porem ainda temos um problema antes de prosseguir para a chamada da URL. Nosso API Gateway possui uma URL nada amigável:
https://4unvlzjev4.execute-api.sa-east-1.amazonaws.com/producao/
precisamos converter essa URL para que ela seja chamada para um domínio nosso, mais curto possível. O objetivo é que a API seja chamada pela URL:
https://s.dominio.com.br
para fazer essa mudança precisamos mexer em 3 servicos na AWS:
Certification Manager: Geração de certificado para o seu domínio (dominio.com.br) para que a API responda via https.
Route53: como o nosso domínio encontra-se registrado na amazon no route53, precisamos criar um registro CNAME para o nosso subdomínio
s.dominio.com.br . (você pode utilizar o seu próprio servidor de DNS, caso seu domínio não esteja registrado na amazon).
CloudFront: Não vamos manipular o cloudfront diretamente, mas ao configurar o domínio para o API Gateway, internamente será gerada uma distribuição no cloudfront, que é o equivalente ao cloudflare na amazon, uma interface que fica na frente do seu site e é utilizada pra fazer cache de conteúdo estático, muito conhecido pelo nome de CDN.
Certification Manager
Acesse o serviço Certification Manager e troque a região da AWS para North Virginia.
No print acima, eu já possuo os certificados gerados, mas não tem problema, pois é muito simples a geração de certificado, você vai selecionar Request a certificate e Request a public certificate na tela seguinte. Na ultima tela, você devera informar a cobertura do seu certificado, nesse ponto é importante informar um * na frente do seu domínio, de forma que qualquer subdomínio seja coberto pelo certificado.
Clique em next e escolha a opção DNS Validation, onde será solicitada a criação de um registro DNS que a AWS irá validar. Caso utilize o route53, a validação será bem rápida, pois existe uma integração entre os dois serviços.
Acima temos a tela final da geração do certificado com as informações do registro CNAME que você precisa criar. Apos a criação do registro o certificado será validado e gerado.
No inicio desse bloco pedimos para modificar a região para North Virginia, isso porque o cloudfront e os serviços que utilizam certificado na AWS, enxergam somente os certificados expedidos nessa região, salvo raras exceções.
API GATEWAY - Parte 2 (Domain Name)
Certificado gerado, agora é hora de voltar ao API Gateway e configurar o seu nome de domínio. Clique em Custom Domain Names e será exibida a seguinte tela:
Clique no botão Create Custom Domain Name e informe os seguintes dados:
Deixe marcada a opção http e informe o seu domain name utilizando "s" como subdomínio. Deixe a opção Edge Optimized como default e selecione o seu certificado que foi gerado no Certification Manager e clique em Save
Se você gerou seu certificado na região North Virginia ele será exibido na opção Edge Optimized, caso contrario ele será exibido na opção regional
apos clicar em Save, será gerada internamente a distribuição do cloudfront junto com uma URL do tipo abcdefgh.cloudfront.net. Esta URL, que pertence ao cloudfront, será utilizada no próximo passo para configurar o route53.
Ainda no Custom Domain Name, precisamos configurar o base path da URL, isso porque o stage não faz parte desse mapeamento (havíamos criado o stage producao, para o API Gateway ao realizamos o deploy), ou seja, nossa url no momento precisa ser chamada assim:
https://s.dominio.com.br/producao
Clique em edit e em base path mappings. Informe como path a raiz "/" seguido do nome da API e do stage producao
Pronto, agora a nossa API deve ser chamada pela URL abaixo:
http://s.dominio.com.br
mas ainda não terminamos.
Se você chamar essa URL agora ela não vai funcionar, pois precisamos dizer ao nosso servidor DNS que o nosso subdominio s.dominio.com.br precisa apontar para distribuição cloudfront que foi gerada. Copie o endereço que é informado no campo Target Domain Name e vamos para o serviço Route53
Route53
O route 53 é o serviço de gerenciamento de domínios da AWS, você pode utiliza-lo ou não, depende aonde o seu serviço está configurado. Caso seu domínio esteja em outro host, não tem problema, o mesmo passo que vamos fazer aqui, você precisa realizar no seu host de DNS.
Este passo é bem rápido, mas apos terminar a publicação pode demorar um pouco.
Acesso o servico Route53 e entre no seu domínio já configurado, clique em Create Record Set e informe os seguintes dados:
Ao clicar em Save Record Set, o seu subdomínio s.dominio.com.br já está apontando para o cloudfront que vai fazer a integração com o API Gateway. Pode testar a execução da sua API utilizando a URL s.dominio.com.br
nosso encurtador de URL está pronto!
Com o encurtador pronto, vamos desenvolver agora o redirecionador da URL curta
Este passo é bem pequeno em relação a tudo o que já fizemos até agora, porem mexe em algumas questões mais avançadas no API Gateway. Vamos começar por atualizar o código da nossa função lambda.
[cc lang="javascript"]
console.log('Loading function');
const doc = require('dynamodb-doc');
const dynamo = new doc.DynamoDB();
exports.handler = (event, context, callback) => {
//console.log('Received event:', JSON.stringify(event, null, 2));
//console.log('Received context:', context);
var codepath = null;
var numberOfDaysExpiration = 7;
var bodyRetorno = null;
const done = (err, res) => callback(null, {
statusCode: err ? '400' : '200',
body: err ? err.message : JSON.stringify(bodyRetorno),
headers: {
'Content-Type': 'application/json',
},
});
switch (event.httpMethod) {
case 'DELETE':
dynamo.deleteItem(JSON.parse(event.body), done);
break;
case 'GET':
console.log("codepath", event.pathParams.codepath)
var params = {
TableName: "urlshort",
Key:{
"codepath": event.pathParams.codepath
}
};
dynamo.getItem(params, function(err, data) {
if (err) {
console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2));
context.done(err, {});
} else {
console.log("GetItem succeeded:", JSON.stringify(data, null, 2));
var dbObject = JSON.stringify(data, null, 2);
console.log("URL to Redirect:", data.Item.originalURL);
var err = new Error("HandlerDemo.ResponseFound Redirection: Resource found elsewhere");
err.name = data.Item.originalURL;
context.done(err, {});
}
});
break;
case 'POST':
//gerando o codigo da URL temporaria
codepath = randomString(6, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
//gerando a data de expiração
var diasExpiracao = process.env.expirationDays;
console.log("dias para expiração da url curta: " , diasExpiracao)
var dataExpiracao = new Date();
dataExpiracao.setDate(dataExpiracao.getDate() + parseInt(diasExpiracao));
console.log("data de expiração do registro", dataExpiracao);
var expirationDate = Math.floor(dataExpiracao / 1000)
console.log("expirationDate" , expirationDate);
console.log('codepath:', codepath);
var parsedJson = JSON.parse(event.body);
var jsonDB = {
"TableName" : "urlshort",
"Item" : {
"codepath" : codepath,
"expirationDate" : expirationDate,
"originalURL" : parsedJson.originalURL
}
}
console.log('jsonDB:', jsonDB);
bodyRetorno = {
"codepath": codepath,
};
dynamo.putItem(jsonDB, done);
break;
default:
done(new Error(`Unsupported method "${event.httpMethod}"`));
}
};
function randomString(length, chars) {
var result = '';
for (var i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
[/cc]
Agora escrevemos o código referente ao método GET. O que estamos fazendo neste bloco é o seguinte:
- Obtemos o parâmetro codepath da URL, que no nosso caso é o parâmetro em negrito https://s.dominio.com.br/r6g7hu
- Este parâmetro será configurado no API Gateway no próximo passo.
- Criamos um json no modelo de busca do dynamoDB e fazemos uma consulta no banco para obter a URL original referente ao codigo que passamos.
- Setamos um erro na resposta da requisição, afinal de contas estamos utilizando um código http 301, que significa redirecionamento permanente do endereço.
- Este erro possui uma descrição quando é criado que começa com o nome HandlerDemo.ResponseFound. Este nome será utilizado no API Gateway para identificar a requisição e processar o redirecionamento.
- Setamos também a url de destino que obtemos do dynamoDB para o atributo err.name. Em um erro 301, o redirecionamento é feito para a URL que encontra-se no header do response com o nome location. Vamos fazer esse mapeamento também no API Gateway.
- Foi feita também uma pequena melhoria no método POST. Agora o payload enviado precisa utilizar um json simples somente com o campo orginalURL, pois Item e TableName já esta sendo setado pela função.
No API gateway, precisamos criar um novo resource na raiz da nossa API, que utilize o seu proprio path como valor que vamos utilizar para o redirecionamento. Clique em Actions -> Create Resource a partir da raiz da API no API Gateway
informe o resource name redirect e informe no resource path a variável {codepath}. Habilite o CORS também para que sua API possa ser chamada diretamente de um browser caso você tenha essa necessidade.
http://s.dominio.com.br/qwe123
codepath = qwe123
Criado o resource, crie o método GET conforme imagem abaixo, lembre-se de não marcar a opção Use Lambda Proxy integration desta vez, ao contrário do que fizemos com o método POST.
Quando criamos o método POST no serviço que gerava a URL curta, utilizamos o API Gateway somente como uma ponte entre a origem e o destino, neste caso o nosso API Gateway vai funcionar como um adapter, vamos precisar transformar os dados recebidos na URL antes de enviar ao Lambda e faremos o mesmo no retorno do Lambda para poder direcionar a chamada para a url de destino.
Apos criar o método GET, temos a seguinte tela:
Existem 4 blocos, 2 blocos no request e dois blocos no response. Vamos precisar configurar todos eles, segue abaixo o que precisa ser feito em cada um deles e no final teremos nosso redirecionador funcionando.
METHOD REQUEST
Neste bloco, apenas verifique se em request path, temos o valor codepath configurado conforme a figura abaixo:
INTEGRATION REQUEST
Neste bloco vamos tratar os dados de entrada para que eles estejam disponíveis para a função lambda. Em URL path parameter, mapeie o campo codepath para method.request.path.codepath
Em mapping template, crie um mapeamento application/json e deixe marcada a opção When no template matches the request Content-Type header. Isso significa que todo o bloco do body estará disponível para integração
Acima temos um json que configura as informações de entrada para que a variavel event do lambda, receba todos os dados da requisição, como path, querystring, body, header. O código completo encontra-se abaixo, pode copiar e colar
Este código é uma receita de bolo e pode ser utilizado sempre que voce criar uma integração entre API Gateway e Lambda. O código foi obtido do site: https://kennbrodhagen.net/2015/12/06/how-to-create-a-request-object-for-your-lambda-event-from-api-gateway/
METHOD RESPONSE
Neste bloco, precisamos criar o retorno 301 e mapear o header location. O resultado deve ficar conforme a imagem abaixo:
INTEGRATION RESPONSE
E por ultimo, tratamos o retorno da função lambda. Clique em add integration response e informe os dados conforme imagem abaixo.
Em lambda error regex, estamos identificando que todo retorno de erro que começe com HandlerDemo.ResponseFound.* será tratado por esta condição que faz o redirecionamento 301 da requisição.
No Header mappings, atribuimos como valor de Location
integration.response.body.errorType. Que é o mesmo valor retornado pela função lambda em err.name, ou seja, a URL de destino.
Com isso fechamos este passo a passo que ajuda quem precisa desenvolver um encurtador de URL e tambem quem ainda esta engatinhando na AWS, pois com uma simples solução tivemos contato com diversos serviços da cloud.
Próximo passo
- Desenvolva o expurgo das URLs curtas no dynamoDB utilizando o recurso TTL.
- Deixe seu API Gateway seguro utilizando authorizers
Espero que tenham gostado, o post ficou bem grande, mas é um case bem legal!