Ejemplos de respuestas automatizadas de control de costos

Arquitectura de referencia de ejemplo

Diagrama de un ejemplo en el que se usan notificaciones programáticas de alertas de presupuesto para automatizar una respuesta de control de costos.
Figura 1: Explica un ejemplo del uso de alertas de presupuesto para automatizar las respuestas de control de costos con Pub/Sub para notificaciones programáticas y funciones de Cloud Functions a fin de automatizar una respuesta.

Si te preocupan los costos y necesitas controlar el entorno relacionado con tu presupuesto, puedes usar las notificaciones de presupuesto programáticas para automatizar tu respuesta de control de costos en función de las notificaciones de presupuesto.

Las notificaciones de presupuesto usan temas Pub/Sub para proporcionar un estado en tiempo real del presupuesto de Facturación de Cloud con la escalabilidad, flexibilidad y confiabilidad del middleware empresarial orientado a los mensajes.

En esta página se muestran ejemplos e instrucciones paso a paso 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 un tema de Pub/Sub para el presupuesto. Esto se describe de forma detallada en Administra las notificaciones de alertas de presupuesto programáticas.

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 la consola de Google Cloud, ve a la página 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 un código fuente y dependencias para que ejecute la función.

  6. Asegúrate de configurar la función que se ejecutará con el nombre correcto de la función.

Página Crear función en la sección Cloud Functions de la consola de Google Cloud. 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 Cloud Function

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 la consola de Google Cloud.

Prueba tu Cloud Function

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 tu función funciona como se esperaba, publica un mensaje en Pub/Sub con este objeto como el cuerpo del mensaje:

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

También puedes agregar atributos de mensajes, como el ID de la cuenta de facturación. Consulta la documentación completa sobre formatos de notificación para obtener más información.

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 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 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 Cloud Function. Asegúrate de que el activador esté configurado con el mismo tema de Pub/Sub que usa el presupuesto.

  2. Agrega dependencias:

    Node.js

    Copia lo siguiente en tu 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": "^126.0.0",
        "slack": "^11.0.1"
      },
      "devDependencies": {
        "@google-cloud/functions-framework": "^3.0.0",
        "c8": "^8.0.0",
        "gaxios": "^6.0.0",
        "mocha": "^10.0.0",
        "promise-retry": "^2.0.0",
        "proxyquire": "^2.1.0",
        "sinon": "^16.0.0",
        "wait-port": "^1.0.4"
      }
    }
    

    Python

    Copia lo siguiente en tu requirements.txt:

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

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

  4. Asegúrese de que los siguientes parámetros postMessage de la API de Slack estén configurados correctamente:

    • Token de acceso OAuth de usuario bot
    • Nombre del canal

Código de 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 => {
  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")

Ahora puedes Probar tu función de Cloud Functions para que un mensaje aparezca en Slack.

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. Inhabilitar la facturación en un proyecto hará que todos los servicios de Google Cloud del proyecto finalicen, incluidos los servicios de nivel gratuito.

¿Por qué inhabilitar la facturación?

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 la consola de Google Cloud.

Antes de comenzar a trabajar con este ejemplo, asegúrate de haber hecho lo siguiente:

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

Escribe una Cloud Function

A continuación, deberás configurar la función de Cloud Functions para llamar a la API de Cloud Billing. 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 mediante los pasos que se describen en Crea una Cloud Function. Asegúrate de que el activador esté configurado con el mismo tema de Pub/Sub que usa el presupuesto.

  2. Agrega las siguientes dependencias:

    Node.js

    Copia lo siguiente en tu 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": "^126.0.0",
        "slack": "^11.0.1"
      },
      "devDependencies": {
        "@google-cloud/functions-framework": "^3.0.0",
        "c8": "^8.0.0",
        "gaxios": "^6.0.0",
        "mocha": "^10.0.0",
        "promise-retry": "^2.0.0",
        "proxyquire": "^2.1.0",
        "sinon": "^16.0.0",
        "wait-port": "^1.0.4"
      }
    }
    

    Python

    Copia lo siguiente en tu requirements.txt:

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

  3. Copia el código que aparece a continuación en la Cloud Function.

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

  5. Según tu entorno de ejecución, la variable de entorno GOOGLE_CLOUD_PROJECT puede configurarse automáticamente. Revisa la lista de variables del entorno que se configuran automáticamente y determina si necesitas configurar de forma manual la variable GOOGLE_CLOUD_PROJECT para el proyecto en que deseas limitar (inhabilitar) la Facturación de Cloud.

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")

Configura los permisos de la cuenta de servicio

Tu Cloud Function se ejecuta como una cuenta de servicio creada de manera automática. Para que la cuenta de servicio pueda inhabilitar la facturación, debes otorgarle los permisos correctos, como los 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 la consola de Google Cloud.

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

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 Permisos de la consola de Google Cloud.

Valida que la Facturación de Cloud esté inhabilitada

Cuando el presupuesto envía una notificación, el proyecto especificado 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 podrá verse en la cuenta de facturación de Cloud, y los recursos del proyecto estarán inhabilitados, incluida la función de Cloud Functions 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 la consola de Google Cloud.

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 Cloud Function

  1. Crea una función nueva mediante los pasos que se describen en Crea una Cloud Function. Asegúrate de que el activador esté configurado con el mismo tema de Pub/Sub que usa el presupuesto.

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

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

  5. Según tu entorno de ejecución, la variable de entorno GCP_PROJECT puede configurarse automáticamente. Revisa la lista de variables del entorno que se configuran automáticamente y determina si necesitas configurar de forma manual la variable GCP_PROJECT para el proyecto que ejecuta las máquinas virtuales.

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

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

Configura los permisos de la cuenta de servicio

  1. Tu Cloud Function se ejecuta como una cuenta de servicio creada de manera automática. 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 la consola de Google Cloud, ve a la página IAM para configurar los permisos correspondientes.
    Ir a la página IAM
     
    Muestra la pantalla IAM en la consola de Google Cloud, en el que puedes establecer los permisos adecuados para la cuenta de servicio que ejecuta la Cloud Function.

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 la consola de Google Cloud.