Exemplos de respostas automatizadas de controle de custos

Exemplo de arquitetura de referência

Diagrama de um exemplo que usa notificações programáticas de alertas de orçamento para automatizar uma resposta de controle de custos.
Figura 1: ilustra um exemplo de uso de alertas de orçamento para automatizar respostas de controle de custos usando o Pub/Sub para notificações programáticas e as funções do Cloud Run para automatizar uma resposta.

Se você quer economizar e precisa controlar seu ambiente de acordo com o orçamento, use as notificações programáticas de orçamento para automatizar a resposta de controle de custos com base na notificação de orçamento.

As notificações de orçamento usam tópicos do Pub/Sub para fornecer um status em tempo real do orçamento do Cloud Billing, usando a escalonabilidade, a flexibilidade e a confiabilidade do middleware empresarial orientado a mensagens para a nuvem.

Este documento tem exemplos e instruções detalhadas sobre como usar as notificações de orçamento com as funções do Cloud Run para automatizar o gerenciamento de custos.

Configurar notificações de orçamento

A primeira etapa é ativar um tópico do Pub/Sub para seu orçamento. Isso é descrito em detalhes em Gerenciar notificações com alertas de orçamento programático.

Depois de ativar as notificações de orçamento, observe o seguinte:

  • Tópico do Pub/Sub: é o endpoint de notificações configurado para o orçamento.
  • ID do orçamento: é um ID exclusivo do orçamento, incluído em todas as notificações. Para localizar esse ID, acesse Gerenciar notificações no orçamento e selecione Conectar um tópico do Pub/Sub a este orçamento.

A seção "Gerenciar notificações" no Console do Google Cloud, onde é possível
            conectar um tópico de Pub/Sub a um orçamento. Ela inclui ID do orçamento, nome do projeto e tópico do Pub/Sub.

Detectar suas notificações

O próximo passo é assinar seu tópico do Pub/Sub para detectar as notificações. Se você não tiver um tópico assinado, o Pub/Sub vai descartar as mensagens publicadas, e não será possível recuperá-las mais tarde.

Há muitas maneiras de assinar o tópico, mas estes exemplos usam gatilhos da função do Cloud Run.

Criar uma função do Cloud Run

Para criar uma nova função do Cloud Run:

  1. No console do Google Cloud, acesse a página das funções do Cloud Run.

    Acessar a página de funções do Cloud Run

  2. Clique em CRIAR FUNÇÃO e atribua a ela um nome que seja significativo para seu orçamento.

  3. Em Gatilho, selecione Tópico do Pub/Sub.

  4. Selecione o tópico que você configurou no seu orçamento.

  5. Forneça o código-fonte e as dependências que a função executará.

  6. Defina a Função a ser executada com o nome correto da função.

A página "Criar função" na seção "Funções do Cloud Run" no console do Google Cloud. Ela inclui o nome da função, o volume de memória alocada, o tipo de gatilho e o tópico do Pub/Sub que você configurou no seu orçamento.

Descrever sua função do Cloud Run

Para informar à função do Cloud Run o que você quer fazer com a notificação, escreva o código usando a edição in-line ou faça upload de um arquivo. Para detalhes sobre as notificações que seu código receberá, consulte Formato de notificação.

Por exemplo, uma função pode registrar notificações, atributos e dados do Pub/Sub recebidos quando for acionada por uma notificação de orçamento. Para saber mais, consulte Gatilhos do Pub/Sub.

Conferir os eventos da função do Cloud Run

Depois de salvar a função do Cloud Run, clique em VER REGISTROS para visualizar as notificações de orçamento registradas. Isso mostra os registros das suas invocações de função.

Mostra onde é possível encontrar os registros de visualização na tela e a lista de eventos de função do Cloud Run no console do Google Cloud.

Testar a função do Cloud Run

As notificações são enviadas ao Pub/Sub e os assinantes recebem as mensagens. Para testar uma notificação de amostra e verificar se sua função está operando como esperado, publique uma mensagem no Pub/Sub usando este objeto como o corpo da mensagem:

