튜토리얼

쿠버네티스 문서의 본 섹션은 튜토리얼을 포함하고 있다. 튜토리얼은 개별 작업 단위보다 더 큰 목표를 달성하기 위한 방법을 보여준다. 일반적으로 튜토리얼은 각각 순차적 단계가 있는 여러 섹션으로 구성된다. 각 튜토리얼을 따라하기 전에, 나중에 참조할 수 있도록 표준 용어집 페이지를 북마크하기를 권한다.

기초

구성

상태 유지를 하지 않는(stateless) 애플리케이션

상태 유지가 필요한(stateful) 애플리케이션

서비스

보안

다음 내용

튜토리얼을 작성하고 싶다면, 튜토리얼 페이지 유형에 대한 정보가 있는 콘텐츠 페이지 유형 페이지를 참조한다.

1 - Hello Minikube

이 튜토리얼에서는 Minikube와 Katacoda를 이용하여 쿠버네티스에서 샘플 애플리케이션을 어떻게 실행하는지 살펴본다. Katacode는 무료로 브라우저에서 쿠버네티스 환경을 제공한다.

목적

  • 샘플 애플리케이션을 minikube에 배포한다.
  • 배포한 애플리케이션을 실행한다.
  • 애플리케이션의 로그를 확인한다.

시작하기 전에

이 튜토리얼은 NGINX를 사용해서 모든 요청에 응답하는 컨테이너 이미지를 제공한다.

minikube 클러스터 만들기

  1. Launch Terminal 을 클릭

  1. 브라우저에서 쿠버네티스 대시보드를 열어보자.

    minikube dashboard
    
  2. Katacoda 환경에서는: 터미널 패널의 상단에서 플러스를 클릭하고, 이어서 Select port to view on Host 1 을 클릭

  3. Katacoda 환경에서는: 30000 을 입력하고 Display Port 를 클릭.

URL을 이용하여 대시보드 접속하기

자동으로 웹 브라우저가 열리는 것을 원치 않는다면, --url 플래그와 함께 다음과 같은 명령어를 실행하여 대시보드 접속 URL을 출력할 수 있다.

minikube dashboard --url

디플로이먼트 만들기

쿠버네티스 파드는 관리와 네트워킹 목적으로 함께 묶여 있는 하나 이상의 컨테이너 그룹이다. 이 튜토리얼의 파드에는 단 하나의 컨테이너만 있다. 쿠버네티스 디플로이먼트는 파드의 헬스를 검사해서 파드의 컨테이너가 종료되었다면 재시작해준다. 파드의 생성 및 스케일링을 관리하는 방법으로 디플로이먼트를 권장한다.

  1. kubectl create 명령어를 실행하여 파드를 관리할 디플로이먼트를 만든다. 이 파드는 제공된 Docker 이미지를 기반으로 한 컨테이너를 실행한다.

    kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
    
  2. 디플로이먼트 보기

    kubectl get deployments
    

    다음과 유사하게 출력된다.

    NAME         READY   UP-TO-DATE   AVAILABLE   AGE
    hello-node   1/1     1            1           1m
    
  3. 파드 보기

    kubectl get pods
    

    다음과 유사하게 출력된다.

    NAME                          READY     STATUS    RESTARTS   AGE
    hello-node-5f76cf6ccf-br9b5   1/1       Running   0          1m
    
  4. 클러스터 이벤트 보기

    kubectl get events
    
  5. kubectl 환경설정 보기

    kubectl config view
    

서비스 만들기

기본적으로 파드는 쿠버네티스 클러스터 내부의 IP 주소로만 접근할 수 있다. hello-node 컨테이너를 쿠버네티스 가상 네트워크 외부에서 접근하려면 파드를 쿠버네티스 서비스로 노출해야 한다.

  1. kubectl expose 명령어로 퍼블릭 인터넷에 파드 노출하기

    kubectl expose deployment hello-node --type=LoadBalancer --port=8080
    

    --type=LoadBalancer플래그는 클러스터 밖의 서비스로 노출하기 원한다는 뜻이다.

    registry.k8s.io/echoserver 이미지 내의 애플리케이션 코드는 TCP 포트 8080에서만 수신한다. kubectl expose를 사용하여 다른 포트를 노출한 경우, 클라이언트는 다른 포트에 연결할 수 없다.

  2. 생성한 서비스 살펴보기

    kubectl get services
    

    다음과 유사하게 출력된다.

    NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
    hello-node   LoadBalancer   10.108.144.78   <pending>     8080:30369/TCP   21s
    kubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          23m
    

    로드 밸런서를 지원하는 클라우드 공급자의 경우에는 서비스에 접근할 수 있도록 외부 IP 주소가 프로비저닝 한다. minikube에서 LoadBalancer타입은 minikube service 명령어를 통해서 해당 서비스를 접근할 수 있게 한다.

  3. 다음 명령어를 실행한다

    minikube service hello-node
    
  4. Katacoda 환경에서만: 플러스를 클릭한 후에 Select port to view on Host 1 를 클릭.

  5. Katacoda 환경에서만: 서비스 출력에서 8080의 반대편에 표시되는 5자리 포트 번호를 기록 한다. 이 포트 번호는 무작위로 생성되며, 사용자마다 다를 수 있다. 포트 번호 텍스트 상자에 포트 번호를 입력한 다음, 포트 표시를 클릭한다. 이전 예시를 사용해서 30369 를 입력한다.

    이렇게 하면 당신의 앱을 서비스하는 브라우저 윈도우를 띄우고 애플리케이션의 응답을 볼 수 있다.

애드온 사용하기

minikube 툴은 활성화하거나 비활성화할 수 있고 로컬 쿠버네티스 환경에서 접속해 볼 수 있는 내장 애드온 셋이 포함되어 있다.

  1. 현재 지원하는 애드온 목록을 확인한다.

    minikube addons list
    

    다음과 유사하게 출력된다.

    addon-manager: enabled
    dashboard: enabled
    default-storageclass: enabled
    efk: disabled
    freshpod: disabled
    gvisor: disabled
    helm-tiller: disabled
    ingress: disabled
    ingress-dns: disabled
    logviewer: disabled
    metrics-server: disabled
    nvidia-driver-installer: disabled
    nvidia-gpu-device-plugin: disabled
    registry: disabled
    registry-creds: disabled
    storage-provisioner: enabled
    storage-provisioner-gluster: disabled
    
  2. 애드온을 활성화 한다. 여기서는 metrics-server를 예시로 사용한다.

    minikube addons enable metrics-server
    

    다음과 유사하게 출력된다.

    The 'metrics-server' addon is enabled
    
  3. 생성한 파드와 서비스를 확인한다.

    kubectl get pod,svc -n kube-system
    

    다음과 유사하게 출력된다.

    NAME                                        READY     STATUS    RESTARTS   AGE
    pod/coredns-5644d7b6d9-mh9ll                1/1       Running   0          34m
    pod/coredns-5644d7b6d9-pqd2t                1/1       Running   0          34m
    pod/metrics-server-67fb648c5                1/1       Running   0          26s
    pod/etcd-minikube                           1/1       Running   0          34m
    pod/influxdb-grafana-b29w8                  2/2       Running   0          26s
    pod/kube-addon-manager-minikube             1/1       Running   0          34m
    pod/kube-apiserver-minikube                 1/1       Running   0          34m
    pod/kube-controller-manager-minikube        1/1       Running   0          34m
    pod/kube-proxy-rnlps                        1/1       Running   0          34m
    pod/kube-scheduler-minikube                 1/1       Running   0          34m
    pod/storage-provisioner                     1/1       Running   0          34m
    
    NAME                           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
    service/metrics-server         ClusterIP   10.96.241.45    <none>        80/TCP              26s
    service/kube-dns               ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP       34m
    service/monitoring-grafana     NodePort    10.99.24.54     <none>        80:30002/TCP        26s
    service/monitoring-influxdb    ClusterIP   10.111.169.94   <none>        8083/TCP,8086/TCP   26s
    
  4. metrics-server 비활성화

    minikube addons disable metrics-server
    

    다음과 유사하게 출력된다.

    metrics-server was successfully disabled
    

제거하기

이제 클러스터에서 만들어진 리소스를 제거할 수 있다.

kubectl delete service hello-node
kubectl delete deployment hello-node

필요하면 Minikube 가상 머신(VM)을 정지한다.

minikube stop

필요하면 minikube VM을 삭제한다.

minikube delete

다음 내용

2 - 쿠버네티스 기초 학습

쿠버네티스 기초

이 튜토리얼에서는 쿠버네티스 클러스터 오케스트레이션 시스템의 기초를 익힐 수 있는 가이드를 제공한다. 각각의 모듈에는 쿠버네티스의 주요 기능과 개념에 대한 배경 지식이 담겨 있으며 대화형 온라인 튜토리얼도 포함되어 있다. 대화형 튜토리얼에서 간단한 클러스터와 그 클러스터 상의 컨테이너화 된 애플리케이션을 직접 관리해볼 수 있다.

대화형 튜토리얼을 사용해서 다음의 내용을 배울 수 있다.

  • 컨테이너화된 애플리케이션을 클러스터에 배포하기.
  • 디플로이먼트를 스케일링하기.
  • 컨테이너화된 애플리케이션을 새로운 소프트웨어 버전으로 업데이트하기.
  • 컨테이너화된 애플리케이션을 디버그하기.

이 튜토리얼에서는 Katacoda를 사용해서 독자의 웹브라우저에서 Minikube가 동작하는 가상 터미널을 구동시킨다. Minikube는 로컬에 설치할 수 있는 작은 규모의 쿠버네티스로써 어디에서든 작동된다. 어떤 소프트웨어도 설치할 필요가 없고, 아무 것도 설정할 필요가 없다. 왜냐하면 대화형 튜토리얼이 웹브라우저 자체에서 바로 동작하기 때문이다.


쿠버네티스가 어떤 도움이 될까?

오늘날의 웹서비스에 대해서, 사용자는 애플리케이션이 24/7 가용하기를 바라고, 개발자는 하루에도 몇 번이고 새로운 버전의 애플리케이션을 배포하기를 바란다. 컨테이너화를 통해 소프트웨어를 패키지하면 애플리케이션을 다운타임 없이 릴리스 및 업데이트할 수 있게 되어서 이런 목표를 달성하는데 도움이 된다. 쿠버네티스는 이렇게 컨테이너화된 애플리케이션을 원하는 곳 어디에든 또 언제든 구동시킬 수 있다는 확신을 갖는데 도움을 주며, 그 애플리케이션이 작동하는데 필요한 자원과 도구를 찾는 것을 도와준다. 쿠버네티스는 구글의 컨테이너 오케스트레이션 부문의 축적된 경험으로 설계되고 커뮤니티로부터 도출된 최고의 아이디어가 결합된 운영 수준의 오픈 소스 플랫폼이다.


2.1 - 클러스터 생성하기

쿠버네티스 클러스터에 대한 내용을 확인하고 Minikube를 사용하여 간단한 클러스터를 생성해 본다.

2.1.1 - Minikube를 사용해서 클러스터 생성하기

목표

  • 쿠버네티스 클러스터가 무엇인지 배운다.
  • Minikube가 무엇인지 배운다.
  • 온라인 터미널을 사용해서 쿠버네티스 클러스터를 시작한다.

쿠버네티스 클러스터

쿠버네티스는 컴퓨터들을 연결하여 단일 형상으로 동작하도록 컴퓨팅 클러스터를 구성하고 높은 가용성을 제공하도록 조율한다. 사용자는 쿠버네티스의 추상화 개념을 통해 개별 머신에 얽매이지 않고 컨테이너화된 애플리케이션을 클러스터에 배포할 수 있다. 이렇게 새로운 배포 모델을 활용하려면, 애플리케이션을 개별 호스트에 독립적인 방식으로 패키징할 필요가 있다. 즉, 컨테이너화가 필요하다. 예전 배치 모델인 설치형 애플리케이션이 특정 머신의 호스트와 밀접하게 통합되는 패키지인 것에 비해, 컨테이너화된 애플리케이션은 유연성(flexible)과 가용성(available)이 훨씬 높다. 쿠버네티스는 이러한 애플리케이션 컨테이너를 클러스터에 분산시키고 스케줄링하는 일을 더욱 효율적으로 자동화한다. 쿠버네티스는 오픈소스 플랫폼이며 운영 수준의 안정성(production-ready)을 제공한다.

쿠버네티스 클러스터는 두 가지 형태의 자원으로 구성된다.

  • 컨트롤 플레인은 클러스터를 조율한다.
  • 노드는 애플리케이션을 구동하는 작업자(worker)이다.

요약:

  • 쿠버네티스 클러스터
  • Minikube

쿠버네티스는 컴퓨터 클러스터에 애플리케이션 컨테이너의 배치(스케줄링) 및 실행을 오케스트레이션하는 운영 수준의 오픈소스 플랫폼이다.


클러스터 다이어그램


컨트롤 플레인은 클러스터 관리를 담당한다. 컨트롤 플레인은 애플리케이션을 스케줄링하거나, 애플리케이션의 항상성을 유지하거나, 애플리케이션을 스케일링하고, 새로운 변경사항을 순서대로 반영(rolling out)하는 일과 같은 클러스터 내 모든 활동을 조율한다.

노드는 쿠버네티스 클러스터 내 워커 머신으로 동작하는 VM 또는 물리적인 컴퓨터다. 각 노드는 노드를 관리하고 쿠버네티스 컨트롤 플레인과 통신하는 Kubelet이라는 에이전트를 갖는다. 노드는 컨테이너 운영을 담당하는 containerd 또는 도커와 같은 툴도 갖는다. 운영 트래픽을 처리하는 쿠버네티스 클러스터는 최소 세 대의 노드를 가져야 하는데, 이는 한 노드가 다운되면 etcd 멤버와 컨트롤 플레인 인스턴스가 사라져 중복성(redundancy)을 잃기 때문이다. 컨트롤 플레인 노드를 추가하여 이러한 위험을 줄일 수 있다.

컨트롤 플레인은 실행 중인 애플리케이션을 호스팅하기 위해 사용되는 노드와 클러스터를 관리한다.

애플리케이션을 쿠버네티스에 배포하기 위해서는, 컨트롤 플레인에 애플리케이션 컨테이너의 구동을 지시하면 된다. 그러면 컨트롤 플레인은 컨테이너를 클러스터의 어느 노드에 구동시킬지 스케줄한다. 노드는 컨트롤 플레인이 제공하는 쿠버네티스 API를 통해서 컨트롤 플레인과 통신한다. 최종 사용자도 쿠버네티스 API를 사용해서 클러스터와 직접 상호작용(interact)할 수 있다.

쿠버네티스 클러스터는 물리 및 가상 머신 모두에 설치될 수 있다. 쿠버네티스 개발을 시작하려면 Minikube를 사용할 수 있다. Minikube는 가벼운 쿠버네티스 구현체이며, 로컬 머신에 VM을 만들고 하나의 노드로 구성된 간단한 클러스터를 생성한다. Minikube는 리눅스, 맥, 그리고 윈도우 시스템에서 구동이 가능하다. Minikube CLI는 클러스터에 대해 시작, 중지, 상태 조회 및 삭제 등의 기본적인 부트스트래핑(bootstrapping) 기능을 제공한다. 하지만, 본 튜토리얼에서는 Minikube가 미리 설치된 채로 제공되는 온라인 터미널을 사용할 것이다.

쿠버네티스가 무엇인지 알아봤으니, 이제 온라인 튜토리얼로 이동해서 우리의 첫 번째 클러스터를 시작해보자!


2.1.2 - 대화형 튜토리얼 - 클러스터 생성하기

화면이 너무 좁아 터미널과 상호작용할 수 없습니다. 데스크톱/태블릿을 사용해주세요.

2.2 - 앱 배포하기

2.2.1 - kubectl을 사용해서 디플로이먼트 생성하기

목표

  • 애플리케이션 디플로이먼트(Deployment)에 대해 배운다.
  • kubectl로 첫 애플리케이션을 쿠버네티스에 배포한다.

쿠버네티스 디플로이먼트

일단 쿠버네티스 클러스터를 구동시키면, 그 위에 컨테이너화된 애플리케이션을 배포할 수 있다. 그러기 위해서, 쿠버네티스 디플로이먼트 설정을 만들어야 한다. 디플로이먼트는 쿠버네티스가 애플리케이션의 인스턴스를 어떻게 생성하고 업데이트해야 하는지를 지시한다. 디플로이먼트가 만들어지면, 쿠버네티스 컨트롤 플레인이 해당 디플로이먼트에 포함된 애플리케이션 인스턴스가 클러스터의 개별 노드에서 실행되도록 스케줄한다.

애플리케이션 인스턴스가 생성되면, 쿠버네티스 디플로이먼트 컨트롤러는 지속적으로 이들 인스턴스를 모니터링한다. 인스턴스를 구동 중인 노드가 다운되거나 삭제되면, 디플로이먼트 컨트롤러가 인스턴스를 클러스터 내부의 다른 노드의 인스턴스로 교체시켜준다.이렇게 머신의 장애나 정비에 대응할 수 있는 자동 복구(self-healing) 메커니즘을 제공한다.

오케스트레이션 기능이 없던 환경에서는, 설치 스크립트가 애플리케이션을 시작하는데 종종 사용되곤 했지만, 머신의 장애가 발생한 경우 복구를 해주지는 않았다. 쿠버네티스 디플로이먼트는 애플리케이션 인스턴스를 생성해주고 여러 노드에 걸쳐서 지속적으로 인스턴스가 구동되도록 하는 두 가지를 모두 하기 때문에 애플리케이션 관리를 위한 접근법에서 근본적인 차이를 가져다준다.

요약:

  • 디플로이먼트
  • Kubectl

디플로이먼트는 애플리케이션 인스턴스를 생성하고 업데이트하는 역할을 담당한다.


쿠버네티스에 첫 번째 애플리케이션 배포하기


Kubectl이라는 쿠버네티스 CLI를 통해 디플로이먼트를 생성하고 관리할 수 있다. Kubectl은 클러스터와 상호 작용하기 위해 쿠버네티스 API를 사용한다. 이 모듈에서는, 쿠버네티스 클러스터 상에 애플리케이션을 구동시키는 디플로이먼트를 생성하기 위해 필요한 가장 일반적인 Kubectl 명령어를 배우게 된다.

디플로이먼트를 생성할 때, 애플리케이션에 대한 컨테이너 이미지와 구동시키고자 하는 복제 수를 지정해야 한다. 디플로이먼트를 업데이트해서 이런 정보를 나중에 변경할 수 있다. 모듈 56의 부트캠프에서 어떻게 스케일하고 업데이트하는지에 대해 다룬다.

애플리케이션이 쿠버네티스 상에 배포되려면 지원되는 컨테이너 형식 중 하나로 패키지 되어야한다.

첫 번째 디플로이먼트로, NGINX를 사용해 모든 요청을 에코(echo)하는 도커 컨테이너로 패키지한 hello-node 애플리케이션을 사용해보자. (아직 hello-node 애플리케이션을 작성하고 컨테이너를 활용해서 배포해보지 않았다면, Hello Minikube 튜토리얼의 지시를 따른다.)

이제 디플로이먼트를 이해했으니, 온라인 튜토리얼을 통해 우리의 첫 번째 애플리케이션을 배포해보자!


2.2.2 - 대화형 튜토리얼 - 앱 배포하기

파드는 쿠버네티스 애플리케이션의 기본 실행 단위이다. 각 파드는 클러스터에서 실행중인 워크로드의 일부를 나타낸다. 파드에 대해 더 자세히 알아본다.


터미널로 상호 작용하기 위해서, 데스크탑/태블릿 버전을 사용해주세요

2.3 - 앱 조사하기

2.3.1 - 파드와 노드 보기

목표

  • 쿠버네티스 파드에 대해 배운다.
  • 쿠버네티스 노드에 대해 배운다.
  • 배포된 애플리케이션의 문제를 해결한다.

쿠버네티스 파드

모듈 2에서 배포를 생성했을 때, 쿠버네티스는 여러분의 애플리케이션 인스턴스에 파드를 생성했다. 파드는 하나 또는 그 이상의 애플리케이션 컨테이너 (도커와 같은)들의 그룹을 나타내는 쿠버네티스의 추상적 개념으로 일부는 컨테이너에 대한 자원을 공유한다. 그 자원은 다음을 포함한다:

  • 볼륨과 같은, 공유 스토리지
  • 클러스터 IP 주소와 같은, 네트워킹
  • 컨테이너 이미지 버전 또는 사용할 특정 포트와 같이, 각 컨테이너가 동작하는 방식에 대한 정보

파드는 특유한 "로컬호스트" 애플리케이션 모형을 만들어. 상대적으로 밀접하게 결합되어진 상이한 애플리케이션 컨테이너들을 수용할 수 있다. 가령, 파드는 Node.js 앱과 더불어 Node.js 웹서버에 의해 발행되는 데이터를 공급하는 상이한 컨테이너를 함께 수용할 수 있다. 파드 내 컨테이너는 IP 주소, 그리고 포트 스페이스를 공유하고 항상 함께 위치하고 함께 스케쥴링 되고 동일 노드 상의 컨텍스트를 공유하면서 동작한다.

파드는 쿠버네티스 플랫폼 상에서 최소 단위가 된다. 우리가 쿠버네티스에서 배포를 생성할 때, 그 배포는 컨테이너 내부에서 컨테이너와 함께 파드를 생성한다. 각 파드는 스케쥴 되어진 노드에게 묶여지게 된다. 그리고 (재구동 정책에 따라) 소멸되거나 삭제되기 전까지 그 노드에 유지된다. 노드에 실패가 발생할 경우, 클러스터 내에 가용한 다른 노드들을 대상으로 스케쥴되어진다.

요약:

  • 파드
  • 노드
  • Kubectl 주요 명령어

파드는 하나 또는 그 이상의 애플리케이션 컨테이너 (도커와 같은)들의 그룹이고 공유 스토리지 (볼륨), IP 주소 그리고 그것을 동작시키는 방식에 대한 정보를 포함하고 있다.


파드 개요


노드

파드는 언제나 노드 상에서 동작한다. 노드는 쿠버네티스에서 워커 머신을 말하며 클러스터에 따라 가상 또는 물리 머신일 수 있다. 각 노드는 컨트롤 플레인에 의해 관리된다. 하나의 노드는 여러 개의 파드를 가질 수 있고, 쿠버네티스 컨트롤 플레인은 클러스터 내 노드를 통해서 파드에 대한 스케쥴링을 자동으로 처리한다. 컨트롤 플레인의 자동 스케줄링은 각 노드의 사용 가능한 리소스를 모두 고려한다.

모든 쿠버네티스 노드는 최소한 다음과 같이 동작한다.

  • Kubelet은, 쿠버네티스 컨트롤 플레인과 노드 간 통신을 책임지는 프로세스이며, 하나의 머신 상에서 동작하는 파드와 컨테이너를 관리한다.
  • 컨테이너 런타임(도커와 같은)은 레지스트리에서 컨테이너 이미지를 가져와 묶여 있는 것을 풀고 애플리케이션을 동작시키는 책임을 맡는다.

만약 컨테이너들이 밀접하게 결합되어 있고 디스크와 같은 자원을 공유해야 한다면 오직 하나의 단일 파드에 함께 스케쥴되어져야 한다.


노드 개요


kubectl로 문제해결하기

모듈 2에서, Kubectl 커맨드-라인 인터페이스를 사용했다. 배포된 애플리케이션과 그 환경에 대한 정보를 얻기 위해 모듈3에서도 계속 그것을 사용할 것이다. 가장 보편적인 운용업무는 다음 kubectl 명령어를 이용하여 처리할 수 있다:

  • kubectl get - 자원을 나열한다
  • kubectl describe - 자원에 대해 상세한 정보를 보여준다.
  • kubectl logs - 파드 내 컨테이너의 로그들을 출력한다
  • kubectl exec - 파드 내 컨테이너에 대한 명령을 실행한다.

언제 애플리케이션이 배포되었으며, 현재 상태가 어떠한지, 그것의 구성은 어떠한지 등을 보기 위해 이러한 명령을 이용할 수 있다.

이제 클러스터 컴포넌트와 커맨드 라인에 대해 알아 보았으니, 애플리케이션을 조사해 보자.

노드는 쿠버네티스에 있어서 워커 머신이며 클러스터에 따라 VM 또는 물리 머신이 될 수 있다. 여러 개의 파드는 하나의 노드 위에서 동작할 수 있다.


2.3.2 - 대화형 튜토리얼 - 앱 조사하기


터미널과 상호작용하기 위해, 데스크탑/태블릿 버전을 이용한다.

2.4 - 앱 외부로 노출하기

2.4.1 - 앱 노출을 위해 서비스 이용하기

목표

  • 쿠버네티스의 서비스에 대해 배운다.
  • 레이블과 레이블셀랙터 오브젝트가 어떻게 서비스와 연관되는지 이해한다.
  • 서비스를 이용하여 쿠버네티스 클러스터 외부로 애플리케이션을 노출한다.

쿠버네티스 서비스들에 대한 개요

쿠버네티스 파드들 은 언젠가는 죽게된다. 파드들은 생명주기를 갖는다. 워커 노드가 죽으면, 노드 상에서 동작하는 파드들 또한 종료된다. 레플리카셋(ReplicaSet)은 여러분의 애플리케이션이 지속적으로 동작할 수 있도록 새로운 파드들의 생성을 통해 동적으로 클러스터를 미리 지정해 둔 상태로 되돌려 줄 수도 있다. 또 다른 예시로서, 3개의 복제본을 갖는 이미지 처리용 백엔드를 고려해 보자. 그 복제본들은 교체 가능한 상태이다. 그래서 프론트엔드 시스템은 하나의 파드가 소멸되어 재생성이 되더라도, 백엔드 복제본들에 의한 영향을 받아서는 안된다. 즉, 동일 노드 상의 파드들이라 할지라도, 쿠버네티스 클러스터 내 각 파드는 유일한 IP 주소를 가지며, 여러분의 애플리케이션들이 지속적으로 기능할 수 있도록 파드들 속에서 발생하는 변화에 대해 자동으로 조정해 줄 방법이 있어야 한다.

쿠버네티스에서 서비스는 하나의 논리적인 파드 셋과 그 파드들에 접근할 수 있는 정책을 정의하는 추상적 개념이다. 서비스는 종속적인 파드들 사이를 느슨하게 결합되도록 해준다. 서비스는 모든 쿠버네티스 오브젝트들과 같이 YAML (보다 선호하는) 또는 JSON을 이용하여 정의된다. 서비스가 대상으로 하는 파드 셋은 보통 LabelSelector에 의해 결정된다 (여러분이 왜 스펙에 selector가 포함되지 않은 서비스를 필요로 하게 될 수도 있는지에 대해 아래에서 확인해 보자).

