์ž๋™ํ™”๋œ ๋น„์šฉ ๊ด€๋ฆฌ ์‘๋‹ต์˜ ์˜ˆ์‹œ

์ฐธ์กฐ ์•„ํ‚คํ…์ฒ˜ ์˜ˆ์‹œ

์˜ˆ์‚ฐ ์•Œ๋ฆผ ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ์•Œ๋ฆผ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋น„์šฉ ๊ด€๋ฆฌ ์‘๋‹ต์„ ์ž๋™ํ™”ํ•˜๋Š” ์˜ˆ์‹œ์˜ ๋‹ค์ด์–ด๊ทธ๋žจ์ž…๋‹ˆ๋‹ค.
๊ทธ๋ฆผ 1: ์˜ˆ์‚ฐ ์•Œ๋ฆผ์„ ํ†ตํ•ด ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ์•Œ๋ฆผ์šฉ Pub/Sub์™€ Cloud Run ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋น„์šฉ ๊ด€๋ฆฌ ์‘๋‹ต์„ ์ž๋™ํ™”ํ•˜์—ฌ ์‘๋‹ต์„ ์ž๋™ํ™”ํ•œ ์˜ˆ์‹œ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

๋น„์šฉ์— ๋ฏผ๊ฐํ•˜๊ณ  ์˜ˆ์‚ฐ์„ ๊ธฐ์ค€์œผ๋กœ ํ™˜๊ฒฝ์„ ์ œ์–ดํ•ด์•ผ ํ•  ๊ฒฝ์šฐ, ์˜ˆ์‚ฐ ์•Œ๋ฆผ์— ๋”ฐ๋ฅธ ๋น„์šฉ ๊ด€๋ฆฌ ์‘๋‹ต์„ ์ž๋™ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ์˜ˆ์‚ฐ ์•Œ๋ฆผ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‚ฐ ์•Œ๋ฆผ์€ Pub/Sub ์ฃผ์ œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํด๋ผ์šฐ๋“œ์šฉ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๋ฉ”์‹œ์ง€ ์ค‘์‹ฌ ๋ฏธ๋“ค์›จ์–ด์˜ ํ™•์žฅ์„ฑ, ์œ ์—ฐ์„ฑ, ์•ˆ์ •์„ฑ์„ ํ™œ์šฉํ•˜์—ฌ Cloud Billing ์˜ˆ์‚ฐ์˜ ์‹ค์‹œ๊ฐ„ ์ƒํƒœ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์ด ๋ฌธ์„œ์—๋Š” ๋น„์šฉ ๊ด€๋ฆฌ ์ž๋™ํ™”๋ฅผ ์œ„ํ•ด Cloud Run ํ•จ์ˆ˜์— ์˜ˆ์‚ฐ ์•Œ๋ฆผ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๊ด€ํ•œ ์˜ˆ์‹œ์™€ ๋‹จ๊ณ„๋ณ„ ์•ˆ๋‚ด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‚ฐ ์•Œ๋ฆผ ์„ค์ •

์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” ์˜ˆ์‚ฐ์— ๋Œ€ํ•œ Pub/Sub ์ฃผ์ œ๋ฅผ ์‚ฌ์šฉ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ์˜ˆ์‚ฐ ์•Œ๋ฆผ ๊ด€๋ฆฌ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

์˜ˆ์‚ฐ ์•Œ๋ฆผ์„ ์‚ฌ์šฉ ์„ค์ •ํ•œ ํ›„์—๋Š” ๋‹ค์Œ์„ ๊ธฐ๋กํ•ด ๋‘์„ธ์š”.

  • Pub/Sub ์ฃผ์ œ: ์˜ˆ์‚ฐ์— ๋Œ€ํ•ด ๊ตฌ์„ฑ๋œ ์•Œ๋ฆผ ์—”๋“œํฌ์ธํŠธ์ž…๋‹ˆ๋‹ค.
  • ์˜ˆ์‚ฐ ID: ๋ชจ๋“  ์•Œ๋ฆผ์— ํฌํ•จ๋œ ์˜ˆ์‚ฐ์˜ ๊ณ ์œ  ID์ž…๋‹ˆ๋‹ค. ์•Œ๋ฆผ ๊ด€๋ฆฌ์—์„œ ์˜ˆ์‚ฐ์˜ ์˜ˆ์‚ฐ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค ์ด ID๋Š” Pub/Sub ์ฃผ์ œ๋ฅผ ์ด ์˜ˆ์‚ฐ์— ์—ฐ๊ฒฐ์„ ์„ ํƒํ•œ ํ›„์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