{
    "budgetDisplayName": "name-of-budget",
    "alertThresholdExceeded": 1.0,
    "costAmount": 100.01,
    "costIntervalStart": "2019-01-01T00:00:00Z",
    "budgetAmount": 100.00,
    "budgetAmountType": "SPECIFIED_AMOUNT",
    "currencyCode": "USD"
}

Você também pode adicionar atributos de mensagem, como o ID da conta de faturamento. Consulte a documentação completa do formato de notificação para ver mais informações.

Enviar notificações para o Slack

O e-mail nem sempre é a melhor maneira de se manter atualizado sobre seus custos na nuvem, principalmente se o orçamento for crítico e urgente. Com as notificações, é possível encaminhar mensagens de orçamento para outras mídias.

Neste exemplo, descrevemos como encaminhar notificações de orçamento para o Slack. Dessa maneira, toda vez que o Faturamento do Cloud publicar uma notificação de orçamento, uma função do Cloud Run usará um bot para postar uma mensagem em um canal do Slack no espaço de trabalho desse bot.

Configurar um canal e permissões do Slack

A primeira etapa é criar seu espaço de trabalho no Slack e os tokens de usuário do bot que são usados para chamar a API Slack. É possível gerenciar os tokens da API em https://api.slack.com/apps. Para instruções detalhadas, consulte Usuários de bot no site do Slack.

Configurar notificações do Slack.

Gravar uma função do Cloud Run

  1. Para criar uma nova função, siga as etapas em Criar uma função do Cloud Run. Verifique se o gatilho está definido como o mesmo tópico do Pub/Sub que o orçamento está configurado para usar.

  2. Adicionar dependências:

    Node.js

    Copie o seguinte para o package.json:

    {
      "name": "cloud-functions-billing",
      "private": "true",
      "version": "0.0.1",
      "description": "Examples of integrating Cloud Functions with billing",
      "main": "index.js",
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "compute-test": "c8 mocha -p -j 2 test/periodic.test.js --timeout=600000",
        "test": "c8 mocha -p -j 2 test/index.test.js --timeout=5000 --exit"
      },
      "author": "Ace Nassri <anassri@google.com>",
      "license": "Apache-2.0",
      "dependencies": {
        "@google-cloud/billing": "^4.0.0",
        "@google-cloud/compute": "^4.0.0",
        "google-auth-library": "^9.0.0",
        "googleapis": "^143.0.0",
        "slack": "^11.0.1"
      },
      "devDependencies": {
        "@google-cloud/functions-framework": "^3.0.0",
        "c8": "^10.0.0",
        "gaxios": "^6.0.0",
        "mocha": "^10.0.0",
        "promise-retry": "^2.0.0",
        "proxyquire": "^2.1.0",
        "sinon": "^18.0.0",
        "wait-port": "^1.0.4"
      }
    }
    

    Python

    Copie o seguinte para o requirements.txt:

    slackclient==2.9.4
    google-api-python-client==2.131.0
    

  3. Escreva o código ou use o exemplo abaixo para postar notificações de orçamento em um canal de chat do Slack usando a API Slack.

  4. Verifique se os seguintes parâmetros postMessage da API Slack estão configurados corretamente:

    • Token de acesso do OAuth do usuário de bot
    • Nome do canal

Exemplo de código:

Node.js

const slack = require('slack');

// TODO(developer) replace these with your own values
const BOT_ACCESS_TOKEN =
  process.env.BOT_ACCESS_TOKEN || 'xxxx-111111111111-abcdefghidklmnopq';
const CHANNEL = process.env.SLACK_CHANNEL || 'general';

exports.notifySlack = async pubsubEvent => {
  const pubsubAttrs = pubsubEvent.attributes;
  const pubsubData = Buffer.from(pubsubEvent.data, 'base64').toString();
  const budgetNotificationText = `${JSON.stringify(
    pubsubAttrs
  )}, ${pubsubData}`;

  await slack.chat.postMessage({
    token: BOT_ACCESS_TOKEN,
    channel: CHANNEL,
    text: budgetNotificationText,
  });

  return 'Slack notification sent successfully';
};