비록 각 파드들이 고유의 IP를 갖고 있기는 하지만, 그 IP들은 서비스의 도움없이 클러스터 외부로 노출되어질 수 없다. 서비스들은 여러분의 애플리케이션들에게 트래픽이 실릴 수 있도록 허용해준다. 서비스들은 ServiceSpec에서 type을 지정함으로써 다양한 방식들로 노출시킬 수 있다:

  • ClusterIP (기본값) - 클러스터 내에서 내부 IP 에 대해 서비스를 노출해준다. 이 방식은 오직 클러스터 내에서만 서비스가 접근될 수 있도록 해준다.
  • NodePort - NAT가 이용되는 클러스터 내에서 각각 선택된 노드들의 동일한 포트에 서비스를 노출시켜준다. <NodeIP>:<NodePort>를 이용하여 클러스터 외부로부터 서비스가 접근할 수 있도록 해준다. ClusterIP의 상위 집합이다.
  • LoadBalancer - (지원 가능한 경우) 기존 클라우드에서 외부용 로드밸런서를 생성하고 서비스에 고정된 공인 IP를 할당해준다. NodePort의 상위 집합이다.
  • ExternalName - CNAME 레코드 및 값을 반환함으로써 서비스를 externalName 필드의 내용(예를 들면, foo.bar.example.com)에 매핑한다. 어떠한 종류의 프록시도 설정되지 않는다. 이 방식은 kube-dns v1.7 이상 또는 CoreDNS 버전 0.0.8 이상을 필요로 한다.

다른 서비스 타입들에 대한 추가 정보는 소스 IP 이용하기 튜토리얼에서 확인 가능하다. 또한 서비스들로 애플리케이션에 접속하기도 참고해 보자.

부가적으로, spec에 selector를 정의하지 않고 말아넣은 서비스들의 몇 가지 유즈케이스들이 있음을 주의하자. selector 없이 생성된 서비스는 상응하는 엔드포인트 오브젝트들 또한 생성하지 않는다. 이로써 사용자들로 하여금 하나의 서비스를 특정한 엔드포인트에 매핑 시킬수 있도록 해준다. selector를 생략하게 되는 또 다른 가능성은 여러분이 type: ExternalName을 이용하겠다고 확고하게 의도하는 경우이다.

요약

  • 파드들을 외부 트래픽에 노출하기
  • 여러 파드에 걸쳐서 트래픽 로드밸런싱 하기
  • 레이블 사용하기

쿠버네티스 서비스는 논리적 파드 셋을 정의하고 외부 트래픽 노출, 로드밸런싱 그리고 그 파드들에 대한 서비스 디스커버리를 가능하게 해주는 추상 계층이다.


서비스와 레이블

서비스는 파드 셋에 걸쳐서 트래픽을 라우트한다. 여러분의 애플리케이션에 영향을 주지 않으면서 쿠버네티스에서 파드들이 죽게도 하고, 복제가 되게도 해주는 추상적 개념이다. 종속적인 파드들 사이에서의 디스커버리와 라우팅은 (하나의 애플리케이션에서 프로트엔드와 백엔드 컴포넌트와 같은) 쿠버네티스 서비스들에 의해 처리된다.

서비스는 쿠버네티스의 객체들에 대해 논리 연산을 허용해주는 기본 그룹핑 단위인, 레이블과 셀렉터를 이용하여 파드 셋과 매치시킨다. 레이블은 오브젝트들에 붙여진 키/밸류 쌍으로 다양한 방식으로 이용 가능하다:

  • 개발, 테스트, 그리고 상용환경에 대한 객체들의 지정
  • 임베디드된 버전 태그들
  • 태그들을 이용하는 객체들에 대한 분류


레이블은 오브젝트의 생성 시점 또는 이후 시점에 붙여질 수 있다. 언제든지 수정이 가능하다. 이제 서비스를 이용하여 우리의 애플리케이션을 노출도 시켜보고 레이블도 적용해 보자.


2.4.2 - 대화형 튜토리얼 - 앱 노출하기

터미널과 상호작용하기 위해, 데스크탑/태블릿 버전을 이용한다.

2.5 - 앱 스케일링하기

2.5.1 - 복수의 앱 인스턴스를 구동하기

목표

  • kubectl을 사용해서 애플리케이션을 스케일한다.

애플리케이션을 스케일하기

지난 모듈에서 디플로이먼트를 만들고, 서비스를 통해서 디플로이먼트를 외부에 노출시켜 봤다. 해당 디플로이먼트는 애플리케이션을 구동하기 위해 단 하나의 파드만을 생성했었다. 트래픽이 증가하면, 사용자 요청에 맞추어 애플리케이션의 규모를 조정할 필요가 있다.

디플로이먼트의 복제 수를 변경하면 스케일링이 수행된다

요약:

  • 디플로이먼트 스케일링하기

kubectl create deployment 명령에 --replicas 파라미터를 사용해서 처음부터 복수의 인스턴스로 구동되는 디플로이먼트를 만들 수도 있다


스케일링 개요


디플로이먼트를 스케일 아웃하면 신규 파드가 생성되어서 가용한 자원이 있는 노드에 스케줄된다. 스케일링 기능은 새로 의도한 상태(desired state)까지 파드의 수를 늘린다. 쿠버네티스는 파드의 오토스케일링 도 지원하지만 본 튜토리얼에서는 다루지 않는다. 0까지 스케일링하는 것도 가능하다. 이 경우 해당 디플로이먼트의 모든 파드가 종료된다.

애플리케이션의 인스턴스를 복수로 구동하게 되면 트래픽을 해당 인스턴스 모두에 분산시킬 방법이 필요해진다. 서비스는 노출된 디플로이먼트의 모든 파드에 네트워크 트래픽을 분산시켜줄 통합된 로드밸런서를 갖는다. 서비스는 엔드포인트를 이용해서 구동중인 파드를 지속적으로 모니터링함으로써 가용한 파드에만 트래픽이 전달되도록 해준다.

디플로이먼트의 복제 수를 변경하면 스케일링이 수행된다.


일단 복수의 애플리케이션의 인스턴스가 구동 중이면, 다운타임 없이 롤링 업데이트를 할 수 있다. 다음 모듈에서 이 내용을 다루도록 하겠다. 이제 온라인 터미널로 가서 애플리케이션을 스케일해보자.


2.5.2 - 대화형 튜토리얼 - 앱 스케일링하기

터미널로 상호 작용하기 위해서, 데스크탑/태블릿 버전을 사용해주세요

2.6 - 앱 업데이트하기

2.6.1 - 롤링 업데이트 수행하기

목표

  • kubectl을 이용하여 롤링 업데이트 수행하기

애플리케이션 업데이트하기

사용자들은 애플리케이션이 항상 가용한 상태일 것이라 여기고 개발자들은 하루에 여러번씩 새로운 버전을 배포하도록 요구 받고있다. 쿠버네티스에서는 이것을 롤링 업데이트를 통해 이루고 있다. 롤링 업데이트는 파드 인스턴스를 점진적으로 새로운 것으로 업데이트하여 디플로이먼트 업데이트가 서비스 중단 없이 이루어질 수 있도록 해준다. 새로운 파드는 가용한 자원을 보유한 노드로 스케줄될 것이다.

이전 모듈에서 여러 개의 인스턴스를 동작시키도록 애플리케이션을 스케일했다. 이것은 애플리케이션의 가용성에 영향을 미치지 않으면서 업데이트를 수행하는 것에 대한 요구이다. 기본적으로, 업데이트가 이루어지는 동안 이용 불가한 파드의 최대 개수와 생성 가능한 새로운 파드의 최대 개수는 하나다. 두 옵션은 (파드에 대한) 개수 또는 백분율로 구성될 수 있다. 쿠버네티스에서, 업데이트는 버전으로 관리되고 어떠한 디플로이먼트 업데이트라도 이전의 (안정적인) 버전으로 원복이 가능하다.

요약:

  • 앱 업데이트하기

롤링 업데이트는 파드 인스턴스를 점진적으로 새로운 것으로 업데이트하여 디플로이먼트 업데이트가 서비스 중단 없이 이루어질 수 있도록 해준다.


롤링 업데이트 개요


애플리케이션 스케일링과 유사하게, 디플로이먼트가 외부로 노출되면, 서비스는 업데이트가 이루어지는 동안 오직 가용한 파드에게만 트래픽을 로드밸런스 할 것이다. 가용한 파드란 애플리케이션의 사용자들에게 가용한 상태의 인스턴스를 말한다.

롤링 업데이트는 다음 동작들을 허용해준다:

  • 하나의 환경에서 또 다른 환경으로의 애플리케이션 프로모션 (컨테이너 이미지 업데이트를 통해)
  • 이전 버전으로의 롤백
  • 서비스 중단 없는 애플리케이션의 지속적인 통합과 지속적인 전달

디플로이먼트가 외부로 노출되면, 서비스는 업데이트가 이루어지는 동안 오직 가용한 파드에게만 트래픽을 로드밸런스 할 것이다.


다음 대화형 튜토리얼에서, 새로운 버전으로 애플리케이션을 업데이트하고, 롤백 또한 수행해 볼 것이다.


2.6.2 - 대화형 튜토리얼 - 앱 업데이트 하기

터미널과 상호작용하기 위해, 데스크탑/태블릿 버전을 이용한다.

3 - 설정

3.1 - 예제: Java 마이크로서비스 구성하기

3.1.1 - MicroProfile, 컨피그맵(ConfigMaps) 및 시크릿(Secrets)을 사용하여 구성 외부화(externalizing)

이 튜토리얼에서는 마이크로서비스의 구성을 외부화하는 방법과 이유를 알아본다. 특히, 쿠버네티스 컨피그맵과 시크릿을 사용하여 환경 변수를 설정한 다음 MicroProfile Config를 이용한 사용 방법을 배운다.

시작하기 전에

쿠버네티스 컨피그맵 및 시크릿 생성하기

쿠버네티스에서 도커 컨테이너에 대한 환경 변수를 설정하는 방법에는 Dockerfile, kubernetes.yml, 쿠버네티스 컨피그맵 및 쿠버네티스 시크릿이 있다. 이 튜토리얼에서는 사용자의 마이크로서비스에 값을 주입하기 위한 환경 변수를 설정하기 위해 후자의 두 가지를 사용하는 방법에 대해 배운다. 컨피그맵 및 시크릿을 사용할 때의 이점 중 하나는 여러 다른 컨테이너에 대해 서로 다른 환경 변수에 할당되는 것을 포함하여, 여러 컨테이너에서 다시 사용할 수 있다는 것이다.

컨피그맵은 기밀이 아닌 키-값 쌍을 저장하는 API 오브젝트이다. 대화형 튜토리얼에서는 컨피그맵을 사용하여 애플리케이션의 이름을 저장하는 방법을 배운다. 컨피그맵에 대한 자세한 정보는 여기에서 문서를 찾을 수 있다.

시크릿은 키-값 쌍을 저장하는 데도 사용되지만, 기밀/민감한 정보를 위한 것이며 Base64 인코딩을 사용하여 저장된다는 점에서 컨피그맵과 다르다. 따라서 시크릿은 자격 증명, 키 및 토큰과 같은 항목을 저장하는 데 적합한 선택이 된다. 이 내용은 대화형 튜토리얼에서 수행할 것이다. 시크릿에 대한 자세한 내용은 여기에서 문서를 찾을 수 있다.

코드로부터 구성 외부화

구성은 일반적으로 환경에 따라 변경되기 때문에, 외부화된 애플리케이션 구성(externalized application configuration)은 유용하다. 이를 이루기 위해, Java의 CDI(콘텍스트와 의존성 주입) 및 MicroProfile Config를 사용한다. MicroProfile Config는 클라우드 네이티브 마이크로서비스를 개발하고 배포하기 위한 개방형 Java 기술 세트인 MicroProfile의 기능이다.

CDI는 느슨하게 결합된 협업 빈(beans)을 통해 애플리케이션을 어셈블할 수 있는 표준 종속성 주입(standard dependency injection) 기능을 제공한다. MicroProfile Config는 애플리케이션, 런타임 및 환경을 포함한 다양한 소스에서 구성 속성을 가져오는 표준 방법을 앱과 마이크로서비스에 제공한다. 소스의 정의된 우선순위에 따라 속성은 애플리케이션이 API를 통해 접근할 수 있는 단일 속성 집합으로 자동 결합된다. 대화형 튜토리얼에서는 CDI와 MicroProfile을 함께 사용하여 쿠버네티스 컨피그맵 및 시크릿을 통한 외부 제공 속성을 검색하고 애플리케이션 코드에 삽입한다.

많은 오픈 소스 프레임워크와 런타임이 MicroProfile Config를 구현하고 지원한다. 대화형 튜토리얼을 통해 클라우드 네이티브 앱과 마이크로서비스를 빌드하고 실행하기 위한 유연한 오픈 소스 Java 런타임인 Open Liberty를 사용하게 될 것이다. 그러나, 모든 MicroProfile 호환 런타임을 대신 사용할 수 있다.

목적

  • 쿠버네티스 컨피그맵 및 시크릿 생성
  • MicroProfile Config를 사용하여 마이크로서비스 구성 주입

예제: MicroProfile, 컨피그맵 및 시크릿을 사용하여 구성 외부화

대화형 튜토리얼 시작

3.1.2 - 대화형 튜토리얼 - Java 마이크로서비스 구성하기

터미널 화면을 조작하려면, 데스크톱/태블릿 버전을 사용하기 바란다.

3.2 - 컨피그맵을 사용해서 Redis 설정하기

이 페이지에서는 컨피그맵(ConfigMap)을 사용해서 Redis를 설정하는 방법에 대한 실세계 예제를 제공하고, 컨피그맵을 사용해서 파드 설정하기 태스크로 빌드를 한다.

목적

  • Redis 설정값으로 컨피그맵을 생성한다.
  • 생성된 컨피그맵을 마운트하고 사용하는 Redis 파드를 생성한다.
  • 설정이 잘 적용되었는지 확인한다.

시작하기 전에

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

버전 확인을 위해서, 다음 커맨드를 실행 kubectl version.

실세상 예제: 컨피그맵을 사용해서 Redis 설정하기

아래 단계를 통해서, 컨피그맵에 저장된 데이터를 사용하는 Redis 캐시를 설정한다.

우선, 비어 있는 설정으로 컨피그맵을 생성한다.

cat <<EOF >./example-redis-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: example-redis-config
data:
  redis-config: ""
EOF

위에서 생성한 컨피그맵을 Redis 파드 매니페스트와 함께 적용한다.

kubectl apply -f example-redis-config.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/pods/config/redis-pod.yaml

Redis 파드 매니페스트의 내용을 검토하고 다음의 사항을 염두에 둔다.

  • config 라는 이름의 볼륨은 spec.volumes[1] 에 의해서 생성된다.
  • spec.volumes[1].items[0] 내부의 keypathconfig 볼륨에 redis.conf 라는 파일명으로 지정된 example-redis-config 컨피그맵의 redis-config 키를 노출시킨다.
  • 그리고 config 볼륨은 spec.containers[0].volumeMounts[1] 에 의해서 /redis-master 에 마운트된다.

이 내용은 위의 example-redis-config 컨피그맵의 data.redis-config 내부 데이터를 파드 안에 있는 /redis-master/redis.conf 파일의 내용으로 노출시키는 순효과(net effect)를 낸다.

apiVersion: v1
kind: Pod
metadata:
  name: redis
spec:
  containers:
  - name: redis
    image: redis:5.0.4
    command:
      - redis-server
      - "/redis-master/redis.conf"
    env:
    - name: MASTER
      value: "true"
    ports:
    - containerPort: 6379
    resources:
      limits:
        cpu: "0.1"
    volumeMounts:
    - mountPath: /redis-master-data
      name: data
    - mountPath: /redis-master
      name: config
  volumes:
    - name: data
      emptyDir: {}
    - name: config
      configMap:
        name: example-redis-config
        items:
        - key: redis-config
          path: redis.conf

생성된 오브젝트를 확인한다.

kubectl get pod/redis configmap/example-redis-config

다음의 결과를 볼 수 있다.

NAME        READY   STATUS    RESTARTS   AGE
pod/redis   1/1     Running   0          8s

NAME                             DATA   AGE
configmap/example-redis-config   1      14s

example-redis-config 컨피그맵의 redis-config 키를 공란으로 둔 것을 기억하자.

kubectl describe configmap/example-redis-config

redis-config 키가 비어 있는 것을 확인할 수 있다.

Name:         example-redis-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
redis-config:

kubectl exec 를 사용하여 파드에 접속하고, 현재 설정 확인을 위해서 redis-cli 도구를 실행한다.

kubectl exec -it redis -- redis-cli

maxmemory 를 확인한다.

127.0.0.1:6379> CONFIG GET maxmemory

기본값인 0을 볼 수 있을 것이다.

1) "maxmemory"
2) "0"

유사하게, maxmemory-policy 를 확인한다.

127.0.0.1:6379> CONFIG GET maxmemory-policy

이것도 기본값인 noeviction 을 보여줄 것이다.

1) "maxmemory-policy"
2) "noeviction"

이제 example-redis-config 컨피그맵에 몇 가지 설정값을 추가해 본다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: example-redis-config
data:
  redis-config: |
    maxmemory 2mb
    maxmemory-policy allkeys-lru    

갱신된 컨피그맵을 적용한다.

kubectl apply -f example-redis-config.yaml

컨피그맵이 갱신된 것을 확인한다.

kubectl describe configmap/example-redis-config

방금 추가한 설정값을 확인할 수 있을 것이다.

Name:         example-redis-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
redis-config:
----
maxmemory 2mb
maxmemory-policy allkeys-lru

설정이 적용되었는지 확인하려면, kubectl exec 를 통한 redis-cli 로 Redis 파드를 다시 확인한다.

kubectl exec -it redis -- redis-cli

maxmemory 를 확인한다.

127.0.0.1:6379> CONFIG GET maxmemory

기본값인 0을 볼 수 있을 것이다.

1) "maxmemory"
2) "0"

유사하게, maxmemory-policy 도 기본 설정인 noeviction 을 보여줄 것이다.

127.0.0.1:6379> CONFIG GET maxmemory-policy

위의 명령은 다음을 반환한다.

1) "maxmemory-policy"
2) "noeviction"

파드는 연관된 컨피그맵에서 갱신된 값을 인지하기 위해서 재시작이 필요하므로 해당 설정값이 변경되지 않은 상태이다. 파드를 삭제하고 다시 생성한다.

kubectl delete pod redis
kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/pods/config/redis-pod.yaml

이제 마지막으로 설정값을 다시 확인해 본다.

kubectl exec -it redis -- redis-cli

maxmemory 를 확인한다.

127.0.0.1:6379> CONFIG GET maxmemory

이것은 이제 갱신된 값인 2097152를 반환한다.

1) "maxmemory"
2) "2097152"

유사하게, maxmemory-policy 도 갱신되어 있다.

127.0.0.1:6379> CONFIG GET maxmemory-policy

이것은 원하는 값인 allkeys-lru 를 반환한다.

1) "maxmemory-policy"
2) "allkeys-lru"

생성된 자원을 삭제하여 작업을 정리한다.

kubectl delete pod/redis configmap/example-redis-config

다음 내용

4 - 보안

4.1 - AppArmor를 사용하여 리소스에 대한 컨테이너의 접근 제한

기능 상태: Kubernetes v1.4 [beta]

AppArmor는 표준 리눅스 사용자와 그룹 기반의 권한을 보완하여, 한정된 리소스 집합으로 프로그램을 제한하는 리눅스 커널 보안 모듈이다. AppArmor는 임의의 애플리케이션에 대해서 잠재적인 공격 범위를 줄이고 더욱 심층적인 방어를 제공하도록 구성할 수 있다. 이 기능은 특정 프로그램이나 컨테이너에서 필요한 리눅스 기능, 네트워크 사용, 파일 권한 등에 대한 접근을 허용하는 프로파일로 구성한다. 각 프로파일은 허용하지 않은 리소스 접근을 차단하는 강제(enforcing) 모드 또는 위반만을 보고하는 불평(complain) 모드로 실행할 수 있다.

AppArmor를 이용하면 컨테이너가 수행할 수 있는 작업을 제한하고 또는 시스템 로그를 통해 더 나은 감사를 제공하여 더 안전한 배포를 실행할 수 있다. 그러나 AppArmor가 은탄환(언제나 통하는 무적의 방법)이 아니며, 애플리케이션 코드 취약점을 보호하기 위한 여러 조치를 할 수 있는 것 뿐임을 잊으면 안된다. 양호하고 제한적인 프로파일을 제공하고, 애플리케이션과 클러스터를 여러 측면에서 강화하는 것이 중요하다.

목적

  • 노드에 프로파일을 어떻게 적재하는지 예시를 본다.
  • 파드에 프로파일을 어떻게 강제 적용하는지 배운다.
  • 프로파일이 적재되었는지 확인하는 방법을 배운다.
  • 프로파일을 위반하는 경우를 살펴본다.
  • 프로파일을 적재할 수 없을 경우를 살펴본다.

시작하기 전에

다음을 보장해야 한다.

  1. 쿠버네티스 버전은 최소 1.4 이다. -- 쿠버네티스 v1.4부터 AppArmor 지원을 추가했다. v1.4 이전 쿠버네티스 컴포넌트는 새로운 AppArmor 어노테이션을 인식하지 못하고 제공되는 AppArmor 설정을 조용히 무시할 것이다. 파드에서 예상하는 보호를 받고 있는지 확인하려면 해당 노드의 Kubelet 버전을 확인하는 것이 중요하다.

    $ kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {@.status.nodeInfo.kubeletVersion}\n{end}'
    
    gke-test-default-pool-239f5d02-gyn2: v1.4.0
    gke-test-default-pool-239f5d02-x1kf: v1.4.0
    gke-test-default-pool-239f5d02-xwux: v1.4.0
    
  2. AppArmor 커널 모듈을 사용 가능해야 한다. -- 리눅스 커널에 AppArmor 프로파일을 강제 적용하기 위해 AppArmor 커널 모듈은 반드시 설치되어 있고 사용 가능해야 한다. 예를 들어 Ubuntu 및 SUSE 같은 배포판은 모듈을 기본값으로 지원하고, 그 외 많은 다른 배포판들은 선택적으로 지원한다. 모듈이 사용 가능한지 확인하려면 /sys/module/apparmor/parameters/enabled 파일을 확인한다.

    $ cat /sys/module/apparmor/parameters/enabled
    Y
    

    Kubelet(>=v1.4)이 AppArmor 기능 지원을 포함하지만, 커널 모듈을 사용할 수 없으면 파드에서 AppArmor 옵션을 실행하는 것이 거부된다.

  1. 컨테이너 런타임이 AppArmor을 지원한다. -- 현재 모든 일반적인 쿠버네티스를 지원하는 도커(Docker), CRI-O 또는 containerd 와 같은 컨테이너 런타임들은 AppArmor를 지원해야 한다. 이 런타임 설명서를 참조해서 클러스터가 AppArmor를 사용하기 위한 요구 사항을 충족하는지 확인해야 한다.

  2. 프로파일이 적재되어 있다. -- AppArmor는 각 컨테이너와 함께 실행해야 하는 AppArmor 프로파일을 지정하여 파드에 적용한다. 커널에 지정한 프로파일이 적재되지 않았다면, Kubelet(>= v1.4)은 파드를 거부한다. 해당 노드에 어떤 프로파일이 적재되었는지는 /sys/kernel/security/apparmor/profiles 파일을 통해 확인할 수 있다. 예를 들어,

    $ ssh gke-test-default-pool-239f5d02-gyn2 "sudo cat /sys/kernel/security/apparmor/profiles | sort"
    
    apparmor-test-deny-write (enforce)
    apparmor-test-audit-write (enforce)
    docker-default (enforce)
    k8s-nginx (enforce)
    

    노드에 프로파일을 적재하는 것에 대해 더 자세한 내용은 프로파일과 함께 노드 설정하기.

AppArmor 지원이 포함된 Kubelet (>= v1.4)이면 어떤 전제 조건이 충족되지 않으면 AppArmor와 함께한 파드를 거부한다. 노드 상에 AppArmor 지원 여부는 노드 준비 조건 메시지를 확인하여(이후 릴리스에서는 삭제될 것 같지만) 검증할 수 있다.

kubectl get nodes -o=jsonpath='{range .items[*]}{@.metadata.name}: {.status.conditions[?(@.reason=="KubeletReady")].message}{"\n"}{end}'
gke-test-default-pool-239f5d02-gyn2: kubelet is posting ready status. AppArmor enabled
gke-test-default-pool-239f5d02-x1kf: kubelet is posting ready status. AppArmor enabled
gke-test-default-pool-239f5d02-xwux: kubelet is posting ready status. AppArmor enabled

파드 보안 강화하기

AppArmor 프로파일은 컨테이너마다 지정된다. 함께 실행할 파드 컨테이너에 AppArmor 프로파일을 지정하려면 파드의 메타데이터에 어노테이션을 추가한다.

container.apparmor.security.beta.kubernetes.io/<container_name>: <profile_ref>

<container_name>은 프로파일을 적용하는 컨테이너 이름이고, <profile_ref>는 적용할 프로파일을 지정한다. profile_ref는 다음 중에 하나이다.

  • 런타임의 기본 프로파일을 적용하기 위한 runtime/default
  • <profile_name>로 이름한 호스트에 적재되는 프로파일을 적용하기 위한 localhost/<profile_name>
  • 적재할 프로파일이 없음을 나타내는 unconfined

어노테이션과 프로파일 이름 형식의 자세한 내용은 API 참조를 살펴본다.

쿠버네티스 AppArmor 의 작동 순서는 모든 선행 조건이 충족되었는지 확인하고, 적용을 위해 선택한 프로파일을 컨테이너 런타임으로 전달하여 이루어진다. 만약 선행 조건이 충족되지 않으면 파드는 거부되고 실행되지 않는다.

프로파일이 적용되었는지 확인하기 위해, 컨테이너 생성 이벤트에 나열된 AppArmor 보안 옵션을 찾아 볼 수 있다.

kubectl get events | grep Created
22s        22s         1         hello-apparmor     Pod       spec.containers{hello}   Normal    Created     {kubelet e2e-test-stclair-node-pool-31nt}   Created container with docker id 269a53b202d3; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]

컨테이너의 루트 프로세스가 올바른 프로파일로 실행되는지는 proc attr을 확인하여 직접 검증할 수 있다.

