신규 프로젝트나 운영 업무를 인계받다 보면, 아무런 가이드 문서 없이 돌아가고 있는 서버 하나만 덩그러니 놓여 있는 상황을 맞닥뜨리곤 한다. 이번 분석의 시작이 그랬다....

시스템 구성도나 서비스 명세서 하나 없이, 리눅스 서버 터미널과 한 판 씨름하며 소스 코드를 하나씩 까보고 프로세스를 역추적하여 Flask + Gunicorn + Nginx로 이어지는 서비스의 실체를 파헤친 과정을 기록으로 남긴다.

 


 

1. 프로세스 역추적: 서비스 찾기

가장 먼저 수행한 작업은 현재 서버에서 무엇이, 어떻게 돌고 있는지 확인하는 것이었다. 일단 리눅스 서버에 접속한 뒤 표준 명령어를 통해 서비스의 정체를 파악했다.

  • 실행 유닛 확인: systemctl list-units --type=service를 통해 관리되고 있는 서비스 목록을 훑었고, api-service라는 이름의 유닛을 발견했다. (물론 모니터링 서비스들도 있었다.)
  • 데몬 설정 분석: systemctl cat api-service 명령어로 서비스의 내부를 들여다보았다. 여기서 WorkingDirectory를 통해 소스 코드의 위치를, ExecStart를 통해 Gunicorn이 엔진으로 사용되고 있음을 확인했다.

2. Gunicorn과 Flask의 역할 모델

분석 결과, 이 서비스는 파이썬 웹 표준인 WSGI(Web Server Gateway Interface) 구조를 따르고 있었다.

자바 개발자의 시각에서 본 구조는 다음과 같다.

구성 요소 실제 역할 Java 진영 매칭
Flask 웹 프레임워크 Spring Boot
Gunicorn WSGI HTTP Server Embedded Tomcat
systemd 서비스 관리자 Windows Service / Daemon
  • Gunicorn (WAS): 단순한 실행 도구가 아니라, 직접 서비스 포트를 바인딩하고 요청을 처리하는 WAS 엔진, 별도의 관리 포트 없이 설정된 서비스 포트 하나를 점유하여 리스닝(Listening) 함
  • Flask (Logic): Gunicorn에 의해 로드되어 실행되는 비즈니스 로직

**WSGI(Web Server Gateway Interface, 위스기)

- 파이썬 애플리케이션(Flask, Django 등)과 웹 서버(Nginx, Apache 등) 사이에서 통신하기 위한 표준 프로토콜
- 자바 개발자에게 익숙한 개념으로 치면 서블릿 스펙(Servlet Spec)과 거의 동일한 역할
- WSGI 서버의 종류 (엔진)

  • Gunicorn: 파이썬에서 가장 많이 쓰는 WSGI 서버 (자바의 톰캣 역할)
  • uWSGI: 성능은 좋지만 설정이 복잡한 또 다른 WSGI 서버
  • Werkzeug: Flask 내장 서버 (개발용으로만 쓰고 운영에선 안 씀)

3. 포트와 프록시의 연결 고리

서버에는 외부 접속을 위한 Nginx가 앞단에 배치되어 있었다. 구성도 없이 netstat과 lsof만으로 파악한 통신 흐름은 다음과 같다.

  1. Nginx (Port 443): 외부 접속을 받는 유일한 통로(L7 Proxy)
  2. Internal Pass: Nginx 설정 파일(proxy_pass)을 확인한 결과, 내부에서 가동 중인 Gunicorn의 서비스 포트로 트래픽을 넘겨주고 있었다.
  3. Endpoint: 구니콘은 이 포트를 점유하여 대기하다가 Flask 앱의 엔드포인트로 요청을 전달한다.

4. 실전 분석 명령어

문서가 없는 환경에서 유효했던 핵심 명령어 셋

  • 포트 점유 프로세스 식별: sudo netstat -tnlp (포트별 PID 및 프로그램명 확인)
  • 포트 기반 서비스 정체 파악: sudo lsof -i :[Port] (해당 포트를 사용하는 실행 파일 경로 역추적)
  • 런타임 로그 분석: sudo journalctl -u [Service] -f (데몬이 뱉는 표준 출력 로그 실시간 모니터링)

5. 결론: "구조를 알면 소스가 보인다"

시스템 구성도가 없더라도 리눅스 서비스 데몬(systemd) -> WAS 엔진(Gunicorn) -> 프레임워크(Flask)로 이어지는 계층 구조를 이해하면 역추적은 생각보다 명확해진다. 자바 서비스 구조를 투영하여 분석한 결과, 엔진의 설정값(ExecStart)과 실제 소스 코드의 엔트리 포인트를 정확히 매칭시킬 수 있었고, 이를 통해 서비스 전체 흐름을 파악할 수 있었다.

 

 


 

