move02 Developer Develop, Enjoy - 즐기면서 개발하기

Kuberenetes 모니터링 시스템 구축(prometheus, grafana)

댓글 달기

Kuberenetes 모니터링 시스템 구축(prometheus, grafana)

지난 번 모니터링 아키텍쳐 포스팅에 이어 Prometheus 를 기반으로 하는 모니터링 시스템에 대한 간략한 설명과 실제 구축 예시를 정리하고자 한다.

모니터링 도구 선정

모니터링 방식에는 크게 push-based와 pull-based가 존재한다고 한다. Push-based 방식은 운영중인 각 서버마다 모니터링 에이전트를 설치해 정보를 수집하고, 이를 모니터링 서버로 보내는 방식이다. 대표주자로는 Graphite, Beats가 있다. Pull-based 방식은 반대로, 일정 간격을 두고 모니터링 서버에서 각 수집대상인 서버(client)를 찾아 메트릭을 직접 가져오는(scrape) 방식이다. 대표주자로는 Prometheus가 있다.

보통 Kubernetes에서는 Pod이나 Container가 지워지고 다시 만들어지는 경우가 잦기 때문에 모니터링 대상이 동적이다. 이러한 상황에서 업데이트나 새로 배포할 때마다 Push-based 모니터링 시스템의 에이전트를 매 번 설치하기 보다는 수집대상을 직접 찾고 가져오는 Pull-based가 잘 어울린다고 볼 수 있다.

push-based, pull-based 각각의 장단점이 있으나 이번 글의 취지와는 벗어나므로 나중에 다루기로..

Prometheus

Pull based 모니터링 툴이다. 메트릭을 수집하여 시계열 데이터로 저장하며 측정 지표는 기록된 타임스탬프와 함께 레이블 이라고 하는 선택적인 key-value 쌍과 함께 저장된다.

alert manager를 통해 사용량 경고 등을 보낼수도 있고, grafana와 통합하여 강력한 메트릭 시각화도 가능하다.

주요 기능

  • 메트릭 이름과 key-value 쌍으로 식별되는 다차원 시계열 데이터 모델
  • 다차원의 데이터를 조회할 수 있는 PromQL
  • 분산 스토리지에 대한 의존성이 없음.
  • HTTP 프로토콜을 통한 Pull 기반의 시계열 수집
  • 중개 gateway를 통한 pushing 기법
  • 서비스 디스커버리나 정적 설정을 통한 대상 식별
  • 다양한 모드의 graphing, dashboarding 지원

프로메테우스가 제공하지 않는 것

다음과 같은 것들이 필요하면 다른 솔루션을 추가로 사용해야 한다.

  • 원시 로그 / 이벤트 수집 : Loki, Elastic stack
  • 요청 추적(Request Tracing) : OpenMetrics, OpenTelemetry
  • 이상 감지(Anomalt Detection)
  • 장기 보관 및 고가용성 : Prometheus Operator, Thanos 등
  • 스케일링 : Prometheus Operator, Thanos 등
  • 사용자 인증 관리

구조

prometheus-architecture

Jobs/Exporters

Job은 Prometheus 서버에 관점에서 바라보았을 때 메트릭 수집이 가능한 여러 인스턴스를 동일한 목적을 기준으로 묶어놓은 집합체이다. prometheus.yml(프로메테우스 설정파일)의 scrape_configs 속성에 값을 넣어 지정해줄 수 있다.

scrape_configs:
  - job_name: 'prometheus'

    static_configs:
        - targets: ['prometheus-server.default.svc.cluster.local:9090']
          labels:
            group: "prometheus"
    
  - job_name: 'node'

    scrape_intervbal: 5s # default 15s

    # service-discovery를 이용해 동적으로 변경되는 타겟에도 대응이 가능하다.
    static_configs:
        - targets: ['prometheus-node-exporter-1.default.svc.cluster.local:8080', 'prometheus-node-exporter-2.default.svc.cluster.local:8080']
          labels:
            group: "production"

        - targets: ['prometheus-node-exporter-3.default.svc.cluster.local:8080']
          labels:
            group: "canary"

인스턴스는 메트릭 수집이 가능하다는 것을 전제로 하기 때문에 주로 그 대상은 Exporter가 된다. 별도의 지정이 없으면 타겟의 /metrics 경로로 메트릭을 요청한다.

참고로 위의 scrape_config 는 참고용 예시일 뿐이고 kubernetes 에서 운영될때는 하단에 설명할 service discovery를 이용해 다르게 작성된다.

Exporter는 metrics pull 요청이 들어오면 요청 당시의 데이터를 리턴한다. (중간중간 값을 저장하지 않고 요청 시점의 데이터만 리턴) Kubernetes에서 주로 사용되는 Exporter는 Node-exporterkube-state-metrics가 있다.

Node-exporter는 클러스터를 구성하는 각 노드에 배포되어 CPU, Memory, Network I/O, Disk I/O 등의 메트릭을 수집한다. 모든 노드에 배포된다는 점이 Push-base 모니터링 시스템의 에이전트와 비슷해보일 수 있으나 기본적으로 수집하여 가지고 있다가 Prometheus 서버에서 요청이 들어올 때 내보낸다는 차이가 있으며, 모든 Exporter가 모니터링 대상에 배포되는 것은 아니다.

kube-state-metrics은 Kubernetes API를 이용하여 대부분의 Kubernetes 오브젝트에 대한 메트릭을 수집한다. Kubernetes API를 이용하기 때문에 하나의 Exporter만 있으면 된다.

요약 Exporter는 모니터링 대상의 Metric을 수집 -> Prometheus 데이터 모델로 변환 -> Metric 요청 시 전송

다양한 Exporter에 대해서 알아보면 공식 홈페이지의 Exporters 문서를 참고.

PushGateway

Exporter와 같이 메트릭을 수집하는 용도의 Component 이지만 설계 목적과 동작 방식이 상이하다. 앞서 설명했듯, Prometheus는 기본적으로 pull 기반의 모니터링 툴이지만 모니터링 대상이 임시로 만들어지는 컴포넌트일 경우 Exporter로 메트릭을 수집하기는 어렵다. 예를 들어, 서비스 레벨의 배치 잡들은 해당 작업을 수행하기 위해 Pod, 컨테이너가 생성되었다가 수행중인 작업이 모두 종료되면 컨테이너와 Pod 모두 사라진다. 이때, 해당 잡을 실행하면서 발생한 메트릭은 Pod이 사라지면 수집할 수 없게된다. 이런 제한적인 상황에서만 사용하도록 만들어진 것이 PushGateway 이다.

실행되는 배치 잡에서 중간에 위치한 PushGateway로 메트릭을 push 해두고 Prometheus 서버에서 메트릭 요청을 PushGateway로 보내어 배치 잡의 메트릭을 수집하는 형태로 메트릭 수집이 이루어진다. 배포된 잡이 PushGateway로 메트릭을 push하기 위해서는 각 언어별 prometheus 라이브러리를 사용해야 하는데, 내가 가장 많이 사용하는 Java의 예시 코드는 아래와 같다.

void executeBatchJob() throws Exception {
    CollectorRegistry registry = new CollectorRegistry();
    Gauge duration = Gauge.build()
        .name("my_batch_job_duration_seconds").help("Duration of my batch job in seconds.").register(registry);
    Gauge.Timer durationTimer = duration.startTimer();
    try {
    // Your code here.

    // This is only added to the registry after success,
    // so that a previous success in the Pushgateway isn't overwritten on failure.
    Gauge lastSuccess = Gauge.build()
        .name("my_batch_job_last_success").help("Last time my batch job succeeded, in unixtime.").register(registry);
    lastSuccess.setToCurrentTime();
    } finally {
    durationTimer.setDuration();
    PushGateway pg = new PushGateway("127.0.0.1:9091");
    pg.pushAdd(registry, "my_batch_job");
    }
}
// 출처 : https://prometheus.github.io/client_java/io/prometheus/client/exporter/PushGateway.html

각 언어별로 방법이 다른 듯 하니 Pushing Metrics 문서를 참고.

공식 문서에서는 PushGateway는 앞서 설명한 배치 잡과 같은 제한된 상황에서만 사용하도록 강력하게 권고하고 있다. 그 이유는 아래와 같다.

  • 단일 PushGateway로 다수의 인스턴스를 모니터링하면 PushGateway는 장애지점이 되기 쉬우며 잠재적 병목현상 유발 컴포넌트가 된다.
  • Exporter를 통해 scrape 할 때마다 up 메트릭을 이용한 Prometheus 서버의 자동 인스턴스 health 모니터링 지표를 볼 수 없다.
  • Pushgateway 특성 상 한 번 들어온 시리즈는 수동으로 API를 통해 삭제하지 않는 한 Prometheus 서버에 지속적으로 노출된다.

Proxy Forwarding 등을 통해 접근할 수 없는 곳에 모니터링 대상이 있는 경우에도 사용하는 대안이라는 글을 많이 본 것 같다. 아직 공감은 되지 않음.

Prometheus server

Retreival

Service Discovery로부터 모니터링 대상 목록을 읽고, Job 들을 순회하면서 Pull 방식으로 metric 요청을 보내고 받아오는 역할을 함.

Service Discovery

위의 Jobs/Exporters에서 prometheus.yml에 scrape_config 속성을 작성하여 메트릭을 수집할 대상을 IP로 지정하였다. 하지만, 실제 컨테이너 운영환경에서는 스케일링, 재배포, 업데이트 등의 이유로 인해 애플리케이션에 할당된 IP가 동적이기 때문에 바람직한 방법이라고 생각되지는 않는다. Service Discovery(SD)는 메트릭 수집 대상 컴포넌트에 대한 메타데이터를 갖고있는 대상을 탐색하여 수집 대상에 대한 정보를 Label로 구분하고 항상 최신화 하여 이러한 문제점을 해결할 수 있다.

Label

service discovery를 이해하기 위해 잠깐 Prometheus의 label에 대해 짚고 넘어가야 한다. Prometheus의 scrape target 은 모두 labels라는 속성이 붙을 수 있는데 kubernetes의 label과 비슷하게 생각할 수 있다. label은 아래와 같은 순서로 적용이 된다.

  1. Prometheus 서버에서 수집되는 모든 타겟에 붙는 global label.
  2. 각 scrape 설정마다 기본으로 설정되는 job label.
  3. scrape 설정 내에 타겟 그룹마다 설정되는 label.
  4. relabeling에 의해 생성되는 label.

단계를 차례대로 지나서 설정이되며 이전 단계와 충돌되는 label은 모두 덮어 씌워진다. 여기서 relabeling 이라는 기법을 통해 기존의 label을 변경하거나 값을 합쳐서 새로운 label을 만들수도 있다. relabeling은 아래와 같이 정의하고 새로 labeling이 이루어진다.

relabel_configs:
- source_labels: ['label_a', 'label_b'] # source label들을 정의한다.
  separator:     ';'                    # 선택된 label들은 separator 와 함께 접합된다. ("foo;bar")
  regex:         '(.*);(.*)'            # 정규표현식으로 match되는 문자열을 정의한다. (["foo", "bar"])
  replacement:   '${1}-${2}'            # regex에서 match된 문자열을 기준으로 만들 값을 정의한다.("foo-bar")
  target_label:  'label_c'              # target_label에 replacement 값을 할당한다. 
돌아와서

미리 수집 대상에 label을 정의해두고 SD를 통해 얻어진 값을 위의 relabling을 통해 항상 최신 값을 유지할 수 있게 해준다. 원활한 메타데이터 참조를 위해 prometheus에서 미리 정의해둔 설정들이 존재한다. (Kubernetes의 SD 설정, 다른 SD 설정 살펴보기)

TSDB

Time Series database의 약자. Local 스토리지에 저장하는 것이 default이며 Remote 스토리지 통합과 OpenMetrics 포맷으로 Backfilling 등도 지원하고 있다.

Backfilling 이전 시점의 데이터를 채우는 것을 뜻 함.

두 시간 단위로 최신 데이터 블록을 보관하다가 압축을 시도하며 기본 데이터 유지기간은 15일이다. storage.tsdb.retention.time, storage.tsdb.retention.size 등의 옵션을 통해 데이터 유지기간을 변경하거나 용량제한으로 데이터 유지를 지정할 수 있다.

HTTP Server

TSDB에 저장된 데이터를 외부에서 조회하기 위한 HTTP API 서버. /api/v1로 시작하는 uri를 통해 조회할 수 있다. 자세한 API 이용 방법은 공식문서 참고.

Alertmanager

Prometheus server에서 보내는 알림을 처리하는 컴포넌트. 알림 규칙은 Prometheus server에서 설정하고 Prometheus server는 이 규칙을 통해 Alertmanager로 알림을 보낸다. Alertmanager는 중복 알람을 제거하거나 그룹핑을 하고 이메일이나 PagerDuty, Slack과 같은 reciver integeration에 알맞게 보내는 역할을 한다. 알림이 오지 않도록 하거나 (Silencing / 알림규칙 변경과 다른 개념) 알림을 억제하는 역할을 한다. (Inhibition / 알림 우선순위, 위험순위 등에 따라 하위 경고 알림 등을 배제하는 것)

자세한 Alertmanager 설정은 공식문서 참고.

Prometheus web ui

Prometheus에서 기본적으로 제공하는 Metric UI 도구. PromQL을 통해 원하는 메트릭을 조회할 수 있으나, Grafana를 통해 더 보기좋게 시각화가 가능하기 때문에 일반적으로 Grafana를 선호하는 편.

설치

Kubernetes에 Prometheus를 설치하기 위해 manifest를 다운받고 설치하는 방법도 있지만 필요한 각종 Exporter, Prometheus Server, 시각화 툴 등 여러가지를 다 따로 설치해야하는 번거로움이 있다.

prometheus community에서 제공하는 helm 차트를 통해 아주 간단하게 설치가 가능하다. (helm chart 저장소)

우선 helm repo를 아래와 같이 추가해준다.

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

그리고 helm search repo prometheus-community 명령어로 설치가 가능한 차트와 버전 등을 확인할 수 있다.

Github을 통해 저장소에 들어가보면 charts 폴더 내에 유용한 exporter들을 포함한 여러 개의 차트가 존재하는 것을 확인할 수 있다.

prometheus-community-helm-charts

여기서 kube-prometheus-stack 를 선택하여 설치하도록 하겠다. 해당 차트는 비교적 최근에 나온 Prometheus Operator를 이용하여 자동으로 모니터링 스택을 정의하고 내 클러스터에 맞는 설정과 배포 또한 진행한다.

Kubernetes 오퍼레이터 패턴
사용자 정의 리소스(CRD)를 이용하여 애플리케이션 및 해당 컴포넌트를 관리하는 쿠버네티스 익스텐션을 이용하는 패턴. 오퍼레이터는 쿠버네티스의 컨트롤러처럼 동작. (정의한 상태가 되도록 작업 수행) (오퍼레이터 패턴에 대한 자세한 연구는 다음 글에…)

helm 저장소에 prometheus-community를 추가한 뒤에 아래의 명령어로 설치가 가능하다.

helm install -n [TARGET_NAMESPACE] [REALEASE_NAME] prometheus-community/kube-prometheus-stack

아래는 Prometheus Operator를 통해 정의된 사용자 정의 리소스들. prometheus-crds

특히 이 차트는 Prometheus server뿐만 아니라 Kubernetes 클러스터 메트릭을 수집할 수 있는 두 가지 Exporter(kube-state-metrics, node-exporter)와 Grafana도 같이 설치해주고 심지어 미리 정의된 대시보드까지 같이 있다.

Prometheus의 구조를 모르더라도 명령어 한 줄로 가장 기본적이면서 공통적인 metric을 바로 수집할 수 있다. Operator가 설치를 완료하기까지 잠시 기다리면 바로 사용이 가능한 Prometheus 서버와 Grafana 대시보드를 확인할 수 있다.

아래의 명령어를 통해 설치가 완료된 것을 확인했다면,

kubectl -n [TARGET_NAMESPACE] get pods -l "release=[RELEASE_NAME]"

포트포워딩을 통해 http://localhost:3000 에서 grafana 대시보드에 접속이 잘 되는지 확인한다.

