Exemples de réponses automatiques de contrôle des coûts

Exemple d'architecture de référence

Schéma d'un exemple d'utilisation de notifications automatisées d'alerte budgétaire pour automatiser une réponse de contrôle des coûts.
Figure 1 : illustre un exemple d'utilisation des alertes de budget pour automatiser les réponses de contrôle des coûts à l'aide de Pub/Sub pour les notifications automatisées et de Cloud Functions pour automatiser une réponse.

Si vous maîtrisez les coûts et souhaitez contrôler votre environnement par rapport à votre budget, vous pouvez utiliser les notifications de budget automatisées pour automatiser votre réponse en fonction de la notification budgétaire.

Les notifications budgétaires utilisent des sujets Pub/Sub pour fournir un état en temps réel du budget Cloud Billing, en utilisant l'évolutivité, la flexibilité et la fiabilité du middleware de messagerie d'entreprise pour le cloud.

Cette page contient des exemples et des instructions détaillées pour utiliser les notifications de budget avec Cloud Functions en vue d'automatiser la gestion des coûts.

Configurer les notifications de budget

La première étape consiste à activer un sujet Pub/Sub pour le budget. Pour en savoir plus, consultez la page Gérer les notifications d'alerte budgétaire.

Une fois que vous avez activé les notifications de budget, veillez à prendre note des informations suivantes :

  • Sujet Pub/Sub : il s'agit du point de terminaison des notifications configuré pour le budget.
  • ID de budget : il s'agit d'un ID unique pour le budget, qui est indiqué dans toutes les notifications. Vous pouvez localiser l'ID du budget depuis le budget sous Manage notifications (Gérer les notifications). L'ID s'affiche lorsque vous sélectionnez Connect a Pub/Sub topic to this budget (Associer un sujet Pub/Sub à ce budget).

La section

Écouter les notifications

L'étape suivante consiste à écouter les notifications en vous abonnant au sujet Pub/Sub. Si le sujet n'a aucun abonné, Pub/Sub abandonnera les messages publiés et vous ne pourrez pas les récupérer plus tard.

Bien qu'il existe de nombreuses façons de créer un abonnement associé au sujet, nous utiliserons dans ces exemples des déclencheurs de fonctions Cloud.

Créer une fonction Cloud

Pour créer une fonction Cloud, procédez comme suit :

  1. Dans Cloud Console, accédez à la page Cloud Functions.

    Accéder à la page "Cloud Functions"

  2. Cliquez sur  CRÉER UNE FONCTION et donnez-lui un nom en relation avec le budget.

  3. Sous Trigger (Déclencheur), sélectionnez Pub/Sub topic (Sujet Pub/Sub).

  4. Sélectionnez le sujet que vous avez configuré dans le budget.

  5. Fournissez le code source et les dépendances que la fonction doit exécuter.

  6. Assurez-vous de définir la Fonction à exécuter sur le nom de fonction correct.

Page

Décrire la fonction Cloud

Pour indiquer à la fonction Cloud ce qu'elle doit faire avec la notification reçue, vous pouvez soit écrire le code à l'aide de l'éditeur intégré, soit importer un fichier. Pour en savoir plus sur les notifications que le code recevra, consultez la section Format des notifications.

Par exemple, une fonction peut enregistrer les notifications, les attributs et les données Pub/Sub reçus lorsqu'elle est déclenchée par une notification de budget. Pour en savoir plus, consultez la section Déclencheurs Pub/Sub.

Afficher les événements de la fonction Cloud

Après avoir enregistré la fonction Cloud, vous pouvez cliquer sur VIEW LOGS (AFFICHER LES JOURNAUX) pour afficher les notifications de budget enregistrées. Les journaux des appels de fonctions s'affichent alors.

Permet de localiser la section

Tester la fonction Cloud

Les notifications sont envoyées à Pub/Sub et les abonnés reçoivent les messages. Pour tester un exemple de notification afin de vous assurer que votre fonction s'exécute comme prévu, publiez un message dans Pub/Sub en utilisant cet objet comme corps du message :

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

Vous pouvez également ajouter des attributs de messages, tels que l'ID du compte de facturation. Pour en savoir plus, consultez la documentation complète sur le format de notification.

Envoyer des notifications à Slack

L'e-mail n'est pas toujours le meilleur moyen de rester informé des coûts associés à votre utilisation du cloud, surtout si votre budget est critique et sensible au facteur temps. Avec les notifications, vous pouvez transférer vos messages budgétaires vers d'autres plates-formes.

