Ejemplos de respuestas automatizadas de control de costos

Ejemplo de arquitectura de referencia

Diagrama de un ejemplo que usa 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 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 un tema de Pub/Sub para tu presupuesto. Esto se describe en detalle 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 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 un código fuente y dependencias para que ejecute la función.

  6. Asegúrate de configurar la Función que se ejecutará en el nombre de la función correcta.

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

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 mensaje, como el ID de la cuenta de facturación. Consulta la documentación completa sobre el formato 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 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 Cloud Function. Asegúrate de que el activador esté configurado en el mismo tema de Pub/Sub que tu presupuesto está configurado para usar.

  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==2.7.2 al archivo requirements.txt de la función:
    slackclient==2.7.2

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

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

    • 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, 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 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)

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. 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 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 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 mediante los pasos que se describen en Crea una Cloud Function. Asegúrate de que el activador esté configurado en el mismo tema de Pub/Sub que tu presupuesto está configurado para usar.

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

    Python

    Agrega google-api-python-client==1.9.3 al archivo requirements.txt de la función:
    google-api-python-client==1.9.3
    

  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 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 en que deseas limitar (inhabilitar) la Facturación de Cloud.

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})`;
  }

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

  await _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 = 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) => {
  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')

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, tienes que otorgarle los permisos correctos, como 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.

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 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 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 en el mismo tema de Pub/Sub que tu presupuesto está configurado para usar.

  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. Según tu entorno de ejecución, la variable de entorno GCP_PROJECT puede configurarse automáticamente. Revisa la lista de variables de entorno configuradas automáticamente y determina si necesitas configurar manualmente 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 {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.