kubectl -n [TARGET_NAMESPACE] port-forward deployment/prometheus-grafana 3000

kube-prometheus-stack 차트를 통해 설치한 grafana의 관리자 계정과 비밀번호는 아래와 같다.

username : admin
password : prom-operator

만약 port-forward를 이용하지 않거나 localhost를 통해 확인할 수 없는 상황이라면(원격 서버를 통한 작업 등) 아래의 Grafana Ingress 섹션을 확인.

Grafana Ingress

주의 Ingress, Service, Endpoint 등 Kubernetes의 네트워크 관련 오브젝트에 대한 기초 지식이 필요하며 클러스터에서 사용할 수 있는 ingress-controller가 배포되어 있어야 함.

방금 설치한 Grafana는 kubernetes 클러스터 내의 서비스이기 때문에 클러스터의 외부에서 접근하려면 로드 밸런서와 Ingress controller, Ingress가 필요하다. 클라우드 벤더(AWS, GCE, Azure)를 이용한다면 벤더가 제공하는 로드밸런서에 맞는 Ingress controller를 설치하고 Ingress를 만들기만 하면 바로 외부에서 접근이 가능하겠지만, On-premise 환경에서는 로드밸런서를 구축하는게 어렵고 불확실한 일이기 때문에 다른 방법을 써야한다.

On-premise 환경에서 할 수 있는 방법은 nginx-ingress-controller를 설치하고 이 서비스를 NodePort나 ExternalIP 방식으로로 노출시켜 접근하는 것이다. MetalLB 같은 BareMetal용 LB 컴포넌트도 있긴 하지만 어디까지나 베타 버전이기도 하기 때문에 조심스러움. 설치는 공식 문서를 확인. 아래는 nginx-ingress-controller가 클러스터 내의 서비스에 외부 접근을 전달하는 그림. nginx-ingress-controller

이런 식으로 nginx-ingress-controller를 설치하여 구성하였다면 아래와 같이 Ingress resource를 생성하는 manifest를 작성한다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: "nginx"
    ingress.kubernetes.io/ssl-passthrough: "true"
    kubernetes.io/ingress.allow-http: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
  name: grafana-ingress
  namespace: default
spec:
  rules:
    - http:
        paths:
        - path: /grafana/?(.*)
          pathType: Prefix
          backend:
            service:
              name: prometheus-grafana
              port: 
                number: 80

nginx-ingress 서비스를 NodePort로 구성하였기 때문에 pathrewrite-target 속성을 이용하여 /grafana뒤에 오는 uri를 응답으로 다시 쓰도록 하였다. 이렇게하면 기본 uri가 마치 http://<노드IP>:<Ingress 서비스포트>/grafana 인 것 처럼 작동하고 서버에서 보내는 redirect 요청도 정상적으로 동작한다. 그러나 해당 uri로 접속하니 redirect가 발생했고 기대한 요청은 http://<노드IP>:<Ingress 서비스포트>/grafana/login이였지만 실제로 발생한 요청은 http://<노드IP>:<Ingress 서비스포트>/login 이였다.

해결 방법은 Grafana 자체에서 helm 차트를 통해 배포하는 grafana 설정파일(grafana.ini)에 서버 root_url과 관련된 설정을 넣는 것.

helm 차트를 클론하여 수정하는 방법도 존재하지만, ConfigMap에서 prometheus-grafana를 찾아 아래 부분을 추가하면 된다.

[server]
domain = example.com
root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/
serve_from_sub_path = true

nginx-ingress가 NodePort로 서비스되면서 reverse proxy처럼 동작해 발생한 문제였다. 이렇게 수정하고나면 http://<노드IP>:<Ingress 서비스포트>/grafana uri를 통해 정상적으로 배포된 grafana에 접속이 가능하다. (참고 : Run Grafana behind a reverse proxy)

참고

Kuberenetes 모니터링 아키텍쳐

댓글 달기

Kuberenetes 모니터링 아키텍쳐

Kubernetes cluster는 여러 대의 노드로 구성되며 이 노드 안에는 또 다시 Container, Pod, Service 등과 같은 애플리케이션 운영 요소들이 자리잡고 있다. 여러 개의 애플리케이션을 운영할 때 어느 애플리케이션이 리소스를 많이 사용하는지, 어느 서비스에 네트워크 I/O가 부하가 걸리는지, 병목현상이 어디서 일어나는지에 대해 파악이 가능하고 이에 맞춰 미리 스케일아웃이 필요한지 판단하는데에 큰 도움을 줄 수 있다고 생각한다.

Kubernetes 모니터링 아키텍쳐

용어 설명

모니터링 아키텍쳐를 설명하기 전에 Kubernetes 에서는 메트릭을 아래의 두 가지로 나누고 있다.

  • System metrics : 모든 개체에서 확인할 수 있는 일반적인 메트릭 (e.g. 컨테이너, 노드 등에서 확인할 수 있는 CPU/memory 사용량) Core metrics와 non-core metrics로 구분
    • core metrics : kubernetes가 이해하고 내부 컴포넌트의 동작과 코어 유틸리티 등을 운영하는데에 사용됨. 리소스 예측, 초기 자원이나 수직적 오토 스케일링, 클러스터 오토 스케일링, HPA(Horizontal Pod Autoscailer) 등을 포함하는 scheduling이나 kubernetes dashboard, kubectl top 과 같은 명령어를 통해 직접적으로 사용되는 메트릭.
    • non-core metrics : kubernetes에서 이해할 수 없으며 코어 메트릭에 포함되는 정보와 더불어 다른 메트릭도 포함된 것.
  • Service metrics : 애플리케이션 코드에 의해 정의되고 관측되는 메트릭. (e.g. API 서버에서 발생한 500에러 횟수) kubernetes infra 요소에 의해 생성되는 메트릭과 유저 애플리케이션에 의해 생성되는 메트릭으로 구분한다. HPA 를 위한 입력값으로도 사용되는 서비스 메트릭은 custom metric이라고 부르기도 함.

Kubernetes는 애플리케이션의 상태를 보기위한 메트릭만 모니터링의 대상으로 정의하기 때문에 애플리케이션에서 발생되는 로그(사용자 행위 등)는 모니터링 대상에서 제외하고 생각한다. (쿠버네티스에서 자체적으로 지원하지 않는다는 뜻)

아키텍쳐 요구사항

쿠버네티스에서 정의하는 모니터링 아키텍쳐에 대한 요구사항은 아래와 같다.

  • Kubernetes의 코어 솔루션을 포함하고
    • node, pod, container의 코어 시스템 메트릭이 Kubernetes 코어 API에서 사용이 가능하도록(이해가 가능하도록) 해야함.
    • Kubelet이 코어 kubernetes 컴포넌트가 정상적으로 동작하도록 하는 제한된 메트릭만 내보낼 수 있게 해야함. (…?)
    • 5000개의 노드까지 스케일업 가능해야 함.
    • 모든 Deployment 구성에서 실행될 수 있도록 충분히 작아야 함.
  • 초기 자원과 vertical pod autoscaling 및 클러스터 분석 쿼리를 지원하는 등 코어 Kubernetes에만 의존하는 기록 데이터를 제공하는 뛰어난(out-of-the-box) 솔루션이 포함되어야 함.
  • 코어 Kubernetes가 아니면서 HPA와 같은 서비스 메트릭을 요구하는 컴포넌트와 통합될 수 있는 서드파티 모니터링 솔루션을 허용해야 함. (외부 모니터링 솔루션과 충돌하지 않아야 한다는 뜻인듯..)

아키텍쳐

본격적인 Kubernetes 모니터링의 아키텍쳐. Core metrics pipeline과 Monitoring pipeline 두 가지로 구분지어 아키텍쳐를 설계하였다.

Core metrics pipeline

Kublet, resource estimetor, metric-server(경량화된 Heapster), master metrics API를 서빙하는 API 서버 들의 요소로 구성되어 있다. 이 메트릭들은 스케줄링 로직과 같은 코어 시스템 컴포넌트나 kubectl top 과 같은 간단한 UI 컴포넌트에서 사용된다. 서드파티 모니터링 시스템과 의 통합은 고려하지 않음.

리소스 메트릭을 이용해 HPA가 동작하기 위해서는 metric-server가 필요하다 한다. kubeadm으로 클러스터를 구성하였다면 https://github.com/kubernetes-sigs/metrics-server/releases 에서 yaml을 받아 배포할 것.

Monitoring pipeline

core metrics만을 위한 Metrics pipeline을 따로 설계한 이유는 코어 컴포넌트에 사용하지 않기 때문에 유연한 모니터링 파이프라인을 구성하기 위해서라고 한다. 쿠버네티스는 자체적인 Monitoring pipeline을 제공하지는 않지만 간단하게 설치가 가능하다.

모니터링 파이프라인에는 아래의 메트릭들이 포함될 수 있다.

  • Core system metrics
  • non-core system metrics
  • 사용자 애플리케이션 컨테이너에서 나온 service metrics
  • kubenetes infra 컨테이너에서 나온 service

Infrastore
쉽게 정리 : 생성된 메트릭들을 다양한 유즈케이스를 재확인하기 위해 필요한 쿼리들을 재사용 가능한 형태로 저장해두고 API로 제공하는 컴포넌트

코어 시스템 메트릭과 이벤트에 대한 질의(historical queries)를 제공하는 컴포넌트. 하나 이상의 API를 노출하여 다음과 같은 경우의 유즈케이스를 핸들링한다.

  1. 초기 자원할당
  2. 수직적 오토스케일링
  3. oldtimer API
  4. debugging, capacity planning 등을 위한 decision-support queries
  5. Kubernetes Dashboard 내 사용량 그래프

모니터링 파이프라인은 위와 같은 다양한 메트릭을 수집하여 커스텀 메트릭을 보고 오토스케일링을 하는 HPA나 Infrastore에 이러한 메트릭들을 제공하기 위해 사용된다. 주로 사용되는 모니터링 파이프라인 조합은 아래와 같다.

  • cAdvisor + collectd + Heapster
  • cAdvisor + Prometheus
  • snapd + Heapster
  • snapd + SNAP cluster-level agent
  • Sysdig

cAdvisor
컨테이너에 대한 정보를 수집하고 가공하여 내보내는 데몬 서비스. 기간별 리소스 사용량, 리소스 분리 매개 변수, 네트워크 통계 등에 대한 정보를 유지한다.

(원문에는 cAdvisor + Prometheus 조합을 예시로 모니터링 파이프라인 수집을 설명하는 부분이 있음. / 여기서는 다음 포스팅에 프로메테우스의 기본 동작을 설명하기 위해 생략)

monitoring-architecture

검은 색은 core metric pipeline 구성요소, 파란 색은 monitoring pipeline 구성요소

정리

다소 어려운 내용이였지만 kubernetes 모니터링 구조의 설계 원칙과 요구사항, 유연한 파이프라이닝을 위한 core metric과 나머지 metric으로 분리한 구조, 수집된 metric을 kubernetes에서 어떻게 사용하는지 등을 이해하는데 도움이 되었다. 물론 실제로 모니터링 시스템을 구축하는데 위와 같은 내용을 다 알아야 할 필요는 없겠지만 향후 클러스터를 확장하게 되면 동작의 이해가 필요할 것 같아서 정리해보았다.

참고

Kubernetes - 애플리케이션 배포(manifest vs helm)

댓글 달기

필수개념편에서 쿠버네티스 오브젝트를 정의하여 클러스터의 의도된 상태를 표현한다고 했다. Pod, Service, Volume 등 각기 다른 종류의 오브젝트를 정의하여 우리가 쿠버네티스 클러스터 안에서 운영하고자 하는 애플리케이션을 만들고 서비스할 수 있다.

이번 편에서는 쿠버네티스 오브젝트를 통해 애플리케이션을 배포하는 방법을 비교해보고 내가 실제로 운용할 서버에서는 어떤 방법을 선택했는지에 대해 적어보려 한다.

Manifest

이전 편까지 살펴본 쿠버네티스 오브젝트는 yaml 형태로 정의하여 사용하고는 했다. 대체로 공식 홈페이지를 보면 yaml 파일과 kubectl apply -f <path> 명령어를 이용하는 예제가 많다.

아래는 쿠버네티스 클러스터 구성 후 기존의 pulse 백엔드 애플리케이션을 쿠버네티스에 배포하는 manifest이다.

apiVersion: v1
kind: Namespace
metadata:
  name: pulse
  labels:
    app: pulse
---
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: pulse-api
  name: pulse-api
  namespace: pulse
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pulse-api
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: pulse-api
    spec:
      containers:
      - image: move02/pulse-api:beta.v1
        name: pulse-api
        ports:
          - containerPort: 8080
        resources: {}
      imagePullSecrets:
        - name: docker-auth
status: {}
---
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: pulse-api
  name: pulse-api
  namespace: pulse
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  # - name: https
  #   port: 443
  #   protocol: TCP
  #   targetPort: 8080
  selector:
    app: pulse-api
  type: ClusterIP
status:
  loadBalancer: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: "nginx"
    ingress.kubernetes.io/ssl-passthrough: "true"
    kubernetes.io/ingress.allow-http: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
  name: backend-ingress
  namespace: pulse
spec:
  rules:
    - http:
        paths:
        - path: /city(/|$)(.*)
          pathType: Prefix
          backend:
            service:
              name: pulse-api
              port: 
                number: 80

pulse 라는 namespace를 만들고 Deployment, Service, Ingress의 스펙을 정의하였다. Deployment에 컨테이너 레지스트리를 따로 설정하지 않으면 dockerhub에서 가져오며 프라이빗 이미지일 경우 인증용 Secret이 따로 필요하다. (Secret을 만드는 방법은 공식문서 참고)

이렇게 manifest를 직접 작성해서 애플리케이션 배포에 필요한 오브젝트를 생성할 수도 있지만, 배포할 애플리케이션의 복잡도가 높고 클러스터 운영 환경이 다양해질 경우(ex. 개발, 테스트, 프로덕션 등) 환경에 맞게 변경해야 할 설정이 다소 존재한다.(포트, ingress 룰, 레지스트리 등) 이럴때, 긴 파일을 다 찾아가며 값을 변경하기에는 번거롭다는 생각이 들었다.

Helm

Helm 쿠버네티스 애플리케이션을 설치하고 관리하게 해주는 툴이다. kubectl로도 가능한 일들이지만 조금 더 관리가 쉬워지며 운영체제의 yum, apt 등과 비슷한 포지션이라고 한다.

기본적으로 Helm Chart 라는 것을 이용해서 설치할 애플리케이션을 설치하고 업그레이드 하는데, 원격 repository를 등록하여 다양한 커뮤니티에서 만든 패키징된 애플리케이션을 손쉽게 설치할 수도 있다.

최신 버전을 이용하다 보니 Helm v3를 설치해서 사용하게 되었고, 이전까지는 쿠버네티스 클러스터에 Tiller 라는 서버사이드 애플리케이션을 배포하여 클라이언트/서버 구조로 운영하였지만 v3부터는 완전한 클라이언트/라이브러리 구조로 바뀌었다고 한다.

Helm chart

Helm을 이용할 때 가장 핵심이 되는 개념이다. 쿠버네티스 리소스와 관련된 셋을 설명하는 파일의 모음이며, 단순하게 Pod 하나만 배포할 수도 있고 복잡한 웹앱 형태로 애플리케이션을 배포할 수도 있다.

차트는 특정한 디렉터리 구조를 가진 파일들로 생성되며, 버전이 지정된 아카이브로 묶어서 배포할 수도 있고 배포된 차트를 끌어와서 바로 설치할 수도 있다. Helm이 패키지 관리에 더 용이하다는 점이 바로 이 점이다. 한 번 차트를 만들어 Helm repository에 배포해두면 배포된 차트를 설치할 때 단 한 줄의 명령어만 입력하면 된다.

아래는 쿠버네티스 클러스터를 모니터링하는 de-facto 표준인 프로메테우스를 Helm으로 설치하는 명령어와 설치 결과이다.

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# 여러가지 차트가 있지만, kube-state-metrics나 node-exporter, grafana 같은 모듈을 모두 한 번에 설치해주기 때문에 처음 설치한다면 이게 가장 편할 듯 함.
# 모든 차트 종류를 보려면 https://github.com/prometheus-community/helm-charts/tree/main/charts 방문
helm install prometheus prometheus-community/kube-prometheus-stack