TODO: 이제 파이썬 소스코드를 분석해야 한다. 하기 프로세스대로 할 예정. 힘내자!

 


1. 진입점(Entry Point) 식별 및 객체 초기화 분석
- 방법: ExecStart에 명시된 api_server:app 구조를 추적한다.
- 분석 포인트: api_server.py 내에서 Flask(__name__) 객체가 생성되는 시점을 찾고, 해당 파일에서 import 하는 설정 파일(Config)이나 초기화 로직(DB 연결, 로깅 설정 등)을 먼저 파악한다. 이는 자바의 ApplicationContext 로딩 과정을 분석하는 것과 같다.

2. API 엔드포인트 및 라우팅 맵핑(Controller) 추출
- 방법: grep -r "@app.route" . 또는 @blueprint.route를 검색한다.
- 분석 포인트: 스프링의 @RequestMapping 대신 사용되는 라우팅 데코레이터를 전수 조사한다.
- 소스가 git 관리가 안 되어 있고 서버에만 존재하여 험난할 듯 싶다.


3. 의존성 및 런타임 환경(Library) 분석
- 방법: requirements.txt 또는 Pipfile, pyproject.toml 존재 여부 확인.
- 분석 포인트: 자바의 pom.xml처럼 프로젝트에 사용된 외부 라이브러리 전체 리스트를 파악한다. 특히 메일 발송(smtplib, Flask-Mail), DB ORM(SQLAlchemy), 비동기 작업(Celery) 등 핵심 모듈의 버전을 확인하여 인프라와의 호환성을 검토한다.

이번 달 부터 윈도우 서버 대규모 이관 프로젝트에 CA로서 참여하게 되었다.

지금까지 실무에서는 리눅스를 주로 다뤄왔지만, 이번 이관 대상은 윈도우 서버뿐이기에 필요한 내용들을 윈도우 중심으로 심도 있게 살펴보았다. 리눅스와는 결이 다른 윈도우 NT 커널의 특성을 이해하고, WDS와 DSC를 활용한 자동화 전략을 세우며 공부한 내용들을 기록해두면 좋을 것 같아서 포스팅한다.

 

** 윈도우 vs 리눅스: 인프라 관리 도구 비교

역할 윈도우 리눅스
이미지 봉인 Sysprep (SID, 드라이버 초기화) 필요 없음 (파일 기반이라 복제가 쉬움)
스크립트 PowerShell (.ps1) Bash Shell (.sh)
네트워크 배포 WDS (PXE 기반) Kickstart / Cobbler (PXE 기반)
설정 자동화 DSC (프레임워크) Ansible / Cloud-init
커널 엔진 NT 커널 Linux 커널

 

 

이제 시작!


 

1. 인프라의 기초: Bare Metal vs VMware

1) 베어메탈(Bare Metal):

- 정의: 가상화 소프트웨어(Hypervisor) 없이 하드웨어 위에 OS를 직접 설치한 날것의 서버
- 장점: 가상화 오버헤드가 없어 최강의 성능 보장, VMware 같은 라이선스 비용이 들지 않음

- 용도: 고성능 DB 서버, 리소스를 풀(Full)로 써야 하는 고성능 연산 서버

 

2) VMware (VM):
- 정의: 물리 서버 1대를 소프트웨어로 쪼개서 여러 대의 가상 머신으로 사용하는 방식
- 장점: 서버 자원을 유연하게 배분할 수 있고, 복제와 이동이 매우 간편
- 용도: 웹 서버, API 서버, 일반 업무용 애플리케이션 서버

 

 

2. 배포의 핵심: 골든 이미지(Golden Image)와 레이어 전략
- 윈도우에서 골든 이미지는 도커와 달리 상태를 통째로 박제한 파일(.WIM)

- 운영 관리를 위해서는 전략적인 레이어 설계가 필요하다.

  • Base Layer: OS + 최신 보안 패치
  • Infra Layer: Base + 모니터링 에이전트(Windows Exporter) + 백신 + 원격 관리 설정. (가장 표준이 되는 골든 이미지)
  • App Layer: Infra + MSSQL 또는 IIS 등 특정 서비스

주의: 윈도우는 도커처럼 레이어가 실시간 조립되지 않음. 각 단계가 합쳐진 '완성본'을 각각 구워야 하므로, 실무에서는 Infra Layer까지만 이미지로 굽고 그 위는 스크립트로 처리하는 것이 효율적이다.


** 도커(Docker) vs 윈도우 골든 이미지

