Se você tem acompanhado meu conteúdo, deve se lembrar que escrevi um artigo intitulado, Use o AWS Lambda e o API Gateway com Node.js e Couchbase NoSQL. Neste artigo, exploramos o uso dos serviços sem servidor da Amazon para criar funções Lambda que interagem com o Couchbase, nosso banco de dados NoSQL.
No entanto, o Lambda não é a única tecnologia sem servidor, também conhecida como funções como serviço (FaaS), no mercado. Veja Apache OpenWhisk por exemplo. Com o OpenWhisk, você pode criar funções de modo semelhante ao que faria com o Lambda, mas implantá-las em um conjunto mais diversificado de locais, sendo o mais popular o IBM Bluemix.
Veremos como criar funções sem servidor usando o OpenWhisk para se comunicar com nosso Servidor Couchbase banco de dados.
Em seguida, há alguns aspectos a serem observados. Você precisará hospedar o Couchbase Server em algum lugar acessível ao mundo exterior. Isso significa que seu computador local não funcionará. Você precisará do Docker para que possamos compilar nossas dependências para trabalhar com o OpenWhisk. Por fim, você precisará de uma conta Bluemix, pelo menos para este exemplo.
Instalação das ferramentas de CLI do Bluemix para o OpenWhisk
Como mencionei anteriormente, o OpenWhisk é um projeto da Apache Foundation. No entanto, por conveniência, vamos usá-lo no Bluemix da IBM.
Crie uma conta para o Nuvem IBM se você ainda não o fez.
Em vez de usar uma ferramenta de estrutura como o Serverless, usaremos a CLI do Bluemix. Faça o download do CLI do IBM Cloud Functions para podermos interagir com o OpenWhisk na IBM.
Antes de começar a trabalhar com sua conta IBM Cloud, é necessário fazer login por meio da CLI. Na linha de comando, execute o seguinte:
1 |
bx login -a API.ng.bluemix.rede -o seu_email@exemplo.com -s dev |
Ao fazer o download da CLI, você receberá o comando exato, mas ele deve ser semelhante ao que apresentei acima.
Agora podemos começar a criar nosso projeto.
Entendendo a estrutura do projeto e o processo de criação de pacotes do OpenWhisk
Se você nunca trabalhou com FaaS antes, as coisas são feitas de forma um pouco diferente da criação de um aplicativo autônomo e dificilmente escalável.
Por exemplo, cada endpoint em nosso projeto FaaS será uma função separada. Combinadas, essas funções criam o que é chamado de pacote. Essas funções são dimensionadas conforme necessário para atender às mudanças na demanda do seu aplicativo.
Dito isso, crie o seguinte:
1 2 3 4 5 6 7 8 9 10 11 12 |
criar --- pacote.json --- criar.js recuperar --- pacote.json --- recuperar.js atualização --- pacote.json --- atualização.js excluir --- pacote.json --- excluir.js |
O projeto deve ter um diretório para cada função que desejamos criar. Cada função terá seu próprio package.json arquivo. Cada package.json pode ser criado executando o seguinte em cada um dos diretórios:
1 |
npm inicial -y |
Em cada uma das package.json você também precisará definir qual arquivo é o seu código de função. Por exemplo, abrir create/package.json e adicione ou altere a seguinte linha:
1 |
"principal": "create.js", |
Ao definir o principal
estamos informando qual arquivo JavaScript contém nossa função.
Quando começarmos a implementar nossas funções, faremos isso de modo que elas façam parte do mesmo pacote.
Projetando uma função para criar dados
Vamos começar o desenvolvimento com a criação de dados em nosso banco de dados. Navegue até o diretório criar e execute o seguinte comando em sua linha de comando:
1 |
npm instalar couchbase uuid joi --salvar |
O comando acima instalará nossas dependências de funções. Usaremos o SDK do Couchbase para Node.js, a biblioteca UUID para gerar chaves exclusivas e a biblioteca Joi para validar a entrada.
Revisaremos a instalação da dependência, mas, pelo menos, ela nos ajudará por enquanto.
Agora, abra o arquivo criar/criar.js 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
const Couchbase = exigir("couchbase"); const UUID = exigir("uuid"); const Joi = exigir("joi"); var balde = nulo; função principal(parâmetros) { se(balde == nulo) { var agrupamento = novo Couchbase.Aglomerado("couchbase://" + parâmetros.hospedeiro); agrupamento.autenticar(parâmetros.nome de usuário, parâmetros.senha); balde = agrupamento.openBucket(parâmetros.bucketName); } var esquema = Joi.objeto().chaves({ primeiro nome: Joi.string().necessário(), sobrenome: Joi.string().necessário(), tipo: Joi.string().proibido().padrão("pessoa") }); var dados = parâmetros; var resposta = {}; retorno novo Promessa((resolver, rejeitar) => { var validação = Joi.validar(dados, esquema, { stripUnknown: verdadeiro }); se(validação.erro) { resposta = { statusCode: 500, corpo: JSON.stringify(validação.erro.detalhes) }; rejeitar(resposta); } var id = UUID.v4(); balde.inserir(id, validação.valor, (erro, resultado) => { se(erro) { resposta = { corpo: JSON.stringify({ código: erro.código, mensagem: erro.mensagem }) }; rejeitar(resposta); } dados.id = id; resposta = { corpo: JSON.stringify(validação.valor) }; resolver(resposta); }); }); } exportações.principal = principal; |
O código acima é um pouco exagerado, portanto, precisamos descobrir o que está acontecendo. Vamos começar com a variável que existe fora de nossa função:
1 |
var balde = nulo; |
Não é a melhor ideia estabelecer uma nova conexão toda vez que a função é chamada. Em vez disso, podemos manter uma instância global do Couchbase Bucket aberto e, enquanto ele existir, usá-lo. Observe que ele nem sempre existirá porque o OpenWhisk destruirá as funções após algum tempo de inatividade.
1 2 3 4 5 |
se(balde == nulo) { var agrupamento = novo Couchbase.Aglomerado("couchbase://" + parâmetros.hospedeiro); agrupamento.autenticar(parâmetros.nome de usuário, parâmetros.senha); balde = agrupamento.openBucket(parâmetros.bucketName); } |
Dentro de nossa função, verificamos se o Bucket já está aberto. Se o Bucket não estiver aberto, estabelecemos uma conexão usando os parâmetros passados para a função. Quando chegar a hora, definiremos parâmetros padrão que contêm essas informações de conexão.
Como estamos criando dados, precisamos validar se a entrada está correta.
1 2 3 4 5 |
var esquema = Joi.objeto().chaves({ primeiro nome: Joi.string().necessário(), sobrenome: Joi.string().necessário(), tipo: Joi.string().proibido().padrão("pessoa") }); |
Estamos esperando um primeiro nome
e sobrenome
esteja presente. Também estamos esperando um tipo
não esteja presente. Podemos validar isso com o seguinte:
1 2 3 4 5 6 7 8 |
var validação = Joi.validar(dados, esquema, { stripUnknown: verdadeiro }); se(validação.erro) { resposta = { statusCode: 500, corpo: JSON.stringify(validação.erro.detalhes) }; rejeitar(resposta); } |
O stripUnknown
removerá os dados não definidos no esquema. Precisamos remover os dados porque nossas informações de entrada e conexão existirão na mesma carga útil. Não queremos que as informações de conexão sejam salvas em nossos documentos. Se houver um erro de validação, ele será retornado. Se não houver erro de validação, podemos prosseguir com a inserção dos dados.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var id = UUID.v4(); balde.inserir(id, validação.valor, (erro, resultado) => { se(erro) { resposta = { corpo: JSON.stringify({ código: erro.código, mensagem: erro.mensagem }) }; rejeitar(resposta); } dados.id = id; resposta = { corpo: JSON.stringify(validação.valor) }; resolver(resposta); }); |
Podemos gerar uma nova chave exclusiva e salvar os dados validados como um documento. Os dados em si serão retornados como uma resposta.
As outras funções seguirão essa mesma estratégia, mais ou menos.
Projetando uma função para recuperar dados com N1QL
Agora que temos dados, vamos tentar recuperá-los do banco de dados com uma invocação de função. Navegue até seu recuperar e execute o seguinte na linha de comando:
1 |
npm instalar couchbase --salvar |
Como não criaremos dados, não precisamos gerar valores exclusivos nem validar nenhum dado do usuário. Por esse motivo, precisamos apenas do SDK do Couchbase para essa função.
Abra o arquivo recuperar/retrieve.js 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 32 33 |
const Couchbase = exigir("couchbase"); var balde = nulo; função principal(parâmetros) { se(balde == nulo) { var agrupamento = novo Couchbase.Aglomerado("couchbase://" + parâmetros.hospedeiro); agrupamento.autenticar(parâmetros.nome de usuário, parâmetros.senha); balde = agrupamento.openBucket(parâmetros.bucketName); } var resposta = {}; var declaração = "SELECT META().id, `" + balde._nome + "`.* FROM `" + balde._nome + "` WHERE type = 'person'"; var consulta = Couchbase.N1qlQuery.fromString(declaração); retorno novo Promessa((resolver, rejeitar) => { balde.consulta(consulta, (erro, resultado) => { se(erro) { resposta = { corpo: JSON.stringify({ código: erro.código, mensagem: erro.mensagem }) }; rejeitar(resposta); } resposta = { corpo: JSON.stringify(resultado) }; resolver(resposta); }); }); } exportações.principal = principal; |
Vamos pular o que já vimos na função anterior e pular para o que há de novo. Quando estivermos conectados a um Bucket aberto, poderemos criar uma consulta N1QL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var declaração = "SELECT META().id, `" + balde._nome + "`.* FROM `" + balde._nome + "` WHERE type = 'person'"; var consulta = Couchbase.N1qlQuery.fromString(declaração); retorno novo Promessa((resolver, rejeitar) => { balde.consulta(consulta, (erro, resultado) => { se(erro) { resposta = { corpo: JSON.stringify({ código: erro.código, mensagem: erro.mensagem }) }; rejeitar(resposta); } resposta = { corpo: JSON.stringify(resultado) }; resolver(resposta); }); }); |
Essa consulta N1QL é do tipo SQL e nos permitirá recuperar todos os documentos que correspondem a determinados critérios. Se houver algum erro, retorne-o como resposta; caso contrário, retorne o conjunto de resultados.
Como não estamos validando nada, essa função de recuperação de dados foi muito mais simples.
Projetando uma função para atualizar dados com mutações de subdocumentos
Agora, digamos que queiramos atualizar documentos no banco de dados. Em vez de recuperar documentos, fazer alterações e salvá-las, enviaremos as alterações diretamente ao banco de dados e deixaremos que ele resolva o problema.
Navegue até a seção atualização e execute o seguinte na linha de comando:
1 |
npm instalar couchbase joi --salvar |
Como estamos aceitando dados de usuários, queremos validar esses dados. Como não estamos criando dados, não precisamos gerar nenhuma chave exclusiva.
Abra o arquivo update/update.js 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
const Couchbase = exigir("couchbase"); const Joi = exigir("joi"); var balde = nulo; função principal(parâmetros) { se(balde == nulo) { var agrupamento = novo Couchbase.Aglomerado("couchbase://" + parâmetros.hospedeiro); agrupamento.autenticar(parâmetros.nome de usuário, parâmetros.senha); balde = agrupamento.openBucket(parâmetros.bucketName); } var esquema = Joi.objeto().chaves({ id: Joi.string().necessário(), primeiro nome: Joi.string().opcional(), sobrenome: Joi.string().opcional() }); var dados = parâmetros; var resposta = {}; retorno novo Promessa((resolver, rejeitar) => { var validação = Joi.validar(dados, esquema, { stripUnknown: verdadeiro }); se(validação.erro) { resposta = { statusCode: 500, corpo: JSON.stringify(validação.erro.detalhes) }; rejeitar(resposta); } var construtor = balde.mutateIn(validação.valor.id); se(validação.valor.primeiro nome) { construtor.substituir("firstname", validação.valor.primeiro nome); } se(validação.valor.sobrenome) { construtor.substituir("lastname" (sobrenome), validação.valor.sobrenome); } construtor.executar((erro, resultado) => { se(erro) { resposta = { statusCode: 500, corpo: JSON.stringify({ código: erro.código, mensagem: erro.mensagem }) }; rejeitar(resposta); } resposta = { statusCode: 200, corpo: JSON.stringify(validação.valor) }; resolver(resposta); }); }); } exportações.principal = principal; |
O código acima lhe parece familiar? Deveria, porque estamos seguindo a mesma estratégia.
Nossa lógica de validação é ligeiramente diferente neste exemplo:
1 2 3 4 5 |
var esquema = Joi.objeto().chaves({ id: Joi.string().necessário(), primeiro nome: Joi.string().opcional(), sobrenome: Joi.string().opcional() }); |
Queremos editar um documento específico, portanto, uma chave é necessária. Como não sabemos o que o usuário deseja atualizar, definimos as propriedades como opcionais.
Para fazer atualizações, faremos operações de subdocumento em nossos documentos. Para fazer isso, podemos usar um construtor de mutação.
1 2 3 4 5 6 7 |
var construtor = balde.mutateIn(validação.valor.id); se(validação.valor.primeiro nome) { construtor.substituir("firstname", validação.valor.primeiro nome); } se(validação.valor.sobrenome) { construtor.substituir("lastname" (sobrenome), validação.valor.sobrenome); } |
Fornecemos um documento para alterar e quaisquer caminhos nos quais as propriedades existam. Os caminhos podem ser muito mais complexos do que os exemplos usados aqui.
Com o conjunto de mutações definido, podemos executá-las no banco de dados.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
construtor.executar((erro, resultado) => { se(erro) { resposta = { statusCode: 500, corpo: JSON.stringify({ código: erro.código, mensagem: erro.mensagem }) }; rejeitar(resposta); } resposta = { statusCode: 200, corpo: JSON.stringify(validação.valor) }; resolver(resposta); }); |
Dependendo do resultado, uma resposta será retornada da invocação da função.
Projetando uma função para remoção de dados
Estamos em nossa função final em um pacote de operações CRUD. Chegou a hora de excluir dados do banco de dados.
Navegue até a seção excluir e execute o seguinte comando:
1 |
npm instalar couchbase joi --salvar |
Aceitaremos a exclusão de chaves de documentos, portanto, precisaremos validar a entrada. Da mesma forma, também precisamos do Couchbase SDK para trabalhar com o banco de dados.
Abra o arquivo delete/delete.js e inclua o seguinte código JavaScript:
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 42 43 44 |
const Couchbase = exigir("couchbase"); const Joi = exigir("joi"); var balde = nulo; função principal(parâmetros) { se(balde == nulo) { var agrupamento = novo Couchbase.Aglomerado("couchbase://" + parâmetros.hospedeiro); agrupamento.autenticar(parâmetros.nome de usuário, parâmetros.senha); balde = agrupamento.openBucket(parâmetros.bucketName); } var esquema = Joi.objeto().chaves({ id: Joi.string().necessário() }); var dados = parâmetros; var resposta = {}; retorno novo Promessa((resolver, rejeitar) => { var validação = Joi.validar(dados, esquema, { stripUnknown: verdadeiro }); se(validação.erro) { resposta = { statusCode: 500, corpo: JSON.stringify(validação.erro.detalhes) }; rejeitar(resposta); } balde.remover(validação.valor.id, (erro, resultado) => { se(erro) { resposta = { corpo: JSON.stringify({ código: erro.código, mensagem: erro.mensagem }) }; rejeitar(resposta); } resposta = { corpo: JSON.stringify(validação.valor) }; resolver(resposta); }); }); } exportações.principal = principal; |
Você provavelmente está vendo o panorama geral agora em relação à criação de funções com o OpenWhisk e o Couchbase, portanto, não vamos analisar a função acima para excluir documentos.
Empacotamento e implementação das funções no OpenWhisk com o Docker
Temos um conjunto de funções prontas para uso, mas não podemos simplesmente empacotá-las e implantá-las no Bluemix. Se fizéssemos isso, teríamos uma série de erros. O Bluemix usa uma versão especial do Linux com uma determinada arquitetura. Baixei as dependências em meu Mac, que não é compatível.
Lembre-se do artigo que escrevi há algum tempo, intitulado, Implantação de dependências nativas do Node.js no AWS Lambda? Precisamos fazer algo semelhante para o OpenWhisk com o Docker.
Com o Docker instalado e pronto para funcionar, execute o seguinte na CLI:
1 2 |
doca puxar openwhisk/nodejs6action doca executar -ele -v /Usuários/garoto/Desktop/couchbase-openwhisk:/projeto openwhisk/nodejs6action /caixa/bash |
Os comandos acima farão o download de uma imagem apropriada do OpenWhisk Docker para o Node.js. Em seguida, implantamos um contêiner com essa imagem no modo de terminal interativo. Esse contêiner também terá um volume mapeado. Estou mapeando meu diretório de projeto local para um diretório dentro do contêiner.
Depois que o comando for executado e o contêiner for implantado, você deverá estar no shell dentro do contêiner.
Para cada função, execute o seguinte:
1 2 |
cd /projeto/criar npm instalar |
Lembre-se de que instalar as dependências de nosso computador host não é suficiente. Precisamos compilar as dependências para o Bluemix. O Docker compilará essas dependências e, como o diretório está mapeado, podemos usá-las no computador host.
Depois que todos os pacotes de funções forem instalados, podemos agrupá-los e implantá-los.
No computador host, crie um arquivo ZIP de cada uma das funções. O arquivo deve conter os arquivos package.json o arquivo JavaScript e o arquivo módulos_nó diretório.
Se você estiver em um Mac ou computador com uma CLI do ZIP, execute o seguinte:
1 2 |
cd criar zíper -r criar.zip * |
Quando você tiver um ZIP de cada função, eles poderão ser implantados executando o seguinte:
1 |
bx wsk ação criar couchbase/excluir --gentil nodejs:padrão excluir.zíper -p hospedeiro ec2-45-236-32-140.computar-1.amazonaws.com -p nome de usuário demonstração -p senha bluemix -p bucketName exemplo |
Introduzi algumas coisas novas no comando acima.
Primeiro, estamos criando um pacote chamado couchbase e, nesse pacote, temos um excluir que é baseada na função delete.zip arquivo. Também estou passando alguns parâmetros padrão. Esses parâmetros serão nossas informações de conexão. Como essas informações são confidenciais, não as estamos passando ao invocar a função, mas sim ao criar a função.
Execute uma variação do comando acima para cada uma de suas funções.
Para executar sua função, tente executar algo como o seguinte:
1 |
bx wsk ação invocar couchbase/criar -p primeiro nome Nic -p sobrenome Raboy --bloqueio |
O comando acima deve passar alguns parâmetros para passar pela validação. A função é chamada de forma bloqueada e, se for bem-sucedida, nossos dados serão salvos no banco de dados e retornados na resposta.
Conclusão
Você acabou de ver como criar um pacote de funções sem servidor para o OpenWhisk que se comunica com o banco de dados NoSQL, Couchbase. O OpenWhisk pode ser usado como uma alternativa ao AWS Lambda, mas os dois certamente não são as únicas opções disponíveis. Independentemente da sua escolha, as funções como serviço (FaaS) são soluções muito escalonáveis para aplicativos de grande porte.
Deseja ver outro exemplo do OpenWhisk? Dê uma olhada em um tutorial que escrevi com o título, Converta uma API RESTful do Node.js em Serverless com o OpenWhisk.
Oi 👋 Nick, ótimo post
Aprendi algo novo hoje sobre o pacote joi 👍🏼
Algumas dicas
Você pode tornar suas ações uma ação da Web e ter um URL público ao criar -web true
Além disso, você pode ignorar o JSON.stringify e definir o corpo como um objeto que será stringify para você.
Para obter o URL público da ação da Web, você pode executar wsk action get couchbase/delete -url
Você pode ler mais sobre o uso de ações da Web nos documentos aqui https://console.bluemix.net/docs/openwhisk/openwhisk_webactions.html
Obrigado pelos comentários!
[...] Use o OpenWhisk para FaaS com Node.js e Couchbase NoSQL [...]