prometheus-stack-install-through-helm

단 세 줄의 명령어로 저렇게 많은 오브젝트가 설치되었고 모니터링 대시보드 툴인 Grafana도 같이 설치되어 모니터링이 제대로 동작하는걸 확인할 수 있었다. 만약 설치 전에 기본 설정을 변경하고 싶다면 아무 이름으로 yaml파일을 만들고 차트의 values.yaml 중 수정하고싶은 속성만 지정하여 다시 작성하면 된다.

예시 차트를 통해 함께 설치되는 grafana의 관리자 비밀번호를 변경하고 싶다면 grafana.yaml 파일을 만들고

# grafana.yaml
grafana:
  adminPassword: mypassword

이런식으로 내용을 작성한 후

helm install -f </path/to/grafana.yaml> prometheus prometheus-community/kube-prometheus-stack

위의 명령어를 통해 커스텀한 설정을 반영하여 설치하면 된다.

모니터링 툴에 대한 자세한 이야기는 다른 일지에서 다룰 예정입니다.

Helm vs Manifest

Manifest에 비해 Helm이 다른 사람들이 배포한 애플리케이션을 설치하는 데에는 훨씬 간단하다는 것은 prometheus 설치 과정 예시를 통해 와닿았을 것이다. 그렇다면 직접 만든 애플리케이션을 배포하는 것은 어떨까?

Manifest 섹션에서 작성했던 pulse 백엔드 애플리케이션 배포를 위해 작성한 오브젝트 들을(네임스페이스 제외) Helm chart로 만들어 배포해보자.

먼저 helm 차트를 만들기 위한 구조를 만들어야 한다.

# 기존에 설치해둔 애플리케이션과 이름이 겹치지 않도록 h를 붙였다
helm create pulse-h

위 명령어를 실행하면 다음과 같은 파일들이 생성된다. helm-chart-structure

아래는 helm 공식문서에 나와있는 차트파일 구조에 대한 설명이다

Chart.yaml          # 차트에 대한 정보를 가진 YAML 파일
LICENSE             # 옵션: 차트의 라이센스 정보를 가진 텍스트 파일
README.md           # 옵션: README 파일
values.yaml         # 차트에 대한 기본 환경설정 값들
values.schema.json  # 옵션: values.yaml 파일의 구조를 제약하는 JSON 파일
charts/             # 이 차트에 종속된 차트들을 포함하는 디렉터리
crds/               # 커스텀 자원에 대한 정의
templates/          # values와 결합될 때, 유효한 쿠버네티스 manifest 파일들이 생성될 템플릿들의 디렉터리
templates/NOTES.txt # 옵션: 간단한 사용법을 포함하는 텍스트 파일

설명과 완전히 동일하지 않지만 얼추 비슷하게 생성이 되었다. (crds는 쿠버네티스의 커스텀 리소스를 정의하는 부분이라고 한다)

생성된 차트파일의 내용을 바꾸어서 SpringBoot 애플리케이션을 실행하는 컨테이너가 배포되고 이를 클러스터에서 서비스할 수 있도록 만들어야한다.

templates 안의 파일들은 Jinja 템플릿엔진을 사용하여 values.yaml이나 차트 release name 같은 외부 값을 참조하여 동적으로 manifest가 만들어지도록 되어있다. pulse 백엔드 같은 경우에는 templates/deployment.yaml 파일의 spec.template.spec.containers 속성 중 ports, livenessProbe, readinessProbe를 커스텀하기 위해 아래와 같이 변경하였다.

# templates/deployment.yaml
# ...생략...
containers:
  - name: 
    securityContext:
    image: ":"
    imagePullPolicy: 
    # 커스텀 시작 지점
    ports:
    livenessProbe: 
      httpGet: 
        path: 
        port: 
      initialDelaySeconds: 
    readinessProbe:
      httpGet:
        path: 
        port: 
      initialDelaySeconds: 
    # 커스텀 끝 지점
    resources:
# ...생략...

해당 설정은 Spring boot helm starter 깃허브 레포를 참고하여 변경하였다. (deployment.yaml 템플릿 외에도 ConfigMap 같은 다른 오브젝트도 추가하는 것이 운영 환경에서는 좋아보임)

이렇게 커스텀이 완료된 템플릿에 value를 씌워야하니 values.yaml 파일에 커스터마이징이 필요한 설정값을 채워넣는다.


# ... 생략 ...

replicaCount: 1

image:
  repository: move02/pulse-api
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: beta.v1

imagePullSecrets: 
  - name: docker-auth

# ... 생략 ...

containerPorts: 
  - name: http
    containerPort: 8080
    protocol: TCP
livenessProbe: 
  path: /actuator/health
  port: http
  initialDelaySeconds: 60
readinessProbe: 
  path: /actuator/health
  port: http
  initialDelaySeconds: 20

service:
  type: ClusterIP
  port: 80

# ... 생략 ...

중간중간 변경하지 않은 부분은 생략하였다. 당장 보기에는 기존에 작성했던 manifest보다 복잡해보이기는 하지만 한 번 만들어두면 여러 환경에 배포를 할 때 prometheus 설치할 때처럼 환경마다 변경이 필요한 설정만 바꾸면 쉽게 변경이 가능하다.

staging 환경이 아니라 production 환경에 맞는 이미지로 빌드하는 경우를 예로 들어 설정을 변경해보자. 안정성을 위해 replica의 갯수를 3으로 늘리고 image의 태그를 latest로 변경하고자 할 때 아래와 같이 파일을 작성한 후 명령어를 입력하면 된다.

# pulse-production-settings.yaml
replicaCount: 3
image:
  tag: latest
helm install -f pulse-production-settings.yaml pulse-api-production ./pulse-h 

실제로 한 번 차트를 만들어두면 다른 환경에 배포하거나 업그레이드를 할 때는

  1. 변경할 속성과 값만 정의하여 파일 만듦
  2. 해당 파일을 반영하여 install 또는 upgrade (파일을 만들지 않고도 커맨드 라인의 –set 옵션을 이용해 간단하게 반영도 가능)

이 두 가지 과정만 거쳐도 된다. helm install 또는 upgrade의 자세한 명령어는 Helm 공식 문서 참고.

많은 설정들이 생략되었고 실제 운영환경에 서비스를 배포하는 단계는 아니기에 예제를 최대한 가볍게 만들었다.

개인적으로 생각해본 운영서버 애플리케이션 배포 방침

  1. 외부 애플리케이션의 설치는 가능한 Helm을 통해 설치할 것
    1. values의 변경이 필요한 경우 반드시 변경 되는 값을 파일로 저장하여 반영
    2. 오퍼레이터 패턴으로 배포하는 제공하는 애플리케이션의 경우 오퍼레이터를 helm 차트로 설치할 것.
    3. 지원하는 repository가 없을 경우에만 manifest를 만들고 note.txt나 readme를 꼭 남길 것.
  2. 직접 애플리케이션을 빌드하여 설치 또는 배포하는 경우
    1. helm 차트를 생성하여 파일의 형태로 관리자에게 공유
    2. 어렵다면 빌드된 docker image와 애플리케이션 실행 환경(서비스 포트, 프레임워크 정보, health 체크 경로 등) 관리자에게 공유
    3. CI/CD 형태로 커맨드라인 건드리는 일 없이 가능한지? 고민해봐야함

참고

Kuberenetes 애플리케이션 배포(Manifest vs Helm)

댓글 달기

필수개념편에서 쿠버네티스 오브젝트를 정의하여 클러스터의 의도된 상태를 표현한다고 했다. Pod, Service, Volume 등 각기 다른 종류의 오브젝트를 정의하여 우리가 쿠버네티스 클러스터 안에서 운영하고자 하는 애플리케이션을 만들고 서비스할 수 있다.

이번 편에서는 쿠버네티스 오브젝트를 통해 애플리케이션을 배포하는 방법을 비교해보고 구축중인 데이터 서버에서는 어떤 방법을 선택했는지에 대해 적어보려 한다.

Manifest

이전 편까지 살펴본 쿠버네티스 오브젝트는 yaml 형태로 정의하여 사용하고는 했다. 대체로 공식 홈페이지를 보면 yaml 파일과 kubectl apply -f <path> 명령어를 이용하는 예제가 많다.

아래는 쿠버네티스 클러스터 구성 후 백엔드 애플리케이션을 쿠버네티스에 배포하는 manifest이다.

apiVersion: v1
kind: Namespace
metadata:
  name: myapp
  labels:
    app: myapp
---
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: myapp-backend
  name: myapp-backend
  namespace: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp-backend
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: myapp-backend
    spec:
      containers:
      - image: move02/myapp-backend:beta.v1
        name: myapp-backend
        ports:
          - containerPort: 8080
        resources: {}
      imagePullSecrets:
        - name: docker-auth
status: {}
---
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: myapp-backend
  name: myapp-backend
  namespace: myapp
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: myapp-backend
  type: ClusterIP
status:
  loadBalancer: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: "nginx"
    ingress.kubernetes.io/ssl-passthrough: "true"
    kubernetes.io/ingress.allow-http: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
  name: backend-ingress
  namespace: myapp
spec:
  rules:
    - http:
        paths:
        - path: /app(/|$)(.*)
          pathType: Prefix
          backend:
            service:
              name: myapp-backend
              port: 
                number: 80

myapp 라는 namespace를 만들고 Deployment, Service, Ingress의 스펙을 정의하였다. Deployment에 컨테이너 레지스트리를 따로 설정하지 않으면 dockerhub에서 가져오며 프라이빗 이미지일 경우 인증용 Secret이 따로 필요하다. (Secret을 만드는 방법은 공식문서 참고)

이렇게 manifest를 직접 작성해서 애플리케이션 배포에 필요한 오브젝트를 생성할 수도 있지만, 배포할 애플리케이션의 복잡도가 높고 클러스터 운영 환경이 다양해질 경우(ex. 개발, 테스트, 프로덕션 등) 환경에 맞게 변경해야 할 설정이 다소 존재한다.(포트, ingress 룰, 레지스트리 등) 이럴때, 긴 파일을 다 찾아가며 값을 변경하기에는 번거롭다는 생각이 들었다.

Helm

Helm 쿠버네티스 애플리케이션을 설치하고 관리하게 해주는 툴이다. kubectl로도 가능한 일들이지만 조금 더 관리가 쉬워지며 운영체제의 yum, apt 등과 비슷한 포지션이라고 한다.

기본적으로 Helm Chart 라는 것을 이용해서 설치할 애플리케이션을 설치하고 업그레이드 하는데, 원격 repository를 등록하여 다양한 커뮤니티에서 만든 패키징된 애플리케이션을 손쉽게 설치할 수도 있다.

최신 버전을 이용하다 보니 Helm v3를 설치해서 사용하게 되었고, 이전까지는 쿠버네티스 클러스터에 Tiller 라는 서버사이드 애플리케이션을 배포하여 클라이언트/서버 구조로 운영하였지만 v3부터는 완전한 클라이언트/라이브러리 구조로 바뀌었다고 한다.

Helm chart

Helm을 이용할 때 가장 핵심이 되는 개념이다. 쿠버네티스 리소스와 관련된 셋을 설명하는 파일의 모음이며, 단순하게 Pod 하나만 배포할 수도 있고 복잡한 웹앱 형태로 애플리케이션을 배포할 수도 있다.

차트는 특정한 디렉터리 구조를 가진 파일들로 생성되며, 버전이 지정된 아카이브로 묶어서 배포할 수도 있고 배포된 차트를 끌어와서 바로 설치할 수도 있다. Helm이 패키지 관리에 더 용이하다는 점이 바로 이 점이다. 한 번 차트를 만들어 Helm repository에 배포해두면 배포된 차트를 설치할 때 단 한 줄의 명령어만 입력하면 된다.

아래는 쿠버네티스 클러스터를 모니터링하는 de-facto 표준인 프로메테우스를 Helm으로 설치하는 명령어와 설치 결과이다.

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# 여러가지 차트가 있지만, kube-state-metrics나 node-exporter, grafana 같은 모듈을 모두 한 번에 설치해주기 때문에 처음 설치한다면 이게 가장 편할 듯 함.
# 모든 차트 종류를 보려면 https://github.com/prometheus-community/helm-charts/tree/main/charts 방문
helm install prometheus prometheus-community/kube-prometheus-stack

prometheus-stack-install-through-helm

단 세 줄의 명령어로 저렇게 많은 오브젝트가 설치되었고 모니터링 대시보드 툴인 Grafana도 같이 설치되어 모니터링이 제대로 동작하는걸 확인할 수 있었다. 만약 설치 전에 기본 설정을 변경하고 싶다면 아무 이름으로 yaml파일을 만들고 차트의 values.yaml 중 수정하고싶은 속성만 지정하여 다시 작성하면 된다.

예시 차트를 통해 함께 설치되는 grafana의 관리자 비밀번호를 변경하고 싶다면 grafana.yaml 파일을 만들고

# grafana.yaml
grafana:
  adminPassword: mypassword

이런식으로 내용을 작성한 후

helm install -f </path/to/grafana.yaml> prometheus prometheus-community/kube-prometheus-stack

위의 명령어를 통해 커스텀한 설정을 반영하여 설치하면 된다.

모니터링 툴에 대한 자세한 이야기는 다른 일지에서 다룰 예정입니다.

Helm vs Manifest

Manifest에 비해 Helm이 다른 사람들이 배포한 애플리케이션을 설치하는 데에는 훨씬 간단하다는 것은 prometheus 설치 과정 예시를 통해 와닿았을 것이다. 그렇다면 직접 만든 애플리케이션을 배포하는 것은 어떨까?

Manifest 섹션에서 작성했던 myapp 백엔드 애플리케이션 배포를 위해 작성한 오브젝트 들을(네임스페이스 제외) Helm chart로 만들어 배포해보자.

먼저 helm 차트를 만들기 위한 구조를 만들어야 한다.

# 기존에 설치해둔 애플리케이션과 이름이 겹치지 않도록 h를 붙였다
helm create myapp-h

위 명령어를 실행하면 다음과 같은 파일들이 생성된다. helm-chart-structure

아래는 helm 공식문서에 나와있는 차트파일 구조에 대한 설명이다

Chart.yaml          # 차트에 대한 정보를 가진 YAML 파일
LICENSE             # 옵션: 차트의 라이센스 정보를 가진 텍스트 파일
README.md           # 옵션: README 파일
values.yaml         # 차트에 대한 기본 환경설정 값들
values.schema.json  # 옵션: values.yaml 파일의 구조를 제약하는 JSON 파일
charts/             # 이 차트에 종속된 차트들을 포함하는 디렉터리
crds/               # 커스텀 자원에 대한 정의
templates/          # values와 결합될 때, 유효한 쿠버네티스 manifest 파일들이 생성될 템플릿들의 디렉터리
templates/NOTES.txt # 옵션: 간단한 사용법을 포함하는 텍스트 파일

설명과 완전히 동일하지 않지만 얼추 비슷하게 생성이 되었다. (crds는 쿠버네티스의 커스텀 리소스를 정의하는 부분이라고 한다)

생성된 차트파일의 내용을 바꾸어서 SpringBoot 애플리케이션을 실행하는 컨테이너가 배포되고 이를 클러스터에서 서비스할 수 있도록 만들어야한다.

templates 안의 파일들은 Jinja 템플릿엔진을 사용하여 values.yaml이나 차트 release name 같은 외부 값을 참조하여 동적으로 manifest가 만들어지도록 되어있다. myapp 백엔드 같은 경우에는 templates/deployment.yaml 파일의 spec.template.spec.containers 속성 중 ports, livenessProbe, readinessProbe를 커스텀하기 위해 아래와 같이 변경하였다.

# templates/deployment.yaml
# ...생략...
containers:
  - name: 
    securityContext:
    image: ":"
    imagePullPolicy: 
    # 커스텀 시작 지점
    ports:
    livenessProbe: 
      httpGet: 
        path: 
        port: 
      initialDelaySeconds: 
    readinessProbe:
      httpGet:
        path: 
        port: 
      initialDelaySeconds: 
    # 커스텀 끝 지점
    resources:
# ...생략...