Dans cet exemple, nous décrivons comment transférer les notifications de budget vers Slack. Ainsi, chaque fois que Cloud Billing publie une notification de budget, une fonction Cloud Functions publie un message, à l'aide d'un bot, sur un canal Slack de l'espace de travail du bot.

Configurer un canal Slack et des autorisations

La première étape consiste à créer un espace de travail Slack ainsi que les jetons de l'utilisateur bot utilisés pour appeler l'API Slack. Les jetons d'API peuvent être gérés à l'adresse https://api.slack.com/apps. Pour obtenir des instructions détaillées, consultez la page concernant les utilisateurs bot sur le site de Slack.

Configurer les notifications Slack.

Écrire une fonction Cloud

  1. Créez une fonction en suivant les étapes décrites dans Créer une fonction Cloud Functions. Assurez-vous que le déclencheur est défini sur le même sujet Pub/Sub que celui sur lequel votre budget est configuré.

  2. Ajoutez des dépendances :

    Node.js

    Ajoutez une dépendance au package npm Slack sur le fichier package.json de la fonction.
    {
      "name": "cloud-functions-billing",
      "version": "0.0.1",
      "dependencies": {
        "slack": "^11.0.1"
      }
    }
    

    Python

    Ajoutez slackclient==2.7.2 au fichier requirements.txt de votre fonction :
    slackclient==2.7.2

  3. Rédigez le code ou utilisez l'exemple ci-dessous pour publier des notifications de budget sur un canal de discussion Slack à l'aide de l'API Slack.

  4. Assurez-vous que les paramètres postMessage de l'API Slack suivants sont correctement définis :

    • Jeton d'accès OAuth de l'utilisateur bot
    • Nom du canal

Exemple de code :

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

Vous pouvez maintenant tester votre fonction Cloud pour afficher un message dans Slack.

Plafonner (désactiver) la facturation pour mettre fin à toute utilisation

Cet exemple vous montre comment plafonner les coûts et comment mettre fin à toute utilisation d'un projet en désactivant Cloud Billing. Ainsi, tous les services Google Cloud arrêteront les services payants du projet.

Vous pouvez vouloir plafonner les coûts si l'on vous impose une limite stricte sur ce que vous pouvez dépenser sur Google Cloud. C'est typiquement le cas des étudiants, chercheurs ou développeurs travaillant dans des environnements de bac à sable. Dans de telles circonstances, il est important de bloquer les dépenses, voire d'arrêter tous les services Google Cloud et leur utilisation dès lors que le seuil budgétaire est atteint.

Dans notre exemple, nous utilisons acme-backend-dev comme projet de non-production, sur lequel Cloud Billing peut être désactivé en toute sécurité.

Configure le plafond budgétaire dans Cloud Console.

Avant de commencer à utiliser cet exemple, assurez-vous d'avoir effectué les opérations suivantes :

  • Activez l'API Cloud Billing. Votre fonction Cloud Functions doit appeler l'API Cloud Billing pour désactiver Cloud Billing pour un projet.

  • Configurez un budget pour surveiller les coûts du projet et activer les notifications de budget.

Affiche la liste des alertes Cloud Billing dans Cloud Console.

Écrire une fonction Cloud

Vous devez désormais configurer la fonction Cloud pour qu'elle appelle l'API Cloud Billing. Cela permet à la fonction Cloud de désactiver Cloud Billing pour le projet acme-backend-dev utilisé dans cet exemple.

  1. Créez une fonction en suivant les étapes décrites dans Créer une fonction Cloud Functions. Assurez-vous que le déclencheur est défini sur le même sujet Pub/Sub que celui sur lequel votre budget est configuré.

  2. Ajoutez les dépendances suivantes :

    Node.js

    Ajoutez des dépendances à googleapis et google-auth-library sur le fichier package.json de la fonction :
    {
     "name": "cloud-functions-billing",
     "version": "0.0.1",
     "dependencies": {
       "google-auth-library": "^2.0.0",
       "googleapis": "^52.0.0"
     }
    }

    Python

    Ajoutez google-api-python-client==1.9.3 au fichier requirements.txt de votre fonction :
    google-api-python-client==1.9.3
    

  3. Copiez le code ci-dessous dans la fonction Cloud Functions.

  4. Définissez la fonction à exécuter sur "stopBilling" (Node) ou "stop_billing" (Python).

  5. Selon votre environnement d'exécution, la variable d'environnement GCP_PROJECT peut être définie automatiquement. Consultez la liste des variables d'environnement définies automatiquement et déterminez si vous devez définir manuellement la variable GCP_PROJECT sur le projet pour lequel vous souhaitez plafonner (désactiver) 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 => {
  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')