Python

import base64
import json
import os

import slack
from slack.errors import SlackApiError

# See https://api.slack.com/docs/token-types#bot for more info
BOT_ACCESS_TOKEN = "xxxx-111111111111-abcdefghidklmnopq"
CHANNEL = "C0XXXXXX"

slack_client = slack.WebClient(token=BOT_ACCESS_TOKEN)


def notify_slack(data, context):
    pubsub_message = data

    # For more information, see
    # https://cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications#notification_format
    try:
        notification_attr = json.dumps(pubsub_message["attributes"])
    except KeyError:
        notification_attr = "No attributes passed in"

    try:
        notification_data = base64.b64decode(data["data"]).decode("utf-8")
    except KeyError:
        notification_data = "No data passed in"

    # This is just a quick dump of the budget data (or an empty string)
    # You can modify and format the message to meet your needs
    budget_notification_text = f"{notification_attr}, {notification_data}"

    try:
        slack_client.api_call(
            "chat.postMessage",
            json={"channel": CHANNEL, "text": budget_notification_text},
        )
    except SlackApiError:
        print("Error posting to Slack")

Agora você pode testar a função do Cloud Run para ver uma mensagem exibida no Slack.

Limitar (desativar) o faturamento para interromper o uso

Neste exemplo, mostramos como limitar custos e interromper o uso de um projeto desativando o Cloud Billing. Desativar o faturamento em um projeto fará com que todos os serviços do Google Cloud no projeto sejam encerrados, incluindo os serviços de nível gratuito.

Por que desativar o faturamento?

Talvez seja útil limitar os custos caso haja um limite absoluto do dinheiro que se pode gastar no Google Cloud. Isso é comum para estudantes, pesquisadores ou desenvolvedores que trabalham em ambientes de sandbox. Nesses casos, convém interromper os gastos e encerrar todos os seus serviços e o uso do Google Cloud quando seu limite de orçamento é atingido.

No nosso exemplo, usamos acme-backend-dev como projeto de não produção em que é seguro desativar o Faturamento do Cloud.

Configure o limite de orçamento no Console do Google Cloud.

Antes de começar a trabalhar com este exemplo, verifique se você fez o seguinte:

Mostrar a lista de alertas do Faturamento do Cloud no Console do Google Cloud.

Gravar uma função do Cloud Run

Em seguida, você precisa configurar a função do Cloud Run para chamar a API Cloud Billing. Isso permite que a função do Cloud Run desative o Cloud Billing para nosso projeto de exemplo acme-backend-dev.

  1. Para criar uma nova função, siga as etapas em Criar uma função do Cloud Run. Verifique se o gatilho está definido como o mesmo tópico do Pub/Sub que o orçamento está configurado para usar.

  2. Adicione as seguintes dependências:

    Node.js

    Copie o seguinte para o package.json:

    {
      "name": "cloud-functions-billing",
      "private": "true",
      "version": "0.0.1",
      "description": "Examples of integrating Cloud Functions with billing",
      "main": "index.js",
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "compute-test": "c8 mocha -p -j 2 test/periodic.test.js --timeout=600000",
        "test": "c8 mocha -p -j 2 test/index.test.js --timeout=5000 --exit"
      },
      "author": "Ace Nassri <anassri@google.com>",
      "license": "Apache-2.0",
      "dependencies": {
        "@google-cloud/billing": "^4.0.0",
        "@google-cloud/compute": "^4.0.0",
        "google-auth-library": "^9.0.0",
        "googleapis": "^143.0.0",
        "slack": "^11.0.1"
      },
      "devDependencies": {
        "@google-cloud/functions-framework": "^3.0.0",
        "c8": "^10.0.0",
        "gaxios": "^6.0.0",
        "mocha": "^10.0.0",
        "promise-retry": "^2.0.0",
        "proxyquire": "^2.1.0",
        "sinon": "^18.0.0",
        "wait-port": "^1.0.4"
      }
    }
    

    Python

    Copie o seguinte para o requirements.txt:

    slackclient==2.9.4
    google-api-python-client==2.131.0
    

  3. Copie o código abaixo na função do Cloud Run.

  4. Defina a função a ser executada como stopBilling (Node) ou stop_billing (Python).

  5. Dependendo do ambiente de execução, a variável de ambiente GOOGLE_CLOUD_PROJECT pode ser definida automaticamente. Revise a lista de variáveis de ambiente definidas automaticamente e determine se você precisa definir manualmente a variável GOOGLE_CLOUD_PROJECT para o projeto para o qual quer limitar (desativar) o Cloud Billing.

