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 o Cloud Functions 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.

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

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

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

    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==2.7.2 ao arquivo requirements.txt da função:
    slackclient==2.7.2

  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

Código de 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 = `${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 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 Faturamento do Cloud. Isso fará com que todos os serviços do Google Cloud encerrem os recursos da camada não gratuita 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.

Configure o limite de orçamento no Console do 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 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. 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

    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": "^52.0.0"
     }
    }

    Python

    Adicione google-api-python-client==1.9.3 ao arquivo requirements.txt da função:
    google-api-python-client==1.9.3
    

  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. 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 para o qual quer limitar (desativar) o Cloud Billing.

Node.js

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

const PROJECT_ID = process.env.GOOGLE_CLOUD_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})`;
  }

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

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

/**
 * @return {Promise} Credentials set globally
 */
const _setAuthCredential = () => {
  const client = new GoogleAuth({
    scopes: [
      '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) => {
  try {
    const res = await billing.getBillingInfo({name: projectName});
    return res.data.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.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

    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 é 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 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

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 acima. O projeto não estará mais visível na conta de faturamento do Cloud e os recursos no projeto serão desativados, incluindo a Função do Cloud, 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. Verifique se o gatilho está definido como o mesmo tópico do Pub/Sub que o orçamento está configurado para usar.

  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. 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 {google} = require('googleapis');
const {GoogleAuth} = require('google-auth-library');

const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
/**
 * @return {Promise} Credentials set globally
 */
const _setAuthCredential = () => {
  const client = new GoogleAuth({
    scopes: [
      '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-central1-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})`;
  }

  _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"
     
    Mostra a tela do IAM no Console do Cloud,
         em que é 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.