자동화된 비용 관리 응답의 예시

참조 아키텍처 예시

예산 알림 프로그래매틱 알림을 사용하여 비용 관리 응답을 자동화하는 예시의 다이어그램입니다.
그림 1: 예산 알림을 통해 프로그래매틱 알림용 Pub/Sub와 Cloud Functions를 사용하여 비용 관리 응답을 자동화하여 응답을 자동화한 예시를 보여줍니다.

비용에 민감하고 예산을 기준으로 환경을 제어해야 할 경우, 예산 알림에 따른 비용 관리 응답을 자동화하기 위해 프로그래매틱 예산 알림을 사용할 수 있습니다.

예산 알림은 Pub/Sub 주제를 사용하여 클라우드용 엔터프라이즈 메시지 중심 미들웨어의 확장성, 유연성, 안정성을 활용하여 Cloud Billing 예산의 실시간 상태를 제공합니다.

이 페이지에는 비용 관리 자동화를 위해 Cloud Functions에 예산 알림을 사용하는 방법에 대한 예시 및 단계별 안내가 포함되어 있습니다.

예산 알림 설정

첫 번째 단계는 예산에 대한 Pub/Sub 주제를 사용 설정하는 것입니다. 자세한 내용은 프로그래매틱 예산 알림 관리를 참조하세요.

예산 알림을 사용 설정한 후에는 다음을 기록해 두세요.

  • Pub/Sub 주제: 예산에 대해 구성된 알림 엔드포인트입니다.
  • 예산 ID: 모든 알림에 포함된 예산의 고유 ID입니다. 알림 관리에서 예산의 예산 ID를 찾을 수 있습니다 이 ID는 Pub/Sub 주제를 이 예산에 연결을 선택한 후에 표시됩니다.

Pub/Sub 주제를 예산에 연결할 수 있는 Google Cloud Console의 알림 관리 섹션입니다. 여기에는 예산 ID, 프로젝트 이름, Pub/Sub 주제가 포함됩니다.

알림 수신 대기

다음 단계는 Pub/Sub 주제를 구독하여 알림을 수신 대기하는 것입니다. 구독자가 없는 경우, Pub/Sub이 게시된 메시지를 삭제하여, 개발자가 나중에 이를 검색할 수 없습니다.

주제 구독 방법에는 여러 가지가 있지만 이 예시에서는 Cloud 함수 트리거를 사용합니다.

Cloud 함수 만들기

새 Cloud 함수를 만들려면 다음 안내를 따르세요.

  1. Cloud Console에서 Cloud Functions 페이지로 이동합니다.

    Cloud Functions 페이지로 이동

  2. 함수 만들기를 클릭하고 해당 예산에 적합한 이름을 함수에 지정합니다.

  3. 트리거에서 Pub/Sub 주제를 선택합니다.

  4. 예산에 구성한 주제를 선택합니다.

  5. 함수를 실행하기 위한 소스 코드 및 종속성을 제공합니다.

  6. 실행할 함수가 올바른 함수 이름으로 설정되었는지 확인합니다.

Cloud Console에서 Cloud Functions 섹션의 함수 만들기 페이지입니다. 여기에는 함수 이름, 할당된 메모리 양, 트리거 유형, 예산에 구성한 Pub/Sub 주제가 포함됩니다.

Cloud 함수 설명

알림으로 수행하려는 작업을 Cloud 함수에 지정하기 위해서는 인라인 편집기를 사용하여 코드를 작성하거나 파일을 업로드할 수 있습니다. 코드가 수신할 알림에 대한 자세한 내용은 알림 형식을 참조하세요.

예를 들어 함수를 활용해 예산 알림으로 트리거될 때 수신된 Pub/Sub 알림, 속성, 데이터를 로깅할 수 있습니다. 자세한 내용은 Pub/Sub 트리거를 참조하세요.

Cloud 함수 이벤트 보기

Cloud 함수를 저장한 후에는 로그 보기를 클릭하여 로깅된 예산 알림을 볼 수 있습니다. 그러면 함수 호출 로그가 표시됩니다.

화면에서 로그 보기를 찾고 Cloud Console에서 Cloud 함수 이벤트 목록을 찾을 수 있는 위치를 표시합니다.

Cloud 함수 테스트

알림이 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 함수가 봇을 사용해서 봇 작업공간의 Slack 채널에 메시지를 게시합니다.

Slack 채널 및 권한 설정

