Exemplos de notificação programática de orçamentos

Se você quer economizar e precisa controlar seu ambiente de acordo com o orçamento, use as notificações programáticas para automatizar as ações relacionadas a questões orçamentárias.

As notificações de orçamento usam o Pub/Sub para fornecer o estado do orçamento em tempo real, usando a escalonabilidade, a flexibilidade e a confiabilidade do middleware corporativo voltado a mensagens para a nuvem.

Nesta página, você verá exemplos e instruções passo a passo sobre como usar as notificações de orçamento com o Cloud Functions para automatizar o gerenciamento de custos.

Configurar notificações de orçamento

A primeira etapa é ativar uma notificação do Pub/Sub para seu orçamento. Esta etapa é descrita em Gerenciar notificações.

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, incluso 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

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 descartará as mensagens publicadas e não será possível recuperá-las depois.

Há muitas maneiras de assinar o tópico, mas usaremos os gatilhos de Função do Cloud nestes exemplos.

Criar uma Função do Cloud

Para criar uma nova Função do Cloud:

  1. No Console do Cloud, acesse a página Cloud Functions.

    Acessar a página "Cloud Functions"

  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á.

A página

Descrever sua Função do Cloud

Para informar à Função do Cloud 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.

Visualizar seus eventos de Função do Cloud

Depois de salvar a Função do Cloud, 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 no Console do Cloud.

Testar a Função do Cloud

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 de teste semelhante a esta no Pub/Sub:

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

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 sensível ao tempo. Com as notificações, você encaminha suas 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 usará um bot para postar uma mensagem em um canal do Slack no espaço de trabalho desse bot.

Configurar um canal do Slack e permissões

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 (links em inglês).

Configurar notificações do Slack.

Criar uma Função do Cloud

  1. Para criar uma nova função, siga as etapas em Criar uma Função do Cloud.

  2. Adicionar dependências:

    Node.js

    Adicione uma dependência do pacote slack npm ao arquivo package.json da função.
    {
      "name": "cloud-functions-billing",
      "version": "0.0.1",
      "dependencies": {
        "slack": "^11.0.1"
      }
    }
    

    Python

    Adicione slackclient==1.3.0 ao arquivo requirements.txt da função:
    slackclient==1.3.0

  3. Escreva o código para postar notificações de orçamento em um canal de bate-papo do Slack usando a API Slack.

  4. Defina os seguintes parâmetros postMessage da API Slack no código:

    • Token de acesso do OAuth do usuário de bot
    • Canal
    • Texto

Exemplo:

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, context) => {
  const pubsubAttrs = pubsubEvent.attributes;
  const pubsubData = Buffer.from(pubsubEvent.data, 'base64').toString();
  const budgetNotificationText = `${pubsubAttrs}, ${pubsubData}`;

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

  return 'Slack notification sent successfully';
};

Python

import slack

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

CHANNEL_ID = 'C0XXXXXX'

slack_client = slack.WebClient(token=BOT_ACCESS_TOKEN)

def notify_slack(data, context):
    pubsub_message = data

    notification_attrs = json.dumps(pubsub_message['attributes'])
    notification_data = base64.b64decode(data['data']).decode('utf-8')
    budget_notification_text = f'{notification_attrs}, {notification_data}'

    slack_client.api_call(
      'chat.postMessage',
      channel=CHANNEL_ID,
      text=budget_notification_text)

Limitar (desativar) o faturamento para interromper o uso

Neste exemplo, mostramos como limitar custos e interromper o uso de um projeto desativando o Faturamento do Cloud. Isso fará com que todos os serviços do Google Cloud encerrem os recursos de nível não gratuito do projeto.

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.

Configurar o limite de orçamento no Console do Cloud.

Antes de começar, é preciso configurar um orçamento para monitorar os custos do projeto e ativar as notificações de orçamento.

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

Criar uma Função do Cloud

Em seguida, você precisará configurar a Função do Cloud para chamar a API Cloud Billing. Isso permite que a Função do Cloud desative o Faturamento do Cloud 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.

  2. Adicione as seguintes dependências:

    Node.js

    Adicione dependências em googleapis e google-auth-library ao arquivo package.json da função:
    {
     "name": "cloud-functions-billing",
     "version": "0.0.1",
     "dependencies": {
        "google-auth-library": "^2.0.0",
        "googleapis": "^33.0.0"
     }
    }

    Python

    Adicione oauth2client==4.1.3 e google-api-python-client==1.7.4 ao arquivo requirements.txt da função:
    oauth2client==4.1.3
    google-api-python-client==1.7.4
    

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

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

  5. Defina o parâmetro PROJECT_NAME para o projeto em que você quer limitar (desativar) o Faturamento do Cloud. Importante: se você não mudar esse parâmetro do projeto, a Conta de faturamento do Cloud será removida do projeto em que a Função do Cloud estiver em execução.