구분 도커 (Container) 윈도우 골든 이미지 (WIM)
구조 호스트 커널 공유 (가벼움) 자체 NT 커널 포함 (무거움)
관리 코드 기반 레시피 (Dockerfile) 설치 후 스냅샷 박제 (WIM)
수정 코드 수정 후 즉시 재빌드 이미지 풀기 -> 수정 -> 다시 굽기
비유 필요한 부품만 가는 레고 방 전체를 복사하는 건축

 

 

3. 대규모 배포 기술: PXE 서버
- 대규모 서버 이관 시 400대 서버에 일일이 파일을 설치하는 것은 번거롭다. 네트워크를 통한 대량 배포 시스템을 구축해야 함

 

1) PXE(Preboot eXecution Environment, 픽시):
- 서버가 켜질 때 하드디스크가 아닌 네트워크(LAN) 선을 통해 OS 설치 파일을 받아오는 시스템
- 중앙의 PXE 서버(WDS 등)가 400대 서버에 동시에 OS를 쏴주는 역할

 

** WDS와 DSC 차이 
- Ansible을 통해 Ansible Tower에서 각 타겟 VM에 접근해서 처리할 수 있지만, 사내 보안 상 그건 불가할 것으로 보임

- 타겟 VM에서 미들웨어 설치 스크립트를 pull로 당겨와서 할 수 있다면? DSC 사용 가능.
  하지만 이것도 보안 상 불가할 수 있음... 그럼 공통 repository를 생성해서 관리하거나 해야 될 듯.

 

=> WDS는 MS에서 제공하는 PXE 서버 역할의 정식 명칭

=> DSC는 리눅스에서도 사용 가능하긴 하지만 윈도우 특화라고 보면 된다. (PowerShell DSC는 윈도우 인프라를 '코드로 관리(IaC)'하기 위한 선언적 프레임워크)

==> WDS가 서버라는 기초 건물(OS+기본 레이어)을 올리는 공사라면, DSC는 그 안에 미들웨어+서비스를 채우고 유지하는 작업

구분 WDS (Deployment) DSC (Configuration)
목적 OS 설치 (OS 굽고 뿌리기) 설정 관리 (OS 설치 후 세부 세팅)
시점 서버가 처음 태어날 때 서버가 운영되는 내내
핵심 파일 .wim (골든 이미지 파일) .ps1 (파워쉘 기반 선언적 코드)
도구 성격 서버 역할(Role) 파워쉘의 기능(Feature)

 


4. 봉인과 소통: Sysprep & PowerShell(.ps1)

- Sysprep(시스프렙): 골든 이미지를 굽기 전, 서버의 고유 정보(SID, 이름)를 지우고 '봉인'하는 필수 단계, 이 과정을 거쳐야 수백대가 충돌 없이 복제됨
- PowerShell (.ps1): 윈도우의 심장인 NT 커널을 조종하는 마스터 키, bat 대신 현대적인 .ps1 스크립트를 통해 모든 자동화를 구현 가능
   => 실무 팁: 실행 권한 문제 해결을 위해 Set-ExecutionPolicy RemoteSigned 명령어가 선행되어야 함

Q. 왜 하는가?: 윈도우 서버는 그대로 복사하면 400대의 SID(주민번호와 같다고 보면 됨)와 이름이 중복되어 네트워크 충돌이 발생
Q. 어떻게 하는가?: C:\Windows\System32\Sysprep\sysprep.exe를 실행하여 '일반화(Generalize)' 옵션을 선택함
* 결과: 컴퓨터 고유 정보는 지워지고, OS 설정과 설치된 프로그램(익스포터 등)만 남은 깨끗한 복제용 상태가 된다!



5. 모니터링 체계: Windows Exporter & 프로메테우스
- Windows Exporter: NT 커널에서 CPU, 메모리, 네트워크 지표를 뽑아내는 전용 에이전트
- 설정: 기본적으로 9182번 포트를 사용, 골든 이미지를 만들 때 'NT 서비스'로 등록해두면 서버가 부팅되자마자 자동으로 모니터링이 시작됨

- linux의 node exporter와 동일 역할임
- 모니터링은 프로메테우스 대신 다른 솔루션 도입할 수도 있어서 추가로 알아봐야 될 듯


 

핵심 요약
대규모 윈도우 인프라 이관은 "한 대를 완벽하게 세팅하여 시스프렙으로 봉인하고(Golden Image), PXE로 400대에 배포한 뒤, 파워쉘 스크립트로 일괄 조종한다" 로 정리 가능

VM 재기동 시 dashboard 접속 불가 관련 프트러블슈팅 기록을 블로그로 남긴다.

 

1. 에러

