🤮

やっちまった。。GCPでプロジェクトをまたいで請求を予算内に抑える方法

2022/06/04に公開

クラウド破産という言葉は他人事だと思っていましたが、やってしまいました。

数万円の請求額

k8sの独学コースを始めたのを失念しており、先月のGCP請求額が数万円になってしまいました。

Budget/Alert機能で通知メールは飛ばしていたのですが、Inboxの海に埋もれていて、実際の請求書が月初にくるまで気づくことができませんでした。これはいかん。

めちゃくちゃ悔しいので、とりあえず自動でクレジットカードをプロジェクトから外すCloud Functionを書いてデプロイしました。その手順をここに記します。

すでに日本語で全く同じ情報あるよ、という場合にはごめんなさい。コメント欄でご指摘ください。

自動でGCPの請求を止める

ほぼ、https://github.com/aioverlords/Google-Cloud-Platform-Killswitch のレポジトリの手順に沿っただけです。ありがとう Tim さん。

ただ、Timさんのスクリプトだとプロジェクト単位でしか請求を止めることができませんでした。 ちょっとだけ拡張して、ここでは請求アカウント(要はクレカ)単位で、止められるようにします。

必要な構成要素は三つです。

  1. Budgets & Alerts
  2. Pub/Sub
  3. Cloud Functions

1. Budgets & Alertsから、使用割合の通知をするメッセージを作成

Budgets & Alerts 設定の下部に "Coonect a Pub/Sub topic to this budget" というものがあるので、通知の閾値を設定したあとは、忘れずにそちらをチェック。

2. Cloud Functionsで、通知メッセージを請求APIとつなげる

Triggerに "Retry on failure" というチェックボックスがあります。正常に動くことが確認できたらこちらをオンにして、一時的な失敗の際に請求が止まらない、といったことがないようにしておきます。

3. Cloud Functionsの実行アカウントに権限追加

Cloud Billing AdminかCloud Billing Viewerの権限が今回の拡張で必要になったのですが、通常のIAMは「プロジェクト単位」なので、見つかりません。

Billingの画面にいって、右側にある"ADD PRINCIPLE"というところから、Cloud Functionsが使っているアカウントに必要な権限を追加してあげます。

Cloud Functionsの実行アカウントは上記画面からチェックできます。

CLIからならこんなハマり方はしないのかもしれませんが、ここで20分くらい使いました..

4. あとは、Cloud FunctionsでJS書くだけ

# Node.js 16

const {
    google
} = require('googleapis');

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

const BILLING_ACCOUNT_ID = 'PUT-YOUR-BILLING-ACCOUNT-ID';

const billingAccounts = google.cloudbilling('v1').billingAccounts.projects;
const billing = google.cloudbilling('v1').projects;

exports.stopBillingForProjects = async pubsubEvent => {
    console.log(pubsubEvent.data);

    const pubsubData = JSON.parse(
        Buffer.from(pubsubEvent.data, 'base64').toString()
    );

    console.log(pubsubData);
    console.log(pubsubData.costAmount);
    console.log(pubsubData.budgetAmount);

    if (pubsubData.costAmount <= pubsubData.budgetAmount) {
        console.log("アクションは不要です。");
        return `アクションは不要です。 (現在: ${pubsubData.costAmount})`;
    }

    _setAuthCredential();

    const res = await billingAccounts.list({
        name: 'billingAccounts/' + BILLING_ACCOUNT_ID,
    });

    for (let projBillingInfo of res.data.projectBillingInfo) {
        console.log(projBillingInfo);

        let projectId = projBillingInfo.projectId;
        let projectName = "projects/" + projectId;

        const billingEnabled = await _isBillingEnabled(projectName);

        if (billingEnabled) {
            console.log("請求を止めます。プロジェクト: " + projectId);
            return _disableBillingForProject(projectName);
        } else {
            console.log("請求はすでに止められています。プロジェクト: " + projectId);
            return '請求はすでに止められています。';
        }

    }

    return res.data.projectBillingInfo;
};

/**
 * @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
        });
        console.log(res);
        return res.data.billingEnabled;
    } catch (e) {
        console.log(
            '請求情報取得時のエラー。まだ請求されているものとして進めます。'
        );
        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
    });
    console.log(res);
    console.log("請求を止めました。");
    return `請求を止めました。 ${JSON.stringify(res.data)}`;
};

これで、安心して眠ることができます

おやすみなさい!

Discussion