프로그래매틱 방식의 예산 알림 예제

비용에 민감하고 예산을 기준으로 환경을 제어해야 할 경우, 예산 작업을 자동화하기 위해 프로그래매틱 방식의 예산 알림을 사용할 수 있습니다.

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

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

예산 알림 설정

첫 번째 단계는 예산에 대한 Cloud Pub/Sub 알림을 사용 설정하는 것입니다. 이 단계는 알림 관리에서 설명합니다.

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

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

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 API 호출에 사용되는 Slack 작업공간 및 봇 사용자 토큰을 만드는 것입니다. https://api.slack.com/apps에서 API 토큰을 관리할 수 있습니다. 자세한 내용은 Slack 사이트에서 봇 사용자를 참조하세요.

Slack 알림 구성

Cloud Function 작성

  1. Cloud Function 만들기의 단계에 따라 새 함수를 만듭니다.

  2. 종속성을 추가합니다.

    Node.js 8(베타)

    slack npm 패키지에 대한 종속 항목을 함수의 package.json 파일에 추가합니다.
    {
      "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 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(베타)

    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. 결제 계정에서 삭제하여 프로젝트에서 결제를 사용 중지하도록 코드를 작성합니다.

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

결제 사용 중지 확인

Compute 가상 머신이 GCP Console에서 중지되었는지 확인하여 함수가 성공적으로 실행되었는지 확인할 수 있습니다.

이 페이지가 도움이 되었나요? 평가를 부탁드립니다.