Pub/Sub ์ฃผ์ œ๋ฅผ ์˜ˆ์‚ฐ์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” Google Cloud ์ฝ˜์†”์˜ ์•Œ๋ฆผ ๊ด€๋ฆฌ ์„น์…˜์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ์˜ˆ์‚ฐ ID, ํ”„๋กœ์ ํŠธ ์ด๋ฆ„, Pub/Sub ์ฃผ์ œ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

์•Œ๋ฆผ ์ˆ˜์‹  ๋Œ€๊ธฐ

๋‹ค์Œ ๋‹จ๊ณ„๋Š” Pub/Sub ์ฃผ์ œ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์•Œ๋ฆผ์„ ์ˆ˜์‹  ๋Œ€๊ธฐํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ตฌ๋…์ž๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, Pub/Sub์ด ๊ฒŒ์‹œ๋œ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ญ์ œํ•˜์—ฌ, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋‚˜์ค‘์— ์ด๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์ฃผ์ œ ๊ตฌ๋… ๋ฐฉ๋ฒ•์—๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€๊ฐ€ ์žˆ์ง€๋งŒ ์ด ์˜ˆ์‹œ์—์„œ๋Š” Cloud Run ํ•จ์ˆ˜ ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Cloud Run ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ

์ƒˆ Cloud Run ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค๋ ค๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์„ธ์š”.

  1. Google Cloud ์ฝ˜์†”์—์„œ Cloud Run ํ•จ์ˆ˜ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

    Cloud Run ํ•จ์ˆ˜ ํŽ˜์ด์ง€๋กœ ์ด๋™

  2. ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ๋ฅผ ํด๋ฆญํ•˜๊ณ  ํ•ด๋‹น ์˜ˆ์‚ฐ์— ์ ํ•ฉํ•œ ์ด๋ฆ„์„ ํ•จ์ˆ˜์— ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

  3. ํŠธ๋ฆฌ๊ฑฐ์—์„œ Pub/Sub ์ฃผ์ œ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

  4. ์˜ˆ์‚ฐ์— ๊ตฌ์„ฑํ•œ ์ฃผ์ œ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

  5. ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ์†Œ์Šค ์ฝ”๋“œ ๋ฐ ์ข…์†์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  6. ์‹คํ–‰ํ•  ํ•จ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ํ•จ์ˆ˜ ์ด๋ฆ„์œผ๋กœ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

Google Cloud ์ฝ˜์†”์—์„œ Cloud Run ์„น์…˜์˜ ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ ํŽ˜์ด์ง€ ์—ฌ๊ธฐ์—๋Š” ํ•จ์ˆ˜ ์ด๋ฆ„, ํ• ๋‹น๋œ ๋ฉ”๋ชจ๋ฆฌ ์–‘, ํŠธ๋ฆฌ๊ฑฐ ์œ ํ˜•, ์˜ˆ์‚ฐ์— ๊ตฌ์„ฑํ•œ Pub/Sub ์ฃผ์ œ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

Cloud Run ํ•จ์ˆ˜ ์„ค๋ช…

์•Œ๋ฆผ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋ ค๋Š” ์ž‘์—…์„ Cloud Run ํ•จ์ˆ˜์— ์ง€์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ธ๋ผ์ธ ํŽธ์ง‘๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๊ฐ€ ์ˆ˜์‹ ํ•  ์•Œ๋ฆผ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•Œ๋ฆผ ํ˜•์‹์„ ์ฐธ์กฐํ•˜์„ธ์š”.

์˜ˆ๋ฅผ ๋“ค์–ด ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•ด ์˜ˆ์‚ฐ ์•Œ๋ฆผ์œผ๋กœ ํŠธ๋ฆฌ๊ฑฐ๋  ๋•Œ ์ˆ˜์‹ ๋œ Pub/Sub ์•Œ๋ฆผ, ์†์„ฑ, ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๊น…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ Pub/Sub ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

Cloud Run ํ•จ์ˆ˜ ์ด๋ฒคํŠธ ๋ณด๊ธฐ

