Ejemplos de notificaciones de presupuesto programáticas

Si te preocupan los costos y necesitas controlar el entorno relacionado con tu presupuesto, puedes usar las notificaciones de presupuesto programáticas para automatizar las acciones de presupuesto.

Las notificaciones de presupuesto usan notificaciones de Pub/Sub para proporcionar un estado del presupuesto en tiempo real mediante la escalabilidad, la flexibilidad y la confiabilidad del middleware empresarial orientado a mensajes para la nube.

En esta página, se muestran instrucciones paso a paso y ejemplos sobre cómo usar las notificaciones de presupuesto con Cloud Functions a fin de automatizar la administración de los costos.

Configura las notificaciones de presupuesto

El primer paso es habilitar una notificación de Pub/Sub para el presupuesto. Este paso se describe en Administrar las notificaciones.

Después de habilitar las notificaciones de presupuesto, ten en cuenta lo siguiente:

  • Tema de Pub/Sub: Es la terminal de notificaciones configurada para el presupuesto.
  • Budget ID (ID de presupuesto): Es un ID único para el presupuesto que se incluye en todas las notificaciones. Podrás encontrar este ID en el presupuesto en Manage notifications (Administra las notificaciones). El ID se muestra después de que seleccionas Connect a Pub/Sub topic to this budget (Conectar un tema de Pub/Sub a este presupuesto).

Sección Administra las notificaciones en Google Cloud Console, en la que puedes vincular un tema de Pub/Sub a un presupuesto. Incluye el ID de presupuesto, el nombre del proyecto y el tema de Pub/Sub.

Escucha tus notificaciones

El próximo paso es escuchar las notificaciones mediante la suscripción a tu tema de Pub/Sub. Si no tienes un suscriptor, Pub/Sub descartará los mensajes publicados y no podrás recuperarlos más tarde.

Aunque hay varias maneras en las que puedes suscribirte al tema, para estos ejemplos usaremos activadores de Cloud Function.

Crea una función de Cloud Functions

Para crear una Cloud Function nueva, haz lo siguiente:

  1. En Cloud Console, ve a la página de Cloud Functions.

    Ir a la página Cloud Functions

  2. Haz clic en CREATE FUNCTION (Crear función) y asígnale un nombre que sea significativo para tu presupuesto.

  3. En Trigger (Activador), selecciona Pub/Sub topic (Tema de Pub/Sub).

  4. Selecciona el tema que configuraste en tu presupuesto.

  5. Proporciona el código fuente y las dependencias para que se ejecute la función.

Página Create function (Crear función) en la sección Cloud Functions de Cloud Console. Incluye el nombre de la función, la cantidad de memoria asignada, el tipo de activador y el tema de Pub/Sub que configuraste en el presupuesto.

Describe tu función de Cloud Functions

Para indicarle a la función de Cloud Functions lo que quieres que haga con la notificación, puedes escribir el código mediante el editor directo o subir un archivo. Para obtener más detalles sobre las notificaciones que recibirá el código, consulta Formato de las notificaciones.

Por ejemplo, una función puede registrar notificaciones, atributos y datos de Pub/Sub cuando se activa por una notificación de presupuesto. Para obtener más información, consulta Activadores de Pub/Sub.

Visualiza los eventos de la función de Cloud Functions

Después de guardar la función de Cloud Functions, puedes hacer clic en VIEW LOGS (Ver registros) para ver tus notificaciones de presupuesto registradas. Esto muestra los registros de las invocaciones de función.

Se muestra dónde puedes encontrar la sección View logs (Ver registros) en la pantalla y la lista de eventos de la función de Cloud Functions en Cloud Console.

Prueba tu función de Cloud Functions

Las notificaciones se envían a Pub/Sub y los suscriptores reciben los mensajes. Para probar una notificación de muestra y asegurarte de que la función actúe como se espera, publica un mensaje de prueba en Pub/Sub similar a este:

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

Envía notificaciones a Slack

Los correos electrónicos no son siempre la mejor manera de mantenerte actualizado con los costos de la nube, en particular si tu presupuesto es fundamental y urgente. Con las notificaciones, puedes reenviar los mensajes de presupuesto a otros medios.

