도커 구조

도커 구조

도커 초기에는 LXC(LinuX Container)를 기반으로 구현하였습니다. 그리고 0.9 버전부터 LXC를 대신한는 libcontainer를 개발하여 사용하고 있습니다.

다음 그림은 도커 0.9 버전의 구조입니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c21284c-197c-4393-8448-a00d37533ecc/docker-execdriver-diagram.png

출처 : https://www.docker.com/blog/docker-0-9-introducing-execution-drivers-and-libcontainer/

도커 1.11 버전부터 containerdrunC 를 컨테이너 런타임으로 사용합니다. 앞서 사용한 libcontainer 프로젝트는 OCI에 기부되었고, runc 라는 이름으로 바뀌게 됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5af65736-5226-47ae-9c67-af23bc193744/docker-containerd.png

dockerd

dockerd 는 컨테이너를 지속적으로 관리하는 데몬 프로세스로서, docker CLI 같은 클라이언트가 사용할 수 있는 RESTful API를 제공하고 있습니다. 흔히 명령어로 사용하는 docker 실행 파일이 docker CLI 입니다. 사용자가 입력한 docker 명령어는, 이 dockerd 로 전달되고, 실행됩니다.

dockerd는 unix, tcp, fd의 세 가지 소켓 유형을 통해, 도커 API 요청을 수신할 수 있습니다.

containerd

containerd는 이미지를 push 하고 pull 하고, 스토리지를 관리하고, 네트워크 기능을 정의할 수 있는 독립 실행형 고수준(high-level) 컨테이너 런타임입니다. runc 같은 저수준(low-level)의 컨테이너 런타임에 해당 명령을 전달하여, 컨테이너를 실행하는 등의 라이프사이클을 관리합니다.

도커에 의해 빠르게 확산되고 있던 컨테이너 환경에서, 컨테이너 런타임을 특정 벤더에 의존하지 않고, 중립적인 입장에서 컨테이너 표준에 맞게 구현하는 것을 목적으로 만들어졌습니다.

Docker Inc는 2016 년 12 월 컨테이너 런타임 부분을 독립적인 오픈 소스 프로젝트인 containerd 로 분리하여, 마이크로 소프트, Google, AWS, IBM 등과 공동으로 개발하기로 발표하였습니다.

그리고, 2017년 3월에는 CNCF (Cloud Native Computing Foundation)에 기부되었고, 이후 이를 담당해왔습니다.

도커는 1.11 이후 버전부터 containerd를 컨테이너 런타임으로 사용하고 있습니다.

containerd-shim

containerd-shimrunc를 실행하고, 컨테이너 프로세스를 제어하는 경량 데몬입니다. 컨테이너와 containerd 의 모든 통신은 containerd-shim 을 통해서 이루어 집니다.

containerd-shim 은 보통 다음과 같은 역할을 담당합니다.

  • 컨테이너의 stdout 및 stderr의 스트림을 제공해 주고 있습니다. 그래서 containerd 가 재시작 중에도 문제가 발생하지 않습니다. containerd 는 stdout 및 stderr의 스트림을 받아서 로그 파일로 저장을 할 수 있습니다.
  • runc 는 컨테이너 프로세스를 실행(fork)한 다음, 포그라운드 프로세스를 종료하여, 컨테이너 프로세스를 의도적으로 데몬화 합니다. 이렇게 되면, 컨테이너 프로세스는 호스트의 init 프로세스가 담당하게 되어서, 컨테이너의 관리가 어려워집니다. 이 문제를 해결하기 위해 shim 프로세스를 subreaper로 만들어서, 컨테이너 프로세스를 shim 프로세스가 관리하도록 합니다.

runc

OCI 런타임 스펙을 구현하고 있는 저수준 컨테이너 런타임입니다. 저수준 컨테이너 런타임이라고 부르는 이유는, 오직 실행 중인 컨테이너 관리에만 그 범위를 집중시키고 있기 때문입니다. 리눅스 커널의 네임스페이스와 cgroups 을 사용하여 격리시키는 기능을 제공합니다. 컨테이너를 생성(spawning)과 실행(running) 할 수있는 CLI로 구현되어 있습니다.

runc 는 도커 프로젝트(이전 이름은 libcontainer)에서 나와, OCI에 기부되었고, 이후 이를 담당해 왔습니다.

dockerdcontainerd 확인해 보기

도커가 설치된 호스트에서 프로세스를 조회해 보면, dockerdcontainerd 가 작동중인 것을 확인할 수 있습니다.

다음은 우분투 18.04 버전에 설치한 도커 19.03.12 버전인 경우 조회해 본 결과입니다.

dockerd 를 조회해 보았습니다.

sudo systemctl status docker.service
● docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2020-07-25 06:31:07 UTC; 3h 11min ago
     Docs: <https://docs.docker.com>
 Main PID: 3375 (dockerd)
    Tasks: 8
   CGroup: /system.slice/docker.service
           └─3375 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242010835Z" level=warning msg="Your kernel does not support swap memory lim
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242240538Z" level=warning msg="Your kernel does not support cgroup rt perio
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242342040Z" level=warning msg="Your kernel does not support cgroup rt runti
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242520743Z" level=info msg="Loading containers: start."
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.430146872Z" level=info msg="Default bridge (docker0) is assigned with an IP
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.580818585Z" level=info msg="Loading containers: done."
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.630878720Z" level=info msg="Docker daemon" commit=48a66213fe graphdriver(s)
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.636328410Z" level=info msg="Daemon has completed initialization"
Jul 25 06:31:07 magi systemd[1]: Started Docker Application Container Engine.
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.677294494Z" level=info msg="API listen on /var/run/docker.sock"

containerd 를 조회해 보았습니다.

