プログラムによる予算通知の例

費用を抑え、予算に応じて環境を管理する必要がある場合は、プログラムによる予算通知を使用して予算操作を自動化できます。

予算通知では、クラウドのエンタープライズ メッセージ指向ミドルウェアが持つスケーラビリティ、柔軟性、信頼性を活用して、Cloud Pub/Sub 通知によってリアルタイムの予算ステータスを提供します。

このページでは、Cloud Functions で予算通知を使用して費用管理を自動化する方法の例と詳しい手順を示します。

予算通知を設定する

最初のステップとして、予算に対する Cloud Pub/Sub 通知を有効にします。この手順については、通知の管理をご覧ください。

予算通知を有効にしたら、次の点に注意してください。

  • Pub/Sub トピック - これは、予算用に構成された通知エンドポイントです。
  • 予算 ID - すべての通知に含まれる予算の一意な ID です。予算の ID は [通知の管理] に表示されます。[Pub/Sub トピックをこの予算に接続する] を選択すると、ID が表示されます。

GCP Console で請求通知を管理する

通知をリッスンする

次に、Pub/Sub トピックに登録して通知をリッスンします。サブスクライバーがない場合、公開されたメッセージは Pub/Sub で削除され、後で取得することはできません。

トピックに登録するにはさまざまな方法がありますが、この例では Cloud Function のトリガーを使用します。

Cloud Function を作成する

新しい Cloud Function を作成するには:

  1. Google Cloud Platform Console の Cloud Functions ページに移動します。新しい関数を作成し、予算にとって意味のある名前を付けます。
  2. [トリガー] で、[Cloud Pub/Sub トピック] を選択します。
  3. 予算に構成したトピックを選択します。
  4. 関数を実行するためのソースコードと依存関係を指定します。

GCP Console で新しい関数を作成する

Cloud Function を記述する

通知で実行することを Cloud Function に指示するには、インライン エディタを使用してコードを記述するか、ファイルをアップロードします。コードが受け取る通知の詳細については、通知形式をご覧ください。

たとえば、関数は予算通知によってトリガーされると、受信した Pub/Sub 通知、属性、データをログに記録します。詳細については、Google Cloud Pub/Sub トリガーご覧ください。

Cloud Function イベントを表示する

Cloud Function を保存した後、[ログを表示] をクリックすると、ログに記録された予算通知を表示できます。これには、関数呼び出しからのログが表示されます。

GCP Console で Cloud Function イベントを表示する

Slack に通知を送信する

特に予算が重要で時間的制約がある場合、必ずしもメールがクラウド費用の状況を把握する最適な方法とは限りません。通知では、予算メッセージを他のメディアに転送できます。

この例では、予算通知を Slack に転送する方法を説明します。この方法では、Cloud Billing で予算通知が発行されるたびに、Cloud Function がボットを使用してボットのワークスペースの Slack チャネルにメッセージを投稿します。

Slack チャネルと権限を設定する

最初に、Slack ワークスペースと、Slack API を呼び出すためのボットユーザー トークンを作成します。API トークンは、https://api.slack.com/apps で管理できます。詳細については、Slack サイトの Bot Users をご覧ください。

Slack 通知を構成する

Cloud Function を記述する

  1. Cloud Function を作成するの手順に従って、新しい関数を作成します。

  2. 依存関係を追加します。

    Node.js 8(ベータ版)

    関数の package.json ファイルに slack npm package への依存関係を追加します。
    {
      "name": "cloud-functions-billing",
      "version": "0.0.1",
      "dependencies": {
        "slack": "^11.0.1"
      }
    }
    

    Python(ベータ版)

    関数の requirements.txt ファイルに slackclient==1.3.0 を追加します。
    slackclient==1.3.0

  3. Slack API を使用して Slack チャット チャネルに予算通知を投稿するコードを記述します。

  4. コードに次の Slack API postMessage パラメータを設定します。

    • ボットユーザー OAuth アクセス トークン
    • チャネル
    • テキスト

例:

Node.js 8(ベータ版)

const slack = require('slack');

const BOT_ACCESS_TOKEN = 'xxxx-111111111111-abcdefghidklmnopq';
const CHANNEL = 'general';

exports.notifySlack = async (data, context) => {
  const pubsubMessage = data;
  const pubsubAttrs = JSON.stringify(pubsubMessage.attributes);
  const pubsubData = Buffer.from(pubsubMessage.data, 'base64').toString();
  const budgetNotificationText = `${pubsubAttrs}, ${pubsubData}`;

  const res = await slack.chat.postMessage({
    token: BOT_ACCESS_TOKEN,
    channel: CHANNEL,
    text: budgetNotificationText,
  });
  console.log(res);
};

Python(ベータ版)

from slackclient import SlackClient

# See https://api.slack.com/docs/token-types#bot for more info
BOT_ACCESS_TOKEN = 'xxxx-111111111111-abcdefghidklmnopq'

CHANNEL_ID = 'C0XXXXXX'

slack_client = SlackClient(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)

課金に上限を設定(課金を無効化)して使用を停止する

この例では、費用に上限を設定し、課金を無効にすることで Google Cloud プロジェクトの使用を停止します。これにより、すべての Google Cloud サービスでプロジェクトの有料枠サービスが終了します。

Google Cloud Platform に使える費用に厳しい制限があるため、費用に上限を設定したい場合があります。これは、サンドボックス環境で作業する学生、研究者、開発者にとってよくあることです。このような場合、支出を停止し、予算の上限に達したら Google Cloud Platform のすべてのサービスと使用を停止したいと考えるかもしれません。

