![[NCLOUD] KVM 서버에서 서버 이미지와 블록 스토리지 스냅샷을 자동으로 관리하는 방법을 알아보기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmLXDi%2FbtsMs6GQUMX%2FHUlNe8QlLtVjJdakjipOOK%2Fimg.png)
안녕하세요.
이번 시간에는 NCLOUD API와 Cloud Functions을 이용하여 KVM 하이퍼바이저 서버에서 서버 이미지를 자동으로 관리하는 방법을 알아보고자 합니다.
0. 문제 상황
(AS-IS) XEN 기반의 서버는 서버 이미지 삭제 시 블록 스토리지 스냅샷도 같이 삭제되었음
(TO-BE) KVM 기반의 서버로 넘어오면서 서버 이미지와 블록 스토리지 스냅샷을 따로 삭제하게 끔 변경
*블록 스토리지 스냅샷이 자동으로 삭제되지 않고 쌓이면서 과도한 비용이 청구되는 상황이 발생
1. Ncloud API 변경 사항
https://manvscloud.com/?p=2691
[NCLOUD] 업데이트된 서버 이미지 백업 자동화 편 (with KVM Hypervisor)
안녕하세요. MANVSCLOUD 김수현입니다. 이전에 [NCLOUD] CLOUD FUNCTIONS을 이용한 서버 이미지 백업 자동화 및 SLACK 알림(https://manvscloud.com/?p=2138)과 [NCLOUD] 백업용 서버 이미지 보관일 설정 및 삭제 자동화(
manvscloud.com
MANVSCLOUD 김수현님께서는 KVM에서 관리하는 서버 이미지의 API가 기존 XEN에서 사용하는 API와 달라져서 기존 API를 이용하여 KVM에서 서버 이미지를 관리할 수 없는 상황이 발생하여 그것을 해결하는 포스팅을 작성해 주셨습니다.
XEN 하이퍼바이저 기반의 서버를 사용할 때는 서버 이미지와 블록 스토리지 스냅샷이 하나의 API로 동시에 삭제되었지만 KVM으로 넘어오면서 서버 이미지와 블록 스토리지 스냅샷을 각각 다른 API를 사용하여 관리하게 끔 변경되었습니다.
아시는 분들은 아시는 내용이지만 서버 이미지를 생성하면 블록 스토리지 스냅샷이 같이 생성되는 것이 디폴트 값입니다.
따라서 자동으로 관리하기 위한 API는 다음과 같습니다.
NCLOUD API | getServerInstanceList | 서버 리스트 조회 |
createServerImage | 서버 이미지 생성 | |
getServerImageList | 서버 이미지 리스트 조회 | |
deleteServerImage | 서버 이미지 삭제 | |
getBlockStorageSnapshotInstanceList | 블록 스토리지 스냅샷 리스트 조회 | |
deleteBlockStorageSnapshotInstances | 블록 스토리지 스냅샷 삭제 |
다음과 같이 5개의 API와 Cloud Functions를 이용하여 서버 이미지와 블록 스토리지를 자동으로 관리하는 로직을 작성하겠습니다.
2. 네이버 클라우드에서 서버 이미지 자동화 구성하기
1. API 키 생성하기
네이버 클라우드 플랫폼 홈페이지에서 마이페이지 - 인증키 관리를 선택합니다.
신규 API 인증키 생성을 누른 후 Access Key ID와 Secret Key를 복사해 둡니다.
2. Slack incoming Webhook 생성하기
다음과 같이 채널을 우클릭한 후 채널 세부정보 보기 - 통합 - 앱 추가를 선택합니다.
Incoming WebHooks의 보기를 선택합니다.
구성을 선택합니다.
Slack에 추가를 선택합니다.
채널을 선택한 후 수신 웹후크 통합 앱 추가를 선택합니다.
웹후크 URL 주소를 복사해 둡니다.
3. Cloud Functions 패키지 생성하기
네이버 클라우드 플랫폼 콘솔에서 Cloud Functions - Action - Package를 생성합니다.
4. Cloud Functions 액션 생성하기
server-image-automation이라는 패키지를 생성한 후 Action을 생성합니다.
다음과 같이 Action 생성 정보를 입력합니다.
import requests
import time
import hashlib
import hmac
import base64
import json
from datetime import datetime, timedelta
def get_timestamp():
return str(int(time.time() * 1000))
def generate_signing_key(secret_key, message):
return base64.b64encode(hmac.new(secret_key, message, digestmod=hashlib.sha256).digest())
def api_request(api_server, uri, access_key, secret_key, method="GET"):
timestamp = get_timestamp()
message = bytes(f"{method} {uri}\n{timestamp}\n{access_key}", 'UTF-8')
secret_key = bytes(secret_key, 'UTF-8')
signingKey = generate_signing_key(secret_key, message)
http_header = {
'x-ncp-apigw-signature-v2': signingKey,
'x-ncp-apigw-timestamp': timestamp,
'x-ncp-iam-access-key': access_key
}
if method.upper() == "GET":
response = requests.get(api_server + uri, headers=http_header)
else:
response = requests.post(api_server + uri, headers=http_header)
return response.json()
def send_slack_message(webhook_url, message):
payload = {'text': message}
response = requests.post(webhook_url, json=payload)
if response.status_code != 200:
print(f"Error sending message to Slack: {response.text}")
def send_slack_message_success(webhook_url, messageTitle, system_name, environment, serviceType, actionType, InstanceNo, InstanceName, create_date_str, extraMessage):
today = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
baseMessage = (
'>*[SUCCESS] {messageTitle}.* \n'
'>`{system_name}` `{environment}` `{serviceType}` `{actionType}`\n'
'>```\n'
'[ Instance ID ] : {InstanceNo}\n'
'[ Instance Name ] : {InstanceName} \n'
'[ Create Date ] : {create_date_str} \n'
'[ Deleted Success Date ] : {delete_date_str} \n'
'[ Success Message ] : {extraMessage}```'
)
sendMessage = baseMessage.format(
messageTitle = messageTitle,
system_name = system_name,
environment = environment,
serviceType = serviceType,
actionType = actionType,
InstanceNo = InstanceNo,
InstanceName = InstanceName,
create_date_str = create_date_str,
delete_date_str = today,
extraMessage = extraMessage
)
send_slack_message(webhook_url, sendMessage)
def send_slack_message_failed(webhook_url, messageTitle, system_name, environment, serviceType, actionType, InstanceNo, InstanceName, create_date_str, extraMessage):
today = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
baseMessage = (
'>*[ERROR] {messageTitle}.* \n'
'>`{system_name}` `{environment}` `{serviceType}` `{actionType}`\n'
'>```\n'
'[ Instance ID ] : {InstanceNo}\n'
'[ Instance Name ] : {InstanceName} \n'
'[ Create Date ] : {create_date_str} \n'
'[ Deleted Failed Date ] : {delete_date_str} \n'
'[ Error Message ] : {extraMessage}```'
)
sendMessage = baseMessage.format(
messageTitle = messageTitle,
system_name = system_name,
environment = environment,
serviceType = serviceType,
actionType = actionType,
InstanceNo = InstanceNo,
InstanceName = InstanceName,
create_date_str = create_date_str,
delete_date_str = today,
extraMessage = extraMessage
)
send_slack_message(webhook_url, sendMessage)
def create_server_image(system_name, environment, serverInstanceNo, access_key, secret_key, api_server, webhook_url):
today = time.strftime("%Y%m%d")
today_full_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
serverImageName = f"backup-{serverInstanceNo}-{today}"
uri = f"/vserver/v2/createServerImage?regionCode=KR&serverInstanceNo={serverInstanceNo}&serverImageName={serverImageName}&responseFormatType=json"
response = api_request(api_server, uri, access_key, secret_key)
serviceType = "Server Image"
actionType = "Create"
if 'responseError' in response:
messageTitle = "Backup creation failed"
send_slack_message_failed(webhook_url, messageTitle, system_name, environment, serviceType, actionType, serverInstanceNo, serverImageName, today_full_str, response['responseError'])
else:
messageTitle = "Backup creation for Server successfully."
extraMessage = f"Server Image '{serverImageName}' created successfully."
send_slack_message_success(webhook_url, messageTitle, system_name, environment, serviceType, actionType, serverInstanceNo, serverImageName, today_full_str, extraMessage)
def delete_server_image(system_name, environment, serverImageNo, serverImageName, formatted_create_date, access_key, secret_key, api_server, webhook_url):
uri = f"/vserver/v2/deleteServerImage?regionCode=KR&serverImageNoList.1={serverImageNo}&responseFormatType=json"
response = api_request(api_server, uri, access_key, secret_key)
serviceType = "Server Image"
actionType = "Delete"
if 'responseError' in response:
messageTitle = "Failed to delete Server Image"
send_slack_message_failed(webhook_url, messageTitle, system_name, environment, serviceType, actionType, serverImageNo, serverImageName, formatted_create_date, response['responseError'])
else:
messageTitle = "Deleted Server Image successfully."
extraMessage = f"Server Image '{serverImageName}' deleted successfully."
send_slack_message_success(webhook_url, messageTitle, system_name, environment, serviceType, actionType, serverImageNo, serverImageName, formatted_create_date, extraMessage)
def delete_block_storage_snapshot(system_name, environment, blockStorageSnapshotInstanceNo, blockStorageSnapshotName, create_date_str, access_key, secret_key, api_server, webhook_url):
uri = f"/vserver/v2/deleteBlockStorageSnapshotInstances?blockStorageSnapshotInstanceNoList.1={blockStorageSnapshotInstanceNo}&responseFormatType=json"
response = api_request(api_server, uri, access_key, secret_key)
serviceType = "Block Storage Snapshot"
actionType = "Delete"
if 'responseError' in response:
print(response)
messageTitle = "Failed to delete block storage snapshot"
send_slack_message_failed(webhook_url, messageTitle, system_name, environment, serviceType, actionType, blockStorageSnapshotInstanceNo, blockStorageSnapshotName, create_date_str, response['responseError'])
else:
messageTitle = "Block storage snapshot deletion was successful"
extraMessage = f"Block Storage Snapshot '{blockStorageSnapshotName}' deleted successfully."
send_slack_message_success(webhook_url, messageTitle, system_name, environment, serviceType, actionType, blockStorageSnapshotInstanceNo, blockStorageSnapshotName, create_date_str, extraMessage)
def main(args):
access_key = args["NCLOUD_ACCESS_KEY"]
secret_key = args["NCLOUD_SECRET_KEY"]
webhook_url = args["webhook_url"]
exclude_patterns = args["exclude_patterns"]
system_name = args["SYSTEM_NAME"]
environment = args["ENVIRONMENT"]
api_server = "https://ncloud.apigw.ntruss.com"
serverInstanceNos_response = api_request(
api_server,
"/vserver/v2/getServerInstanceList?regionCode=KR&responseFormatType=json",
access_key,
secret_key
)
for serverInstance in serverInstanceNos_response.get('getServerInstanceListResponse', {}).get('serverInstanceList', []):
if serverInstance['serverInstanceNo'] not in exclude_patterns:
create_server_image(system_name, environment, serverInstance['serverInstanceNo'], access_key, secret_key, api_server, webhook_url)
time.sleep(1)
images_response = api_request(
api_server,
"/vserver/v2/getServerImageList?regionCode=KR&responseFormatType=json",
access_key,
secret_key
)
snapshots_response = api_request(
api_server,
"/vserver/v2/getBlockStorageSnapshotInstanceList?regionCode=KR&responseFormatType=json",
access_key,
secret_key
)
current_date = datetime.now()
one_day_ago = current_date - timedelta(days=28)
for image in images_response.get('getServerImageListResponse', {}).get('serverImageList', []):
image_name = image['serverImageName']
if 'backup' in image_name:
create_date_str = image['createDate']
formatted_create_date_str = datetime.strptime(create_date_str, "%Y-%m-%dT%H:%M:%S%z")
formatted_create_date = formatted_create_date_str.strftime("%Y-%m-%d %H:%M:%S")
image_date_str = image_name.split('-')[-1]
image_date = datetime.strptime(image_date_str, '%Y%m%d')
if image_date < one_day_ago:
delete_server_image(system_name, environment, image['serverImageNo'], image_name, formatted_create_date, access_key, secret_key, api_server, webhook_url)
time.sleep(1)
for snapshot in snapshots_response.get('getBlockStorageSnapshotInstanceListResponse', {}).get('blockStorageSnapshotInstanceList', []):
blockStorageSnapshotInstanceNo = snapshot["blockStorageSnapshotInstanceNo"]
blockStorageSnapshotName = snapshot["blockStorageSnapshotName"]
create_date_str = snapshot['createDate']
formatted_create_date_str = datetime.strptime(create_date_str, "%Y-%m-%dT%H:%M:%S%z")
formatted_create_date = formatted_create_date_str.strftime("%Y-%m-%d %H:%M:%S")
create_date = datetime.strptime(create_date_str.split("T")[0], "%Y-%m-%d")
if create_date < one_day_ago:
delete_block_storage_snapshot(system_name, environment, blockStorageSnapshotInstanceNo, blockStorageSnapshotName, formatted_create_date, access_key, secret_key, api_server, webhook_url)
time.sleep(1)
return {
"message": "Function executed successfully"
}
다음은 실제 배치성 동작을 수행하는 로직입니다. 조금 더 코드를 다듬으면 좋을 것 같습니다.
주요 값들은 Cloud Functions에서 디폴트 파라미터를 통해 입력받고 API 키의 경우 KMS를 통한 암호화도 진행합니다.
암호화할 시 Key Management Service (KMS)를 미리 생성한 후 NCLOUD_ACCESS_KEY, NCLOUD_SECRET_KEY, webhook_url를 암호화합니다.
VPC와 서브넷을 설정한 후 생성합니다. 주의할 점은 Cloud Functions는 현재 KR-2 Private Subnet에서만 생성됩니다. 또한 Slack은 공인망 통신이 필요하기 때문에 NAT Gateway가 필수적으로 필요합니다.
3. Cloud Functions Trigger 생성하기
Cloud Functions을 업데이트하기 전에는 Trigger를 액션과 별개로 생성할 수 있었지만 이제는 반드시 액션을 생성한 후 트리거를 추가하도록 변경되었습니다. (이 부분은 조금 불편합니다.)
신규 생성 - Cron - 다음을 선택합니다.
테스트를 위해 다음과 같이 1분 간격으로 실행하게 끔 진행합니다. (그냥 Cloud Functions 실행으로 하셔도 괜찮습니다.)
다음과 같이 서버 이미지가 설정한 포맷대로 나오는 것을 확인할 수 있습니다.
Cron 정보 설정을 다음과 같이 0 3 1 * *으로 바꾸어 매달 1일에 실행되게 끔 설정합니다.
이번 시간에는 NCLOUD API와 Cloud Functions을 이용하여 KVM 하이퍼바이저 서버에서 서버 이미지를 자동으로 관리하는 방법을 알아봤고 이 방법을 이용하면 아주 저렴한 비용으로 서버 이미지와 블록 스토리지 스냅샷을 손쉽게 관리할 수 있다는 장점이 있습니다.
비교적 커스텀도 굉장히 자유로우니 한 번 구성해 보신 후 사용자 입맛에 맞게 끔 변경하면 좋을 것 같습니다.
감사합니다.
'Cloud > Naver Cloud' 카테고리의 다른 글
[NCLOUD] 기업 환경에서 VPN Full Tunneling과 Split Tunneling에 대해 알아보기 (0) | 2025.02.18 |
---|---|
[NCLOUD] Rocky Linux 8.10에서 Docker 설치하는 방법 알아보기 (0) | 2024.12.04 |
[NCLOUD] 하이브리드 클라우드 환경에서 통신 테스트하는 방법 알아보기 (0) | 2024.10.31 |
[NCLOUD] 서버 오류 시 서버 접근 제어 콘솔이 비활성화 되어 있을 때 조치 가이드 (1) | 2024.10.28 |
[NCLOUD] Cloud Functions과 Slack을 이용한 나만의 비용 관리 봇 생성하기 (0) | 2024.09.30 |
클라우드, 개발, 자격증, 취업 정보 등 IT 정보 공간
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!