kubectl exec <pod_name> -- cat /proc/1/attr/current
k8s-apparmor-example-deny-write (enforce)

예시

이 예시는 AppArmor를 지원하는 클러스터를 이미 구성하였다고 가정한다.

먼저 노드에서 사용하려는 프로파일을 적재해야 한다. 사용할 프로파일은 파일 쓰기를 거부한다.

#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}

파드를 언제 스케줄할지 알지 못하므로 모든 노드에 프로파일을 적재해야 한다. 이 예시에서는 SSH를 이용하여 프로파일을 설치할 것이나 다른 방법은 프로파일과 함께 노드 설정하기에서 논의한다.

NODES=(
    # The SSH-accessible domain names of your nodes
    gke-test-default-pool-239f5d02-gyn2.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-x1kf.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-xwux.us-central1-a.my-k8s)
for NODE in ${NODES[*]}; do ssh $NODE 'sudo apparmor_parser -q <<EOF
#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}
EOF'
done

다음으로 쓰기 금지 프로파일된 "Hello AppArmor" 파드를 실행한다.

apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor
  annotations:
    # 쿠버네티스에 'k8s-apparmor-example-deny-write' AppArmor 프로파일을 적용함을 알린다.
    # 잊지 말 것은 쿠버네티스 노드에서 실행 중인 버전이 1.4 이상이 아닌 경우에는 이 설정은 무시된다는 것이다.
    container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-deny-write
spec:
  containers:
  - name: hello
    image: busybox:1.28
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
kubectl create -f ./hello-apparmor.yaml

파드 이벤트를 살펴보면, 'k8s-apparmor-example-deny-write' AppArmor 프로파일로 생성된 파드 컨테이너를 확인할 수 있다.

kubectl get events | grep hello-apparmor
14s        14s         1         hello-apparmor   Pod                                Normal    Scheduled   {default-scheduler }                           Successfully assigned hello-apparmor to gke-test-default-pool-239f5d02-gyn2
14s        14s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulling     {kubelet gke-test-default-pool-239f5d02-gyn2}   pulling image "busybox"
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulled      {kubelet gke-test-default-pool-239f5d02-gyn2}   Successfully pulled image "busybox"
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Created     {kubelet gke-test-default-pool-239f5d02-gyn2}   Created container with docker id 06b6cd1c0989; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Started     {kubelet gke-test-default-pool-239f5d02-gyn2}   Started container with docker id 06b6cd1c0989

proc attr을 확인하여 컨테이너가 실제로 해당 프로파일로 실행 중인지 확인할 수 있다.

kubectl exec hello-apparmor -- cat /proc/1/attr/current
k8s-apparmor-example-deny-write (enforce)

마지막으로 파일 쓰기를 통해 프로파일을 위반하면 어떻게 되는지 확인할 수 있다.

kubectl exec hello-apparmor -- touch /tmp/test
touch: /tmp/test: Permission denied
error: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1

이제 정리하면서, 적재되지 않은 프로파일을 지정하면 어떻게 되는지 살펴본다.

kubectl create -f /dev/stdin <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor-2
  annotations:
    container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-allow-write
spec:
  containers:
  - name: hello
    image: busybox:1.28
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
EOF
pod/hello-apparmor-2 created
kubectl describe pod hello-apparmor-2
Name:          hello-apparmor-2
Namespace:     default
Node:          gke-test-default-pool-239f5d02-x1kf/
Start Time:    Tue, 30 Aug 2016 17:58:56 -0700
Labels:        <none>
Annotations:   container.apparmor.security.beta.kubernetes.io/hello=localhost/k8s-apparmor-example-allow-write
Status:        Pending
Reason:        AppArmor
Message:       Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded
IP:
Controllers:   <none>
Containers:
  hello:
    Container ID:
    Image:     busybox
    Image ID:
    Port:
    Command:
      sh
      -c
      echo 'Hello AppArmor!' && sleep 1h
    State:              Waiting
      Reason:           Blocked
    Ready:              False
    Restart Count:      0
    Environment:        <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-dnz7v (ro)
Conditions:
  Type          Status
  Initialized   True
  Ready         False
  PodScheduled  True
Volumes:
  default-token-dnz7v:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-dnz7v
    Optional:   false
QoS Class:      BestEffort
Node-Selectors: <none>
Tolerations:    <none>
Events:
  FirstSeen    LastSeen    Count    From                        SubobjectPath    Type        Reason        Message
  ---------    --------    -----    ----                        -------------    --------    ------        -------
  23s          23s         1        {default-scheduler }                         Normal      Scheduled     Successfully assigned hello-apparmor-2 to e2e-test-stclair-minion-group-t1f5
  23s          23s         1        {kubelet e2e-test-stclair-node-pool-t1f5}             Warning        AppArmor    Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded

파드 상태는 Pending이며, 오류 메시지는 Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded이다. 이벤트도 동일한 메시지로 기록되었다.

관리

프로파일과 함께 노드 설정하기

현재 쿠버네티스는 AppArmor 프로파일을 노드에 적재하기 위한 네이티브 메커니즘을 제공하지 않는다. 프로파일을 설정하는 여러 방법이 있다. 예를 들면 다음과 같다.

  • 각 노드에서 파드를 실행하는 데몬셋을 통해서 올바른 프로파일이 적재되었는지 확인한다. 예시 구현은 여기에서 찾아볼 수 있다.
  • 노드 초기화 시간에 노드 초기화 스크립트(예를 들어 Salt, Ansible 등)나 이미지를 이용
  • 예시에서 보여준 것처럼, 프로파일을 각 노드에 복사하고 SSH를 통해 적재한다.

스케줄러는 어떤 프로파일이 어떤 노드에 적재되는지 고려하지 않으니, 프로파일 전체 집합이 모든 노드에 적재되어야 한다. 대안적인 방법은 각 프로파일(혹은 프로파일의 클래스)을 위한 노드 레이블을 노드에 추가하고, 노드 셀렉터를 이용하여 파드가 필요한 프로파일이 있는 노드에서 실행되도록 한다.

AppArmor 비활성화

클러스터에서 AppArmor를 사용하지 않으려면, 커맨드라인 플래그로 비활성화 할 수 있다.

--feature-gates=AppArmor=false

비활성화되면, AppArmor 프로파일을 포함한 파드는 "Forbidden" 오류로 검증 실패한다.

프로파일 제작

AppArmor 프로파일을 만들고 올바르게 지정하는 것은 매우 까다로울 수 있다. 다행히 이 작업에 도움 되는 도구가 있다.

  • aa-genprofaa-logprof는 애플리케이션 활동과 로그와 수행에 필요한 행동을 모니터링하여 일반 프로파일 규칙을 생성한다. 자세한 사용방법은 AppArmor 문서에서 제공한다.
  • bane은 단순화된 프로파일 언어를 이용하는 도커를 위한 AppArmor 프로파일 생성기이다.

AppArmor 문제를 디버깅하기 위해서 거부된 것으로 보이는 시스템 로그를 확인할 수 있다. AppArmor 로그는 dmesg에서 보이며, 오류는 보통 시스템 로그나 journalctl에서 볼 수 있다. 더 많은 정보는 AppArmor 실패에서 제공한다.

API 참조

파드 어노테이션

컨테이너를 실행할 프로파일을 지정한다.

  • : container.apparmor.security.beta.kubernetes.io/<container_name> <container_name>는 파드 내에 컨테이너 이름과 일치한다. 분리된 프로파일은 파드 내에 각 컨테이너로 지정할 수 있다.
  • : 아래 기술된 프로파일 참조

프로파일 참조

  • runtime/default: 기본 런타임 프로파일을 참조한다.
  • localhost/<profile_name>: 노드(localhost)에 적재된 프로파일을 이름으로 참조한다.
  • unconfined: 이것은 컨테이너에서 AppArmor를 효과적으로 비활성시킨다.

다른 어떤 프로파일 참조 형식도 유효하지 않다.

다음 내용

참고 자료

4.2 - 파드 시큐리티 스탠다드를 네임스페이스 수준에 적용하기

파드 시큐리티 어드미션(PSA, Pod Security Admission)은 베타로 변경되어 v1.23 이상에서 기본적으로 활성화되어 있다. 파드 시큐리티 어드미션은 파드가 생성될 때 파드 시큐리티 스탠다드(Pod Security Standards)를 적용하는 어드미션 컨트롤러이다. 이 튜토리얼에서는, 각 네임스페이스별로 baseline 파드 시큐리티 스탠다드를 강제(enforce)할 것이다.

파드 시큐리티 스탠다드를 클러스터 수준에서 여러 네임스페이스에 한 번에 적용할 수도 있다. 이에 대한 안내는 파드 시큐리티 스탠다드를 클러스터 수준에 적용하기를 참고한다.

시작하기 전에

워크스테이션에 다음을 설치한다.

클러스터 생성하기

  1. 다음과 같이 KinD 클러스터를 생성한다.

    kind create cluster --name psa-ns-level --image kindest/node:v1.23.0
    

    다음과 비슷하게 출력될 것이다.

    Creating cluster "psa-ns-level" ...
     ✓ Ensuring node image (kindest/node:v1.23.0) 🖼 
     ✓ Preparing nodes 📦  
     ✓ Writing configuration 📜 
     ✓ Starting control-plane 🕹️ 
     ✓ Installing CNI 🔌 
     ✓ Installing StorageClass 💾 
    Set kubectl context to "kind-psa-ns-level"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-ns-level
    
    Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
    
  2. kubectl context를 새로 생성한 클러스터로 설정한다.

    kubectl cluster-info --context kind-psa-ns-level
    

    다음과 비슷하게 출력될 것이다.

    Kubernetes control plane is running at https://127.0.0.1:50996
    CoreDNS is running at https://127.0.0.1:50996/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    

네임스페이스 생성하기

example이라는 네임스페이스를 생성한다.

kubectl create ns example

다음과 비슷하게 출력될 것이다.

namespace/example created

파드 시큐리티 스탠다드 적용하기

  1. 내장 파드 시큐리티 어드미션이 지원하는 레이블을 사용하여 이 네임스페이스에 파드 시큐리티 스탠다드를 활성화한다. 이 단계에서는 latest 버전(기본값)에 따라 baseline(기준) 파드 시큐리티 스탠다드에 대해 경고를 설정한다.

    kubectl label --overwrite ns example \
       pod-security.kubernetes.io/warn=baseline \
       pod-security.kubernetes.io/warn-version=latest
    
  2. 어떠한 네임스페이스에도 복수 개의 파드 시큐리티 스탠다드를 활성화할 수 있으며, 이는 레이블을 이용하여 가능하다. 다음 명령어는 최신 버전(기본값)에 따라, baseline(기준) 파드 시큐리티 스탠다드는 enforce(강제)하지만 restricted(제한된) 파드 시큐리티 스탠다드에 대해서는 warn(경고)audit(감사)하도록 설정한다.

    kubectl label --overwrite ns example \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest
    

파드 시큐리티 스탠다드 검증하기

  1. example 네임스페이스에 최소한의 파드를 생성한다.

    cat <<EOF > /tmp/pss/nginx-pod.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
    EOF
    
  2. 클러스터의 example 네임스페이스에 해당 파드 스펙을 적용한다.

    kubectl apply -n example -f /tmp/pss/nginx-pod.yaml
    

    다음과 비슷하게 출력될 것이다.

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    
  3. 클러스터의 default 네임스페이스에 해당 파드 스펙을 적용한다.

    kubectl apply -n default -f /tmp/pss/nginx-pod.yaml
    

    다음과 비슷하게 출력될 것이다.

    pod/nginx created
    

파드 시큐리티 스탠다드는 example 네임스페이스에만 적용되었다. 동일한 파드를 default 네임스페이스에 생성하더라도 경고가 발생하지 않는다.

정리하기

kind delete cluster --name psa-ns-level 명령을 실행하여 생성했던 클러스터를 삭제한다.

다음 내용

4.3 - 파드 시큐리티 스탠다드를 클러스터 수준에 적용하기

파드 시큐리티 어드미션(PSA, Pod Security Admission)은 베타로 변경되어 v1.23 이상에서 기본적으로 활성화되어 있다. 파드 시큐리티 어드미션은 파드가 생성될 때 파드 시큐리티 스탠다드(Pod Security Standards)를 적용하는 어드미션 컨트롤러이다. 이 튜토리얼은 baseline 파드 시큐리티 스탠다드를 클러스터 수준(level)에 적용하여 표준 구성을 클러스터의 모든 네임스페이스에 적용하는 방법을 보여 준다.

파드 시큐리티 스탠다드를 특정 네임스페이스에 적용하려면, 파드 시큐리티 스탠다드를 네임스페이스 수준에 적용하기를 참고한다.

만약 쿠버네티스 버전이 v1.30이 아니라면, 해당 버전의 문서를 확인하자.

시작하기 전에

워크스테이션에 다음을 설치한다.

적용할 알맞은 파드 시큐리티 스탠다드 선택하기

파드 시큐리티 어드미션을 이용하여 enforce, audit, 또는 warn 모드 중 하나로 내장 파드 시큐리티 스탠다드를 적용할 수 있다.

현재 구성에 가장 적합한 파드 시큐리티 스탠다드를 고르는 데 도움이 되는 정보를 수집하려면, 다음을 수행한다.

  1. 파드 시큐리티 스탠다드가 적용되지 않은 클러스터를 생성한다.

    kind create cluster --name psa-wo-cluster-pss --image kindest/node:v1.24.0
    

    다음과 비슷하게 출력될 것이다.

    Creating cluster "psa-wo-cluster-pss" ...
    ✓ Ensuring node image (kindest/node:v1.24.0) 🖼
    ✓ Preparing nodes 📦  
    ✓ Writing configuration 📜
    ✓ Starting control-plane 🕹️
    ✓ Installing CNI 🔌
    ✓ Installing StorageClass 💾
    Set kubectl context to "kind-psa-wo-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-wo-cluster-pss
    
    Thanks for using kind! 😊
    
  2. kubectl context를 새로 생성한 클러스터로 설정한다.

    kubectl cluster-info --context kind-psa-wo-cluster-pss
    

    다음과 비슷하게 출력될 것이다.

     Kubernetes control plane is running at https://127.0.0.1:61350
    
    CoreDNS is running at https://127.0.0.1:61350/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  3. 클러스터의 네임스페이스 목록을 조회한다.

    kubectl get ns
    

    다음과 비슷하게 출력될 것이다.

    NAME                 STATUS   AGE
    default              Active   9m30s
    kube-node-lease      Active   9m32s
    kube-public          Active   9m32s
    kube-system          Active   9m32s
    local-path-storage   Active   9m26s
    
  4. --dry-run=server를 사용하여 다른 파드 시큐리티 스탠다드가 적용되었을 때 어떤 것이 변경되는지 확인한다.

    1. Privileged
      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=privileged
      

    다음과 비슷하게 출력될 것이다.

    namespace/default labeled
    namespace/kube-node-lease labeled
    namespace/kube-public labeled
    namespace/kube-system labeled
    namespace/local-path-storage labeled
    
    1. Baseline
      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=baseline
      

    다음과 비슷하게 출력될 것이다.

    namespace/default labeled
    namespace/kube-node-lease labeled
    namespace/kube-public labeled
    Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "baseline:latest"
    Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes
    Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes
    Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged
    namespace/kube-system labeled
    namespace/local-path-storage labeled
    
    1. Restricted
     kubectl label --dry-run=server --overwrite ns --all \
     pod-security.kubernetes.io/enforce=restricted
    

    다음과 비슷하게 출력될 것이다.

    namespace/default labeled
    namespace/kube-node-lease labeled
    namespace/kube-public labeled
    Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "restricted:latest"
    Warning: coredns-7bb9c7b568-hsptc (and 1 other pod): unrestricted capabilities, runAsNonRoot != true, seccompProfile
    Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true
    Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
    Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
    namespace/kube-system labeled
    Warning: existing pods in namespace "local-path-storage" violate the new PodSecurity enforce level "restricted:latest"
    Warning: local-path-provisioner-d6d9f7ffc-lw9lh: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
    namespace/local-path-storage labeled
    

위의 출력에서, privileged 파드 시큐리티 스탠다드를 적용하면 모든 네임스페이스에서 경고가 발생하지 않는 것을 볼 수 있다. 그러나 baselinerestricted 파드 시큐리티 스탠다드에 대해서는 kube-system 네임스페이스에서 경고가 발생한다.

모드, 버전, 및 파드 시큐리티 스탠다드 설정

이 섹션에서는, 다음의 파드 시큐리티 스탠다드를 latest 버전에 적용한다.

  • baseline 파드 시큐리티 스탠다드는 enforce 모드로 적용
  • restricted 파드 시큐리티 스탠다드는 warnaudit 모드로 적용

baseline 파드 시큐리티 스탠다드는 예외 목록을 간결하게 유지하고 알려진 권한 상승(privilege escalations)을 방지할 수 있는 편리한 절충안을 제공한다.

추가적으로, kube-system 내의 파드가 실패하는 것을 방지하기 위해, 해당 네임스페이스는 파드 시큐리티 스탠다드가 적용되지 않도록 제외할 것이다.

사용 중인 환경에 파드 시큐리티 어드미션을 적용할 때에는 다음의 사항을 고려한다.

  1. 클러스터에 적용된 위험 상태에 따라, restricted와 같은 더 엄격한 파드 시큐리티 스탠다드가 더 좋을 수도 있다.

  2. kube-system 네임스페이스를 적용 대상에서 제외하면 이 네임스페이스의 파드가 privileged로 실행될 수 있다. 실제 사용 환경에서는, 최소 권한 원칙을 준수하도록, 접근을 kube-system 네임스페이스로 제한하는 엄격한 RBAC 정책을 적용할 것을 강력히 권장한다.

  3. 파드 시큐리티 어드미션 컨트롤러가 이러한 파드 시큐리티 스탠다드를 구현하는 데 사용할 수 있는 구성 파일을 생성한다.

    mkdir -p /tmp/pss
    cat <<EOF > /tmp/pss/cluster-level-pss.yaml 
    apiVersion: apiserver.config.k8s.io/v1
    kind: AdmissionConfiguration
    plugins:
    - name: PodSecurity
      configuration:
        apiVersion: pod-security.admission.config.k8s.io/v1
        kind: PodSecurityConfiguration
        defaults:
          enforce: "baseline"
          enforce-version: "latest"
          audit: "restricted"
          audit-version: "latest"
          warn: "restricted"
          warn-version: "latest"
        exemptions:
          usernames: []
          runtimeClasses: []
          namespaces: [kube-system]
    EOF
    
  4. API 서버가 클러스터 생성 과정에서 이 파일을 처리할 수 있도록 구성한다.

    cat <<EOF > /tmp/pss/cluster-config.yaml 
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
    - role: control-plane
      kubeadmConfigPatches:
      - |
        kind: ClusterConfiguration
        apiServer:
            extraArgs:
              admission-control-config-file: /etc/config/cluster-level-pss.yaml
            extraVolumes:
              - name: accf
                hostPath: /etc/config
                mountPath: /etc/config
                readOnly: false
                pathType: "DirectoryOrCreate"
      extraMounts:
      - hostPath: /tmp/pss
        containerPath: /etc/config
        # optional: if set, the mount is read-only.
        # default false
        readOnly: false
        # optional: if set, the mount needs SELinux relabeling.
        # default false
        selinuxRelabel: false
        # optional: set propagation mode (None, HostToContainer or Bidirectional)
        # see https://kubernetes.io/ko/docs/concepts/storage/volumes/#마운트-전파-propagation
        # default None
        propagation: None
    EOF
    
  5. 이러한 파드 시큐리티 스탠다드를 적용하기 위해 파드 시큐리티 어드미션을 사용하는 클러스터를 생성한다.

     kind create cluster --name psa-with-cluster-pss --image kindest/node:v1.24.0 --config /tmp/pss/cluster-config.yaml
    

    다음과 비슷하게 출력될 것이다.

     Creating cluster "psa-with-cluster-pss" ...
      ✓ Ensuring node image (kindest/node:v1.24.0) 🖼 
      ✓ Preparing nodes 📦  
      ✓ Writing configuration 📜 
      ✓ Starting control-plane 🕹️ 
      ✓ Installing CNI 🔌 
      ✓ Installing StorageClass 💾 
     Set kubectl context to "kind-psa-with-cluster-pss"
     You can now use your cluster with:
    
     kubectl cluster-info --context kind-psa-with-cluster-pss
    
     Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
    
  6. kubectl context를 새로 생성한 클러스터로 설정한다.

     kubectl cluster-info --context kind-psa-with-cluster-pss
    

    다음과 비슷하게 출력될 것이다.

     Kubernetes control plane is running at https://127.0.0.1:63855
     CoreDNS is running at https://127.0.0.1:63855/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
     To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  7. 기본 네임스페이스에 생성할 최소한의 구성에 대한 파드 명세를 다음과 같이 생성한다.

    cat <<EOF > /tmp/pss/nginx-pod.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
    EOF
    
  8. 클러스터에 해당 파드를 생성한다.

     kubectl apply -f /tmp/pss/nginx-pod.yaml
    

    다음과 비슷하게 출력될 것이다.

     Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
     pod/nginx created
    

정리하기

kind delete cluster --name psa-with-cluster-psskind delete cluster --name psa-wo-cluster-pss 명령을 실행하여 생성했던 클러스터를 삭제한다.

다음 내용

5 - 상태 유지를 하지 않는 애플리케이션

5.1 - 외부 IP 주소를 노출하여 클러스터의 애플리케이션에 접속하기

이 페이지에서는 외부 IP 주소를 노출하는 쿠버네티스 서비스 오브젝트를 생성하는 방법에 대해 설명한다.

시작하기 전에

  • kubectl을 설치한다.
  • Google Kubernetes Engine 또는 Amazon Web Services와 같은 클라우드 공급자를 사용하여 쿠버네티스 클러스터를 생성한다. 이 튜토리얼은 외부 로드 밸런서를 생성하는데, 클라우드 공급자가 필요하다.
  • kubectl이 쿠버네티스 API 서버와 통신하도록 설정한다. 자세한 내용은 클라우드 공급자의 설명을 참고한다.

목적

  • Hello World 애플리케이션을 다섯 개의 인스턴스로 실행한다.
  • 외부 IP 주소를 노출하는 서비스를 생성한다.
  • 실행 중인 애플리케이션에 접근하기 위해 서비스 오브젝트를 사용한다.

다섯 개의 파드에서 실행되는 애플리케이션에 대한 서비스 만들기

  1. 클러스터에서 Hello World 애플리케이션을 실행한다.

    apiVersion: apps/v1
       kind: Deployment
       metadata:
         labels:
           app.kubernetes.io/name: load-balancer-example
         name: hello-world
       spec:
         replicas: 5
         selector:
           matchLabels:
             app.kubernetes.io/name: load-balancer-example
         template:
           metadata:
             labels:
               app.kubernetes.io/name: load-balancer-example
           spec:
             containers:
             - image: gcr.io/google-samples/node-hello:1.0
               name: hello-world
               ports:
               - containerPort: 8080
       
    kubectl apply -f https://k8s.io/examples/service/load-balancer-example.yaml
    

    위의 명령어는 디플로이먼트(Deployment) 오브젝트와 관련된 레플리카셋(ReplicaSet) 오브젝트를 생성한다. 레플리카셋은 다섯 개의 파드가 있으며, 각 파드는 Hello World 애플리케이션을 실행한다.

  2. 디플로이먼트에 대한 정보를 확인한다.

    kubectl get deployments hello-world
    kubectl describe deployments hello-world
    
  3. 레플리카셋 오브젝트에 대한 정보를 확인한다.

    kubectl get replicasets
    kubectl describe replicasets
    
  4. 디플로이먼트를 외부로 노출시키는 서비스 오브젝트를 생성한다.

    kubectl expose deployment hello-world --type=LoadBalancer --name=my-service
    
  5. 서비스에 대한 정보를 확인한다.

    kubectl get services my-service
    

    결과는 아래와 같은 형태로 나타난다.

    NAME         TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)    AGE
    my-service   LoadBalancer   10.3.245.137   104.198.205.71   8080/TCP   54s
    
  6. 서비스에 대한 자세한 정보를 확인한다.

    kubectl describe services my-service
    

    출력 결과는 다음과 유사하다.

    Name:           my-service
    Namespace:      default
    Labels:         app.kubernetes.io/name=load-balancer-example
    Annotations:    <none>
    Selector:       app.kubernetes.io/name=load-balancer-example
    Type:           LoadBalancer
    IP:             10.3.245.137
    LoadBalancer Ingress:   104.198.205.71
    Port:           <unset> 8080/TCP
    NodePort:       <unset> 32377/TCP
    Endpoints:      10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2 more...
    Session Affinity:   None
    Events:         <none>
    

    서비스에 의해 노출된 외부 IP 주소 (LoadBalancer Ingress)를 기억해두자. 예시에서 외부 IP 주소는 104.198.205.71이다. 그리고 PortNodePort의 값을 기억해두자. 예시에서 Port는 8080이고 NodePort는 32377이다.

  7. 위의 출력 결과를 통해, 서비스에 여러 엔드포인트가 있음을 알 수 있다. 10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2. 이 주소는 Hello World 애플리케이션을 실행 중인 파드의 내부 주소다. 해당 주소가 파드 주소인지 확인하려면, 아래 명령어를 입력하면 된다.

    kubectl get pods --output=wide
    

    출력 결과는 다음과 유사하다.

    NAME                         ...  IP         NODE
    hello-world-2895499144-1jaz9 ...  10.0.1.6   gke-cluster-1-default-pool-e0b8d269-1afc
    hello-world-2895499144-2e5uh ...  10.0.1.8   gke-cluster-1-default-pool-e0b8d269-1afc
    hello-world-2895499144-9m4h1 ...  10.0.0.6   gke-cluster-1-default-pool-e0b8d269-5v7a
    hello-world-2895499144-o4z13 ...  10.0.1.7   gke-cluster-1-default-pool-e0b8d269-1afc
    hello-world-2895499144-segjf ...  10.0.2.5   gke-cluster-1-default-pool-e0b8d269-cpuc
    
  8. Hello World 애플리케이션에 접근하기 위해 외부 IP 주소 (LoadBalancer Ingress)를 사용한다.

    curl http://<external-ip>:<port>
    

    <external-ip>는 서비스의 외부 IP 주소 (LoadBalancer Ingress)를 의미하며, <port>는 서비스 정보에서 Port 값을 의미한다. 만약 minikube를 사용하고 있다면, minikube service my-service 명령어를 통해, 자동으로 브라우저 내에서 Hello World 애플리케이션에 접근할 수 있다.

    성공적인 요청에 대한 응답으로 hello 메세지가 나타난다.

    Hello Kubernetes!
    

