카테고리 없음
Slack 기반 GCE Managed Instance Group 오토스케일링 자동화 구축 가이드 (Pub/Sub 연동, 상태 알림 포함)
달빛궁전-
2025. 8. 8. 10:00
목표 및 배경
목표 : Slack에서 버튼 한 번 클릭만으로 GCP Regional Managed Instance Group(MIG)의 인스턴스 개수를 신속하게 조정
배경 : 운영자들이 수동으로 GCP 콘솔/명령어 대신, 더 친숙한 Slack 환경에서 증설·감소를 할 수 있도록 함
Pub/Sub 기반 비동기 아키텍처로 Cloud Run이 혹여 동작을 못하였다고 해도 확인 후 재시도가 가능합니다.
증설/감소 시 현 상태(기존 인스턴스 수 → 변경 후 인스턴스 수)를 명확하게 Slack으로 안내받아 실시간 모니터링 효과까지 볼 수 있습니다.
1. 아키텍처
1-1. 아키텍처 설명
Slack (/auto 명령 or 버튼)
↓
Cloud Run 서비스(슬랙 이벤트 트리거)
↓ (Pub/Sub 메시지 발행)
Pub/Sub (증설/감소 요청 큐)
↓ (푸시 구독)
Cloud Run Processor (MIG 정보 조회 + 증설/감소 실행)
↓
Slack Webhook(채널/DM)로 결과 메시지: "기존 x대에서 y대로 증설/감소됨"
서비스 계정
서비스 계정 용도Roles 권한부여주요 권한(Permission, 실제 API/용도)
서비스 계정 | ||
트리거용 Cloud Run (Slack → Pub/Sub) |
Pub/Sub Publisher | pubsub.topics.publish: Pub/Sub 토픽에 메시지 게시 pubsub.topics.get: 토픽 정보 읽기 |
Cloud Run Invoker | run.routes.invoke: Cloud Run 서비스 HTTP/HTTPS 엔드포인트 인증 호출 | |
프로세서용 Cloud Run (Pub/Sub → MIG 조정) |
Compute Instance Admin (v1) | compute.instanceGroupManagers.get: MIG 상태 조회 compute.instanceGroupManagers.resize: MIG 증설/감소 compute.instances.get/insert/delete: VM 등 Compute 인스턴스 관리 |
Pub/Sub Subscriber | pubsub.subscriptions.consume: Pub/Sub 구독 메시지 수신/처리(pull, ack 포함) | |
Cloud Run Invoker | run.routes.invoke: Cloud Run Processor 엔드포인트 인증 POST 허용 | |
Pub/Sub 푸시 인증용 시스템 계정 | Service Account Token Creator | iam.serviceAccounts.getAccessToken: 서비스 계정 액세스 토큰 생성 iam.serviceAccounts.getOpenIdToken: OIDC 토큰 생성 iam.serviceAccounts.signJwt: JWT 서명 (Pub/Sub Push 인증 등) |
(A) 트리거용 Cloud Run 서비스 계정에 부여할 최소 권한
-
roles/pubsub.publisher
: Slack 이벤트를 받아 Pub/Sub 메시지를 발행하기 위한 권한입니다. -
roles/run.invoker
: 필요한 경우 Cloud Run 서비스 호출 권한입니다. (Slack, Pub/Sub 호출 허용)
(B) 프로세서용 Cloud Run 서비스 계정에 부여할 최소 권한
-
roles/compute.instanceGroupManager
: GCE Managed Instance Group(Regional MIG 등) 크기 조회 및 변경 권한 -
roles/pubsub.subscriber
: Pub/Sub 메시지 구독 및 수신 권한 -
roles/run.invoker
: Pub/Sub 푸시 호출 권한 (Cloud Run 서비스 접근 허용)
(C) Pub/Sub 푸시 인증용 서비스 계정 (자동 시스템 계정)
-
Google Cloud에서 자동 생성하는 계정 예:
service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com -
역할:
roles/iam.serviceAccountTokenCreator
: Pub/Sub가 인증된 토큰을 생성하여 Cloud Run 서비스에 안전하게 HTTP 푸시 호출하기 위해 필요합니다.
2. Cloud Run 기본 환경 변수 (env.list)
아래의 내용을 환경변수로 지정합니다.
env.list로 따로 저장을 합니다.
키:값 쌍으로 표현되어야 하기 때문에 YAML 포맷으로 작성합니다.
GCP_PROJECT_ID: ctu-gcp-dsa2-unit // 프로젝트 ID
GCE_REGION: asia-northeast3 // AutoScling 대상 REGION
MIG_NAME: my-mig-web-prod // GCE Instance Group Name
CATEGORY_TARGET_SIZE_JSON: '{"Low":2,"Normal":4,"High":8,"Very High":12}' // 증설 카테고리
SLACK_SIGNING_SECRET: xoxb-your-signing-secret // slack Secret Key
PUBSUB_TOPIC: mig-autoscale-requests // Pubsub 주제
SLACK_RESULT_WEBHOOK_URL: https://hooks.slack.com/services/XXXX/XXXX/XXXX // slack Webhook 메시지 발송 URL
3. Slack 트리거용 Cloud Run 서비스 (main.py)
import os
import json
import time
import hmac
import hashlib
from flask import Flask, request, make_response
from google.cloud import pubsub_v1
# 환경 변수 로드
SLACK_SIGNING_SECRET = os.environ.get('SLACK_SIGNING_SECRET', '')
PUBSUB_TOPIC = os.environ.get('PUBSUB_TOPIC', '')
PROJECT_ID = os.environ.get('GCP_PROJECT_ID', '')
CATEGORY_TARGET_SIZE_JSON = os.environ.get('CATEGORY_TARGET_SIZE_JSON', '{}')
try:
CATEGORY_TARGET_SIZE = json.loads(CATEGORY_TARGET_SIZE_JSON)
except json.JSONDecodeError:
CATEGORY_TARGET_SIZE = {}
# Flask 앱 및 Pub/Sub 퍼블리셔 초기화
app = Flask(__name__)
publisher = pubsub_v1.PublisherClient()
topic_path = ''
if PROJECT_ID and PUBSUB_TOPIC:
topic_path = publisher.topic_path(PROJECT_ID, PUBSUB_TOPIC)
def verify_slack_signature(req):
"""슬랙 요청 서명 검증(보안 필수!)"""
timestamp = req.headers.get('X-Slack-Request-Timestamp')
slack_signature = req.headers.get('X-Slack-Signature')
if not timestamp or not slack_signature:
return False
# 5분 이상 차이나는 timestamp는 무효(Replay 방지)
if abs(time.time() - int(timestamp)) > 60 * 5:
return False
body = req.get_data(as_text=True)
basestring = f'v0:{timestamp}:{body}'
computed = 'v0=' + hmac.new(
SLACK_SIGNING_SECRET.encode(),
basestring.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, slack_signature)
@app.route('/slack/events', methods=['POST'])
def slack_events():
"""
- 슬래시 커맨드(/auto 등) 입력시: 증설 카테고리 버튼 UI 리턴
- 버튼 클릭시: Pub/Sub 메시지 발행, 슬랙에 접수 알림
"""
try:
if not verify_slack_signature(request):
return make_response("Unauthorized", 401)
# 슬래시커맨드 입력시: 버튼 UI 리턴
if request.form.get('command') in ['/auto', '/autoscale']:
blocks = [
{"type": "section",
"text": {"type": "mrkdwn",
"text": "*오토스케일링*\n증설 카테고리를 선택하세요:"}},
{"type": "actions",
"elements": [
{"type": "button",
"text": {"type": "plain_text", "text": category},
"value": category,
"action_id": f"select_{category.lower().replace(' ', '_')}"}
for category in CATEGORY_TARGET_SIZE.keys()
]}
]
response_body = {"blocks": blocks, "response_type": "ephemeral"}
return make_response(json.dumps(response_body), 200, {"Content-Type": "application/json"})
# 버튼 클릭시(인터랙션): Pub/Sub로 메시지 적재, 즉시 접수 알림
if 'payload' in request.form:
payload = json.loads(request.form['payload'])
user_id = payload.get('user', {}).get('id', 'unknown')
actions = payload.get('actions', [])
if actions and len(actions) > 0:
action = actions[0]
selected_category = action.get('value')
if not topic_path:
return make_response("Pub/Sub topic not configured", 500)
pubsub_data = {
"category": selected_category,
"user_id": user_id,
"timestamp": payload.get('action_ts', '')
}
publisher.publish(topic_path, json.dumps(pubsub_data).encode())
response_text = f"⏳ <@{user_id}>님, '{selected_category}' 증설 요청이 접수되었습니다. 상태는 별도 알림으로 안내됩니다."
response = {
"replace_original": True,
"text": response_text
}
return make_response(json.dumps(response), 200, {"Content-Type": "application/json"})
# 그 외 200
return make_response("", 200)
except Exception as e:
# 예외 발생시 500 에러 및 로그
print(f"Exception in slack_events handler: {e}")
return make_response("Internal Server Error", 500)
Cloud Run 배포 명령어
gcloud run deploy 서비스명-slack-gce-mig-trigger \
--source . \ (소스코드 위치)
--region asia-northeast3 \ (Cloud Run 배포 리전)
--env-vars-file env.list \ (환경설정 변수 파일위치)
--allow-unauthenticated \ (별도 인증없이 허용 / slack에서 인증을 하기에 cloud run은 해당옵션으로 진행)
--service-account=slack@***.iam.gserviceaccount.com (cloud run 실행할 서비스 계정 / 계정항목에서 설명)
4. Pub/Sub Processor용 Cloud Run (main.py)
import os
import json
import base64
import requests
from flask import Flask, request, make_response
from google.cloud import compute_v1
app = Flask(__name__)
# 환경 변수 로드
PROJECT_ID = os.environ.get('GCP_PROJECT_ID', '')
REGION = os.environ.get('GCE_REGION', '')
MIG_NAME = os.environ.get('MIG_NAME', '')
SLACK_RESULT_WEBHOOK_URL = os.environ.get('SLACK_RESULT_WEBHOOK_URL', '')
CATEGORY_TARGET_SIZE_JSON = os.environ.get('CATEGORY_TARGET_SIZE_JSON', '{}')
try:
CATEGORY_TARGET_SIZE = json.loads(CATEGORY_TARGET_SIZE_JSON)
except json.JSONDecodeError:
CATEGORY_TARGET_SIZE = {}
compute_client = compute_v1.RegionInstanceGroupManagersClient()
def send_slack_result_message(message: str):
"""Slack Webhook으로 작업 결과 알림 전송"""
if not SLACK_RESULT_WEBHOOK_URL:
print("Slack Webhook URL이 설정되지 않음.")
return
try:
response = requests.post(SLACK_RESULT_WEBHOOK_URL, json={"text": message})
if response.status_code != 200:
print(f"Slack 메시지 전송 실패: {response.status_code}, {response.text}")
except Exception as e:
print(f"Slack Webhook 예외: {e}")
def do_resize_with_current(target_size):
"""기존 인스턴스 수 조회 후 스케일링 및 알림 메시지 전송"""
try:
mig = compute_client.get(
project=PROJECT_ID,
region=REGION,
instance_group_manager=MIG_NAME
)
current_size = mig.target_size
compute_client.resize(
project=PROJECT_ID,
region=REGION,
instance_group_manager=MIG_NAME,
size=target_size
)
if target_size > current_size:
msg = f":white_check_mark: 기존 인스턴스는 {current_size}대였고, 증설 명령을 통해 {target_size}대로 증설되었습니다."
elif target_size < current_size:
msg = f":white_check_mark: 기존 인스턴스는 {current_size}대였고, 감소 명령을 통해 {target_size}대로 감소하였습니다."
else:
msg = f":white_check_mark: 인스턴스 수는 {target_size}대로 변경되지 않았습니다."
send_slack_result_message(msg)
except Exception as e:
send_slack_result_message(f":x: 스케일링 작업 실패: {e}")
@app.route('/', methods=['POST'])
def pubsub_handler():
"""Pub/Sub 푸시 메시지 엔드포인트(증설/감소 작업 실행)"""
envelope = request.get_json(silent=True)
if not envelope or 'message' not in envelope:
return make_response("Bad Request", 400)
pubsub_message = envelope['message']
try:
data = json.loads(base64.b64decode(pubsub_message.get('data', '')).decode())
except Exception as e:
print(f"Pub/Sub 메시지 디코딩 오류: {e}")
return make_response("", 400)
category = data.get('category')
user_id = data.get('user_id', 'unknown')
if category not in CATEGORY_TARGET_SIZE:
send_slack_result_message(f":x: 잘못된 카테고리 '{category}' 요청이 들어왔습니다.")
return make_response("", 200)
target_size = CATEGORY_TARGET_SIZE[category]
do_resize_with_current(target_size)
return make_response("", 200)
배포 및 Pub/Sub 푸시 구독 연결
gcloud run deploy mig-autoscale-processor \
--source . \
--region asia-northeast3 \
--env-vars-file env.list \
--service-account=slack@***.iam.gserviceaccount.com@****.iam.gserviceaccount.com \
--no-allow-unauthenticated \
# Cloud Run 배포 후 변수설정하여, 구독연결
SERVICE_URL=$(gcloud run services describe mig-autoscale-processor --region asia-northeast3 --format='value(status.url)')
gcloud pubsub subscriptions create mig-autoscale-sub \
--topic=mig-autoscale-requests \
--push-endpoint=$SERVICE_URL \
--push-auth-service-account=slack@.iam.gserviceaccount.com (프로세서용 Cloud Run 서비스 계정 연결)
5. Slack 앱 설정
- https://api.slack.com/apps → Create New App 선택