Cloud Run ํ•จ์ˆ˜๋ฅผ ์ €์žฅํ•œ ํ›„์—๋Š” ๋กœ๊ทธ ๋ณด๊ธฐ๋ฅผ ํด๋ฆญํ•˜์—ฌ ๋กœ๊น…๋œ ์˜ˆ์‚ฐ ์•Œ๋ฆผ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋กœ๊ทธ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

ํ™”๋ฉด์—์„œ ๋กœ๊ทธ ๋ณด๊ธฐ๋ฅผ ์ฐพ๊ณ  Google Cloud ์ฝ˜์†”์—์„œ Cloud Run ํ•จ์ˆ˜ ์ด๋ฒคํŠธ ๋ชฉ๋ก์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š” ์œ„์น˜๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

Cloud Run ํ•จ์ˆ˜ ํ…Œ์ŠคํŠธ

์•Œ๋ฆผ์ด 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 Run ํ•จ์ˆ˜๊ฐ€ ๋ด‡์„ ์‚ฌ์šฉํ•ด์„œ ๋ด‡ ์ž‘์—…๊ณต๊ฐ„์˜ Slack ์ฑ„๋„์— ๋ฉ”์‹œ์ง€๋ฅผ ๊ฒŒ์‹œํ•ฉ๋‹ˆ๋‹ค.

Slack ์ฑ„๋„ ๋ฐ ๊ถŒํ•œ ์„ค์ •

์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” Slack API ํ˜ธ์ถœ์— ์‚ฌ์šฉ๋˜๋Š” Slack ์ž‘์—…๊ณต๊ฐ„ ๋ฐ ๋ด‡ ์‚ฌ์šฉ์ž ํ† ํฐ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. https://api.slack.com/apps์—์„œ API ํ† ํฐ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ Slack ์‚ฌ์ดํŠธ์—์„œ ๋ด‡ ์‚ฌ์šฉ์ž๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

Slack ์•Œ๋ฆผ ๊ตฌ์„ฑ