정리하기

서비스를 삭제하려면, 아래의 명령어를 입력한다.

kubectl delete services my-service

Hello World 애플리케이션을 실행 중인 디플로이먼트, 레플리카셋, 파드를 삭제하려면, 아래의 명령어를 입력한다.

kubectl delete deployment hello-world

다음 내용

애플리케이션과 서비스 연결하기에 대해 더 배워 본다.

5.2 - 예시: Redis를 사용한 PHP 방명록 애플리케이션 배포하기

이 튜토리얼에서는 쿠버네티스와 Docker를 사용하여 간단한 (운영 수준이 아닌) 멀티 티어 웹 애플리케이션을 빌드하고 배포하는 방법을 보여준다. 이 예제는 다음과 같은 구성으로 이루어져 있다.

  • 방명록 항목을 저장하기 위한 단일 인스턴스 Redis
  • 여러 개의 웹 프론트엔드 인스턴스

목적

  • Redis 리더를 실행
  • 2개의 Redis 팔로워를 실행
  • 방명록 프론트엔드를 실행
  • 프론트엔드 서비스를 노출하고 확인
  • 정리하기

시작하기 전에

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

쿠버네티스 서버의 버전은 다음과 같거나 더 높아야 함. 버전: v1.14. 버전 확인을 위해서, 다음 커맨드를 실행 kubectl version.

Redis 데이터베이스를 실행

방명록 애플리케이션은 Redis를 사용하여 데이터를 저장한다.

Redis 디플로이먼트를 생성하기

아래의 매니페스트 파일은 단일 복제본 Redis 파드를 실행하는 디플로이먼트 컨트롤러에 대한 명세를 담고 있다.

# SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-leader
  labels:
    app: redis
    role: leader
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
        role: leader
        tier: backend
    spec:
      containers:
      - name: leader
        image: "docker.io/redis:6.0.5"
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 6379
  1. 매니페스트 파일을 다운로드한 디렉터리에서 터미널 창을 시작한다.

  2. redis-leader-deployment.yaml 파일을 이용하여 Redis 디플로이먼트를 생성한다.

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-leader-deployment.yaml
    
  3. 파드의 목록을 질의하여 Redis 파드가 실행 중인지 확인한다.

    kubectl get pods
    

    결과는 아래와 같은 형태로 나타난다.

    NAME                            READY     STATUS    RESTARTS   AGE
    redis-leader-fb76b4755-xjr2n   1/1     Running   0          13s
    
  4. Redis 리더 파드의 로그를 보려면 다음 명령어를 실행한다.

    kubectl logs -f deployment/redis-leader
    

Redis 리더 서비스 생성하기

방명록 애플리케이션에서 데이터를 쓰려면 Redis와 통신해야 한다. Redis 파드로 트래픽을 프록시하려면 서비스를 생성해야 한다. 서비스는 파드에 접근하기 위한 정책을 정의한다.

# SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook
apiVersion: v1
kind: Service
metadata:
  name: redis-leader
  labels:
    app: redis
    role: leader
    tier: backend
spec:
  ports:
  - port: 6379
    targetPort: 6379
  selector:
    app: redis
    role: leader
    tier: backend
  1. redis-leader-service.yaml 파일을 이용하여 Redis 서비스를 실행한다.

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-leader-service.yaml
    
  2. 서비스의 목록을 질의하여 Redis 서비스가 실행 중인지 확인한다.

    kubectl get service
    

    결과는 아래와 같은 형태로 나타난다.

    NAME           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
    kubernetes     ClusterIP   10.0.0.1     <none>        443/TCP    1m
    redis-leader   ClusterIP   10.103.78.24 <none>        6379/TCP   16s
    

Redis 팔로워 구성하기

Redis 리더는 단일 파드이지만, 몇 개의 Redis 팔로워 또는 복제본을 추가하여 가용성을 높이고 트래픽 요구를 충족할 수 있다.

# SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-follower
  labels:
    app: redis
    role: follower
    tier: backend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
        role: follower
        tier: backend
    spec:
      containers:
      - name: follower
        image: gcr.io/google_samples/gb-redis-follower:v2
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 6379
  1. redis-follower-deployment.yaml 파일을 이용하여 Redis 서비스를 실행한다.

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-follower-deployment.yaml
    
  2. 파드의 목록을 질의하여 2개의 Redis 팔로워 레플리카가 실행 중인지 확인한다.

    kubectl get pods
    

    결과는 아래와 같은 형태로 나타난다.

    NAME                             READY   STATUS    RESTARTS   AGE
    redis-follower-dddfbdcc9-82sfr   1/1     Running   0          37s
    redis-follower-dddfbdcc9-qrt5k   1/1     Running   0          38s
    redis-leader-fb76b4755-xjr2n     1/1     Running   0          11m
    

Redis 팔로워 서비스 생성하기

방명록 애플리케이션이 데이터를 읽으려면 Redis 팔로워와 통신해야 한다. Redis 팔로워를 발견 가능(discoverable)하게 만드려면, 새로운 서비스를 구성해야 한다.

# SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook
apiVersion: v1
kind: Service
metadata:
  name: redis-follower
  labels:
    app: redis
    role: follower
    tier: backend
spec:
  ports:
    # the port that this service should serve on
  - port: 6379
  selector:
    app: redis
    role: follower
    tier: backend
  1. redis-follower-service.yaml 파일을 이용하여 Redis 서비스를 실행한다.

    kubectl apply -f https://k8s.io/examples/application/guestbook/redis-follower-service.yaml
    
  2. 서비스의 목록을 질의하여 Redis 서비스가 실행 중인지 확인한다.

    kubectl get service
    

    결과는 아래와 같은 형태로 나타난다.

    NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
    kubernetes       ClusterIP   10.96.0.1       <none>        443/TCP    3d19h
    redis-follower   ClusterIP   10.110.162.42   <none>        6379/TCP   9s
    redis-leader     ClusterIP   10.103.78.24    <none>        6379/TCP   6m10s
    

방명록 프론트엔드를 설정하고 노출하기

방명록을 위한 Redis 저장소를 구성하고 실행했으므로, 이제 방명록 웹 서버를 실행한다. Redis 팔로워와 마찬가지로, 프론트엔드는 쿠버네티스 디플로이먼트(Deployment)를 사용하여 배포된다.

방명록 앱은 PHP 프론트엔드를 사용한다. DB에 대한 요청이 읽기인지 쓰기인지에 따라, Redis 팔로워 또는 리더 서비스와 통신하도록 구성된다. 프론트엔드는 JSON 인터페이스를 노출하고, jQuery-Ajax 기반 UX를 제공한다.

방명록 프론트엔드의 디플로이먼트 생성하기

# SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
        app: guestbook
        tier: frontend
  template:
    metadata:
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: gcr.io/google_samples/gb-frontend:v5
        env:
        - name: GET_HOSTS_FROM
          value: "dns"
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 80
  1. frontend-deployment.yaml 파일을 이용하여 프론트엔드 디플로이먼트를 생성한다.

    kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-deployment.yaml
    
  2. 파드의 목록을 질의하여 세 개의 프론트엔드 복제본이 실행되고 있는지 확인한다.

    kubectl get pods -l app=guestbook -l tier=frontend
    

    결과는 아래와 같은 형태로 나타난다.

    NAME                        READY   STATUS    RESTARTS   AGE
    frontend-85595f5bf9-5tqhb   1/1     Running   0          47s
    frontend-85595f5bf9-qbzwm   1/1     Running   0          47s
    frontend-85595f5bf9-zchwc   1/1     Running   0          47s
    

프론트엔드 서비스 생성하기

서비스의 기본 유형은 ClusterIP 이기 때문에 생성한 Redis 서비스는 컨테이너 클러스터 내에서만 접근할 수 있다. ClusterIP는 서비스가 가리키는 파드 집합에 대한 단일 IP 주소를 제공한다. 이 IP 주소는 클러스터 내에서만 접근할 수 있다.

게스트가 방명록에 접근할 수 있도록 하려면, 외부에서 볼 수 있도록 프론트엔드 서비스를 구성해야 한다. 그렇게 하면 클라이언트가 쿠버네티스 클러스터 외부에서 서비스를 요청할 수 있다. 그러나 쿠버네티스 사용자는 ClusterIP를 사용하더라도 kubectl port-forward를 사용해서 서비스에 접근할 수 있다.

# SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook
apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: guestbook
    tier: frontend
spec:
  # if your cluster supports it, uncomment the following to automatically create
  # an external load-balanced IP for the frontend service.
  # type: LoadBalancer
  #type: LoadBalancer
  ports:
    # the port that this service should serve on
  - port: 80
  selector:
    app: guestbook
    tier: frontend
  1. frontend-service.yaml 파일을 이용하여 프론트엔드 서비스를 실행한다.

    kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-service.yaml
    
  2. 서비스의 목록을 질의하여 프론트엔드 서비스가 실행 중인지 확인한다.

    kubectl get services
    

    결과는 아래와 같은 형태로 나타난다.

    NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
    frontend         ClusterIP   10.97.28.230    <none>        80/TCP     19s
    kubernetes       ClusterIP   10.96.0.1       <none>        443/TCP    3d19h
    redis-follower   ClusterIP   10.110.162.42   <none>        6379/TCP   5m48s
    redis-leader     ClusterIP   10.103.78.24    <none>        6379/TCP   11m
    

kubectl port-forward를 통해 프론트엔드 서비스 확인하기

  1. 다음 명령어를 실행해서 로컬 머신의 8080 포트를 서비스의 80 포트로 전달한다.

    kubectl port-forward svc/frontend 8080:80
    

    결과는 아래와 같은 형태로 나타난다.

    Forwarding from 127.0.0.1:8080 -> 80
    Forwarding from [::1]:8080 -> 80
    
  2. 방명록을 보기 위해 브라우저에서 http://localhost:8080 페이지를 로드한다.

LoadBalancer를 통해 프론트엔드 서비스 확인하기

frontend-service.yaml 매니페스트를 LoadBalancer와 함께 배포한 경우, 방명록을 보기 위해 IP 주소를 찾아야 한다.

  1. 프론트엔드 서비스의 IP 주소를 얻기 위해 아래 명령어를 실행한다.

    kubectl get service frontend
    

    결과는 아래와 같은 형태로 나타난다.

    NAME       TYPE           CLUSTER-IP      EXTERNAL-IP        PORT(S)        AGE
    frontend   LoadBalancer   10.51.242.136   109.197.92.229     80:32372/TCP   1m
    
  2. IP 주소를 복사하고, 방명록을 보기 위해 브라우저에서 페이지를 로드한다.

웹 프론트엔드 확장하기

서버가 디플로이먼트 컨트롤러를 사용하는 서비스로 정의되어 있으므로 필요에 따라 확장 또는 축소할 수 있다.

  1. 프론트엔드 파드의 수를 확장하기 위해 아래 명령어를 실행한다.

    kubectl scale deployment frontend --replicas=5
    
  2. 파드의 목록을 질의하여 실행 중인 프론트엔드 파드의 수를 확인한다.

    kubectl get pods
    

    결과는 아래와 같은 형태로 나타난다.

    NAME                             READY   STATUS    RESTARTS   AGE
    frontend-85595f5bf9-5df5m        1/1     Running   0          83s
    frontend-85595f5bf9-7zmg5        1/1     Running   0          83s
    frontend-85595f5bf9-cpskg        1/1     Running   0          15m
    frontend-85595f5bf9-l2l54        1/1     Running   0          14m
    frontend-85595f5bf9-l9c8z        1/1     Running   0          14m
    redis-follower-dddfbdcc9-82sfr   1/1     Running   0          97m
    redis-follower-dddfbdcc9-qrt5k   1/1     Running   0          97m
    redis-leader-fb76b4755-xjr2n     1/1     Running   0          108m
    
  3. 프론트엔드 파드의 수를 축소하기 위해 아래 명령어를 실행한다.

    kubectl scale deployment frontend --replicas=2
    
  4. 파드의 목록을 질의하여 실행 중인 프론트엔드 파드의 수를 확인한다.

    kubectl get pods
    

    결과는 아래와 같은 형태로 나타난다.

    NAME                            READY     STATUS    RESTARTS   AGE
    frontend-85595f5bf9-cpskg        1/1     Running   0          16m
    frontend-85595f5bf9-l9c8z        1/1     Running   0          15m
    redis-follower-dddfbdcc9-82sfr   1/1     Running   0          98m
    redis-follower-dddfbdcc9-qrt5k   1/1     Running   0          98m
    redis-leader-fb76b4755-xjr2n     1/1     Running   0          109m
    

정리하기

디플로이먼트 및 서비스를 삭제하면 실행 중인 모든 파드도 삭제된다. 레이블을 사용하여 하나의 명령어로 여러 자원을 삭제해보자.

  1. 모든 파드, 디플로이먼트, 서비스를 삭제하기 위해 아래 명령어를 실행한다.

    kubectl delete deployment -l app=redis
    kubectl delete service -l app=redis
    kubectl delete deployment frontend
    kubectl delete service frontend
    

    결과는 아래와 같은 형태로 나타난다.

    deployment.apps "redis-follower" deleted
    deployment.apps "redis-leader" deleted
    deployment.apps "frontend" deleted
    service "frontend" deleted
    
  2. 파드의 목록을 질의하여 실행 중인 파드가 없는지 확인한다.

    kubectl get pods
    

    결과는 아래와 같은 형태로 나타난다.

    No resources found in default namespace.
    

다음 내용

6 - 상태 유지가 필요한(stateful) 애플리케이션

6.1 - 스테이트풀셋 기본

이 튜토리얼은 스테이트풀셋(StatefulSet)을 이용하여 애플리케이션을 관리하는 방법을 소개한다. 어떻게 스테이트풀셋의 파드를 생성하고, 삭제하며, 스케일링하고, 업데이트하는지 시연한다.

시작하기 전에

튜토리얼을 시작하기 전에 다음의 쿠버네티스 컨셉에 대해 익숙해야 한다.

목적

스테이트풀셋은 상태 유지가 필요한(stateful) 애플리케이션과 분산시스템에서 이용하도록 의도했다. 그러나 쿠버네티스 상에 스테이트풀 애플리케이션과 분산시스템을 관리하는 것은 광범위하고 복잡한 주제이다. 스테이트풀셋의 기본 기능을 보여주기 위해 이 둘을 결합하지 않고, 스테이트풀셋을 사용한 단순 웹 애플리케이션을 배포할 것이다.

이 튜토리얼을 마치면 다음 항목에 대해 익숙해질 것이다.

  • 스테이트풀셋을 어떻게 생성하는지
  • 스테이트풀셋이 어떻게 파드를 관리하는지
  • 스테이트풀셋을 어떻게 삭제하는지
  • 스테이트풀셋은 어떻게 스케일링하는지
  • 스테이트풀셋의 파드는 어떻게 업데이트하는지

스테이트풀셋 생성하기

아래 예제를 이용해서 스테이트풀셋을 생성하자. 이는 스테이트풀셋 개념에서 보인 예제와 유사하다. 이것은 web과 이 스테이트풀셋 파드의 IP 주소를 게시하는 헤드리스 서비스nginx 를 생성한다.

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

위에 예제를 다운로드 받아서 파일이름을 web.yaml으로 저장하자.

2개의 터미널창을 사용한다. 첫째 터미널에서 kubectl get을 이용해서 스테이트풀셋의 파드가 생성되는지 감시하자.

kubectl get pods -w -l app=nginx

두 번째 터미널에서 kubectl applyweb.yaml에 정의된 헤드리스 서비스와 스테이트풀셋을 생성한다.

kubectl apply -f web.yaml
service/nginx created
statefulset.apps/web created

상기 명령어는 NGINX 웹 서버를 실행하는 2개의 파드를 생성한다. nginx 서비스의 정보를 가져온다.

kubectl get service nginx
NAME      TYPE         CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx     ClusterIP    None         <none>        80/TCP    12s

그리고 web 스테이트풀셋 정보를 가져와서 모두 성공적으로 생성되었는지 확인한다.

kubectl get statefulset web
NAME      DESIRED   CURRENT   AGE
web       2         1         20s

차례대로 파드 생성하기

N개의 레플리카를 가진 스테이트풀셋은 배포 시에 순차적으로 {0..N-1} 순으로 생성된다. 첫째 터미널에서 kubectl get 명령의 출력 내용을 살펴보자. 결국 그 내용은 아래 예와 비슷할 것이다.

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         19s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         18s

참고로 web-1 파드는 web-0 파드가 Running (파드의 단계 참고) 및 Ready (파드의 컨디션에서 type 참고) 상태가 되기 전에는 시작되지 않음에 주의한다.

스테이트풀셋 안에 파드

스테이트풀셋 안에 파드는 고유한 순번과 동일한 네트워크 신원을 가진다.

파드 순번 살펴보기

스테이트풀셋의 파드를 가져오자.

kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          1m
web-1     1/1       Running   0          1m

스테이트풀셋 개념에서 언급했듯 스테이트풀셋의 파드는 끈끈하고 고유한 정체성을 가진다. 이 정체성은 스테이트풀셋 컨트롤러에서 각 파드에 주어지는 고유한 순번에 기인한다. 파드의 이름의 형식은 <스테이트풀셋 이름>-<순번> 이다. 앞서 web 스테이트풀셋은 2개의 레플리카를 가졌으므로 web-0web-1 2개 파드를 생성한다.

안정적인 네트워크 신원 사용하기

각 파드는 각 순번에 따른 안정적인 호스트네임을 갖는다. 각 파드에서 hostname 명령어를 실행하도록 kubectl exec를 이용하자.

for i in 0 1; do kubectl exec "web-$i" -- sh -c 'hostname'; done
web-0
web-1

dnsutils 패키지에서 nslookup 명령을 제공하는 컨테이너를 실행하도록 kubectl run을 이용하자. 파드의 호스트네임에 nslookup을 이용하면 클러스터 내부 DNS 주소를 확인할 수 있다.

kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm

위 명령으로 새로운 셸을 시작한다. 새 셸에서 다음을 실행한다.

# dns-test 컨테이너 셸에서 다음을 실행한다.
nslookup web-0.nginx

출력 결과는 다음과 비슷하다.

Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.6

nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.6

(이제 exit 명령으로 컨테이너 셸에서 종료한다.)

헤드리스 서비스의 CNAME은 SRV 레코드를 지칭한다 (Running과 Ready 상태의 각 파드마다 1개). SRV 레코드는 파드의 IP 주소를 포함한 A 레코드 엔트리를 지칭한다.

첫째 터미널에서 스테이트풀셋의 파드를 가져오자.

kubectl get pod -w -l app=nginx

두 번째 터미널에서 스테이트풀셋 내에 파드를 모두 삭제하기 위해 kubectl delete를 이용하자.

kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

스테이트풀셋이 재시작되고 두 파드가 Running과 Ready 상태로 전환되도록 기다리자.

kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         34s

파드의 호스트네임과 클러스터 내부 DNS 엔트리를 보기 위해 kubectl execkubectl run을 이용하자. 먼저, 파드의 호스트네임을 확인한다.

for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1

그리고 다음을 실행한다.

kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm /bin/sh

이 명령으로 새로운 셸이 시작된다. 새 셸에서 다음을 실행한다.

# dns-test 컨테이너 셸에서 이것을 실행한다.
nslookup web-0.nginx

출력 결과는 다음과 비슷하다.

Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.7

nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.8

(이제 exit 명령으로 컨테이너 셸을 종료한다.)

파드의 순번, 호스트네임, SRV 레코드와 A 레코드이름은 변경되지 않지만 파드의 IP 주소는 변경될 수 있다. 이는 튜토리얼에서 사용하는 클러스터나 다른 클러스터에도 동일하다. 따라서 다른 애플리케이션이 IP 주소로 스테이트풀셋의 파드에 접속하지 않도록 하는 것이 중요하다.

스테이트풀셋의 활성 멤버를 찾아 연결할 경우 헤드리스 서비스(nginx.default.svc.cluster.local)의 CNAME을 쿼리해야 한다. CNAME과 연관된 SRV 레코드는 스테이트풀셋의 Running과 Ready 상태의 모든 파드들을 담고 있다.

애플리케이션에서 이미 활성상태(liveness)와 준비성(readiness) 테스트하는 연결 로직을 구현되어 있다면 파드web-0.nginx.default.svc.cluster.local, web-1.nginx.default.svc.cluster.local)의 SRV레코드를 안정적으로 사용할 수 있어 애플리케이션은 파드가 Running과 Ready 상태로 전환할 때 파드의 주소를 검색할 수 있다.

안정적인 스토리지에 쓰기

web-0web-1에 대해 퍼시스턴트볼륨클레임(PersistentVolumeClaim)을 가져오자.

kubectl get pvc -l app=nginx

출력 결과는 다음과 비슷하다.

NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-15c268c7-b507-11e6-932f-42010a800002   1Gi        RWO           48s
www-web-1   Bound     pvc-15c79307-b507-11e6-932f-42010a800002   1Gi        RWO           48s

스테이트풀셋 컨트롤러는 2개의 퍼시스턴트볼륨에 묶인 2개의 퍼시스턴트볼륨클레임을 생성했다.

본 튜토리얼에서 사용되는 클러스터는 퍼시스턴트볼륨을 동적으로 프로비저닝하도록 설정되었으므로 생성된 퍼시스턴트볼륨도 자동으로 묶인다.

NGINX 웹서버는 기본 색인 파일로 /usr/share/nginx/html/index.html을 이용합니다. 스테이트풀셋 spec내의 volumeMounts 필드는 /usr/share/nginx/html 디렉터리가 퍼시스턴트볼륨으로 제공되는지 보증합니다.

파드의 호스트네임을 index.html 파일에 작성하고 NGINX 웹서버가 해당 호스트네임을 제공하는지 확인해보자.

for i in 0 1; do kubectl exec "web-$i" -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html'; done

for i in 0 1; do kubectl exec -it "web-$i" -- curl localhost; done
web-0
web-1

첫째 터미널에서 스테이트풀셋의 파드를 감시하자.

kubectl get pod -w -l app=nginx

두 번째 터미널에서 스테이트풀셋의 모든 파드를 삭제하자.

kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

첫 번째 터미널에서 실행 중인 kubectl get명령어의 출력을 확인하고, 모든 파드가 Running과 Ready 상태로 전환될 때까지 기다리자.

kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         34s

웹서버에서 자신의 호스트네임을 계속 제공하는지 확인하자.

for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

비록 web-0web-1이 재스케줄링되어도 계속해서 자신의 호스트네임을 제공하는데 이는 각 퍼시스턴트볼륨클레임에 연관된 퍼시스턴트볼륨이 해당 volumeMounts로 재마운트되기 때문이다. web-0web-1의 스케줄링에 관계없이 각각의 퍼시스턴트볼륨은 적절하게 마운트된다.

스테이트풀셋 스케일링

스테이트풀셋을 스케일링하는 것은 레플리카 개수를 늘리거나 줄이는 것을 의미한다. 이것은 replicas 필드를 갱신하여 이뤄진다. kubectl scale이나 kubectl patch을 이용해서 스테이트풀셋을 스케일링할 수 있다.

스케일 업

터미널창에서 스테이트풀셋의 파드를 감시하자.

kubectl get pods -w -l app=nginx

다른 터미널창에서 kubectl scale을 이용하여 레플리카 개수를 5로 스케일링하자.

kubectl scale sts web --replicas=5
statefulset.apps/web scaled

첫 번째 터미널에서 실행 중인 kubectl get명령어의 출력을 확인하고, 3개의 추가 파드가 Running과 Ready 상태로 전환될 때까지 기다리자.

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2h
web-1     1/1       Running   0          2h
NAME      READY     STATUS    RESTARTS   AGE
web-2     0/1       Pending   0          0s
web-2     0/1       Pending   0         0s
web-2     0/1       ContainerCreating   0         0s
web-2     1/1       Running   0         19s
web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         0s
web-3     0/1       ContainerCreating   0         0s
web-3     1/1       Running   0         18s
web-4     0/1       Pending   0         0s
web-4     0/1       Pending   0         0s
web-4     0/1       ContainerCreating   0         0s
web-4     1/1       Running   0         19s

스테이트풀셋 컨트롤러는 레플리카 개수를 스케일링한다. 스테이트풀셋 생성으로 스테이트풀셋 컨트롤러는 각 파드을 순차적으로 각 순번에 따라 생성하고 후속 파드 시작 전에 이전 파드가 Running과 Ready 상태가 될 때까지 기다린다.

스케일 다운

터미널에서 스테이트풀셋의 파드를 감시하자.

kubectl get pods -w -l app=nginx

다른 터미널에서 kubectl patch으로 스테이트풀셋을 다시 3개의 레플리카로 스케일링하자.

kubectl patch sts web -p '{"spec":{"replicas":3}}'
statefulset.apps/web patched

web-4web-3이 Terminating으로 전환되기까지 기다리자.

kubectl get pods -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          3h
web-1     1/1       Running             0          3h
web-2     1/1       Running             0          55s
web-3     1/1       Running             0          36s
web-4     0/1       ContainerCreating   0          18s
NAME      READY     STATUS    RESTARTS   AGE
web-4     1/1       Running   0          19s
web-4     1/1       Terminating   0         24s
web-4     1/1       Terminating   0         24s
web-3     1/1       Terminating   0         42s
web-3     1/1       Terminating   0         42s

순차 파드 종료

컨트롤러는 순번의 역순으로 한 번에 1개 파드를 삭제하고 다음 파드를 삭제하기 전에 각각이 완전하게 종료되기까지 기다린다.

스테이트풀셋의 퍼시스턴트볼륨클레임을 가져오자.

kubectl get pvc -l app=nginx
NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-15c268c7-b507-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-1   Bound     pvc-15c79307-b507-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-2   Bound     pvc-e1125b27-b508-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-3   Bound     pvc-e1176df6-b508-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-4   Bound     pvc-e11bb5f8-b508-11e6-932f-42010a800002   1Gi        RWO           13h

여전히 5개의 퍼시스턴트볼륨클레임과 5개의 퍼시스턴트볼륨이 있다. 파드의 안전한 스토리지를 탐색하면서 스테이트풀셋의 파드가 삭제될 때에 파드에 마운트된 스테이트풀셋의 퍼시스턴트볼륨이 삭제되지 않은 것을 보았다. 스테이트풀셋 스케일 다운으로 파드 삭제할 때에도 여전히 사실이다.

스테이트풀셋 업데이트하기

