コスト管理の自動レスポンスの例

リファレンス アーキテクチャの例

予算アラートに関するプログラムによる通知を使用して費用管理レスポンスを自動化する例を示した図
図 1: 予算アラートを使用して、プログラムによる通知に Pub/Sub を使用して費用管理レスポンスを自動化し、Cloud Functions でレスポンスを自動化する例を示す図。

費用を重視し、予算に関連して環境を制御する必要がある場合は、プログラムによる予算通知を使用して、予算通知に基づいて費用管理レスポンスを自動化できます。

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

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

予算通知を設定する

最初のステップとして、予算の Pub/Sub トピックを有効にします。詳細については、プログラムによる予算アラート通知を管理するをご覧ください。

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

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

Google Cloud Console の [通知の管理] セクションでは、Pub/Sub サブトピックを予算に関連付けることができます。ここには、予算 ID、プロジェクト名、Pub/Sub トピックが含まれます。

通知をリッスンする

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

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

Cloud Function を作成

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

  1. Cloud Console で、Cloud Functions のページに移動します。

    Cloud Functions ページに移動

  2. [ 関数を作成] をクリックして、予算に対して意味を持つ名前を付けます。

  3. [トリガー] で、[Cloud Pub/Sub] を選択します。

  4. 予算に構成したトピックを選択します。

  5. 関数を実行するためのソースコードと依存関係を指定します。

  6. [実行する関数] を正しい関数名に設定します。

Cloud Console の Cloud Functions セクションにある [関数の作成] ページ。関数名、割り当てられたメモリ量、トリガーのタイプ、予算で構成した Pub/Sub トピックが含まれます。

Cloud Function を記述する

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

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

Cloud Function イベントを表示する

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

画面上でログを表示し、Cloud Console の Cloud Function イベントを確認できます。

Cloud Function をテストする

通知が Pub/Sub に送信され、登録者がメッセージを受信します。サンプル通知をテストして、関数が意図したとおりに機能していることを確認するには、このオブジェクトをメッセージ本文として Pub/Sub で次のようなメッセージを発行します。

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

請求先アカウント ID などのメッセージ属性を追加することもできます。詳細については、通知形式のドキュメントの全文をご覧ください。

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

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

    Python

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

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

  4. 次の Slack API postMessage パラメータが正しく設定されていることを確認します。

    • ボットユーザー OAuth アクセス トークン
    • チャネルの名前

コードの例:

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)

これで、Cloud Function をテストして、Slack にメッセージが表示されることを確認できるようになりました。

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

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

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

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

Cloud Console で予算上限を構成します。

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

Cloud Console に Cloud Billing アラートのリストが表示されます。

Cloud Function を記述する

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

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

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

    Node.js

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

    Python

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

  3. 以下のコードを Cloud Function にコピーします。

  4. 実行する関数を stopBilling(Node)または stop_billing(Python)に設定します。

  5. Cloud Billing に上限を設定する(無効にする)プロジェクトに PROJECT_NAME パラメータを設定します。

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

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

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

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

サービス アカウントの情報が Cloud Console の Cloud Function セクションに表示されています。

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

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

Cloud Console の [権限] セクションで、サービス アカウント名と請求アカウント管理者のロールを選択できます。

また、このサンプルコードを実行するには、Cloud Billing API を有効にする必要があります。

Cloud Billing が無効であることを確認する

予算が通知を送信すると、プロジェクトから Cloud 請求先アカウントが削除されます。この関数のテストを行うには、上記のテスト メッセージと一緒にサンプル メッセージをパブリッシュします。Cloud 請求先アカウントの下にプロジェクトが表示されなくなり、プロジェクトのリソースが無効になります。同じプロジェクトの場合、この関数も無効になります。

Cloud 請求先アカウントにリンクされているプロジェクトのリストに、サンプル プロジェクトが表示されていません。これにより、プロジェクトで Cloud Billing が無効になっていることを確認できます。

Cloud Console で、プロジェクトの Cloud Billing を手動で有効に戻すことができます

使用を選択的に制御する

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

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

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

Cloud Function を記述する

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

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

  3. 以下のコードを Cloud Function にコピーします。

  4. 実行する関数を limitUse(Node)または limit_use(Python)に設定します。

  5. ZONE パラメータを設定します。これは、このサンプルでインスタンスが停止されるゾーンです。

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

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

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

    Cloud Console の Cloud IAM 画面。Cloud Function を実行しているサービス アカウントに適切な権限を設定できます。

インスタンスが停止していることを検証する

予算が通知を送信すると、Compute Engine 仮想マシンは停止します。

この関数のテストを行うには、上記のテスト メッセージでサンプル メッセージをパブリッシュします。関数が正常に実行されたことを確認するには、Cloud Console で Compute Engine 仮想マシンを確認します。