Node.js

const {CloudBillingClient} = require('@google-cloud/billing');
const {InstancesClient} = require('@google-cloud/compute');

const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
const billing = new CloudBillingClient();

exports.stopBilling = async pubsubEvent => {
  const pubsubData = JSON.parse(
    Buffer.from(pubsubEvent.data, 'base64').toString()
  );
  if (pubsubData.costAmount <= pubsubData.budgetAmount) {
    return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
  }

  if (!PROJECT_ID) {
    return 'No project specified';
  }

  const billingEnabled = await _isBillingEnabled(PROJECT_NAME);
  if (billingEnabled) {
    return _disableBillingForProject(PROJECT_NAME);
  } else {
    return 'Billing already disabled';
  }
};

/**
 * Determine whether billing is enabled for a project
 * @param {string} projectName Name of project to check if billing is enabled
 * @return {bool} Whether project has billing enabled or not
 */
const _isBillingEnabled = async projectName => {
  try {
    const [res] = await billing.getProjectBillingInfo({name: projectName});
    return res.billingEnabled;
  } catch (e) {
    console.log(
      'Unable to determine if billing is enabled on specified project, assuming billing is enabled'
    );
    return true;
  }
};

/**
 * Disable billing for a project by removing its billing account
 * @param {string} projectName Name of project disable billing on
 * @return {string} Text containing response from disabling billing
 */
const _disableBillingForProject = async projectName => {
  const [res] = await billing.updateProjectBillingInfo({
    name: projectName,
    resource: {billingAccountName: ''}, // Disable billing
  });
  return `Billing disabled: ${JSON.stringify(res)}`;
};

Python

import base64
import json
import os

from googleapiclient import discovery

PROJECT_ID = os.getenv("GCP_PROJECT")
PROJECT_NAME = f"projects/{PROJECT_ID}"
def stop_billing(data, context):
    pubsub_data = base64.b64decode(data["data"]).decode("utf-8")
    pubsub_json = json.loads(pubsub_data)
    cost_amount = pubsub_json["costAmount"]
    budget_amount = pubsub_json["budgetAmount"]
    if cost_amount <= budget_amount:
        print(f"No action necessary. (Current cost: {cost_amount})")
        return

    if PROJECT_ID is None:
        print("No project specified with environment variable")
        return

    billing = discovery.build(
        "cloudbilling",
        "v1",
        cache_discovery=False,
    )

    projects = billing.projects()

    billing_enabled = __is_billing_enabled(PROJECT_NAME, projects)

    if billing_enabled:
        __disable_billing_for_project(PROJECT_NAME, projects)
    else:
        print("Billing already disabled")


def __is_billing_enabled(project_name, projects):
    """
    Determine whether billing is enabled for a project
    @param {string} project_name Name of project to check if billing is enabled
    @return {bool} Whether project has billing enabled or not
    """
    try:
        res = projects.getBillingInfo(name=project_name).execute()
        return res["billingEnabled"]
    except KeyError:
        # If billingEnabled isn't part of the return, billing is not enabled
        return False
    except Exception:
        print(
            "Unable to determine if billing is enabled on specified project, assuming billing is enabled"
        )
        return True


