
VPS(Virtual Private Server) : 물리적인 서버 한 대를 가상화 기술로 쪼개어, 마치 여러 대의 독립된 서버인 것처럼 사용자에게 빌려주는 서비스| 단계 | 주제 | 목표 |
|---|---|---|
| 1단계 | 멀티 테넌트 네트워크 최적화 | 사용자별 네트워크 격리(VLAN/VXLAN) 및 외부 인터넷 통신(Floating IP) 환경 완벽 구축 |
| 2단계 | 클라우드 모니터링 시스템 구축 | 오픈스택 내부 자원(CPU, RAM, Traffic) 실시간 감시 및 Prometheus, Grafana 연동 |
| 3단계 | 리소스 제한 및 쿼터 관리 | 제한된 노트북 환경에서 특정 사용자의 자원 독점 방지를 위한 Quota 설정 |
| 단계 | 주제 | 목표 |
|---|---|---|
| 1단계 | OpenStack API 연동 및 프로비저닝 자동화 | Python SDK를 이용한 인스턴스 생명주기 제어 및 커스텀 UI 연동 |
| 2단계 | 네트워크 아키텍처 및 접속 제어 구현 | 테넌트 격리, Floating IP 및 DNAT(포트포워딩) 자동화 로직 구현 |
| 3단계 | SLA 기반 운영 모니터링 고도화 | 가용성 및 성능 지표 정의, Grafana를 활용한 서비스 상태 시각화 |
from openstack.connection import Connection
class OpenStackManager:
# clouds.yaml을 사용하여 인증 정보 외부화
# 보안 및 유지보수를 위해 SDK의 Connection 프로토콜 활용
def __init__(self, cloud_name='my-openstack'):
self.conn = openstack.connect(cloud=cloud_name)
def get_network_info(self):
# Neutron API를 호출해 현재 활성화된 네트워크와 서브넷 조회
networks = list(self.conn.network.networks())
return networks
def get_compute_quotas(self, project_name='admin'):
# Nova API를 호출해 프로젝트의 CPU/RAM 할당량 확인
# 사용량 요약 기능을 위한 데이터 소스
project = self.conn.identity.find_project(project_name)
limits = self.conn.compute.get_limits(project=project.id)
return limits.absolute
if __name__ == "__main__":
manager = OpenStackManager()
print("--- Networks ---")
for net in manager.get_network_info():
print(f"Network Name: {net.name}, ID: {net.id}")
--- Networks ---
Network Name: shared_net, ID: 4d393aff-910e-44c6-9b2e-31a87ca3320e
Network Name: database_net, ID: 4f15933f-65f1-4d2c-99dd-885a91675985
Network Name: client_net, ID: 5fb54755-942e-4ebc-9823-4096b8c55243
Network Name: ext_net, ID: 68dcfc55-ed46-4462-85f9-6eee681702c8
Network Name: lb-mgmt-net, ID: 81fd0f08-0d82-4177-98f2-c3b792f59b50
Network Name: private_net, ID: 95096954-5810-40a1-9b3e-6870b1a09789
Network Name: application_net, ID: f71c1b5f-a229-4980-b188-fd1266b4411e
def create_vps_with_access(self, instance_name, network_name, image_name, flavor_name, key_name):
def create_vps_with_access(self, instance_name, network_name, image_name, flavor_name, key_name):
# 인스턴스 생성 후 접속용 Floating IP까지 자동 매핑
try:
image = self.conn.compute.find_image(image_name)
flavor = self.conn.compute.find_flavor(flavor_name)
networks = list(self.conn.network.networks(name=network_name))
if not networks:
raise Exception(f"Network '{network_name}'을 찾을 수 없습니다.")
network_id = networks[0].id
server = self.conn.compute.create_server(
name=instance_name,
image_id=image.id,
flavor_id=flavor.id,
networks=[{"uuid": network_id}],
key_name=key_name
)
# 인스턴스가 ACTIVE 상태가 될 때까지 대기
server = self.conn.compute.wait_for_server(server)
# Floating IP 할당 및 연결 (외부 네트워크에서 IP 하나를 가져옴)
ext_nets = list(self.conn.network.networks(name="ext_net"))
if ext_nets:
# 인스턴스에 연결된 포트(Port) ID 찾기
ports = list(self.conn.network.ports(device_id=server.id))
if not ports:
raise Exception("인스턴스에 연결된 네트워크 포트를 찾을 수 없습니다.")
port_id = ports[0].id
# floating IP 생성 및 포트 결합
fip = self.conn.network.create_ip(
floating_network_id=ext_nets[0].id,
port_id=port_id
)
fip_addr = fip.floating_ip_address
addresses = server.addresses.get(network_name, [])
fixed_ip = addresses[0]['addr'] if addresses else "N/A"
return {
"instance_id": server.id,
"fixed_ip": fixed_ip,
"floating_ip": fip_addr
}
except Exception as e:
print(f"[Internal Error] {e}")
raise e
--- [테스트] portfolio-vps-01 생성 시작 ---
--- [성공] 인스턴스 정보 ---
ID: 044c7b8a-a001-4dc9-9165-5144c71b5da9
Fixed IP: 172.20.0.140
Floating IP: 192.168.35.215
server.id)가 아니라, 서버에 꽂힌 랜카드(port_id)를 타겟팅한다.conn.compute(Nova)에서 conn.network(Neutron)로 명령의 주체가 바뀌었다.add_floating_ip_to_server -> create_ip(port_id=port_id)[실패] 에러 발생: NotFoundException: 404: Client Error for url: http://192.168.35.200:8774/v2.1/servers/cf991ee4-acc1-4faf-85d3-402230a1aacb/action, The resource could not be found.정리 : nova api v2.1에서는 add_floating_ip 메서드가 지원되지 않아 404 not found 에러가 발생했고, 이는 인프라 내에서 컴퓨팅과 네트워크 역할이 분리되어 있음을 알 수 있다. UUID를 기반으로 neutron port를 추적하고 neutron api를 호출하여 포트와 floating ip를 결합하는 방식으로 변경했다.
# Before 코드
# 외부 네트워크에서 IP 객체를 먼저 생성
fip = self.conn.network.create_ip(floating_network_id=ext_nets[0].id)
# Nova API를 호출하여 서버(인스턴스)에 IP 연결 시도 -> 404 에러 발생
self.conn.compute.add_floating_ip_to_server(server.id, fip.floating_ip_address)
##############################################################################
# After 코드
# 1. 인스턴스(server.id)와 연결된 실제 네트워크 '포트' ID를 조회
ports = list(self.conn.network.ports(device_id=server.id))
port_id = ports[0].id
# 2. Neutron API를 사용하여 IP 생성과 동시에 포트에 결합 (Atomic 작업)
fip = self.conn.network.create_ip(
floating_network_id=ext_nets[0].id,
port_id=port_id # 서버 ID 대신 포트 ID를 사용
)
def create_security_group_with_rules(self, sg_name):
# 전용 보안 그룹 생성 및 필수 규칙(SSH, ICMP) 추가
# 기존 보안 그룹 확인
existing_sg = self.conn.network.find_security_group(sg_name)
if existing_sg:
return existing_sg
# 보안 그룹 생성 및 규칙 추가
print(f"--- 보안 그룹 {sg_name} 생성 중... ---")
sg = self.conn.network.create_security_group(name=sg_name)
self.conn.network.create_security_group_rule(
security_group_id=sg.id,
direction='ingress',
ethertype='IPv4',
protocol='tcp',
port_range_min=22,
port_range_max=22
)
self.conn.network.create_security_group_rule(
security_group_id=sg.id,
direction='ingress',
ethertype='IPv4',
protocol='icmp'
)
return sg
def get_instance_cpu_usage(prometheus_url, instance_ip):
# 프로메테우스 API를 호출해 특정 인스턴스의 CPU 사용량 조회
query = f'100 - (avg by (instance) (irate(node_cpu_seconds_total{{instance=~"{instance_ip}:.*", mode="idle"}}[5m])) * 100)'
try:
response = requests.get(f"{prometheus_url}/api/v1/query", params={'query': query})
result = response.json()
if result['data']['result']:
usage = result['data']['result'][0]['value'][1]
return round(float(usage), 2)
except Exception as e:
print(f"Monitoring Error : {e}")
return None
# 테스트 코드 실행 결과
--- 보안 그룹 sg-test-vps-sg 생성 중... ---
생성 완료! Floating IP: 192.168.35.211
인스턴스(192.168.35.221) 현재 CPU 사용률: No Data%
문제 : 보안 그룹 생성 및 인스턴스에 할당은 정상적으로 되지만, 모니터링이 되지 않는다.
# 현재 prometheus.yml 파일 내용 (수정 전)
global:
scrape_interval: 10s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['localhost:9100']
- job_name: 'openstack-exporter'
scrape_interval: 60s
scrape_timeout: 50s
static_configs:
- targets: ['localhost:9180']
prometheus.yml 설정은 localhost(프로메테우스가 설치된 서버 본체)의 데이터만 수집하도록 되어 있어 있다.static_configs에 인스턴스의 IP가 등록되어 있어야 한다. 프로메테우스는 명시적으로 등록된 대상이 아니면 데이터를 가져오지 않는다.조치 전 테스트 (인스턴스 내부에 익스포터가 있을 때 메트릭 수집이 되는지 확인)
Ubuntu로 인스턴스 생성 후 9100번 포트 개방 및 create_security_group_with_rules 메서드에서도 9100 포트 허용하는 규칙 추가
Ubuntu 인스턴스 내에서 node_exporter 실행
# 1. node_exporter 다운로드
wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz
# 2. 압축 해제
tar xvfz node_exporter-1.7.0.linux-amd64.tar.gz
cd node_exporter-1.7.0.linux-amd64
# 3. 익스포터 실행
./node_exporter
prometheus.yml 파일 수정
# ~/openstack-monitoring/prometheus.yml
scrape_configs:
# ... 기존 설정 유지 ...
- job_name: 'vps-instances'
static_configs:
- targets: ['192.168.35.215:9100']
# 192.168.35.215 IP를 가진 인스턴스의 CPU 사용량 확인
100 - (avg by (instance) (irate(node_cpu_seconds_total{instance="192.168.35.221:9100", mode="idle"}[1m])) * 100)
packer-vps/
├── ubuntu-vps.pkr.hcl # Packer 메인 설정 파일
└── scripts/
└── setup-exporter.sh # node_exporter 설치 및 서비스 등록 스크립트
--------------------------------------------------------------------------
# setup-exporter.sh
#!/bin/bash
set -e
# 1. node_exporter 다운로드 및 배치
export VERSION="1.7.0"
wget https://github.com/prometheus/node_exporter/releases/download/v${VERSION}/node_exporter-${VERSION}.linux-amd64.tar.gz
tar xvfz node_exporter-${VERSION}.linux-amd64.tar.gz
sudo mv node_exporter-${VERSION}.linux-amd64/node_exporter /usr/local/bin/
# 2. systemd 서비스 파일 생성 (인스턴스 시작 시 자동 실행 설정)
sudo tee /etc/systemd/system/node_exporter.service <<EOF
[Unit]
Description=Node Exporter
After=network.target
[Service]
User=root
ExecStart=/usr/local/bin/node_exporter
[Install]
WantedBy=multi-user.target
EOF
# 3. 서비스 활성화
sudo systemctl daemon-reload
sudo systemctl enable node_exporter
# ubuntu-vps.pkr.hcl
packer {
required_plugins {
openstack = {
version = ">= 1.0.0"
source = "github.com/hashicorp/openstack"
}
}
}
source "openstack" "ubuntu_vps" {
cloud = "my-openstack"
image_name = "ubuntu-22.04-monitoring-v1"
source_image = "57d47e9f-ff9a-42c9-ad77-2e268989fe9e"
flavor = "m1.small"
network_discovery_cidrs = ["10.0.0.0/24"]
floating_ip_network = "ext_net"
ssh_username = "ubuntu"
volume_size = 10
}
build {
sources = ["source.openstack.ubuntu_vps"]
# 스크립트 실행으로 환경 구성
provisioner "shell" {
script = "scripts/setup-exporter.sh"
}
Service Discovery (SD) 설정을 통해 이 과정을 자동화한다.role: instance : 오픈스택의 가상 머신(Nova) 정보를 가져오겠다는 설정identity_endpoint : Keystone API 주소를 입력하여 인증을 수행relabel_configs : 오픈스택 API가 넘겨주는 수많은 정보 중, 실제 접속해야 할 Floating IP(__meta_openstack_public_ip)를 프로메테우스의 수집 주소(__address__)로 변환 - job_name: 'openstack-vps-sd'
openstack_sd_configs:
- role: instance
region: RegionOne
identity_endpoint: http://192.168.35.100:5000/v3
username: admin
password: WdWx1qAjoRyEUYOOa2XMxuCINHkgjzYjehNI7bmX
project_name: admin
domain_name: Default
relabel_configs:
- source_labels: [__meta_openstack_public_ip]
target_label: __address__
replacement: '${1}:9100'
- source_labels: [__meta_openstack_instance_name]
target_label: instance_name
$ docker restart prometheus

source_labels: [__meta_openstack_instance_public_ip] => [__meta_openstack_public_ip]
relabel_configs에서 참조한 오픈스택 메타데이터 라벨에 값이 비어 있기 때문이다.__meta_openstack_public_ip임을 확인 및 코드 수정
def get_unified_dashboard_data(self, prometheus_url):
# 오픈스택 인스턴스 정보와 프로메테우스 메트릭 통합
unified_data = []
servers = self.conn.compute.servers()
for server in servers:
fixed_ip = "N/A"
floating_ip = "N/A"
# 모든 네트워크 정보를 돌며 Fixed와 Floating IP를 구분해서 추출
for net_name, addr_list in server.addresses.items():
for addr in addr_list:
if addr.get('OS-EXT-IPS:type') == 'floating':
floating_ip = addr['addr']
elif addr.get('OS-EXT-IPS:type') == 'fixed':
fixed_ip = addr['addr']
cpu_usage = self.get_instance_cpu_usage(prometheus_url, floating_ip)
unified_data.append({
"id": server.id,
"name": server.name,
"status": server.status,
"fixed_ip": fixed_ip,
"floating_ip": floating_ip,
"cpu": cpu_usage,
"created_at": server.created_at
})
return unified_data
# flask main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from openstack_driver import OpenStackManager
from fastapi.responses import FileResponse
app = FastAPI(title="KHS Private Cloud Portal")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
manager = OpenStackManager()
PROM_URL = "http://192.168.35.100:9090"
@app.get("/")
async def read_index():
return FileResponse('index.html')
@app.get("/api/dashboard")
async def get_dashboard():
try:
data = manager.get_unified_dashboard_data(PROM_URL)
return data
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/instance")
async def create_instance(name: str):
try:
result = manager.create_vps_with_access(
instance_name=name,
network_name="shared_net",
image_name="ubuntu-22.04-monitoring-v1",
flavor_name="m1.small",
key_name="khs-main_keypair"
)
return {"message": "Success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
---------------------------------------------------------------
$ pip insatll fastapi uvicorn
# Python module 방식으로 서버 실행
$ python -m uvicorn main:app --reload
# Before Code (get_unified_dashboard_data 함수)
for server in servers:
addresses = server.addresses.get('shared_net', [])
ip = addresses[0]['addr'] if addresses else "N/A"
cpu_usage = self.get_realtime_cpu_usage(prometheus_url, ip)
unified_data.append({
"id": server.id,
"name": server.name,
"status": server.status, # ACTIVE, ERROR, BUILD 등
"ip": ip,
"cpu": cpu_usage,
"created_at": server.created_at
})
# After Code
for net_name, addr_list in server.addresses.items():
for addr in addr_list:
if addr.get('OS-EXT-IPS:type') == 'floating':
floating_ip = addr['addr']
elif addr.get('OS-EXT-IPS:type') == 'fixed':
fixed_ip = addr['addr']
addresses[0]['addr']은 네트워크 리스트의 첫 번째 항목인 Fixed IP(내부망)을 가져온다. 하지만 프로메테우스는 Floating IP(외부망)을 기준으로 데이터를 수집하고 있기 때문에 No Data가 뜨는 상황.OS-EXT-IPS:type 속성을 검사하여 floating 타입의 IP만 골라낸다.server.addresses.items() : 오픈스택 인스턴스에 할당된 모든 네트워크 정보를 담고 있는 딕셔너리.{네트워크 이름: [주소 정보 리스트]} 형태의 쌍을 반환fixed (고정 IP) : 인스턴스가 생성될 때 가상 네트워크(Neutron)로부터 할당받는 사설 IP. 인스턴스의 실제 vNIC(랜카드)에 설정되는 주소.floating (유동/플로팅 IP) : 외부망(Public Network)에서 가져온 공인/외부 IP. 인스턴스에 직접 설정되는 것이 아니라, 오픈스택의 L3 가상 라우터에서 1:1 NAT 방식으로 Fixed IP와 매핑.{
"shared_net": [
{
"addr": "172.20.0.141",
"version": 4,
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:xx:xx:xx",
"OS-EXT-IPS:type": "fixed"
},
{
"addr": "192.168.35.221",
"version": 4,
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:xx:xx:xx",
"OS-EXT-IPS:type": "floating"
}
],
"another_net": [...]
}
# sqlite3를 사용해서 간단한 DB 구성
def init_db():
conn = sqlite3.connect('cloud_portal.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE, # 로그인 ID
project_id TEXT, # 오픈스택 내 테넌트(Project)의 UUID, 자원 격리의 단위
network_id TEXT, # 해당 사용자가 사용할 VXLAN 기반 가상 네트워크의 ID
# 이 ID가 다르면 같은 IP 대역을 써도 패킷이 VNI로 격리된다
project_name TEXT
)
''')
conn.commit()
conn.close()
오픈스택에서 네트워크를 생성할 때 --share 옵션을 주면 모든 프로젝트가 해당 네트워크를 볼 수 있고 인스턴스를 연결할 수 있다. 옵션을 주지 않으면 해당 네트워크를 만든 프로젝트(default admin) 내부에서만 보이고 사용 가능하다.
각 프로젝트의 네트워크가 같은 대역을 사용하더라도, 물리적인 전송 단계에서는 서로 다른 VNI(VXLAN Network Identifier) 태그가 붙어 캡슐화된다.
L3 물리 망에서는 충돌이 일어나지 않고, 각 프로젝트의 가상 라우터가 이를 독립적으로 처리한다.
멀티 테넌시 테스트를 위해 Project_A라는 별도의 프로젝트를 만들고, 프로젝트 내에서 네트워크와 서브넷을 생성
# 새로운 프로젝트 생성
openstack project create --domain Default Project_A
# 프로젝트 전용 네트워크 생성 (--project 옵션 사용)
openstack network create --project Project_A Net_A
# 서브넷 생성 (IP 대역 설정)
openstack subnet create --project Project_A --network Net_A --subnet-range 10.0.0.0/24 Subnet_A
# 네트워크 확인
openstack network show Net_A
# 유저별 인스턴스 생성을 위한 유저 목록 반환 API
# main.py
@app.get("/api/users")
async def get_users():
try:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
users = cursor.execute('SELECT username, project_name FROM users').fetchall()
conn.close()
return [dict(u) for u in users]
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
배포 실패: OpenStackManager.create_vps_with_access() got an unexpected keyword argument 'network_id'network_id를 쓰는 이유self.conn.network.networks(name=network_name)에서 network_name 변수 사용def create_vps_with_access(self, instance_name, network_id, image_name, flavor_name, key_name):
# 인스턴스 생성 후 접속용 Floating IP까지 자동 매핑
try:
# 자원 찾기
image = self.conn.compute.find_image(image_name)
flavor = self.conn.compute.find_flavor(flavor_name)
# network_name 검색 제거 (이미 network_id를 파라미터로 받음)
# 보안 그룹 생성
sg_name = f"{instance_name}-sg"
sg = self.create_security_group_with_rules(sg_name)
# 서버 생성
server = self.conn.compute.create_server(
name=instance_name,
image_id=image.id,
flavor_id=flavor.id,
networks=[{"uuid": network_id}], # 전달받은 network_id 사용
key_name=key_name,
security_groups=[{"name": sg.name}]
)
# ACTIVE 상태 대기
server = self.conn.compute.wait_for_server(server)
# Floating IP 처리 초기화
fip_addr = "N/A"
ext_nets = list(self.conn.network.networks(name="ext_net"))
if ext_nets:
ports = list(self.conn.network.ports(device_id=server.id))
if ports:
port_id = ports[0].id
fip = self.conn.network.create_ip(
floating_network_id=ext_nets[0].id,
port_id=port_id
)
fip_addr = fip.floating_ip_address
# Fixed IP 추출 (네트워크 이름을 몰라도 첫 번째 사설 IP를 가져옴)
fixed_ip = "N/A"
for net_addresses in server.addresses.values():
for addr in net_addresses:
if addr.get('OS-EXT-IPS:type') == 'fixed':
fixed_ip = addr['addr']
break
return {
"instance_id": server.id,
"fixed_ip": fixed_ip,
"floating_ip": fip_addr
}
except Exception as e:
print(f"[Internal Error] {e}")
raise e
배포 실패: NotFoundException: 404: Client Error for url: http://192.168.35.200:9696/v2.0/floatingips, External network 68dcfc55-ed46-4462-85f9-6eee681702c8 is not reachable from subnet 3449ea8c-0cb2-49a3-86a5-ff25e85cc433. Therefore, cannot associate Port 7a7958f0-68f2-4d52-80ac-87d38770d614 with a Floating IP.# Router 생성 및 외부망을 게이트웨이로 설정하고, 사설 서브넷을 인터페이스로 추가
openstack router create khs-router # 라우터 생성
openstack router set --external-gateway ext_net khs-router # 외부망 연결
openstack router add subnet khs-router 3449ea8c-0cb2-49a3-86a5-ff25e85cc433 # 인터페이스 추가project_id)로 명시하거나 해당 프로젝트의 스코프를 변경한 연결을 사용# main.py
@app.post("/api/instances")
async def create_instance(name: str, username: str):
...
try:
result = manager.create_vps_with_access(
instance_name=name,
project_id=user['project_id'], # DB에서 가져온 project_id 추가
network_id=user['network_id'],
image_name="ubuntu-22.04-monitoring-v1",
flavor_name="m1.small",
key_name="khs-main-keypair"
)
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
# openstack_driver.py
# self.conn 부분을 target_conn. 으로 변경하여 자원 생성의 주체를 해당 프로젝트로 명시
def create_vps_with_access(self, instance_name, project_id, network_id, image_name, flavor_name, key_name):
try:
target_conn = self.conn.connect_as(project_id=project_id)
print(f"[*] 프로젝트 스코핑 완료: {project_id}")
image = target_conn.compute.find_image(image_name)
flavor = target_conn.compute.find_flavor(flavor_name)
if not image or not flavor:
raise Exception("이미지 또는 플래버를 찾을 수 없습니다.")
sg_name = f"{instance_name}-sg"
sg = self.create_security_group_with_rules_in_project(target_conn, sg_name)
print(f"[*] 인스턴스 생성 시작: {instance_name} (Network: {network_id})")
server = target_conn.compute.create_server(
name=instance_name,
image_id=image.id,
flavor_id=flavor.id,
networks=[{"uuid": network_id}],
key_name=key_name,
security_groups=[{"name": sg.name}]
)
...
def create_security_group_with_rules_in_project(self, target_conn, sg_name):
...
target_conn은 내부적으로 "이 프로젝트의 자격으로 일하겠다"는 토큰을 발급받는다. 이 토큰을 사용하면 생성되는 모든 객체의 project_id 필드가 자동으로 채워진다.
get_dashboard 함수 수정 : 오픈스택에서 가져온 인스턴스의 project_id를 DB에 등록된 username으로 치환하여 프론트엔드에 전달# main.py
@app.get("/api/dashboard")
async def get_dashboard():
try:
# DB에서 모든 유저 정보 가져오기 (매핑용)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
db_users = conn.execute('SELECT username, project_id, project_name FROM users').fetchall()
conn.close()
user_map = {u['project_id']: u['username'] for u in db_users}
project_name_map = {u['project_id']: u['project_name'] for u in db_users}
# 오픈스택에서 모든 프로젝트의 인스턴스 가져오기
all_instances = manager.get_unified_dashboard_data(PROM_URL)
# 인스턴스 데이터에 유저 정보 입히기
for inst in all_instances:
p_id = inst.get('project_id')
inst['owner'] = user_map.get(p_id, "Unknown")
inst['project_display_name'] = project_name_map.get(p_id, "Admin/Other")
return all_instances
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# openstack_driver.py
def get_unified_dashboard_data(self, prometheus_url):
try:
# all_projects=True 옵션을 주어야 다른 프로젝트의 인스턴스가 보인다.
instances = list(self.conn.compute.servers(all_projects=True))
...
# 반환 데이터에 project_id가 포함되어야 함
dashboard_data = []
for server in instances:
dashboard_data.append({
"instance_id": server.id,
"name": server.name,
"status": server.status,
"project_id": server.project_id, #
"fixed_ip": self._get_fixed_ip(server),
"floating_ip": self._get_floating_ip(server),
"cpu": self._get_cpu_metric(server.id, prometheus_url)
...
# openstack_driver.py
def delete_instance(self, instance_id):
try:
server = self.conn.compute.get_server(instance_id)
project_id = server.project_id
instance_name = server.name
sg_name = f"{instance_name}-sg"
target_conn = self.conn.connect_as(project_id=project_id)
# floating IP 정리
ports = list(target_conn.network.ports(device_id=instance_id))
for port in ports:
fips = list(target_conn.network.ips(port_id=port.id))
for fip in fips:
print(f"Floating IP 제거 중 : {fip.floating_ip_address}")
target_conn.network.delete_ip(fip.id)
# 인스턴스 삭제
print(f"인스턴스 삭제 요청 : {instance_id}")
target_conn.compute.delete_server(instance_id)
# 보안그룹 삭제 전 인스턴스 삭제 대기
target_conn.compute.wait_for_delete(server, wait=25)
# 보안그룹 삭제
sg = target_conn.network.find_security_group(sg_name)
if sg:
print(f"인스턴스와 연결된 보안그룹 삭제 중 : {sg_name}")
target_conn.network.delete_security_group(sg.id)
print("[ 모든 자원 정리 완료] ")
return True
except Exception as e:
print(f"인스턴스 삭제 오류 : {e}")
raise e
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
# main.py
@app.delete("/api/instances/destroy/{instance_id}")
async def delete_instance(instance_id: str):
try:
manager.delete_instance(instance_id)
return {"status": "success", "message": "Instance deletion complete"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
setup_tenant_infrastructure 함수멀티 테넌시 환경에서의 메트릭 수집은 프로메테우스 서버가 외부 네트워크에 할당된 인스턴스의 Floating IP 주소를 수집 엔드포인트로 식별하여 요청을 전송하는 방식으로 이루어진다.
해당 트래픽은 각 테넌트 프로젝트 가상 라우터의 목적지 네트워크 주소 변환(DNAT)를 거쳐 인스턴스의 Fixed IP로 라우팅된다. 이는 L3 계층에서 네트워크 격리를 유지하며 외부 관제 시스템과 통신을 보장한다.
# prometheus.yml
- job_name: 'openstack-vps-sd'
openstack_sd_configs:
- role: instance
region: RegionOne
identity_endpoint: http://192.168.35.100:5000/v3
username: admin
password:
project_name: admin
all_tenants: true # <------------
domain_name: Default
relabel_configs:
- source_labels: [__meta_openstack_public_ip]
target_label: __address__
replacement: '${1}:9100'
- source_labels: [__meta_openstack_instance_name]
target_label: instance_name
- source_labels: [__meta_openstack_instance_id]
target_label: openstack_instance_id
이미 노트북에 설치되어 있는 Cloudflare Tunnel을 사용하여 외부에서도 대시보드에 접속할 수 있게 한다.
# main.py 수정
# *.khs-server.cloud 외부 도메인을 통해 접속한 브라우저가 대시보드 안의 API를 호출할 때,
# 보안상의 이유로 브라우저가 차단하는 것을 방지하기 위해 CORS 설정 업데이트
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://hcloud.khs-server.cloud",
"http://localhost:8000", # 로컬 테스트용
"http://localhost:8070",
"http://192.168.35.100:8070"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# dockerfile
FROM python:3.10-slim
WORKDIR /app
RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8070", "--proxy-headers"]
# docker-compose.yaml
services:
hcloud-dashboard:
image: hcloud-dashboard:latest
container_name: hcloud-portal
network_mode: "host"
volumes:
- ./cloud_portal.db:/app/cloud_portal.db # 로컬 DB 파일 컨테이너와 동기화
- ~/.config/openstack:/root/.config/openstack:ro # Openstack 인증 파일 마운트
environment:
- TZ=Asia/Seoul
restart: always
트러블 슈팅
docker compose up -d --build
Dockerfile:5
--------------------
3 | WORKDIR /app
4 |
5 | >>> RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*
6 |
7 | COPY requirements.txt .
--------------------
failed to solve: process "/bin/sh -c apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*" did not complete successfully: network bridge not found
docker build --network=host -t hcloud-dashboard .
docker compose up -d --build--network=host를 사용하면 빌드 컨테이너가 Docker의 가상 네트워크 브리지를 생성하지 않고, 호스트의 네트워크 스택(Network Stack)을 그대로 공유한다.network_mode: "host"를 설정하면 컨테이너 내부 프로세스가 호스트의 네트워크 네임스페이스에서 실행된다. 즉, 컨테이너 내의 FastAPI가 localhost:9090(Prometheus)을 호출하면 별도의 포트 포워딩 없이 호스트의 9090 포트로 직접 연결된다.방법 1 :
Apache Guacamole를 이용해서 외부 사용자가 별도의 설정없이 브라우저에서 SSH 접속
=> 외부 사용자 -> ssh.khs-server.cloud(HTTPS) -> Guacamole -> 인스턴스(SSH)
=> 원리 : Guacamole 웹 페이지 접속 -> 웹 브라우저와 Guacamole 서버는 WebSocket으로 통신 -> 서버 내부의 guacd라는 프로세스가 실제로 인스턴스에 SSH 접속을 수행 -> guacd가 받은 터미널 화면 정보를 Guacamole 웹 서버가 랜더링
=> 장점 : 사용자가 SSH 클라이언트 없고, 인스턴스의 IP를 직접 노출하지 않아도 된다.
방법 2 :Cloudflare Tunnel (Zero Trust / Warp)
=> 장점 : 이미 호스트 노트북에 컨테이너로 떠있는 터널을 활용할 수 있다. / L4 트래픽 터널링 기술 이해
=> 단점 : 외부 사용자도 cloudflare warp를 설치해야 한다.
방법 3 :Tailscale을 설치하고, Subnet Router 기능을 활성화
=> 외부 사용자가 Tailscale 망에 초대되어 접속하고 인스턴스 floating IP로 직접 SSH 접속
=> 장점 : 네트워크 복잡도가 낮고, 모든 포트를 한 번에 개방할 수 있다.
Private Network Routing 기능 활성화
외부 사용자 세팅
트러블 슈팅 (=관리자 세팅 과정)

외부 사용자가 cloudflare Warp에 팀 이름을 입력했을 때 이메일 입력 폼이 뜨지 않고, 사진처럼 enrollment request is invalid 에러 페이지가 뜬다.
로그인 방식 확인 (사용자들이 어떤 방식으로 인증할지 결정 : 이메일 OTP 방식 사용)
Zero Trust - Access controls - Access settings - Login methodsOne-time PIN(이메일 OTP) 등록 후 선택
기기 등록 정책(Device Enrollment) 생성
Zero Trust - Team & Resources - Devices - Management - Device enrollment permissions (Manage) - Policies (Add a policy) - Select existing policies (생성한 Policy 선택)



이메일 확인 후 Warp 연결은 됐으나 SSH 연결이 안 되는 상황
Zero Trust - Team & Resources - Devices - Device profiles - Default (Edit) - Split Tunnels (Manage) - 192.168.0.0/16 삭제


# 인스턴스에 SSH 연결
$ ssh -i test_user_key.pem ubuntu@192.168.35.207
def get_instance_metrics(...):
queries = {
"cpu": f'100 - (avg by (instance) (irate(node_cpu_seconds_total{{instance=~"{instance_ip}:.*", mode="idle"}}[1m])) * 100)',
"memory": f'(1 - (node_memory_MemAvailable_bytes{{instance=~"{instance_ip}:.*"}} / node_memory_MemTotal_bytes{{instance=~"{instance_ip}:.*"}})) * 100',
"disk_read": f'sum(rate(node_disk_read_bytes_total{{instance=~"{instance_ip}:.*"}}[1m]))',
"disk_write": f'sum(rate(node_disk_written_bytes_total{{instance=~"{instance_ip}:.*"}}[1m]))'
}
for key, q in queries.items():
try:
response = requests.get(f"{prometheus_url}/api/v1/query", params={'query': q}, timeout=2)
data = response.json()
if data['status'] == 'success' and data['data']['result']:
val = float(data['data']['result'][0]['value'][1])
# 디스크 I/O는 KB/s 단위로 변환, 나머지는 소수점 둘째자리 반올림
results[key] = round(val / 1024, 2) if "disk" in key else round(val, 2)
else:
results[key] = "No Data" if "disk" not in key else 0
def get_unified_dashboard_data(...):
unified_data.append({
...
"cpu": metrics["cpu"],
"memory": metrics["memory"],
"disk_read": metrics["disk_read"],
"disk_write": metrics["disk_write"],
...
})
시스템 부하 (Load) : 1분간 평균 부하, 코어 수 대비 Load가 높다면 VM 스케줄링 지연 발생 가능성이 높다.CPU 사용률(%) : 전체 CPU 중 idle 상태를 제외한 비중을 확인해 80~90%가 지속되면 물리 노드의 확장(Scale-out)이나 VM 재배치가 필요한 시점이다.메모리 가용률(%) : 단순 캐시를 제외하고 OS와 애플리케이션이 즉시 할당 가능한 메모리 비중을 확인한다. 10% 미만일 경우 OOM Killer 작동하여 핵심 프로세스를 죽일 수 있다.스토리지 병목 : 초당 읽기/쓰기 횟수를 합산하여 디스크 하드웨어가 감당할 수 있는 한계치(IOPS)에 도달했는지 확인한다. 특정 VM의 비정상적인 로그 쓰기나 DB 부하를 잡을 수 있다.시스템 파티션 안정성 : /etc/hosts가 위치한 마운트 포인트(보통 루프 파티션 /)의 여유 공간을 확인한다. 로그 파일이나 이미지 임시 파일로 인해 디스크가 100% 차버리면 노드 전체가 Read-only 상태가 되거나 멈춘다.대역폭 포화 및 트래픽 이상 감지 : lo(로컬), veth(컨테이너 가상 인터페이스) 등을 제외한 실제 물리 NIC의 트래픽만 합산한다. VM 간의 대규모 데이터 이동이나 외부 공격(DDoS)으로 인한 대역폭 포화를 감시한다.def get_host_resource_usage(self, prometheus_url):
host_label = 'instance=~"localhost:9100.*"'
queries = {
"cpu_load": f"node_load1{{{host_label}}}",
"cpu_usage": f"100 - (avg by (instance) (irate(node_cpu_seconds_total{{{host_label}, mode='idle'}}[1m])) * 100)",
"mem_usage": f"(1 - (avg by (instance) (node_memory_MemAvailable_bytes{{{host_label}}}) / avg by (instance) (node_memory_MemTotal_bytes{{{host_label}}}))) * 100",
"disk_usage": f"100 - (sum by (instance) (node_filesystem_avail_bytes{{{host_label}, mountpoint='/etc/hosts'}}) / sum by (instance) (node_filesystem_size_bytes{{{host_label}, mountpoint='/etc/hosts'}}) * 100)",
"net_throughput": f"(sum(rate(node_network_receive_bytes_total{{{host_label}, device!~'lo|veth.*|docker.*|br.*|tap.*'}}[1m])) + sum(rate(node_network_transmit_bytes_total{{{host_label}, device!~'lo|veth.*|docker.*|br.*|tap.*'}}[1m]))) * 8 / 1024 / 1024",
"disk_iops": f"sum(rate(node_disk_reads_completed_total{{{host_label}}}[1m])) + sum(rate(node_disk_writes_completed_total{{{host_label}}}[1m]))"
}
...
| 지표명 | 모니터링 필요성 (Why) | 활용 시나리오 (When) |
|---|---|---|
| Host OS Load | 시스템이 처리할 수 있는 양보다 많은 작업이 대기 중인지 확인한다. | 수치가 CPU 코어 수보다 높으면 새로운 VM 생성을 제한하거나 배포 정책을 수정해야 한다. |
| Total CPU Usage | 실제 연산 자원이 얼마나 쓰이는지 측정하여 'Noisy Neighbor' 현상을 감지한다. | 특정 VM이 CPU를 독점하여 다른 VM의 성능을 저하시키는지 확인하고 할당량(Quota)을 조정한다. |
| Total RAM Usage | 메모리는 고갈 시 OOM(Out of Memory) Killer가 중요 프로세스를 강제 종료시킬 수 있는 치명적 자원이다. | 메모리 점유율이 90%를 넘으면 기존 VM을 최적화하거나 물리 메모리를 증설해야 하는 신호로 본다. |
| Root Disk Usage | 로그 파일, 이미지 캐시 등으로 인해 디스크가 꽉 차면 전체 클라우드 서비스가 중단된다. | 80% 도달 시 오래된 인스턴스 스냅샷이나 사용하지 않는 볼륨을 정리하는 관리 작업을 수행한다. |
| Network Throughput | 물리 랜카드의 대역폭 한계치에 도달했는지 감시하여 네트워크 지연을 방지한다. | 백업이나 대용량 데이터 전송 시 인스턴스들의 통신 속도가 느려지는 원인을 파악할 때 활용한다. |
| Disk IOPS | 디스크의 데이터 읽기/쓰기 횟수를 감시하여 성능 저하(I/O Wait)를 찾아낸다. | CPU 사용량은 낮은데 시스템이 버벅거릴 경우, 디스크의 물리적 한계를 확인하는 핵심 지표이다. |
# main.py
def run_ansible_setup(instance_name: str):
# 인스턴스가 ACTIVE여도 SSH 서비스가 뜰 수 있도록 대기
time.sleep(10)
try:
# --limit 옵션으로 생성된 인스턴스만 타겟
cmd = [
"ansible-playbook",
"-i", "inventory.py",
"init_setup.yaml",
"--limit", instance_name
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f" Ansible Success : [{instance_name}] 초기 설정 완료")
else:
print(f"Ansible Fail : [{instance_name}] 초기 설정 실패 : {result.stderr}")
# inventory.py
#!/usr/bin/env python3
def get_inventory():
# 소스 데이터 확보 : 앤서블이 관리해야 할 대상을 DB에서 가져온다.
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "cloud_portal.db")
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# instance_name을 hostname으로 사용하기 위해 조회
db_instances = cursor.execute("SELECT instance_id, username, instance_name FROM instances").fetchall()
conn.close()
manager = OpenStackManager()
# 앤서블 표준 포맷 생성 : 앤서블이 이해할 수 있는 JSON 구조의 틀
# '_meta' : 호스트별 변수(IP, 키 경로 등)를 저장
# 'openstack_nodes' : 그룹 이름. 앤서블 플레이북의 'hosts: openstack_nodes'와 매칭됨
inventory = {
'_meta': {'hostvars': {}},
'all': {'children': ['openstack_nodes']},
'openstack_nodes': {'hosts': []}
}
for row in db_instances:
try:
# 실시간 상태 확인 : DB에는 있지만 오픈스택에서 삭제되었을 수도 있으므로 API로 실시간 조회
servers = list(manager.conn.compute.servers(all_projects=True, uuid=row['instance_id']))
# 리스트가 비어있다면 앤서블에 알려줄 필요가 없으므로 스킵
if not servers: continue
server = servers[0]
# IP 추출 : 앤서블이 SSH 접속에 필요한 외부 IP(Floating IP) 추출
floating_ip = "N/A"
for net_name, addr_list in server.addresses.items():
for addr in addr_list:
if addr.get("OS-EXT-IPS:type") == "floating":
floating_ip = addr['addr']
if floating_ip == "N/A": continue
# 5. 호스트 변수 설정 : 앤서블이 대상 서버에 어떻게 접속할지 상세 정보를 넘겨줌
hostname = row['instance_name']
inventory['openstack_nodes']['hosts'].append(hostname)
inventory['_meta']['hostvars'][hostname] = {
'ansible_host': floating_ip, # 실제 접속할 IP 주소
'ansible_user': 'ubuntu', # 접속 계정 (Ubuntu 이미지 기본값)
# SSH 키 경로 : 절대 경로를 사용하여 도커 컨테이너 환경에서도 파일을 찾도록 설정
'ansible_ssh_private_key_file': os.path.join(BASE_DIR, "user_keys", f"{row['username']}_key.pem"),
# StrictHostKeyChecking=no: 처음 접속하는 서버의 지문(fingerprint)을 자동으로 수락
# 이게 없으면 앤서블이 "모르는 서버인데 믿어도 돼?"라고 물어보느라 멈춘다.
'ansible_ssh_common_args': '-o StrictHostKeyChecking=no',
# 플레이북에서 동적으로 사용할 변수 (호스트네임 동기화용)
'target_hostname': row['instance_name']
}
except Exception:
continue
return inventory
if __name__ == "__main__":
# 앤서블은 이 스크립트를 실행한 결과값(JSON)만 가져가서 사용
print(json.dumps(get_inventory()))
# init_setup.yaml
---
- name: OpenStack Instance Post-Deployment Setup
hosts: openstack_nodes # inventory.py에서 정의한 그룹 이름과 일치
become: yes # 패키지 설치를 위해 sudo 권한을 사용
tasks:
- name: 1. 패키지 리스트 업데이트 및 업그레이드
apt:
# update_cache: yes == apt update
update_cache: yes
# upgrade: dist== apt dist-upgrade
# 인프라 보안 패치를 한 번에 끝내기 위해 전체 OS 패키지를 최신 버전으로 업그레이드
upgrade: dist
# autoremove: yes == apt autoremove
# 업그레이드 과정에서 더 이상 필요 없어진 오래된 패키지들을 지워 디스크 용량 확보
autoremove: yes
- name: 2. OS 내부 호스트네임 설정 (DB 이름과 동기화)
hostname:
# inventory.py에서 넘겨준 target_hostname 변수를 사용하여 서버 실제 이름 변경
name: "{{ target_hostname }}"
- name: 3. /etc/hosts 파일에 호스트네임 반영
# lineinfile 모듈 : 파일 안의 특정 줄을 찾아서 수정하거나 추가
lineinfile:
path: /etc/hosts
# 127.0.1.1로 시작하는 줄을 찾아 교체
regexp: '^127.0.1.1'
line: "127.0.1.1 {{ target_hostname }}"
def get_cleanup_candidates(self, db_instance_ids):
cleanup_list = []
now = datetime.now(timezone.utc)
all_servers = list(self.conn.compute.servers(all_projects=True))
for server in all_servers:
created_at = datetime.fromisoformat(server.created_at.replace('Z', '+00:00'))
is_old = (now - created_at) > timedelta(hours=12) # 12시간 경과 체크
is_orphaned = server.id not in db_instance_ids # DB 미등록 체크
if is_old or is_orphaned:
cleanup_list.append({"type": "Instance", "id": server.id, "reason": "12시간 경과" if is_old else "유령 리소스"})
# port_id가 없는 미사용 Floating IP
all_fips = list(self.conn.network.ips())
for fip in all_fips:
if not fip.port_id:
cleanup_list.append({"type": "Floating IP", "id": fip.id, "reason": "미사용 IP"})
return cleanup_list

# docker-compose.yml
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./alert_rules.yml:/etc/prometheus/alert_rules.yml # 알림 규칙 파일
network_mode: "host"
restart: always
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
network_mode: "host"
volumes:
- /home/khs/openstack-monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
restart: always
# alert_rules.yml
groups:
- name: HostResourceAlerts
rules:
- alert: HostMemoryWarning
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "호스트 메모리 주의"
description: "현재 메모리 사용량 : {{ $value | printf \"%.2f\" }}%"
- alert: HostMemoryDanger
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
for: 2m
labels:
severity: critical
annotations:
summary: "호스트 메모리 고갈 위기"
description: "현재 메모리 사용량 : {{ $value | printf \"%.2f\" }}%"
- alert: HostDiskSpaceDanger
expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 10
for: 5m
labels:
severity: critical
annotations:
summary: "디스크 용량 고갈 위기"
description: "루트 파티션의 남은 용량 10% 미만"
- alert: HostHighLoad
expr: node_load1 > (count by(instance) (node_cpu_seconds_total{mode="idle"}) * 1.5)
for: 5m
labels:
severity: warning
annotations:
summary: "시스템 부하 지수 상승"
description: "현재 시스템 Load({{ $value }})가 코어 수 대비 높음. 처리 속도 저하 위험."
- alert: MonitoringExporterDown
expr: up{job="node-exporter"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "모니터링 에이전트 중단"
description: "대상({{ $labels.job }})이 응답하지 않음. 데이터 수집이 중단되었습니다."
- name: OpenStackAlerts
rules:
- alert: OpenStackServiceDown
expr: openstack_compute_service_state == 0
for: 1m
labels:
severity: critical
annotations:
summary: "오픈스택 서비스 장애"
description: "서비스({{ $labels.service }}) 상태 DOWN."
# alertmanager.yml
route:
group_by: ['alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 30m
receiver: 'discord'
receivers:
- name: 'discord'
discord_configs:
- webhook_url: 'discord_webhook-url'
# prometheus.yml
...
alerting:
alertmanagers:
- static_configs:
- targets: ['localhost:9093']
node_memory_MemAvailable_bytes : 커널이 새로운 프로세스에 즉시 할당할 수 있는 메모리 양.node_memory_MemTotal_bytes : 시스템의 전체 물리 메모리 크기.node_filesystem_avail_bytes{mountpoint="/"} : 루트 디렉토리에 마운트된 파일 시스템의 가용 바이트.node_load1 : 최근 1분간 실행 중이거나 실행을 대기하는 프로세스의 평균 수.count by(instance) (node_cpu_seconds_total{mode="idle"}) : 시스템의 전체 CPU 코어 수를 동적으로 계산.up : 프로메테우스가 대상을 성공적으로 스크레이핑했는지 나타내는 합성 메트릭. (1: 성공, 0: 실패)openstack_compute_service_state : 오픈스택 익스포터가 수집한 각 서비스의 상태 값. (1: Up, 0: Down)
Open vSwith(OVS)를 사용한다.VXLAN/VLAN : 주로 사용되는 터널링 기술로, 같은 Private IP 대역을 쓰더라도 VNI(VXLAN Network Identifier)를 통해 L2 레이어에서 패킷이 격리된다.OVS Bridges : 인스턴스는 br-int (Integration Bridge)에 연결되고, 외부와의 통신은 br-ex (External Bridge)를 통해 이루어진다.인터페이스 탭은 해당 라우터가 연결된 사설 서브넷 (Internal Subnet) 목록만 보여준다.add_interface_to_router 함수로 추가하며, 인터페이스 탭에 표시된다.external_gateway_info로 설정하며, 이는 인터페이스 목록이 아니라 라우터의 개요 탭에서 확인 가능하다.# 라우터 생성 혹은 업데이트 시 설정
router = self.conn.network.create_router(
name=router_name,
project_id=project_id,
external_gateway_info={"network_id": ext_net.id} # <-----
)
# 라우터에 서브넷을 연결
self.conn.network.add_interface_to_router(
router.id,
subnet_id=subnet.id # <------
)
Split Tunnel링 : WARP 클라이언트는 사용자 PC에서 나가는 모든 패킷의 목적지 IP를 감시한다.node_load1{instance="localhost:9100"}100 - (avg by (instance) (irate(node_cpu_seconds_total{mode='idle'}[1m])) * 100)(1 - (avg by (instance) (node_memory_MemAvailable_bytes) / avg by (instance) (node_memory_MemTotal_bytes))) * 100100 - (sum by (instance) (node_filesystem_avail_bytes{mountpoint='/etc/hosts'}) / sum by (instance) (node_filesystem_size_bytes{mountpoint='/etc/hosts'}) * 100)(sum(rate(node_network_receive_bytes_total{device!~'lo|...'}[1m])) + sum(rate(node_network_transmit_bytes_total{...}[1m]))) * 8 / 1024 / 1024sum(rate(node_disk_reads_completed_total[1m])) + sum(rate(node_disk_writes_completed_total[1m]))Play로 구성되며, 각 Play는 특정 호스트 그룹을 대상으로 수행할 Task들의 집합으로 이루어진다.