해당 설정은 Spring boot helm starter 깃허브 레포를 참고하여 변경하였다. (deployment.yaml 템플릿 외에도 ConfigMap 같은 다른 오브젝트도 추가하는 것이 운영 환경에서는 좋아보임)

이렇게 커스텀이 완료된 템플릿에 value를 씌워야하니 values.yaml 파일에 커스터마이징이 필요한 설정값을 채워넣는다.


# ... 생략 ...

replicaCount: 1

image:
  repository: move02/myapp-backend
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: beta.v1

imagePullSecrets: 
  - name: docker-auth

# ... 생략 ...

containerPorts: 
  - name: http
    containerPort: 8080
    protocol: TCP
livenessProbe: 
  path: /actuator/health
  port: http
  initialDelaySeconds: 60
readinessProbe: 
  path: /actuator/health
  port: http
  initialDelaySeconds: 20

service:
  type: ClusterIP
  port: 80

# ... 생략 ...

중간중간 변경하지 않은 부분은 생략하였다. 당장 보기에는 기존에 작성했던 manifest보다 복잡해보이기는 하지만 한 번 만들어두면 여러 환경에 배포를 할 때 prometheus 설치할 때처럼 환경마다 변경이 필요한 설정만 바꾸면 쉽게 변경이 가능하다.

staging 환경이 아니라 production 환경에 맞는 이미지로 빌드하는 경우를 예로 들어 설정을 변경해보자. 안정성을 위해 replica의 갯수를 3으로 늘리고 image의 태그를 latest로 변경하고자 할 때 아래와 같이 파일을 작성한 후 명령어를 입력하면 된다.

# myapp-production-settings.yaml
replicaCount: 3
image:
  tag: latest
helm install -f myapp-production-settings.yaml myapp-api-production ./myapp-h 

실제로 한 번 차트를 만들어두면 다른 환경에 배포하거나 업그레이드를 할 때는

  1. 변경할 속성과 값만 정의하여 파일 만듦
  2. 해당 파일을 반영하여 install 또는 upgrade (파일을 만들지 않고도 커맨드 라인의 –set 옵션을 이용해 간단하게 반영도 가능)

이 두 가지 과정만 거쳐도 된다. helm install 또는 upgrade의 자세한 명령어는 Helm 공식 문서 참고.

많은 설정들이 생략되었고 실제 운영환경에 서비스를 배포하는 단계는 아니기에 예제를 최대한 가볍게 만들었다.

myapp 데이터 서버 애플리케이션 배포 방침

  1. 외부 애플리케이션의 설치는 가능한 Helm을 통해 설치할 것
    1. values의 변경이 필요한 경우 반드시 변경 되는 값을 파일로 저장하여 반영
    2. 지원하는 repository가 없을 경우에만 manifest를 만들고 note.txt나 readme를 꼭 남길 것.
  2. 직접 애플리케이션을 빌드하여 설치 또는 배포하는 경우
    1. helm 차트를 생성하여 파일의 형태로 관리자에게 공유
    2. 어렵다면 빌드된 docker image와 애플리케이션 실행 환경(서비스 포트, 프레임워크 정보, health 체크 경로 등) 관리자에게 공유

참고

Kubernetes - 필수개념 정리

댓글 달기

쿠버네티스를 운영하기 위한 기본 지식 정리. 쿠버네티스 구성요소와 관련된 가벼운 지식 소개는 https://www.redhat.com/ko/topics/containers/kubernetes-architecture 참고

개념 및 구성요소 정리

클러스터 구조

클러스터 전체를 관리하는 컨트롤러로써 마스터가 존재하고, 컨테이너가 배포되는 머신인 노드(worker node)가 존재한다.

  • 마스터 노드와 워커 노드
  • 마스터와 노드
  • control plane과 nodes

마스터는 control plane 이라는 이름으로도 불리우는데 워커 노드와 그 안의 pod 들을 관리하는 역할을 한다. 운영환경에서 마스터와 노드들은 fault-tolerance와 high availability를 위해 대부분 여러 서버에 걸쳐서 운영된다.

오브젝트

쿠버네티스를 이해하기 위한 가장 중요한 개념이다. 쿠버네티스가 상태를 관리하는 대상(리소스)을 통틀어 칭한는 단어. 여러가지 형태의 오브젝트를 제공하고, 새로운 오브젝트를 추가하기가 쉽기 때문에 확장성이 좋음.

공식 문서 에서는.. “쿠버네티스 시스템에서 영속성을 갖는 개체”, “하나의 의도를 담은 레코드(a record of intent)” 라고 소개하고 있으며 쿠버네티스 시스템(컨트롤 플레인) 은 클러스터가 “의도한” 상태가 되도록 동작한다고 소개하고 있다. (무슨 의미인지는 아직 잘 모르겠음.)

명세(Spec), 상태(Status)

오브젝트의 명세(spec)는 특성(설정 정보, 원하는 특징, 의도한 상태)을 기술한 설명서. yaml, json 등의 형태로 기술 가능. 오브젝트의 상태(status)는 쿠버네티스 시스템과 컴포넌트에 의해 제공되고 업데이트된 오브젝트의 현재 상태. 컨트롤 플레인은 모든 오브젝트의 상태를 사용자가 의도한 상태(desired state)와 일치시키기 위해 지속적이고 능동적으로 관리함.

Describe Object(오브젝트 기술)

쿠버네티스에 오브젝트를 생성하고 관리하기 위해서는 오브젝트에 대한 기본적인 정보와 함께 의도한 상태(desired state)를 기술한 오브젝트 spec을 제시해야 함. 대부분 .yaml 형태로 기술하여 kubectl에 제공하고, kubectl API는 이것을 json으로 변환하여 생성 요청을 한다.

아래는 nginx deployment 오브젝트의 예시와 필수 필드를 설명하는 것이다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2 # tells deployment to run 2 pods matching the template
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

생성된 (또는 제공되는) yaml 파일을 아래의 명령어를 입력해 실제 오브젝트 생성 요청을 할 수 있다. kubectl apply -f <object spec file>

아래는 공통적으로 모든 오브젝트가 가져야하는 필수 속성

  • apiVersion: 오브젝트를 생성하기 위해 사용하고 있는 쿠버네티스 API 버전
  • kind: 생성하고자 하는 오브젝트의 종류
  • metadata: 오브젝트의 이름, UID, namespace 등을 포함하여 오브젝트를 구분지어주는 데이터
  • spec: 오브젝트에 대한 의도한 상태(desired state of an object)

Pod

Pod 는 쿠버네티스에서 가장 작은 배포 단위의 컴퓨팅 자원이다.

하나 이상의 컨테이너를 담고있으며, 스토리지와 네트워크 자원을 공유하고 컨테이너 실행방법에 대한 명세 등을 포함함. 아래는 nginx 컨테이너를 포함하는 Pod의 명세 예시

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.14.2
    ports:
    - containerPort: 80

IP와 스토리지(볼륨)을 공유하는 특성 때문에 같은 Pod 내의 컨테이너끼리 localhost를 통해 통신이 가능하고, 다른 컨테이너의 로그 등을 수집할 수 있다.

핵심 애플리케이션과 이를 뒷받침 또는 모니터링하기 위한 프로그램을 같이 배포하는 패턴을 MSA에서 사이드카 패턴이라고 한다. (고 한다. 이 외에도 Ambassador, Adaptor Container 등 다양한 패턴이 있음) ex) 로그 수집기

Volume

일반적으로 컨테이너마다 로컬 디스크를 생성되지만 이는 영구적이지 않기 때문에 컨테이너의 재기동, 삭제와 관계없이 데이터를 영속시켜야 하는 경우에는 볼륨 이라는 특별한 스토리지를 사용해야 한다.

도커의 볼륨은 단순히 호스트의 디스크를 마운트하거나 다른 컨테이너에 있는 반면, 쿠버네티스는 여러 유형의 볼륨을 갖는다.

볼륨은 하나의 Pod에 있는 컨테이너들이 공유할 수 있는 외장디스크처럼 생각하면 편하다. 서로 다른 유형의 볼륨도 하나의 Pod에 붙여서 사용 가능함. (Pod 또는 Deployment 스펙에 정의하기 - spec.volumes)

라이프사이클에 따라 임시 볼륨과 퍼시스턴트 볼륨(PV)로 나뉘기도 하고 연결할 수 있는 볼륨의 종류(클라우드, 파일서버 종류 등)에 따라 나뉘기도 한다.

PV와 PVC에 대한 개념은 중요하므로 다음에 따로 정리할 예정.

Service

애플리케이션이 실행되는 컨테이너는 Pod위에 올려서 사용하는데 이 Pod는 여러 개를 묶어서 클러스터처럼 이용할 수도 있고, 장애가 발생하거나 하면 노드를 옮겨다닐 수 있기 때문에 IP 동적으로 변한다. 이처럼 동적으로 변하는 IP를 지속적으로 추적해서 안정적인 연결을 제공하기 위해 쿠버네티스에서는 서비스 라는 오브젝트를 사용한다.

라벨(레이블)과 라벨 셀렉터 라는 개념을 이용해서 Pod를 계속 추적하게 된다. Pod를 정의할때 메타데이터 속성에 라벨을 정의하고 서비스를 정의할 때 Pod에 붙어있는 라벨을 골라서 하나의 서비스로 묶어 제공할 수 있도록 라벨 셀렉터를 이용한다. 라벨과 셀렉터

아래는 세 개의 Pod을 하나의 Service로 묶는 예시이다.

kind: Pod
apiVersion: v1
metadata:
  app: pulse-api
spec:
  containers:
  - name: pulse1
    image: <my-image-name>:<tag>
    ports:
    - containerPort: 8080

---

kind: Pod
apiVersion: v1
metadata:
  app: pulse-api
spec:
  containers:
  - name: pulse2
    image: <my-image-name>:<tag>
    ports:
    - containerPort: 8080

---

kind: Pod
apiVersion: v1
metadata:
  app: pulse-api
spec:
  containers:
  - name: pulse3
    image: <my-image-name>:<tag>
    ports:
    - containerPort: 8080
kind: Service
apiVersion: v1
metadata:
  name: pulse-service
spec:
  selector:
    app: pulse-api
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

서비스를 퍼블리싱 하는데에 있어 몇 가지 유형이 있다.

  • ClusterIP: 서비스에 클러스터 내부 IP를 할당한다. 서비스 클러스터 내에서는 이 서비스에 접근이 가능하지만, 클러스터 외부에서는 외부 IP를 할당받지 못했기 때문에 클러스터 바깥쪽에서 이 서비스로 연결하고자 하자면 Ingress를 사용해야 한다.
  • LoadBalancer: 로드밸런서가 제공되는 상황에서 사용할 수 있는 방법으로, 외부 IP를 가진 로드밸런서를 할당함. 대부분 IaaS 플랫폼(GCP, AWS, Azure) 등에서 서비스를 노출시킬 때 사용하는 방법이며 Bare-metal 클러스터의 경우에는 MetalLB 같은 로드밸런서 구현체를 사용할 수 있지만 프로젝트가 아직 베타단계 수준이므로 고민해봐야 할 선택지이다. (레퍼런스도 아직까지 많지 않음.)
  • NodePort: 클러스터 IP뿐 아니라, 클러스터를 구성하는 모든 노드의 IP와 포트를 통해 접근이 가능하게 됨. 예를 들어 10.1.1.1 ~ 10.1.1.6 의 여섯 개의 노드로 구성된 클러스터 위에 nodePort 를 31234로 지정한 서비스를 배포하면 10.1.1.1:31234, 10.1.1.2:31234, … , 10.1.1.6:31234 모두에서 접근이 가능하다는 뜻.
  • ExternalName: 클러스터 내부에서 클러스터 외부에 있는 애플리케이션을 호출할 때 사용한다. (클러스터 외부에 있는 DB 호출 시) 일반적으로 exxternalName 속성에 DNS를 지정하는데 IP로 지정하고자 할 경우 Endpoints 리소스를 추가로 설정해야한다.

라벨과 라벨 셀렉터만 가지고도 마법처럼 Pod을 묶어 부하분산과 Fail over를 지원하는 점이 인상깊지만 내부 동작은 훨씬 더 복잡하니 다음 기회에 더 자세히 공부해서 정리하도록 한다. (+ 쿠버네티스 네트워크)

Namespace

동일한 쿠버네티스 클러스터 내에서 구분지을 수 있는 논리적인 가상 클러스터를 네임스페이스라고 한다.

공식 문서에서는 사용자가 거의 없거나 수십명 정도 되는 경우에 네임스페이스를 고려할 필요가 없다고 하지만, 하나의 클러스터가 여러 사용목적을 가지면 논리적인 단위 구분을 통해 리소스를 구분지을 필요가 있다고 생각한다.

kube- 접두사로 시작하는 네임스페이스는 쿠버네티스 시스템용으로 예약되어 있으므로, 사용자가 사용하지 않도록 한다.

대부분의 리소스를 네임스페이스 별로 생성하여 관리할 수 있으며, 네임스페이스간 완전히 분리되는 것으로 서로 다른 네임스페이스의 Pod 간에도 통신이 가능하다.

다른 네임스페이스에 있는 서비스와 통신하고 싶을때는 빌트인 DNS 서비스를 이용하여 서비스 이름을 가르키면 된다.

<Service Name>.<Namespace Name>.svc.cluster.local

반대로 원하는 수준의 충분한 네임스페이스 간 네트워크 분리를 제공하지 않는다고 한다. 네트워크 정책 등을 이용하여 막을 수는 있지만, 결제나 보안과 같은 기능을 쿠버네티스에 올릴때는 높은 수준의 분리 정책을 원하는 경우에는 쿠버네티스 클러스터 자체를 분리하길 권장한다고 한다. (쿠버네티스 공식 블로그 - 네임스페이스)

라벨, 셀렉터

라벨은 쿠버네티스의 리소스를 선택하는데 사용이 된다. 각 리소스(오브젝트)는 라벨을 가질 수 있고 선택조건을 지정하여 해당 라벨을 가진 리소스만 선택이 가능하다. 아래와 같이 metadata 속성 안에 지정이 가능하고 key:value 형태로 정의한다. 생성 이후에 얼마든지 수정이 가능하다.

metadata: 
  labels: 
    release : stable,
    environment : production
    tier: backend

라벨 키는 슬래시(/)를 기준으로 prefix와 name으로 구분한다. <prefix>/<name> : value 아래는 prefix, name, value에 관한 규칙.

  • prefix는 선택이며, 253자를 넘지 않아야 한다.
  • prefix는 DNS의 하위 도메인으로 해야한다. (<- 개인적으로 이 말이 무슨 뜻인지 잘 이해가 안감.)
  • name은 필수 값이고 (당연하지만) 시작과 끝은 알파벳과 숫자이며, -, _, .과 함께 사용할 수 있다.
  • value는 비워둘 수도 있고 비워두지 않는다면 시작과 끝은 알파벳과 숫자이며, -, _, .과 함께 사용할 수 있다.

셀렉터는 이전의 Service에서 잠깐 언급했지만, 사용자가 지정한 라벨을 이용해 오브젝트들을 선택할 수 있게 해준다. 일치성 기준집합성 기준 이라느 두 가지 셀렉터를 지원하는데 =, =!로 질의를 하는지 in, notin로 질의하는지에 대한 차이이다. 조합해서 같이 사용할수도 있다.

어노테이션 이라는 사용자정의 메타데이터도 존재하는데, 라벨과의 차이는 쿠버네티스 시스템에서 식별을 하느냐 하지 않느냐의 차이이다. 단순하게 생각하면 ‘셀렉터에 잡히지 않는 메타데이터’ 라고 이해하면 될 듯 하다. 주로 다른 사용자에게 해당 리소스에 대해 알려주고싶은 유용한 정보(만든 사람, 참고url, 빌드번호, PR, commit id 등)를 작성.

라벨은 규칙 안에서 원하는대로 작성이 가능하지만 쿠버네티스에서 공식적으로 권장하는 레이블이 있으니 참고하면 좋을듯.

컨트롤러

