Desenvolvimento local com MongoDB Atlas CLI e Docker
Avalie esse Tutorial
Precisa de uma experiência consistente de desenvolvimento e implantação, pois os desenvolvedores trabalham em equipes e usam máquinas diferentes para suas tarefas diárias? É aqui que o Docker te salva com os contêineres. Uma experiência comum pode incluir executar uma versão local do MongoDB Community em um contêiner e um aplicativo em outro contêiner. Essa estratégia funciona para algumas organizações, mas e se você quiser aproveitar todos os benefícios que vêm com o MongoDB Atlas, além de uma estratégia de contêiner para o desenvolvimento de seu aplicativo?
Neste tutorial, veremos como criar um aplicativo da Web compatível com o MongoDB, agrupá-lo em um contêiner com o Docker e gerenciar a criação e a destruição do MongoDB Atlas com o Atlas CLI durante a implantação do contêiner.
Deve-se observar que este tutorial foi criado para uma configuração de desenvolvimento ou teste em seu computador local. Não é aconselhável usar todas as técnicas encontradas neste tutorial em uma configuração de produção. Avalie cuidadosamente quando se trata do código incluído.
Há muitas partes dinâmicas neste tutorial, portanto, você precisará de algumas coisas antes para ser bem-sucedido:
- Uma conta do MongoDB Atlas
- Alguma familiaridade com Node.js e JavaScript
O Atlas CLI pode criar uma conta Atlas para você junto com quaisquer chaves e IDs, mas para o escopo deste tutorial, você precisará de uma criada junto com acesso rápido à "Chave de API pública", "Chave de API privada", "Organização ID" e "ID do projeto" na sua conta. Você pode ver como fazer isso na documentação.
O Docker será o grande foco deste tutorial. Você não precisa de nada além do Docker aplicativo Node.js e a Atlas CLI serão gerenciados pelo contêiner Docker, não pelo seu computador host.
Em seu computador host, crie um diretório de projeto. O nome não é importante, mas, para este tutorial, usaremos mongodbexample como diretório de projeto.
Vamos começar criando um aplicativo Node.js que se comunica com o MongoDB usando o driver Node.js para MongoDB. O aplicativo será simples em termos de funcionalidade. Ele se conectará ao MongoDB, criará um banco de dados e uma coleção, inserirá um documento e exibirá um endpoint da API para mostrar o documento com uma solicitação HTTP.
No diretório do projeto, crie um novo diretório de aplicativo para o aplicativo Node.js funcionar. No diretório do aplicativo, usando uma linha de comando, execute o seguinte:
1 npm init -y 2 npm install express mongodb
Se você não tiver o Node.js instalado, basta criar um arquivo package.json dentro do diretório app com o seguinte conteúdo:
1 { 2 "name": "mongodbexample", 3 "version": "1.0.0", 4 "description": "", 5 "main": "main.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1", 8 "start": "node main.js" 9 }, 10 "keywords": [], 11 "author": "", 12 "license": "ISC", 13 "dependencies": { 14 "express": "^4.18.2", 15 "mongodb": "^4.12.1" 16 } 17 }
Em seguida, precisaremos definir a lógica do nosso aplicativo. Dentro do diretório app, precisamos criar um arquivo main.js. Dentro do main.js, adicione o seguinte código JavaScript:
1 const { MongoClient } = require("mongodb"); 2 const Express = require("express"); 3 4 const app = Express(); 5 6 const mongoClient = new MongoClient(process.env.MONGODB_ATLAS_URI); 7 let database, collection; 8 9 app.get("/data", async (request, response) => { 10 try { 11 const results = await collection.find({}).limit(5).toArray(); 12 response.send(results); 13 } catch (error) { 14 response.status(500).send({ "message": error.message }); 15 } 16 }); 17 18 const server = app.listen(3000, async () => { 19 try { 20 await mongoClient.connect(); 21 database = mongoClient.db(process.env.MONGODB_DATABASE); 22 collection = database.collection(`${process.env.MONGODB_COLLECTION}`); 23 collection.insertOne({ "firstname": "Nic", "lastname": "Raboy" }); 24 console.log("Listening at :3000"); 25 } catch (error) { 26 console.error(error); 27 } 28 }); 29 30 process.on("SIGTERM", async () => { 31 if(process.env.CLEANUP_ONDESTROY == "true") { 32 await database.dropDatabase(); 33 } 34 mongoClient.close(); 35 server.close(() => { 36 console.log("NODE APPLICATION TERMINATED!"); 37 }); 38 });
Há muita coisa acontecendo nas poucas linhas de código acima. Vamos detalhá-lo!
Antes de detalharmos as partes, anote as variáveis de ambiente usadas em todo o código JavaScript. No final, passaremos esses valores pelo Docker para que tenhamos uma experiência mais dinâmica com o nosso desenvolvimento local.
O primeiro trecho importante de código a ser enfocado é o início do nosso serviço de aplicativo:
1 const server = app.listen(3000, async () => { 2 try { 3 await mongoClient.connect(); 4 database = mongoClient.db(process.env.MONGODB_DATABASE); 5 collection = database.collection(`${process.env.MONGODB_COLLECTION}`); 6 collection.insertOne({ "firstname": "Nic", "lastname": "Raboy" }); 7 console.log("Listening at :3000"); 8 } catch (error) { 9 console.error(error); 10 } 11 });
Usando o cliente que foi configurado perto da parte superior do arquivo, podemos nos conectar ao MongoDB. Uma vez conectados, podemos obter uma referência a um banco de dados e coleção. Esse banco de dados e coleção não precisa existir antes disso, pois ele será criado automaticamente quando os dados forem inseridos. Com referência a uma coleção, inserimos um documento e começamos a ouvir solicitações de API por meio de HTTP.
Isso nos leva ao nosso único endpoint:
1 app.get("/data", async (request, response) => { 2 try { 3 const results = await collection.find({}).limit(5).toArray(); 4 response.send(results); 5 } catch (error) { 6 response.status(500).send({ "message": error.message }); 7 } 8 });
Quando o endpoint
/data
é consumido, os cinco primeiros documento de nossa coleção são retornados ao usuário. Caso contrário, se houver algum problema, uma mensagem de erro será retornada.Isso nos leva a algo opcional, mas potencialmente valioso quando se trata de uma implantação do Docker para desenvolvimento local:
1 process.on("SIGTERM", async () => { 2 if(process.env.CLEANUP_ONDESTROY == "true") { 3 await database.dropDatabase(); 4 } 5 mongoClient.close(); 6 server.close(() => { 7 console.log("NODE APPLICATION TERMINATED!"); 8 }); 9 });
O código acima diz que, quando um evento de encerramento for enviado para o aplicativo, descarte o banco de dados que criamos e feche a conexão com o MongoDB, bem como com o serviço Express Framework. Isso pode ser útil se quisermos desfazer tudo o que criamos quando o contêiner parar. Se você deseja que suas alterações persistam, talvez não seja necessário. Por exemplo, se você quiser que seus dados existam entre as implantações de contêineres, a persistência será necessária. Por outro lado, talvez você esteja usando o contêiner como parte de um pipeline de teste e queira limpá-lo quando terminar, os comandos de encerramento podem ser valiosos.
Portanto, temos um aplicativo Node.js com muitas variáveis de ambiente. O que vem a seguir?
Embora tenhamos o aplicativo, nosso cluster do MongoDB Atlas pode não estar disponível para nós. Por exemplo, talvez esta seja a primeira vez que somos expostos ao Atlas e nada foi criado ainda. Precisamos ser capazes de criar um cluster de forma rápida e fácil, configurar nossas regras de acesso IP, especificar usuários e permissões e, em seguida, conectar-se ao nosso aplicativo Node.js.
É aqui que o CLI do MongoDB Atlas faz o trabalho pesado!
Há muitas maneiras diferentes de criar um script. Alguns curtem o Bash, outros curtem o ZSH ou outra coisa. Vamos usar o ZX, que é um wrapper JavaScript para o Bash.
No diretório do projeto, e não no diretório do aplicativo, crie um arquivo docker_run_script.mjs com o seguinte código:
1 #!/usr/bin/env zx 2 3 $.verbose = true; 4 5 const runtimeTimestamp = Date.now(); 6 7 process.env.MONGODB_CLUSTER_NAME = process.env.MONGODB_CLUSTER_NAME || "examples"; 8 process.env.MONGODB_USERNAME = process.env.MONGODB_USERNAME || "demo"; 9 process.env.MONGODB_PASSWORD = process.env.MONGODB_PASSWORD || "password1234"; 10 process.env.MONGODB_DATABASE = process.env.MONGODB_DATABASE || "business_" + runtimeTimestamp; 11 process.env.MONGODB_COLLECTION = process.env.MONGODB_COLLECTION || "people_" + runtimeTimestamp; 12 process.env.CLEANUP_ONDESTROY = process.env.CLEANUP_ONDESTROY || false; 13 14 var app; 15 16 process.on("SIGTERM", () => { 17 app.kill("SIGTERM"); 18 }); 19 20 try { 21 let createClusterResult = await $`atlas clusters create ${process.env.MONGODB_CLUSTER_NAME} --tier M0 --provider AWS --region US_EAST_1 --output json`; 22 await $`atlas clusters watch ${process.env.MONGODB_CLUSTER_NAME}` 23 let loadSampleDataResult = await $`atlas clusters loadSampleData ${process.env.MONGODB_CLUSTER_NAME} --output json`; 24 } catch (error) { 25 console.log(error.stdout); 26 } 27 28 try { 29 let createAccessListResult = await $`atlas accessLists create --currentIp --output json`; 30 let createDatabaseUserResult = await $`atlas dbusers create --role readWriteAnyDatabase,dbAdminAnyDatabase --username ${process.env.MONGODB_USERNAME} --password ${process.env.MONGODB_PASSWORD} --output json`; 31 await $`sleep 10` 32 } catch (error) { 33 console.log(error.stdout); 34 } 35 36 try { 37 let connectionString = await $`atlas clusters connectionStrings describe ${process.env.MONGODB_CLUSTER_NAME} --output json`; 38 let parsedConnectionString = new URL(JSON.parse(connectionString.stdout).standardSrv); 39 parsedConnectionString.username = encodeURIComponent(process.env.MONGODB_USERNAME); 40 parsedConnectionString.password = encodeURIComponent(process.env.MONGODB_PASSWORD); 41 parsedConnectionString.search = "retryWrites=true&w=majority"; 42 process.env.MONGODB_ATLAS_URI = parsedConnectionString.toString(); 43 app = $`node main.js`; 44 } catch (error) { 45 console.log(error.stdout); 46 }
Mais uma vez, vamos detalhar o que está rolando!
Como no aplicativo Node.js, o script ZX usará muitas variáveis de ambiente. No final, essas variáveis serão passadas com o Docker, mas você pode codificá-las a qualquer momento se quiser testar coisas fora do Docker.
A primeira coisa importante a observar é a padronização das variáveis de ambiente:
1 process.env.MONGODB_CLUSTER_NAME = process.env.MONGODB_CLUSTER_NAME || "examples"; 2 process.env.MONGODB_USERNAME = process.env.MONGODB_USERNAME || "demo"; 3 process.env.MONGODB_PASSWORD = process.env.MONGODB_PASSWORD || "password1234"; 4 process.env.MONGODB_DATABASE = process.env.MONGODB_DATABASE || "business_" + runtimeTimestamp; 5 process.env.MONGODB_COLLECTION = process.env.MONGODB_COLLECTION || "people_" + runtimeTimestamp; 6 process.env.CLEANUP_ONDESTROY = process.env.CLEANUP_ONDESTROY || false;
O snippet acima não é um requisito, mas se você quiser evitar a configuração ou a passagem de variáveis, defini-las como padrão pode ser útil. No exemplo acima, o uso de
runtimeTimestamp
nos permitirá criar um banco de dados e uma coleção exclusivos, se quisermos. Isso pode ser útil se vários desenvolvedores planejarem usar as mesmas imagens do Docker para implantar contêineres, pois assim cada desenvolvedor estará em uma área de sandbox. Se o desenvolvedor optar por desfazer a implementação, apenas seu banco de dados e coleção exclusivos serão descartados.Em seguida, temos o seguinte:
1 process.on("SIGTERM", () => { 2 app.kill("SIGTERM"); 3 });
Também temos algo semelhante no aplicativo Node.js. Nós o temos no script porque, no fim, o script controlará o aplicativo. Então, quando nós (ou o Docker) paramos o script, o mesmo evento de parada é passado para o aplicativo. Se não fizéssemos isso, o aplicativo não teria um desligamento normal e a lógica de descarte não seria aplicada.
Agora temos três blocos try/catch, cada um focando em algo específico.
O primeiro bloco é responsável pela criação de um cluster com dados de amostra:
1 try { 2 let createClusterResult = await $`atlas clusters create ${process.env.MONGODB_CLUSTER_NAME} --tier M0 --provider AWS --region US_EAST_1 --output json`; 3 await $`atlas clusters watch ${process.env.MONGODB_CLUSTER_NAME}` 4 let loadSampleDataResult = await $`atlas clusters loadSampleData ${process.env.MONGODB_CLUSTER_NAME} --output json`; 5 } catch (error) { 6 console.log(error.stdout); 7 }
Se o cluster já existir, um erro será detectado. Temos três blocos porque, no nosso cenário, tudo bem se certas partes já existirem.
Em seguida, nos preocupamos com usuários e acesso:
1 try { 2 let createAccessListResult = await $`atlas accessLists create --currentIp --output json`; 3 let createDatabaseUserResult = await $`atlas dbusers create --role readWriteAnyDatabase,dbAdminAnyDatabase --username ${process.env.MONGODB_USERNAME} --password ${process.env.MONGODB_PASSWORD} --output json`; 4 await $`sleep 10` 5 } catch (error) { 6 console.log(error.stdout); 7 }
Queremos que nosso IP local seja adicionado à lista de acesso e que um usuário seja criado. Neste exemplo, estamos criando um usuário com acesso extensivo, mas talvez você queira refinar o nível de permissão que ele tem em seu próprio projeto. Por exemplo, talvez o contêiner seja para uma experiência de sandbox. Nesse cenário, faz sentido que o usuário tenha criado acesso apenas ao banco de dados e à coleção no sandbox. Buscamos
sleep
esses comandos porque eles não são instantâneos e queremos ter certeza de que tudo está pronto antes de tentarmos nos conectar.Por fim, tentamos conectar:
1 try { 2 let connectionString = await $`atlas clusters connectionStrings describe ${process.env.MONGODB_CLUSTER_NAME} --output json`; 3 let parsedConnectionString = new URL(JSON.parse(connectionString.stdout).standardSrv); 4 parsedConnectionString.username = encodeURIComponent(process.env.MONGODB_USERNAME); 5 parsedConnectionString.password = encodeURIComponent(process.env.MONGODB_PASSWORD); 6 parsedConnectionString.search = "retryWrites=true&w=majority"; 7 process.env.MONGODB_ATLAS_URI = parsedConnectionString.toString(); 8 app = $`node main.js`; 9 } catch (error) { 10 console.log(error.stdout); 11 }
Após a conclusão do primeiro bloco try / catch, teremos umastring de conexão. Podemos finalizar nossa string com um objeto de URL Node.js incluindo o nome de usuário e a senha e, em seguida, podemos executar nosso aplicativo Node.js. Lembre-se de que as variáveis de ambiente e todas as manipulações que fizemos nelas em nosso script serão passadas para o aplicativo Node.js.
Neste momento, temos um aplicativo e um script para preparar o MongoDB Atlas e iniciar o aplicativo. É hora de colocar tudo em uma imagem do Docker para ser implantado como um contêiner.
Na raiz do seu diretório de projeto, adicione um arquivo Dockerfile com o seguinte:
1 FROM node:18 2 3 WORKDIR /usr/src/app 4 5 COPY ./app/* ./ 6 COPY ./docker_run_script.mjs ./ 7 8 RUN curl https://fastdl.mongodb.org/mongocli/mongodb-atlas-cli_1.3.0_linux_x86_64.tar.gz --output mongodb-atlas-cli_1.3.0_linux_x86_64.tar.gz 9 RUN tar -xvf mongodb-atlas-cli_1.3.0_linux_x86_64.tar.gz && mv mongodb-atlas-cli_1.3.0_linux_x86_64 atlas_cli 10 RUN chmod +x atlas_cli/bin/atlas 11 RUN mv atlas_cli/bin/atlas /usr/bin/ 12 13 RUN npm install -g zx 14 RUN npm install 15 16 EXPOSE 3000 17 18 CMD ["./docker_run_script.mjs"]
A imagem personalizada do Docker será baseada em uma imagem Node.js que nos permitirá executar nosso aplicativo Node.js, bem como nosso script ZX.
Depois que nossos arquivos são copiados para a imagem, executamos alguns comandos para baixar e extrair o MongoDB Atlas CLI.
Por fim, instalamos o ZX e as dependências do nosso aplicativo e executamos o script do ZX. O comando
CMD
para executar o script é feito quando o contêiner é executado. Todo o resto é feito quando a imagem é criada.Poderíamos construir nossa imagem a partir deste arquivo Dockerfile, mas é muito mais fácil de gerenciar quando há uma configuração Compose. No diretório do projeto, crie um arquivo docker-compose.yml com o seguinte YAML:
1 version: "3.9" 2 services: 3 web: 4 build: 5 context: . 6 dockerfile: Dockerfile 7 ports: 8 - "3000:3000" 9 environment: 10 MONGODB_ATLAS_PUBLIC_API_KEY: YOUR_PUBLIC_KEY_HERE 11 MONGODB_ATLAS_PRIVATE_API_KEY: YOUR_PRIVATE_KEY_HERE 12 MONGODB_ATLAS_ORG_ID: YOUR_ORG_ID_HERE 13 MONGODB_ATLAS_PROJECT_ID: YOUR_PROJECT_ID_HERE 14 MONGODB_CLUSTER_NAME: examples 15 MONGODB_USERNAME: demo 16 MONGODB_PASSWORD: password1234 17 # MONGODB_DATABASE: sample_mflix 18 # MONGODB_COLLECTION: movies 19 CLEANUP_ONDESTROY: true
Você deverá trocar os valores das variáveis de ambiente pelos seus próprios valores. No exemplo acima, as variáveis de banco de dados e de coleção estão comentadas para que os padrões sejam usados no script ZX.
Para ver tudo em ação, execute o seguinte a partir da linha de comando no computador host:
1 docker-compose up
O comando acima usará o arquivo docker-compose.yml para criar a imagem Docker se ela ainda não existir. O processo de criação agrupará nossos arquivos, instalará nossas dependências e obterá o MongoDB Atlas CLI. Quando o Compose implantar um contêiner a partir da imagem, as variáveis de ambiente serão passadas para o script ZX responsável por configurar o MongoDB Atlas. Quando estiver pronto, o script ZX executará a aplicação Node.js, passando ainda mais as variáveis de ambiente. Se a variável
CLEANUP_ONDESTROY
foi definida como true
, quando o contêiner for interrompido, o banco de dados e a coleção serão removidos.A CLI do MongoDB Atlas pode ser uma ferramenta poderosa para trazer o MongoDB Atlas para sua experiência de desenvolvimento local no Docker. Essencialmente, você estaria trocando uma versão local do MongoDB pela lógica Atlas CLI para gerenciar uma versão em nuvem mais rica em recursos do MongoDB.
O MongoDB Atlas aprimora a experiência do MongoDB dando acesso a mais recursos, como Atlas Search, Charts e App Services, que permitem criar ótimos aplicativos com o mínimo de esforço.