쿠버네티스 1.7 이상에서 스테이트풀셋 컨트롤러는 자동 업데이트를 지원한다. 전략은 스테이트풀셋 API 오브젝트의 spec.updateStrategy 필드로 결정된다. 이 기능은 컨테이너 이미지, 스테이트풀셋의 리소스 요청이나 혹은 한계와 레이블과 파드의 어노테이션을 업그레이드하기 위해 사용될 수 있다. RollingUpdateOnDelete의 2개의 유효한 업데이트 전략이 있다.

RollingUpdate 업데이트 전략은 스테이트풀셋에서 기본 값이다.

롤링 업데이트

RollingUpdate 업데이트 전략은 스테이트풀셋의 보장사항을 유지하면서 스테이트풀셋 내의 모든 파드를 역순으로 업데이트한다.

스테이트풀셋 web의 업데이트 전략을 RollingUpdate으로 패치하자.

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate"}}}'
statefulset.apps/web patched

터미널 창에서 스테이트풀셋 web의 컨테이너 이미지를 바꾸도록 또 패치하자.

kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.8"}]'
statefulset.apps/web patched

다른 터미널창에서 스테이트풀셋의 파드를 감시하자.

kubectl get pod -l app=nginx -w

출력 결과는 다음과 비슷하다.

NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          7m
web-1     1/1       Running   0          7m
web-2     1/1       Running   0          8m
web-2     1/1       Terminating   0         8m
web-2     1/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Pending   0         0s
web-2     0/1       Pending   0         0s
web-2     0/1       ContainerCreating   0         0s
web-2     1/1       Running   0         19s
web-1     1/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         6s
web-0     1/1       Terminating   0         7m
web-0     1/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Pending   0         0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         10s

스테이트풀셋 내에 파드는 순번의 역순으로 업데이트된다. 이 스테이트풀셋 컨트롤러는 각 파드를 종료시키고 다음 파드를 업데이트하기 전에 그것이 Running과 Ready 상태로 전환될 때까지 기다린다. 알아둘 것은 비록 스테이트풀셋 컨트롤러에서 이전 파드가 Running과 Ready 상태가 되기까지 다음 파드를 업데이트하지 않아도 현재 버전으로 파드를 업데이트하다 실패하면 복원한다는 것이다.

업데이트를 이미 받은 파드는 업데이트된 버전으로 복원되고 아직 업데이트를 받지 못한 파드는 이전 버전으로 복원한다. 이런 식으로 컨트롤러는 간헐적인 오류가 발생해도 애플리케이션을 계속 건강하게 유지하고 업데이트도 일관되게 유지하려 한다.

컨테이너 이미지를 살펴보기 위해 파드를 가져오자.

for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
registry.k8s.io/nginx-slim:0.8
registry.k8s.io/nginx-slim:0.8
registry.k8s.io/nginx-slim:0.8

스테이트풀셋의 모든 파드가 지금은 이전 컨테이너 이미지를 실행 중이이다.

단계적으로 업데이트 하기

RollingUpdate 업데이트 전략의 파라미터인 partition를 이용하여 스테이트풀셋의 단계적으로 업데이트할 수 있다. 단계적 업데이트는 스테이트풀셋의 모든 파드를 현재 버전으로 유지하면서 스테이트풀셋의 .spec.template에 변경을 허용한다.

스테이트풀셋 webupdateStrategy 필드에 partition을 추가하자.

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}'
statefulset.apps/web patched

컨테이너의 이미지를 바꾸도록 스테이트풀셋을 다시 패치한다.

kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"registry.k8s.io/nginx-slim:0.7"}]'
statefulset.apps/web patched

스테이트풀셋의 파드를 삭제하자.

kubectl delete pod web-2
pod "web-2" deleted

파드가 Running과 Ready 상태가 되기까지 기다리자.

kubectl get pod -l app=nginx -w
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          4m
web-1     1/1       Running             0          4m
web-2     0/1       ContainerCreating   0          11s
web-2     1/1       Running   0         18s

파드의 컨테이너 이미지를 가져오자.

kubectl get pod web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.8

비록 업데이트 전략이 RollingUpdate이지만 스테이트풀셋은 파드를 그것의 원래 컨테이너로 복원한다. 파드의 순번이 updateStrategy에서 지정된 파티션보다 작기 때문이다.

카나리(Canary) 롤링 아웃

위에서 지정한 partition값을 차감시키면 변경사항을 테스트하기 위해 카나리 롤아웃을 할 수 있다.

스테이트풀셋에 partition을 차감하도록 패치하자.

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'
statefulset.apps/web patched

web-2 파드가 Running과 Ready 상태가 되기까지 기다리자.

kubectl get pod -l app=nginx -w
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          4m
web-1     1/1       Running             0          4m
web-2     0/1       ContainerCreating   0          11s
web-2     1/1       Running   0         18s

파드의 컨테이너를 가져오자.

kubectl get po web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.7

partition을 바꾸면 스테이트풀셋 컨트롤러는 자동으로 web-2 파드를 업데이트하는데 이는 해당 파드의 순번이 partition 이상이기 때문이다.

web-1 파드를 삭제하자.

kubectl delete pod web-1
pod "web-1" deleted

web-1 파드가 Running과 Ready 상태가 되기까지 기다리자.

kubectl get pod -l app=nginx -w

출력 결과는 다음과 비슷하다.

NAME      READY     STATUS        RESTARTS   AGE
web-0     1/1       Running       0          6m
web-1     0/1       Terminating   0          6m
web-2     1/1       Running       0          2m
web-1     0/1       Terminating   0         6m
web-1     0/1       Terminating   0         6m
web-1     0/1       Terminating   0         6m
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         18s

web-1 파드의 컨테이너 이미지를 가져오자.

kubectl get pod web-1 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.8

web-1 는 원래 환경설정으로 복원되었는데 이는 파드의 순번이 partition보다 작기 때문이다. 스테이트풀셋의 .spec.template이 갱신되면, 지정된 partition 이상의 순번을 가진 모든 파드는 업데이트된다. 미만의 순번을 가진 파드라면 삭제되거나 종료되어 원래 환경설정으로 복원된다.

단계적 롤아웃

카나리 롤아웃에서 했던 방법과 비슷하게 분할된 롤링 업데이트를 이용하여 단계적 롤아웃(e.g. 선형, 기하 또는 지수적 롤아웃)을 수행할 수 있다. 단계적 롤아웃을 수행하려면 컨트롤러가 업데이트를 일시 중지할 순번으로 partition를 정하자.

partition은 현재 2이다. partition을 0으로 바꾸자.

kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}'
statefulset.apps/web patched

스테이트풀셋의 모든 파드가 Running과 Ready 상태가 되기까지 기다리자.

kubectl get pod -l app=nginx -w

출력 결과는 다음과 비슷하다.

NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          3m
web-1     0/1       ContainerCreating   0          11s
web-2     1/1       Running             0          2m
web-1     1/1       Running   0         18s
web-0     1/1       Terminating   0         3m
web-0     1/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Pending   0         0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         3s

스테이트풀셋에 있는 파드의 컨테이너 이미지 상세 정보를 가져오자.

for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
registry.k8s.io/nginx-slim:0.7
registry.k8s.io/nginx-slim:0.7
registry.k8s.io/nginx-slim:0.7

partition0으로 이동하여 스테이트풀셋에서 계속해서 업데이트 처리를 하도록 허용하였다.

삭제 시 동작

OnDelete 업데이트 전략은 예전 동작(1.6 이하)으로, 이 업데이트 전략을 선택하면, 스테이트풀셋 컨트롤러는 스테이트풀셋의 .spec.template 필드에 수정 사항이 발생해도 자동으로 파드를 업데이트하지 않는다. 이 전략은 .spec.template.updateStrategy.typeOnDelete로 설정하여 선택할 수 있다.

스테이트풀셋 삭제하기

스테이트풀셋은 비종속적(non-cascading), 종속적(cascading) 삭제를 둘 다 지원한다. 비종속적 삭제에서는 스테이트풀셋이 지워질 때에 스테이트풀셋의 파드는 지워지지 않는다. 종속적 삭제에서는 스테이트풀셋과 그에 속한 파드가 모두 지워진다.

비종속적 삭제

터미널창에서 스테이트풀셋의 파드를 감시하자.

kubectl get pods -w -l app=nginx

다른 터미널에서는 스테이트풀셋을 지우기 위해 kubectl delete 명령어를 이용하자. 이 명령어에 --cascade=orphan 파라미터가 추가되었다. 이 파라미터는 쿠버네티스에 스테이트풀셋만 삭제하고 그에 속한 파드는 지우지 않도록 요청한다.

kubectl delete statefulset web --cascade=orphan
statefulset.apps "web" deleted

상태를 확인하기 위해 파드를 가져오자.

kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          6m
web-1     1/1       Running   0          7m
web-2     1/1       Running   0          5m

비록 web이 삭제되고 있어도, 모든 파드는 여전히 Running과 Ready 상태이다. web-0을 삭제하자.

kubectl delete pod web-0
pod "web-0" deleted

스테이트풀셋의 파드를 가져오자.

kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-1     1/1       Running   0          10m
web-2     1/1       Running   0          7m

스테이트풀셋 web이 삭제되는 동안 web-0은 재시작하지 않았다.

첫째 터미널에서 스테이트풀셋의 파드를 감시하자.

kubectl get pods -w -l app=nginx

두 번째 터미널에서 스테이트풀셋을 다시 생성하자. nginx 서비스(가지지 말았어야 하는)를 삭제하기 전까지는 그 서비스가 이미 존재한다는 에러를 볼 것이라는 것을 명심하자.

kubectl apply -f web.yaml
statefulset.apps/web created
service/nginx unchanged

이 에러는 무시하자. 이것은 다만 해당 서비스가 있더라도 nginx 헤드리스 서비스를 생성하려고 했음을 뜻한다.

첫째 터미널에서 실행 중인 kubectl get 명령어의 출력을 살펴보자.

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-1     1/1       Running   0          16m
web-2     1/1       Running   0          2m
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         18s
web-2     1/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m

web 스테이트풀셋이 다시 생성될 때 먼저 web-0 시작한다. web-1은 이미 Running과 Ready 상태이므로 web-0이 Running과 Ready 상태로 전환될 때는 이 파드에 적용됐다. 스테이트풀셋에 replicas를 2로 하고 web-0을 재생성했다면 web-1이 이미 Running과 Ready 상태이고, web-2은 종료되었을 것이다.

파드의 웹서버에서 제공한 index.html 파일 내용을 다른 관점으로 살펴보자.

for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

스테이트풀셋과 web-0 파드를 둘 다 삭제했으나 여전히 index.html 파일에 입력했던 원래 호스트네임을 제공한다. 스테이트풀셋은 파드에 할당된 퍼시스턴트볼륨을 결코 삭제하지 않기때문이다. 다시 스테이트풀셋을 생성하면 web-0을 시작하며 원래 퍼시스턴트볼륨을 다시 마운트한다.

단계식 삭제

터미널창에서 스테이트풀셋의 파드를 감시하자.

kubectl get pods -w -l app=nginx

다른 터미널창에서 스테이트풀셋을 다시 지우자. 이번에는 --cascade=orphan 파라미터를 생략하자.

kubectl delete statefulset web
statefulset.apps "web" deleted

첫째 터미널에서 실행 중인 kubectl get 명령어의 출력을 살펴보고 모든 파드가 Terminating 상태로 전환될 때까지 기다리자.

kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          11m
web-1     1/1       Running   0          27m
NAME      READY     STATUS        RESTARTS   AGE
web-0     1/1       Terminating   0          12m
web-1     1/1       Terminating   0         29m
web-0     0/1       Terminating   0         12m
web-0     0/1       Terminating   0         12m
web-0     0/1       Terminating   0         12m
web-1     0/1       Terminating   0         29m
web-1     0/1       Terminating   0         29m
web-1     0/1       Terminating   0         29m

스케일 다운 섹션에서 보았듯 파드는 각 순번의 역순으로 하나씩 종료된다. 파드가 종료될 때 스테이트풀 컨트롤러는 이전 파드가 완전히 종료되기까지 기다린다.

kubectl delete service nginx
service "nginx" deleted

스테이트풀셋과 헤드리스 서비스를 한번 더 다시 생성하자.

kubectl apply -f web.yaml
service/nginx created
statefulset.apps/web created

스테이트풀셋의 모든 파드가 Running과 Ready 상태로 전환될 때 index.html 파일 내용을 검색하자.

for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

스테이트풀셋과 그 내부의 모든 파드를 삭제했지만 퍼시스턴트볼륨이 마운트된 채로 다시 생성되고 web-0web-1은 계속 각 호스트네임을 제공한다.

최종적으로 nginx 서비스를 삭제한다.

kubectl delete service nginx
service "nginx" deleted

그리고 web 스테이트풀셋을 삭제한다.

kubectl delete statefulset web
statefulset "web" deleted

파드 관리 정책

일부 분산 시스템의 경우 스테이트풀셋의 순서 보증은 불필요하거나 바람직하지 않다. 이러한 시스템은 고유성과 신원만 필요하다. 이를 해결하기 위해 쿠버네티스 1.7에서 .spec.podManagementPolicy를 스테이트풀셋 API 오브젝트에 도입했다.

OrderedReady 파드 관리

OrderedReady 파드 관리는 스테이트풀셋에서는 기본이다. 이는 스테이트풀셋 컨트롤러가 지금까지 위에서 설명했던 순서를 보증함을 뜻한다.

Parallel 파드 관리

Parallel 파드 관리는 스테이트풀셋 컨트롤러가 모든 파드를 병렬로 시작하고 종료하는 것으로, 다른 파드를 시작/종료하기 전에 파드가 Running과 Ready 상태로 전환되거나 완전히 종료되기까지 기다리지 않음을 뜻한다. 이 옵션은 스케일링 동작에만 영향을 미치며, 업데이트 동작에는 영향을 미치지 않는다.

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  podManagementPolicy: "Parallel"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

상기 예제를 다운로드받아 파일 이름을 web-parallel.yaml로 저장하자.

이 매니페스트는 web 스테이트풀셋의 .spec.podManagementPolicyParallel인 것 말고는 이전에 다운로드 받았던 것과 동일하다.

터미널에서 스테이트풀셋의 파드를 감시하자.

kubectl get pod -l app=nginx -w

다른 터미널에서 매니페스트 안에 스테이트풀셋과 서비스를 생성하자.

kubectl apply -f web-parallel.yaml
service/nginx created
statefulset.apps/web created

첫째 터미널에서 실행했던 kubectl get 명령어의 출력을 살펴보자.

kubectl get pod -l app=nginx -w
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-1     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         10s
web-1     1/1       Running   0         10s

스테이트풀셋 컨트롤러는 web-0web-1를 둘 다 동시에 시작했다.

두 번째 터미널을 열어 놓고 다른 터미널창에서 스테이트풀셋을 스케일링하자.

kubectl scale statefulset/web --replicas=4
statefulset.apps/web scaled

kubectl get 명령어를 실행 중인 터미널의 출력을 살펴보자.

web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         7s
web-3     0/1       ContainerCreating   0         7s
web-2     1/1       Running   0         10s
web-3     1/1       Running   0         26s

스테이트풀셋은 두 개의 새 파드를 시작하였다. 두 번째 것을 런칭하기 위해 먼저 런칭한 것이 Running과 Ready 상태가 될 때까지 기다리지 않는다.

정리하기

정리의 일환으로 kubectl 명령을 실행할 준비가 된 두 개의 터미널이 열려 있어야 한다.

kubectl delete sts web
# sts는 statefulset의 약자이다.

kubectl get 명령으로 해당 파드가 삭제된 것을 확인할 수 있다.

kubectl get pod -l app=nginx -w
web-3     1/1       Terminating   0         9m
web-2     1/1       Terminating   0         9m
web-3     1/1       Terminating   0         9m
web-2     1/1       Terminating   0         9m
web-1     1/1       Terminating   0         44m
web-0     1/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-3     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-1     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-2     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-1     0/1       Terminating   0         44m
web-1     0/1       Terminating   0         44m
web-1     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-3     0/1       Terminating   0         9m
web-3     0/1       Terminating   0         9m
web-3     0/1       Terminating   0         9m

삭제하는 동안, 스테이트풀셋은 모든 파드를 동시에 삭제한다. 해당 파드를 삭제하기 전에 그 파드의 순서상 후계자를 기다리지 않는다.

kubectl get 명령어가 실행된 터미널을 닫고 nginx 서비스를 삭제하자.

kubectl delete svc nginx

이 튜토리얼에서 퍼시스턴트볼륨으로 사용했던 영구 스토리지를 삭제한다.

kubectl get pvc
NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
www-web-0   Bound    pvc-2bf00408-d366-4a12-bad0-1869c65d0bee   1Gi        RWO            standard       25m
www-web-1   Bound    pvc-ba3bfe9c-413e-4b95-a2c0-3ea8a54dbab4   1Gi        RWO            standard       24m
www-web-2   Bound    pvc-cba6cfa6-3a47-486b-a138-db5930207eaf   1Gi        RWO            standard       15m
www-web-3   Bound    pvc-0c04d7f0-787a-4977-8da3-d9d3a6d8d752   1Gi        RWO            standard       15m
www-web-4   Bound    pvc-b2c73489-e70b-4a4e-9ec1-9eab439aa43e   1Gi        RWO            standard       14m
kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS   REASON   AGE
pvc-0c04d7f0-787a-4977-8da3-d9d3a6d8d752   1Gi        RWO            Delete           Bound    default/www-web-3   standard                15m
pvc-2bf00408-d366-4a12-bad0-1869c65d0bee   1Gi        RWO            Delete           Bound    default/www-web-0   standard                25m
pvc-b2c73489-e70b-4a4e-9ec1-9eab439aa43e   1Gi        RWO            Delete           Bound    default/www-web-4   standard                14m
pvc-ba3bfe9c-413e-4b95-a2c0-3ea8a54dbab4   1Gi        RWO            Delete           Bound    default/www-web-1   standard                24m
pvc-cba6cfa6-3a47-486b-a138-db5930207eaf   1Gi        RWO            Delete           Bound    default/www-web-2   standard                15m
kubectl delete pvc www-web-0 www-web-1 www-web-2 www-web-3 www-web-4
persistentvolumeclaim "www-web-0" deleted
persistentvolumeclaim "www-web-1" deleted
persistentvolumeclaim "www-web-2" deleted
persistentvolumeclaim "www-web-3" deleted
persistentvolumeclaim "www-web-4" deleted
kubectl get pvc
No resources found in default namespace.

6.2 - 예시: WordPress와 MySQL을 퍼시스턴트 볼륨에 배포하기

이 튜토리얼은 WordPress 사이트와 MySQL 데이터베이스를 Minikube를 이용하여 어떻게 배포하는지 보여준다. 애플리케이션 둘 다 퍼시스턴트 볼륨과 퍼시스턴트볼륨클레임을 데이터를 저장하기 위해 사용한다.

퍼시스턴트볼륨(PV)는 관리자가 수동으로 프로비저닝한 클러스터나 쿠버네티스 스토리지클래스를 이용해 동적으로 프로비저닝된 저장소의 일부이다. 퍼시스턴트볼륨클레임(PVC)은 PV로 충족할 수 있는 사용자에 의한 스토리지 요청이다. 퍼시스턴트볼륨은 파드 라이프사이클과 독립적이며 재시작, 재스케줄링이나 파드를 삭제할 때에도 데이터를 보존한다.

목적

  • 퍼시스턴트볼륨클레임과 퍼시스턴트볼륨 생성
  • 다음을 포함하는 kustomization.yaml 생성
    • 시크릿 생성자
    • MySQL 리소스 구성
    • WordPress 리소스 구성
  • kubectl apply -k ./로 생성한 kustomization 을 적용
  • 정리

시작하기 전에

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

버전 확인을 위해서, 다음 커맨드를 실행 kubectl version. 이 예시는 kubectl 1.14 이상 버전에서 동작한다.

다음 설정 파일을 다운로드한다.

  1. mysql-deployment.yaml

  2. wordpress-deployment.yaml

퍼시스턴트볼륨클레임과 퍼시스턴트볼륨 생성

MySQL과 Wordpress는 각각 데이터를 저장할 퍼시스턴트볼륨이 필요하다. 퍼시스턴트볼륨클레임은 배포 단계에 생성된다.

많은 클러스터 환경에서 설치된 기본 스토리지클래스(StorageClass)가 있다. 퍼시스턴트볼륨클레임에 스토리지클래스를 지정하지 않으면 클러스터의 기본 스토리지클래스를 사용한다.

퍼시스턴트볼륨클레임이 생성되면 퍼시스턴트볼륨이 스토리지클래스 설정을 기초로 동적으로 프로비저닝된다.

kustomization.yaml 생성하기

시크릿 생성자 추가

시크릿은 암호나 키 같은 민감한 데이터들을 저장하는 오브젝트이다. 1.14 버전부터 kubectl은 kustomization 파일을 이용해서 쿠버네티스 오브젝트를 관리한다. kustomization.yaml의 제너레이터로 시크릿을 생성할 수 있다.

다음 명령어로 kustomization.yaml 내에 시크릿 제네레이터를 추가한다. YOUR_PASSWORD는 사용하기 원하는 암호로 변경해야 한다.

cat <<EOF >./kustomization.yaml
secretGenerator:
- name: mysql-pass
  literals:
  - password=YOUR_PASSWORD
EOF

MySQL과 WordPress에 필요한 리소스 구성 추가하기

다음 매니페스트는 MySQL 디플로이먼트 단일 인스턴스를 기술한다. MySQL 컨케이너는 퍼시스턴트볼륨을 /var/lib/mysql에 마운트한다. MYSQL_ROOT_PASSWORD 환경 변수는 시크릿에서 가져와 데이터베이스 암호로 설정한다.

apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:5.6
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim

다음의 매니페스트는 단일-인스턴스 WordPress 디플로이먼트를 기술한다. WordPress 컨테이너는 웹사이트 데이터 파일을 위해 /var/www/html에 퍼시스턴트볼륨을 마운트한다. WORDPRESS_DB_HOST 환경 변수에는 위에서 정의한 MySQL 서비스의 이름이 설정되며, WordPress는 서비스를 통해 데이터베이스에 접근한다. WORDPRESS_DB_PASSWORD 환경 변수에는 kustomize가 생성한 데이터베이스 패스워드가 설정된다.

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
  type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress:4.8-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: wordpress-persistent-storage
        persistentVolumeClaim:
          claimName: wp-pv-claim
  1. MySQL 디플로이먼트 구성 파일을 다운로드한다.

    curl -LO https://k8s.io/examples/application/wordpress/mysql-deployment.yaml
    
  2. WordPress 구성 파일을 다운로드한다.

    curl -LO https://k8s.io/examples/application/wordpress/wordpress-deployment.yaml
    
  3. 두 파일을 kustomization.yaml에 추가하자.

cat <<EOF >>./kustomization.yaml
resources:
  - mysql-deployment.yaml
  - wordpress-deployment.yaml
EOF

적용하고 확인하기

kustomization.yaml은 WordPress 사이트와 MySQL 데이터베이스를 배포하는 모든 리소스를 포함한다. 다음과 같이 디렉터리를 적용할 수 있다.

kubectl apply -k ./

이제 모든 오브젝트가 존재하는지 확인할 수 있다.

  1. 시크릿이 존재하는지 다음 명령어를 실행하여 확인한다.

    kubectl get secrets
    

    응답은 아래와 비슷해야 한다.

    NAME                    TYPE                                  DATA   AGE
    mysql-pass-c57bb4t7mf   Opaque                                1      9s
    
  2. 퍼시스턴트볼륨이 동적으로 프로비저닝되었는지 확인한다.

    kubectl get pvc
    

    응답은 아래와 비슷해야 한다.

    NAME             STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       AGE
    mysql-pv-claim   Bound     pvc-8cbd7b2e-4044-11e9-b2bb-42010a800002   20Gi       RWO            standard           77s
    wp-pv-claim      Bound     pvc-8cd0df54-4044-11e9-b2bb-42010a800002   20Gi       RWO            standard           77s
    
  3. 다음 명령어를 실행하여 파드가 실행 중인지 확인한다.

    kubectl get pods
    

    응답은 아래와 비슷해야 한다.

    NAME                               READY     STATUS    RESTARTS   AGE
    wordpress-mysql-1894417608-x5dzt   1/1       Running   0          40s
    
  4. 다음 명령어를 실행하여 서비스가 실행 중인지 확인해보자.

    kubectl get services wordpress
    

    응답은 아래와 비슷해야 한다.

    NAME        TYPE            CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
    wordpress   LoadBalancer    10.0.0.89    <pending>     80:32406/TCP   4m
    
  5. 다음 명령어를 실행하여 WordPress 서비스의 IP 주소를 얻어온다.

    minikube service wordpress --url
    

    응답은 아래와 비슷해야 한다.

    http://1.2.3.4:32406
    
  6. IP 주소를 복사해서 웹 브라우저에서 사이트를 열어 보자.

    아래 스크린샷과 유사한 WordPress 설정 페이지를 볼 수 있어야 한다.

    wordpress-init

정리하기

  1. 다음 명령을 실행하여 시크릿, 디플로이먼트, 서비스와 퍼시스턴트볼륨클레임을 삭제하자.

    kubectl delete -k ./
    

다음 내용

6.3 - 예시: 카산드라를 스테이트풀셋으로 배포하기

이 튜토리얼은 쿠버네티스에서 아파치 카산드라를 실행하는 방법을 소개한다. 데이터베이스인 카산드라는 데이터 내구성을 제공하기 위해 퍼시스턴트 스토리지가 필요하다(애플리케이션 상태). 이 예제에서 사용자 지정 카산드라 시드 공급자는 카산드라가 클러스터에 가입할 때 카산드라가 인스턴스를 검색할 수 있도록 한다.

스테이트풀셋 은 상태있는 애플리케이션을 쿠버네티스 클러스터에 쉽게 배포할 수 있게 한다. 이 튜토리얼에서 이용할 기능의 자세한 정보는 스테이트풀셋을 참조한다.

목적

  • 카산드라 헤드리스 Service를 생성하고 검증한다.
  • 스테이트풀셋(StatefulSet)을 이용하여 카산드라 링을 생성한다.
  • 스테이트풀셋을 검증한다.
  • 스테이트풀셋을 수정한다.
  • 스테이트풀셋과 포함된 파드를 삭제한다.

시작하기 전에

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

이 튜토리얼을 완료하려면, 파드, 서비스, 스테이트풀셋에 대한 기본 지식이 있어야 한다.

추가적인 Minikube 설정 요령

카산드라를 위한 헤드리스 서비스 생성하기

쿠버네티스 에서 서비스는 동일 작업을 수행하는 파드의 집합을 기술한다.

