OCI(Oracle Cloud Infrastructure) 프리티어 환경에서 서버를 운영하다 보면 '메모리 부족'과 CPU 부하에 항상 신경을 쓰기 마련입니다.
단순히 htop으로 상황을 파악하는 것을 넘어, 왜 Grafana를 선택했는지와 그 과정에서 마주한 기술적 난관들을 정리했습니다.
1. 왜 Grafana인가? (htop vs OCI Monitoring vs Grafana)
서버 관리를 위해 가장 먼저 고려한 도구는 htop과 OCI Monitoring이었지만, 각각 명확한 한계가 있었습니다.
htop (수동의 한계): 실시간 확인은 유용하지만, 매번 터미널을 열고 SSH로 원격 접속해야 하는 번거로움이 있었습니다. 또한 과거의 지표를 기록하지 않아 장애 원인 분석에 불리했습니다.
OCI Monitoring (리소스의 부담): 오라클에서 제공하는 모니터링 플러그인은 편리하지만, 생각보다 메모리 점유율이 높았습니다. 저사양 VM에서는 이조차 부담스러웠기에, 더 가볍고 직관적인 커스텀 모니터링 환경이 필요했습니다.
플러그인을 비활성화하니 메모리 용량 확보와 CPU 사용량이 획기적으로 개선되었습니다.
Grafana & Prometheus (IaC 기반): 지표 수집(Prometheus)과 시각화(Grafana)를 분리하여 가볍게 운영할 수 있고, 무엇보다 코드로 인프라를 관리(IaC)할 수 있다는 점이 매력적이었습니다.
2. 아키텍처 설계: IaC 기반의 컨테이너 스택
단순 설치가 아닌, 언제든 동일한 환경을 재현할 수 있도록 Docker Compose와 설정 파일(YML)을 이용한 코드로 인프라를 정의했습니다.
🛠️ 모니터링 스택 구성 요소
Node Exporter: 호스트 서버의 CPU, Memory, Disk 등 하드웨어 지표를 추출.
# docker-compose.yml
node-exporter:
image: prom/node-exporter
container_name: node-exporter
restart: always
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
- "--path.rootfs=/rootfs"Prometheus: Exporter가 추출한 지표를 주기적으로 수집(Scrape)하고 저장.
# docker-compose.yml
prometheus:
image: prom/prometheus
container_name: prometheus
restart: always
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus # prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"] Grafana: 저장된 데이터를 쿼리하여 아름다운 대시보드로 시각화.
# docker-compose.yml
grafana:
image: grafana/grafana
container_name: grafana
restart: always
ports:
- "4001:4001"
environment:
- GF_SERVER_HTTP_PORT=4001
- GF_SECURITY_ADMIN_USER=donggyu
- GF_SECURITY_ADMIN_PASSWORD=donggyu
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning/datasources/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/dashboards:/etc/grafana/dashboards apiVersion: 1
providers:
- name: 'default'
type: file
updateIntervalSeconds: 10
disableDeletion: false
options:
path: /etc/grafana/dashboards 3. 기술적 구현: 설정 자동화(Provisioning)
Grafana를 매번 수동으로 설정하는 대신, Provisioning 기능을 활용해 컨테이너가 뜨자마자 데이터 소스와 대시보드가 자동으로 로드되도록 설정했습니다.
🛠️ 데이터 소스 IaC 관리
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true 🛠️ 프로비저닝(대시보드) IaC 관리
apiVersion: 1
providers:
- name: 'default'
type: file
updateIntervalSeconds: 10
disableDeletion: false
options:
path: /etc/grafana/dashboards # Dashboard.yml 대시보드 1860번 json 파일.
{
"apiVersion": "dashboard.grafana.app/v2",
"kind": "Dashboard",
"metadata": {
"name": "rYdddlPWk",
"namespace": "default",
"uid": "b5ffa21e-98ba-4c76-a266-5b54f5177c83",
"resourceVersion": "1776533074160997",
"generation": 1,
"creationTimestamp": "2026-04-18T17:24:34Z",
"labels": {
"grafana.app/deprecatedInternalID": "919858657472512"
},
"annotations": {
"grafana.app/createdBy": "user:cfjhbjl9chwqoe",
"grafana.app/folder": "",
"grafana.app/saved-from-ui": "Grafana v13.0.1 (a100054f)"
}
},
...............이 설정을 통해 그라파나 UI에서 일일이 클릭하며 DB를 연결할 필요 없이, docker-compose up -d 한 줄로 모든 준비가 끝납니다.
4. 적용 및 성과: 시각화의 힘
직접 대시보드를 그리느라 고생할 필요 없이, Grafana에서 제공하는 공식 템플릿(예: Node Exporter Full)을 활용하여 단번에 전문적인 관제 시스템을 갖췄습니다.
IaC의 효용 체감: 도커 컴포즈로 관리하니 환경 구축이 매우 간편해졌으며, 다른 서버로 확장할 때도 코드만 복사하면 즉시 동일한 모니터링 환경이 보장된다는 점에 큰 감탄을 느꼈습니다.
시각적 가시성: SSH 없이도 모바일이나 PC 웹 브라우저에서 RAM 점유율 추이를 그래프로 확인하며 선제적인 자원 관리가 가능해졌습니다.
5. 트러블슈팅: 도입 과정의 난관들
[Issue 1] 대시보드 반영 실패 및 경로 설정 오류
Grafana 볼륨을 등록했음에도 설정한 대시보드 JSON 파일이 UI에 나타나지 않는 문제가 발생했습니다. 확인 결과, 컨테이너 내부의 기본 경로와 볼륨 마운트 경로가 충돌하여 파일이 제대로 덮어씌워지지 않았습니다.
grafana:
image: grafana/grafana
container_name: grafana
restart: always
ports:
- "4001:4001"
environment:
- GF_SERVER_HTTP_PORT=4001
- GF_SECURITY_ADMIN_USER=donggyu
- GF_SECURITY_ADMIN_PASSWORD=donggyu
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning/datasources/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/dashboards:/var/lib/grafana/dashboards
volumes:
minio_data:
postgres_data:
prometheus_data:
grafana_data: # 이 부분으로 대시보드 설정이 덮어쓰여진다...해결:
/var/lib/grafana와 설정 경로를 명확히 분리하고, 대시보드 파일 위치를/etc/grafana/dashboards로 변경하여 Provider 설정과 일치시켰습니다.
# docker-compose.yml 수정
grafana:
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/dashboards:/etc/grafana/dashboards # ✅ /etc 경로로 일관성 유지
# dashboard_provider.yml
options:
path: /etc/grafana/dashboards # ✅ 마운트된 경로와 동일하게 설정[Issue 2] 권한 분쟁과 파일 복사 프로세스 개선
CI/CD 과정에서 curl을 통해 매번 설정 파일을 다운로드하는 방식은 관리가 어렵고 반복적이었습니다. 또한, 기존 폴더의 권한 문제로 인해 scp로 파일을 덮어쓰는 과정에서 에러가 잦았습니다.
해결: 파일 전송 전, SSH 액션을 통해 기존 설정 폴더를 깨끗하게 정리(
rm -rf)하고 권한을 조정하는 전처리 단계를 추가했습니다. 이후scp-action을 통해 로컬의 설정 파일을 통째로 전송하는 방식으로 단순화했습니다.
- name: Prepare and Copy Files
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.ORACLE_HOST }}
username: ${{ secrets.ORACLE_USER }}
key: ${{ secrets.ORACLE_SSH_KEY }}
port: ${{ secrets.ORACLE_SSH_PORT }}
script: |
mkdir -p ~/app/blog-server
sudo rm -rf ~/app/blog-server/prometheus ~/app/blog-server/grafana
sudo chown -R $USER:$USER ~/app/blog-server
- name: Copy config files to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.ORACLE_HOST }}
username: ${{ secrets.ORACLE_USER }}
key: ${{ secrets.ORACLE_SSH_KEY }}
port: ${{ secrets.ORACLE_SSH_PORT }}
source: "docker-compose.yml,prometheus,grafana"
target: "~/app/blog-server"
overwrite: true[Issue 3] yml 파일 설정 오류
그라파나 프로비저닝 설정할 때 순서를 잘못 작성할 경우 오류가 발생한다. 설정파일이라서 순서 상관없을 줄 알았는데 작성 순서에 유념해야한다는 것을 알게되었다.
apiVersion: 1
providers:
- name: 'default'
type: file
updateIntervalSeconds: 10 # options 보다 앞에 작성
disableDeletion: false # options 보다 앞에 작성
options:
path: /etc/grafana/dashboards [Issue 4] 무중단 배포의 한계와 롤백 시스템 구축
--force-recreate 옵션이 무중단 배포를 보장해줄 것이라 믿었지만, 실제로는 컨테이너가 중단된 후 재생성되는 방식이었습니다. docker-compose만으로는 완전한 롤링 업데이트 구현에 한계가 있음을 깨닫고, 우선 안정적인 롤백 시스템을 구축하는 데 집중했습니다.
해결: 새로운 이미지를
pull하기 전 현재 작동 중인 이미지 ID(OLD_IMAGE_ID)를 백업합니다. 배포 실패 시docker tag를 이용해latest태그를 이전 이미지 ID로 복구하고 다시 실행하는 로직을 구현했습니다.
- name: Deploy on Oracle VM
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.ORACLE_HOST }}
username: ${{ secrets.ORACLE_USER }}
key: ${{ secrets.ORACLE_SSH_KEY }}
port: ${{ secrets.ORACLE_SSH_PORT }}
script: |
# Docker 없으면 설치
# 배포 전 '현재 정상 작동 중인 이미지 ID' 백업 (롤백 핵심)
OLD_IMAGE_ID=$(sudo docker images -q ghcr.io/${{ github.repository }}:latest)
# .env 직접 생성
# GHCR 로그인
# 최신 이미지 pull
sudo docker compose pull
if sudo docker compose up -d --remove-orphans; then
echo "✅ All services deployed successfully!"
sudo docker image prune -f
else
echo "❌ Deployment failed! Starting rollback..."
# 롤백: latest 태그를 이전 안정 버전 ID로 복구
if [ ! -z "$OLD_IMAGE_ID" ]; then
echo "Restoring latest tag to previous image: $OLD_IMAGE_ID"
sudo docker tag $OLD_IMAGE_ID ghcr.io/${{ github.repository }}:latest
fi
# 전체 서비스를 이전 상태로 다시 실행
sudo docker compose up -d
# 전체적인 서비스 상태 확인 (blog-server 중심)
if [ -z "$(sudo docker ps -q --filter "name=blog-server")" ]; then
echo "🚨 Critical: Rollback failed. System is down."
exit 1
fi
echo "✅ Rollback completed. Stable version is running."
exit 1
fi
sudo docker logout ghcr.io6. 마치며
처음 시도해 본 모니터링 시스템 구축이었지만, 도커와 코드를 통한 인프라 관리의 강력함을 다시 한번 체감했습니다. 아직 개선해야 할 부분은 많지만, 서비스의 품질을 유지한 채 꾸준히 기술적 완성도를 높여가겠습니다.
이번 구축을 통해 OCI 플러그인을 비활성화함으로써 서버 메모리 부담을 확실히 줄일 수 있었습니다. 도커 컴포즈로 시스템을 코딩하듯 구축하는 과정은 즐거웠지만, 배포 시 발생하는 아주 짧은 다운타임은 여전히 숙제로 남았습니다.
다음 단계로는 Docker Swarm이나 Blue-Green 배포 전략을 학습하여 진정한 의미의 무중단 롤링 업데이트를 시도해볼 계획입니다. 또한, 시스템 지표를 넘어 애플리케이션 로그까지 통합 관리할 수 있도록 Loki 도입을 준비하며 서비스의 품질과 기술적 완성도를 꾸준히 높여가겠습니다.