- Slash Command
슬랙 명령어 설정
-
원하는 커맨드(/auto 등) 추가 후 Request URL 설정
-
Request URL: Cloud Run Trigger의 Service URL+ /slack/events 입력 [...]/slack/events
-
Interactivity (슬랙 메시지 내에서 사용자가 버튼 클릭, 메뉴 선택 등 상호작용을 할 때, 그 이벤트를 슬랙이 지정한 서버(웹훅 URL)로 전달해 주도록 하기 위한 기능)
ON, 위 Slash Command과 동일한 Request URL 입력
-
Bot Token Scopes: commands, chat:write, incoming-webhook
Scope 이름 용도 요약 commands 슬래시 커맨드(/auto 등) 등록 및 사용 incoming-webhook Webhook 방식으로 슬랙 채널에 메시지 전송 chat:write 봇이 슬랙 채널/DM에 메시지 보내기 - Incoming Webhooks : auto Scaling가 완료된 후 Slack Webhook(채널/DM)로 결과 메시지를 받기 위한 설정
-
앱 워크스페이스에 설치 : Installed App 에서 설치진행
6. 운영
-
Slack에서 /auto 실행 → 증설 카테고리 버튼 클릭
-
Cloud Run 서비스 로그와 Pub/Sub 구독 확인
-
Processor에서 증설/감소 및 슬랙 알림 메시지까지 정상 표시 점검
-
알림 메시지 예시
-
:white_check_mark: 기존 인스턴스는 1대였고, 증설 명령을 통해 2대로 증설되었습니다.
-
:white_check_mark: 기존 인스턴스는 6대였고, 감소 명령을 통해 2대로 감소하였습니다.
Slack 동작화면
GCE Instance Group AutoScaling 동작화면