VM 재기동 시 dashboard 외부 접속 불가하여 port-forward로 port open 하였더니 CPU 부하로 터미널 정지됨

현상: 대시보드 접속을 위해 kubectl port-forward 실행 중 시스템 전체가 멈춤(Hang).
로그 에러: kernel:watchdog: BUG: soft lockup - CPU#0 stuck for 35s!.
영향: 터미널 입력 불능 및 쿠버네티스 컨트롤 플레인 통신 마비.

 

2. 원인

  • 리소스 과부하 (Resource Exhaustion): 마스터 노드에 API 서버, 네트워크(Calico), 모니터링(Metrics Server), 대시보드가 밀집되어 기동 시 CPU 점유율이 임계치를 초과함.
  • 통신 방식의 비효율성: port-forward는 사용자 PC와 VM 간의 터널링을 위해 실시간 CPU 인터럽트를 과다하게 발생시킴. 저사양 환경에서 이 인터럽트 처리가 밀리며 커널이 락업(Lockup)에 빠짐.
  • 접속 설정의 폐쇄성: 기본 설치된 대시보드는 ClusterIP(내부용) 타입으로, 외부 브라우저에서 직접 접근이 불가능하여 사용자가 무리한 터널링을 시도하게 유도함.

3. 해결

Vagrant 파일에 아래 script 추가함

-> 서비스를 ClusterIP에서 NodePort로 변경하여 VM IP의 특정 포트(30000)를 대시보드에 직접 할당 등

 


echo '======== [9-2] Dashboard 설치 ========‘

echo '======== ***** 대시보드 외부 오픈 (추가) ***** ========‘
# [설정 1] NodePort 30000번으로 고정
sleep 10  # <--- 대시보드 API가 준비될 시간을 줌
kubectl patch svc kubernetes-dashboard -n kubernetes-dashboard -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "nodePort": 30000, "targetPort": 8443}]}}’
# [설정 2] 로그인 화면에서 Skip 버튼 활성화
kubectl patch deployment kubernetes-dashboard -n kubernetes-dashboard --type 'json' -p '[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--enable-skip-login"}]’
# [설정 3] Skip 버튼 클릭 시 관리자 권한 부여
kubectl create clusterrolebinding dashboard-skip-admin --clusterrole=cluster-admin --serviceaccount=kubernetes-dashboard:kubernetes-dashboard
echo '======== ***** 대시보드 외부 오픈 (추가) 끝 ***** ========‘

 

 

여러모로 알아야 할 게 많다.

K8S 로컬 실습 중 며칠 지나고 마스터 노드를 다시 기동했더니 정적 파드 실행 오류가 발생했다. 아래는 그 원인과 해결방안이다.

 

 

1. 에러

MobaXTerm으로 접속한 k8s-master 터미널이 멈추는 오류 (SSH 세션이 끊겨서 발생한 것으로 예상) 를 해결하기 위해 Virtual Box에서 VM 재기동하였으나, 일부 pod creating error 가 발생했다.

-> kube-controller-manager, kube-scheduler

 

2. 원인

여러 원인을 찾아보았으나, 결과적으로 VM 재기동 후 컨테이너 런타임(containerd/docker)이 기존의 마운트 정보나 컨테이너 상태를 정상적으로 복구하지 못해 발생하는 일시적인 프로세스 꼬임 현상으로 보였다.

 

3. 해결

아래 명령어로 매니페스트 파일을 잠시 옮겼다가 다시 가져와서 Kubelet이 파드를 삭제 후 완전히 새로 생성하도록 강제 (이래야 AGE가 0s부터 시작하며 프로세스가 초기화 된다고 한다.)

 

# 1. 파일을 임시 디렉토리로 이동 (파드 삭제 유도)

mv /etc/kubernetes/manifests/kube-controller-manager.yaml /tmp/

mv /etc/kubernetes/manifests/kube-scheduler.yaml /tmp/

 

# 2. 10초 대기 후 파일을 원래 위치로 복사 (파드 재생성 유도)

mv /tmp/kube-controller-manager.yaml /etc/kubernetes/manifests/

mv /tmp/kube-scheduler.yaml /etc/kubernetes/manifests/

 

 

문제 해결!

※ Label vs Selector 핵심 비교

구분 Label (이름표) Selector (검색기)
정의 리소스를 식별하기 위한 Key-Value 특정 Label을 가진 리소스를 선택하는 필터
위치 metadata.labels 항목에 작성 spec.selector 항목에 작성
사용 주체 Pod, Node (관리 대상) Service, Deployment, HPA (관리 주체)
비유 상품에 붙어 있는 바코드/태그 바코드를 찍어 물건을 분류하는 스캐너
역할 "나는 'A'라는 그룹 소속이다"라고 표시 "나는 'A' 그룹인 애들만 관리하겠다"라고 연결

 


 