다음의 서비스는 클러스터에서 카산드라 파드와 클라이언트 간에 DNS 찾아보기 용도로 사용한다.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: cassandra
  name: cassandra
spec:
  clusterIP: None
  ports:
  - port: 9042
  selector:
    app: cassandra

cassandra-service.yaml 파일에서 카산드라 스테이트풀셋 노드를 모두 추적하는 서비스를 생성한다.

kubectl apply -f https://k8s.io/examples/application/cassandra/cassandra-service.yaml

검증하기(선택)

카산드라 서비스 살펴보기

kubectl get svc cassandra

결과는 다음과 같다.

NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
cassandra   ClusterIP   None         <none>        9042/TCP   45s

cassandra 서비스가 보이지 않는다면, 이와 다른 응답이라면 서비스 생성에 실패한 것이다. 일반적인 문제에 대한 서비스 디버깅하기를 읽어보자.

카산드라 링을 생성하는 스테이트풀셋 이용하기

스테이트풀셋 매니페스트에는 다음을 포함하는데 3개 파드로 구성된 카산드라 링을 생성한다.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  labels:
    app: cassandra
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      terminationGracePeriodSeconds: 1800
      containers:
      - name: cassandra
        image: gcr.io/google-samples/cassandra:v13
        imagePullPolicy: Always
        ports:
        - containerPort: 7000
          name: intra-node
        - containerPort: 7001
          name: tls-intra-node
        - containerPort: 7199
          name: jmx
        - containerPort: 9042
          name: cql
        resources:
          limits:
            cpu: "500m"
            memory: 1Gi
          requests:
            cpu: "500m"
            memory: 1Gi
        securityContext:
          capabilities:
            add:
              - IPC_LOCK
        lifecycle:
          preStop:
            exec:
              command: 
              - /bin/sh
              - -c
              - nodetool drain
        env:
          - name: MAX_HEAP_SIZE
            value: 512M
          - name: HEAP_NEWSIZE
            value: 100M
          - name: CASSANDRA_SEEDS
            value: "cassandra-0.cassandra.default.svc.cluster.local"
          - name: CASSANDRA_CLUSTER_NAME
            value: "K8Demo"
          - name: CASSANDRA_DC
            value: "DC1-K8Demo"
          - name: CASSANDRA_RACK
            value: "Rack1-K8Demo"
          - name: POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
        readinessProbe:
          exec:
            command:
            - /bin/bash
            - -c
            - /ready-probe.sh
          initialDelaySeconds: 15
          timeoutSeconds: 5
        # These volume mounts are persistent. They are like inline claims,
        # but not exactly because the names need to match exactly one of
        # the stateful pod volumes.
        volumeMounts:
        - name: cassandra-data
          mountPath: /cassandra_data
  # These are converted to volume claims by the controller
  # and mounted at the paths mentioned above.
  # do not use these in production until ssd GCEPersistentDisk or other ssd pd
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: fast
      resources:
        requests:
          storage: 1Gi
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: k8s.io/minikube-hostpath
parameters:
  type: pd-ssd

cassandra-statefulset.yaml 파일로 카산드라 스테이트풀셋 생성

# cassandra-statefulset.yaml을 수정하지 않은 경우에 이것을 사용한다.
kubectl apply -f https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml

클러스터에 맞게 cassandra-statefulset.yaml 를 수정해야 하는 경우 다음을 다운로드한 다음 수정된 버전을 저장한 폴더에서 해당 매니페스트를 적용한다. https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml

# cassandra-statefulset.yaml을 로컬에서 수정한 경우에 사용한다.
kubectl apply -f cassandra-statefulset.yaml

카산드라 스테이트풀셋 검증하기

  1. 카산드라 스테이트풀셋 얻기

    kubectl get statefulset cassandra
    

    응답은 다음과 유사하다.

    NAME        DESIRED   CURRENT   AGE
    cassandra   3         0         13s
    

    StatefulSet리소스는 순차적으로 파드를 배포한다.

  2. 순차적으로 생성된 현황을 보기 위해 파드를 살펴보자.

    kubectl get pods -l="app=cassandra"
    

    응답은 다음과 유사하다.

    NAME          READY     STATUS              RESTARTS   AGE
    cassandra-0   1/1       Running             0          1m
    cassandra-1   0/1       ContainerCreating   0          8s
    

    모든 3개 파드가 배포되기까지 몇 분이 소요될 수 있다. 배포 후, 동일 명령은 다음과 유사하게 응답한다.

    NAME          READY     STATUS    RESTARTS   AGE
    cassandra-0   1/1       Running   0          10m
    cassandra-1   1/1       Running   0          9m
    cassandra-2   1/1       Running   0          8m
    
  3. 첫 번째 파드 내부에 링의 상태를 보여주는 카산드라 nodetool을 실행하자.

    kubectl exec -it cassandra-0 -- nodetool status
    

    이 응답은 다음과 비슷하게 보일 것이다.

    Datacenter: DC1-K8Demo
    ======================
    Status=Up/Down
    |/ State=Normal/Leaving/Joining/Moving
    --  Address     Load       Tokens       Owns (effective)  Host ID                               Rack
    UN  172.17.0.5  83.57 KiB  32           74.0%             e2dd09e6-d9d3-477e-96c5-45094c08db0f  Rack1-K8Demo
    UN  172.17.0.4  101.04 KiB  32           58.8%             f89d6835-3a42-4419-92b3-0e62cae1479c  Rack1-K8Demo
    UN  172.17.0.6  84.74 KiB  32           67.1%             a6a1e8c2-3dc5-4417-b1a0-26507af2aaad  Rack1-K8Demo
    

카산드라 스테이트풀셋 수정하기

kubectl edit를 사용하여 카산드라 스테이트풀셋의 크기를 수정한다.

  1. 다음 명령어를 실행한다.

    kubectl edit statefulset cassandra
    

    이 명령은 터미널에서 편집기를 연다. 변경해야할 행은 replicas 필드이다. 다음 예제는 스테이트풀셋 파일에서 발췌했다.

    # 다음의 오브젝트를 수정한다. '#'로 시작하는 행은 무시되고,
    # 빈 파일은 편집을 중단한다. 저장할 때 오류가 발생하면 이 파일이
    # 관련 실패와 함께 다시 열린다.
    #
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      creationTimestamp: 2016-08-13T18:40:58Z
      generation: 1
      labels:
      app: cassandra
      name: cassandra
      namespace: default
      resourceVersion: "323"
      uid: 7a219483-6185-11e6-a910-42010a8a0fc0
    spec:
      replicas: 3
    
  2. 레플리카 개수를 4로 바꾸고, 매니페스트를 저장한다.

    스테이트풀셋은 4개의 파드를 실행하기 위해 스케일 한다.

  3. 검증하기 위해 카산드라 스테이트풀셋을 살펴보자

    kubectl get statefulset cassandra
    

    결과는 다음과 유사하다.

    NAME        DESIRED   CURRENT   AGE
    cassandra   4         4         36m
    

정리하기

스테이트풀셋을 삭제하거나 스케일링하는 것은 스테이트풀셋에 연관된 볼륨을 삭제하지 않는다. 당신의 데이터가 스테이트풀셋의 관련된 모든 리소스를 자동으로 제거하는 것보다 더 가치있기에 이 설정은 당신의 안전을 위한 것이다.

  1. 다음 명령어(한 줄로 연결된)를 실행하여 카산드라 스테이트풀셋을 모두 제거하자.

    grace=$(kubectl get pod cassandra-0 -o=jsonpath='{.spec.terminationGracePeriodSeconds}') \
      && kubectl delete statefulset -l app=cassandra \
      && echo "Sleeping ${grace} seconds" 1>&2 \
      && sleep $grace \
      && kubectl delete persistentvolumeclaim -l app=cassandra
    
  2. 다음 명령어를 실행하여 카산드라에 대해 설정한 서비스를 제거하자.

    kubectl delete service -l app=cassandra
    

카산드라 컨테이너 환경 변수

이 튜토리얼의 파드 는 구글의 컨테이너 레지스트리gcr.io/google-samples/cassandra:v13 이미지를 이용한다. 이 도커 이미지는 debian-base에 기반하였고 OpenJDK 8을 포함한다.

이 이미지는 아파치 데비안 리포의 표준 카산드라 설치본을 포함한다. 환경 변수를 이용하여 cassandra.yaml에 삽입된 값을 바꿀 수 있다.

환경 변수기본값
CASSANDRA_CLUSTER_NAME'Test Cluster'
CASSANDRA_NUM_TOKENS32
CASSANDRA_RPC_ADDRESS0.0.0.0

다음 내용

6.4 - 분산 시스템 코디네이터 ZooKeeper 실행하기

이 튜토리얼은 아파치 ZooKeeper 쿠버네티스에서 스테이트풀셋PodDisruptionBudget파드안티어피니티(PodAntiAffinity)를 이용한 Apache Zookeeper 실행을 설명한다.

시작하기 전에

이 튜토리얼을 시작하기 전에 다음 쿠버네티스 개념에 친숙해야 한다.

반드시 최소한 4개의 노드가 있는 클러스터가 필요하며, 각 노드는 적어도 2 개의 CPU와 4 GiB 메모리가 필요하다. 이 튜토리얼에서 클러스터 노드를 통제(cordon)하고 비우게(drain) 할 것이다. 이것은 클러스터를 종료하여 노드의 모든 파드를 축출(evict)하는 것으로, 모든 파드는 임시로 언스케줄된다는 의미이다. 이 튜토리얼을 위해 전용 클러스터를 이용하거나, 다른 테넌트에 간섭을 하는 혼란이 발생하지 않도록 해야 합니다.

이 튜토리얼은 클러스터가 동적으로 퍼시스턴트볼륨을 프로비저닝하도록 구성한다고 가정한다. 그렇게 설정되어 있지 않다면 튜토리얼을 시작하기 전에 수동으로 3개의 20 GiB 볼륨을 프로비저닝해야 한다.

목적

이 튜토리얼을 마치면 다음에 대해 알게 된다.

  • 어떻게 스테이트풀셋을 이용하여 ZooKeeper 앙상블을 배포하는가.
  • 어떻게 앙상블을 일관되게 설정하는가.
  • 어떻게 ZooKeeper 서버 디플로이먼트를 앙상블 안에서 퍼뜨리는가.
  • 어떻게 PodDisruptionBudget을 이용하여 계획된 점검 기간 동안 서비스 가용성을 보장하는가.

ZooKeeper

아파치 ZooKeeper는 분산 애플리케이션을 위한 분산 오픈 소스 코디네이션 서비스이다. ZooKeeper는 데이터를 읽고 쓰고 갱신을 지켜보도록 한다. 데이터는 파일시스템처럼 계층적으로 관리되고 앙상블(ZooKeeper 서버의 집합) 내에 모든 ZooKeeper서버에 복제된다. 데이터에 모든 연산은 원자적이고 순처적으로 일관된다. ZooKeeper는 Zab 합의 프로토콜을 이용하여 앙상블 내에 모든 서버에 걸쳐 상태 머신을 복제하여 이를 보장한다.

앙상블은 리더 선출을 위해 Zab 프로토콜을 사용하고, 리더 선출과 선거가 완료되기 전까지 앙상블은 데이터를 쓸 수 없다. 완료되면 앙상블은 Zab을 이용하여 확인하고 클라이언트에 보이도록 모든 쓰기를 쿼럼(quorum)에 복제한다. 가중치있는 쿼럼과 관련 없이, 쿼럼은 현재 리더를 포함하는 앙상블의 대다수 컴포넌트이다. 예를 들어 앙상블이 3개 서버인 경우, 리더와 다른 서버로 쿼럼을 구성한다. 앙상블이 쿼럼을 달성할 수 없다면, 앙상블은 데이터를 쓸 수 없다.

ZooKeeper는 전체 상태 머신을 메모리에 보존하고 모든 돌연변이를 저장 미디어의 내구성 있는 WAL(Write Ahead Log)에 기록한다. 서버 장애시 WAL을 재생하여 이전 상태를 복원할 수 있다. WAL이 무제한으로 커지는 것을 방지하기 위해 ZooKeeper는 주기적으로 저장 미디어에 메모리 상태의 스냅샷을 저장한다. 이 스냅샷은 메모리에 직접 적재할 수 있고 스냅샷 이전의 모든 WAL 항목은 삭제될 수 있다.

ZooKeeper 앙상블 생성하기

아래 매니페스트에는 헤드리스 서비스, 서비스, PodDisruptionBudget, 스테이트풀셋을 포함한다.

apiVersion: v1
kind: Service
metadata:
  name: zk-hs
  labels:
    app: zk
spec:
  ports:
  - port: 2888
    name: server
  - port: 3888
    name: leader-election
  clusterIP: None
  selector:
    app: zk
---
apiVersion: v1
kind: Service
metadata:
  name: zk-cs
  labels:
    app: zk
spec:
  ports:
  - port: 2181
    name: client
  selector:
    app: zk
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  selector:
    matchLabels:
      app: zk
  maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: zk
spec:
  selector:
    matchLabels:
      app: zk
  serviceName: zk-hs
  replicas: 3
  updateStrategy:
    type: RollingUpdate
  podManagementPolicy: OrderedReady
  template:
    metadata:
      labels:
        app: zk
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"
      containers:
      - name: kubernetes-zookeeper
        imagePullPolicy: Always
        image: "registry.k8s.io/kubernetes-zookeeper:1.0-3.4.10"
        resources:
          requests:
            memory: "1Gi"
            cpu: "0.5"
        ports:
        - containerPort: 2181
          name: client
        - containerPort: 2888
          name: server
        - containerPort: 3888
          name: leader-election
        command:
        - sh
        - -c
        - "start-zookeeper \
          --servers=3 \
          --data_dir=/var/lib/zookeeper/data \
          --data_log_dir=/var/lib/zookeeper/data/log \
          --conf_dir=/opt/zookeeper/conf \
          --client_port=2181 \
          --election_port=3888 \
          --server_port=2888 \
          --tick_time=2000 \
          --init_limit=10 \
          --sync_limit=5 \
          --heap=512M \
          --max_client_cnxns=60 \
          --snap_retain_count=3 \
          --purge_interval=12 \
          --max_session_timeout=40000 \
          --min_session_timeout=4000 \
          --log_level=INFO"
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/zookeeper
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

터미널을 열고 kubectl apply 명령어로 매니페스트를 생성하자.

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

이는 zk-hs 헤드리스 서비스, zk-cs 서비스, zk-pdb PodDisruptionBudget과 zk 스테이트풀셋을 생성한다.

service/zk-hs created
service/zk-cs created
poddisruptionbudget.policy/zk-pdb created
statefulset.apps/zk created

kubectl get을 사용하여 스테이트풀셋 컨트롤러가 스테이트풀셋 파드를 생성하는지 확인한다.

kubectl get pods -w -l app=zk

zk-2 파드가 Running and Ready 상태가 되면, CTRL-C를 눌러 kubectl을 종료하자.

NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

스테이트풀셋 컨트롤러는 3개의 파드를 생성하고, 각 파드는 ZooKeeper 서버를 포함한 컨테이너를 가진다.

리더 선출 촉진

익명 네트워크에서 리더 선출을 위한 종료 알고리즘이 없기에, Zab은 리더 선출을 위해 명시적인 멤버 구성을 해야 한다. 앙상블의 각 서버는 고유 식별자를 가져야 하고, 모든 서버는 식별자 전역 집합을 알아야 하며, 각 식별자는 네트워크 주소에 연관되어야 한다.

kubectl exec를 이용하여 zk 스테이트풀셋의 파드의 호스트네임을 알아내자.

for i in 0 1 2; do kubectl exec zk-$i -- hostname; done

스테이트풀셋 컨트롤러는 각 순번 인덱스에 기초하여 각 파드에 고유한 호스트네임을 부여한다. 각 호스트네임은 <스테이트풀셋 이름>-<순번 인덱스> 형식을 취한다. zk 스테이트풀셋의 replicas 필드는 3으로 설정되었기 때문에, 그 스테이트풀셋 컨트롤러는 3개 파드의 호스트네임을 zk-0, zk-1, zk-2로 정한다.

zk-0
zk-1
zk-2

ZooKeeper 앙상블에 서버들은 고유 식별자로서 자연수를 이용하고 서버 데이터 디렉터리에 my 라는 파일로 서버 식별자를 저장한다.

각 서버에서 다음 명령어를 이용하여 myid 파일의 내용을 확인하자.

for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done

식별자는 자연수이고, 순번 인덱스들도 음수가 아니므로, 순번에 1을 더하여 순번을 만들 수 있다.

myid zk-0
1
myid zk-1
2
myid zk-2
3

zk 스테이트풀셋의 각 파드 Fully Qualified Domain Name (FQDN)을 얻기 위해 다음 명령어를 이용하자.

for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done

zk-hs 서비스는 모든 파드를 위한 도메인인 zk-hs.default.svc.cluster.local을 만든다.

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

쿠버네티스 DNS의 A 레코드는 FQDN을 파드의 IP 주소로 풀어낸다. 쿠버네티스가 파드를 리스케줄하면, 파드의 새 IP 주소로 A 레코드를 갱신하지만, A 레코드의 이름은 바뀌지 않는다.

ZooKeeper는 그것의 애플리케이션 환경설정을 zoo.cfg 파일에 저장한다. kubectl exec를 이용하여 zk-0 파드의 zoo.cfg 내용을 보자.

kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg

아래 파일의 server.1, server.2, server.3 속성에서 1, 2, 3은 ZooKeeper 서버의 myid 파일에 구분자와 연관된다. 이들은 zk 스테이트풀셋의 파드의 FQDNS을 설정한다.

clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

합의 달성

합의 프로토콜에서 각 참가자의 식별자는 유일해야 한다. Zab 프로토콜에서 동일한 고유 식별자를 요청하는 참가자는 없다. 이는 시스템 프로세스가 어떤 프로세스가 어떤 데이터를 커밋했는지 동의하게 하는데 필요하다. 2개 파드를 동일 순번으로 시작하였다면 두 대의 ZooKeeper 서버는 둘 다 스스로를 동일 서버로 식별한다.

합의 프로토콜에서 각 참여자의 식별자는 고유해야 한다. Zab 프로토콜에 두 참여자가 동일한 고유 식별자로 요청해서는 안된다. 이는 시스템 프로세스가 어떤 프로세스가 어떤 데이터를 커밋했는지 동의하도록 하기 위해 필수적이다. 동일 순번으로 두 개의 파드가 실행했다면 두 ZooKeeper 서버는 모두 동일한 서버로 식별된다.

kubectl get pods -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

각 파드의 A 레코드는 파드가 Ready 상태가 되면 입력된다. 따라서 ZooKeeper 서버의 FQDN은 단일 엔드포인트로 확인되고 해당 엔드포인트는 myid 파일에 구성된 식별자를 가진 고유한 ZooKeeper 서버가 된다.

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

이것은 ZooKeeper의 zoo.cfg 파일에 servers 속성이 정확히 구성된 앙상블로 나타나는 것을 보증한다.

server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

서버가 Zab 프로토콜로 값을 커밋 시도하면, 합의를 이루어 값을 커밋하거나(리더 선출에 성공했고 나머지 두 개 파드도 Running과 Ready 상태라면) 실패한다(조건 중 하나라도 충족하지 않으면). 다른 서버를 대신하여 쓰기를 승인하는 상태는 발생하지 않는다.

앙상블 무결성 테스트

가장 기본적인 테스트는 한 ZooKeeper 서버에 데이터를 쓰고 다른 ZooKeeper 서버에서 데이터를 읽는 것이다.

아래 명령어는 앙상블 내에 zk-0 파드에서 /hello 경로로 world를 쓰는 스크립트인 zkCli.sh를 실행한다.

kubectl exec zk-0 -- zkCli.sh create /hello world
WATCHER::

WatchedEvent state:SyncConnected type:None path:null
Created /hello

zk-1 파드에서 데이터를 읽기 위해 다음 명령어를 이용하자.

kubectl exec zk-1 -- zkCli.sh get /hello

zk-0에서 생성한 그 데이터는 앙상블 내에 모든 서버에서 사용할 수 있다.

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

내구성있는 저장소 제공

ZooKeeper 기본 섹션에서 언급했듯이 ZooKeeper는 모든 항목을 내구성있는 WAL에 커밋하고 메모리 상태의 스냅샷을 저장 미디에에 주기적으로 저장한다. 내구성을 제공하기 위해 WAL을 이용하는 것은 복제된 상태 머신을 이루는 합의 프로토콜에서 이용하는 일반적인 기법이다.

kubectl delete 명령을 이용하여 zk 스테이트풀셋을 삭제하자.

kubectl delete statefulset zk
statefulset.apps "zk" deleted

스테이트풀셋의 파드가 종료되는 것을 지켜보자.

kubectl get pods -w -l app=zk

zk-0이 완전히 종료되면 CTRL-C를 이용해 kubectl을 종료하자.

zk-2      1/1       Terminating   0         9m
zk-0      1/1       Terminating   0         11m
zk-1      1/1       Terminating   0         10m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m

zookeeper.yaml 매니페스트를 다시 적용한다.

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

zk 스테이트풀셋 오브젝트를 생성하지만, 매니페스트에 다른 API 오브젝트는 이미 존재하므로 수정되지 않는다.

스테이트풀셋 컨트롤러가 스테트풀셋의 파드를 재생성하는 것을 확인한다.

kubectl get pods -w -l app=zk

zk-2 파드가 Running과 Ready가 되면 CTRL-C를 이용하여 kubectl을 종료한다.

NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

아래 명령어로 무결성 테스트에서 입력한 값을 zk-2 파드에서 얻어온다.

kubectl exec zk-2 zkCli.sh get /hello

zk 스테이트풀셋의 모든 파드를 종료하고 재생성했음에도, 앙상블은 여전히 원래 값을 돌려준다.

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

zk 스테이트풀셋의 specvolumeClaimTemplates 필드는 각 파드에 프로비전될 퍼시스턴트볼륨을 지정한다.

volumeClaimTemplates:
  - metadata:
      name: datadir
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi

스테이트풀셋 컨트롤러는 스테이트풀셋의 각 파드에 대한 퍼시스턴트볼륨클레임을 생성한다.

다음 명령어를 이용하여 스테이트풀셋퍼시스턴트볼륨클레임을 살펴보자.

kubectl get pvc -l app=zk

스테이트풀셋의 파드를 재생성할 때에 파드의 퍼시스턴트볼륨도 다시 마운트한다.

NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
datadir-zk-0   Bound     pvc-bed742cd-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-1   Bound     pvc-bedd27d2-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-2   Bound     pvc-bee0817e-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h

스테이트풀셋의 컨테이너 templatevolumeMounts 부분이 ZooKeeper 서버의 데이터 디렉터리에 퍼시스턴트볼륨 마운트하는 내용이다.

volumeMounts:
- name: datadir
  mountPath: /var/lib/zookeeper

zk 스테이트풀셋이 (재)스케줄링될 때 항상 동일한 퍼시스턴트볼륨을 ZooKeeper의 서버 디렉터리에 마운트한다. 파드를 재스케줄할 때에도 ZooKeeper의 WAL을 통해 이뤄진 모든 쓰기와 모든 그 스냅샷도 내구성을 유지한다.

일관된 구성 보장하기

리더 선출 촉진합의 달성 섹션에서 알렸듯이, ZooKeeper 앙상블에 서버는 리더 선출과 쿼럼을 구성하기 위한 일관된 설정이 필요하다. 또한 Zab 프로토콜의 일관된 설정도 네트워크에 걸쳐 올바르게 동작하기 위해서 필요하다. 이 예시에서는 매니페스트에 구성을 직접 포함시켜서 일관된 구성을 달성한다.

zk 스테이트풀셋을 살펴보자.

kubectl get sts zk -o yaml
…
command:
      - sh
      - -c
      - "start-zookeeper \
        --servers=3 \
        --data_dir=/var/lib/zookeeper/data \
        --data_log_dir=/var/lib/zookeeper/data/log \
        --conf_dir=/opt/zookeeper/conf \
        --client_port=2181 \
        --election_port=3888 \
        --server_port=2888 \
        --tick_time=2000 \
        --init_limit=10 \
        --sync_limit=5 \
        --heap=512M \
        --max_client_cnxns=60 \
        --snap_retain_count=3 \
        --purge_interval=12 \
        --max_session_timeout=40000 \
        --min_session_timeout=4000 \
        --log_level=INFO"
…

ZooKeeper 서버를 시작하는데 사용한 명령어는 커맨드라인 파라미터로 환경 구성을 전달했다. 환경 변수를 이용하여서도 앙상블에 환경 구성을 전달할 수 있다.

로깅 설정하기

zkGenConfig.sh 스크립트로 생성된 파일 중 하나는 ZooKeeper의 로깅을 제어한다. ZooKeeper는 Log4j를 이용하며 기본 로깅 구성으로는 시간과 파일 크기 기준의 롤링 파일 어펜더를 사용한다.

zk 스테이트풀셋의 한 파드에서 로깅 설정을 살펴보는 아래 명령어를 이용하자.

kubectl exec zk-0 cat /usr/etc/zookeeper/log4j.properties

아래 로깅 구성은 ZooKeeper가 모든 로그를 표준 출력 스트림으로 처리하게 한다.

zookeeper.root.logger=CONSOLE
zookeeper.console.threshold=INFO
log4j.rootLogger=${zookeeper.root.logger}
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold}
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n

이는 컨테이너 내에서 안전하게 로깅하는 가장 단순한 방법이다. 표준 출력으로 애플리케이션 로그를 작성하면, 쿠버네티스는 로그 로테이션을 처리한다. 또한 쿠버네티스는 애플리케이션이 표준 출력과 표준 오류에 쓰인 로그로 인하여 로컬 저장 미디어가 고갈되지 않도록 보장하는 정상적인 보존 정책을 구현한다.

파드의 마지막 20줄의 로그를 가져오는 kubectl logs 명령을 이용하자.

kubectl logs zk-0 --tail 20

kubectl logs를 이용하거나 쿠버네티스 대시보드에서 표준 출력과 표준 오류로 쓰인 애플리케이션 로그를 볼 수 있다.