En este ejemplo, se describe cómo reenviar notificaciones de presupuesto a Slack. De esta manera, cada vez que la Facturación de Cloud publique una notificación de presupuesto, una función de Cloud Functions usa un bot para publicar un mensaje en un canal de Slack del espacio de trabajo del bot.

Configura un canal y los permisos de Slack

El primer paso es crear el espacio de trabajo de Slack y los tokens de usuarios bot que se usan para llamar a la API de Slack. Los tokens de la API se pueden administrar en https://api.slack.com/apps. Para obtener instrucciones detalladas, consulta Bot Users (Usuarios bot) en el sitio de Slack.

Configura las notificaciones de Slack.

Escribe una Cloud Function

  1. Crea una función nueva mediante los pasos que se describen en Crea una función de Cloud Functions.

  2. Agrega dependencias:

    Node.js

    Agrega una dependencia en el paquete de administración de socios de red de Slack al archivo package.json de la función.
    {
      "name": "cloud-functions-billing",
      "version": "0.0.1",
      "dependencies": {
        "slack": "^11.0.1"
      }
    }
    

    Python

    Agrega slackclient==1.3.0 al archivo requirements.txt de la función:
    slackclient==1.3.0

  3. Escribe el código para publicar notificaciones de presupuesto en un canal de chat de Slack mediante la API de Slack.

  4. Configura los siguientes parámetros postMessage de la API de Slack en tu código:

    • Token de acceso OAuth de usuario bot
    • Canal
    • Texto

Por ejemplo:

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)

Limita (inhabilita) la facturación para detener el uso

En este ejemplo, se muestra cómo limitar los costos y detener el uso de un proyecto mediante la inhabilitación de la Facturación de Cloud. Esto hará que todos los servicios de Google Cloud cancelen los servicios de nivel no gratuito del proyecto.

Es posible que limites los costos porque tienes un límite estricto en la cantidad de dinero que puedes invertir en Google Cloud. Esto es habitual en el caso de estudiantes, investigadores o desarrolladores que trabajan en entornos de zona de pruebas. En estos casos, quieres detener el gasto y es posible que estés dispuesto a cerrar todos los servicios y el uso de Google Cloud cuando se alcance el límite del presupuesto.

Para nuestro ejemplo, usamos acme-backend-dev como un proyecto de no producción en el cual la Facturación de Cloud se puede inhabilitar de forma segura.

Configura el límite de presupuesto en Cloud Console.

Antes de comenzar, tienes que configurar un presupuesto para supervisar los costos del proyecto y habilitar las notificaciones de presupuesto.

Se muestra la lista de las alertas de la Facturación de Cloud en Cloud Console.

Escribe una función de Cloud Functions

A continuación, deberás configurar la función de Cloud Functions para llamar a la API de Facturación de Cloud. Esto permite que la función de Cloud Functions inhabilite la Facturación de Cloud para nuestro proyecto de ejemplo acme-backend-dev.

  1. Crea una función nueva con los pasos que se describen en Crea una función de Cloud Functions.

  2. Agrega las siguientes dependencias:

    Node.js

    Agrega dependencias en googleapis y google-auth-library al archivo package.json de la función:
    {
     "name": "cloud-functions-billing",
     "version": "0.0.1",
     "dependencies": {
        "google-auth-library": "^2.0.0",
        "googleapis": "^33.0.0"
     }
    }

    Python

    Agrega oauth2client==4.1.3 y google-api-python-client==1.7.4 al archivo requirements.txt de la función:
    oauth2client==4.1.3
    google-api-python-client==1.7.4
    

  3. Copia el código que aparece a continuación en la función de Cloud Functions.

  4. Configura la función para que se ejecute en “stopBilling” (Node) o “stop_billing” (Python).

  5. Establece el parámetro PROJECT_NAME en el proyecto en el que deseas limitar (inhabilitar) la Facturación de Cloud. Importante: Si no modificas este parámetro de proyecto, la cuenta de facturación de Cloud se quitará del proyecto en el que se ejecuta esta función de Cloud Functions.

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)}')

Configura los permisos de la cuenta de servicio

La fución de Cloud Functions se ejecuta como una cuenta de servicio creada automáticamente. Para que la cuenta de servicio pueda inhabilitar la facturación, tienes que otorgarle permisos de administrador de facturación.