def __disable_billing_for_project(project_name, projects):
    """
    Disable billing for a project by removing its billing account
    @param {string} project_name Name of project disable billing on
    """
    body = {"billingAccountName": ""}  # Disable billing
    try:
        res = projects.updateBillingInfo(name=project_name, body=body).execute()
        print(f"Billing disabled: {json.dumps(res)}")
    except Exception:
        print("Failed to disable billing, possibly check permissions")

Configurar permissões da conta de serviço

Sua função do Cloud Run é executada como uma conta de serviço criada automaticamente. Para que essa conta de serviço possas desativar o faturamento, será preciso conceder a ela as permissões corretas, como Administrador de faturamento.

Para identificar a conta de serviço correta, visualize os detalhes da sua função do Cloud Run listada na parte inferior da página.

Mostra onde as informações da conta de serviço são encontradas na seção &quot;Função do Cloud Run&quot; do console do Google Cloud.

Gerencie permissões de administrador de faturamento na página "Faturamento" no Console do Google Cloud.

Para conceder privilégios de administrador da conta de faturamento à de serviço, selecione o nome dessa conta.

Veja onde selecionar o nome da conta de serviço e a função de administrador da conta de faturamento na seção &quot;Permissões&quot; do Console do Google Cloud.

Validar se o Faturamento do Cloud está desativado

Quando o orçamento enviar uma notificação, o projeto especificado terá mais uma Conta de faturamento do Cloud. Se você quiser testar a função, publique uma mensagem de exemplo com a mensagem de teste anterior. O projeto não estará mais visível na conta do Cloud Billing e os recursos no projeto serão desativados, incluindo a função do Cloud Run, se ela estiver no mesmo projeto.

Mostra que o projeto de exemplo não está mais visível na lista de projetos vinculados à Conta de faturamento do Cloud. Isso confirma que o Faturamento do Cloud está desativado para o projeto.

É possível reativar manualmente o Cloud Billing para seu projeto no Console do Google Cloud.

Controlar o uso de forma seletiva

Limitar (desativar) o Faturamento do Cloud conforme descrito no exemplo anterior é um processo binário e definitivo. Isso significa que seu projeto ou está ativado, ou está desativado. Quando o projeto é desativado, todos os serviços são interrompidos e todos os recursos são excluídos.

Se você precisar de uma resposta com mais detalhes, controle seletivamente os recursos. Por exemplo, caso queira interromper alguns recursos do Compute Engine, mas deixar o Cloud Storage intacto, você tem como controlar seletivamente o uso. Isso reduz seu custo por hora sem desativar completamente seu ambiente.

É possível escrever uma política com o detalhamento que você quiser. No entanto, para nosso exemplo, nosso projeto está realizando pesquisas com várias máquinas virtuais do Compute Engine e está armazenando resultados no Cloud Storage. Neste exemplo de função do Cloud Run, todas as instâncias do Compute Engine são encerradas, mas isso não afeta nossos resultados armazenados depois que o orçamento é excedido.

Gravar uma função do Cloud Run

  1. Para criar uma nova função, siga as etapas em Criar uma função do Cloud Run. Verifique se o gatilho está definido como o mesmo tópico do Pub/Sub que o orçamento está configurado para usar.

  2. Verifique se você adicionou as dependências descritas em Limitar (desativar) o faturamento para interromper o uso.

  3. Copie o código abaixo na função do Cloud Run.

  4. Defina a função a ser executada como limitUse (Node) ou limit_use (Python).

  5. Dependendo do ambiente de execução, a variável de ambiente GCP_PROJECT pode ser definida automaticamente. Revise a lista de variáveis de ambiente definidas automaticamente e determine se você precisa definir manualmente a variável GCP_PROJECT para o projeto que executa as máquinas virtuais.

  6. Defina o parâmetro ZONE. Trata-se da zona em que as instâncias serão interrompidas para este exemplo.

Node.js

const {CloudBillingClient} = require('@google-cloud/billing');
const {InstancesClient} = require('@google-cloud/compute');

const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
const instancesClient = new InstancesClient();
const ZONE = 'us-central1-a';

