Pausar automaticamente clusters inativos
Avalie esse Artigo
Há alguns anos, escrevi um artigo sobre como pausar e/ou dimensionar clusters usando Atlas Triggers agendados. Este artigo representa uma reviravolta nesse conceito, adicionando uma preocupação que pausará clusters em toda a organização com base na inatividade. Especificamente, estou analisando o histórico deacesso ao banco de dados para determinar a atividade.
É importante observar esta limitação de registro:
Se um cluster experimentar um pico de atividade e gerar uma quantidade extremamente grande de mensagens de log, o Atlas poderá parar de coletar e armazenar novos logs por um período de tempo.
Portanto, esse script pode dar um falso positivo de que um cluster está inativo quando, na verdade, acontece exatamente o oposto. Dado, no entanto, que a intenção desse script é gerenciar ambientes inferiores, que não são de produção, não vejo os falsos positivos como uma grande preocupação.
A implementação usa um trigger. O trigger chama uma série de Atlas App Services, que usam as Atlas API para iterar sobre os projetos da organização e seus clusters associados, testando a inatividade do cluster (conforme explicado na introdução) e, finalmente, pausando o cluster se ele estiver realmente inativo.

Para chamar as API Administrativas do Atlas, primeiro você precisará de uma API Key com o roleProprietário da Organização. As API Keys são criadas no Access Manager, que você encontrará no menu Organização à esquerda:

ou na barra de menu na parte superior:


Clique em Criar chave de API. Dê uma descrição à chave e certifique-se de definir as permissões para Proprietário da organização:

Ao clicar em Avançar, você verá suas chaves Públicas e Privadas. Salve sua chave privada , pois o Atlas nunca mais a mostrará a você.
Como uma camada adicional de segurança, você também tem a opção de definir uma lista de acesso IP para essas chaves. Estou pulando esta etapa, para que minha chave funcione de qualquer lugar.

Como essa solução funciona em toda a sua organização do Atlas, eu gosto de hospedá-la em seu próprio projeto do Atlas dedicado.

O Atlas App Services fornece um poderoso backend de desenvolvimento de aplicativos como serviço. Para começar a usá-lo, basta clicar na aba App Services.

Você verá que o App Services oferece vários modelos para você começar. Para esse caso de uso, basta selecionar a primeira opção para Criar seu próprio aplicativo:

Em seguida, você verá opções para vincular uma fonte de dados, nomear seu aplicativo e escolher um modelo de implantação. A iteração atual desse utilitário não usa uma fonte de dados, então você pode ignorar essa etapa (o App Services criará um cluster gratuito para você). Você também pode deixar o modelo de implantação como padrão (Global), a menos que queira limitar o aplicativo a uma região específica.
Dei o nome de Atlas Cluster Automation ao aplicativo:

Nessa situação, você tem duas opções:
- Basta importar o aplicativo App Services e ajustar qualquer uma das funções para atender às suas necessidades.
- Crie o aplicativo do zero (pule para a próxima seção).
A extração depende da chave secreta da API, portanto, a importação falhará se ela não for configurada previamente.
Use o menu
Values
à esquerda para Criar um Segredo chamado AtlasPrivateKeySecret
contendo a chave privada que você criou anteriormente (o segredo não está entre aspas):
npm install -g mongodb-realm-cli
Para configurar seu aplicativo com o realm-cli, você deve se conectar ao Atlas usando suas chaves de API:
1 ✗ realm-cli login --api-key="<Public API Key>" --private-api-key="<Private API Key>" 2 Successfully logged in
Selecione o
App Settings
e copie o ID do seu aplicativo:
Execute o seguinte
realm-cli push
comando no diretório em que você extraiu a exportação:1 realm-cli push --remote="<Your App ID>" 2 3 ... 4 A summary of changes 5 ... 6 7 ? Please confirm the changes shown above Yes 8 Creating draft 9 Pushing changes 10 Deploying draft 11 Deployment complete 12 Successfully pushed app up:
Após a importação, substitua o `AtlasPublicKey' pelo valor da chave pública da sua API.



O trigger está programado para disparar a cada 30 minutos. Observe que a funçãopausaClusters que o trigger chama atualmente apenas registra a atividade do cluster. Isso é para que você possa monitorar e verificar se a função se comporta conforme o desejado. Quando estiver pronto, descomente a linha que chama a funçãopausaCluster:
1 if (!is_active) { 2 console.log(`Pausing ${project.name}:${cluster.name} because it has been inactive for more then ${minutesInactive} minutes`); 3 //await context.functions.execute("pauseCluster", project.id, cluster.name, pause);
Além disso, a funçãopauseClusters pode ser configurada para excluir projetos (como os dedicados a cargas de trabalho de produção):
1 /* 2 * These project names are just an example. 3 * The same concept could be used to exclude clusters or even 4 * configure different inactivity intervals by project or cluster. 5 * These configuration options could also be stored and read from 6 * and Atlas database. 7 */ 8 excludeProjects = ['PROD1', 'PROD2'];
Agora que você revisou o rascunho, como etapa final, implante o aplicativo do App Services.

Para entender o que está incluído no aplicativo, estas são as etapas para criá-lo do zero.
As funções que precisamos criar chamarão a API de administração do Atlas, portanto, precisamos armazenar nossas chaves públicas e privadas de API, o que vamos fazer usando Valores e segredos. O código de amostra que forneço referência a esses valores como
AtlasPublicKey
e AtlasPrivateKey
, portanto, use esses mesmos nomes, a menos que queira alterar o código em que eles são referenciados.Você encontrará
Values
no menu Build:
Primeiro, crie um Valor,
AtlasPublicKey
, para sua chave pública (observe que a chave está entre aspas):
Crie um Segredo,
AtlasPrivateKeySecret
, contendo sua chave privada (o segredo não está entre aspas):
O Segredo não pode ser acessado diretamente, portanto, crie um segundo Valor,
AtlasPrivateKey
, que se vincule ao segredo:

As quatro funções que precisam ser criadas são auto-explicativas, então não vamos fornecer um monte de explicações adicionais aqui.
Essa função autônoma pode ser testada no console do App Services para ver a lista de todos os projetos da sua organização.
1 /* 2 * Returns an array of the projects in the organization 3 * See https://docs.atlas.mongodb.com/reference/api/project-get-all/ 4 * 5 * Returns an array of objects, e.g. 6 * 7 * { 8 * "clusterCount": { 9 * "$numberInt": "1" 10 * }, 11 * "created": "2021-05-11T18:24:48Z", 12 * "id": "609acbef1b76b53fcd37c8e1", 13 * "links": [ 14 * { 15 * "href": "https://cloud.mongodb.com/api/atlas/v1.0/groups/609acbef1b76b53fcd37c8e1", 16 * "rel": "self" 17 * } 18 * ], 19 * "name": "mg-training-sample", 20 * "orgId": "5b4e2d803b34b965050f1835" 21 * } 22 * 23 */ 24 exports = async function() { 25 26 // Get stored credentials... 27 const username = await context.values.get("AtlasPublicKey"); 28 const password = await context.values.get("AtlasPrivateKey"); 29 30 const arg = { 31 scheme: 'https', 32 host: 'cloud.mongodb.com', 33 path: 'api/atlas/v1.0/groups', 34 username: username, 35 password: password, 36 headers: {'Content-Type': ['application/json'], 'Accept-Encoding': ['bzip, deflate']}, 37 digestAuth:true, 38 }; 39 40 // The response body is a BSON.Binary object. Parse it and return. 41 response = await context.http.get(arg); 42 43 return EJSON.parse(response.body.text()).results; 44 };
Depois que
getProjects
é chamado, o trigger itera sobre os resultados, passando o projectId
para essa funçãogetProjectClusters
.Para testar essa função, você precisa fornecer um
projectId
. Por padrão, o console fornece "Hello world!", portanto, testo essa entrada e forneço alguns valores padrão para facilitar o teste.1 /* 2 * Returns an array of the clusters for the supplied project ID. 3 * See https://docs.atlas.mongodb.com/reference/api/clusters-get-all/ 4 * 5 * Returns an array of objects. See the API documentation for details. 6 * 7 */ 8 exports = async function(project_id) { 9 10 if (project_id == "Hello world!") { // Easy testing from the console 11 project_id = "5e8f8268d896f55ac04969a1" 12 } 13 14 // Get stored credentials... 15 const username = await context.values.get("AtlasPublicKey"); 16 const password = await context.values.get("AtlasPrivateKey"); 17 18 const arg = { 19 scheme: 'https', 20 host: 'cloud.mongodb.com', 21 path: `api/atlas/v1.0/groups/${project_id}/clusters`, 22 username: username, 23 password: password, 24 headers: {'Content-Type': ['application/json'], 'Accept-Encoding': ['bzip, deflate']}, 25 digestAuth:true, 26 }; 27 28 // The response body is a BSON.Binary object. Parse it and return. 29 response = await context.http.get(arg); 30 31 return EJSON.parse(response.body.text()).results; 32 };
Essa função contém a lógica que determina se o cluster pode ser pausado.
A maior parte do trabalho nessa função é manipular o carimbo de data/hora no log de acesso ao banco de dados para que ele possa ser comparado com a hora atual e a janela de lookback.
Além de retornar verdadeiro (ativo) ou falso (inativo), a função registra suas descobertas, por exemplo:
Checking if cluster 'SA-SHARED-DEMO' has been active in the last 60 minutes
1 Wed Nov 03 2021 19:52:31 GMT+0000 (UTC) - job is being run 2 Wed Nov 03 2021 18:52:31 GMT+0000 (UTC) - cluster inactivity before this time will be reported inactive 3 Wed Nov 03 2021 19:48:45 GMT+0000 (UTC) - last logged database access 4 Cluster is Active: Username 'brian' was active in cluster 'SA-SHARED-DEMO' 4 minutes ago.
Como
getClusterProjects
, há um bloco que você pode usar para fornecer alguns ID de projeto de teste e nomes de cluster para facilitar o teste no console do App Services.1 /* 2 * Used the database access history to determine if the cluster is in active use. 3 * See https://docs.atlas.mongodb.com/reference/api/access-tracking-get-database-history-clustername/ 4 * 5 * Returns true (active) or false (inactive) 6 * 7 */ 8 exports = async function(project_id, clusterName, minutes) { 9 10 if (project_id == 'Hello world!') { // We're testing from the console 11 project_id = "5e8f8268d896f55ac04969a1"; 12 clusterName = "SA-SHARED-DEMO"; 13 minutes = 60; 14 } /*else { 15 console.log (`project_id: ${project_id}, clusterName: ${clusterName}, minutes: ${minutes}`) 16 }*/ 17 18 // Get stored credentials... 19 const username = await context.values.get("AtlasPublicKey"); 20 const password = await context.values.get("AtlasPrivateKey"); 21 22 const arg = { 23 scheme: 'https', 24 host: 'cloud.mongodb.com', 25 path: `api/atlas/v1.0/groups/${project_id}/dbAccessHistory/clusters/${clusterName}`, 26 //query: {'authResult': "true"}, 27 username: username, 28 password: password, 29 headers: {'Content-Type': ['application/json'], 'Accept-Encoding': ['bzip, deflate']}, 30 digestAuth:true, 31 }; 32 33 // The response body is a BSON.Binary object. Parse it and return. 34 response = await context.http.get(arg); 35 36 accessLogs = EJSON.parse(response.body.text()).accessLogs; 37 38 now = Date.now(); 39 const MS_PER_MINUTE = 60000; 40 var durationInMinutes = (minutes < 30, 30, minutes); // The log granularity is 30 minutes. 41 var idleStartTime = now - (durationInMinutes * MS_PER_MINUTE); 42 43 nowString = new Date(now).toString(); 44 idleStartTimeString = new Date(idleStartTime).toString(); 45 console.log(`Checking if cluster '${clusterName}' has been active in the last ${durationInMinutes} minutes`) 46 console.log(` ${nowString} - job is being run`); 47 console.log(` ${idleStartTimeString} - cluster inactivity before this time will be reported inactive`); 48 49 clusterIsActive = false; 50 51 accessLogs.every(log => { 52 if (log.username != 'mms-automation' && log.username != 'mms-monitoring-agent') { 53 54 // Convert string log date to milliseconds 55 logTime = Date.parse(log.timestamp); 56 57 logTimeString = new Date(logTime); 58 console.log(` ${logTimeString} - last logged database access`); 59 60 var elapsedTimeMins = Math.round((now - logTime)/MS_PER_MINUTE, 0); 61 62 if (logTime > idleStartTime ) { 63 console.log(`Cluster is Active: Username '${log.username}' was active in cluster '${clusterName}' ${elapsedTimeMins} minutes ago.`); 64 clusterIsActive = true; 65 return false; 66 } else { 67 // The first log entry is older than our inactive window 68 console.log(`Cluster is Inactive: Username '${log.username}' was active in cluster '${clusterName}' ${elapsedTimeMins} minutes ago.`); 69 clusterIsActive = false; 70 return false; 71 } 72 } 73 return true; 74 75 }); 76 77 return clusterIsActive; 78 79 };
Por fim, se o cluster estiver inativo, passamos o ID do projeto e o nome do cluster para
pauseCluster
. Essa função também pode retomar um cluster, embora esse recurso não seja utilizado neste caso de uso.1 /* 2 * Pauses the named cluster 3 * See https://docs.atlas.mongodb.com/reference/api/clusters-modify-one/ 4 * 5 */ 6 exports = async function(projectID, clusterName, pause) { 7 8 // Get stored credentials... 9 const username = await context.values.get("AtlasPublicKey"); 10 const password = await context.values.get("AtlasPrivateKey"); 11 12 const body = {paused: pause}; 13 14 const arg = { 15 scheme: 'https', 16 host: 'cloud.mongodb.com', 17 path: `api/atlas/v1.0/groups/${projectID}/clusters/${clusterName}`, 18 username: username, 19 password: password, 20 headers: {'Content-Type': ['application/json'], 'Accept-Encoding': ['bzip, deflate']}, 21 digestAuth:true, 22 body: JSON.stringify(body) 23 }; 24 25 // The response body is a BSON.Binary object. Parse it and return. 26 response = await context.http.patch(arg); 27 28 return EJSON.parse(response.body.text()); 29 };
Esta função será chamada por um trigger. Como não é possível passar um parâmetro para um gatilho agendado, ele usa uma janela de retrospectiva codificada de 60 minutos que você pode alterar para atender às suas necessidades. Você pode até armazenar o valor em um banco de dados do Atlas e construir uma interface do usuário para gerenciar sua configuração :-).
A função avaliará todos os projetos e clusters na organização em que está hospedada. Entendendo que há prováveis projetos ou clusters que você nunca quer pausado, a função também inclui uma array excludeProjects, onde você pode especificar uma lista de nomes de projetos a serem excluídos da avaliação.
Por fim, você notará que a chamada para
pauseCluster
foi comentada. Sugiro que você execute essa função por alguns dias e analise os logs do Trigger para verificar se ela se comporta como esperado.1 /* 2 * Iterates over the organizations projects and clusters, 3 * pausing clusters inactive for the configured minutes. 4 */ 5 exports = async function() { 6 7 minutesInactive = 60; 8 9 /* 10 * These project names are just an example. 11 * The same concept could be used to exclude clusters or even 12 * configure different inactivity intervals by project or cluster. 13 * These configuration options could also be stored and read from 14 * and Atlas database. 15 */ 16 excludeProjects = ['PROD1', 'PROD2']; 17 18 const projects = await context.functions.execute("getProjects"); 19 20 projects.forEach(async project => { 21 22 if (excludeProjects.includes(project.name)) { 23 console.log(`Project '${project.name}' has been excluded from pause.`) 24 } else { 25 26 console.log(`Checking project '${project.name}'s clusters for inactivity...`); 27 28 const clusters = await context.functions.execute("getProjectClusters", project.id); 29 30 clusters.forEach(async cluster => { 31 32 if (cluster.providerSettings.providerName != "TENANT") { // It's a dedicated cluster than can be paused 33 34 if (cluster.paused == false) { 35 36 is_active = await context.functions.execute("clusterIsActive", project.id, cluster.name, minutesInactive); 37 38 if (!is_active) { 39 console.log(`Pausing ${project.name}:${cluster.name} because it has been inactive for more then ${minutesInactive} minutes`); 40 //await context.functions.execute("pauseCluster", project.id, cluster.name, true); 41 } else { 42 console.log(`Skipping pause for ${project.name}:${cluster.name} because it has active database users in the last ${minutesInactive} minutes.`); 43 } 44 } 45 } 46 }); 47 } 48 }); 49 50 return true; 51 };
Sim, ainda estamos usando um scheduled trigger, mas desta vez o trigger será executado periodicamente para verificar se há inatividade do cluster. Agora, seus desenvolvedores que trabalham tarde da noite não terão mais o cluster pausado abaixo deles.

Como uma etapa final, você precisa implantar o aplicativo Atlas App Services.

A gênese deste artigo foi que um cliente, quando apresentado meu artigo anterior sobre agendamento de pausas de cluster, perguntou se o mesmo poderia ser alcançado com base na inatividade. É minha confiança que, com as APIs do Atlas, tudo pode ser alcançado. A única questão era o que constitui inatividade? Dado o batimento cardíaco e a replicação que ocorrem naturalmente, sempre há algum "activity" no cluster. Por fim, decidi pelo acesso ao banco de dados como guia. Com o tempo, essa métrica pode ser combinada com algumas métricas adicionais ou alterada para algo completamente diferente, mas os ossos do processo estão aqui.
Relacionado
Tutorial
Como usar o MongoDB Atlas e os LLMs do IBM watsonx.ai em seus aplicativos de GenAI sem interrupções
Mar 12, 2025 | 9 min read
Artigo
Colocando o RAG em produção com o chatbot de IA da documentação do MongoDB
Aug 29, 2024 | 11 min read