1. k8s 환경 세팅

# k8s 에서 directory 생성
[root@k8s-master ~]#
mkdir -p /root/k8s-local-volume/1231

# k8s 대시보드에서 objec들 생성

dashboard 접속 > Namespace [모든 네임스페이스] > [+] 버튼 > [입력을 통해 생성] > yaml 파일 붙여넣기 > 업로드

- 아래는 yml.

더보기

apiVersion: v1
kind: Namespace
metadata:
  name: anotherclass-123
  labels:
    part-of: k8s-anotherclass
    managed-by: dashboard
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: api-tester-1231-files
  labels:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231-files
    version: 1.0.0
    managed-by: dashboard
spec:
  capacity:
    storage: 2G
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  local:
    path: "/root/k8s-local-volume/1231"
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - {key: kubernetes.io/hostname, operator: In, values: [k8s-master]}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  namespace: anotherclass-123
  name: api-tester-1231-files
  labels:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231
    version: 1.0.0
    managed-by: kubectl
spec:
  resources:
    requests:
      storage: 2G
  accessModes:
    - ReadWriteMany
  selector:
    matchLabels:
      part-of: k8s-anotherclass
      component: backend-server
      name: api-tester
      instance: api-tester-1231-files
---
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: anotherclass-123
  name: api-tester-1231-properties
  labels:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231
    version: 1.0.0
    managed-by: dashboard
data:
  spring_profiles_active: "dev"
  application_role: "ALL"
  postgresql_filepath: "/usr/src/myapp/datasource/postgresql-info.yaml"
---
apiVersion: v1
kind: Secret
metadata:
  namespace: anotherclass-123
  name: api-tester-1231-postgresql
  labels:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231
    version: 1.0.0
    managed-by: dashboard
stringData:
  postgresql-info.yaml: |
    driver-class-name: "org.postgresql.Driver"
    url: "jdbc:postgresql://postgresql:5431"
    username: "dev"
    password: "dev123"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: anotherclass-123
  name: api-tester-1231
  labels:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231
    version: 1.0.0
    managed-by: dashboard
spec:
  selector:
    matchLabels:
      part-of: k8s-anotherclass
      component: backend-server
      name: api-tester
      instance: api-tester-1231
  replicas: 2
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        part-of: k8s-anotherclass
        component: backend-server
        name: api-tester
        instance: api-tester-1231
        version: 1.0.0
    spec:
      nodeSelector:
        kubernetes.io/hostname: k8s-master
      containers:
        - name: api-tester-1231
          image: 1pro/api-tester:v1.0.0
          ports:
          - name: http
            containerPort: 8080
          envFrom:
            - configMapRef:
                name: api-tester-1231-properties
          startupProbe:
            httpGet:
              path: "/startup"
              port: 8080
            periodSeconds: 5
            failureThreshold: 36
          readinessProbe:
            httpGet:
              path: "/readiness"
              port: 8080
            periodSeconds: 10
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: "/liveness"
              port: 8080
            periodSeconds: 10
            failureThreshold: 3
          resources:
            requests:
              memory: "100Mi"
              cpu: "100m"
            limits:
              memory: "200Mi"
              cpu: "200m"
          volumeMounts:
            - name: files
              mountPath: /usr/src/myapp/files/dev
            - name: secret-datasource
              mountPath: /usr/src/myapp/datasource
      volumes:
        - name: files
          persistentVolumeClaim:
            claimName: api-tester-1231-files
        - name: secret-datasource
          secret:
            secretName: api-tester-1231-postgresql
---
apiVersion: v1
kind: Service
metadata:
  namespace: anotherclass-123
  name: api-tester-1231
  labels:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231
    version: 1.0.0
    managed-by: dashboard
spec:
  selector:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231
  ports:
    - port: 80
      targetPort: http
      nodePort: 31231
  type: NodePort
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  namespace: anotherclass-123
  name: api-tester-1231-default
  labels:
    part-of: k8s-anotherclass
    component: backend-server
    name: api-tester
    instance: api-tester-1231
    version: 1.0.0
    managed-by: dashboard
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-tester-1231
  minReplicas: 2
  maxReplicas: 4
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 120

object 삭제 script

[root@k8s-master ~]# kubectl delete ns anotherclass-123

[root@k8s-master ~]# kubectl delete pv api-tester-1231-files

 

 

2. Obejct

 

3. Label / Selector / Naming (1)

 

3. Label / Selector / Naming (2)

 

 

 

 

k8s Web Application 배포 시 세팅해야 하는 기본(주요) 리소스