Node.js

const {google} = require('googleapis');
const {auth} = require('google-auth-library');

const PROJECT_ID = process.env.GCP_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
const billing = google.cloudbilling('v1').projects;

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

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

/**
 * @return {Promise} Credentials set globally
 */
const _setAuthCredential = async () => {
  const res = await auth.getApplicationDefault();

  let client = res.credential;
  if (client.hasScopes && !client.hasScopes()) {
    client = client.createScoped([
      'https://www.googleapis.com/auth/cloud-billing',
      'https://www.googleapis.com/auth/cloud-platform',
    ]);
  }

  // Set credential globally for all requests
  google.options({
    auth: client,
  });
};

/**
 * 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) => {
  const res = await billing.getBillingInfo({name: projectName});
  return res.data.billingEnabled;
};

/**
 * 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.updateBillingInfo({
    name: projectName,
    resource: {billingAccountName: ''}, // Disable billing
  });
  return `Billing disabled: ${JSON.stringify(res.data)}`;
};

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

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

    projects = billing.projects()

    if __is_billing_enabled(PROJECT_NAME, projects):
        print(__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
    """
    res = projects.getBillingInfo(name=project_name).execute()
    return res['billingEnabled']

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
    @return {string} Text containing response from disabling billing
    """
    body = {'billingAccountName': ''}  # Disable billing
    res = projects.updateBillingInfo(name=project_name, body=body).execute()
    print(f'Billing disabled: {json.dumps(res)}')

Configurar permissões da conta de serviço

Sua Função do Cloud é executada como uma conta de serviço criada automaticamente. Para que essa conta desative o faturamento, é preciso conceder a ela as permissões de administrador de faturamento.

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

Mostra onde as informações da conta de serviço são encontradas na seção

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

Para conceder privilégios de administrador da conta de faturamento à conta 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

Neste exemplo de código, também é necessário que a API Cloud Billing esteja ativada.

Validar se o Faturamento do Cloud está desativado

Quando o orçamento enviar uma notificação, o projeto não 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 acima. O projeto não ficará mais visível na Conta de faturamento do Cloud e os recursos no projeto serão desativados, incluindo a função, 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 Faturamento do Cloud para seu projeto no Console do 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 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, todas as instâncias do Compute Engine serão encerradas, mas isso não afetará nossos resultados armazenados depois que o orçamento for excedido.

Criar uma Função do Cloud

  1. Para criar uma nova função, siga as etapas em Criar uma Função do Cloud.

  2. Certifique-se de ter adicionado as dependências descritas em Limitar (desativar) o faturamento para interromper o uso.

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

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

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

Node.js

const {google} = require('googleapis');
const {auth} = require('google-auth-library');

const PROJECT_ID = process.env.GCP_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
/**
 * @return {Promise} Credentials set globally
 */
const _setAuthCredential = async () => {
  const res = await auth.getApplicationDefault();

  let client = res.credential;
  if (client.hasScopes && !client.hasScopes()) {
    client = client.createScoped([
      'https://www.googleapis.com/auth/cloud-billing',
      'https://www.googleapis.com/auth/cloud-platform',
    ]);
  }

  // Set credential globally for all requests
  google.options({
    auth: client,
  });
};
const compute = google.compute('v1');
const ZONE = 'us-west1-a';

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

  await _setAuthCredential();

  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 res = await compute.instances.list({
    project: projectId,
    zone: zone,
  });

  const instances = res.data.items || [];
  const ranInstances = instances.filter((item) => item.status === 'RUNNING');
  return ranInstances.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 compute.instances
        .stop({
          project: projectId,
          zone: zone,
          instance: instanceName,
        })
        .then((res) => {
          console.log(`Instance stopped successfully: ${instanceName}`);
          return res.data;
        });
    })
  );
};

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 é executada como uma conta de serviço criada automaticamente. Para controlar o uso, você precisa conceder à conta de serviço permissões aos serviços do projeto em que ela fará alterações.
  2. Para identificar a conta de serviço correta, visualize os detalhes da sua Função do Cloud, listada na parte inferior da página.
  3. No Console do Cloud, acesse a página IAM para definir as permissões apropriadas.
    Acessar a página "IAM"
     
    Veja a tela do Cloud IAM no Console do Cloud, onde é possível definir as permissões apropriadas para a conta de serviço que executa a Função do Cloud.

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 acima. Para confirmar que a função foi executada com êxito, verifique as máquinas virtuais do Compute Engine no Console do Cloud.