Cloud Run ํ•จ์ˆ˜ ์ž‘์„ฑ

  1. Cloud Run ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ์˜ ๋‹จ๊ณ„์— ๋”ฐ๋ผ ์ƒˆ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์˜ˆ์‚ฐ์— ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •๋œ ๊ฒƒ๊ณผ ๋™์ผํ•œ Pub/Sub ์ฃผ์ œ๋กœ ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

  2. ์ข…์† ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

    Node.js

    ๋‹ค์Œ์„ package.json์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.

    {
      "name": "cloud-functions-billing",
      "private": "true",
      "version": "0.0.1",
      "description": "Examples of integrating Cloud Functions with billing",
      "main": "index.js",
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "compute-test": "c8 mocha -p -j 2 test/periodic.test.js --timeout=600000",
        "test": "c8 mocha -p -j 2 test/index.test.js --timeout=5000 --exit"
      },
      "author": "Ace Nassri <anassri@google.com>",
      "license": "Apache-2.0",
      "dependencies": {
        "@google-cloud/billing": "^4.0.0",
        "@google-cloud/compute": "^4.0.0",
        "google-auth-library": "^9.0.0",
        "googleapis": "^143.0.0",
        "slack": "^11.0.1"
      },
      "devDependencies": {
        "@google-cloud/functions-framework": "^3.0.0",
        "c8": "^10.0.0",
        "gaxios": "^6.0.0",
        "mocha": "^10.0.0",
        "promise-retry": "^2.0.0",
        "proxyquire": "^2.1.0",
        "sinon": "^18.0.0",
        "wait-port": "^1.0.4"
      }
    }
    

    Python

    ๋‹ค์Œ์„ requirements.txt์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.

    slackclient==2.9.4
    google-api-python-client==2.131.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 => {
  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 Run ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜์—ฌ Slack์— ํ‘œ์‹œ๋œ ๋ฉ”์‹œ์ง€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ ์ค‘์ง€๋ฅผ ์œ„ํ•œ ๊ฒฐ์ œ ์ƒํ•œ ์„ค์ •(์‚ฌ์šฉ ์ค‘์ง€)

์ด ์˜ˆ์—์„œ๋Š” Cloud Billing์„ ์‚ฌ์šฉ ์ค‘์ง€ํ•˜์—ฌ ํ”„๋กœ์ ํŠธ์˜ ๋น„์šฉ ์ƒํ•œ์„ ์„ค์ •ํ•˜๊ณ  ์‚ฌ์šฉ์„ ์ค‘์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์—์„œ ๊ฒฐ์ œ๋ฅผ ์‚ฌ์šฉ ์ค‘์ง€ํ•˜๋ฉด ๋ฌด๋ฃŒ ๋“ฑ๊ธ‰ ์„œ๋น„์Šค๋ฅผ ํฌํ•จํ•œ ํ”„๋กœ์ ํŠธ์˜ ๋ชจ๋“  Google Cloud ์„œ๋น„์Šค๊ฐ€ ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ์ œ๋ฅผ ์‚ฌ์šฉ ์ค‘์ง€ํ•˜๋Š” ์ด์œ 

Google Cloud์— ์ง€์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๊ธˆ์•ก์ด ์ œํ•œ์ ์ด์–ด์„œ ๋น„์šฉ ์ƒํ•œ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ๋Š” ํ•™์ƒ, ์—ฐ๊ตฌ์› ๋˜๋Š” ์ƒŒ๋“œ๋ฐ•์Šค ํ™˜๊ฒฝ์—์„œ ์ž‘์—…ํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ์„œ ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ ์˜ˆ์‚ฐ ํ•œ๋„์— ๋„๋‹ฌํ•  ๋•Œ ์ง€์ถœ์„ ๋ฉˆ์ถ”๊ณ  ๋ชจ๋“  Google Cloud ์„œ๋น„์Šค ๋ฐ ์‚ฌ์šฉ์„ ์ค‘์ง€ํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ์˜ˆ์‹œ์—์„œ๋Š” Cloud Billing์„ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉ ์ค‘์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๋น„ํ”„๋กœ๋•์…˜ ํ”„๋กœ์ ํŠธ๋กœ acme-backend-dev๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Google Cloud ์ฝ˜์†”์—์„œ ์˜ˆ์‚ฐ ์ƒํ•œ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ด ์˜ˆ์‹œ ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

Google Cloud ์ฝ˜์†”์—์„œ Cloud Billing ์•Œ๋ฆผ ๋ชฉ๋ก์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

Cloud Run ํ•จ์ˆ˜ ์ž‘์„ฑ

๊ทธ๋Ÿฐ ๋‹ค์Œ Cloud Billing API๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก Cloud Run ํ•จ์ˆ˜๋ฅผ ๊ตฌ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด Cloud Run ํ•จ์ˆ˜๊ฐ€ ์˜ˆ์‹œ ํ”„๋กœ์ ํŠธ์ธ acme-backend-dev์— ๋Œ€ํ•ด Cloud Billing์„ ์‚ฌ์šฉ ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. Cloud Run ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ์˜ ๋‹จ๊ณ„์— ๋”ฐ๋ผ ์ƒˆ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์˜ˆ์‚ฐ์— ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •๋œ ๊ฒƒ๊ณผ ๋™์ผํ•œ Pub/Sub ์ฃผ์ œ๋กœ ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

  2. ๋‹ค์Œ ์ข…์† ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

    Node.js

    ๋‹ค์Œ์„ package.json์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.

    {
      "name": "cloud-functions-billing",
      "private": "true",
      "version": "0.0.1",
      "description": "Examples of integrating Cloud Functions with billing",
      "main": "index.js",
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "compute-test": "c8 mocha -p -j 2 test/periodic.test.js --timeout=600000",
        "test": "c8 mocha -p -j 2 test/index.test.js --timeout=5000 --exit"
      },
      "author": "Ace Nassri <anassri@google.com>",
      "license": "Apache-2.0",
      "dependencies": {
        "@google-cloud/billing": "^4.0.0",
        "@google-cloud/compute": "^4.0.0",
        "google-auth-library": "^9.0.0",
        "googleapis": "^143.0.0",
        "slack": "^11.0.1"
      },
      "devDependencies": {
        "@google-cloud/functions-framework": "^3.0.0",
        "c8": "^10.0.0",
        "gaxios": "^6.0.0",
        "mocha": "^10.0.0",
        "promise-retry": "^2.0.0",
        "proxyquire": "^2.1.0",
        "sinon": "^18.0.0",
        "wait-port": "^1.0.4"
      }
    }
    

    Python

    ๋‹ค์Œ์„ requirements.txt์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.

    slackclient==2.9.4
    google-api-python-client==2.131.0
    

  3. ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ Cloud Run ํ•จ์ˆ˜์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.

  4. ์‹คํ–‰ํ•  ํ•จ์ˆ˜๋ฅผ stopBilling (๋…ธ๋“œ) ๋˜๋Š” stop_billing(Python)๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  5. ๋Ÿฐํƒ€์ž„์— ๋”ฐ๋ผ GOOGLE_CLOUD_PROJECT ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์ž๋™์œผ๋กœ ์„ค์ •๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๋™์œผ๋กœ ์„ค์ •๋œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ชฉ๋ก์„ ๊ฒ€ํ† ํ•˜๊ณ  Cloud Billing์„ ์ œํ•œ(์‚ฌ์šฉ ์ค‘์ง€)ํ•˜๋ ค๋Š” ํ”„๋กœ์ ํŠธ์— GOOGLE_CLOUD_PROJECT ๋ณ€์ˆ˜๋ฅผ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