본 문서의 오브젝트 섹션에서 컨트롤 플레인은 모든 오브젝트의 상태를 사용자가 의도한 상태(desired state)와 일치시키기 위해 지속적이고 능동적으로 관리한다고 했다. 정확하게 얘기하자면 컨트롤 플레인의 kube-controller-manager 컴포넌트가 컨트롤러를 구동하여 컨트롤러가 추적하는 리소스를 의도된 상태가 되게끔 명령을 내리는 것이다.

공식 홈페이지는 컨트롤러 및 컨트롤 루프를 온도 조절기에 비유하는데, 사용자가 설정한 온도가 의도한 상태(Desired state)이고 조절기는 실내 온도가 설정온도에 맞게끔 지속적으로 현재 상태의도한 상태에 맞추려고 동작한다. 각 컨트롤러는 지속적으로 클러스터의 리소스를 추적하면서 클러스터의 상태를 의도한 상태에 가깝도록 움직인다.

리소스를 개발자가 직접 손대지 않고 리소스의 의도한 상태를 정의하기만 하면 해당 상태로 되기 위해 만들기 위해 대신 일을 해주는 것이 컨트롤러 라고 이해하면 될 것 같다. 컨트롤러를 직접 생성하기 보다는 필요한 리소스를 추상화한 오브젝트를 명세하면 내장된 컨트롤러가 일을 해주는 것.

컨트롤러를 생성하는 대표적인 워크로드 리소스의 종류는 아래와 같다. (컨트롤러의 종류)

  • ReplicaSet : 여러 개의 Pod 집합을 안정적으로 유지하도록 하는 리소스. 명시된 파드 갯수에 대한 가용성 보증을 위해 정의하고 사용됨.
  • Deployment : ReplicaSet과 ReplicationController 상위 추상화하여 사용되는 리소스. 상태가 없는(stateless) 애플리케이션을 배포하고 업데이트 할 때 일반적으로 많이 사용됨. 기본적으로 Pod의 레플리케이션의 관리를 하면서도 롤백을 위한 기존 컨트롤러 관리 등 여러 기능을 포함함.
  • StatefulSet : 상태 유지가 필요한(stateful) Pod이 유지되도록 하는 리소스. Pod 집합의 디플로이먼트와 스케일링을 관리하고, 순서와 고유성을 보장한다. 같은 애플리케이션을 실행하는 Pod이더라도 각각의 역할이 다르다는 얘기. DB처럼 데이터를 영속시키거나 pod 간의 순서에 민감한 어플리케이션을 실행할 때 주로 사용된다.
  • Daemonset : 모든 노드에 동일한 Pod을 실행시키고자 할 때 사용하는 리소스. 주로 메트릭, 로그, 스토리지 등을 활용하고자 할 때 사용한다. (클러스터 내 네트워킹 관리를 위해 kube-proxy가 데몬셋으로 기본적으로 존재한다.)
  • Job : 배치성 프로세스를 위한 리소스. 스케줄링된 작업을 수행하기 위해 Pod을 만들고 그 안에서 작업이 완료될 때 까지 실행하는 Pod을 관리한다. (장애에 따라 pod 재 생성 또는 job 재 실행 등)
  • CronJob : Job과 동일하지만 cron을 이용해서 반복적으로 작업을 수행한다. Job 보다 상위의 컨트롤러로 Job을 관리할 수도 있다.
  • ReplicationController : 지정된 수의 Pod 레플리카가 계속 실행되도록 하는 리소스. replicaset을 유지하도록 조정하는 컨트롤러. 이 컨트롤러를 직접 생성하고 실행시키기 보다는 ReplicaSet을 구성하는 Deployment를 통해 Pod의 레플리케이션을 하는 방법이 권장된다. (직접 사용할 일 없다는 뜻)

각 리소스의 자세한 예시는 조금 더 시간이 지난 후에 컨트롤러에 대해 자세히 정리해보도록 하겠다.

참고

Kubernetes - 쿠버네티스 설치하기

댓글 달기

새로운 프로젝트를 진행하며 다량의 소셜 데이터를 분석하고, 저장, 변환하는 등 일련의 과정에 필요한 데이터 서버가 필요했다. 기존에 Elasticsearch 클러스터를 도커 컨테이너로 구성해서 운영중이였는데, 데이터 양이 점점 쌓일 경우 확장이 필요한데 컨테이너 환경에서 무중단 스케일 아웃이 어려운 편이고 물리서버 자체도 임시로 bare-metal 서버를 사용중이였기 때문에 스케일 업도 쉽지않은 상황이었다.

때마침, 새로운 IDC센터의 서버 구축이 완료되어 추가 서버자원 사용이 가능해졌고 냉큼 6대의 VM을 할당받고 쿠버네티스 클러스터를 구성하게 되었다.

설치 환경

6개의 VM을 이용

  • OS : Ubuntu 18.04
  • CPU : 8Core
  • RAM : 16GB

3개의 마스터(컨트롤 플레인) 노드와 3개의 워커노드로 구성할 예정

Install with Kubeadm

준비물

Linux(Debian, RedHat, 패키지매니저 없는 버전) 서버만 대상으로 함.

  • 2GB 이상의 RAM
  • 2코어 이상 CPU
  • 클러스터를 구성할 전체 노드의 네트워크 연결 가능성
  • 모든 노드에 대해 고유한 호스트 네임, MAC 주소, product_uuid
  • 특정 포트 개방(일단 방화벽을 비활성화 후 설치하는 것을 추천)
  • swap 비활성화 (swapoff -a)

설치 과정

1. MAC 주소, product_uuid 고유성 확인

  • MAC 주소 확인 : ip link 또는 ifconfig -a

    ip link show
    
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 52:54:00:87:9e:e6 brd ff:ff:ff:ff:ff:ff # 52:54: 로 시작하는 부분이 Network 인터페이스의 MAC 주소
    
  • product_uuid 확인 : sudo cat /sys/class/dmi/id/product_uuid

    (원문 참조) 일부 가상 머신은 동일한 값을 가질 수 있지만 하드웨어 장치는 고유한 주소를 가질 가능성이 높다. 쿠버네티스는 이러한 값을 사용하여 클러스터의 노드를 고유하게 식별한다. 이러한 값이 각 노드에 고유하지 않으면 설치 프로세스가 실패할 수 있다.

2. iptables가 브리지된 트래픽 확인할 수 있도록 하기

  • lsmod | grep br_netfilter 명령어를 통해 br_netfilter 모듈이 로드되었는지 확인
  • 명시적으로 로드하려면 sudo modprobe br_netfilter 를 실행

리눅스 노드의 iptables가 브리지된 트래픽을 올바르게 보기 위한 요구 사항으로, sysctl 구성에서 net.bridge.bridge-nf-call-iptables 가 1로 설정되어 있는지 확인해야 한다.

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
br_netfilter
EOF

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sudo sysctl --system

3. 필수 포트 확인

컨트롤 플레인 노드

프로토콜 포트범위 목적 사용자
TCP 6443 쿠버네티스 API 서버 모두
TCP 2379-2380 etcd 서버 클라이언트 API kube-apiserver, etcd
TCP 10250 kubelet API 자체, 컨트롤 플레인
TCP 10251 kube-scheduler 자체
TCP 10252 kube-controller-manager 자체

워커 노드

프로토콜 포트범위 목적 사용자
TCP 10250 kubelet API 자체, 컨트롤 플레인
TCP 30000-32767 NodePort 서비스 모두

4. 컨테이너 런타임 설치

쿠버네티스는 컨테이너 런타임 인터페이스(CRI)를 사용하여 사용자가 선택한 컨테이너와 소통함.

컨테이너 런타임 종류

  • docker
  • containerd
  • CRI-O

각 컨테이너 런타임은 공식 홈페이지를 통해 설치 (특별한 일이 없다면 도커를 추천)

CRI : Kubelet과 컨테이너 런타임을 통합시키는 API

5. cgroup 드라이버 구성 설정

cgroup(control groups의 약자)는 프로세스들의 자원 사용을 제한하고 격리시키는 리눅스 커널 기능. 보안과 자원효율을 위해 컨테이너 기술의 기반이 되는 요소.

cgroup에 대한 자세한 내용은 cgroup 소개 글을 읽어보는 것으로 대체하도록 함.

cgroup 별로 관리자가 하나씩 할당되며 같은 그룹 내에 할당된 리소스를 단순화하고 사용가능 / 사용중 인 리소스를 일관성있게 확인이 가능하지만, 관리자가 두 개인 경우 리소스도 두 개의 관점에서 보게됨.

대부분의 서비스가 kubernetes 클러스터 위에서 동작하는 것을 감안했을때 리눅스 init 시스템이 사용하는 드라이버와 docker, kubelet 의 cgroup 드라이버를 맞춰주는 것이 리소스 관리적인 면에서 효율적임.

kubernetes의 권장 cgroup 설정은 systemd이기 때문에 docker를 컨테이너 런타임으로 설정한 경우 cgroup을 동일하게 맞춰주어야 한다.