2016-12-06 19:34:16,236 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52740
2016-12-06 19:34:16,237 [myid:1] - INFO  [Thread-1136:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52740 (no session established for client)
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52749
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52749
2016-12-06 19:34:26,156 [myid:1] - INFO  [Thread-1137:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52749 (no session established for client)
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52750
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52750
2016-12-06 19:34:26,226 [myid:1] - INFO  [Thread-1138:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52750 (no session established for client)
2016-12-06 19:34:36,151 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [Thread-1139:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52760 (no session established for client)
2016-12-06 19:34:36,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [Thread-1140:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52761 (no session established for client)
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [Thread-1141:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52767 (no session established for client)
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [Thread-1142:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52768 (no session established for client)

쿠버네티스는 많은 로그 솔루션과 통합된다. 클러스터와 애플리케이션에 가장 적합한 로그 솔루션을 선택할 수 있다. 클러스터 수준의 로그 적재(ship)와 통합을 위해서는 로그 순환과 적재를 위해 사이드카 컨테이너를 배포하는 것을 고려한다.

권한 없는 사용자를 위해 구성하기

컨테이너 내부의 권한있는 유저로 애플리케이션을 실행할 수 있도록 하는 최상의 방법은 논쟁거리이다. 조직에서 애플리케이션을 권한 없는 사용자가 실행한다면, 진입점을 실행할 사용자를 제어하기 위해 시큐리티컨텍스트를 이용할 수 있다.

zk 스테이트풀셋의 파드 templateSecurityContext를 포함한다.

securityContext:
  runAsUser: 1000
  fsGroup: 1000

파드 컨테이너에서 UID 1000은 ZooKeeper 사용자이며, GID 1000은 ZooKeeper의 그룹에 해당한다.

zk-0 파드에서 프로세스 정보를 얻어오자.

kubectl exec zk-0 -- ps -elf

securityContext 오브젝트의 runAsUser 필드 값이 1000 이므로 루트 사용자로 실행하는 대신 ZooKeeper 프로세스는 ZooKeeper 사용자로 실행된다.

F S UID        PID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S zookeep+     1     0  0  80   0 -  1127 -      20:46 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
0 S zookeep+    27     1  0  80   0 - 1155556 -    20:46 ?        00:00:19 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg

기본적으로 파드의 퍼시스턴트볼륨은 ZooKeeper 서버의 데이터 디렉터리에 마운트되고, 루트 사용자만이 접근 가능하다. 이 구성은 ZooKeeper 프로세스가 WAL에 기록하고 스냅샷을 저장하는 것을 방지한다.

zk-0 파드의 ZooKeeper 데이터 디렉터리의 권한을 얻어오는 아래 명령어를 이용하자.

kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data

securityContext 오브젝트의 fsGroup 필드 값이 1000 이므로, 파드의 퍼시스턴트 볼륨의 소유권은 ZooKeeper 그룹으로 지정되어 ZooKeeper 프로세스에서 읽고 쓸 수 있다.

drwxr-sr-x 3 zookeeper zookeeper 4096 Dec  5 20:45 /var/lib/zookeeper/data

ZooKeeper 프로세스 관리하기

ZooKeeper 문서에서는 "ZooKeeper의 서버 프로세스(JVM)을 관리할 감독 프로세스를 필요할 것이다."라고 말한다. 와치독(감독 프로세스)를 활용하여 실패한 프로세스를 재시작하는 것은 분산시스템에서 일반적인 방식이다. 쿠버네티스에서 애플리케이션을 배포할 때에는 감독 프로세스로 외부 유틸리티를 사용하기보다 쿠버네티스를 애플리케이션의 와치독으로서 사용해야 한다.

앙상블 관리하기

zk 스테이트풀셋RollingUpdate 업데이트 전략을 이용하도록 구성되었다.

kubectl patch로 서버에 할당된 cpu 수를 갱신할 수 있다.

kubectl patch sts zk --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value":"0.3"}]'
statefulset.apps/zk patched

업데이트 상황을 지켜보기 위해 kubectl rollout status 이용하자.

kubectl rollout status sts/zk
waiting for statefulset rolling update to complete 0 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 1 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 2 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
statefulset rolling update complete 3 pods at revision zk-5db4499664...

이것은 파드를 역순으로 한 번에 하나씩 종료하고, 새로운 구성으로 재생성한다. 이는 롤링업데이트 동안에 쿼럼을 유지하도록 보장한다.

이력과 이전 구성을 보기 위해 kubectl rollout history 명령을 이용하자.

kubectl rollout history sts/zk

출력은 다음과 비슷할 것이다.

statefulsets "zk"
REVISION
1
2

수정사항을 롤백하기 위해 kubectl rollout undo 명령을 이용하자.

kubectl rollout undo sts/zk

출력은 다음과 비슷할 것이다.

statefulset.apps/zk rolled back

프로세스 장애 관리하기

재시작 정책은 쿠버네티스가 파드 내에 컨테이너의 진입점에서 프로세스 실패를 어떻게 다루는지 제어한다. 스테이트풀셋의 파드에서 오직 적절한 재시작 정책는 Always이며 이것이 기본 값이다. 상태가 유지되는 애플리케이션을 위해 기본 정책을 절대로 변경하지 말자.

zk-0 파드에서 실행 중인 ZooKeeper 서버에서 프로세스 트리를 살펴보기 위해 다음 명령어를 이용하자.

kubectl exec zk-0 -- ps -ef

컨테이너의 엔트리 포인트로 PID 1 인 명령이 사용되었으며 ZooKeeper 프로세스는 엔트리 포인트의 자식 프로세스로 PID 27 이다.

UID        PID  PPID  C STIME TTY          TIME CMD
zookeep+     1     0  0 15:03 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
zookeep+    27     1  0 15:03 ?        00:00:03 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg

다른 터미널에서 다음 명령어로 zk 스테이트풀셋의 파드를 확인한다.

kubectl get pod -w -l app=zk

또 다른 터미널에서 다음 명령어로 zk-0 파드의 ZooKeeper 프로세스를 종료시킨다.

kubectl exec zk-0 -- pkill java

ZooKeeper 프로세스의 종료는 부모 프로세스의 종료를 일으킨다. 컨테이너 재시작정책이 Always이기 때문에 부모 프로세스를 재시작했다.

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          21m
zk-1      1/1       Running   0          20m
zk-2      1/1       Running   0          19m
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Error     0          29m
zk-0      0/1       Running   1         29m
zk-0      1/1       Running   1         29m

애플리케이션이 스크립트(zkServer.sh 같은)를 애플리케이션의 비지니스 로직을 구현한 프로세스를 시작하기 위해 이용한다면, 그 스크립트는 자식 프로세스와 함께 반드시 종료되어야 한다. 이는 쿠버네티스가 애플리케이션의 비지니스 로직을 구현한 프로세스가 실패할 때에 애플리케이션 컨테이너를 재시작하는 것을 보증한다.

활성도(Liveness) 테스트하기

실패한 애플리케이션을 재시작하도록 구성하는 것은 분산 시스템을 건강하게 유지하는데 충분하지 않다. 시스템의 프로세스는 살아있지만 응답이 없을 수 있고, 혹은 다른 건강하지 않은 경우의 시나리오가 있다. 애플리케이션 프로세스가 건강하지 않고 재시작해야만 한다는 것을 쿠버네티스에게 알리도록 활성도 검사를 이용해야 한다.

zk 스테이트풀셋에 파드 template에 활성도 검사를 명시한다.

  livenessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5

검사는 ZooKeeper의 ruok 4 글자 단어를 이용해서 서버의 건강을 테스트하는 배쉬 스크립트를 호출한다.

OK=$(echo ruok | nc 127.0.0.1 $1)
if [ "$OK" == "imok" ]; then
    exit 0
else
    exit 1
fi

한 터미널에서 zk 스테이트풀셋의 파드를 지켜보기 위해 다음 명령어를 이용하자.

kubectl get pod -w -l app=zk

다른 창에서 zk-0 파드의 파일시스템에서 zookeeper-ready 스크립트를 삭제하기 위해 다음 명령어를 이용하자.

kubectl exec zk-0 -- rm /opt/zookeeper/bin/zookeeper-ready

ZooKeeper의 활성도 검사에 실패하면, 쿠버네티스는 자동으로 프로세스를 재시작하여 앙상블에 건강하지 않은 프로세스를 재시작하는 것을 보증한다.

kubectl get pod -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Running   0          1h
zk-0      0/1       Running   1         1h
zk-0      1/1       Running   1         1h

준비도 테스트

준비도는 활성도와 동일하지 않다. 프로세스가 살아 있다면, 스케줄링되고 건강하다. 프로세스가 준비되면 입력을 처리할 수 있다. 활성도는 필수적이나 준비도의 조건으로는 충분하지 않다. 몇몇 경우 특별히 초기화와 종료 시에 프로세스는 살아있지만 준비되지 않을 수 있다.

준비도 검사를 지정하면, 쿠버네티스는 준비도가 통과할 때까지 애플리케이션 프로세스가 네트워크 트래픽을 수신하지 않게 한다.

ZooKeeper 서버에서는 준비도가 활성도를 내포한다. 그러므로 zookeeper.yaml 매니페스트에서 준비도 검사는 활성도 검사와 동일하다.

  readinessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5

활성도와 준비도 검사가 동일함에도 둘 다 지정하는 것은 중요하다. 이는 ZooKeeper 앙상블에 건강한 서버만 아니라 네트워크 트래픽을 수신하는 것을 보장한다.

노드 실패 방지

ZooKeeper는 변조된 데이터를 성공적으로 커밋하기 위한 서버의 쿼럼이 필요하다. 3개의 서버 앙상블에서 성공적으로 저장하려면 2개 서버는 반드시 건강해야 한다. 쿼럼 기반 시스템에서, 멤버는 가용성을 보장하는 실패 영역에 걸쳐 배포된다. 중단을 방지하기 위해 개별 시스템의 손실로 인해 모범 사례에서는 동일한 시스템에 여러 인스턴스의 응용 프로그램을 함께 배치하는 것을 배제한다.

기본적으로 쿠버네티스는 동일 노드상에 스테이트풀셋의 파드를 위치시킬 수 있다. 생성한 3개의 서버 앙상블에서 2개의 서버가 같은 노드에 있다면, 그 노드는 실패하고 ZooKeeper 서비스 클라이언트는 그 파드들의 최소 하나가 재스케줄링될 때까지 작동 중단을 경험할 것이다.

노드 실패하는 사건 중에도 중요 시스템의 프로세스가 재스케줄될 수 있게 항상 추가적인 용량을 프로비전해야 한다. 그렇게 하면 쿠버네티스 스케줄러가 ZooKeeper 서버 하나를 다시 스케줄하는 동안까지만 작동 중단될 것이다. 그러나 서비스에서 노드 실패로 인한 다운타임을 방지하려 한다면, 파드안티어피니티를 설정해야 한다.

zk 스테이트풀셋의 파드의 노드를 알아보기 위해 다음 명령어를 이용하자.

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

zk 스테이트풀셋에 모든 파드는 다른 노드에 배포된다.

kubernetes-node-cxpk
kubernetes-node-a5aq
kubernetes-node-2g2d

이는 zk 스테이트풀셋의 파드에 파드안티어피니티(PodAntiAffinity)를 지정했기 때문이다.

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
            - key: "app"
              operator: In
              values:
                - zk
        topologyKey: "kubernetes.io/hostname"

requiredDuringSchedulingIgnoredDuringExecution 필드는 쿠버네티스 스케줄러에 topologyKey로 정의된 도메인에서 appzk라고 레이블링된 두 개 파드가 위치하지 않도록 한다. topologyKey kubernetes.io/hostname은 도메인이 개별 노드임을 나타낸다. 다른 규칙과 레이블, 셀렉터를 사용하여 앙상블을 물리적인, 네트워크, 전원 장애 분야에 걸쳐 확산하도록 이 기법을 확장할 수 있다.

생존 유지

이 섹션에서는 노드를 통제(cordon)하고 비운다(drain). 공유된 클러스터에서 이 튜토리얼을 진행한다면, 다른 테넌트에 부정적인 영향을 비치지 않음을 보증해야 한다.

이전 섹션은 계획되지 않은 노드 실패에서 살아남도록 어떻게 파드를 확산할 것인가에 대해 알아보았다. 그러나 계획된 점검으로 인해 발생하는 일시적인 노드 실패에 대한 계획도 필요하다.

클러스터에서 다음 명령으로 노드를 살펴보자.

kubectl get nodes

이 튜토리얼에서는 클러스터가 최소 4개의 노드로 구성되었다고 가정한다. 클러스터의 노드가 4개보다 많다면, kubectl cordon 명령을 이용하여 4개 노드를 제외하고 다른 모든 노드를 통제(cordon)한다. 이렇게 4개 노드만 사용하도록 제한하여, 다음의 유지보수 시뮬레이션 예시에서 주키퍼 파드를 스케줄링할 때 어피니티와 PodDisruptionBudget 제약이 발생하도록 할 수 있다.

kubectl cordon <노드-이름>

zk-pdb PodDisruptionBudget을 살펴보고자 이 명령어를 이용하자.

kubectl get pdb zk-pdb

max-unavailable 필드는 쿠버네티스가 zk 스테이트풀셋에서 최대 1개의 파드는 언제든지 가용하지 않을 수 있음을 나타낸다.

NAME      MIN-AVAILABLE   MAX-UNAVAILABLE   ALLOWED-DISRUPTIONS   AGE
zk-pdb    N/A             1                 1

한 터미널에서 zk 스테이트풀셋의 파드를 지켜보는 이 명령어를 이용하자.

kubectl get pods -w -l app=zk

다른 터미널에서 현재 스케줄되는 파드의 노드를 살펴보자.

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

출력은 다음과 비슷할 것이다.

kubernetes-node-pb41
kubernetes-node-ixsl
kubernetes-node-i4c4

zk-0파드가 스케줄되는 노드를 통제하기 위해 kubectl drain를 이용하자.

kubectl drain $(kubectl get pod zk-0 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

출력은 다음과 비슷할 것이다.

node "kubernetes-node-group-pb41" cordoned

WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-group-pb41, kube-proxy-kubernetes-node-group-pb41; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-o5elz
pod "zk-0" deleted
node "kubernetes-node-group-pb41" drained

클러스터에 4개 노드가 있기 때문에 kubectl drain이 성공하여 zk-0을 다른 노드로 재스케줄링 된다.

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m

계속해서 스테이트풀셋의 파드를 첫 터미널에서 지켜보고 zk-1 이 스케줄된 노드를 비워보자.

kubectl drain $(kubectl get pod zk-1 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

출력은 다음과 비슷할 것이다.

"kubernetes-node-ixsl" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-ixsl, kube-proxy-kubernetes-node-ixsl; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-voc74
pod "zk-1" deleted
node "kubernetes-node-ixsl" drained

zk-1 파드는 스케줄되지 않는데 이는 zk StatefulSet이 오직 2개 노드가 스케줄되도록 파드를 위치시키는 것을 금하는 PodAntiAffinity 규칙을 포함하였기 때문이고 그 파드는 Pending 상태로 남을 것이다.

kubectl get pods -w -l app=zk

출력은 다음과 비슷할 것이다.

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s

계속해서 스테이트풀셋의 파드를 지켜보고 zk-2가 스케줄된 노드를 비워보자.

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

출력은 다음과 비슷할 것이다.

node "kubernetes-node-i4c4" cordoned

WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
WARNING: Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog; Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4
There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget.
pod/zk-2

kubectl 을 종료하기 위해 CTRL-C를 이용하자.

zk-2를 추출하는 것은 zk-budget을 위반하기 때문에 셋째 노드를 비울 수 없다. 그러나 그 노드는 통제 상태로 남는다.

zk-0에서 온전성 테스트 때에 입력한 값을 가져오는 zkCli.sh를 이용하자.

kubectl exec zk-0 zkCli.sh get /hello

PodDisruptionBudget이 존중되기 때문에 서비스는 여전히 가용하다.

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x200000002
ctime = Wed Dec 07 00:08:59 UTC 2016
mZxid = 0x200000002
mtime = Wed Dec 07 00:08:59 UTC 2016
pZxid = 0x200000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

kubectl uncordon 이용하여 첫 노드의 통제를 풀자.

kubectl uncordon kubernetes-node-pb41

출력은 다음과 비슷할 것이다.

node "kubernetes-node-pb41" uncordoned

zk-1은 이 노드에서 재스케줄된다. zk-1이 Running과 Ready가 될 때까지 기다리자.

kubectl get pods -w -l app=zk

출력은 다음과 비슷할 것이다.

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         12m
zk-1      0/1       ContainerCreating   0         12m
zk-1      0/1       Running   0         13m
zk-1      1/1       Running   0         13m

zk-2가 스케줄된 노드를 비워보자.

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

출력은 다음과 비슷할 것이다.

node "kubernetes-node-i4c4" already cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
pod "heapster-v1.2.0-2604621511-wht1r" deleted
pod "zk-2" deleted
node "kubernetes-node-i4c4" drained

이번엔 kubectl drain 이 성공한다.

zk-2가 재스케줄되도록 두 번째 노드의 통제를 풀어보자.

kubectl uncordon kubernetes-node-ixsl

출력은 다음과 비슷할 것이다.

node "kubernetes-node-ixsl" uncordoned

kubectl drainPodDisruptionBudget과 결합하면 유지보수 중에도 서비스를 가용하게 할 수 있다. drain으로 노드를 통제하고 유지보수를 위해 노드를 오프라인하기 전에 파드를 추출하기 위해 사용한다면 서비스는 혼란 예산을 표기한 서비스는 그 예산이 존중은 존중될 것이다. 파드가 즉각적으로 재스케줄 할 수 있도록 항상 중요 서비스를 위한 추가 용량을 할당해야 한다.

정리하기

  • kubectl uncordon은 클러스터 내에 모든 노드를 통제 해제한다.
  • 반드시 이 튜토리얼에서 사용한 퍼시스턴트 볼륨을 위한 퍼시스턴트 스토리지 미디어를 삭제하자. 귀하의 환경과 스토리지 구성과 프로비저닝 방법에서 필요한 절차를 따라서 모든 스토리지가 재확보되도록 하자.

7 - 서비스

7.1 - 서비스와 애플리케이션 연결하기

컨테이너 연결을 위한 쿠버네티스 모델

지속적으로 실행중이고, 복제된 애플리케이션을 가지고 있다면 네트워크에 노출할 수 있다.

쿠버네티스는 파드가 배치된 호스트와는 무관하게 다른 파드와 통신할 수 있다고 가정한다. 쿠버네티스는 모든 파드에게 자체 클러스터-프라이빗 IP 주소를 제공하기 때문에 파드간에 명시적으로 링크를 만들거나 컨테이너 포트를 호스트 포트에 매핑할 필요가 없다. 이것은 파드 내의 컨테이너는 모두 로컬호스트(localhost)에서 서로의 포트에 도달할 수 있으며 클러스터의 모든 파드는 NAT 없이 서로를 볼 수 있다는 의미이다. 이 문서의 나머지 부분에서는 이러한 네트워킹 모델에서 신뢰할 수 있는 서비스를 실행하는 방법에 대해 자세히 설명할 것이다.

이 튜토리얼은 간단한 nginx 서버를 사용하여 개념을 시연한다.

파드를 클러스터에 노출하기

이 작업은 이전 예시에서 수행해 보았지만, 네트워킹 관점을 중점에 두고 다시 한번 수행해 보자. nginx 파드를 생성하고, 해당 파드에 컨테이너 포트 사양이 있는 것을 참고한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

이렇게 하면 클러스터의 모든 노드에서 접근할 수 있다. 파드를 실행 중인 노드를 확인한다.

kubectl apply -f ./run-my-nginx.yaml
kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-3800858182-jr4a2   1/1       Running   0          13s       10.244.3.4    kubernetes-minion-905m
my-nginx-3800858182-kna2y   1/1       Running   0          13s       10.244.2.5    kubernetes-minion-ljyd

파드의 IP를 확인한다.

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.4]]
    [map[ip:10.244.2.5]]

이제 클러스터의 모든 노드로 ssh 접속하거나 curl과 같은 도구를 사용하여 두 IP 주소에 질의를 전송할 수 있을 것이다. 컨테이너는 노드의 포트 80을 사용하지 않으며 , 트래픽을 파드로 라우팅하는 특별한 NAT 규칙도 없다는 것을 참고한다. 이것은 동일한 containerPort를 사용하여 동일한 노드에서 여러 nginx 파드를 실행하는 것이 가능하고, 또한 서비스에 할당된 IP 주소를 사용하여 클러스터의 다른 파드나 노드에서 접근할 수 있다는 의미이다. 호스트 노드의 특정 포트를 배후(backing) 파드로 포워드하고 싶다면, 가능은 하지만 네트워킹 모델을 사용하면 그렇게 할 필요가 없어야 한다.

만약 궁금하다면 쿠버네티스 네트워킹 모델을 자세히 읽어본다.

서비스 생성하기

이제 우리에게는 플랫(flat)하고 클러스터 전역에 걸치는 주소 공간에서 실행되고 있는 nginx 파드가 있다. 이론적으로는 이러한 파드와 직접 대화할 수 있지만, 노드가 죽으면 어떻게 되는가? 파드가 함께 죽으면 디플로이먼트에서 다른 IP를 가진 새로운 파드를 생성한다. 이 문제를 해결하는 것이 바로 서비스이다.

쿠버네티스 서비스는 클러스터 어딘가에서 실행되는 논리적인 파드 집합을 정의하고 추상화함으로써 모두 동일한 기능을 제공한다. 생성시 각 서비스에는 고유한 IP 주소(clusterIP라고도 한다)가 할당된다. 이 주소는 서비스의 수명과 연관되어 있으며, 서비스가 활성화 되어 있는 동안에는 변경되지 않는다. 파드는 서비스와 통신하도록 구성할 수 있으며, 서비스와의 통신은 서비스의 맴버 중 일부 파드에 자동적으로 로드-밸런싱 된다.

kubectl expose 를 사용해서 2개의 nginx 레플리카에 대한 서비스를 생성할 수 있다.

kubectl expose deployment/my-nginx
service/my-nginx exposed

이것은 다음 yaml 파일을 kubectl apply -f 로 실행한 것과 동일하다.

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

이 사양은 run: my-nginx 레이블이 부착된 모든 파드에 TCP 포트 80을 대상으로 하는 서비스를 만들고 추상화된 서비스 포트에 노출시킨다 (targetPort 는 컨테이너가 트래픽을 수신하는 포트, port 는 추상화된 서비스 포트로 다른 파드들이 서비스에 접속하기위해 사용하는 모든 포트일 수 있다). 서비스의 API 오브젝트를 보고 서비스 정의에서 지원되는 필드 목록을 확인한다. 서비스를 확인한다.

kubectl get svc my-nginx
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
my-nginx   ClusterIP   10.0.162.149   <none>        80/TCP    21s

앞에서 언급한 바와 같이, 서비스 밑에는 여러 파드들이 있다. 이 파드들은 엔드포인트슬라이스(EndpointSlice)를 통해 노출된다. 해당 서비스의 셀렉터는 지속적으로 평가되고, 그 결과는 레이블을 사용하여 서비스에 연결된 엔드포인트슬라이스로 POST된다. 파드가 죽으면, 해당 파드를 엔드포인트로 갖는 엔드포인트슬라이스에서 자동으로 제거된다. 신규 파드가 특정 서비스의 셀렉터에 매치되면 해당 서비스를 위한 엔드포인트슬라이스에 자동으로 추가된다. 엔드포인트를 확인해 보면, IP가 첫 번째 단계에서 생성된 파드의 IP와 동일하다는 것을 알 수 있다.

kubectl describe svc my-nginx
Name:                my-nginx
Namespace:           default
Labels:              run=my-nginx
Annotations:         <none>
Selector:            run=my-nginx
Type:                ClusterIP
IP:                  10.0.162.149
Port:                <unset> 80/TCP
Endpoints:           10.244.2.5:80,10.244.3.4:80
Session Affinity:    None
Events:              <none>
kubectl get ep my-nginx
NAME       ENDPOINTS                     AGE
my-nginx   10.244.2.5:80,10.244.3.4:80   1m

이제 클러스터의 모든 노드에서 <CLUSTER-IP>:<PORT> 로 nginx 서비스를 curl을 할 수 있을 것이다. 서비스 IP는 완전히 가상이므로 외부에서는 절대로 연결되지 않음에 참고한다. 만약 이것이 어떻게 작동하는지 궁금하다면 서비스 프록시에 대해 더 읽어본다.

서비스에 접근하기

쿠버네티스는 서비스를 찾는 두 가지 기본 모드인 환경 변수와 DNS를 지원한다. 전자는 기본적으로 작동하지만 후자는 CoreDNS 클러스터 애드온이 필요하다.

환경 변수

파드가 노드에서 실행될 때 kubelet은 각기 활성화된 서비스에 대해 일련의 환경 변수 집합을 추가한다. 이것은 순서 문제를 야기한다. 이유를 확인하려면 실행 중인 nginx 파드 환경을 점검해야 한다(실제 사용자의 파드 이름은 다를 것이다).

kubectl exec my-nginx-3800858182-jr4a2 -- printenv | grep SERVICE
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443

서비스에 대한 언급이 없다는 것에 참고해야 한다. 이것은 서비스 이전에 레플리카를 생성했기 때문이다. 이 작업을 수행할 때 또 다른 단점은 스케줄러가 두 파드를 모두 동일한 머신에 배치할 수도 있다는 것이며, 이로 인해 전체 서비스가 중단될 수 있다. 두개의 파드를 죽이고 디플로이먼트가 파드를 재생성하기를 기다리는 것으로 이를 정상화 할 수 있다. 이번에는 서비스가 레플리카들 에 존재한다. 이렇게 하면 올바른 환경 변수뿐만 아니라 파드의 스케줄러-수준의 서비스 분배(모든 노드에 동일한 용량이 제공되는 경우)가 된다.

kubectl scale deployment my-nginx --replicas=0; kubectl scale deployment my-nginx --replicas=2;

kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE     IP            NODE
my-nginx-3800858182-e9ihh   1/1       Running   0          5s      10.244.2.7    kubernetes-minion-ljyd
my-nginx-3800858182-j4rm4   1/1       Running   0          5s      10.244.3.8    kubernetes-minion-905m

파드가 죽고 재생성되었기 때문에 다른 이름을 가지는 것을 알 수 있다.

kubectl exec my-nginx-3800858182-e9ihh -- printenv | grep SERVICE
KUBERNETES_SERVICE_PORT=443
MY_NGINX_SERVICE_HOST=10.0.162.149
KUBERNETES_SERVICE_HOST=10.0.0.1
MY_NGINX_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT_HTTPS=443

DNS

쿠버네티스는 DNS 클러스터 애드온 서비스를 제공하며 dns 이름을 다른 서비스에 자동으로 할당한다. 다음 명령어로 이것이 클러스터에서 실행 중인지 확인할 수 있다.

kubectl get services kube-dns --namespace=kube-system
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   ClusterIP   10.0.0.10    <none>        53/UDP,53/TCP   8m

이 섹션의 나머지 부분에서는 수명이 긴 IP의 서비스(my-nginx)와 이 IP 에 이름을 할당한 DNS 서버가 있다고 가정한다. 여기서는 CoreDNS 클러스터 애드온(애플리케이션 이름 kube-dns)을 사용하므로, 표준 방법(예: gethostbyname())을 사용해서 클러스터의 모든 파드에서 서비스와 통신할 수 있다. 만약 CoreDNS가 실행 중이 아니라면 CoreDNS README 또는 CoreDNS 설치를 참조해서 활성화 할 수 있다. 이것을 테스트하기 위해 다른 curl 애플리케이션을 실행한다.

kubectl run curl --image=radial/busyboxplus:curl -i --tty
Waiting for pod default/curl-131556218-9fnch to be running, status is Pending, pod ready: false
Hit enter for command prompt

이제, nslookup my-nginx 를 입력하고 실행한다:

[ root@curl-131556218-9fnch:/ ]$ nslookup my-nginx
Server:    10.0.0.10
Address 1: 10.0.0.10

Name:      my-nginx
Address 1: 10.0.162.149

서비스 보안

지금까지는 클러스터 내부에서만 ngnix 서버에 엑세스 해왔다. 서비스를 인터넷에 공개하기 전에 통신 채널이 안전한지 확인해야 한다. 이를 위해선 다음이 필요하다.

  • https에 대한 자체 서명한 인증서 (신원 인증서를 가지고 있지 않은 경우)
  • 인증서를 사용하도록 구성된 nginx 서버
  • 파드에 접근할 수 있는 인증서를 만드는 시크릿

nginx https 예제에서 이 모든 것을 얻을 수 있다. 이를 위해서는 도구를 설치해야 한다. 만약 설치하지 않으려면 나중에 수동으로 단계를 수행한다. 한마디로:

make keys KEY=/tmp/nginx.key CERT=/tmp/nginx.crt
kubectl create secret tls nginxsecret --key /tmp/nginx.key --cert /tmp/nginx.crt
secret/nginxsecret created
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

그리고 또한 컨피그맵:

kubectl create configmap nginxconfigmap --from-file=default.conf
configmap/nginxconfigmap created
kubectl get configmaps
NAME             DATA   AGE
nginxconfigmap   1      114s

다음은 make를 실행하는데 문제가 있는 경우에 수행해야 하는 수동 단계이다(예시로 windows).

# Create a public private key pair
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=my-nginx/O=my-nginx"
# Convert the keys to base64 encoding
cat /d/tmp/nginx.crt | base64
cat /d/tmp/nginx.key | base64

이전 명령의 출력을 사용해서 다음과 같이 yaml 파일을 생성한다. base64로 인코딩된 값은 모두 한 줄에 있어야 한다.

apiVersion: "v1"
kind: "Secret"
metadata:
  name: "nginxsecret"
  namespace: "default"
type: kubernetes.io/tls
data:
  tls.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURIekNDQWdlZ0F3SUJBZ0lKQUp5M3lQK0pzMlpJTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ1l4RVRBUEJnTlYKQkFNVENHNW5hVzU0YzNaak1SRXdEd1lEVlFRS0V3aHVaMmx1ZUhOMll6QWVGdzB4TnpFd01qWXdOekEzTVRKYQpGdzB4T0RFd01qWXdOekEzTVRKYU1DWXhFVEFQQmdOVkJBTVRDRzVuYVc1NGMzWmpNUkV3RHdZRFZRUUtFd2h1CloybHVlSE4yWXpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBSjFxSU1SOVdWM0IKMlZIQlRMRmtobDRONXljMEJxYUhIQktMSnJMcy8vdzZhU3hRS29GbHlJSU94NGUrMlN5ajBFcndCLzlYTnBwbQppeW1CL3JkRldkOXg5UWhBQUxCZkVaTmNiV3NsTVFVcnhBZW50VWt1dk1vLzgvMHRpbGhjc3paenJEYVJ4NEo5Ci82UVRtVVI3a0ZTWUpOWTVQZkR3cGc3dlVvaDZmZ1Voam92VG42eHNVR0M2QURVODBpNXFlZWhNeVI1N2lmU2YKNHZpaXdIY3hnL3lZR1JBRS9mRTRqakxCdmdONjc2SU90S01rZXV3R0ljNDFhd05tNnNTSzRqYUNGeGpYSnZaZQp2by9kTlEybHhHWCtKT2l3SEhXbXNhdGp4WTRaNVk3R1ZoK0QrWnYvcW1mMFgvbVY0Rmo1NzV3ajFMWVBocWtsCmdhSXZYRyt4U1FVQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjcKTUI4R0ExVWRJd1FZTUJhQUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjdNQXdHQTFVZEV3UUZNQU1CQWY4dwpEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRVhTMW9FU0lFaXdyMDhWcVA0K2NwTHI3TW5FMTducDBvMm14alFvCjRGb0RvRjdRZnZqeE04Tzd2TjB0clcxb2pGSW0vWDE4ZnZaL3k4ZzVaWG40Vm8zc3hKVmRBcStNZC9jTStzUGEKNmJjTkNUekZqeFpUV0UrKzE5NS9zb2dmOUZ3VDVDK3U2Q3B5N0M3MTZvUXRUakViV05VdEt4cXI0Nk1OZWNCMApwRFhWZmdWQTRadkR4NFo3S2RiZDY5eXM3OVFHYmg5ZW1PZ05NZFlsSUswSGt0ejF5WU4vbVpmK3FqTkJqbWZjCkNnMnlwbGQ0Wi8rUUNQZjl3SkoybFIrY2FnT0R4elBWcGxNSEcybzgvTHFDdnh6elZPUDUxeXdLZEtxaUMwSVEKQ0I5T2wwWW5scE9UNEh1b2hSUzBPOStlMm9KdFZsNUIyczRpbDlhZ3RTVXFxUlU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
  tls.key: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2RhaURFZlZsZHdkbFIKd1V5eFpJWmVEZWNuTkFhbWh4d1NpeWF5N1AvOE9ta3NVQ3FCWmNpQ0RzZUh2dGtzbzlCSzhBZi9WemFhWm9zcApnZjYzUlZuZmNmVUlRQUN3WHhHVFhHMXJKVEVGSzhRSHA3VkpMcnpLUC9QOUxZcFlYTE0yYzZ3MmtjZUNmZitrCkU1bEVlNUJVbUNUV09UM3c4S1lPNzFLSWVuNEZJWTZMMDUrc2JGQmd1Z0ExUE5JdWFubm9UTWtlZTRuMG4rTDQKb3NCM01ZUDhtQmtRQlAzeE9JNHl3YjREZXUraURyU2pKSHJzQmlIT05Xc0RadXJFaXVJMmdoY1kxeWIyWHI2UAozVFVOcGNSbC9pVG9zQngxcHJHclk4V09HZVdPeGxZZmcvbWIvNnBuOUYvNWxlQlkrZStjSTlTMkQ0YXBKWUdpCkwxeHZzVWtGQWdNQkFBRUNnZ0VBZFhCK0xkbk8ySElOTGo5bWRsb25IUGlHWWVzZ294RGQwci9hQ1Zkank4dlEKTjIwL3FQWkUxek1yall6Ry9kVGhTMmMwc0QxaTBXSjdwR1lGb0xtdXlWTjltY0FXUTM5SjM0VHZaU2FFSWZWNgo5TE1jUHhNTmFsNjRLMFRVbUFQZytGam9QSFlhUUxLOERLOUtnNXNrSE5pOWNzMlY5ckd6VWlVZWtBL0RBUlBTClI3L2ZjUFBacDRuRWVBZmI3WTk1R1llb1p5V21SU3VKdlNyblBESGtUdW1vVlVWdkxMRHRzaG9reUxiTWVtN3oKMmJzVmpwSW1GTHJqbGtmQXlpNHg0WjJrV3YyMFRrdWtsZU1jaVlMbjk4QWxiRi9DSmRLM3QraTRoMTVlR2ZQegpoTnh3bk9QdlVTaDR2Q0o3c2Q5TmtEUGJvS2JneVVHOXBYamZhRGR2UVFLQmdRRFFLM01nUkhkQ1pKNVFqZWFKClFGdXF4cHdnNzhZTjQyL1NwenlUYmtGcVFoQWtyczJxWGx1MDZBRzhrZzIzQkswaHkzaE9zSGgxcXRVK3NHZVAKOWRERHBsUWV0ODZsY2FlR3hoc0V0L1R6cEdtNGFKSm5oNzVVaTVGZk9QTDhPTm1FZ3MxMVRhUldhNzZxelRyMgphRlpjQ2pWV1g0YnRSTHVwSkgrMjZnY0FhUUtCZ1FEQmxVSUUzTnNVOFBBZEYvL25sQVB5VWs1T3lDdWc3dmVyClUycXlrdXFzYnBkSi9hODViT1JhM05IVmpVM25uRGpHVHBWaE9JeXg5TEFrc2RwZEFjVmxvcG9HODhXYk9lMTAKMUdqbnkySmdDK3JVWUZiRGtpUGx1K09IYnRnOXFYcGJMSHBzUVpsMGhucDBYSFNYVm9CMUliQndnMGEyOFVadApCbFBtWmc2d1BRS0JnRHVIUVV2SDZHYTNDVUsxNFdmOFhIcFFnMU16M2VvWTBPQm5iSDRvZUZKZmcraEppSXlnCm9RN3hqWldVR3BIc3AyblRtcHErQWlSNzdyRVhsdlhtOElVU2FsbkNiRGlKY01Pc29RdFBZNS9NczJMRm5LQTQKaENmL0pWb2FtZm1nZEN0ZGtFMXNINE9MR2lJVHdEbTRpb0dWZGIwMllnbzFyb2htNUpLMUI3MkpBb0dBUW01UQpHNDhXOTVhL0w1eSt5dCsyZ3YvUHM2VnBvMjZlTzRNQ3lJazJVem9ZWE9IYnNkODJkaC8xT2sybGdHZlI2K3VuCnc1YytZUXRSTHlhQmd3MUtpbGhFZDBKTWU3cGpUSVpnQWJ0LzVPbnlDak9OVXN2aDJjS2lrQ1Z2dTZsZlBjNkQKckliT2ZIaHhxV0RZK2Q1TGN1YSt2NzJ0RkxhenJsSlBsRzlOZHhrQ2dZRUF5elIzT3UyMDNRVVV6bUlCRkwzZAp4Wm5XZ0JLSEo3TnNxcGFWb2RjL0d5aGVycjFDZzE2MmJaSjJDV2RsZkI0VEdtUjZZdmxTZEFOOFRwUWhFbUtKCnFBLzVzdHdxNWd0WGVLOVJmMWxXK29xNThRNTBxMmk1NVdUTThoSDZhTjlaMTltZ0FGdE5VdGNqQUx2dFYxdEYKWSs4WFJkSHJaRnBIWll2NWkwVW1VbGc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"

이제 파일을 사용해서 시크릿을 생성한다.

kubectl apply -f nginxsecrets.yaml
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

이제 nginx 레플리카를 수정하여 암호화된 인증서를 사용한 https 서버와 서비스를 실행하고, 두 포트(80과 443)를 노출한다.

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      - name: configmap-volume
        configMap:
          name: nginxconfigmap
      containers:
      - name: nginxhttps
        image: bprashanth/nginxhttps:1.0
        ports:
        - containerPort: 443
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
        - mountPath: /etc/nginx/conf.d
          name: configmap-volume

nginx-secure-app의 매니페스트에 대한 주목할만한 점:

  • 이것은 동일한 파일에 디플로이먼트와 서비스의 사양을 모두 포함하고 있다.
  • nginx 서버 는 포트 80에서 HTTP 트래픽을 443에서 HTTPS 트래픽 서비스를 제공하고, nginx 서비스는 두 포트를 모두 노출한다.
  • 각 컨테이너는 /etc/nginx/ssl 에 마운트된 볼륨을 통해 키에 접근할 수 있다. 이것은 nginx 서버가 시작되기 전에 설정된 것이다.
kubectl delete deployments,svc my-nginx; kubectl create -f ./nginx-secure-app.yaml

이 시점에서 모든 노드에서 nginx 서버에 연결할 수 있다.

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.5]]
node $ curl -k https://10.244.3.5
...
<h1>Welcome to nginx!</h1>

마지막 단계에서 curl에 -k 파라미터를 제공한 방법에 참고한다. 이는 인증서 생성시 nginx를 실행하는 파드에 대해 아무것도 모르기 때문에 curl에 CName 불일치를 무시하도록 지시해야하기 때문이다. 서비스를 생성해서 인증서에 사용된 CName을 서비스 조회시 파드에서 사용된 실제 DNS 이름과 연결했다. 파드에서 이것을 테스트 해보자(단순히 동일한 시크릿이 재사용되고 있으며, 파드는 서비스에 접근하기위해 nginx.crt만 필요하다).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: curl-deployment
spec:
  selector:
    matchLabels:
      app: curlpod
  replicas: 1
  template:
    metadata:
      labels:
        app: curlpod
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: curlpod
        command:
        - sh
        - -c
        - while true; do sleep 1; done
        image: radial/busyboxplus:curl
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
kubectl apply -f ./curlpod.yaml
kubectl get pods -l app=curlpod
NAME                               READY     STATUS    RESTARTS   AGE
curl-deployment-1515033274-1410r   1/1       Running   0          1m
kubectl exec curl-deployment-1515033274-1410r -- curl https://my-nginx --cacert /etc/nginx/ssl/tls.crt
...
<title>Welcome to nginx!</title>
...

서비스 노출하기

애플리케이션의 일부인 경우 원한다면 외부 IP 주소에 서비스를 노출할 수 있다. 쿠버네티스는 이를 수행하는 2가지 방법인 NodePorts와 LoadBalancers를지원한다. 마지막 섹션에서 생성된 서비스는 이미 NodePort 를 사용했기에 노드에 공용 IP가 있는경우 nginx HTTPS 레플리카가 인터넷 트래픽을 처리할 준비가 되어 있다.

kubectl get svc my-nginx -o yaml | grep nodePort -C 5
  uid: 07191fb3-f61a-11e5-8ae5-42010af00002
spec:
  clusterIP: 10.0.162.149
  ports:
  - name: http
    nodePort: 31704
    port: 8080
    protocol: TCP
    targetPort: 80
  - name: https
    nodePort: 32453
    port: 443
    protocol: TCP
    targetPort: 443
  selector:
    run: my-nginx
kubectl get nodes -o yaml | grep ExternalIP -C 1
    - address: 104.197.41.11
      type: ExternalIP
    allocatable:
--
    - address: 23.251.152.56
      type: ExternalIP
    allocatable:
...

$ curl https://<EXTERNAL-IP>:<NODE-PORT> -k
...
<h1>Welcome to nginx!</h1>

이제 클라우드 로드 밸런서를 사용하도록 서비스를 재생성한다. my-nginx 서비스의 TypeNodePort 에서 LoadBalancer 로 변경한다.

kubectl edit svc my-nginx
kubectl get svc my-nginx
NAME       TYPE           CLUSTER-IP     EXTERNAL-IP        PORT(S)               AGE
my-nginx   LoadBalancer   10.0.162.149   xx.xxx.xxx.xxx     8080:30163/TCP        21s
curl https://<EXTERNAL-IP> -k
...
<title>Welcome to nginx!</title>

EXTERNAL-IP 의 IP 주소는 공용 인터넷에서 이용할 수 있는 주소이다. CLUSTER-IP 는 클러스터/프라이빗 클라우드 네트워크 내에서만 사용할 수 있다.

AWS에서는 LoadBalancer 유형은 IP가 아닌 (긴)호스트네임을 사용하는 ELB를 생성한다는 점을 참고한다. 이것은 일반적인 kubectl get svc 의 출력에 맞추기에는 매우 길기 때문에 실제로 이를 보려면 kubectl describe service my-nginx 를 수행해야 한다. 다음과 같은 것을 보게 된다.

kubectl describe service my-nginx
...
LoadBalancer Ingress:   a320587ffd19711e5a37606cf4a74574-1142138393.us-east-1.elb.amazonaws.com
...

다음 내용

7.2 - 소스 IP 주소 이용하기

쿠버네티스 클러스터에서 실행 중인 애플리케이션은 서비스 추상화를 통해서 서로를, 그리고 외부 세계를 찾고 통신한다. 이 문서는 다른 종류의 서비스로 전송된 패킷의 소스 IP에 어떤 일이 벌어지는지와 이 동작을 필요에 따라 어떻게 전환할 수 있는지 설명한다.

시작하기 전에

용어

이 문서는 다음 용어를 사용한다.

NAT
네트워크 주소 변환
소스 NAT
패킷 상의 소스 IP 주소를 변경하는 것. 이 페이지에서는 일반적으로 노드 IP 주소로의 변경을 의미함.
대상 NAT
패킷 상의 대상 IP 주소를 변경하는 것. 이 페이지에서는 일반적으로 파드 IP 주소로의 변경을 의미함.
VIP
쿠버네티스의 모든 서비스에 할당되어 있는 것과 같은, 가상 IP 주소.
Kube-proxy
모든 노드에서 서비스 VIP 관리를 조율하는 네트워크 데몬.

전제 조건

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

이 예시는 HTTP 헤더로 수신한 요청의 소스 IP 주소를 회신하는 작은 nginx 웹 서버를 이용한다. 다음과 같이 생성할 수 있다.

kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.4

출력은 다음과 같다.

deployment.apps/source-ip-app created

목적

  • 간단한 애플리케이션을 다양한 서비스 종류로 노출하기
  • 각 서비스 유형에 따른 소스 IP NAT 의 동작 이해하기
  • 소스 IP 주소 보존에 관한 절충 사항 이해

Type=ClusterIP 인 서비스에서 소스 IP

iptables 모드 (기본값)에서 kube-proxy를 운영하는 경우 클러스터 내에서 클러스터IP로 패킷을 보내면 소스 NAT를 통과하지 않는다. kube-proxy가 실행중인 노드에서 http://localhost:10249/proxyMode 를 입력해서 kube-proxy 모드를 조회할 수 있다.

kubectl get nodes

출력은 다음과 유사하다.

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

한 노드의 프록시 모드를 확인한다. (kube-proxy는 포트 10249에서 수신대기한다.)

# 질의 할 노드의 쉘에서 이것을 실행한다.
curl localhost:10249/proxyMode

출력은 다음과 같다.

iptables

소스 IP 애플리케이션을 통해 서비스를 생성하여 소스 IP 주소 보존 여부를 테스트할 수 있다.

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

출력은 다음과 같다.

service/clusterip exposed
kubectl get svc clusterip

출력은 다음과 같다.

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

그리고 동일한 클러스터의 파드에서 클러스터IP를 치면:

kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm

출력은 다음과 같다.

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

그런 다음 해당 파드 내에서 명령을 실행할 수 있다.

# "kubectl run" 으로 터미널 내에서 이것을 실행한다.
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

그런 다음 wget 을 사용해서 로컬 웹 서버에 쿼리한다.

# "10.0.170.92"를 "clusterip"라는 이름의 서비스의 IPv4 주소로 변경한다.
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

client_address 는 클라이언트 파드와 서버 파드가 같은 노드 또는 다른 노드에 있는지 여부에 관계없이 항상 클라이언트 파드의 IP 주소이다.

Type=NodePort 인 서비스에서 소스 IP

Type=NodePort인 서비스로 보내진 패킷은 소스 NAT가 기본으로 적용된다. NodePort 서비스를 생성하여 이것을 테스트할 수 있다.

kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

출력은 다음과 같다.

service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

클라우드 공급자 상에서 실행한다면, 위에 보고된 nodes:nodeport를 위한 방화벽 규칙을 열어주어야 한다. 이제 위에 노드 포트로 할당받은 포트를 통해 클러스터 외부에서 서비스에 도달할 수 있다.

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

출력은 다음과 유사하다.

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

명심할 것은 정확한 클라이언트 IP 주소가 아니고, 클러스터 내부 IP 주소이다. 왜 이런 일이 발생했는지 설명한다.

  • 클라이언트는 node2:nodePort로 패킷을 보낸다.
  • node2는 소스 IP 주소(SNAT)를 패킷 상에서 자신의 IP 주소로 교체한다.
  • noee2는 대상 IP를 패킷 상에서 파드의 IP로 교체한다.
  • 패킷은 node 1로 라우팅 된 다음 엔드포인트로 라우팅 된다.
  • 파드의 응답은 node2로 다시 라우팅된다.
  • 파드의 응답은 클라이언트로 다시 전송된다.

이를 그림으로 표현하면 다음과 같다.

source IP nodeport figure 01

그림. Source IP Type=NodePort using SNAT

이를 피하기 위해 쿠버네티스는 클라이언트 소스 IP 주소를 보존하는 기능이 있다. service.spec.externalTrafficPolicy 의 값을 Local 로 하면 오직 로컬 엔드포인트로만 프록시 요청하고 다른 노드로 트래픽 전달하지 않는다. 이 방법은 원본 소스 IP 주소를 보존한다. 만약 로컬 엔드 포인트가 없다면, 그 노드로 보내진 패킷은 버려지므로 패킷 처리 규칙에서 정확한 소스 IP 임을 신뢰할 수 있으므로, 패킷을 엔드포인트까지 전달할 수 있다.

다음과 같이 service.spec.externalTrafficPolicy 필드를 설정하자.

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

출력은 다음과 같다.

service/nodeport patched

이제 다시 테스트를 실행해보자.

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

출력은 다음과 유사하다.

client_address=104.132.1.79

엔드포인트 파드가 실행 중인 노드에서 올바른 클라이언트 IP 주소인 딱 한 종류의 응답만 수신한다.

어떻게 이렇게 되었는가:

  • 클라이언트는 패킷을 엔드포인트가 없는 node2:nodePort 보낸다.
  • 패킷은 버려진다.
  • 클라이언트는 패킷을 엔드포인트를 가진 node1:nodePort 보낸다.
  • node1은 패킷을 올바른 소스 IP 주소로 엔드포인트로 라우팅 한다.

이를 시각적으로 표현하면 다음과 같다.

source IP nodeport figure 02

그림. Source IP Type=NodePort preserves client source IP address

Type=LoadBalancer 인 서비스에서 소스 IP

Type=LoadBalancer인 서비스로 보낸 패킷은 소스 NAT를 기본으로 하는데, Ready 상태로 모든 스케줄된 모든 쿠버네티스 노드는 로드 밸런싱 트래픽에 적합하다. 따라서 엔드포인트가 없는 노드에 패킷이 도착하면 시스템은 엔드포인트를 포함한 노드에 프록시를 수행하고 패킷 상에서 노드의 IP 주소로 소스 IP 주소를 변경한다 (이전 섹션에서 기술한 것처럼).

로드밸런서를 통해 source-ip-app을 노출하여 테스트할 수 있다.

kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

출력은 다음과 같다.

service/loadbalancer exposed

서비스의 IP 주소를 출력한다.

kubectl get svc loadbalancer

다음과 유사하게 출력된다.

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

다음으로 이 서비스의 외부 IP에 요청을 전송한다.

curl 203.0.113.140

다음과 유사하게 출력된다.

CLIENT VALUES:
client_address=10.240.0.5
...

그러나 구글 클라우드 엔진/GCE 에서 실행 중이라면 동일한 service.spec.externalTrafficPolicy 필드를 Local로 설정하면 서비스 엔드포인트가 없는 노드는 고의로 헬스 체크에 실패하여 강제로 로드밸런싱 트래픽을 받을 수 있는 노드 목록에서 자신을 스스로 제거한다.

이를 그림으로 표현하면 다음과 같다.

Source IP with externalTrafficPolicy

이것은 어노테이션을 설정하여 테스트할 수 있다.

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

쿠버네티스에 의해 service.spec.healthCheckNodePort 필드가 즉각적으로 할당되는 것을 봐야 한다.

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

출력은 다음과 유사하다.

  healthCheckNodePort: 32122

service.spec.healthCheckNodePort 필드는 /healthz에서 헬스 체크를 제공하는 모든 노드의 포트를 가리킨다. 이것을 테스트할 수 있다.

kubectl get pod -o wide -l app=source-ip-app

출력은 다음과 유사하다.

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

다양한 노드에서 /healthz 엔드포인트를 가져오려면 curl 을 사용한다.

# 선택한 노드에서 로컬로 이것을 실행한다.
curl localhost:32122/healthz
1 Service Endpoints found

다른 노드에서는 다른 결과를 얻을 수 있다.

# 선택한 노드에서 로컬로 이것을 실행한다.
curl localhost:32122/healthz
No Service Endpoints Found

컨트롤 플레인에서 실행중인 컨트롤러는 클라우드 로드 밸런서를 할당한다. 또한 같은 컨트롤러는 각 노드에서 포트/경로(port/path)를 가르키는 HTTP 상태 확인도 할당한다. 엔드포인트가 없는 2개의 노드가 상태 확인에 실패할 때까지 약 10초간 대기한 다음, curl 을 사용해서 로드밸런서의 IPv4 주소를 쿼리한다.

curl 203.0.113.140

출력은 다음과 유사하다.

CLIENT VALUES:
client_address=198.51.100.79
...

크로스-플랫폼 지원

일부 클라우드 공급자만 Type=LoadBalancer 를 사용하는 서비스를 통해 소스 IP 보존을 지원한다. 실행 중인 클라우드 공급자에서 몇 가지 다른 방법으로 로드밸런서를 요청한다.

  1. 클라이언트 연결을 종료하고 새 연결을 여는 프록시를 이용한다. 이 경우 소스 IP 주소는 클라이언트 IP 주소가 아니고 항상 클라우드 로드밸런서의 IP 주소이다.

  2. 로드밸런서의 VIP에 전달된 클라이언트가 보낸 요청을 중간 프록시가 아닌 클라이언트 소스 IP 주소가 있는 노드로 끝나는 패킷 전달자를 이용한다.

첫 번째 범주의 로드밸런서는 진짜 클라이언트 IP를 통신하기 위해 HTTP Forwarded 또는 X-FORWARDED-FOR 헤더 또는 프록시 프로토콜과 같은 로드밸런서와 백엔드 간에 합의된 프로토콜을 사용해야 한다. 두 번째 범주의 로드밸런서는 서비스의 service.spec.healthCheckNodePort 필드의 저장된 포트를 가르키는 HTTP 헬스 체크를 생성하여 위에서 설명한 기능을 활용할 수 있다.

정리하기

서비스를 삭제한다.

kubectl delete svc -l app=source-ip-app

디플로이먼트, 레플리카셋 그리고 파드를 삭제한다.

kubectl delete deployment source-ip-app

다음 내용