첫 번째 단계는 Slack API 호출에 사용되는 Slack 작업공간 및 봇 사용자 토큰을 만드는 것입니다. https://api.slack.com/apps에서 API 토큰을 관리할 수 있습니다. 자세한 내용은 Slack 사이트에서 봇 사용자를 참조하세요.

Slack 알림 구성

Cloud 함수 작성

  1. Cloud 함수 만들기의 단계에 따라 새 함수를 만듭니다. 예산에 사용하도록 설정된 것과 동일한 Pub/Sub 주제로 트리거가 설정되었는지 확인합니다.

  2. 종속 항목을 추가합니다.

    Node.js

    slack npm 패키지에 대한 종속 항목을 함수의 package.json 파일에 추가합니다.
    {
      "name": "cloud-functions-billing",
      "version": "0.0.1",
      "dependencies": {
        "slack": "^11.0.1"
      }
    }
    

    Python

    slackclient==2.7.2을 함수의 requirements.txt 파일에 추가합니다.
    slackclient==2.7.2

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

이제 Cloud 함수를 테스트하여 Slack에 표시된 메시지를 확인할 수 있습니다.

사용 중지를 위한 결제 상한 설정(사용 중지)

이 예시에서는 비용 상한을 설정하는 방법을 보여주고 Cloud Billing을 사용 중지하여 프로젝트 사용을 중지합니다. 그러면 모든 Google Cloud 서비스가 프로젝트에서 무료 등급이 아닌 서비스를 종료합니다.

Google Cloud에 지출할 수 있는 금액이 제한적이어서 비용 상한을 설정할 수 있습니다. 이러한 경우는 학생, 연구원 또는 샌드박스 환경에서 작업하는 개발자에게서 일반적입니다. 이러한 경우 예산 한도에 도달할 때 지출을 멈추고 모든 Google Cloud 서비스 및 사용을 중지해야 할 수 있습니다.

이 예시에서는 Cloud Billing을 안전하게 사용 중지하기 위한 비프로덕션 프로젝트로 acme-backend-dev를 사용합니다.

Cloud Console에서 예산 상한을 구성합니다.

이 예시 작업을 시작하기 전에 다음을 수행했는지 확인합니다.

Cloud Console에서 Cloud Billing 알림 목록을 표시합니다.

Cloud 함수 작성

그런 후 Cloud Billing API를 호출하도록 Cloud 함수를 구성해야 합니다. 그러면 Cloud 함수가 예시 프로젝트인 acme-backend-dev에 대해 Cloud Billing을 사용 중지할 수 있습니다.

  1. Cloud 함수 만들기의 단계에 따라 새 함수를 만듭니다. 예산에 사용하도록 설정된 것과 동일한 Pub/Sub 주제로 트리거가 설정되었는지 확인합니다.

  2. 다음 종속 항목을 추가합니다.

    Node.js

    googleapisgoogle-auth-library의 종속 항목을 함수의 package.json 파일에 추가합니다.
    {
     "name": "cloud-functions-billing",
     "version": "0.0.1",
     "dependencies": {
       "google-auth-library": "^2.0.0",
       "googleapis": "^52.0.0"
     }
    }

    Python

    google-api-python-client==1.9.3을 함수의 requirements.txt 파일에 추가합니다.
    google-api-python-client==1.9.3
    

  3. 아래 코드를 Cloud 함수에 복사합니다.

  4. 실행할 함수를 'stopBilling'(노드) 또는 'stop_billing'(Python)으로 설정합니다.

  5. 런타임에 따라 GCP_PROJECT 환경 변수가 자동으로 설정될 수 있습니다. 자동으로 설정된 환경 변수 목록을 검토하고 Cloud Billing을 제한(사용 중지)하려는 프로젝트로 GCP_PROJECT 변수를 수동으로 설정해야 하는지 여부를 확인합니다.

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

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

서비스 계정 권한 구성

Cloud 함수는 자동으로 생성된 서비스 계정으로 실행됩니다. 서비스 계정이 결제를 사용 중지할 수 있도록 결제 관리자와 같은 올바른 권한을 부여해야 합니다.

올바른 서비스 계정을 식별하기 위해서는 Cloud 함수 세부정보를 확인합니다. 서비스 계정은 페이지 아래에 나열됩니다.

Cloud Console의 Cloud 함수 섹션에서 서비스 계정 정보를 찾을 수 있는 위치를 보여줍니다.

Cloud Console의 결제 페이지에서 결제 관리자 권한을 관리할 수 있습니다.

서비스 계정에 결제 계정 관리자 권한을 부여하려면 서비스 계정 이름을 선택합니다.