# docker daemon.json 설정 변경 (exec-opts 속성에 주목 / 나머지 설정은 사용자 환경에 맞게..)
cat <<EOF | sudo tee /etc/docker/daemon.json {
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF

# 데몬 리로드 및 도커 재시작
sudo systemctl daemon-reload
sudo systemctl restart docker

# docker cgroup 확인
docker info

kubernetes도 마찬가지로 cgroupDriver 속성의 값을 systemd로 바꿔주어야 한다. kubeadm 설정을 커스터마이징 할 수 있는 kubead-config 파일을 작성하고 설정한 내용대로 클러스터를 구성하도록 명령하면 된다. 아래는 cgroupDriver를 변경하는 가장 기본적인 예시.

# kubeadm-config.yaml
kind: ClusterConfiguration
apiVersion: kubeadm.k8s.io/v1beta3
kubernetesVersion: v1.21.0
---
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
cgroupDriver: systemd

설정파일을 저장한 후 해당 설정을 기준으로 kubeadm init 명령어를 아래와 같이 실행

kubeadm init --config kubeadm-config.yaml

kubeadm v1.22 버전부터는 kubeadm 설정에 KubeletConfiguration 아래의 cgroupDriver 필드를 지정하지 않을 경우 기본으로 systemd가 되기 때문에 cgroup을 systemd를 사용할 예정이라면 따로 지정할 필요가 없음

cgroupfs를 사용할 경우 공식문서 확인

6. kubead, kubelet, kubectl 설치

모든 노드(머신, VM)에 아래 패키지들을 설치

  • kubeadm: 클러스터를 부트스트래핑하기 위한 패키지
  • kubelet: 클러스터의 모든 머신에서 컨트롤 플레인과 통신하는 어플리케이션. 노드에 작업을 요청할 경우 kubelet을 통해 작업을 실행함.
  • kubectl: 클러스터와 통신하기 위한 커맨드라인 유틸

kubeadm vs kubelet + kubectl 버전과 관련된 이슈가 있기 때문에 업그레이드 또는 기존 kubelet, kubectl 위에 kubeadm 설치할 경우 버전 및 버전차이 정책을 꼭 확인해야함. (새로 설치하는 경우는 무방할듯)

추천 설치방법은 OS 패키지 관리자를 통해 설치하는 방법. 그 외의 방법은 공식문서 참고.

  1. 패키지 관리자 업데이트 및 기본 패키지 설치

     sudo apt-get update
     sudo apt-get install -y apt-transport-https ca-certificates curl
    
  2. 구글 클라우드의 공개 키 다운로드

     sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
    
  3. 쿠버네티스 레포지토리 추가

     echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
    

    이 과정에서 Conflicting values set for option Signed-By regarding source.... 로 시작하는 에러가 발생하면 도커나 쿠버네티스와 관련해서 기존에 추가되어있던 레포지토리가 존재하고 이 버전이 충돌나서 그럴 확률이 높음.

    Ubuntu 기준 /etc/apt/sources.list.d/ 아래에 있는 등록된 레포들을 확인하면서 주석처리 하거나 파일 제거 후 재등록 해보면서 해결

  4. 패키지 관리자를 다시 업데이트하고 kubelet, kubead, kubectl 설치 및 버전 고정

     sudo apt-get update
     sudo apt-get install -y kubelet kubeadm kubectl
     sudo apt-mark hold kubelet kubeadm kubectl
    

클러스터 구성하기

설치된 kubeadm을 이용하여 가장 처음에 언급했던 3대의 마스터 노드와 3대의 워커노드로 클러스터를 구성한다.

HA 구성 관련 참고사항

고가용성을 위해서는 여러 대의 노드(마스터, 워커 둘 다)가 존재해야하고, 로드밸런서를 통해 부하분산을 해줘야한다. OnPremise 환경에서는 로드밸런서를 따로 준비해야하므로, Metal LB같은 OnPremise용 쿠버네티스 로드밸런서를 준비해야 한다.

여기서 말하는 로드밸런서는 일반 사용자(고객)에게 제공되는 웹 서비스를 위한 로드밸런서가 아니라 마스터 노드로 몰리는 워커 노드의 API 요청을 분산하기 위한 로드 밸런서이다. (아래 이미지 참고)

Kubernetes 마스터 노드의 HA 구성도 etcd 를 같은 서버에 두냐 분리하냐로 구분할 수 있음. 위의 이미지는 etcd를 같은 서버에 둔 경우의 아키텍쳐(stacked etcd / pulse lake에 구성된 클러스터도 해당 방식으로 구성).

자세한 사항은 공식문서 참고.

클러스터에 조인하기

# Control-plane(master)
You can now join any number of control-plane nodes by copying certificate authorities
and service account keys on each node and then running the following as root:

kubeadm join <load balancer IP>:<load balancer port> --token <token> \
      --discovery-token-ca-cert-hash 
      <ca-cert-hash> \
      --control-plane

# Worker nodes
Then you can join any number of worker nodes by running the following on each as root:

kubeadm join <load balancer IP>:<load balancer port> --token <token> \
      --discovery-token-ca-cert-hash 
      <ca-cert-hash> 

실제로 할때는 처음 kubeadm init 을 한 마스터 노드에서 certificate key를 발급하고 해당 키를 이용하여 추가 옵션을 넣어주어야 제대로 동작했음.

# 첫 번째 마스터 노드(kubeadm init)
$ sudo kubeadm init phase upload-certs --upload-certs 
[upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
[upload-certs] Using certificate key:
<certificate key>

# 노드 Join
kubeadm join <load balancer IP>:<load balancer port> --token <token> \
      --discovery-token-ca-cert-hash 
      <ca-cert-hash> \
      --control-plane # control node 대상에만 옵션 추가
      --certificate-key <위에 나온 certificate key>

Kubeadm 클러스터 완전 초기화

# Docker 초기화 (citypulse 서버 경우에는 /var/lib/docker 기본 마운트 폴더를 커스텀하여 조금 다름)
docker rm -f `docker ps -aq`
docker volume rm `docker volume ls -q`
sudo umount /var/lib/docker/volumes
sudo rm -rf /var/lib/docker/
sudo systemctl restart docker

# kubeadm 초기화
sudo kubeadm reset
sudo systemctl restart kubelet

Install with Kubespray

Ansible에 의존성을 두고있기 때문에 IaC(Infrastructure as Code)에 대한 이해도(적어도 Ansible과 플레이북) 가 필요하고 결정적으로 설치 가이드라인을 따라하다 실패했기 때문에, 나중에 다시 정리하는 것으로 함.

Kubespray 참고

  • https://kubernetes.io/ko/docs/setup/production-environment/tools/kubespray/
  • https://kubespray.io/
  • https://lapee79.github.io/article/setup-production-ready-kubernetes-on-baremetal-with-kubespray/
  • https://schoolofdevops.github.io/ultimate-kubernetes-bootcamp/cluster_setup_kubespray/

참고

MSA 이해하기

댓글 달기

등장 배경

어플리케이션의 크기가 점차 커지면서, 기존의 모놀리틱한 아키텍처의 단점이 드러나기 시작했다.

  • 어플리케이션의 크기가 커질수록 영향도 파악 및 전체 시스템 구조의 파악에 어려움
  • 빌드 시간 및 테스트시간, 배포시간의 증가
  • 서비스를 유연한 스케일 아웃이 어려움
  • 장애의 전파

여러 개의 작은 서비스로 나누어 변경이 용이하도록 하기위해 MSA가 등장했다고 볼 수 있다.

MSA 정의

마틴 파울러의 MSA 정의 “the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery.”

핵심은 각 서비스가 스스로 돌아갈 수 있고 서로 가벼운 메커니즘에 의해 통신이 가능해야 하며, 개별적 배포가 가능해야 한다는 것이다.

여기서 지난 번 사내서비스 개발기에서 언급했던 Gradle 멀티모듈 프로젝트 만들기와 핵심이 일맥상통하는 것 같다. 즉, 가볍고 독립적인 모듈 또는 서비스 여러 개로 분리하여 하나의 어플리케이션을 구성하되 지나친 의존성은 배제해야 한다는 것이다. (common 모듈의 저주)

장점

  • 서비스 별 개별 배포 가능 ( 배포 시 전체 서비스의 중단이 없음)
  • 요구사항을 신속하게 반영하여 빠르게 배포할 수 있음.
  • 특정 서비스에 대한 확장성이 용이함.
  • 장애가 전체 서비스로 확장될 가능성이 적음
  • 부분적 장애에 대한 격리가 수월함

단점

  • 서비스 간 호출 시 API를 사용하기 때문에, 통신 비용이나, Latency가 증가
  • 테스트와 트랜잭션의 복잡도가 증가하고, 많은 자원을 필요로 함
  • 데이터가 여러 서비스에 걸쳐 분산되기 때문에 한번에 조회하기 어렵고, 데이터의 정합성 또한 관리하기 어려움

참고

Blocking/Non-blocking vs Sync/Async

댓글 달기

Reactor로 토이 프로젝트를 하고있는데, 전체 스택을 자연스럽게 비동기나 논블로킹한 스택으로 쓰다보니 개념을 확실히 정리하고 사용하는 것이 좋겠다고 생각이 들었다.

Context에 따라 달라지는 의미

많은 글에서 이 헷갈리는 용어들의 비교를 대부분 ‘관심사항이 다르다.’ 라고 표현하고 있다. 예를 들면 Blocking과 Non-Blocking은 제어권의 반환 시점에 관심을 두고있고, Synchronous와 Asynchronous는 작업 완료 여부를 어디서 신경쓰는지에 관심을 둔다고 한다. 틀린 말은 아닌 것 같은데 그렇다면 일반적인 API 호출에서는 Blocking 한 작업과 Synchronous 한 작업의 실행 결과는 같지 않나? 에 대한 의문이 들었고, 조금 더 찾아보다 보니 가려운 부분을 긁어주는 글을 보게되었다.

Index라는 용어가 바닥에 깔려있는 의미는 동일하지만 어떤 문맥에서 얘기를 하냐에 따라 정확한 의미가 달라지듯, Blocking/Non-Blocking - Synchronous/Asynchronous도 마찬가지라는 뜻이다.

Method API 호출 또는 Thread 에서의 의미

아래에서 설명하겠지만 이 경우에는 System call이 일어나지 않는 단순 함수 API 호출이나 Thread 관점에서의 두 용어를 정리하는 부분이다. 이 관점에서는 synchronous와 blocking은 같은 개념으로 간주한다. 그렇기 때문에, synchronous(또는 blocking)는 api call을 보낸 후 응답을 받을 때까지 대기 상태에 있다 응답을 받은 후 종료되고 asynchronous는 api call을 보낸 후 실행 여부와 관계없이 바로 응답을 받는다.

어플리케이션에서 운영체제로의 System Call 이 발생할 때의 의미

대부분의 글이 전재로 하는 상황이 바로 이것이다. 대표적인 경우가 I/O가 발생하는 상황이라고 할 수 있다.

Blocking / Non-Blocking

어플리케이션의 실행이 운영체제의 대기 큐에 들어가고 system call이 완료될 때까지 기다리는 것이 Blocking 어플리케이션의 실행이 운영체제의 대기 큐에 들어가지 않고 system call 완료 여부와 관계 없이 바로 응답을 보내는 것이 Non-Blocking

I/O 가 일어나는 상황을 예로 들어 조금 더 구체적으로 정리하자면.

  • Blocking I/O는 자신이 해당 자원을 사용할 수 있을 때까지 기다렸다가 작업 수행을 수행한다. 이 경우, 기다리는 시간동안 어플리케이션은 다른 작업을 할 수 없다.
  • Non-Blocking I/O는 I/O가 실제로 일어나는지 진행상황에 관계없이 결과를 바로 반환한다.
    • 만약 사용할 수 없는 경우, EWOULDBLOCK 과 같은 결과를 리턴한다.
    • 만약 사용이 가능하다면, buffer에 있는 데이터를 복사하고 데이터의 길이를 리턴한다.
    • 실제 I/O 작업 시간과는 상관없이 빠른 리턴이 일어나기 때문에, 유저 프로세스의 작업이 중지되어 있는 시간이 적다.

Synchronous / Asynchronous

system call이 완료되고 다음 코드를 실행하는 것이 Synchronous systemc call이 완료되는 것과 관계없이 다음 코드를 실행하는 것이 Asynchronous

Blocking vs Synchronous

system call 반환을 기다리는 동안 대기 큐에 머무는 것이 필수가 아니면 synchronous system call 반환을 기다리는 동안 대기 큐에 머무는 것이 필수이면 blocking

Non-Blocking vs Asynchronous

system call이 반환될 때 실행된 결과와 함께 반환될 경우는 Non-blocking system call이 반환될 때 실행된 결과와 함께 반환되지 않는 경우 Asynchronous

둘의 핵심 차이점은 누가 system call 완료에 신경을 쓰는지 이다. 단순 Non-Blocking만 채택할 경우 코드의 실행 자체는 Synchronous 하게 되며 polling 방식처럼 데이터를 정상적으로 받을 떄까지 재요청을 보낸다. 반면, Asynchronous의 경우 system call 완료에 신경쓰지 않고 운영체제의 callback을 받은 시점에 완료 처리를 하게된다. (이 때, Async + Blocking으로 코드를 짠다면 callback을 운영체제에게 맡기긴 하지만 어플리케이션 자체는 운영체제 대기큐에 들어있으므로 어플리케이션이 block 상태가 된다. Async를 쓰는 의미가 없다는 뜻.)

Java의 경우 7부터 나온 NIO2 패키지의 AsynchronousChannel 인터페이스를 통해 완전한 Non-blocking(+ Asynchronous) IO를 수행할 수 있다. 예시.

AsynchronousServerSocketChannel listener
 = AsynchronousServerSocketChannel.open().bind(null);

listener.accept(
 attachment, new CompletionHandler<AsynchronousSocketChannel, Object>() {
   public void completed(
     AsynchronousSocketChannel client, Object attachment) {
         // listener IO 완료 시
     }
   public void failed(Throwable exc, Object attachment) {
         // listener IO 실패 시
     }
 });

결론

간단하게 정리할 수 있을 줄 알았는데 생각보다 오랜 시간이 걸렸다. 단순히 외우기 보다는 이해를 하고 정리하려다 보니 운영체제 지식을 꽤나 많이 요구하는 것 같다. 역시 개발자는 기본이 탄탄해야 새로운 것을 받아들이기 수월하다는 생각이 든다. + 이 모든 것은 Reactive Programming에 대한 개념을 정리하기 위한 초석.

참고

Spring Boot Test

댓글 달기

이번 포스팅에서는 JUnit과 Spring Test 라이브러리를 이용한 Spring 환경에서의 테스트 환경 설정부터 이러한 개발환경에서 좋은 테스트 코드를 작성하기 위한 방법에 대해 알아볼 예정이다. (이번 포스팅의 예시 코드는 Spring MVC 사용을 기준으로 작성하였다. Spring Webflux 환경에서의 테스트 정리는 공부 더 하고 다음 글에 작성하기로..)

Language & Project version

  • Java 11
  • Spring Boot 2.4.4
  • Gradle 6.8.3

Spring Test

spring-boot-test 라이브러리에서 Test의 설정을 지원하는 annotation에는 다양한 클래스가 존재하는데 기본으로 많이 사용되는 @SpringBootTest를 시작으로 몇 가지만 살펴보도록 하겠다.

테스트 환경 설정

Spring Boot 프로젝트를 시작하고 gradle dependency에 아래의 구문을 추가한다.

testImplementation 'org.springframework.boot:spring-boot-starter-test'

spring-boot-start-test 는 단위 테스트를 위한 JUnit과 Mock 객체 지원을 위한 Mockito, 가독성이 좋은 테스트 표현식을 만들기 위한 Matcher 라이브러리인 Hamcrest 등을 포함하고 있다. spring-boot-start-test가 포함하는 전체 라이브러리는 아래와 같다. (Spring Boot 버전 2.2.0 이상)

  • JUnit 5: The de-facto standard for unit testing Java applications.
  • Spring Test & Spring Boot Test: Utilities and integration test support for Spring Boot applications
  • AssertJ: A fluent assertion library.
  • Hamcrest: A library of matcher objects (also known as constraints or predicates).
  • Mockito: A Java mocking framework.
  • JSONassert: An assertion library for JSON.
  • JsonPath: XPath for JSON.

만약 spring-boot-start-test를 쓰지 않고 싶다면, spring-boot-test를 포함해 다른 라이브러리의 버전을 명시하면서 따로 가져오면 된다.

간단하게 테스트 관련 라이브러리들이 잘 동작하는지 확인하는 테스트를 작성해보자.

package com.example.demo;

import com.example.demo.service.BookRepository;
import com.example.demo.service.TestConfiguration;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles
class DemoApplicationTests {

	@Autowired
	@Qualifier("shoesRepository")
	private ShoesRepository shoesRepo;

	@Test
	void getShoesListTest() {
		assertThat(shoesRepo.getShoesList().size(), is(3));
	}
}

@SpringBootTest 어노테이션을 통해 Spring Boot Test를 라는 사실을 명시하고 테스트에 필요한 의존성을 제공해준다. (JUnit4 을 사용한다면 @RunWith(SpringRunner.class) 와 같은 어노테이션을 붙여줘야 하지만 JUnit5 부터는 @SpringBootTest 어노테이션에 포함되기 때문에 작성할 필요가 없다.)

@SpringBootTest 어노테이션의 classes 속성을 통해 클래스를 선택적으로 로드할 수도 있다. 위에서 작성한 테스트의 경우 @Configuration 어노테이션을 통해 TestConfiguration 이라는 클래스에서 사용할 빈을 선택적으로 등록했기 때문에 적어주었다. classes 속성에 클래스를 명시하지 않으면 모든 Bean을 가져온다.

@ActiveProfiles를 통해 profile 파일에 정의되어 있는 환경 속성들을 가져와서 테스트 어플리케이션을 설정한다. 만약, 실행환경 별로 여러 개의 profile이 정의되어 있다면(ex. profile-stage, profile-local, profile-product 등) @Activeprofiles("dev") 와 같이 특정 profile을 지정하는 것이 가능하다.

여기까지가 Spring Boot의 Test 관련 라이브러리들을 잘 가져왔는지에 대한 테스트였다.

@SpringBootTest

@SpringBootTest 어노테이션은 org.springframework.boot.test.context 패키지에 위치하고 있으며 당연하겠지만 Spring Boot 기반의 테스트를 실행하는 테스트 클래스에 지정하는 어노테이션이다.

다음과 같은 기능을 제공한다. (from API Doc)

  1. 별도의 @ContextConfiguration(loader=...) 어노테이션이 지정되어있지 않으면 SpringBootContextLoader를 default ContextLoader로 사용한다.
  2. 내부에 @Configuration이나 classes 속성을 따로 정의하지 않았다면 @SpringBootConfiguration을 자동적으로 검색한다.
  3. properties 속성을 통해 커스텀 환경속성을 정의할 수 있다.
  4. args 속성을 통해 어플리케이션의 argument를 정의할 수 있다.
  5. 특정 포트나 임의의 포트를 이용하여 실제 웹서버를 시작할 수 있는 기능을 포함한 몇 가지 webEnvironment 모드를 제공한다.
  6. 실제 웹 서버를 실행시키는 웹 테스트를 위한 TestRestTemplate 과 WebTestClient bean을 등록한다.

1, 2번 특징을 통해 별도의 설정을 해주지 않으면 모든 bean을 로드하여 테스트를 할 수 있게 해준다. 반대로 말하면, 별도의 설정을 하지 않으면 테스트가 의도한바 보다 너무 무거워진다는 뜻이다.

5번의 webEnvironment 에서 제공하는 몇 가지 모드의 옵션은 다음과 같다.

  • MOCK(default) : web ApplicationContext 를 로드하고 mock 웹 환경을 제공한다.
  • RANDOM_PORT : WebServerApplicationContext(실제 서블릿 환경)를 로드하고 실제 웹 환경을 제공한다. 내장된 서버가 시작되고 랜덤한 포트를 listen한다. 통합테스트을 할 때만 이 옵션을 지정하는 것이 좋다.
  • DEFINED_PORT : WebServerApplicationContext를 로드하고 실제 휍 환경을 제공한다.
  • NONE : SpringApplication을 이용해 ApplicationContext를 로드하지만, 웹 환경은 제공하지 않는다.

TestConfiguration을 통해 기존의 Configuration을 커스터마이징이 가능하다. TestConfiguration은 ComponentScan 과정에서 생성되며, 테스트가 실행될 떄 bean을 생성하여 등록한다.

내부의 ComponentScan을 통해 감지되기 때문에 classes 속성을 통해 특정 클래스만 지정하면 TestConfiguration은 감지되지 않는다. 그런 경우에 classes 속성 값에 TestConfiguration 클래스를 추가하거나 @Import 어노테이션을 통해 가져올 수 있다.

package com.example.demo;

import com.example.demo.service.RecommendationService;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

import java.util.Random;

@TestConfiguration
public class DemoTestConfiguration {
    @Bean
    public RecommendationService restTemplate() {
        return new RecommendationService(){
            @Override
            public String recommendShoes() {
                return repository.getShoesList().get(new Random().nextInt(3));
            }
        };
    }
}
package com.example.demo;

import com.example.demo.service.RecommendationService;
import com.example.demo.service.ShoesRepository;
import com.example.demo.service.DemoConfiguration;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@SpringBootTest
@Import(DemoTestConfiguration.class)
class DemoApplicationTests {

	@Autowired
	@Qualifier("shoesRepository")
	private ShoesRepository shoesRepo;

	@Autowired
	@Qualifier("recommendationService")
	private RecommendationService service;

	@Test
	void getShoesListTest() {
		assertThat(shoesRepo.getShoesList().size(), is(3));
	}

	@Test
	void recommendTest() {
		assertThat(service.recommendShoes(), notNullValue());
	}
}

Note. @Import 어노테이션에 대해.. @ComponentScan 을 통해 ApplicationContext에 bean을 등록할 수 있는데 @Import라는 어노테이션이 따로 있는 이유는 무엇일까? Spring은 ‘설정보다는 관습’ 이라는 철학을 기억해야한다. (baeldung 가이드에서는 @ComponentScan은 관습에 가깝지만 @Import는 설정에 가까워 보인다는 의견을 내놓았다.) @Import 는 모든(또는 다수의) Configuration이나 Component를 등록하고싶지 않거나, @ComponentScan으로 닿지 않는 영역의 클래스를 가져올 때 유용하다. 가벼운 테스트 환경을 유지하기 위해 따로 테스트마다 필요한 설정만 모아두고 사용할 때 좋아보인다.

Mocking

spring-boot-starter-test 라이브러리에 포함된 Mockito를 이용하여 mock 객체를 만들고 이를 이용해 테스트를 해볼 수 있다. 이렇게하면 스프링 빈 주입에 의존하지 않아도 되고, 구체적인 동작이 구현되지 않은 컴포넌트의 테스트가 가능하다.

package com.example.demo;

import com.example.demo.service.RecommendationService;
import com.example.demo.service.Shoes;
import com.example.demo.service.ShoesRepository;
import com.example.demo.service.DemoConfiguration;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@SpringBootTest(classes = ShoesRepository.class)
class DemoApplicationTests {

	@Autowired
	@Qualifier("shoesRepository")
	private ShoesRepository shoesRepo;

	@Test
	void getShoesListTest() {
		assertThat(shoesRepo.getShoesList().size(), is(3));
	}

	@Test
	void recommendTest() {
		RecommendationService service = Mockito.mock(RecommendationService.class);
		Mockito.when(service.recommendShoes()).thenReturn(new Shoes("tennis"));

		assertThat(service.recommendShoes().getName(), is("tennis"));
	}
}

직접 Mockito를 이용하는 방법도 있지만, @MockBean 어노테이션을 사용해 Mock 객체를 빈으로 등록할 수 있다. @MockBean으로 선언된 빈을 주입받으면 해당 빈에 의존성이 있는 다른 빈에는 Mock 객체를 주입한다.

package com.example.demo;

import com.example.demo.service.RecommendationService;
import com.example.demo.service.Shoes;
import com.example.demo.service.ShoesRepository;
import com.example.demo.service.DemoConfiguration;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;

@SpringBootTest(classes = RecommendationService.class)
class DemoApplicationTests {

	@MockBean
	private ShoesRepository shoesRepo;

	@Autowired
	@Qualifier("recommendationService")
	private RecommendationService service;

	@Test
	void getShoesListTest() {
		assertThat(shoesRepo.getShoesList().size(), is(3));
	}

	@Test
	void recommendTest() {
		assertThat(service.recommendShoes().getName(), notNullValue());
	}
}

Controller 등을 테스트 할 때에는 MockMVC 객체를 이용해 서블릿을 모킹하여 테스트할 수 있다.

package com.example.demo;

import com.example.demo.service.*;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@SpringBootTest(classes = DemoConfiguration.class)
@AutoConfigureMockMvc
class DemoApplicationTests {

	@Autowired
	MockMvc mockMvc;

	@Test
	public void controllerTest() throws Exception {
		mockMvc.perform(
				MockMvcRequestBuilders.get("/shoes/recommend")
						.contentType(MediaType.APPLICATION_JSON))
				.andExpect(MockMvcResultMatchers.status().isOk())
				.andExpect(MockMvcResultMatchers.jsonPath("name").isNotEmpty());
	}
}

Note. DemoConfiguration.class 를 통해 설정을 해주었는데 이때, Configuration 클래스에 @EnableWebMvc 어노테이션을 붙어야, Spring MVC 설정과 관련된 지원을 받아 정상적으로 Controller를 테스트할 수 있다.

Test Slices

test-starter 패키지에 들어있는 spring-boot-test-autoconfigure 모듈은 전체가 스프링 어플리케이션이 아니라 원하는 부분만 조각으로 떼어내서 테스트 할 수 있는 설정을 자동으로 할 수 있도록 도와준다. @...Test 접미사가 붙으며 공통적으로 ApplicationContext를 로드하고 해당 테스트 슬라이스와 관련된 설정을 자동으로 해주는 @AutoConfigure... 어노테이션을 포함한다.

각각의 슬라이스는 적절한 컴포넌트만 찾도록 ComponentScan을 제한하며 매우 제한된 자동설정 클래스 집합을 로드한다. @...Test 어노테이션의 excludeAutoConfiguration 이라는 속성을 이용하거나 @ImportAutoConfiguration#exclude 어노테이션을 통해 특정 클래스를 제외시키면서 설정을 가져올 수 있다.

하나의 테스트에서 여러 슬라이스를 사용하는 것은 지원하지 않는다. 이를 원할경우, @...Test 어노테이션 중 하나를 선택하고, @AutoConfigure...를 통해 원하는 슬라이스의 설정들을 (+ 필요한 빈) 직접 로드해야 한다.

목록

다 사용해보면서 포스팅을 작성할 수 없기 때문에 @...Test 슬라이스들의 종류만 나열하고 넘어가겠다. 각 슬라이스에 포함된 auto-configuration 어노테이션들은 이 페이지에서 확인할 수 있다. 각 슬라이스에 대한 자세한 설명은 공식문서 - Auto-configured Tests 에서 확인할 수 있다.

  • @DataCassandraTest
  • @DataJdbcTest
  • @DataJpaTest
  • @DataLdapTest
  • @DataMongoTest
  • @DataNeo4jTest
  • @DataR2dbcTest
  • @DataRedisTest
  • @JdbcTest
  • @JooqTest
  • @JsonTest
  • @RestClientTest
  • @WebFluxTest
  • @WebMvcTest
  • @WebServiceClientTest

Spring Unit Test?

앞선 TDD 정리 포스팅에서 단위테스트의 Best practic를 보면 단위 테스트는 고립되어야 한다. 라고 되어있다. 그러나 Spring Framework는 객체 생성과 주입에 깊게 관여하기 때문에 @SpringBootTest 와 같은 어노테이션을 사용하는 테스트 Unit Test라고 말하기 어렵다고 생각한다. (Test Slice들도 마찬가지)

아래 예시 코드와 같이 Spring에서 bean으로 등록해둔 클래스도 단위 테스트가 가능하다.

// ShoesController class
@Controller
public class ShoesController {

    @GetMapping("/shoes")
    public ModelAndView getShoes() {
        return new ModelAndView("shoes", "shoe_type", "boots");
    }
}

// ShoesHandlerTest class

import com.sample.move02.handler.ShoesController;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

public class ShoesControllerTest {
    private ShoesController controller;

    @Before
    public void setUp() {
        controller = new ShoesController();
    }

    @Test
    public void getShoesShouldUseShoesView() {
       assertThat(controller.getShoes().getViewName()).isEqualTo("shoes");
    }

    @Test
    public void getShoesShouldAddAShoeTypeModel() {
        assertThat(controller.getShoes().getModel()).containsEntry("shoe_type", "boots");
    }
}

위의 예제처럼 Test 자동화를 위한 JUnit 프레임워크 외에 다른 의존성이 없어야만 “단위 테스트” 라고 부를만 하다. 이렇기 때문에 Spring 프레임워크 위에서 모든 클래스에 대해 완벽한 단위테스트를 실행하기는 어렵고 번거롭다.

Spring Test에서 제공하는 Slice나 클래스의 부분적인 주입을 적절히 이용하여 Spring 컨테이너 위에서도 가능한 가볍게 테스트를 작성하는 습관을 들이는 것이 좋아보인다.

참고

TDD 정리

댓글 달기

Test Driven Development (TDD)

TDD(Test-Driven-Development) 는 애자일을 실천하기 위한 소프트웨어 개발방법론 중 하나이다. Test First Development(TFD) 와 Refactoring이 결합된 형태라고도 한다.

TDD 과정

보통은 개발할 때 설계 -> 코드 개발 -> 테스트 와 같은 과정을 거치게 된다. 하지만 TDD는 기존 방법과는 다르게, 테스트케이스를 먼저 작성한 이후에 실제 코드를 개발하는 리팩토링 절차를 밟게된다.

normal
tdd

위 단계를 조금 더 구체적으로 설명하자면 아래와 같다.

  1. 테스트 코드를 먼저 작성함으로써 테스트 코드가 개발을 주도하도록 한다.
    • 테스트 코드가 개발을 주도하기 위해서는 반드시 실패를 포함하는 테스트 코드의 작성이 앞서야 한다.
  2. 앞서 작성된 테스트 코드를 통과할 수 있는 ‘최소한의 구현 코드’를 작성한다.
    • 최소한의 구현 코드는 개선될 수 있는 많은 여지를 퐇마하는 코드이다. 단지 테스트만 패스하면 되도록 작성한다.
  3. 최소한의 구현 코드를 리팩토링 단계에서 개선한다.
  4. 테스트 코드 작성, 최소한의 구현 코드 작성, 구현 코드에 대한 리팩토링 순으로 짧은 주기를 반복하며 점증적으로 개발한다.

이 때 테스트케이스는 자동화가 되어 생산성을 높일 수 있어야 한다. (JUnit 같은 툴을 이용)

장점

  1. 당연하게도 결함이 줄어든다. 목표로 하는 테스트케이스를 먼저 작성하고 테스트를 통과하게 코드를 작성하기 때문에 결함을 줄일 수 있다.
  2. 개발을 할 때 작은 단계별로 나누어 코드를 작성할 수 있도록 한다. 새로운 기능을 위한 코드를 작성한다고 가정했을 때, TDD로 개발을 수행했다면 기존 코드는 이미 Test로 검증이 되어있기 때문에 추가된 코드로 범위를 한정지어 결함을 찾기 더 쉬워진다.
  3. TDD는 코드 수정에 대해 불안감을 떨치고 확신을 얻을 수 있게 해주는 강점을 갖게해준다고 할 수 있다.
  4. 또한, 테스트 코드를 먼저 작성하는 것은 목표를 미리 구상하는 작업이라고 할 수 있다. 클래스를 만들고 어떻게 동작할지를 먼저 정의하는 구현보다는 객체가 어떤 행동을 해야하는지에 대한 인터페이스에 집중하게 하여 객체가 어떤 방식으로 기능을 수행하는지 보다는 객체가 무슨 역할을 가져야 하는지 그리고 이러한 역할을 위해 무엇을 노출해야하는지 등에 집중할 수 있다.
  5. 4번 과정에 집중하면 코드의 복잡도가 낮아지게 된다. 코드의 복잡도가 낮아지는 것은 결국 읽기 쉬운 클린 코드를 짤 수 있게 된다는 뜻이며, 이는 곧 유지보수 비용의 감소로 이어질 수 있다.

Unit Test을 작성하는 행위는 검증보다는 설계와 문서화 행위라고 할 수 있다. 단위 테스트를 작성함으로써 기능 검증과 관련된 많은 피드백 루프를 최소화할 수 있다. - Bob martin

단점?

  1. 기존 개발 프로세스에 비해 테스트케이스 설계가 추가되므로 생산비용이 증가된다.
    • 이것은 한정적으로 맞는 얘기인데, 단기적인 프로젝트를 완성하고 더 이상 그 프로젝트를 볼 일이 없거나 성과를 우선으로 내야한다면 테스트코드를 설계하고 작성하는 시간이 추가되기 때문에 손해일 수 있다.
    • 하지만, 장기적인 product를 개발한다는 관점에서 보면 이는 틀린 말일 수 있다. 테스트 코드를 작성하는데에 초기 비용이 늘어나는 것은 분명한 사실이지만, 장기적으로 보았을 때 더 높은 품질의 코드와 결함을 고치는 시간이 감소됨이 따라 오히려 생산성이 증가한다. (옛날에 작성해둔 버그를 찾는 시간과 이를 고치는 시간이 대부분 감소한다고 볼 수 있다.)
  2. 어렵다
    • 이제껏 개발하던 방식을 바꿔야하기 때문에 당연히 어려울 수 있다.

Unit Test

TDD에서 결국 자동화된 테스트케이스 중 상당 부분은 Unit Test일 것이다.

Unit Test는 Unit 을 테스트하는 방법으로, Unit이 의미하는 것은 테스트가 가능한 가장 작은 단위의 소프트웨어 컴포넌트를 의미하며 이것들은 대체로 단일 기능을 수행한다. Unit은 작은 규모여서 설계, 실행, 기록과 테스트결과의 분석이 쉬워야 한다. 좋은 Unit Test를 작성하기 위해서는 Unit이 의미하는 컴포넌트의 구조 또한 중요하다는 뜻이다.

개발일정의 단축을 위해(또는 귀찮다는 이유로..) 유닛테스트를 생략한다면 시스템 테스트나 통합 테스트 나아가 빌드 이후에 베타 테스트를 진행할 때 많은 버그가 일어날 가능성이 매우 높고 이 단계에서 결함을 발견하고 수정하는 비용은 매우 높다.

defect_cost_graph

결국 유닛테스트의 특징과 장점, 해야하는 이유 등은 TDD와 동일하다고 생각할 수 있다. (적어도 나한테는 그렇게 느껴진다.)

Best Practices

단위 테스트를 설계하고 작성하는데에 있어 Best Practices를 살펴보자.

1. 단위 테스트는 신뢰할 수 있어야 한다.

테스트는 코드가 잘못된 경우에만 실패해야 한다. 그렇지 않으면 테스트 결과를 신뢰할 수 없다.

2. 단위 테스트는 유지가 가능해야하며 가독성이 좋아야한다.

production 코드가 변경되면 테스트도 업데이트 되어야 한다. 그러므로, 테스트 코드 또한 읽고 이해하기 쉬워야한다. 항상 정리하고 이름을 명료하고 가독성 좋게 붙일 것.

3. 단위 테스트는 단일 유스케이스에 대해서만 입증해야 한다.

좋은 테스트는 단일 유즈케이스에 대한 한 가지에 대해서만 입증한다. 이렇게 되면, 테스트 코드가 단순해지고 이해하기 쉬워지며 이는 유지보수나 디버깅을 하기에 용이하게 만들어준다. 테스트가 한 가지 이상의 기능을 입증하게 되면 테스트 코드가 복잡해지며 유지보수에 많은 시간이 걸린다.

4. 단위 테스트는 고립되어야 한다.

테스트는 순서나 서로에게 영향을 주는 일이 없이 어떤 곳에서든 실행할 수 있어야 한다. 가능하다면, 글로벌하거나 외부의 환경 요소에 대해 의존성을 갖지 않게 하는것이 좋다. 의존성이 있는 테스트는 불안정하며 실행하기 어렵고 유지보수나 디버그가 어렵게 한다.

5. 단위 테스트는 자동화되어야 한다.

단위테스트는 매일, 매 시간 또는 CI/CD 환경에서 매번 일어나기 때문에 자동화된 프로세스에서 돌아가도록 해야한다. 자동화된 테스트의 결과는 쉽게 접근 가능하며 모든 팀원이 리뷰해야 한다.

6. 테스트 단계별 분배를 잘 해야한다.

테스트 자원의 이상적인 분배를 묘사하기 위해 흔히 사용되는 Testing Pyramid Model을 보면, Pyramid의 상단으로 갈 수록 테스트를 빌드하기 복잡하고 깨지기 쉬우며 실행 속도도 느리고 디버그도 느리다. 그러므로, 자동화된 단위 테스트를 통해 충분한 테스트를 거치고 상위 단계로 올라가는 것이 좋다. testing_pyramid_model

7. 단위 테스트는 조직 내에서 정립된 관행을 가지고 실행되어야 한다.

어떤 난이도의 테스트도 잘 이끌며 확장성이고 유연한 단위 테스트 프로세스를 얻기 위해서는 적절한 테스트 방법을 정리할 필요가 있다. production 코드를 작성하듯 테스트 코드를 작성하라는 뜻이다. 테스트 코드 역시 팀원들과 함께 좋은 테스트 코드인가에 대해 의논할 필요가 있다. 이를 위해 조직 내에 테스트 코드의 작성 규칙을 정립하고 이 규칙에 맞춰 테스트를 작성하고 실행하는 노력이 필요하다.

결론

TDD 역시 탄탄하고 유연한 설계가 뒷받침 되어야 훌륭한 테스트케이스 설계와 테스트 코드가 나올 수 있다고 생각한다. 모든 개발 조직이 TDD를 사용해야 한다는 것은 아니지만, 더 효율적인 방법을 위해서 배우는 시간을 투자하는 것이 아깝고 그 시간에 하나라도 더 찍어내는게 맞다고 생각이 든다면 이미 도태되는 과정이라고 생각된다.

다음 포스팅은 JUnit을 이용한 Java, Spring 환경에서 단위테스트를 작성하는 방법에 대해 정리 할 예정.

참고

소프트웨어 테스팅(일반)

댓글 달기

개발에 있어 테스트가 중요하다는 것은 이제 개발자가 아닌 동료들도 알고있다. 소프트웨어는 일상에서 뗄 수 없는 관계이고, 이런 서비스를 만들고 제공하는 입장에서 SW 결함으로 인한 문제가 발생한다면 꽤나 치명적이다. 모든 프로그램이나 인간이 완벽하지 않기 때문에, 테스트를 통해 프로그램에 오류가 있음을 검증하고 이로 인해 발생가능한 문제들을 최대한 방지해야 한다.

일반적인 테스트 관련 개념의 정리와 TDD, 내가 자주 사용하는 Java(+ Spring)의 테스팅 기법으로 나누어 포스팅할 예정.

소프트웨어 테스트 개요

테스트 정의

  • 소프트웨어를 실행하여 결과가 올바른지 판단하는 과정
  • 노출되지 않은 숨어있는 결함(Fault)을 찾기 위해 소프트웨어를 작동시키는 일련의 행위와 절차
  • 오류 발견을 목적으로 프로그램을 실행하여 품질을 평가하는 과정
  • 개발된 소프트웨어의 결함과 문제를 식별하고 품질을 평가하며 품질을 개선하기 위한 일련의 활동
  • SW의 동작과 성능, 안정성이 요구되는 수준을 만족하는지 확인하기 위한 결함을 발견하는 메커니즘

소프트웨어 오류의 원인

  • 요구사항 오류
  • 설계 오류
  • 코딩 오류
  • 기타 오류
  • 오류 분포

테스트 용이성(Testability)

아키텍처를 구성하는 요소들이 얼마나 테스트에 적합한가를 나타내는 품질 속성

  • 제어 용이성 : 프로그램을 제어하기 용이하도록 설계 -> 제어 용이성이 높을수록 테스트를 자동화할 수 있는 부분이 많아진다.
  • 관찰 가능성 : 프로그램 내부 상태를 쉽게 파악할 수 있도록 설계
  • 단순성 : 시스템 구조 등을 가능한 한 단순하게 설계
  • 분할 용이성 : 테스트할 대상 영역을 제어하여 문제가 발생된 곳을 고립시킴으로써 독립적으로 모듈을 테스트 할 수 있도록 설계
  • 운영 용이성 : 프로그램이 오작동해도 테스트 작업을 계속할 수 있도록 설계
  • 안정성 : 테스트 동안에 소프트웨어 변경이 자주 발생되지 않도록 설계
  • 이해 용이성 : 소프트웨어 설계 정보가 잘 조직화되어 쉽게 접근 가능하도록 하여 소프트웨어를 잘 이해할 수 있도록 설계

객체지향 패러다임의 SOLID 원칙과 비슷한 내용이라고 이해됨.

테스트 구성도 및 구성요소

테스트 구성도

테스트 구성요소

| 과정 | 구성요소 | 특징 및 설명 | |—|—|—| | 테스트 준비 | Test Basis | - 시스템 요구사항이 기록된 모든 문서
- Test Case 생성 시 사용되는 기초 자료
- 기능, 요구사항, 제약 사항 명시를 기록 | | | Test Suite | - Test Case의 집합, Test Case간 사전/사후 조건 연관 관계 | | | Test Case | - 테스트를 수행하기 위한 입려규 값, 사전 조건 등을 입력하고, 예상 결과까지 기록하여 결과가 올바른지 확인 | | | Test Script | - 테스트에 대한 절차 명세 | | 테스트 수행 | Test Bed | - 테스트를 수행하기 위해 필요한 모든 지원 요소를 포함하는 환경 | | | Test Target | - 테스트 수행의 대상이 되는 컴포넌트, 시스템 | | | Test Harness | - 테스트 수행을 위해 필요한 Stub과 Driver로 구성된 테스트환경
- 시험을 지원하는 목적 하에 생성된 코드와 데이터 | | | Test Driver | - 컴포넌트나 시스템을 제어하거나 호출하는 테스트 도구
- 상향식(Bottom-up) 테스트에서 아직 통합되지 않은 상위 컴포넌트 동작을 시뮬레이션하는 모의 모듈 | | | Test Stub | - 하향식(Top-Down) 테스트에서 사용하는 모듈
- 테스트 대상과 협력 구동하는 컴포넌트가 개발되지 않은 시점에서 필요한 테스트 진행을 위해 생성하는 더미(Dummy) 컴포넌트 | | 테스트 결과 | Test log | - 테스트 실행에 대한 관련 세부 항목의 시간 순서에 따른 기록 | | | Tes Report | - 테스트 활동과 결과를 다루는 문서
- 완료 조건에 대비하여 상응하는 테스트 항목을 평가 | | | Incident Report | - 테스트 중에 일어난, 조사를 요구하는 모든 이벤트 보고서
- 테스트 활동 수행 중 발생한 문제점 |

소프트웨어 테스트 분류

개발 단계별 테스트 분류

위 다이어그램은 V&V모델 또는 V다이어그램이라고 부름 V&V는 개발 단계별 산출물의 단계 초기에 설정된 조건의 만족 여부(Verification)와 구현된 S/W가 사용자 요구사항 및 기대치를 만족하는지(Validation) 검증 및 확인하는 활동

각 테스트 단계는 좌측 분석, 설계, 구현 단계와 대칭관계에 있다고 본다. 이 중, 우측의 테스트 분야를 떼서 단계별로 살펴보자.

단위 테스트

구현 단계에서 각 모듈이 구현된 후에 단위 테스트를 수행, 모듈이 단독적으로 실행할 수 있는 환경이 필요. 개발된 각각의 모듈을 테스트. 내가 작성한 코드를 테스트하는 코드의 형태를 취한다.

단위 테스트에 관련해서는 TDD와 함께 좀 더 자세하게 설명하겠다.

장점

  • 코드를 “어떻게” 작성하는지 생각하는데에 도움을 준다.
  • 단위 테스트를 하다보면 함수나 메소드를 더 작게 만들게되는 경향이 있으며, 이것은 코드를 이해하고 테스트하기 쉽게 만들어주고 이렇게 작아진 함수나 메소드는 코드를 변화시키는 것 또한 쉽도록 한다. (단일 책임의 원칙이 생각남)

통합 테스트

단위 테스트가 완료된 모듈 간의 상호작용(인터페이스)이 올바르게 되는지 테스트.

  • 개별적인 모듈에 대한 테스트가 불충분하게 수행된 경우를 보완
  • 제한된 상황만을 고려한 Test Driver / Test Stib 때문에 실제 모듈과 통합하는 과정에서 오류가 발생하는지 확인
  • 전역 변수 등으로 인해 모듈간의 예기치 못한 상호작용 때문에 발생하는 부작용으로 인한 오류 발생을 확인

종류

  1. 빅뱅 테스트 : 모든 컴포넌트를 다 만들고 통합하여 테스트하는 방식
  2. 하향식 통합(Top-Down) : 가장 상위 모듈을 테스트하기 위해 하위 모듈들을 Test Stub으로 대치 후 테스트를 수행하는 방법
    • 장점
      • 설계상의 오류를 빨리 발견할 수 있음
    • 단점
      • Test Stub이 필요하기 떄문에 비용이 많이 소요될 수 있음
      • Black Box 테스트만 사용하는 경우 하위 모듈이 충분하게 테스트되지 않을 가능성이 있음
  3. 상향식 통합(Bottom-Up) : 하위 모듈을 먼저 테스트하고 상위에 있는 모듈들을 통합하는 방식
    • 장점 : 하위에 있는 모듈을 충분하게 테스트할 수 있고, Test Stub 비용이 들지 않음.
    • 단점 : 설계 오류를 조기에 발견하지 못함.
  4. 샌드위치 통합 : 특정 테스트 대상 모듈을 중심으로 상하위 임시 모듈을 연결하여 통합하는 방법

시스템 테스트

전체 시스템에 대한 초기의 목적을 만족하는가를 확인하는 테스트. 통합 테스트가 완료된 후에 완전한 시스템에 대해 시슽메 명세에 따라 개발되었는지를 검증하기 위해 수행하는 테스트. 시스템의 기능 측면에서뿐만 아니라 사용성, 견고성, 신뢰성, 성능, 안전성, 보안성과 같은 비기능적 요구사항을 시스템이 만족하는지도 검증.

인수테스트

사용자의 요구사항을 만족하는가를 확인하는 테스트.실제 사용자 환경에서 테스트하며, 시스템 테스트에서 사용한 테스트 시나리오들을 이용할 수 있다.

  1. 알파 테스트 : 사용자에 의해 테스트가 수행되지만 개발자 환경에서 통제된 상태로 수행
  2. 베타 테스트 : 소프트웨어를 일정 수의 사용자들이 사용하고 피드백을 받음. 보통 개발자는 참여하지 않음.

테스트 케이스 생성별 구분

Black Box 테스트

기능 요구사항 등 프로그램 외부 명세를 보면서 테스트하는 기법. 상세한 기능 요구사항이 요구됨.

White Box 테스트

프로그램 내부 로직을 보면서 테스트. 프로그램 구현 및 내부 구조 이해를 위한 지식이 요구됨.

기타 구분별 테스트 유형

프로그램 실행 여부

  • 동적 테스트 : 프로그램을 실행하며 소프트웨어 시스템의 기능 테스트와 자원 사용 및 성능 확인 등 비기능 테스트를 겸한다. 모든 레벨에서 테스트 가능하며, 블랙박스 또는 화이트박스 테스트로 수행.
  • 정적 테스트 : 코드를 실행하지 않고 테스트하는 기법으로 리뷰와 같은 수동적 기법과 정적 분석과 같은 자동화된 기법이 있다. 정적 테스트는 개발 프로세스 초기에 개발 산출물들에 대해 결함을 발견함으로써 개발 비용을 낮추는데에 도움이 된다.

품질 특성에 따른 분류

  • 기능 테스트 : 기능 요구사항 검증
  • 사용성 테스트 : 소프트웨어를 쉽게 사용할 수 있는 정도, 편의 기능 제공 정도를 측정하고 검증 확인하는 테스트
  • 성능 테스트 : 시스템에 부하를 주면서 응답시간, 처리량, 속도 등 성능지표 추이를 측정
  • 스트레스 테스트 : 복합데이터 또는 대량의 데이터를 이용한 고부하 장기 테스트
  • 회복 테스트 : 문제가 발생했을 때 복귀 능력 검증
  • 보안 테스트 : 외부의 침입이나 해킹에 대응하는 능력 검증

    접근 방법

  • 탐색적 테스트 : 테스트 엔지니어의 경험, 지식과 직관에 기초한 테스트. 소프트웨어 시스템 분석을 위한 문서가 제대로 준비되어 있지 않은 경우 시스템 테스트에 유용
  • 위험기반 테스트 : 리스크 분석과 결정된 우선순위에 따른 테스팅

기타

  • 설치/업그레이드 테스트 : 소프트웨어 제품의 설치 또는 제거, 이전 버전에서 업그레이드 확인.
  • 회귀 테스트 : 앞 단계에서 정상적으로 수행된 테스트를 Refactoring된 시스템에 대해서 다시 테스트를 수행함으로써 변경에 의한 영향을 검증

참고

자연재난 시뮬레이션

댓글 달기

프로젝트를 하게 된 계기

스마트시티 연구소의 DigitalTwin 추진안 개편 과정 중 기존에 다른 연구원이 구현했던 시스템의 리팩토링을 맡게되었다.

Stack

  • Language : Java, Javascript
  • Framework : Spring4
  • DBMS : MariaDB
  • Library 및 그 외 사용 툴
    • OpenLayers : 브라우저에서 GIS 데이터를 Layer 별로 시각화하기 위해 사용
    • Geoserver : 빠른 GIS 데이터 계산 및 렌더링을 위해 사용

Diagrams

  • 시각화 성능개선을 위한 리팩토링을 수행했기 때문에 전체적인 프로젝트의 구조는 싣지 않았다.

주요 기능

  • 폭우, 대설 위험도 시각화 rain_risk snow_risk

담당 파트 및 고찰

  • GIS 데이터 경량화
    • 기존 시스템의 문제점은 BaseMap의 행정구역 경계가 뚜렷하지 않아 지역별 위험도의 가시성을 높이기 위해 모든 행정구역의 Polygon을 렌더링한다.
    • 읍면동 단위만 5000개가 넘게 존재하고 하나의 행정구역을 그리기 위해 선이 꺾이는 모든 지점을 점으로 찍기 때문에 모든 GIS 데이터를 Client가 다 fetch 하기까지 1분이 넘는 시간이 걸렸다.
    • Douglas-Peucker 알고리즘을 이용하는 툴을 이용해 각 행정구역의 polygon을 간소화시켜 fetch 시간을 5초로 감소시킴.
  • Geoserver
    • 기존 시스템은 gis 정보를 담고있는 geojson 파일을 그대로 client에게 전달하는 방식이었다.
    • 원본 데이터를 가공하여 데이터를 발행하는 부분을 핵심 로직과 분리하기 위해 Geoserver를 도입하였다. geoserver
  • JS Promise를 이용한 비동기 동작 코드의 가독성 향상
    • 기존 코드는 비동기로 동작하는 코드와 동기로 동작하는 코드의 구분이 모호하여 처음 리팩토링을 시작했을때 코드실행 순서 파악에 어려움을 겪었다.
    • Promise, async, await 을 이용한 코드로 재구성하여 가독성을 높이고, 동기코드의 실행 순서를 보장하였다.

한 마디

  • 실무에서 남이 개발한 코드를 개선하는 경험은 처음이라 당황스러운 부분이 많았음.
  • 문서화와 주석, 가독성 좋은 코드 작성의 중요성에 대해 다시 한 번 깨닫게 됨.
  • 일반적인 SW 스킬도 중요하지만 구현하는 서비스에 관련된 기본지식과 경험도 중요하다.

Spring File Download 정리

댓글 달기

Spring MVC 프레임워크에서 파일 다운로드를 구현하는 방법에 대해 정리해보았다.

생성-Singleton

댓글 달기

생성 패턴 중 Singleton 패턴에 대해 알아본다.

생성-Prototype

댓글 달기

생성 패턴 중 Prototype 패턴에 대해 알아본다.

Threadsafe - 3

댓글 달기

Thread Safe 란 무엇인지, 어떻게 적용할 수 있는지에 대한 방법을 알아보았다.

Threadsafe - 2

댓글 달기

Thread Safe 란 무엇인지, 어떻게 적용할 수 있는지에 대한 방법을 알아보았다.

Java Garbage Collection 기본 정리

댓글 달기

Java 의 Garbage Collection 기능에 대해 기본적인 것을 정리해보았다.

생성-Builder

댓글 달기

생성 패턴 중 Builder 패턴에 대해 알아본다.

Threadsafe - 1

댓글 달기

Thread Safe 란 무엇인지, 어떻게 적용할 수 있는지에 대한 방법을 알아보았다.

생성-Factory method

댓글 달기

생성 패턴 중 Factory method 패턴에 대해 알아본다.

생성-Abstract Factory

댓글 달기

생성 패턴 중 Abstract Factory 패턴에 대해 알아본다.

디자인패턴 개요

댓글 달기

디자인 패턴 공부의 시작점

D3 Tutorial 따라하기

댓글 달기

D3 Tutorial의 BarChart 그리기를 정리한 것.

D3 Data join과 Selection

댓글 달기

D3의 핵심 개념인 Data Join과 Selection에 대해 정리한 것.

D3 기초 정리

댓글 달기

D3 selection 및 데이터 핸들링 정리

Java IO

댓글 달기

Java IO 패키지 정리

Batch Processing

댓글 달기

Batch 프로세싱에 대한 정리와 Java(JDBC)에서의 응용

Spring MVC 설정 정리

댓글 달기

Spring MVC를 사용하기 위한 설정 정리

Spring AOP 용어 정리

댓글 달기

Spring AOP 및 AOP 관련 용어 정리

Spring IoC

댓글 달기

Spring 개요와 IoC의 주요 개념 정리

서울어코드 사업단 프로그램 신청 관리 페이지

댓글 달기

충북대학교 소프트웨어학과 서울어코드 사업단의 프로그램을 개설, 관리하고 참여신청이 가능한 서비스

서울어코드 사업단 프로그램 신청 관리 페이지 feature image

JSP와 Servlet

댓글 달기

JSP와 Servlet에 대해 간단하게 정리해보았습니다

충북대학교 학식 식단알리미

댓글 달기

카카오톡 자동응답 API를 이용한 기숙사, 학식 식단알리미

Mobee 영화 취향저격 커뮤니티

댓글 달기

빅데이터 프로젝트 in CBNU bigdata class

Mobee 영화 취향저격 커뮤니티 feature image

JVM 메모리 구조 및 동작원리

댓글 달기

수업듣고 정리한 JVM 메모리 구조 및 동작원리

HTML에 대해 몰랐던 것들

댓글 달기

지금까지 HTML을 사용하면서 몰랐던 사실들, 주의해야할 점 등을 정리해보았다.

강남엄마 경력요약서

댓글 달기

강남엄마에서 Rails Backend 개발자로 일했던 경력이자 경험