この例では、課金を安全に無効にできる非本番環境プロジェクトとして acme-backend-dev を使用します。

GCP Console で予算上限を構成する

開始する前に、予算を設定してプロジェクト費用をモニタリングし、予算通知を有効にする必要があります。

GCP Console で請求アラートを管理する

サービス アカウント権限を構成する

Cloud Function は自動的に作成されたサービス アカウントとして実行されます。サービス アカウントで課金を無効にできるようにするには、課金管理者の権限を付与する必要があります。

正しいサービス アカウントを特定するには、Cloud Function の詳細を表示します。サービス アカウントはページの下部に表示されます。

GCP Console でサービス アカウントを特定する

課金管理者の権限は、Google Cloud Platform Console の [お支払い] ページで管理できます。

サービス アカウントに請求先アカウント管理者の権限を付与するには、サービス アカウント名を選択します。

GCP Console で権限を管理する

Cloud Function を記述する

次に、Cloud Billing API を呼び出すように Cloud Function を構成する必要があります。これにより、Cloud Function でサンプル プロジェクト acme-backend-dev の課金を無効にできます。

  1. Cloud Function を作成するの手順で新しい関数を作成します。

  2. 次の依存関係を追加します。

    Node.js 8(ベータ版)

    関数の package.json ファイルに googleapisgoogle-auth-library への依存関係を追加します。
    {
     "name": "cloud-functions-billing",
     "version": "0.0.1",
     "dependencies": {
        "google-auth-library": "^2.0.0",
        "googleapis": "^33.0.0"
     }
    }

    Python(ベータ版)

    関数の requirements.txt ファイルに oauth2client==4.1.3google-api-python-client==1.7.4 を追加します。
    oauth2client==4.1.3
    google-api-python-client==1.7.4
    

  3. 請求先アカウントからプロジェクトを削除することで、プロジェクトに対する課金を無効にするコードを記述します。

  4. PROJECT_NAME パラメータを設定します。これは、課金に上限を設定する(課金を無効にする)プロジェクトです。

Node.js 8(ベータ版)

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

const PROJECT_ID = process.env.GCP_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
const compute = google.compute('v1');
const ZONE = 'us-west1-b';

exports.limitUse = async (data, context) => {
  const pubsubData = JSON.parse(Buffer.from(data.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);
  await _stopInstances(PROJECT_ID, ZONE, instanceNames);
};

/**
 * @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) => {
  if (!instanceNames.length) {
    return 'No running instances were found.';
  }
  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
from oauth2client.client import GoogleCredentials

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,
        credentials=GoogleCredentials.get_application_default()
    )

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

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,
        credentials=GoogleCredentials.get_application_default()
    )
    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()

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

課金が無効になったことを検証する

この関数がトリガーされたら、Cloud Function が成功したことを検証できます。プロジェクトは請求先アカウントに表示されなくなり、プロジェクト内のリソースは無効になります。

課金が無効になったことを検証する

GCP Console でプロジェクトの課金を手動で再度有効にできます。

使用を選択的に制御する

前の例で説明した上限の設定(課金の無効化)は二分法的かつ極端なものです。プロジェクトは有効または無効のいずれかです。無効にすると、すべてのサービスが停止し、すべてのリソースは最終的に削除されます。

よりきめ細かなレスポンスが必要な場合は、リソースを選択的に制御できます。たとえば一部のコンピューティング リソースを停止し、ストレージをそのままにする場合は、使用を選択的に制御できます。これにより、環境を完全に無効にすることなく、時間あたりのコストを削減できます。

きめ細かなポリシーを自由に記述できます。ただし、この例のプロジェクトでは、いくつかのコンピューティング仮想マシンを使って研究を進め、結果を Cloud Storage に保存しています。この Cloud Function の例では、すべてのコンピューティング インスタンスがシャットダウンされますが、予算を超えても保存された結果には影響しません。

サービス アカウント権限を構成する

  1. Cloud Function は自動的に作成されたサービス アカウントとして実行されます。使用を制御するには、変更が必要なプロジェクトのサービスにサービス アカウントの権限を付与する必要があります。
  2. 正しいサービス アカウントを特定するには、Cloud Function の詳細を表示します。サービス アカウントはページの下部に表示されます。
  3. GCP Console の IAM ページに移動して、適切な権限を設定します。
     
    GCP Console で請求アラートを管理する

Cloud Function を記述する

  1. Cloud Function を作成するの手順で新しい関数を作成します。

  2. 課金に上限を設定(課金を無効化)して使用を停止するの依存関係が追加されていることを確認します。

  3. プロジェクト内のリソースをシャットダウンするコードを記述します。

  4. PROJECT_NAME パラメータを設定します。これは上限を構成するプロジェクトです。

Node.js 8(ベータ版)

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 (data, context) => {
  const pubsubData = JSON.parse(Buffer.from(data.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
from googleapiclient import discovery
from oauth2client.client import GoogleCredentials

PROJECT_ID = os.getenv('GCP_PROJECT')
PROJECT_NAME = f'projects/{PROJECT_ID}'

課金が無効になったことを検証する

この関数が正常に実行されたことを検証するには、コンピューティング仮想マシンが停止したことを GCP Console で確認します。

このページは役立ちましたか?評価をお願いいたします。