Cloud Console의 권한 섹션에서 서비스 계정 이름 및 결제 계정 관리자 역할을 선택할 수 있는 위치를 보여줍니다.

Cloud Billing이 사용 중지되었는지 확인합니다.

예산이 알림을 전송하면 지정된 프로젝트에 더 이상 Cloud Billing 계정이 포함되지 않습니다.다. 함수를 테스트하려면 위의 테스트 메시지를 사용하여 샘플 메시지를 게시합니다. 프로젝트가 더 이상 Cloud Billing 계정에 표시되지 않으며, 동일한 프로젝트에 있는 경우 Cloud 함수를 포함하여 프로젝트의 리소스가 사용 중지됩니다.

예시 프로젝트가 Cloud Billing 계정에 연결된 프로젝트 목록에 더 이상 표시되지 않음을 보여줍니다. 이것으로 프로젝트에 대해 Cloud Billing이 사용 중지되었는지 확인할 수 있습니다.

Cloud Console에서 프로젝트에 대해 Cloud Billing을 수동으로 다시 사용 설정할 수 있습니다.

선택적으로 사용량 제어

이전 예시에 설명된 대로 Cloud Billing 상한 설정(사용 중지)의 결과는 두 가지뿐이고 돌이킬 수 없습니다. 즉 프로젝트는 사용 설정되거나 사용 중지됩니다. 사용 중지된 경우에는 모든 서비스가 중지되고 모든 리소스가 결과적으로 삭제됩니다.

보다 세밀한 대응이 필요한 경우에는 선택적으로 리소스를 제어할 수 있습니다. 예를 들어 일부 Compute Engine 리소스를 중지하고 Cloud Storage를 그대로 두려면 선택적으로 사용량을 제어할 수 있습니다. 이렇게 하면 환경을 완전히 사용 중지하지 않아도 시간당 비용을 줄일 수 있습니다.

원하는 대로 정책을 세밀하게 작성할 수 있습니다. 하지만 이 예시에서 프로젝트는 많은 수의 Compute Engine 가상 머신이 포함된 연구를 실행 중이며, Cloud Storage에 결과를 저장합니다. 이 Cloud 함수 예시는 모든 Compute Engine 인스턴스를 종료하지만, 예산이 초과된 후에도 저장된 결과에 영향을 주지 않습니다.

Cloud 함수 작성

  1. Cloud 함수 만들기의 단계에 따라 새 함수를 만듭니다. 예산에 사용하도록 설정된 것과 동일한 Pub/Sub 주제로 트리거가 설정되었는지 확인합니다.

  2. 사용량 중지를 위한 결제 상한 설정(사용 중지)에 설명된 종속 항목을 추가했는지 확인합니다.

  3. 아래 코드를 Cloud 함수에 복사합니다.

  4. 실행할 함수를 'limitUse'(노드) 또는 'limit_use'(Python)로 설정합니다.

  5. 런타임에 따라 GCP_PROJECT 환경 변수가 자동으로 설정될 수 있습니다. 자동으로 설정된 환경 변수 목록을 검토하고 가상 머신을 실행하는 프로젝트에 GCP_PROJECT 변수를 수동으로 설정해야 하는지 여부를 확인합니다.

  6. ZONE 매개변수를 설정합니다. 이 샘플에 대해 인스턴스가 중지되는 영역입니다.

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

서비스 계정 권한 구성

  1. Cloud 함수는 자동으로 생성된 서비스 계정으로 실행됩니다. 사용량 제어를 위해서는 프로젝트에서 변경이 필요한 모든 서비스에 서비스 계정 권한을 부여해야 합니다.
  2. 올바른 서비스 계정을 식별하기 위해 Cloud 함수의 세부정보를 확인합니다. 서비스 계정은 페이지 아래에 나열됩니다.
  3. Cloud Console에서 IAM 페이지로 이동하여 적절한 권한을 설정합니다.
    IAM 페이지로 이동
     
    Cloud 함수를 실행하는 서비스 계정에 대해 적절한 권한을 설정하는 Cloud Console의 IAM 화면을 보여줍니다.

인스턴스가 중지되었는지 확인

예산이 알림을 전송하면 Compute Engine 가상 머신이 중지됩니다.

함수를 테스트하려면 위의 테스트 메시지를 사용하여 샘플 메시지를 게시합니다. 함수가 성공적으로 실행되었는지 확인하려면 Cloud Console에서 Compute Engine 가상 머신을 확인합니다.