[Deep Dive] OCI 서버 모니터링 도입기: Prometheus & Grafana 구축과 트러블슈팅

OCI(Oracle Cloud Infrastructure) 프리티어 환경에서 서버를 운영하다 보면 '메모리 부족'과 CPU 부하에 항상 신경을 쓰기 마련입니다.

단순히 htop으로 상황을 파악하는 것을 넘어, 왜 Grafana를 선택했는지와 그 과정에서 마주한 기술적 난관들을 정리했습니다.

1. 왜 Grafana인가? (htop vs OCI Monitoring vs Grafana)

서버 관리를 위해 가장 먼저 고려한 도구는 htopOCI Monitoring이었지만, 각각 명확한 한계가 있었습니다.

  • htop (수동의 한계): 실시간 확인은 유용하지만, 매번 터미널을 열고 SSH로 원격 접속해야 하는 번거로움이 있었습니다. 또한 과거의 지표를 기록하지 않아 장애 원인 분석에 불리했습니다.

  • OCI Monitoring (리소스의 부담): 오라클에서 제공하는 모니터링 플러그인은 편리하지만, 생각보다 메모리 점유율이 높았습니다. 저사양 VM에서는 이조차 부담스러웠기에, 더 가볍고 직관적인 커스텀 모니터링 환경이 필요했습니다.

스크린샷 2026-04-19 오후 12.52.43스크린샷 2026-04-19 오후 1.10.13

플러그인을 비활성화하니 메모리 용량 확보와 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 점유율 추이를 그래프로 확인하며 선제적인 자원 관리가 가능해졌습니다.

스크린샷 2026-04-19 오후 1.48.28

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.io

6. 마치며

처음 시도해 본 모니터링 시스템 구축이었지만, 도커와 코드를 통한 인프라 관리의 강력함을 다시 한번 체감했습니다. 아직 개선해야 할 부분은 많지만, 서비스의 품질을 유지한 채 꾸준히 기술적 완성도를 높여가겠습니다.

이번 구축을 통해 OCI 플러그인을 비활성화함으로써 서버 메모리 부담을 확실히 줄일 수 있었습니다. 도커 컴포즈로 시스템을 코딩하듯 구축하는 과정은 즐거웠지만, 배포 시 발생하는 아주 짧은 다운타임은 여전히 숙제로 남았습니다.

다음 단계로는 Docker Swarm이나 Blue-Green 배포 전략을 학습하여 진정한 의미의 무중단 롤링 업데이트를 시도해볼 계획입니다. 또한, 시스템 지표를 넘어 애플리케이션 로그까지 통합 관리할 수 있도록 Loki 도입을 준비하며 서비스의 품질과 기술적 완성도를 꾸준히 높여가겠습니다.