Configurer les autorisations du compte de service

La fonction Cloud Functions est exécutée en tant que compte de service automatiquement créé. Pour que le compte de service puisse désactiver la facturation, vous devez lui accorder les autorisations appropriées, telles que Administrateur de facturation.

Pour identifier le bon compte de service, consultez les informations de la fonction. Le compte de service est répertorié au bas de la page.

Indique l'emplacement des informations du compte de service dans la section

Vous pouvez gérer les autorisations de l'administrateur de la facturation sur la page Facturation de Cloud Console.

Pour accorder des privilèges de Billing Account Administrator (Administrateur de compte de facturation) au compte de service, sélectionnez le nom du compte de service.

Indique où sélectionner le nom du compte de service et le rôle

Vérifier que Cloud Billing est désactivé

Lorsque le budget envoie une notification, le projet spécifié n'a plus de compte Cloud Billing. Si vous souhaitez tester la fonction, publiez un exemple de message semblable à celui ci-dessus. Le projet ne sera plus visible sous le compte Cloud Billing et les ressources du projet seront désactivées, y compris la fonction Cloud si elle se trouve dans ce projet.

Indique que l'exemple de projet n'est plus visible dans la liste des projets associés au compte Cloud Billing. Cela permet de vérifier que Cloud Billing est désactivé pour le projet.

Vous pouvez réactiver manuellement Cloud Billing pour votre projet dans Cloud Console.

Contrôler l'utilisation de manière sélective

Le plafonnement (la désactivation) de Cloud Billing, tel qu'il est décrit dans l'exemple précédent, est binaire et final. Le projet est soit activé, soit désactivé. Lorsqu'il est désactivé, tous les services sont arrêtés et toutes les ressources finissent par être supprimées.

Si vous souhaitez obtenir une réponse plus nuancée, vous pouvez contrôler les ressources de manière sélective. Par exemple, si vous souhaitez arrêter certaines ressources Compute Engine, mais garder Cloud Storage intact, vous pouvez contrôler l'utilisation de manière sélective. Cela réduit votre coût horaire sans désactiver complètement votre environnement.

Vous pouvez écrire des règles aussi nuancées que vous le souhaitez. Cependant, dans notre exemple, le projet effectue des recherches sur plusieurs machines virtuelles Compute Engine et stocke les résultats dans Cloud Storage. Cet exemple de fonction Cloud arrêtera toutes les instances de Compute Engine, mais n'aura aucune incidence sur les résultats stockés une fois le budget dépassé.

Écrire une fonction Cloud

  1. Créez une fonction en suivant les étapes décrites dans Créer une fonction Cloud Functions. Assurez-vous que le déclencheur est défini sur le même sujet Pub/Sub que celui sur lequel votre budget est configuré.

  2. Assurez-vous que vous avez ajouté les dépendances décrites dans Plafonner (désactiver) la facturation pour mettre fin à toute utilisation.

  3. Copiez le code ci-dessous dans la fonction Cloud Functions.

  4. Définissez la fonction à exécuter sur "limitUse" (Node) ou "limit_use" (Python).

  5. Selon votre environnement d'exécution, la variable d'environnement GCP_PROJECT peut être définie automatiquement. Consultez la liste des variables d'environnement définies automatiquement et déterminez si vous devez définir manuellement la variable GCP_PROJECT sur le projet qui exécute les machines virtuelles.

  6. Définissez le paramètre ZONE. Dans cet exemple, il s'agit de la zone dans laquelle les instances seront arrêtées.

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

Configurer les autorisations du compte de service

  1. La fonction Cloud Functions est exécutée en tant que compte de service automatiquement créé. Pour contrôler l'utilisation, vous devez accorder au compte de service des autorisations sur tous les services du projet dont il aura besoin pour apporter des modifications.
  2. Pour identifier le bon compte de service, consultez les informations de la fonction. Le compte de service est répertorié au bas de la page.
  3. Dans Cloud Console, accédez à la page IAM pour définir les autorisations appropriées.
    Accéder à la page "IAM"
     
    Affiche l'écran IAM dans Cloud Console, où vous pouvez définir les autorisations appropriées pour le compte de service qui exécute la fonction Cloud.

Vérifier que les instances sont arrêtées

Lorsque le budget envoie une notification, les machines virtuelles Compute Engine sont arrêtées.

Pour tester la fonction, publiez un exemple de message semblable à celui ci-dessus. Vous pouvez valider l'exécution réussie de la fonction en vérifiant les machines virtuelles Compute Engine dans Cloud Console.