Tem havido muito burburinho em torno das funções como serviço (FaaS), comumente chamadas de sem servidor. Um provedor popular para essas funções é a Amazon com seu AWS Lambda serviço. É possível criar uma função em qualquer uma das tecnologias Lambda compatíveis, por exemplo, Node.js, implantá-la no AWS Lambda, acessá-la por meio do AWS API Gateway e escaloná-la por conta própria para atender à demanda.
Veremos como usar o Node.js com o AWS Lambda para criar funções que podem se comunicar com documentos NoSQL no formato Servidor Couchbase.
A suposição daqui para frente é que você tenha um cluster ou uma única instância do Couchbase Server em execução em algum lugar na nuvem. Em outras palavras, este projeto não funcionará se você estiver executando o Couchbase em seu computador local. Como estamos executando isso no AWS, pode fazer sentido que o Couchbase exista como uma instância do EC2. Isso pode ser feito por usando uma AMI disponível.
Criação de um projeto FaaS para Lambda e API Gateway com a estrutura sem servidor
Se você já trabalhou com o AWS Lambda e o AWS API Gateway antes, sabe que não é a experiência mais agradável começar algo do zero e depois implantar. Para facilitar um pouco nossa vida, usaremos o Estrutura sem servidorque cuidará de grande parte do trabalho pesado, sem alterar a forma como desenvolvemos as funções Lambda.
Supondo que você já tenha o Node.js instalado, execute o seguinte para obter a CLI do Serverless Framework:
|
1 |
npm install -g serverless |
Com a CLI, podemos gerar novos projetos com base em modelos disponíveis, não limitados ao AWS. Para criar um novo projeto com a CLI, execute o seguinte:
|
1 |
serverless create --template aws-nodejs --path ./my-project |
Estaremos criando várias funções dentro do meu projeto que foi criado. É aqui que as coisas podem ficar um pouco estranhas.
Nosso projeto Node.js exigirá o SDK do Couchbase, um pacote para gerar valores UUID e um pacote para validação de dados. O problema é que estou usando um Mac e o Lambda está usando uma versão especial do Linux. Se eu tentar fazer o download das dependências no meu Mac, elas não serão compatíveis com o Linux. Em vez disso, temos que passar por alguns obstáculos.
Há um artigo on-line intitulado, Uso de dependências nativas com o AWS Lambdaque explica como o Docker pode ser usado para realizar a tarefa de dependências de projeto nativas que funcionam para o Lambda.
Em resumo, você deve executar o seguinte com o Docker instalado e com a CLI no diretório atual do projeto:
|
1 2 |
docker pull amazonlinux docker run -v $(pwd):/lambda-project -it amazonlinux |
Os comandos acima extrairiam a imagem do Amazon Linux e implantariam um contêiner, mapeando o diretório atual do projeto como um volume de contêiner. Dentro do contêiner, o Node.js pode ser instalado e as dependências do nosso pacote podem ser obtidas. Para ver como fazer isso, confira um artigo anterior que escrevi intitulado, Implantação de dependências nativas do Node.js no AWS Lambda.
Você precisará instalar as três dependências a seguir para o projeto:
|
1 2 3 |
npm install couchbase --save npm install joi --save npm install uuid --save |
Não desanime de continuar por causa do requisito do Amazon Linux. Basicamente, você está apenas executando os três comandos de dentro do contêiner e, como o contêiner tem um volume mapeado, todos os arquivos obtidos acabarão diretamente no seu projeto.
Desenvolvimento de funções para operações CRUD no banco de dados NoSQL
Com o projeto Serverless Framework pronto, podemos nos concentrar no que realmente importa. O objetivo aqui é criar quatro funções diferentes, uma para cada criação, recuperação, atualização e exclusão de documentos NoSQL do Couchbase. É isso mesmo, estamos criando um conjunto de funções CRUD.
Abra o arquivo handler.js porque precisamos fazer o bootstrap:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
'use strict'; const Couchbase = require("couchbase"); const UUID = require("uuid"); const Joi = require("joi"); var cluster = new Couchbase.Cluster("couchbase://your-server-here"); cluster.authenticate("demo", "123456"); var bucket = cluster.openBucket("example"); bucket.on("error", error => { console.dir(error); }); module.exports.create = (event, context, callback) => { }; module.exports.retrieve = (event, context, callback) => { }; module.exports.update = (event, context, callback) => { }; module.exports.delete = (event, context, callback) => { }; |
No código acima, importamos as dependências do nosso projeto, nos conectamos a um cluster do Couchbase, nos autenticamos nesse cluster e abrimos um determinado Bucket. Certifique-se de alterar as informações do Couchbase para refletir suas informações reais.
Também criamos as quatro funções mencionadas anteriormente. No entanto, até o momento, essas funções não fazem nada.
Algo importante a ser observado aqui. Nossa lógica de conexão com o banco de dados está acontecendo fora de nossas funções. É uma tarefa dispendiosa conectar-se ao banco de dados a cada invocação de uma função. Ao mover essas informações para fora, elas ainda podem ser acessadas por qualquer contêiner de função Lambda.
Começando com o criar função:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
module.exports.create = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false; var schema = Joi.object().keys({ firstname: Joi.string().required(), lastname: Joi.string().required(), type: Joi.string().forbidden().default("person") }); var data = JSON.parse(event.body); var response = {}; var validation = Joi.validate(data, schema); if(validation.error) { response = { statusCode: 500, body: JSON.stringify(validation.error.details) }; return callback(null, response); } var id = UUID.v4(); bucket.insert(id, validation.value, (error, result) => { if(error) { response = { statusCode: 500, body: JSON.stringify({ code: error.code, message: error.message }) }; return callback(null, response); } data.id = id; response = { statusCode: 200, body: JSON.stringify(data) }; callback(null, response); }); }; |
Há muita coisa acontecendo no código acima, portanto, precisamos dividi-lo.
Primeiro, estamos adicionando a seguinte linha:
|
1 |
context.callbackWaitsForEmptyEventLoop = false; |
Ao adicionar a linha acima, estamos mudando a forma como a função espera para responder. Se não a definirmos como false, as coisas assíncronas a seguir provavelmente atingirão o tempo limite antes da conclusão.
|
1 2 3 4 5 6 7 8 |
var schema = Joi.object().keys({ firstname: Joi.string().required(), lastname: Joi.string().required(), type: Joi.string().forbidden().default("person") }); var data = JSON.parse(event.body); var response = {}; var validation = Joi.validate(data, schema); |
Como criaremos dados com base na entrada do usuário, é uma boa ideia validar essa entrada do usuário. Ao usar JoiSe o evento for um evento, podemos criar um esquema de validação e usá-lo para validar o corpo de dados passado com o evento. Talvez você reconheça que o Joi é uma estrutura de validação que usei em artigos anteriores, como, Criar uma API RESTful com Node.js, Hapi e Couchbase NoSQL.
|
1 2 3 4 5 6 7 |
if(validation.error) { response = { statusCode: 500, body: JSON.stringify(validation.error.details) }; return callback(null, response); } |
Se houver um erro de validação, enviaremos um erro 500 como resposta. Caso contrário, prosseguiremos com a geração de uma nova chave de documento e a salvaremos no Couchbase Server:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var id = UUID.v4(); bucket.insert(id, validation.value, (error, result) => { if(error) { response = { statusCode: 500, body: JSON.stringify({ code: error.code, message: error.message }) }; return callback(null, response); } data.id = id; response = { statusCode: 200, body: JSON.stringify(data) }; callback(null, response); }); |
Observe como o Lambda espera que as respostas sejam formatadas. As respostas devem ter um statusCode e um corpo.
Agora vamos dar uma olhada no recuperar função:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
module.exports.retrieve = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false; var response = {}; var statement = "SELECT META().id, `" + bucket._name + "`.* FROM `" + bucket._name + "` WHERE type = 'person'"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, (error, result) => { if(error) { response = { statusCode: 500, body: JSON.stringify({ code: error.code, message: error.message }) }; return callback(null, response); } response = { statusCode: 200, body: JSON.stringify(result) }; callback(null, response); }); }; |
O objetivo com o recuperar A função Lambda é usar o N1QL para consultar todos os documentos no Couchbase e retorná-los. Dependendo do resultado, formataremos a resposta do Lambda de forma apropriada. Observe que estamos alterando o callbackWaitsForEmptyEventLoop valor. Isso ocorrerá em cada uma de nossas funções.
Como estruturamos o atualização será semelhante à forma como estruturamos a função criar função:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
module.exports.update = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false; var schema = Joi.object().keys({ firstname: Joi.string().optional(), lastname: Joi.string().optional() }); var data = JSON.parse(event.body); var response = {}; var validation = Joi.validate(data, schema); if(validation.error) { response = { statusCode: 500, body: JSON.stringify(validation.error.details) }; return callback(null, response); } var builder = bucket.mutateIn(event.pathParameters.id); if(data.firstname) { builder.replace("firstname", data.firstname); } if(data.lastname) { builder.replace("lastname", data.lastname); } builder.execute((error, result) => { if(error) { response = { statusCode: 500, body: JSON.stringify({ code: error.code, message: error.message }) }; return callback(null, response); } response = { statusCode: 200, body: JSON.stringify(data) }; callback(null, response); }); }; |
Observe que começamos fazendo alguma validação de dados com base em nosso esquema. Se os dados forem considerados válidos, poderemos prosseguir com as operações de subdocumentos no Couchbase:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var builder = bucket.mutateIn(event.pathParameters.id); if(data.firstname) { builder.replace("firstname", data.firstname); } if(data.lastname) { builder.replace("lastname", data.lastname); } builder.execute((error, result) => { if(error) { response = { statusCode: 500, body: JSON.stringify({ code: error.code, message: error.message }) }; return callback(null, response); } response = { statusCode: 200, body: JSON.stringify(data) }; callback(null, response); }); |
Basicamente, estamos criando um construtor de mutações com base nas informações encontradas na solicitação. As mutações adicionadas ocorrerão em relação a uma chave quando executadas. O resultado da execução será retornado como uma resposta do Lambda.
Observe o seguinte:
|
1 |
event.pathParameters.id |
Esses são dados que não vêm do corpo. Ele será configurado em uma das próximas etapas.
Agora ficamos com o excluir função:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
module.exports.delete = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false; var schema = Joi.object().keys({ id: Joi.string().required() }); var data = JSON.parse(event.body); var response = {}; var validation = Joi.validate(data, schema); if(validation.error) { response = { statusCode: 500, body: JSON.stringify(validation.error.details) }; return callback(null, response); } bucket.remove(data.id, (error, result) => { if(error) { response = { statusCode: 500, body: JSON.stringify({ code: error.code, message: error.message }) }; return callback(null, response); } response = { statusCode: 200, body: JSON.stringify(data) }; callback(null, response); }); }; |
Se você acompanhou tudo até agora, o excluir não parecerá nada novo para você. Estamos validando dados e executando-os no banco de dados.
Com as funções criadas, podemos nos concentrar nas informações de configuração da Serverless Framework. Abra a seção serverless.yml e inclua o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
service: couchbase-lambda provider: name: aws runtime: nodejs6.10 functions: create: handler: handler.create events: - http: path: create method: post retrieve: handler: handler.retrieve events: - http: path: retrieve method: get update: handler: handler.update events: - http: path: update/{id} method: put delete: handler: handler.delete events: - http: path: delete method: delete |
A parte mais importante do documento YAML acima são as funções. Nomeamos cada função e as vinculamos à função de controle apropriada. A função eventos são como essas funções serão invocadas em um navegador. O eventos são a configuração do AWS API Gateway.
No eventos definimos as informações do caminho, como cada endpoint pode ser acessado, seja a partir de uma solicitação GET ou POST ou qualquer outra coisa, e qualquer tipo de variável de caminho.
Lembra-se da frase que eu lhe disse para lembrar?
|
1 |
event.pathParameters.id |
Isso é mapeado para o id no valor update/{id} caminho.
Nesse momento, o projeto está pronto para ser implantado no Amazon Web Services.
Implementação das funções no AWS Lambda com suporte ao API Gateway
Não entraremos em detalhes quando se trata de configurar o Serverless Framework com suas chaves públicas e privadas da Amazon, mas nos preocuparemos com a implantação, supondo que elas estejam configuradas.
Na CLI do Serverless Framework, execute o seguinte:
|
1 |
serverless deploy |
O comando acima utilizará o código das funções, bem como o módulos_nó que foram gerados a partir do contêiner do Amazon Linux Docker e enviá-los para a nuvem do AWS. O Serverless Framework se preocupará em configurar o AWS Lambda e o API Gateway com base no que estava no contêiner serverless.yml arquivo.
Conclusão
Você acabou de ver como se comunicar com Servidor Couchbase de um conjunto de funções controladas pelo AWS Lambda e pelo AWS API Gateway. Isso é útil se você quiser criar aplicativos que são cobrados pelo tempo em que são usados, e não pelo tempo em que estão ativos. Incluir o Couchbase não foi muito diferente de incluí-lo em qualquer outro aplicativo Node.js, com exceção das dependências nativas do Node.js.
Se você decidir usar o Couchbase na nuvem, certifique-se de que as portas corretas estejam abertas para o Lambda. Cometi o erro de esquecer de abrir todas as portas exigidas pelo SDK e isso me atrasou um pouco.
Para obter mais informações sobre como usar o Couchbase Server, consulte o Portal do desenvolvedor do Couchbase.
Oi Nic,
Estou tentando colocar isso em funcionamento com um exemplo que apenas se conecta a um servidor couchbase (no AWS) e tem uma função que retorna um texto de teste.
Ele está apresentando este erro:
"errorMessage": "/var/lang/lib/libstdc++.so.6: a versão `CXXABI_1.3.9′ não foi encontrada (exigida por /var/task/node_modules/couchbase/build/Release/couchbase_impl.node)",
e meu google-fu está falhando. Você sabe qual é o problema?
Preciso atualizar o c++ na imagem da janela de encaixe?
No entanto, parece que é um problema de versão do compilador na AWS.