Node.js

const {CloudBillingClient} = require('@google-cloud/billing');
const {InstancesClient} = require('@google-cloud/compute');

const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
const billing = new CloudBillingClient();

exports.stopBilling = async pubsubEvent => {
  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';
  }

  const billingEnabled = await _isBillingEnabled(PROJECT_NAME);
  if (billingEnabled) {
    return _disableBillingForProject(PROJECT_NAME);
  } else {
    return 'Billing already disabled';
  }
};

/**
 * 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.getProjectBillingInfo({name: projectName});
    return res.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.updateProjectBillingInfo({
    name: projectName,
    resource: {billingAccountName: ''}, // Disable billing
  });
  return `Billing disabled: ${JSON.stringify(res)}`;
};

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 Run ํ•จ์ˆ˜๋Š” ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ ์„œ๋น„์Šค ๊ณ„์ •์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค ๊ณ„์ •์ด ๊ฒฐ์ œ๋ฅผ ์‚ฌ์šฉ ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฒฐ์ œ ๊ด€๋ฆฌ์ž์™€ ๊ฐ™์€ ์˜ฌ๋ฐ”๋ฅธ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์˜ฌ๋ฐ”๋ฅธ ์„œ๋น„์Šค ๊ณ„์ •์„ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Cloud Run ํ•จ์ˆ˜ ์„ธ๋ถ€์ •๋ณด๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค ๊ณ„์ •์€ ํŽ˜์ด์ง€ ์•„๋ž˜์— ๋‚˜์—ด๋ฉ๋‹ˆ๋‹ค.

Google Cloud ์ฝ˜์†”์˜ Cloud Run ํ•จ์ˆ˜ ์„น์…˜์—์„œ ์„œ๋น„์Šค ๊ณ„์ • ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š” ์œ„์น˜๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

Google Cloud ์ฝ˜์†”์˜ ๊ฒฐ์ œ ํŽ˜์ด์ง€์—์„œ ๊ฒฐ์ œ ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋น„์Šค ๊ณ„์ •์— ๊ฒฐ์ œ ๊ณ„์ • ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๋ ค๋ฉด ์„œ๋น„์Šค ๊ณ„์ • ์ด๋ฆ„์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

Google Cloud ์ฝ˜์†”์˜ ๊ถŒํ•œ ์„น์…˜์—์„œ ์„œ๋น„์Šค ๊ณ„์ • ์ด๋ฆ„ ๋ฐ ๊ฒฐ์ œ ๊ณ„์ • ๊ด€๋ฆฌ์ž ์—ญํ• ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ์œ„์น˜๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

Cloud Billing์ด ์‚ฌ์šฉ ์ค‘์ง€๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‚ฐ์ด ์•Œ๋ฆผ์„ ์ „์†กํ•˜๋ฉด ์ง€์ •๋œ ํ”„๋กœ์ ํŠธ์— ๋” ์ด์ƒ Cloud Billing ๊ณ„์ •์ด ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.๋‹ค. ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ์œ„์˜ ํ…Œ์ŠคํŠธ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ˜ํ”Œ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฒŒ์‹œํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ๊ฐ€ ๋” ์ด์ƒ Cloud Billing ๊ณ„์ •์— ํ‘œ์‹œ๋˜์ง€ ์•Š์œผ๋ฉฐ, ๋™์ผํ•œ ํ”„๋กœ์ ํŠธ์— ์žˆ๋Š” ๊ฒฝ์šฐ Cloud Run ํ•จ์ˆ˜๋ฅผ ํฌํ•จํ•˜์—ฌ ํ”„๋กœ์ ํŠธ์˜ ๋ฆฌ์†Œ์Šค๊ฐ€ ์‚ฌ์šฉ ์ค‘์ง€๋ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ ํ”„๋กœ์ ํŠธ๊ฐ€ Cloud Billing ๊ณ„์ •์— ์—ฐ๊ฒฐ๋œ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก์— ๋” ์ด์ƒ ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ด๊ฒƒ์œผ๋กœ ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•ด Cloud Billing์ด ์‚ฌ์šฉ ์ค‘์ง€๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Google Cloud ์ฝ˜์†”์—์„œ ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•ด Cloud Billing์„ ์ˆ˜๋™์œผ๋กœ ๋‹ค์‹œ ์‚ฌ์šฉ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ ํƒ์ ์œผ๋กœ ์‚ฌ์šฉ๋Ÿ‰ ์ œ์–ด

์ด์ „ ์˜ˆ์‹œ์— ์„ค๋ช…๋œ ๋Œ€๋กœ Cloud Billing ์ƒํ•œ ์„ค์ •(์‚ฌ์šฉ ์ค‘์ง€)์˜ ๊ฒฐ๊ณผ๋Š” ๋‘ ๊ฐ€์ง€๋ฟ์ด๊ณ  ๋Œ์ดํ‚ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ฆ‰ ํ”„๋กœ์ ํŠธ๋Š” ์‚ฌ์šฉ ์„ค์ •๋˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ ์ค‘์ง€๋ฉ๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ๊ฐ€ ์‚ฌ์šฉ ์ค‘์ง€๋˜๋ฉด ๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ์ค‘์ง€๋˜๊ณ  ๋ชจ๋“  ๋ฆฌ์†Œ์Šค๊ฐ€ ๊ฒฐ๊ณผ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.

๋ณด๋‹ค ์„ธ๋ฐ€ํ•œ ๋Œ€์‘์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋Š” ์„ ํƒ์ ์œผ๋กœ ๋ฆฌ์†Œ์Šค๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์ผ๋ถ€ Compute Engine ๋ฆฌ์†Œ์Šค๋ฅผ ์ค‘์ง€ํ•˜๊ณ  Cloud Storage๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๋ ค๋ฉด ์„ ํƒ์ ์œผ๋กœ ์‚ฌ์šฉ๋Ÿ‰์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ™˜๊ฒฝ์„ ์™„์ „ํžˆ ์‚ฌ์šฉ ์ค‘์ง€ํ•˜์ง€ ์•Š์•„๋„ ์‹œ๊ฐ„๋‹น ๋น„์šฉ์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์›ํ•˜๋Š” ๋Œ€๋กœ ์ •์ฑ…์„ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ์˜ˆ์‹œ์—์„œ ํ”„๋กœ์ ํŠธ๋Š” ๋งŽ์€ ์ˆ˜์˜ Compute Engine ๊ฐ€์ƒ ๋จธ์‹ ์ด ํฌํ•จ๋œ ์—ฐ๊ตฌ๋ฅผ ์‹คํ–‰ ์ค‘์ด๋ฉฐ, Cloud Storage์— ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด Cloud Run ํ•จ์ˆ˜ ์˜ˆ์‹œ๋Š” ๋ชจ๋“  Compute Engine ์ธ์Šคํ„ด์Šค๋ฅผ ์ข…๋ฃŒํ•˜์ง€๋งŒ, ์˜ˆ์‚ฐ์ด ์ดˆ๊ณผ๋œ ํ›„์—๋„ ์ €์žฅ๋œ ๊ฒฐ๊ณผ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Cloud Run ํ•จ์ˆ˜ ์ž‘์„ฑ

  1. Cloud Run ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ์˜ ๋‹จ๊ณ„์— ๋”ฐ๋ผ ์ƒˆ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์˜ˆ์‚ฐ์— ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •๋œ ๊ฒƒ๊ณผ ๋™์ผํ•œ Pub/Sub ์ฃผ์ œ๋กœ ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

  2. ์‚ฌ์šฉ๋Ÿ‰ ์ค‘์ง€๋ฅผ ์œ„ํ•œ ๊ฒฐ์ œ ์ƒํ•œ ์„ค์ • (์‚ฌ์šฉ ์ค‘์ง€)์— ์„ค๋ช…๋œ ์ข…์† ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

  3. ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ Cloud Run ํ•จ์ˆ˜์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.

  4. ์‹คํ–‰ํ•  ํ•จ์ˆ˜๋ฅผ limitUse (๋…ธ๋“œ) ๋˜๋Š” limit_use (Python)๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  5. ๋Ÿฐํƒ€์ž„์— ๋”ฐ๋ผ GCP_PROJECT ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์ž๋™์œผ๋กœ ์„ค์ •๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๋™์œผ๋กœ ์„ค์ •๋œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ชฉ๋ก์„ ๊ฒ€ํ† ํ•˜๊ณ  ๊ฐ€์ƒ ๋จธ์‹ ์„ ์‹คํ–‰ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์— GCP_PROJECT ๋ณ€์ˆ˜๋ฅผ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

  6. ZONE ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ƒ˜ํ”Œ์— ๋Œ€ํ•ด ์ธ์Šคํ„ด์Šค๊ฐ€ ์ค‘์ง€๋˜๋Š” ์˜์—ญ์ž…๋‹ˆ๋‹ค.

Node.js

const {CloudBillingClient} = require('@google-cloud/billing');
const {InstancesClient} = require('@google-cloud/compute');

const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
const PROJECT_NAME = `projects/${PROJECT_ID}`;
const instancesClient = new InstancesClient();
const ZONE = 'us-central1-a';

exports.limitUse = async pubsubEvent => {
  const pubsubData = JSON.parse(
    Buffer.from(pubsubEvent.data, 'base64').toString()
  );
  if (pubsubData.costAmount <= pubsubData.budgetAmount) {
    return `No action necessary. (Current cost: ${pubsubData.costAmount})`;
  }

  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 [instances] = await instancesClient.list({
    project: projectId,
    zone: zone,
  });
  return instances
    .filter(item => item.status === 'RUNNING')
    .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 instancesClient
        .stop({
          project: projectId,
          zone: zone,
          instance: instanceName,
        })
        .then(() => {
          console.log(`Instance stopped successfully: ${instanceName}`);
        });
    })
  );
};

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 Run ํ•จ์ˆ˜๋Š” ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ ์„œ๋น„์Šค ๊ณ„์ •์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ๋Ÿ‰์„ ์ œ์–ดํ•˜๋ ค๋ฉด ํ”„๋กœ์ ํŠธ์—์„œ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•œ ๋ชจ๋“  ์„œ๋น„์Šค์— ์„œ๋น„์Šค ๊ณ„์ • ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. ์˜ฌ๋ฐ”๋ฅธ ์„œ๋น„์Šค ๊ณ„์ •์„ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด Cloud Run ํ•จ์ˆ˜์˜ ์„ธ๋ถ€์ •๋ณด๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค ๊ณ„์ •์€ ํŽ˜์ด์ง€ ์•„๋ž˜์— ๋‚˜์—ด๋ฉ๋‹ˆ๋‹ค.
  3. Google Cloud ์ฝ˜์†”์—์„œ IAM ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜์—ฌ ์ ์ ˆํ•œ ๊ถŒํ•œ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    IAM ํŽ˜์ด์ง€๋กœ ์ด๋™
     
    Cloud Run ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์„œ๋น„์Šค ๊ณ„์ •์— ๋Œ€ํ•ด ์ ์ ˆํ•œ ๊ถŒํ•œ์„ ์„ค์ •ํ•˜๋Š” Google Cloud ์ฝ˜์†”์˜ IAM ํ™”๋ฉด์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

์ธ์Šคํ„ด์Šค๊ฐ€ ์ค‘์ง€๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

์˜ˆ์‚ฐ์ด ์•Œ๋ฆผ์„ ์ „์†กํ•˜๋ฉด Compute Engine ๊ฐ€์ƒ ๋จธ์‹ ์ด ์ค‘์ง€๋ฉ๋‹ˆ๋‹ค.

ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ์•ž์˜ ํ…Œ์ŠคํŠธ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ˜ํ”Œ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฒŒ์‹œํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด Google Cloud ์ฝ˜์†”์—์„œ Compute Engine ๊ฐ€์ƒ ๋จธ์‹ ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.