Para identificar la cuenta de servicio correcta, visualiza los detalles de la función de Cloud Functions. La cuenta de servicio se encuentra en la parte inferior de la página.

Se muestra dónde se puede encontrar la información de la cuenta de servicio en la sección Cloud Functions de Cloud Console.

Puedes administrar los permisos de administrador de facturación en la página de facturación de Cloud Console.

Para otorgar privilegios de administrador de cuenta de facturación a la cuenta de servicio, selecciona el nombre de la cuenta de servicio.

Se muestra dónde seleccionar el nombre de la cuenta de servicio y la función de administrador de la cuenta de facturación en la sección Permissions (Permisos) de Cloud Console.

En este código de ejemplo, también se requiere que se habilite la API de Facturación de Cloud.

Valida que la Facturación de Cloud esté inhabilitada

Cuando el presupuesto envía una notificación, el proyecto ya no tendrá una cuenta de facturación de Cloud. Si deseas probar la función, publica un mensaje de muestra con el mensaje de prueba anterior. El proyecto ya no será visible en la cuenta de facturación de Cloud y los recursos del proyecto estarán inhabilitados, incluida la función si está en el mismo proyecto.

Se muestra que el proyecto de ejemplo ya no es visible en la lista de proyectos vinculados a la cuenta de facturación de Cloud. Esto valida que la Facturación de Cloud esté inhabilitada en el proyecto.

Puedes volver a habilitar la Facturación de Cloud de forma manual para el proyecto en Cloud Console.

Controla el uso de manera selectiva

La limitación (inhabilitación) de la Facturación de Cloud, como se describe en el ejemplo anterior, es binaria y terminal. El proyecto estará habilitado o inhabilitado. Cuando está inhabilitado, todos los servicios se detienen y, al final, todos los recursos se borran.

Si requieres una respuesta más matizada, puedes controlar los recursos de manera selectiva. Por ejemplo, si deseas detener algunos recursos de Compute Engine, pero dejar Cloud Storage intacto, puedes controlar el uso de forma selectiva. Esto reduce el costo por hora sin inhabilitar completamente el entorno.

Puedes escribir una política matizada a tu gusto. Sin embargo, en nuestro ejemplo, el proyecto ejecuta una investigación con varias máquinas virtuales de Compute Engine y almacena resultados en Cloud Storage. Con esta función de Cloud Functions de ejemplo, se cerrarán todas las instancias de Compute Engine, pero los resultados almacenados no se verán afectados después de que se exceda el presupuesto.

Escribe una función de Cloud Functions

  1. Crea una función nueva con los pasos que se describen en Crea una función de Cloud Functions.

  2. Asegúrate de que agregaste las dependencias que se describen en Limita (inhabilita) la facturación para detener el uso.

  3. Copia el código que aparece a continuación en la función de Cloud Functions.

  4. Configura la función que se ejecutará en “limitUse” (Nodo) o “limit_use” (Python).

  5. Establece el parámetro ZONE. Esta será la zona en la que se detendrán las instancias para esta muestra.

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}')

Configura los permisos de la cuenta de servicio

  1. La función de Cloud Functions se ejecuta como una cuenta de servicio creada automáticamente. Para controlar el uso, tienes que otorgar permisos de cuenta de servicio a cualquier servicio en el proyecto que necesite hacer cambios.
  2. Para identificar la cuenta de servicio correcta, visualiza los detalles de la función de Cloud Functions. La cuenta de servicio se encuentra en la parte inferior de la página.
  3. En Cloud Console, ve a la página IAM para configurar los permisos correspondientes.
    Ir a la página IAM
     
    Se muestra la pantalla de Cloud IAM en Cloud Console, en la que puedes configurar los permisos adecuados para la cuenta de servicio que ejecuta la función de Cloud Functions.

Valida que las instancias se detengan

Cuando el presupuesto envía una notificación, las máquinas virtuales de Compute Engine se detienen.

Para probar la función, publica un mensaje de muestra con el mensaje de prueba anterior. Para validar que la función se ejecutó de manera correcta, verifica las máquinas virtuales de Compute Engine en Cloud Console.