sudo systemctl status containerd.service
containerd.service - containerd container runtime
   Loaded: loaded (/lib/systemd/system/containerd.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2020-07-25 06:31:05 UTC; 3h 11min ago
     Docs: <https://containerd.io>
 Main PID: 3118 (containerd)
    Tasks: 9
   CGroup: /system.slice/containerd.service
           └─3118 /usr/bin/containerd

Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738189803Z" level=info msg="loading plugin "io.containerd.grpc.v1.images
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738198104Z" level=info msg="loading plugin "io.containerd.grpc.v1.leases
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738208904Z" level=info msg="loading plugin "io.containerd.grpc.v1.namesp
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738217304Z" level=info msg="loading plugin "io.containerd.internal.v1.op
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744531308Z" level=info msg="loading plugin "io.containerd.grpc.v1.snapsh
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744556608Z" level=info msg="loading plugin "io.containerd.grpc.v1.tasks"
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744565509Z" level=info msg="loading plugin "io.containerd.grpc.v1.versio
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744574009Z" level=info msg="loading plugin "io.containerd.grpc.v1.intros
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.745122618Z" level=info msg=serving... address="/run/containerd/container
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.745135418Z" level=info msg="containerd successfully booted in 0.165150s"

dockerdcontainerd 가 분리되어 있기 때문에, 도커 버전을 올릴 때 재시작 하여도, 컨테이너의 재시작 없이 사용할 수 있습니다.

컨테이너 실행 과정 살펴보기

pstree 명령어를 이용하여, 프로세스를 트리 모양으로 확인해 보겠습니다.

sudo pstree

실행중인 컨테이너가 없다면, 다음과 같은 결과를 확인할 수 있습니다.

systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd───8*[{containerd}]
        ├─cron
...

nginx 컨테이너를 실행해 보겠습니다.

sudo docker run -d --name nginx nginx:latest

docker ps 명령어를 실행하여, 실행중인 컨테이너를 확인할 수 있습니다.

sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
79c0a9468509        nginx:latest        "/docker-entrypoint.…"   5 seconds ago       Up 4 seconds        80/tcp              nginx

pstree 명령어를 이용하여, 프로세스를 트리 모양으로 확인해 보겠습니다.

sudo pstree

containerd의 자식 프로세스로 containerd-shim 이 생성된 것을 알 수 있습니다.

systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd─┬─containerd-shim─┬─nginx───nginx
        │            │                 └─9*[{containerd-shim}]
        │            └─8*[{containerd}]
        ├─cron
        ├─dbus-daemon

앞서 살펴본, docker run 을 이용한 nginx 컨테이너의 실행 과정을 정리하면 다음과 같습니다.

  • docker 명령어, 즉 docker CLI 를 실행하면, dockerd 로 요청을 전달합니다.
  • dockerd는 gRPC를 통해 containerd 요청을 전달합니다.
  • containerdexec를 통해, containerd-shim 을 자식으로 생성합니다.
  • containerd-shimrunc 를 이용하여, 컨테이너를 생성하고 실행합니다.
  • runc 는 컨테이너가 정상적으로 실행되면 종료됩니다.
  • containerd-shim 은 컨테이너에서 실행되는 프로세스의 부모가 됩니다.

도커에는 컨테이너 안에 프로세스를 새로 실행할 수 있는 docker exec 라는 명령어가 있습니다.

다음 명령어를 실행하여, nginx 컨테이너에 bash 쉘을 추가로 실행해 보겠습니다.

sudo docker exec -it nginx bash

nginx 컨테이너 bash 쉘이 실행되고, -it 옵션을 사용했기 때문에 터미널에서 쉘을 이용할 수 있습니다.

다른 터미널을 열어서, pstree 를 실행해 보겠습니다.

pstree
systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd─┬─containerd-shim─┬─bash
        │            │                 ├─nginx───nginx
        │            │                 └─9*[{containerd-shim}]
        │            └─8*[{containerd}]
        ├─cron
...

containerd-shim 프로세스의 자식 프로세스로 bash 프로세스가 추가된 것을 확인할 수 있습니다.

참고 문서

우분투에 Docker 설치하기

Ubuntu Bionic 18.04 (LTS) 에 도커 설치하기

사전 확인

이전 버전 삭제하기

만약 이전 버전의 도커가 설치되어 있다면 먼저 삭제해야합니다. 패키지명은 docker, docker.io 또는 docker-engine 입니다

sudo apt-get remove docker docker-engine docker.io containerd runc

도커 설치하기

리포지토리를 이용하여 설치하기

새로운 호스트 시스템에, 처음으로 도커 엔진을 설치하기 위해서는, 도커 리포지토리를 설정해야 합니다. 설정 후후 리포지토리를 이용하여 도커를 설치하고 업데이트할 수 있습니다.

리포지토리 설정하기

https를 통해 리포지토리를 사용할 수 있도록, 적절한 패키지 인덱스 및 설치 패키지 업데이트 합니다.

sudo apt-get update

sudo apt-get install \\
    apt-transport-https \\
    ca-certificates \\
    curl \\
    gnupg-agent \\
    software-properties-common

도커의 공식 GPG key를 추가합니다

curl -fsSL <https://download.docker.com/linux/ubuntu/gpg> | sudo apt-key add -

도커를 설치하기 위한 리포지토리를 추가합니다.

sudo add-apt-repository \\
   "deb [arch=amd64] <https://download.docker.com/linux/ubuntu> \\
   $(lsb_release -cs) \\
   stable"

도커 설치하기

apt 패키지 인덱스를 업데이트하고, 도커 엔진과, containerd 를 설치합니다.

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli 

도커 확인하기

docker 를 일반 사용자 계정에서 사용하려면, docker 그룹에 사용자 계정을 추가해 줘야합니다.

sudo usermod -aG docker <your-username>

로그 아웃을 한 다음, 다시 접속을 해야 설정이 적용됩니다.

docker info 명령어를 실행하여, 설치된 도커의 정보를 확인할 수 있습니다.

docker info

정상적으로 실행되면, 다음과 같은 결과를 확인할 수 있습니다.

Client:
 Debug Mode: false

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 0
 Server Version: 19.03.12
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Native Overlay Diff: true
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc version: dc9208a3303feef5b3839f4323d9beb36df0a9dd
 init version: fec3683
 Security Options:
  apparmor
  seccomp
   Profile: default
 Kernel Version: 4.15.0-76-generic
 Operating System: Ubuntu 18.04.4 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 1
 Total Memory: 3.852GiB
 Name: magi
 ID: 53WF:R22P:ZLLX:5564:H5MV:2WDA:S7UK:CK5Y:G647:TH6W:D74Z:UPDZ
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: <https://index.docker.io/v1/>
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false

WARNING: No swap limit support

우분투 18.04에 네트워크 카드 추가하기

우분투 18.04 부터는 네트워크 카드를 설정하려면, netplan을 사용해야 합니다.

네트워크 인터페이스 추가

네트워크 인터페이스를 추가한 다음, ifconfig 명령어를 실행해 봅니다. 별다른 옵션을 주기 않았기 때문에, 추가된 네트워크 인터페이스가 보이지 않습니다.

ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 944  bytes 67408 (67.4 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 944  bytes 67408 (67.4 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

추가한 네트워크 인터페이스 확인

ifconfig -a 명령어를 이용하여, 추가한 네트워크 인터페이스를 확인할 수 있습니다.

ifconfig -a

eth0 이 추가된 것을 확인할 수 있습니다.

eth0: flags=4098<BROADCAST,MULTICAST>  mtu 1500
        ether 00:15:5d:15:0e:00  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 944  bytes 67408 (67.4 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 944  bytes 67408 (67.4 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

설정에 네트워크 인터페이스를 추가하기

/etc/netplan/*.yaml 파일에 네트워크 설정을 추가해 줘야 합니다

cat /etc/netplan/*.yaml
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets: {}
    version: 2

현재 설정된 네트워크 인터페이스가 없기 때문에, ethernets 필드에 아무런 값이 없습니다.

/etc/netplan/*.yaml 파일에 네트워크 설정을 추가합니다.

sudo vi /etc/netplan/*.yaml

ifconfig -a 명령어를 통해서 확인한 네트워크 인터페이스 이름을 ethernets 필드에 추가하고, 설정값을 지정해줍니다. 아래 예제에서는 dhcp 를 사용하기 때문에, IP 직접 명시하지 않았습니다.

# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
            addresses: []
            dhcp4: true
    version: 2

만약 고정 IP를 사용한다면 아래처럼 설정할 수 있습니다.

# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
            addresses: [192.168.21.100/24]
            gateway4: 192.168.56.1
            nameservers:
                    addresses: [8.8.8.8,8.8.4.4]
            dhcp4: no
            dhcp6: no
    version: 2

설정 적용하기

다음 명령어를 실행하여, 변경한 설정을 적용합니다.

sudo netplan apply

설정이 적용되면 ifconfig 명령어를 실행하여, 적용한 내용을 확인할 수 있습니다.

ifconfig

정상적으로 적용되었다면, 추가한 eth0 을 확인할 수 있습니다.

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.21.39  netmask 255.255.255.0  broadcast 192.168.21.255
        inet6 fe80::215:5dff:fe15:e00  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:15:0e:00  txqueuelen 1000  (Ethernet)
        RX packets 47668  bytes 70216376 (70.2 MB)
        RX errors 0  dropped 6  overruns 0  frame 0
        TX packets 9362  bytes 772780 (772.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 120  bytes 9592 (9.5 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 120  bytes 9592 (9.5 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Graceful Shutdown Pods in a Kubernetes Cluster

애플리케이션 서버를 직접 클라이언트에 노출하지 않는 것이 좋습니다. 로드 밸런서나, 리버스 프록시 같은 것을 중간에 둬서 서비스 트래픽을 제어할 수 있도록 합니다. 애플리케이션을 종료할 필요가 있을 경우, 로드 밸런서나 리버스 프록시에서 해당 애플리케이션을 제외 시켜버리면, 서비스 트랙이 해당 애플리케이션으로 전달되지 않으므로, 애플리케이션의 입장에서는 새로운 요청을 들어오지 않게됩니다.

로드 밸런서에 특정 서버를 제외할 수 있는 방법은 크게 두가지 있습니다.

  • 로드 밸런서에 서버를 제외 시키는 명령어를 보내기
  • 로드 밸런서에서 서버의 상태를 체크하여, 정상 상태가 아닐 경우 자동으로 제외하기

일반적으로는 두 번째 방법을 많이 사용합니다. 로드 밸런서에서 주기적으로 서버들을 체크하여, 정상 상태가 아닐 경우, 자동으로 해당 서버를 제외시켜버리는 것입니다. 그리고 정상 상태로 돌아올 경우 다시 로드 밸런서에 해당 서버를 추가 시킵니다.

쿠버네티스의 상태 프로브(Health Probe)

쿠버네티스는 컨테이너의 상태를 주기적으로 진단합니다. 각 노드에 설치된 kubelet 에 의하여 컨테이너의 상태를 진단하게 됩니다. 컨테이너의 상태를 진단하기 위하여, 다음과 같은 세가지 타입의 핸들러를 제공하고 있습니다.

  • HTTP : 지정한 경로와 포트에 HTTP GET 요청을 수행합니다. HTTP 응답 코드가 200 보드 크거나 같고, 400보다 작으면 진단이 성공한것으로 간주합니다.
  • TCP : 지정한 포트에 대하여 TCP 검사를 수행합니다. 포트가 활성화 되어 있으면, 진단이 성공한 것으로 간주합니다.
  • Exec : 컨테이너 안에서 지정한 명령어를 실행합니다. 명령어의 종료 코드가 0이면 진단이 성공한것으로 간주합니다.

라이브니스 프로브(Liveness Probe)

애플리케이션이 교착 상태 같은 비정상 상태에 빠졌을 경우, 단순히 프로세스의 상태 확인 만으로는 정상인지 아닌지 판단하기가 어렵습니다. 애플리케이션이 정상 작동 하지 않는 상태에서도, 프로세스의 상태는 정상으로 나올 수 있기 때문입니다. 이런 종류의 문제와, 비즈니스 로직에 따른 다른 형태의 장애를 감지가기 위해서, 쿠버네티스는 라이브니스 프로브를 사용합니다.

쿠버네티스의 노드에 설치된 kubelet이 정기적으로 파드 컨테이너의 라이브니스 상태를 점검하게 됩니다. 만약 비정상 상태가 감지된다면, 컨테이너를 재시작 시킵니다.

애플리케이션의 상태를 확인 할 수 있는 방법은 다음과 같습니다.

애플리케이션이 정상상태인지 아닌지를 판단하는 것은 개발자의 구현에 달려있습니다. 정상상태가 아닐 경우, 컨테이너가 다시 시작된다는 사실을 잘 기억하고 있어야 합니다. 컨테이너 재시작이 아무 소용 없는 경우라면, 해당 라이브니스의 상태값은 아무런 도움이 되지 않기 때문입니다.

레디니스 프로브(Readiness Probe)

쿠버네티스는 파드(Pod)를 이용하여, 애플리케이션을 실행 시킵니다. 그리고 이 파드에 주어진 주소를 이용하여 애플리케이션에 접근할 수 있습니다. 하지만 파드란 것은 상황에 따라 생성되고 없어지는 성질을 가지고 있습니다. 예를 들어, 새로운 배포에 의해 기존 파드가 제거되고, 새로운 파드가 생성될 수 있습니다. 그리고 스케일 아웃을 하기 위하여, 파드의 개수를 늘리 수도 있습니다.

이러한 이유로, 파드의 주소 만으로는 애플리케이션에 접근하는게 쉽지 않습니다. 그래서 쿠버네티스에서는 서비스(Service) 라는 개념을 만들어서, 파드의 애플리케이션으로 접근할 수 있게 하였습니다. 서비스는 레이블 셀렉터를 이용하여, 자신에게 속한 포드들의 목록을 가져올 수 있습니다. 그래서 파드들이 생성되고, 사라지더라도 대상 파드들 중 하나에게 트래픽을 보낼 수 있습니다. 하지만 파드들이 실행중에 있더라도, 무조건 트래픽을 보내면 문제가 생길 수 있습니다. 파드가 비정상 상태에 있을 수도 있기 때문입니다. 그래서 서비스는 파드의 상태를 체크할 필요가 있습니다. 이럴때 사용되는게 레니니스(Readniess) 프로브 입니다. 서비스는 파드의 레디니스(Readiness) 상태를 체크하고, 정상 상태일 때문에 해당 파드로 트래픽을 보냅니다. (정확히 말하면, 서비스가 트래픽을 보내는 것은 아니지만, 개념적으로는 그렇게 이해하시는게 편할것입니다.) 그리고 비정상 상황 파드일 경우, 트래픽을 보내는 대상에서 제외시켜버립니다.

정리하자면, 애플리케이션이 작업을 처리할 준비가 되어있는지를 판별하고, 준비가 되었다면 트래픽을 수신할 수 있도록 해줍니다.

파드 종료 생명주기

애플리케이션의 배포 단위는 파드입니다. 파드는 하나 또는 그 이상의 컨테이너로 이루어져 있습니다. 파드가 종료되면, 파드에 속한 컨테이너도 종료됩니다.

컨테이너가 종료될때 다음과 같은 절차를 따르게 됩니다.

  • 노드에 있는 kubelet이 파드에 정의한 preStop 훅(hook)을 호출합니다.
  • preStop 훅이 완료되면, kubelet 은 파드 컨테이너에서 실행중인 애플리케이션에게 SIGTERM 신호를 보냅니다.
  • kubelet 은 컨테이너가 종료될때 까지 유예 시간(기본값 30초) 동안 기다립니다. 그 후 SIGKILL 을 사용하여 프로세스를 강제로 종료 시킵니다. 이 유예 시간은 preStop 훅을 실행하는 시간까지 포함됩니다.
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5dc89ac3-b345-4f1f-aa3d-1a572ea8c46b/Container_Stop.png

이러한 흐름을 참조하여, preStop 훅이나, SIGTERM 신호를 이용하여, 애플리케이션이 정상적으로 종료될 수 있도록 정리 작업 같은 것을 할 수 있습니다. 예를 들면, 새로운 요청이 들어오지 못하게 하고, 이미 대기열에 있는 작업들의 처리가 완료될때까지 종료가 되지 않도록 기다리게 할 수 있습니다.

물론, preStop 훅이나, SIGTERM 신호를 이용하여 블록킹하더라도, 컨테이너가 종료되는것을 막을 수 없습니다. 유예 기간이 지나면 강제적으로 종료되기 때문입니다.

종료전 훅

preStop 훅은 컨테이너가 종료되기 전에 호출하는 블럭킹 호출입니다. SIGTERM 신호와 동일한 의미를 가지고 있으며, 컨테이너가 SIGTERM 신호를 처리하는 것이 불가능할 때 사용합니다.

preStop 훅 에서 사용할 수 있는 핸들러는 다음과 같습니다.

  • httpGet: 지정한 경로와 포트에 HTTP GET 요청을 수행합니다.
  • exec: 컨테이너 안에서 지정한 명령어를 실행합니다.

다음은 preStop 훅을 사용하는 컨테이너 예제입니다. 애플리케이션의 xxx 엔드포인트를 호출합니다.

apiVersion: v1
kind: Pod
metadata:
  name: pre-stop-hook
spec:
  containers:
  - name: demo-app
    image: xxxx
    lifecycle:
      preStop:
        httpGet:
          path: /shutdown
          port: 8080

SIGTERM 신호

컨테이너를 종료하려고 할때, kubelet 은 컨테이너에게 SIGTERM 신호를 보냅니다. SIGTERM 신호는 컨테이너한테, 종료할거라는 것을 미리 알려주는 역할을 합니다. SIGTERM 신호를 받은 애플리케이션은 가능한 한 빠르게 애플리케이션을 종료시켜야 합니다. 그리고 안전하고 깨끗하게 애플리케이션을 종료하기 위한 작업들도 진행할 수 있습니다.

SIGKILL 신호

SIGTERM 신호 후에도 컨테이너가 종료되지 않는다면, SIGKILL 신호를 발생시켜 강제로 종료시킵니다. SIGTERM 신호 발생 후, 지정된 유예 시간 동안 컨테이너가 종료되지 않으면, SIGKILL 신호를 발생시키는 것입니다. 이 유예 시간은 .spec.termicationGracePeriodSeconds 필드를 이용하여 설정할 수 있습니다.

종료 후 트래픽

파드의 정상적인 종료는, 종료 전에 들어온 트래픽을 처리하도록 보장할 수 있습니다. 하지만 파드가 종료된 후에도 트래픽을 계속 수신하여, 서비스에서 다운타임이 발생할 수 도 있습니다.

어떻게 해서 이런 문제가 발생하는지에 대해서 살펴보겠습니다. 다음 예제에서는 두 개의 포드가 실행되어 있고, 트래픽을 받기 위해서 하나의 서비스 가 있습니다. 트래픽은 서비스를 거처 두 개의 포드 중 하나로 전달됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6a050a05-fa7f-4b24-8141-7591171354c5/Kubernetes-service_(1).png

이 시점에서 파드 A1을 삭제한다가 가정하겠습니다. 파드 A1을 종료하기 위해서 kubectlpreStop 훅을 먼저 실행하게 되고, 파드 A1의 애플리케이션을 정상적인 종료 절차가 시작됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5217f1df-6adc-4491-9c7e-5c0ce1776eff/Kubernetes-preStop_(1).png

파드 A1의 종료를 위하여, preStop 훅을 호출 하는 시점에서도, 파드는 서비스에 등록되어 있을 수 있기 때문에 여전히 트래픽을 받을 수 있습니다.

파드 A1의 애플리케이션이 preStop 훅을 수신 받으면, 정상적인 종료를 위한 절차를 실행할 것입니다. 일반적인 종료 절차는, 이미 들어온 요청을 처리하고, 애플리케이션을 종료시키는 등의 작업일 것입니다. 그런데 이런 절차에 의해서, 애플리케이션이 종료되었는데, 새롭게 들어오는 추가 트래픽이 발생한다면, 오류를 발생시킬 수 있습니다.

그래서, 보다 안전하게 파드를 종료시키려면, 해당 파드를 서비스로부터 제거해야 하는 것입니다.

파드 종료 절차

API를 통해서 파드를 삭제한다고 가정해보겠습니다. API를 통해서 파드를 삭제하면, 메타데이터 서버에 파드가 삭제되길 원한다고 표시되기만 합니다. 이렇게 되면, 파드 삭제 통지가 전파가 됩니다. 그리고, 파드 삭제에 관련있는 시스템은 다음과 같은 처리를 하게 됩니다.

  • 파드를 실행하는 kubelet은 앞에 설명한 “파드 종료 생명주기”의 종료 절차를 시작하게 됩니다.
  • 엔드포인트 컨트롤러(endpoints controller) 가 유효한 엔드포인트 목록에서 파드를 제거하게 되고, 해당 서비스에서 파드가 제거됩니다.

여기서 중요한 점은, 여러 시스템이 관련이 있고, 서로 다른 노드에서 실행될 수도 있기 때문에, 이러한 절차가 병렬로 처리될 수도 있다는 것입니다. 그래서 종료 절차가 시작된 후에도 트래픽을 계속 수신할 수도 있는 것입니다.

문제 완화

불행히도 이러한 문제점을 완벽하게 해결할 수 있는 방법은 없습니다. 하지만 충분한 종료 절차 지연을 두어서, 대부분의 경우에는 이문제를 회피할 수 있습니다. 가장 간단한 방법은 preStop 훅에, sleep을 실행하여, 종료 절차를 지연시키는 것입니다.

다음은 preStop 훅에 지연을 추가한 매니페스트의 일부분입니다. 정확하게 몇초를 지연시키는것이 좋다는 것은 알 수 없습니다. 하지만, “Kubernete in Action”책에서 5-10초를 추천하였기 때문에, 예제에서는 5초를 사용하였습니다.

lifecycle:
  preStop:
    exec:
      command: [
        "sh", "-c",
        # Introduce a delay to the shutdown sequence to wait for the
        # pod eviction event to propagate. Then, gracefully shutdown
        "sleep 5",
      ]
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dcd36ceb-3ceb-4137-b11c-3c85e121b568/Kubernetes-sleep.png

preStop 훅에 의해서 5초간 지연을 시켰기 때문에, 유효한 엔드포인트 목록에서 파드를 제거하고, 해당 서비스에서도 파드를 제거하기 충분한 시간을 확보할 수 있게 됩니다.

Github Actions 소개

Github Actions

Github Actions 이란?

Github Actions는 Github 저장소를 기반으로 워크플로우(Workflow)를 자동화 할 수 있는 도구입니다. 워크플로우를 이용하면, 저장소에서 발생하는 이벤트를 이용하여, 소스를 빌드, 테스트, 패키징, 배포 등을 자동화 처리할 수 있습니다.

워크플로우는 Runner 에 의해서 실행됩니다. Runner는 Github에서 호스팅하는 머신에서 컨테이너를 실행주는 역할을 합니다. 물론 사용자가 호스팅하는 머신에서도 실행시킬 수 있습니다.

현재 Github 마켓플레이스에는 다양한 워크플로우가 공유되어 있기 때문에, 필요한 워크플로우를 찾아서 쉽게 사용할 수 있습니다.

Github Actions에 대한 자세한 내용은 아래 페이지에서 확인할 수 있습니다.

가격

공개저장소의 경우 한달에 500MB 스토리지와, 2000분의 실행시간 동안 무료로 사용할 수 있습니다.

개인저장소의 경우 사용하는 제품 플랜에 따라 다릅니다. 자세한 사항은 About billing for GitHub Actions 에서 확인할 수 있습니다.

워크플로우는 저장소마다 최대 20까지 등록할 수 있습니다. 그리고 워크플로우의 Job은 최대 6시간 동안 실행될 수 있고, 그 시간을 초과하게 되면 자동으로 중지됩니다. 그리고 사용하는 Github 플랜에 따라 전체 저장소에서 동시에 실행할 수 있는 Job의 개수등이 정해져 있습니다.


Github Actions의 기본 개념

Github Actions를 이해하기 위해 알아야 할 기본 개념에 대해서 알아보겠습니다. 기본 개념은 Workflow, Event, Job, Step, Action, Runner 등이 있습니다.

Workflow

최상위 개념으로서, 여러 Job으로 구성되어 있습니다. 그리고 Event에 의해 트리거되어 자동으로 실행할 수 있습니다.

Workflow 는 YAML 파일로 작성되어 있으며, 저장소의 .github/workflows/ 디렉토리에 저장되어 있습니다.

Event

Workflow를 트리거(Trigger)할 때 사용되는 특정 활동 같은 것을 의미합니다.

주요 이벤트는 다음과 같습니다.

  • 저장소에서 발생하는 이벤트, 예를 들어 특정 브랜치로 Push 하거나, Pull Request 하는 것입니다.
  • 특정 브랜치로 push 하는 이벤트
  • 특정 브랜치로 Pull Request 하는 이벤트
  • Webhook 이벤트
  • 특정 시간에 실행하는 스케줄 이벤트

자세한 내용은 Events that trigger workflows 에서 확인할 수 있습니다.

Job

Job은 여러개의 Step 으로 구성되어 있습니다. 그리고 다른 Job 에 의존관계를 가질 수 있으며, 병렬 실행도 가능합니다. Job은 가상 환경의 인스턴스에서 실행됩니다.

Step

Task들의 집합으로, 명령어를 실행하거나, Action을 실행할 수 있습니다.

Action

워크플로우의 가장 작은 블럭으로서, 어떤한 행위를 하는 개별 태스크(Task)를 의미합니다. 재사용이 가능한 컴포넌트로서, 마켓플레이스에 있는 공용 Action을 사용할 수 도 있고, 개인이 직접 만든 Action도 사용할 수 있습니다.

Runner

워크플로우의 Job들을 실행시켜주는 역할을 합니다. 워크플로우가 실행되면 인스턴스를 생성하고, Gitbub Action Runner 애플리케이션을 설치한, Job들을 실행시킵니다.

Github에서 직접 호스팅해주는 Github-hosted Runner와 자신이 직접 호스팅하는 Self-hosted Runner 가 있습니다.

좀 더 자세한 내용은 Core concepts 에서 확인할 수 있습니다.


Github Action 생성해 보기

Github Actions 의 워크플로우를 사용하려면, 지정된 디렉토리에 직접 yaml 파일을 생성거나, Github 화면의 “Actions” 탭에서 생성할 수 있습니다.

  • 직접 yaml 파일을 생성할 경우 디렉토리
.github/workflows/main.yaml
  • Github 화면의 “Actions” 탭
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c7654b08-f50a-4be1-95b5-1a01a5a06b9c/Untitled.png

워크플로우 등록하기

Github 화면의 “Actions” 탭을 이용하기 기본 워크플로우를 생성해 보겠습니다.

“Simple workflow”의 “Set up this workflow”를 클릭합니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cff85535-5416-4ae0-9463-ce82aed904be/Untitled.png

“Set up this workflow”를 클릭하면, 워프플로우를 편집할 수 있는 화면이 나타납니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e6e5474b-e4bd-4231-8859-f4587d70f4db/Untitled.png

기존적으로 생성된 워크플로우의 내용은 다음과 같습니다. 이 워크플로우는 단순히 “Hwllo, world!” 같은 문자열을 echo를 이용하여 출력해주는 역할을 합니다.

# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
    - uses: actions/checkout@v2

    # Runs a single command using the runners shell
    - name: Run a one-line script
      run: echo Hello, world!

    # Runs a set of commands using the runners shell
    - name: Run a multi-line script
      run: |
        echo Add other actions to build,
        echo test, and deploy your project.

화면 오른쪽 상단의 “Start commit”을 클릭해서, 워프플로우를 등록합니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cb79b7e2-38e6-42c0-8afa-2c09164c8922/Untitled.png

워크플로우 실행하기

등록한 워크플로우를 실행해보겠습니다. 현재는 별도로 실행을 할 수 있는 버튼이 없습니다. 그래서 워크플로우 지정한 이벤트를 발생시켜서 실행을 해야합니다.

다음 워크플로우 코드를 보면, master 브랜치에, pushpull_request 이벤트가 발생할때, 워크플로우가 실행된다는것을 확인할 수 있습니다.

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

push 이벤트를 발생시키위하여, 저장소에 [README.md](<http://readme.md>) 파일을 추가해 보겠습니다. “Code” 탭으로 이동하여, “Add a README”를 클릭합니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f5b1e40e-7ece-4f3f-8e3e-531ea3ac65a3/Untitled.png

[README.md](<http://readme.md>) 를 편집할 수 있는 화면이 나타납니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/afa6d115-329e-4428-934f-a4162387fdba/Untitled.png

기본값을 사용하거나, 적당한 값을 입력 한 후, 편집 화면의 아래에 있는 “Commit new file” 버튼을 클릭합니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8dd5eb1c-bbc4-49b3-a8f1-89b00ea51d86/Untitled.png

이제 [README.md](<http://readme.md>) 파일이 저장소로 push 됩니다. 그러면 push 이벤트에 의해서, 앞서 작성한 워크플라우가 자동으로 실행됩니다. 워크플로우의 실행을 확인 하라면, “Actions”탭을 클릭하면 됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/efc03d06-fc80-44fd-884c-5549e56c4a9e/Untitled.png

push 이벤트의 의해서 실행중인 워크플로우를 확인할 수 있습니다. commit 이름을 클릭하면, 워크플로우의 실행 현황을 알 수 있습니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/492c3dad-e0a8-4dd5-88f0-6d221a58f91d/Untitled.png

왼쪽 메뉴의 build 를 클릭하면, 워크플로우의 실행 단계를 확인할 수 있습니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6ee2551a-0882-4964-b5b5-36d79ee9ac35/Untitled.png

참고

Kubernetes Monitoring – 3. kube-prometheus 설치 후 해야 할일

kube-prometheus 를 설치 후 해야할 일들에 대해서 알아 보겠습니다.

편안하고 안전한 모니터링을 위해서 다음과 같은 작업을 하면 좋습니다.

  • prometheus-k8s, alertmanager-main 에서 PV(Persistence Volume) 을 사용하도록 변경합니다.
  • prometheus-k8s, alertmanager-main, grafana를 위한 Ingress 를 생성합니다.
  • alertmanager-main 에서 노티피케이션을 보낼 수 있도록 설정을 추가해 줍니다.

PV(Persistence Volume) 사용하기

프로메테우스 서버 같은 애플리케이션들은 데이터를 스토리지에 저장합니다. 그래서, 포드(POD)를 이용하여 애플리케이션을 실행할 때, 사용할 수 있는 스토리지를 지정해 줘야 데이터를 저장할 수 있습니다. kube-prometheus 의 기본값을 사용하여 설치하였을 경우에, 설정된 스토리지는 호스트 노드의 임시 디렉토리입니다. 그래서 포드가 다른 곳에 재배치되는 등의 상황에서는 기존 데이터를 사용할 수 없는 문제가 생깁니다. 이러한 문제를 해결하기 위해서 쿠버네티스에 지원하는 볼륨들 중에서 영속성을 가지고, 노드에 상관없이 이용할 수는 볼륨을 사용해야 합니다. 이 문서에서는 쿠버네티스트의 PV(Persistence Volume)를 사용하겠습니다. PV는 단어 그대로, 영속성을 가지는 볼륨으로서, 데이터를 지속적으로 저장할 수 있습니다. 한 가지 주의할 점은, PV를 사용하기 위해서는 쿠버네티스 클러스터에 PV를 위한 프로비저닝 설정이 되어 있어야합니다. PV를 위한 동적 프로비저닝이 된다는 가정하에 설명을 진행하도록 하겠습니다.

먼저 쿠버네티스 클러스터에서 지원하는 스토리지 클래스를 조회해 봅니다. 다음 명령어를 실행하면 스토리지 클래스를 조회할 수 있습니다.

kubectl get storageclass

지원하는 스토리지 클래스가 있을 경우 다음과 같은 형식으로 출력됩니다.

NAME                   PROVISIONER                AGE
ssd (default)          cinder.csi.openstack.org   51d

prometheus-k8s 에서 PV 사용하기

동적 프로비저닝을 이용하여 PV를 생성해 보겠습니다. 동적 프로비저닝을 이용하면, PVC(PersistentVolumeClaim)을 생성하면, 프로비저너가 자동으로 PV를 생성해 줍니다.

다음 명령어를 실행하여 k8s 라는 이름의 Prometheus 리소스를 편집하겠습니다.

kubectl -n monitoring edit prometheus k8s

Prometheus 리소스 매니페스트를 다음과 같이 수정합니다. Prometheus 리소스 매니페스트의 storage 필드에 volumeClaimTemplate 을 추가해 줍니다. 사용할 storageClassName 이름과 용량을 지정해 줍니다.

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  labels:
    prometheus: k8s
  name: k8s
  namespace: monitoring
spec:
  externalUrl: <http://prometheus.my-domain.com/>
  storage:
    volumeClaimTemplate:
      spec:
        resources:
          requests:
            storage: 80Gi
        storageClassName: ssd

alertmanager-main 에서 PV 사용하기

다음 명령어를 실행하여 main이라는 이름의 AlertManager 리소스를 편집하겠습니다.

kubectl -n monitoring edit alertmanager main

AlertManager 리소스 매니페스트를 다음과 같이 수정합니다. AlertManager 리소스 매니페스트의 storage 필드에 volumeClaimTemplate 을 추가해 줍니다. 사용할 storageClassName 이름과 용량을 지정해 줍니다.

apiVersion: monitoring.coreos.com/v1
kind: Alertmanager
metadata:
  labels:
    alertmanager: main
  name: main
  namespace: monitoring
spec:
  externalUrl: <http://alertmanager.my-domain.com/>
  storage:
    volumeClaimTemplate:
      spec:
        resources:
          requests:
            storage: 10Gi
        storageClassName: ssd

Grafana 에서 PV 사용하기

그라파나의 대시보드 같은 정보를 저장하기 위하여서 PV를 사용하는게 좋습니다. 기본 설정은 임시디렉토로 되어 있습니다. 그래서, 그라파에서 새로운 대시보드를 생성할 경우 문제가 생길 수 있습니다. 포드가 재시작되는 경우 새로 생성한 대시보드 정보가 사라져 버릴 수 있기 때문입니다. 그래서 그라파나에서 PV를 사용하도록 설정해 주는 좋습니다.

그라파나는 prometheus-operator 에서 관리하는 대상이 아닙니다. 쿠버네티스의 기본 리소스인 Deployment 를 사용하고 있습니다. 그래서 먼저 PVC 를 생성하여, Deployment 에 추가 해줘햐합니다.

PVC 생성하기

먼저 그라파나 PVC 매니페스트를 작성합니다.

grafana-pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app: grafana
  name: grafana
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: ssd

그리고, 다음 명령어를 실행하여 grafan이라는 이름의 PVC 를 생성하겠습니다.

kubectl -n monitoring apply -f grafana-pvc.yaml

그라파나 Deployment 수정하기

다음 명령어를 실행하여 grafan이라는 이름의 Deployment 리소스를 편집하겠습니다.

kubectl -n monitoring edit deploy grafana

Deployment 리소스 매니페스트를 다음과 같이 수정합니다.

Deployment 리소스 매니페스트의 spec.template.spec.volumes 필드에 있는 name: grafana-storage 부분을 persistentVolumeClaim 을 사용하도록 수정해 줍니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: grafana
  name: grafana
...
spec:
...
  template:
    spec:
...
      volumes:
      - name: grafana-storage
        persistentVolumeClaim:
          claimName: grafana

그리고 디렉토리의 권한을 맞추기 위해서 securityContext을 수정해 줍니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: grafana
  name: grafana
...
spec:
...
  template:
    spec:
      securityContext:
        runAsUser: 472
        fsGroup: 472

이 부분은 그라파나 컨테이너 이미지 버전에 따라서 다르기 때문에 필요 없을 수도 있습니다. 만약 추가하지 않으면 다음과 같은 에러가 발생할 수 있습니다.

$ kubectl logs grafana-84b4f94f5-7hnl9

GF_PATHS_DATA='/var/lib/grafana' is not writable.
You may have issues with file permissions, more information here: <http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later>
mkdir: can't create directory '/var/lib/grafana/plugins': Permission denied

다음은 수정한 매니페스트의 일부분입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: grafana
  name: grafana
...
spec:
  replicas: 1
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      labels:
        app: grafana
    spec:
      containers:
      - image: grafana/grafana:7.0.3
        name: grafana
        ports:
        - containerPort: 3000
          name: http
...
      nodeSelector:
        beta.kubernetes.io/os: linux
      securityContext:
        runAsUser: 472
        fsGroup: 472
      serviceAccountName: grafana
      volumes:
      - name: grafana-storage
        persistentVolumeClaim:
          claimName: grafana

Kubernetes Monitoring – 2. 모니터링 이해하기

프로메테우스를 이용한 쿠버네티스 모니터링 이해하기

kube-prometheus에 의해서 설치된 컴포넌트들을 살펴보도록 하겠습니다.

설치된 컴포넌트

kube-prometheus를 설치하면, 다음과 같은 컴포넌트들이 생성됩니다.

  • prometheus -operator : prometheus 와 alertmanager를 생성/삭제하고 관리하는 prometheus -operator가 설치됩니다.
  • prometheus : prometheus-k8s 라는 이름의 기본 프로메테우스 서버가 생성됩니다.
  • alertmanager : alertmanager-main 라는 이름의 기본 알럿 매니저가 생성됩니다.
  • node-exporter : 노드의 메트릭을 수집하기 위한 node-exporter가 생성됩니다.
  • prometheus -adapter : HPA를 위한 proemtheus-adapter가 생성됩니다.
  • kube-state-metrics : 쿠버네티스 클러스터의 메트릭을 수집하기 위한 kube-state-metrics가 생성됩니다.
  • grafana : 메트릭 시각화를 위한 grafana가 생성됩니다.

메트릭 수집

기본적으로 생성되는 prometheus-k8s 라는 이름의 프로메테우스 서버에서 수집하는 대상들에 대해서 알아보겠습니다. 설치한 버전에 따라서 약간의 차이는 존재할 수 있습니다.

프로메테우스 서버에서 수집하는 대상은 프로메테우스 서버 웹 화면의 메뉴에서 Status → Targets을 클릭하면 확인해 볼 수 있습니다. kube-prometheus 를 이용하여 설치했기 때문에, 기본적인 수집 대상이 등록되어 있습니다.

kubelet

쿠버네티스의 각 노드들에는 kubelet 이 기본적으로 설치되어 있습니다. kubelet 에는 cAdvisor가 통합되어 있는데, 이 cAdvisor 를 통해서 개별 컨테이너의 메트릭 정보들을 가져올 수 있습니다.

프로메테우스 서버는 kubelet을 이용하여 컨테이너의 메트릭 정보들을 수집합니다.

node-exporter

node-exporter는 쿠버네티스의 DaemonSet 리소스로 정의되어 있습니다. 그래서 쿠버네티스의 각 노드들에 node-exporter가 설치됩니다. node-exporter는 설치된 노드의 CPU, 메모리, 네트워크 같은 메트릭을 수집하도록 도와줍니다. node-exporter에 대한 자세한 사항은 해당 페이지를 참고하시기 바랍니다.

프로메테우스 서버는 node-exporter을 이용하여 노드의 메트릭 정보들을 수집합니다.

kube-state-metrics

kube-state-metrics 는 쿠버네티스 클러스터 레벨의 메트릭을 수집할 수 있도록 도와줍니다. 디플로이먼트(deployment)나 포드(pod) 등의 메트릭과 자원 예약 같은 정보를 제공합니다. kube-state-metrics에서 제공하는 메트릭에 대한 자세한 내용은 해당 페이지를 참고 바랍니다.

프로메테우스 서버는 kube-state-metrics을 이용하여 쿠버네티스 클러스터의 메트릭 정보들을 수집합니다.

기타 내부 컴포넌트

kube-system 네임스페이스 있는 내부 컴포넌트들의 메트릭도 수집을 합니다. etcd, coredns, kube-apiserver, kube-controller-manager, kube-scheduler 등이 있습니다.

알럿 관리

프로메테우스 서버에서 알럿 룰(AerltRule)을 정의하여, 알럿을 발생 시킬 수 있습니다. 발생한 알럿은 서버에 설정된 알럿 매니저(AlertManager)로 전송됩니다. prometheus-k8s 프로메테우스 서버의 경우에는 alertmanager-main 이라는 이름의 알럿 매니저로 알럿이 발송됩니다.

알럿 룰 (Alert Rules)

알럿 룰은 프로메테우스 서버에서 정의할 수 있습니다. 알럿 룰은 일반적으로 yaml 파일으로 이루어져 있으며 프로메테우스 서버가 해당 파일을 읽어서 알럿 룰을 읽어오게 됩니다.

알럿 룰은 프로메테우스 서버 웹 화면의 메뉴에서 Status → Rules 을 클릭하면 확인해 볼 수 있습니다. kube-prometheus 를 이용하여 설치했기 때문에, 기본적인 알럿 룰들이 등록되어 있습니다.

그리고 룰에 의해서 생성된 알럿들은 웹 화면의 메뉴의 Alerts 에서 확인할 수 있습니다.

알럿 룰은 알럿 룰을 정의한 yaml 파일은 만든 다음에, 프로메테우스 서버 설정에 추가 하면 됩니다. 이 글에서는 prometheus-operator를 사용하기 때문에, PrometheusRule 라는 사용자 리소스를 이용하여 알럿 룰을 정의할 수 있습니다.

다음은 PrometheusRule 매니페스트의 일부분 입니다.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  labels:
    prometheus: k8s
    role: alert-rules
  name: prometheus-k8s-rules
  namespace: monitoring
spec:
  groups:
  - name: node-exporter
    rules:
    - alert: NodeFilesystemSpaceFillingUp
      annotations:
        description: Filesystem on {{ $labels.device }} at {{ $labels.instance }}
          has only {{ printf "%.2f" $value }}% available space left and is filling
          up.
        runbook_url: https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-nodefilesystemspacefillingup
        summary: Filesystem is predicted to run out of space within the next 24 hours.
      expr: |
        (
          node_filesystem_avail_bytes{job="node-exporter",fstype!=""} / node_filesystem_size_bytes{job="node-exporter",fstype!=""} * 100 < 40
        and
          predict_linear(node_filesystem_avail_bytes{job="node-exporter",fstype!=""}[6h], 24*60*60) < 0
        and
          node_filesystem_readonly{job="node-exporter",fstype!=""} == 0
        )
      for: 1h
      labels:
        severity: warning

프로메테우서 서버에서 발생한 알럿은 알럿 매니저로 발송됩니다. 알럿 매니저는 해당 알럿을 받아서, 설정에 따라 노티피케이션을 발송하게 됩니다. 노티피케이션은 슬랙, 메일 등 다양한 곳으로 보낼 수 있습니다. 자세한 사항은 해당 페이지를 참고하시기 바랍니다.

기본적으로 생성된alertmanager-main 알럿 매니저에서 별다른 설정이 없기 때문에, 노티피케이션이 발송되지 않습니다.

다음은 알럿 매니저의 화면입니다.

메트릭 시각화

프로메테우스 서버의 웹 화면에서 간단한 그래프를 그릴 수 있지만, 전문 시각화 도구인 그라파나(Grafana)를 이용하면, 보다 편리하고 멋지게 시각화를 할 수 있습니다.

그라파나는 데이터를 가져올 수 있는 데이터 소스를 등록할 수 있습니다. kube-prometheus 를 이용하여 설치했기 때문에 기본적으로 proemtheus-k8s 가 데이터 소스로 등록되어 있습니다. 그리고, 기본적인 대시보드들도 등록되어 있기 때문에, 바로 쿠버네티스 클러스터의 상태를 대시보드로 확인할 수 있습니다.

다음은 기본적으로 등록되어 있는 대시보드 목록의 일부분입니다. 대시보드 이름을 클릭하면 대시보드 화면으로 이동합니다.

다음은 쿠버네티스 클러스터의 대시보드 화면입니다.

Kubernetes Monitoring – 1. kube-prometheus 설치하기

쿠버네티스 클러스터와 애플리케이션을 모니터링 하기 위한 방법은 다양하게 존재합니다. 퍼블릭 클라우드를 사용하는 경우에는, 클라우드 프로바이더에서 제공하는 모니터링 도구들을 사용할 수 있습니다. 그리고, 직접 쿠버네티스 클러스터를 설치하여 사용하는 경우에는, 오픈소스들을 사용하여 모니터링할 수 있습니다. 이 글에서는 모니터링 오픈소스 중의 하나인 프로메테우스를 이용하여, 모니터링을 하는 방법에 대해서 알아보겠습니다.

쿠버네티스 기본 메트릭 서버

쿠버네티스에서는 리소스의 메트릭을 조회할 수 있는 메트릭 API를 제공하고 있습니다. 메트릭 API를 사용하면, 컨테이너 CPU 및 메모리 사용량과 같은 리소스 사용량을 조회할 수 있습니다. 이 메트릭 API는 kubectl top 같은 명령어를 실행될때 사용되며, HPA(Horizontal Pod Autoscaler) 같은 클러스터 컨트롤러에서도 사용을 하고 있습니다.

메트릭 API

메트릭 API를 통해 지정한 노드나 파드에서 사용하고 있는 현재 리소스의 양을 알 수 있습니다. 하지만, 메트릭 API는 메트릭 값을 저장하고 있지 않으므로, 10분 전에 사용한 리소스의 양을 조회할 수 는 없습니다. 한 가지 주의할 점은 메트릭 API를 사용하려면, 메트릭 서버가 클러스터에 배포되어 있어야합니다.

메트릭 API에 대한 상세한 정보는 k8s.io/metrics 리포지터리에서 확인할 수 있습니다.

메트릭 서버

메트릭 서버는 클러스터 전역에서 리소스 사용량 데이터를 집계합니다. 메트릭 서버는 오토스케일링 의 목적으로만 사용해야 합니다. 그래서 모니터링 솔루션에 메트릭스를 전달하거나, 모니터링 솔루션 메트릭의 소스로 사용해서는 안됩니다.

프로메테우스 서버

프로메테우스 소개

프로메테우스는 현재 쿠버네티스 상에서 가장 많이 사용되고 있는 오픈 소스 기반 모니터링 시스템입니다. CNCF에 소속되어 있으며, 쿠버네티스 클러스터 및 컨테이너들를 손쉽게 모니터링 가능합니다.

프로메테우스의 주요 특징은 다음과 같습니다.

  • 메트릭 이름과 key-value 형태로 식별되는 시계열 데이터를 제공합니다.
  • PromQL 이라는 쿼리 언어를 사용할 수 있습니다.
  • 경고(Alert)와 룰셋(Ruleset)을 만들 수 도 있습니다.
  • Grafana 같은 시스템을 이용하여 간단하게 대시보드를 만들 수 있습니다.

프로메테우스 구조

모니터링을 위해 메트릭을 수집하는 방식은 크게 두 가지가 있습니다. Push 방식과 Pull 방식입니다.

Push 방식은 각 서버나 애플리케이션에 클라이언트를 설치하고, 이 클라이언트가 메트릭 데이터를 수집해서 메트릭 서버로 보내는 방식입니다.

Pull 방식은 각 서버나 애플리케이션이 메트릭을 수집할 수 있는 엔드포인트를 제공합니다. 그래서 메트릭 서버가 해당 엔드포인트를 호출하여 메트릭을 가지고 가는 방식입니다.

프로메테우스는 Pull 방식을 사용합니다. 즉, 애플리케이션이 작동하고 있으면, 메트릭 서버가 주기적으로 애플리케이션의 메트릭 엔드포인트에 접속해서 데이터를 가져오는 방식을 취하는 것입니다.

다음 다이어그램은 프로메테우스의 구조와 일부 생태계 구성요소를 보여줍니다.

출처 : https://prometheus.io/docs/introduction/overview/

프로메테우스 생태계는 여러 컴포넌트로 구성되어 있으며, 대부분은 선택 사항입니다.

  • Prometeus Server : 시계열 데이터를 스크랩하고 저장합니다.
  • Service discovery : 프로메테우스는 메트릭을 Pull 하기 때문에, 메트릭 수집 대상에 대한 정보가 필요합니다. 메트릭 수집 대상은 파일 같은 것을 이용하여 직접 관리할 수 있고, 쿠버네티스와 연동하여 자동으로 수집 대상을 동기화할 수 있습니다.
  • Pushgateway : ‘Short-lived jobs’ 을 지원하기 위해서 메트릭을 Push 하기 위한 게이트입니다. 애플리케이션이 Pushgateway에 메트릭을 Push 한 후, Prometheus Server가 Pushgateway에 접근해 메트릭을 Pull 해서 가져옵니다.
  • Jobs/Expoerters : Exporter 는 프로메테우스가 메트릭을 수집할 수 있도록, 특정 서버나 애플리케이션의 메트릭을 노출할 수 있게 도와주는 에이전트라고 볼 수 있습니다. Exporter는 서버 상태를 나타내는 Node exporter 같은 것이 존재하며, 다양한 커스텀 Exporter 이 개발되어 사용되고 있습니다.이러한 Exporter를 사용하여, 메트릭을 프로메테우스어 수집해 갈 수 있습니다.
  • Alertmanager : 경고(Alert)를 관리합니다. 메트릭에 대한 어떠한 지표를 지정해놓고, 그 규칙을 위반하는 사항에 대해 경고을 전송하는 역할을 합니다. 발생한 경고를 Slack이나 Email등으로 발송할 수 있습니다.
  • Service discovery : 프로메테우스는 메트릭을 Pull 하기 때문에, 메트릭 수집 대상에 대한 정보가 필요합니다. 메트릭 수집 대상은 파일 같은 것을 이용하여 직접 관리할 수 있고, 쿠버네티스와 연동하여 자동으로 수집 대상을 동기화할 수 있습니다.
  • Data visualiztion : Data visualiztion은 다양한 모니터링 대시보드를 위한 시각화를 제공합니다. 프로메테우스 web UI에서도 수집한 데이터를 이용하여 간단한 그래프를 그릴 수 있습니다. 일반적으로 Grafana 같은 전문 시각환 도구를 이용하여, 수집한 데이터를 시각화 합니다.

쿠버네티스에서 프로메테우스 사용하기

쿠버네티스에서 프로메테우스를 사용하는 방법은 크게 두 가지가 있습니다.

  • Prometheus : 프로메테우스 서버를 직접 생성하여 사용하는 방법입니다.
  • Prometheus Operator : 프로메테우스 오퍼레이터를 먼저 설치 한 다음, 오퍼레이터를 이용하여 프로메테우스 서버를 생성하는 방법입니다.

이 글에서는 프로메테우스 오퍼레이터를 이용합니다. 프로메테우스 오퍼레이터를 사용하는 이유는, 보다 간단하게 프로메테우스 서버를 생성할 수 있기 때문입니다. 사용자의 다양한 요구를 만족시키거나, 자동화를 하기에서는 프로메테우스 오퍼레이터가 조금 더 편합니다. 하지만 프로메테우스 오퍼레이터에 대한 학습 비용이 더 발생하기 때문에 간단히 사용할 경우에는 직접 프로메테우스 서버를 생성하여 사용하는게 좋을 수도 있습니다.

Prometheus Operator

프로메테우스 오퍼레이터는 쿠버네티스의 서비스들을 쉽게 모니터링할 수 있도록 해줍니다. 그리고, 프로메테우스 인스턴스의 배포와 관리하는 기능을 제공하고 있습니다. 사용자는 쿠버네티스의 사용자 리소스를 이용하여, 프로메테우스 모니터링 인스턴스를 생성, 구성 및 관리할 수 있습니다.

프로메테우스 오퍼레이터는 설치 후 다음과 같은 기능을 제공한다.

  • 생성 / 삭제 : 프로메테우스 오퍼레이터를 사용하여, 특정 애플리케이션 또는 팀을 위한 프로메테우스 인스턴스를 쉽게 실행할 수 있습니다.
  • 단순 구성: 쿠버네티스의 리소스를 이용하여 프로메테우스의 설정을 구성할 수 있습니다.
  • 레이블을 통한 대상 서비스 : 쿠버네티스 레이블 쿼리를 기반으로 모니터링 대상 구성을 자동으로 생성할 수 있습니다.

Prometheus Operator vs. kube-prometheus vs. community helm chart

프로메테우스 오퍼레이터를 설치하는 방법도 크게 세 가지가 있습니다. 이 글에서 kube-prometheus 를 사용하겠습니다.

Prometheus Operator

프로메테우스 오퍼레이터는 프로메테우스와 Alertmanager를 관리하고 운영합니다.

kube-prometheus

kube-prometheus 프로메테우스 오퍼레이터와 일련의 매니페스트들을 결합하여 쿠버네티스 클러스트와 그 위에서 실행 중인 애플리케이션을 모니터링하는 것을 도와줍니다.

kube-prometheus 패키지에는 다음과 같은 패키지가 포함되어 있습니다.

helm chart

stable/prometheus-operator 헬름 차트는 kube-prometheus 와 비슷한 기능을 제공합니다. 이 차트는 커뮤니티에 의해 유지되고 있습니다. 자세한 내용은 차트의 readme 를 참고하십시오.

kube-prometheus

쿠버네티스 호환성

다음은 쿠버네티스 버전과 kube-prometheus 버전의 호환성을 나타낸 것입니다.

kube-prometheus stackKubernetes 1.14Kubernetes 1.15Kubernetes 1.16Kubernetes 1.17Kubernetes 1.18
release-0.3
release-0.4✔ (v1.16.5+)
release-0.5
HEAD

kube-prometheus 설치

이미 만들어진 매니페스트를 이용하여 kube-prometheus를 설치하겠습니다.

  • 저장소에서 kube-prometheus 를 가지고 옵니다.
git clone <https://github.com/coreos/kube-prometheus.git>
cd kube-prometheus
  • 디렉토리에 있는 매니페스트를 사용하여 모니터링 스택을 생성합니다.
kubectl create -f manifests/setup
until kubectl get servicemonitors --all-namespaces ; do date; sleep 1; echo ""; done
kubectl create -f manifests/

모니터링 컴포넌트를 배포할 때 레이스 조건을 피하기 위해 먼저 네임스페이스와 CustomResourceDefinitions를 생성합니다. 두 폴더의 리소스를 단일 명령 kubectl create -f manifests/setup -f manifests로 적용할 수 있지만, 모든 컴포넌트가 성공적으로 생성되기 위해서는 명령을 여러 번 실행해야 할 수도 있기 때문에 나누어서 실행하였습니다.

  • 만약 설치한 모니터링 스택을 제거하려면 다음 명령어를 사용할 수 있습니다.
kubectl delete --ignore-not-found=true -f manifests/ -f manifests/setup

대시보드 접근하기

Prometheus

$ kubectl --namespace monitoring port-forward svc/prometheus-k8s 9090

http://localhost:9090 로 접속할 수 있습니다.

Grafana

$ kubectl --namespace monitoring port-forward svc/grafana 3000

http://localhost:3000 로 접속할 수 있습니다. Grafana의 기본 사용자:비밀번호는 admin:admin 입니다.

Alert Manager

$ kubectl --namespace monitoring port-forward svc/alertmanager-main 9093

Then access via http://localhost:9093 로 접속할 수 있습니다.

참고자료

Kubernetes Container Timezone

쿠버네티스 컨테이너 타임존 변경하기

컨테이너를 이용하여 애플리케이션을 실행시킬 때 타임존을 변경해야 하는 경우가 생길 수 있습니다. 예를 들어, 로그를 남길 때 시간을 기록하게 되는데, 이 시간이 자신의 타임존과 다르다면 많이 불편할 것입니다. 이럴 경우 컨테이너의 타임존을 자신이 속한 타임존으로 변경해주면, 개발자는 좀더 효과적으로 로그를 확인할 수 있습니다.

이 글에서는 컨테이너의 타임존을 변경하는 방법을 이용하여, 쿠버티스 포드(POD)를 실행할때 타임존은 변경해보도록 하겠습니다

기본 타임존 (UTC +0)

먼저 busybox 컨테이너 이미지를 이용하여 POD 를 생성해 보겠습니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: default-timezone
spec:
  containers:
  - image: busybox
    name: busybox
    args:
    - sleep
    - "100000"
EOF

생성한 default-timezone 포드가 정상적으로 작동되면, 실행중인 컨테이너의 shell에 접속해서 date 명령어를 실행해 보겠습니다.

kubectl exec -it default-timezone sh
/ # date
Tue Jun  9 11:55:56 UTC 2020

/ # strings /etc/localtime
TZif2
TZif2
UTC0

/ # exit

서울 타임존 (UTC +9)

이제 타임존을 UTC에서 한국의 서울(Asia/Seoul)로 변경해 보겠습니다.

새로운 타임존을 컨테이너에 주입하기 위하여, volumeMountsvolumes 을 사용하였습니다. volumes에서 hostPath 를 이용하여 호스트 노드에 있는 /usr/share/zoneinfo/Asia/Seoul 을 포드에 추가합니다. 그런 다음 컨테이너에서 volumeMounts 필드를 이용하여 추가한 볼륨을 컨테이너의 /etc/localtime 로 마운트합니다. 다시 말해서, 호스트 노드에 있는 /usr/share/zoneinfo/Asia/Seoul 을 컨테이너의 /etc/localtime 로 마운트하는 것입니다.

다음은 타임존을 서울로 설정한 포드 예제입니다. 예제를 실행하면, seoul-timezone 포드가 생성됩니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name : seoul-timezone
spec:
  containers:
  - image: busybox
    name: busybox
    args:
    - sleep
    - "100000"
    volumeMounts:
    - name: tz-seoul
      mountPath: /etc/localtime
  volumes:
    - name: tz-seoul
      hostPath:
        path: /usr/share/zoneinfo/Asia/Seoul
EOF

생성한 seoul-timezone 포드가 정상적으로 작동되면, 실행중인 컨테이너의 shell에 접속해서 date 명령어를 실행해 보겠습니다.

kubectl exec -it seoul-timezone sh
/ # date
Tue Jun  9 20:58:01 KST 2020

/ # strings /etc/localtime
TZif2
5qx
TZif2
KST-9

/ # exit

타임존 환경변수 설정

대부분의 경우 앞서 설명한 방법만으로 충분하지만, 혹시 안되는 경우가 있다면 다음과 같이 TZ 라는 환경 변수에 타임존을 직접 지정해 줄 수 있습니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name : seoul-timezone
spec:
  containers:
  - image: busybox
    name: busybox
    env:
    - name: TZ
      value: Asia/Tokyo
    args:
    - sleep
    - "100000"
    volumeMounts:
    - name: tz-seoul
      mountPath: /etc/localtime
  volumes:
    - name: tz-seoul
      hostPath:
        path: /usr/share/zoneinfo/Asia/Seoul
EOF

SpringBoot(자바) 애플리케이션에서 타임존 설정

다음은 SpringBoot 애플리케이션에 타임존 변경을 추가한 디플로이먼트 매니페스트 예제입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: api-server
    app.kubernetes.io/component: server
    app.kubernetes.io/part-of: example
  name: api-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: api-server
      app.kubernetes.io/component: server
      app.kubernetes.io/part-of: example
  template:
    metadata:
      labels:
        app.kubernetes.io/name: api-server
        app.kubernetes.io/component: server
        app.kubernetes.io/part-of: example
    spec:
      containers:
        - env:
            - name: JAVA_TOOL_OPTIONS
              value: "-Xms1024m -Xmx2048m"
            - name: SPRING_PROFILES_ACTIVE
              value: develop
            - name: TZ
              value: Asia/Seoul
          image: kangwoo/api-server
          imagePullPolicy: Always
          name: api-server
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /actuator/health
              port: 8089
              scheme: HTTP
            initialDelaySeconds: 60
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /actuator/info
              port: 8089
              scheme: HTTP
            initialDelaySeconds: 60
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          volumeMounts:
            - name: tz-seoul
              mountPath: /etc/localtime
          resources:
            limits:
              cpu: "1"
              memory: 2Gi
            requests:
              cpu: "1"
              memory: 2Gi
      dnsPolicy: ClusterFirst
      volumes:
        - name: tz-seoul
          hostPath:
            path: /usr/share/zoneinfo/Asia/Seoul
      restartPolicy: Always
      terminationGracePeriodSeconds: 30

참고로, 자바 애플리케이션의 경우 TZ라는 환경 변수 대신에, system property로 다음과 같이 추가할 수도 있습니다.

-Duser.timezone=Asia/Seoul

Kubeflow 1.0.0에서 InferenceService 의 상태 갱신이 실패할때

Kubeflow 1.0.0 이나 1.0.1에서 kfserving이 정상적으로 작동하지 않는 분을 위해서 참고삼아 적어 봅니다.

Kubeflow 설치 환경이 워낙 다양하고, 설치 환경 파일도 다양하기 때문에 이 글이 의미가 없을 수도 있습니다.

그리고, 이 방법은 정상적인 방법이 아니기 때문에, 단순히 테스트 삼아 KFServing을 사용하는게 목적일때만 사용하시기 바랍니다.

혹시 업그레이드가 가능하신 분들은 최신 버전으로 업데이트해 보시기 바랍니다.


KFServing는 InferenceService 라는 사용자 리소스를 사용합니다. InferenceService 는 정의한 추론 서버를 실행시킨 다음, 서버가 정상적으로 구동되었는지를 체크하는데, 환경에 따라서 이 상태 체크가 정상적으로 안되는 문제가 있습니다. 그 이유는 istio 때문입니다. Kubeflow에서 인증/권한을 위해서 istio를 사용합니다. InferenceService 를 관리하는 컨트롤러에서 상태 체크를 위해서 URL을 호출하는데, istio에서 인증/권한 체크 부분 때문에 200 OK가 반환되지 않는 문제가 생기는 것이죠. 그래서 실패한 상태로 인식이 되고, InferenceService 가 정상작동하지 않습니다.

이 문제를 제대로 해결하기 위해서는, Kubeflow에서 해결해 줄때까지 기다리던지, 아니면, kfserving + istio 전문가를 부르면 됩니다.

하지만 그럴 여건이 안되거나, 단순히 취미 생활로 KFServing 을 사용할 예정이라면, 다음과 같이 작동하게는 바꿀 수 있습니다. (권장하는 방법은 아닙니다.)

KFServing 에서 사용하는 istio-ingressgateway를 만들고, 보안 설정 부분을 삭제하는 것입니다.

먼저 kfserving-ingressgateway 을 생성합니다. 만약 생성되어 있다면 무시하시면 됩니다. 보통 istio-system 네임스페이스나, knative-serving 네임스페이스 존재할 수 있습니다. DeploymentService 를 확인해 보시면 됩니다.

 $ kubectl -n istio-system get deploy
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
cluster-local-gateway      1/1     1            1           54d
istio-citadel              1/1     1            1           54d
istio-galley               1/1     1            1           54d
istio-ingressgateway       1/1     1            1           54d
istio-pilot                1/1     1            1           54d
istio-policy               1/1     1            1           54d
istio-sidecar-injector     1/1     1            1           54d
istio-telemetry            1/1     1            1           54d
kfserving-ingressgateway   1/1     1            1           54d
prometheus                 1/1     1            1           54d
$ kubectl -n istio-system get service
NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                                                                                                                                                   AGE
authservice                ClusterIP      10.98.242.196    <none>        8080/TCP                                                                                                                                                                                  54d
cluster-local-gateway      ClusterIP      10.109.28.157    <none>        80/TCP,443/TCP,31400/TCP,15011/TCP,8060/TCP,15029/TCP,15030/TCP,15031/TCP,15032/TCP                                                                                                       54d
istio-citadel              ClusterIP      10.102.226.47    <none>        8060/TCP,15014/TCP                                                                                                                                                                        54d
istio-galley               ClusterIP      10.111.115.206   <none>        443/TCP,15014/TCP,9901/TCP                                                                                                                                                                54d
istio-ingressgateway       NodePort       10.103.205.239   <none>        15020:30536/TCP,80:31380/TCP,443:31390/TCP,31400:31400/TCP,15029:32168/TCP,15030:32077/TCP,15031:31505/TCP,15032:32021/TCP,15443:31546/TCP                                                54d
istio-pilot                ClusterIP      10.110.53.9      <none>        15010/TCP,15011/TCP,8080/TCP,15014/TCP                                                                                                                                                    54d
istio-policy               ClusterIP      10.106.248.16    <none>        9091/TCP,15004/TCP,15014/TCP                                                                                                                                                              54d
istio-sidecar-injector     ClusterIP      10.105.132.134   <none>        443/TCP,15014/TCP                                                                                                                                                                         54d
istio-telemetry            ClusterIP      10.105.24.245    <none>        9091/TCP,15004/TCP,15014/TCP,42422/TCP                                                                                                                                                    54d
kfserving-ingressgateway   LoadBalancer   10.101.141.37    <pending>     15020:30543/TCP,80:32380/TCP,443:32390/TCP,31400:32400/TCP,15011:30263/TCP,8060:32119/TCP,853:32180/TCP,15029:32156/TCP,15030:30674/TCP,15031:30230/TCP,15032:32563/TCP,15443:30995/TCP   54d
prometheus                 ClusterIP      10.101.81.54     <none>        9090/TCP                                                                                                                                                                                  54d

만약 없다면 생성해 줍니다.

kubeflow 네임스페이스에 있는 inferenceservice-config라는 ConfigMap을 수정해 줍니다. inferenceservice-configingress 부분의 ingressService 를 앞서 생성했거나, 존재하는 kfserving-ingressgateway 으로 수정해 줍니다. 주소 형식은 “서비스명.네임스페이스.svc.cluster.local” 입니다.

$ kubectl -n kubeflow edit cm inferenceservice-config 

apiVersion: v1
data:
...
  ingress: |-
    {
        "ingressGateway" : "knative-ingress-gateway.knative-serving",
        "ingressService" : "kfserving-ingressgateway.istio-system.svc.cluster.local"
    }
...

설정이 변경이 되었으면, 혹시 모르니 kfserving-controller-manager 를 재시작해 줍니다.

그리고, istio RBAC 설정이 담겨 있는 clusterrbacconfig 을 삭제합니다.

$ kubectl get clusterrbacconfig
$ kubectl delete clusterrbacconfig XXXX

이제 kfserving-ingressgateway 의 주소로 요청을 보낼 수 있고, 상태가 갱신되는것을 확인할 수 있습니다. 아.마.도….