- 웹 서비스를 완성하는 4대 핵심 리소스 : 앱 관리(Deployment), 내부 연결(Service), 부하 대응(HPA), 도메인 접속(Ingress)

  > k8s 설정 yml에서 kind에 들어가는 속성

  • Deployment : 컨테이너를 몇 개 띄울지 결정하고, 앱이 죽으면 다시 살리며, 업데이트 시 중단 없이 교체해주는 컨트롤러
  • Service : 파드는 IP가 수시로 변하므로, 서비스라는 고정된 이름을 통해 파드들이 서로 통신하거나 외부에서 접속할 수 있게 연결
  • HPA (Horizontal Pod Autoscaler) : CPU나 메모리 사용량이 설정치를 넘어가면 Deployment의 replicas를 자동으로 늘려 서버 다운을 방지
  • Ingress: 여러 서비스를 하나의 공인 IP로 묶고, 도메인(예: api.test.com)이나 주소 경로(예: /login)에 따라 트래픽을 분산하며 SSL 보안 인증서를 적용

리소스 구분

리소스명 (kind) 핵심 역할 실무 비유
Deployment 앱 생성 및 버전 관리 본체 (Server)
Service 고정 주소(IP/Port) 제공 내부 입구 (Gateway)
HPA 부하에 따른 자동 확장 보험 (Autoscaling)
Ingress 도메인 연결 및 경로 설정 간판/문지기 (Domain/L7)

 


 

1. App 배포 환경 구성 및 배포
- k8s dashboard 접속 > Namespace [default] > [+] 버튼 > [입력을 통해 생성] > yaml 파일 붙여넣기 > 업로드

- 아래는 테스트용으로 썼던 yml. (나중엔 내가 개발한 APP으로 image 빌드 후 배포해보자)

더보기

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-1-2-2-1
spec:
  selector:
    matchLabels:
      app: '1.2.2.1'
  replicas: 2
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: '1.2.2.1'
    spec:
      containers:
        - name: app-1-2-2-1
          image: 1pro/app
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 8080
          startupProbe:
            httpGet:
              path: "/ready"
              port: http
            failureThreshold: 20
          livenessProbe:
            httpGet:
              path: "/ready"
              port: http
          readinessProbe:
            httpGet:
              path: "/ready"
              port: http
          resources:
            requests:
              memory: "100Mi"
              cpu: "100m"
            limits:
              memory: "200Mi"
              cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: app-1-2-2-1
spec:
  selector:
    app: '1.2.2.1'
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 31221
  type: NodePort
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: app-1-2-2-1
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: app-1-2-2-1
  minReplicas: 2
  maxReplicas: 4
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 40

 

 

 

2. App에 지속적으로 트래픽 보내기 (Traffic Routing 테스트)

# k8s-master에서 2초마다 트래픽 요청

[root@k8s-master ~]# while true; do curl http://192.168.56.30:31221/hostname; sleep 2; echo '';  done;

 

3. AppMemory Leak 나게 하기 (Self-Healing 테스트)

# k8s-master에서 실행

[root@k8s-master ~]# curl 192.168.56.30:31221/memory-leak

* 결과 : pod 1개가 restart

 

4. App에 부하주기 (AutoScaling 테스트)

# k8s-master에서 요청

[root@k8s-master ~]# curl 192.168.56.30:31221/cpu-load

* pod2개 더 생성되어 총 4개가 됨

>> YAML에 설정한 HPA(Horizontal Pod Autoscaler)CPU 부하를 감지하고, 서비스 안정성을 위해 파드 개수를 자동으로 늘린 것.
>> [
상세 원인 분석]

  • 부하 발생: /cpu-load 경로로 접속하면 해당 파드의 CPU 사용량이 강제로 급증
  • 임계치 초과: 앞서 작성한 YAML에서 CPU 사용량이 40%를 넘으면 확장하도록 설정함
  • 자동 확장: 쿠버네티스가 "현재 2개로는 부하를 감당하기 어렵다"고 판단하여, 설정된 최대치인 4개(maxReplicas: 4)까지 파드를 2개 더 생성한 것

5. App 이미지 업데이트 (RollingUpdate 테스트)

- Namespace: default > 디플로이먼트 > ... > 편집

spec:

      containers:

        - name: app-1-2-2-1

          image: 1pro/app-update  # 수정

- Script 실행 시

[root@k8s-master ~]# kubectl set image -n default deployment/app-1-2-2-1 app-1-2-2-1=1pro/app-update

* 결과 : 업데이트 된 이미지가 적용된 pod가 올라올 동안, 이전 pod도 유지됨

 

update 중지 script