exports.limitUse = async pubsubEvent => {
  const pubsubData = JSON.parse(
    Buffer.from(pubsubEvent.data, 'base64').toString()
  );
  if (pubsubData.costAmount <= pubsubData.budgetAmount) {
    return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
  }

  const instanceNames = await _listRunningInstances(PROJECT_ID, ZONE);
  if (!instanceNames.length) {
    return 'No running instances were found.';
  }

  await _stopInstances(PROJECT_ID, ZONE, instanceNames);
  return `${instanceNames.length} instance(s) stopped successfully.`;
};

/**
 * @return {Promise} Array of names of running instances
 */
const _listRunningInstances = async (projectId, zone) => {
  const [instances] = await instancesClient.list({
    project: projectId,
    zone: zone,
  });
  return instances
    .filter(item => item.status === 'RUNNING')
    .map(item => item.name);
};

/**
 * @param {Array} instanceNames Names of instance to stop
 * @return {Promise} Response from stopping instances
 */
const _stopInstances = async (projectId, zone, instanceNames) => {
  await Promise.all(
    instanceNames.map(instanceName => {
      return instancesClient
        .stop({
          project: projectId,
          zone: zone,
          instance: instanceName,
        })
        .then(() => {
          console.log(`Instance stopped successfully: ${instanceName}`);
        });
    })
  );
};

Python

import base64
import json
import os

from googleapiclient import discovery

PROJECT_ID = os.getenv("GCP_PROJECT")
PROJECT_NAME = f"projects/{PROJECT_ID}"
ZONE = "us-west1-b"


def limit_use(data, context):
    pubsub_data = base64.b64decode(data["data"]).decode("utf-8")
    pubsub_json = json.loads(pubsub_data)
    cost_amount = pubsub_json["costAmount"]
    budget_amount = pubsub_json["budgetAmount"]
    if cost_amount <= budget_amount:
        print(f"No action necessary. (Current cost: {cost_amount})")
        return

    compute = discovery.build(
        "compute",
        "v1",
        cache_discovery=False,
    )
    instances = compute.instances()

    instance_names = __list_running_instances(PROJECT_ID, ZONE, instances)
    __stop_instances(PROJECT_ID, ZONE, instance_names, instances)


def __list_running_instances(project_id, zone, instances):
    """
    @param {string} project_id ID of project that contains instances to stop
    @param {string} zone Zone that contains instances to stop
    @return {Promise} Array of names of running instances
    """
    res = instances.list(project=project_id, zone=zone).execute()

    if "items" not in res:
        return []

    items = res["items"]
    running_names = [i["name"] for i in items if i["status"] == "RUNNING"]
    return running_names


def __stop_instances(project_id, zone, instance_names, instances):
    """
    @param {string} project_id ID of project that contains instances to stop
    @param {string} zone Zone that contains instances to stop
    @param {Array} instance_names Names of instance to stop
    @return {Promise} Response from stopping instances
    """
    if not len(instance_names):
        print("No running instances were found.")
        return

    for name in instance_names:
        instances.stop(project=project_id, zone=zone, instance=name).execute()
        print(f"Instance stopped successfully: {name}")

Configurar permissões da conta de serviço

  1. Sua função do Cloud Run é executada como uma conta de serviço criada automaticamente. Para controlar o uso, você precisa conceder à conta de serviço permissões para todos os serviços do projeto em que ela precisa fazer alterações.
  2. Para identificar a conta de serviço correta, visualize os detalhes da sua função do Cloud Run, listada na parte inferior da página.
  3. No console do Google Cloud, acesse a página IAM para definir as permissões apropriadas.
    Acessar a página "IAM"
     
    Mostra a tela do IAM no Console do Google Cloud,
         em que é possível definir as permissões apropriadas para a conta de serviço
         que executa a função do Cloud Run.

Confirmar que as instâncias foram interrompidas

Quando o orçamento envia uma notificação, as máquinas virtuais do Compute Engine são interrompidas.

Para testar a função, publique uma mensagem de exemplo com a mensagem de teste anterior. Para confirmar que a função foi executada com êxito, verifique as máquinas virtuais do Compute Engine no Console do Google Cloud.