[root@k8s-master ~]# kubectl rollout undo -n default deployment/app-1-2-2-1

 

object 삭제 script

[root@k8s-master ~]# kubectl delete -n default deploy app-1-2-2-1

[root@k8s-master ~]# kubectl delete -n default svc app-1-2-2-1

[root@k8s-master ~]# kubectl delete -n default hpa app-1-2-2-1

이번엔 이어서 모니터링 도구들을 설치해보자. 

 

프로젝트 환경에서 Prometheus와 Grafana는 VM 모니터링과 DB 모니터링에 사용했었는데 Loki는 이번에 처음 사용해봤다.

 

 

1. 프로메테우스 vs 로키 : 데이터의 차이

구분 프로메테우스 (Prometheus) 로키 (Loki-Stack)
수집 데이터 숫자 (Metrics) 문자 (Logs)
핵심 질문 "현재 CPU 사용량이 몇 %인가?" "에러 발생 시점에 기록된 내용은 무엇인가?"
저장 방식 시계열 데이터베이스 (TSDB) 레이블 기반 인덱싱 및 압축 저장
주요 용도 시스템 부하 감시 및 실시간 알람 구체적인 장애 원인 분석 및 디버깅
관계 독립적 엔진 (별도 구축 필요) 독립적 엔진 (별도 구축 필요)
통합 환경 Grafana (두 데이터를 한 화면에 표시) Grafana (두 데이터를 한 화면에 표시)

 

 

2. 로키 스택, 프로메테우스, 그라파나 설치

- 하기 설치 시 스토리지 연동은 하지 않았으므로 VM 재기동시마다 로그 초기화됨


** 본 설치는 kube-prometheus git 프로젝트의 매니페스트(YAML) 방식을 활용해 메트릭(Prometheus)과 로그(Loki)를 통합 구축하는 과정임

 

Q. Helm과 Yaml 설치 방식의 차이는?

구분 YAML 방식 (Manifest) Helm 방식 (Package Manager)
개념 모든 설정을 직접 작성한 문서 설정을 템플릿화한 패키지 (Chart)
설치 도구 kubectl helm
설정 변경 수십 개의 파일 내부를 일일이 수정 values.yaml 파일 하나만 수정
버전 관리 수동 (파일을 직접 백업/관리) 자동 (Revision 관리로 원클릭 롤백)
학습 효과 매우 높음 (내부 구조를 다 알게 됨) 낮음 (내부가 감춰져 있음)

 

- yaml 방식이 설정파일들을 뜯어볼 수 있어서 공부할 때는 좋지만, 실무에서는 버전 관리, 롤백, 환경별 설정 분리가 가능한 helm 방식이 표준

 

1) Github(k8s-1pro)에서 Prometheus(with Grafana), Loki-Stack yaml 다운로드

- [k8s-master] Console 접속 후 아래 명령 실행

# git 설치

[root@k8s-master ~]# yum -y install git

 

# 로컬 저장소 생성

git init monitoring

git config --global init.defaultBranch main

cd monitoring

 

# remote 추가 ([root@k8s-master monitoring]#)

git remote add -f origin https://github.com/k8s-1pro/install.git

 

# sparse checkout 설정

git config core.sparseCheckout true

echo "ground/k8s-1.27/prometheus-2.44.0" >> .git/info/sparse-checkout

echo "ground/k8s-1.27/loki-stack-2.6.1" >> .git/info/sparse-checkout

 

# 다운로드

git pull origin main

 

2) Prometheus(with Grafana) 설치

- Github : https://github.com/prometheus-operator/kube-prometheus/tree/release-0.14

 

# 설치 – monitoring directory에서 진행 ([root@k8s-master monitoring]#)

kubectl apply --server-side -f ground/k8s-1.27/prometheus-2.44.0/manifests/setup

kubectl wait --for condition=Established --all CustomResourceDefinition --namespace=monitoring

kubectl apply -f ground/k8s-1.27/prometheus-2.44.0/manifests

 

# 설치 확인 ([root@k8s-master]#)

kubectl get pods -n monitoring

 

3) Loki-Stack 설치

# 설치 – monitoring director에서 진행 ([root@k8s-master monitoring]#)

kubectl apply -f ground/k8s-1.27/loki-stack-2.6.1

 

# 설치 확인

kubectl get pods -n loki-stack

 

4) Grafana 접속

▶ 접속 URL : http://192.168.56.30:30001

로그인 :​ id: admin, pw: admin

 

5) Grafana에서 Loki-Stack 연결

Connect data : Home > Connections > Connect data

검색에 [loki] 입력 후 항목 클릭

 

6) Grafana 대시보드 생성

- 메뉴 이동: Grafana 좌측 메뉴에서 Dashboards > New > Import를 클릭

- ID 입력: 아래의 공식 ID를 입력하고 Load

 > Prometheus (K8S 상태): 315 (Kubernetes Cluster 모니터링 표준)

 > Loki (로그 확인): 15141 (Loki용 공식 로그 대시보드)

 

 

 

모니터링 설치 삭제 시

- Prometheus(with Grafana), Loki-stack 삭제

[k8s-master] Console 접속 후 아래 명령 실행

 

# 모니터링 설치 폴더로 이동

[root@k8s-master ~]# cd monitoring

 

# Prometheus 삭제

kubectl delete --ignore-not-found=true -f ground/k8s-1.27/prometheus-2.44.0/manifests -f ground/k8s-1.27/prometheus-2.44.0/manifests/setup

 

# Loki-stack 삭제

kubectl delete -f ground/k8s-1.27/loki-stack-2.6.1


 

- TODO : 1. 설치 yml 별도로 생성하여 진행하기 / 2. VM 재기동시 이력이 초기화 되지 않도록 스토리지 설정하기

앞서 K8S 를 구축하면 아래와 같은 K8S 컴포넌트들을 확인할 수 있다. 

 

1. K8S 컴포넌트 

Pod 명칭 (핵심 키워드) 설명
calico-apiserver Calico 네트워크 정책 전용 API 확장 서비스
calico-kube-controllers 네트워크 리소스 상태 감시 및 정책 관리 컨트롤러
calico-node 노드 간 통신 및 실제 라우팅을 수행하는 네트워크 엔진
calico-typha 노드 확장 시 DB 부하를 줄여주는 네트워크 최적화 도구
csi-node-driver 컨테이너와 가상 머신 저장소(디스크)를 연결하는 드라이버
coredns 클러스터 내 서비스 이름을 IP로 변환하는 DNS 서비스
etcd 모든 클러스터 상태 데이터가 저장되는 핵심 데이터베이스
kube-apiserver 모든 명령이 통과하는 쿠버네티스의 중앙 제어 관문
kube-controller-manager 노드 상태 및 복제본 수 등을 관리하는 클러스터 감시자
kube-proxy 트래픽을 컨테이너로 전달하기 위한 네트워크 규칙 관리
kube-scheduler 자원 상태에 따라 컨테이너를 최적의 노드에 배치
metrics-server CPU 및 메모리 사용량을 실시간 수집하는 성능 분석 도구
dashboard-metrics-scraper 웹 대시보드 표시용 통계 데이터를 수집하는 도구
kubernetes-dashboard 웹 브라우저에서 클러스터를 관리하게 해주는 GUI UI
tigera-operator Calico 네트워크 엔진의 설치와 관리를 자동화하는 관리자

 

 

2. K8S 인터페이스 규격

- K8S 인터페이스 규격이란 : 쿠버네티스가 다양한 외부 기술(컨테이너 실행, 네트워크, 저장소)을 마치 '레고 블록'처럼 자유롭게 갈아 끼울 수 있도록 만든 표준 연결 고리

 

- CRI : 컨테이너 실행(런타임) 인터페이스
- CNI : 네트워크 연결(통신) 인터페이스
- CSI : '외부 저장소 연결(스토리지)'을 위한 표준 규격

* CRI & CNI : Kubelet이 CRI를 통해 컨테이너를 생성하면, 이어서 CNI가 해당 컨테이너의 네트워크 환경(IP 등)을 설정함. 1.24 버전 이후부터는 Kubelet이 직접 CNI를 부르는 대신 CRI가 CNI를 호출하는 구조로 최적화 됨

* CSI :
> 독립성: 쿠버네티스 코드를 수정하지 않고도 새로운 스토리지 드라이버를 추가할 수 있음
> 자동화: 사용자가 PVC(Persistent Volume Claim)를 요청하면, CSI가 자동으로 실제 스토리지를 생성하고 파드에 연결(Mount)해 줌
> 다양성: 클라우드 디스크뿐만 아니라 온프레미스의 하드웨어 스토리지도 플러그인 형태로 연결 가능

 

# CRI / CNI / CSI 핵심 비교 

구분 CRI (Runtime) CNI (Network) CSI (Storage)
역할 컨테이너 실행/관리 IP 할당 및 통신 볼륨 연결 및 데이터 저장
비유 컴퓨터 본체 (연산) 랜선 (통신) 외장 하드 (저장)
실무 질문 "컨테이너를 띄워라" "통신이 되게 하라" "데이터를 저장하라"
대표 예시 Containerd, CRI-O Calico, Cilium AWS EBS, NFS, Rook/Ceph

 

+ Recent posts