블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 

'클라우드 컴퓨팅 & NoSQL/도커 & 쿠버네티스'에 해당되는 글 52

  1. 2019.08.15 쿠버네티스 #25 - Pod 스케쥴링 #3 리소스 부족 관리
  2. 2019.08.11 쿠버네티스 #24 - Pod 스케쥴링 #2 Affinity
  3. 2019.08.04 쿠버네티스 #23 - Pod 스케쥴링 #1
  4. 2019.07.04 쿠버네티스용 Continuous Deployment 툴인 Skaffold #2 (1)
  5. 2019.06.25 쿠버네티스용 Continuous Deployment 툴인 Skaffold
  6. 2019.06.23 쿠버네티스 패키지 매니저 Helm #2-5. Chart 배포 (Repository)
  7. 2019.06.20 쿠버네티스 패키지 매니저 Helm #2-4. Chart Hook
  8. 2019.06.16 쿠버네티스 패키지 매니저 Helm #2-3. Charts (디렉토리 구조)
  9. 2019.06.11 쿠버네티스 패키지 매니저 Helm #2-2. Chart (버전과 릴리즈)
  10. 2019.06.09 쿠버네티스 패키지 매니저 Helm #2-1. Chart
  11. 2019.06.04 쿠버네티스 패키지 매니저 Helm #1 - 개념, 설치
  12. 2019.04.24 서버리스 오픈소스 - knative #2 비동기 처리를 위한 Eventing
  13. 2019.04.23 서버리스 오픈소스 - knative #1 소개 & Serving
  14. 2019.02.19 [팁] 쿠버네티스 StatefulSet에서 Headless 서비스를 이용한 Pod discovery
  15. 2019.02.17 도커 컨테이너 보안 취약점 스캔 도구 Anchore
  16. 2019.02.17 [팁] minikube에서 Loadbalancer type 서비스 테스트 하기
  17. 2019.02.17 [팁] 우분투에서 minikube를 VM 없이 실행하기
  18. 2019.02.14 쿠버네티스 #22 - StatefulSet을 이용한 상태유지 Pod (데이타베이스) 관리하기 1/2 (1)
  19. 2019.02.11 쿠버네티스 - PodDisruptionBudget
  20. 2019.02.10 쿠버네티스 - PodPreset
 

쿠버네티스 고급 스케쥴링

#3 리소스 부족 (Resource starving) 관리


조대협 (http://bcho.tistory.com)


request와 limit의 개념이 있기 때문에 생기는 문제인데, request 된 양에 따라서 컨테이너를 만들었다고 하더라도, 컨테이너가 운영이되다가 자원이 모자르면 limit 에 정의된 양까지 계속해서 리소스를 요청하게 된다.

 컨테이너의 총 Limit의 양이 실제 시스템이 가용한 resource의 양보다 많을 수 있는 경우가 발생한다. 처음 CPU resource가 1 CPU 였는데, limit이 4이면,Pod 가 배포된 후에, 동작하다가 limit 양까지 증가되서 4 CPU가 되어 버릴 수 있다. 이때 node의 물리 CPU가 4 이면, CPU 리소스 부족이 발생할 수 있다. 이렇게 limit의 총량이 물리 resource의 총량 보다 많은 상황을 over committed 상태라고 한다.


Node에서 리소스 부족이 발생하면 쿠버네티스 클러스터는 이 상태를 해결하기 위해서 부족한 리소스를 회수하기 시작한다. 

리소스에 종류에 따라서 다른 동작을 하는데, 크게 Compressible 리소스와 Un-compressible 리소스에 따라 동작 방식이 다르다.

Compressible resource

Compressible resource는 CPU와 같이 할당된 리소스 양을 줄일 수 있는 리소스이다. 리소스 부족이 발생하면, node에 배포된 Pod의 CPU를 현재 사용하고 있는 양에서 초반 생성할때 할당된 request 크기까지 강제적으로 줄여서 리소스 부족을 해결한다. 이를 throttling 이라고 한다

이 경우 문제는 CPU 사용량일 request 이상으로 증가한 것은 그만큼 그 Pod가 애플리케이션을 동작시키기 위해서 CPU가 필요하였기 때문에 추가 할당되었던 것이다. Request 크기 까지 줄이면 할당된 CPU양을 강제적으로 줄이는 것이기 때문에, 성능 저하가 발생할 수 있다.

이런 문제를 예방하려면, Pod를 배포할때 request와 limit 값을 같이 주면, throttling은 발생하지 않지만 정확히 얼마의 CPU를 할당해야 하는지가 중요하다. 그래서 Pod 별로 적정한 CPU양을 계산하기 위해서는 개발/테스트 환경에서 request/limit 값을 서로 다르게 준 상태에서 부하 테스트를 통해서 적정 CPU 양을 찾아낸 후에 그 값을 적용하는 것이 좋다. 

Un-compressible resource

메모리나 로컬 디스크 공간은 compressible resource에 해당하지 않기 때문에, 강제적으로 Throttle을 할 수 없다. 그래서 쿠버네티스 클러스터는 리소스를 수거하기 위해서 리소스 사용량이 많은 Pod를 강제로 종료 시킨다. 

로컬 디스크의 경우에는 Pod를 강제 종료 시키기전에, 사용되지 않은 컨테이너 이미지등 사용되지 않은 공간을 삭제 해서 먼저 공간 확보를 시도하고, 그래도 공간이 모자르면 우선 순위에 따라서 Pod를 하나 삭제 한다. 삭제된 Pod는 컨트롤러 (ReplicaSet 등)에 의해서 관리가 되고 있으면 자동으로 다른 Node에서 생성된다. 

메모리의 경우에는 캐슁되는 이미지공간등이 없기 때문에, 바로 Pod를 우선순위에 따라서 삭제한다. 이를 Eviction이라고 한다.

우선 순위를 결정하는 로직은 복합적인 값들이 관여되는데, PodPriority 등, Pod 의 우선 순위등이 관련되는데, 대체적으로 Pod의 리소스 사용량이 request에 비해서 오버된 양이 가장 큰 Pod를 우선적으로 삭제 한다. 

예를 들어서 request가 3G, 4G 인 Pod가 각각 있고, 사용량이 5G, 4G 이면, request가 3G인 Pod가 사용량이 2G가 초과했고, 4G인 Pod는 초과하지 않았기 때문에, request가 3G인 Pod를 삭제(Evict)하게 된다. 

Vertical Pod Auto-scaler (VPA)

Pod의 Resource(CPU,Memory)  적절 request를 결정하는 다른 방법으로는 Vertical Pod Auto-scaler(VPA)를 사용하는 방법이 있다. 현재 beta 기능인데, 쿠버네티스 1.11 버전 이상에 별도로 추가 설치해야 한다. 

참고 : https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler

VPA는 리소스로 설정해서 Deployment에 적용할 수 있다.


apiVersion: autoscaling.k8s.io/v1beta2

kind: VerticalPodAutoscaler

metadata:

  name: my-app-vpa

spec:

  targetRef:

    apiVersion: "extensions/v1beta1"

    kind:       Deployment

    name:       my-app

  updatePolicy:

    updateMode: "Auto"

출처 : https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler


위의 예제는 my-app 이라는 Deployment에 VPA를 적용한 예제인데, VPA는 Auto 모드와 Manual 모드가 있다. 설정은 updatePolicy.updateMode에서 지정하면 된다. 

Auto 모드로 하면, VPA가 적절한 CPU와 Memory을 측정한 후에, Pod 내의 컨테이너 resource의 request 값을 적절한 값으로 변경해준다. 대신 주의해야할 점은 resource의 request를 변경하는 유일한 방법은 Pod를 리스타트 하는 방법이기 때문에 VPA를 Auto 모드로 적용한 경우 원하지 않은 Pod 리스타트가 발생할 수 있다.


만약에 Manual 모드로 설정하고, Pod를 운영하면, VPA가 직접 request 내용을 변경하지 않고, 적절하게 필요한 request 양을 추천 해준다. VPA를 만들어서 Deployment에 적용한 후 수분 정도 운영하다가 kubetctl get vpa 명령으로 내용을 보면 추천되는 request 양을 알려준다. 

% kubectl get vpa [VPA 이름]  --output yaml

다음은 위의 명령을 실행한 결과 예제이다. 

....

  recommendation:

    containerRecommendations:

    - containerName: my-rec-container

      lowerBound:

        cpu: 25m

        memory: 262144k

      target:

        cpu: 25m

        memory: 262144k

     upperBound:

        cpu: 7931m

        memory: 8291500k


위의 결과에서 출력된 target 값이 추천 resource 양이다. CPU 25m, 메모리 262144k 가 추천되는 값이다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 고급 스케쥴링 기법

#2 Affinity
조대협 (http://bcho.tistory.com)

Affinity

Taint가 Pod가 배포되지 못하도록 하는 정책이라면, affinity는 Pod를 특정 Node에 배포되도록 하는 정책이다. affinity는 Node를 기준으로 하는 Node affinity와, 다른 Pod가 배포된 위치(node) 를 기준으로 하는 Pod affinity 두 가지가 있다. 

Node affinity 

Node affinity는 Pod가 특정 node로 배포되도록 하는 기능이다. 예전에 label에서 설명했던 node selector 도 node의 label과 pod의 selector label이 매칭되는 node에만 배포하도록 하기 때문에, 사실상 Node affinity와 같은 기능을 한다 

Node affinity는 Hard affinity와 Soft affinity가 있다. Node affinity는 Pod가 조건이 딱 맞는 node 에만 배포되도록 하는 기능이고, Soft affinity는 Pod가 조건에 맞는 node에 되도록(반드시가 아니라)이면 배포되도록 하는 기능이다. 앞에서 언급한 node selector는 Hard affinity에 해당한다. 


아래는 Pod 설정 YAML에서 node affinity를 적용한 예제이다. 


pod-with-node-affinity.yaml docs/concepts/configuration  

apiVersion: v1

kind: Pod

metadata:

  name: with-node-affinity

spec:

  affinity:

    nodeAffinity:

      requiredDuringSchedulingIgnoredDuringExecution:

        nodeSelectorTerms:

        - matchExpressions:

          - key: kubernetes.io/e2e-az-name

            operator: In

            values:

            - e2e-az1

            - e2e-az2

      preferredDuringSchedulingIgnoredDuringExecution:

      - weight: 1

        preference:

          matchExpressions:

          - key: another-node-label-key

            operator: In

            values:

            - another-node-label-value

  containers:

  - name: with-node-affinity

    image: k8s.gcr.io/pause:2.0



출처 : From : https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity


requiredDuringSchedulingIgnoredDuringExecution는 Hard affinity 정의 이다. nodeSelectorTerms 부분에, matchExpressions을 사용하여, label set-based selector 문법을 이용하면 된다.

위의 예제는 node에 label key “kubernetes.io/e2e-az-name” 의 값이 eze-az1 이나 eze-az2 인 node를 선택하도록 하는 설정이다. 


Node affinity는 여러 affinity를 동시에 적용할 수 있는데, 위의 예제에서는 두 개의 Affinity를 정의하였다. 두번째 Affinity는 Soft affinity로, preferredDuringSchedulingIgnoredDuringExecution: 으로 정의한다. Soft affinity는 앞서 언급한것과 같이 조건에 맞는 node로 되도록이면 배포될 수 있도록 그 node로 배포 선호도를 주는 기능이다. 이때 weight 필드를 이용해서 선호도를 조정할 수 있는데, weight은 1~100이고, node 의 soft affinity의 weight 값들을 합쳐서 그 값이 높은 node를 우선으로 고려하도록 우선 순위를 주는데 사용할 수 있다. 


특정 Node로 배포되게 하는 Affinity 설정도 있지만, 반대로 특정 Node로 배포되는 것을 피하도록 하는 AntiAffinity라는 설정도 있다. nodeAffinity 대신 nodeAntiAffinity라는 Notation을 사용하면 되고, Affinity와는 다르게 반대로, 조건에 맞는 Node를 피해서 배포하도록 되낟. 

Inter-Pod affinity

Node affinity가 node의 label을 기준으로 Pod가 배포될 node는 선택한다면, Inter pod affinity는 기존에 배포된 Pod를 기준으로 해서, 배포될 node를 결정한다. 

Pod affinity는 데이타 베이스의 Master / Slave pod 가 다른 node 에 배포되도록 하기 위해서 master pod가 배포된 node를 피해서 배포하게 한다던가. 클러스터 시스템에서 클러스터를 이루는 각각의 Pod가 다른 node에 배포도록 하는 등에 전략에 사용할 수 있다. 


Node affinity와 마찬가지로 Hard affinity와 Soft affinity가 있다. Node affinity아 마찬가지로 requiredDuringSchedulingIgnoredDuringExecution 로 hard affinity를,preferredDuringSchedulingIgnoredDuringExecution로 soft affinity를 정의한다.

Node를 선택할때 Inter pod affinity는 node affinity와 다르게 topology key 라는 것을 사용한다. 

Pod affinity는 Pod affinity에 의해서 해당 node를 선택한 후에, 그 node의 label을 하나 선택한다. 선택하는 label은 topology key로 지정하는데, 이 topology key에 매칭 되는 node 들을 배포 대상으로 선택한다. Node affinity와 마찬가지로 특정 Pod와 같이 배포되는 것을 피하는 AntiAffinity 도 있다. Pod affinity는 podAffinity라는 notation을 사용하고, Anti affinity는 PodAntiAffinity라는 Notation을 사용한다. 


개념이 복잡하기 때문에 아래 예제를 보면서 이해해보도록 하자


apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: cloud/zone
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:2.0


출처  https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity



와 같이 Pod 설정 파일이 있다고 했을때, 배포는 다음 그림과 같다. 



  PodAffinity에서 requiredDuringSchedulingIgnoredDuringExecution 로 Hard affinity를 정의하였다. 이렇게 되면, 기존에 배포된 Pod 중에서, key가 “security”이고, value가 “S1”인, Pod1이 배포된 node인 Node 1을 기준으로 하는데, topologyKey로 정의된 cloud/zone label의 값”z1”을 기준 Node인 Node 1에서 읽어서, Node 들 중에 label이 “cloud/zone=z1” 인 Node 들만을 후보로 선택해서, Node 1, Node 2 를 배포 가능 Node 로 선택한다.


Inter-Pod affinity 예제

다른 예제를 보자. 아래 예제는 redis-cache Pod를 3개 배포하는데, 각각 다른 Node에 분산되서 배포되도록 하는 예제이다. 


apiVersion: apps/v1

kind: Deployment

metadata:

  name: redis-cache

spec:

  selector:

    matchLabels:

      app: store

  replicas: 3

  template:

    metadata:

      labels:

        app: store

    spec:

      affinity:

        podAntiAffinity:

          requiredDuringSchedulingIgnoredDuringExecution:

          - labelSelector:

              matchExpressions:

              - key: app

                operator: In

                values:

                - store

            topologyKey: "kubernetes.io/hostname"

      containers:

      - name: redis-server

        image: redis:3.2-alpine


출처 : https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity


PodAntiAffinity를 사용한 예로, requiredDuringSchedulingIgnoredDuringExecution 를 이용한 Hard affinity이다. Pod의 label이 “app=store”가 있는 Pod가 배포되어 있는 Node 중에서 topologyKey가 kubernetes.io/hostname (쿠버네티스에서 자동으로 미리 저장하는 label로 Node의 이름을 정의하는 label 이다.) 로 되어 있기 때문에, node의 kubernetes.io/hostname label 값이 이 Node와 다른 Node를 배포 타겟으로 설정한다. 즉 “app=store” 이름으로 배포된 Pod가 없는 Node에 배포하는 설정이다.

이 Redis pod는 “app=store” 라는 label을 가지고 있는 Pod이기 때문에, 이미 Node에 이 Redis Pod가 배포되어 있으면, 그 Node에는 배포되지 않기 때문에 Redis Pod가 배포되지 않은 다른 Node에 중첩되서 배포되지 않도록 해준다. 


이 상태에서 nginx 서버를 배포해보자. Nginx pod를 3개 배포하는데, 각각을  서로 다른 node에 배포하도록 하되, 대신 redis가 배포된 node에 배포하도록 하는 설정이다.  


apiVersion: apps/v1

kind: Deployment

metadata:

  name: web-server

spec:

  selector:

    matchLabels:

      app: web-store

  replicas: 3

  template:

    metadata:

      labels:

        app: web-store

    spec:

      affinity:

        podAntiAffinity:

          requiredDuringSchedulingIgnoredDuringExecution:

          - labelSelector:

              matchExpressions:

              - key: app

                operator: In

                values:

                - web-store

            topologyKey: "kubernetes.io/hostname"

        podAffinity:

          requiredDuringSchedulingIgnoredDuringExecution:

          - labelSelector:

              matchExpressions:

              - key: app

                operator: In

                values:

                - store

            topologyKey: "kubernetes.io/hostname"

      containers:

      - name: web-app

        image: nginx:1.12-alpine


출처  https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity


먼저 podAntiAffinity로 “app:web-store” Pod가 배포되어 있는 node를 찾은 후에, 그 Node이 kubernetes.io/hostname 를 topologyKey로 해서, 그 node들을 제외한다.

즉 “app:web-store” 라벨을 가지고 있는 Pod 들이 배포된 Node를 제외하는 설정이다. 그런데, 이 Pod는 label을 “app:web-store” 라벨을 가지도록 되어 있기 때문에, 이 Pod가 배포되어 있는 Node에 배포하지 말고, 다른 Node에 분산해서 배포하라는 내용이다.


여기에 PodAffinity로 “app:store”인 Pod가 배포되어 있는 Node에서 topologyKey로 “kubernetes.io/hostname”을 사용하였기 때문에, “app:store”인 라벨을 가지고 있는 Pod가 배포되어 있는 Node를 찾아서 배포하라는 내용이다. 다시 말해서 “app:store”인 Pod와 같은 Node에 배포하라는 의미이다. 


아래는 실행결과 인데, web-server가 각각 다른 Node에 겹치지 않고 분리되서 부탁되어 있는 것을 볼 수 있고, redis-cache Pod도 역시 서로 다른 Node에 겹치지 않게 배포되어 있는 것을 확인할 수 있다. 그리고 web-server와 redis-cache pod들은 하나씩 같은 Node에 배포된것을 확인할 수 있다. 


NAME                           READY STATUS RESTARTS AGE       IP NODE

redis-cache-1450370735-6dzlj   1/1 Running 0 8m        10.192.4.2 kube-node-3

redis-cache-1450370735-j2j96   1/1 Running 0 8m        10.192.2.2 kube-node-1

redis-cache-1450370735-z73mh   1/1 Running 0 8m        10.192.3.1 kube-node-2

web-server-1287567482-5d4dz    1/1 Running 0 7m        10.192.2.3 kube-node-1

web-server-1287567482-6f7v5    1/1 Running 0 7m        10.192.4.3 kube-node-3

web-server-1287567482-s330j    1/1 Running 0 7m        10.192.3.2 kube-node-2

출처  https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 고급 스케쥴링 기법

#1 스케쥴링과 Taint&Toleration

조대협 (http://bcho.tistory.com)

쿠버네티스 스케쥴링

쿠버네티스에서 Pod 를 생성 요청 했을때, Pod를 적정 node에 배치하는 것을 스케쥴링이라고 한다. Pod를 어느 node에 배치할것인가에 대해서는 생각 보다 많은 고려가 필요하다. 먼저 Pod가 생성하기 위한 충분한 리소스 (CPU와 메모리)가 남아 있어야 하고, 디스크 볼륨을 사용할 경우, attach하고자 하는 디스크 볼륨이 해당 node에서 접근이 가능해야 한다.

또한 애플리케이션 특성에 따라서, Pod의 배포에 대해 배려가 필요한 경우도 있다. 예를 들어 MySQL을 HA 모드로 배포하기 위해서 마스터, 슬레이브 노드 각각을 배포하고자 할때, 마스터 슬레이브가 같은 node에 배포되게 되면, 해당 node가 문제가 생기면 마스터,슬레이브 노드 모두가 서비스가 불가능해지기 때문에, HA에 의한 가용성을 지원할 수 없다. 그래서 마스터 슬레이브 노드를 다른 node에 배포해야 하고, 더 나아가 다른 rack, 클라우드의 경우에는 다른 Zone(존)에 배포될 필요가 있다. 

이 모든 것을 제어 하는 것을 스케쥴링이라고 한다. 

이 장에서는 쿠버네티스의 스케쥴링이 어떻게 작동을 하는지 그리고, 이 스케쥴링을 제어할 수 있는 고급 기법에 대해서 알아보고자 한다. 

스케쥴링 작동의 기본 원리

(참고 : 쿠버네티스의 스케쥴링 정책은 이 에 매우 잘 설명되어 있다. )

Pod 생성이 요청 되면, 쿠버네티스 스케쥴러는 먼저 필터라는 것을 이용해서, Pod가 배포될 수 있는 Node를 선정하는 작업을 한다.

크게 보면 세 종류의 필터를 사용하는데, 다음과 같다.

  • 볼륨 필터

  • 리소스 필터

  • 토폴로지 필터

각각을 살펴보자

볼륨필터

Pod를 생성할 때, 생성하고자 하는 Pod의 디스크 볼륨에 대해서 Node가 지원할 수 있는지를 확인한다. 

예를 들어 클라우드에서 생성되는 Pod가 zone 1에 생성된 디스크를 attach해야 하는 조건을 가지고 있을때, 특정 클라우드들의 경우 다른 zone의 디스크를 attach할 수 없기 때문에, zone 1 이외에 있는 Node들을 후보에서 제외하고, 해당 볼륨을 attach할 수 있는 Node 들만 후보로 남긴다.

또는 쿠버네티스에서는 사용자가 볼륨에 node-affinity를 정의해서 특정 node 에만 그 볼륨을 attach할 수 있도록 하는데, 이러한 조건에 부합하지 않는 Node들을 제거하고 후보 Node 리스트를 만든다.

리소스 필터

다음으로 적용되는 필터가 리소스 필터인데, 해당 Node 들이 Pod를 배포할만한 충분한 리소스 (CPU,Memory,Disk)가 있는지를 확인하는 단계이다.

CPU 와 메모리 여유분이 Pod가 요청한 만큼 충분한지, 그리고 Node의 디스크 공간도 확인하는데, 앞에서 언급한 디스크 볼륨과 다소 차이가 있는 것이, Node가 Pod를 실행하기 위해서는 Pod를 실행하기 위한 디스크 공간이 필요하다. Pod 의 컨테이너 이미지를 저장하기 위한 공간등이 이에 해당한다. 

CPU,Memory,Disk 뿐 아니라 네트워크 포트도 체크를 하는데, Pod가 Node 포트를 사용하는 Pod 일 경우, 예를 들어 Pod가  Node의 8080 포트를 사용하고자 하는데, 이미 해당 Node의 8080 포트가 다른 Pod에 의해서 점유된 경우, 새로운 Pod를 생성할 수 없기 때문에 그 Node를 Pod를 생성하기 위한 Pod 리스트에서 제외한다. 


일반적인 경우에는 볼륨 필터와 리소스 필터를 거친 Node들을 후보로 두고 이 중에서 적절한 Node를 선택해서 Pod를 배포한다. 

고급 스케쥴링 정책

Pod를 배포할때, 사용자가 특정 Node를 선택할 수 있도록 정책을 정의할 수 있다. 예를 들어 앞에서 언급한것과 같이 MySQL의 마스터, 슬레이브가 같은 Node에 배포되지 않도록 Pod의 스케쥴링 정책을 인위적으로 조정할 수 있다. 이를 고급 스케쥴링 기법이라고 하는데, 자세한 설명은 이 문서를 참고하기 바란다. 

Taint & Toleration

먼저 살펴볼 스케쥴링 정책은 Taint와 Toleration이다.

Taint는 Node에 정의할 수 있고, Toleration은 Pod에 정의할 수 있는데, 한마디로 쉽게 설명하면, Taint 처리가 되어 있는 Node에는 Pod가 배포되지 않는다. Taint 처리가 되어 있는 Node에는 Taint에 맞는 Toleration을 가지고 있는 Pod 만 배포될 수 있다.

Taint

Taint는 label과 유사하게 <key>=<value>:<effect> 형태로 정의되서 node에 적용된다. key와 value는 사용자가 마음대로 정할 수 있으며, effect는 NoSchedule, PreferNoSchedule,NoExecute 3가지로 정의할 수 있다. NoSchedule은 taint 처리가 되어 있는 node에 대해서는 Pod가 이에 맞는 toleration을 가지고 있다면 이 Node에는 그 Pod를 배포하지 못하도록 막는 effect 이다. (나머지 2가지 effect에 대해서는 뒤에서 설명한다.)

Node에 taint 를 적용하는 방법은 다음과 같다.

%kubectl taint node [NODE_NAME] [KEY]=[VALUE]:[EFFECT]

형태로 적용하면 된다.

예를 들어 gke-terry-gke11-default-pool-317bb64b-21kd Node에 key가 “node-type”이고, value가 “production”이고, Effect가 NoSchedule인 Taint를 적용하고자 하면 다음과 같이 명령을 실행하면 된다. 

 

%kubectl taint node gke-terry-gke11-default-pool-317bb64b-21kd node-type=production:NoSchedule

node/gke-terry-gke11-default-pool-317bb64b-21kd tainted


이렇게 taint를 적용한 후, Taint가 제대로 적용이 되었는지, kubectl get nodes gke-terry-gke11-default-pool-317bb64b-21kd -o yaml 명령을 이용해서 확인해보면 다음과 같이 taint가 적용되어 있는 것을 확인할 수 있다. 


apiVersion: v1

kind: Node

metadata:

: (중략)

   name: gke-terry-gke11-default-pool-317bb64b-21kd

: (중략)

spec:

: (중략)

  taints:

  - effect: NoSchedule

    key: node-type

    value: production


이렇게 Taint 처리가 된 Node는 알맞은 Toleration이 정의되지 않은 Pod는 배포될 수 없다. 


Node에 Taint를 적용하는 방법은 앞에서 설명한 것과 같이 node 이름을 정의해서 하나의 특정 Node에 적용하는 방법도 있지만, node 에 적용된 label을 이용하여, label이 일치 하는 여러개의 node에 동시에도 적용할 수 있다. 

방법은 아래와 같이 -l 옵션을 이용해서 적용하고자 하는 node의 label의 key/value를 적용하면 된다. 

%kubectl taint node -l [LABEL_KEY]=[LABEL_VALUE] [KEY]=[VALUE]:[EFFECT]


예를 들어서 아래와 같이 -l  옵션을 적용하면,

%kubectl taint node -l node-label=zone1 node-type=production:NoSchedule

Node 중에서 label이 node-label=zone1인 모든 node에, node-type=production:NoSchedule 인 Taint가 적용된다. 

Toleration

그러면 Taint 처리가되어 있는 Node에 Pod를 배포하기 위해서 사용하는 Toleration이란 무엇인가?

Toleration처리가 되어 있는 Node에 배포될 수 있는 일종의 티켓과 같은 개념이라고 생각하면 된다. Taint 처리가 되어 있는 Node에 Toleration이라는 티켓을 가지고 있으면, 그 Node에 Pod가 배포될 수 있다. (“배포된다"가 아니라, “배포될 수 있다" 라는 의미에 주의하도록 하자. 그 Node가 아니라 다른 조건에 맞는 Node가 있다면, 배포될 수 있다.)

Toleration의 정의는 Match operator를 사용하여 Pod Spec에 정의한다.

tolerations:

- key: "key"

  operator: "Equal"

  value: "value

  effect: "NoSchedule"


이렇게 정의하면, key,value,effect 3개가 Taint와 일치하는 Node에 Pod가 배포될 수 있다. 

조금 더 광범위하게 정의를 하려면, “Exist”를 사용하면 된다. 

tolerations:

- key: "key"

  operator: "Exists

  effect: "NoSchedule"


이렇게 정의하면, Taint에 위에서 정의한 Key가 있고, effect가 “NoSchedule”로 설정된 Node에 value 값에 상관 없이 배포될 수 있다.

또는 아래와 같이 tolerations 절에서 effect 항목을 제외하면, 해당 key로 Taint가 적용되어 있는 모든 Node에 대해서 이  Pod를 배포하는 것이 가능하다. 

tolerations:

- key: "key"

  operator: "Exists


Taints는 특정 nodes에 일반적인 Pod가 배폭되는 것을 막을 수 있다. 가장 좋은 예로는 쿠버네티스의 마스터 Node에 적용된 Taints가 이에 해당한다. 쿠버네티스 마스터 Node에는 관리를 위한 Pod만이 배포되어야 하기 때문에, 일반적인 Pod를 배포할 수 없도록 Taints가 이미 적용되어 있고, 마스터 Node에 Pod를 배포하기 위해서는 이에 맞는 Toleration을 가지고 있어야 한다. 

이 외에도 운영용 Node로 특정 Node들을 적용해놓고, 개발이나 스테이징 환경용 Pod이 (실수로라도) 배포되지 못하게 한다는 것등에 사용할 수 있다. 

Taint와 Toleration 개념 정리

앞에서 Taint와 Toleration의 개념과 사용법에 대해서 설명하였는데, 이를 이해하기 편하게 그림으로 정리해서 보자




<그림. Taints와 Toleration의 개념>

출처 : https://livebook.manning.com/#!/book/kubernetes-in-action/chapter-16/section-16-3-1


Master node에는 node-role.kubernetes.io/master 라는 key로 value 없이 effect만 “NoSchedule”로 Taint를 정의하였다. toleration을 가지지 못한 일반적인 Pod는 Master node에는 배포될 수 없고, Taint 처리가 되어 있지 않은 regular node에만 배포가 가능한다.

System pod의 경우 node-role.kubernetes.io/master 라는 key로, effect가 “NoSchedule”인 toleration을 가지고 있기 때문에, Taint가 없는 Regular node에는 당연히 배포가 가능하고, Toleration에 맞는 Taint를 가지고 있는 Master node에 배포될 수 있다. 

Taint Effect 

Taint와 Toleration에 대한 사용법과 개념을 이해하였으면, 이제 Taint effect에 대해서 조금 더 자세하게 알아보도록 하자. 앞에서도 설명했듯이 Taint에 적용할 수 있는 effect는 아래와 같이 3가지가 있다. 

  • NoSchedule : Pod가 배포되지 못한다. (Toleration이 일치하면 배포됨)

이 effect로 Taint가 적용된 Node는 일치하는 Toleration을 가지고 있는 Pod가 아닌 경우에는 배포되지 못한다. 단, 이는 새롭게 배포되는 Pod에만 적용되고 이미 배포되어 있는 Pod에는 적용되지 않는다. 다시 말해서, Node 1에 Pod 1이 돌고 있는데, 이 Node 1에 Taint를 적용하면, Taint 적용전에 배포되서 돌고 있는 Pod 1에는 영향을 주지 않는다. Pod 1는 알맞은 Toleration이 없더라도, 종료되서 새롭게 스케쥴링이 되지 않는 이상 Node 1에 배포된 상태로 동작한다. 


만약에 이미 돌고 있는 Pod들에게도 영향을 주려면 NoExecute 라는 effect 를 사용하면 된다. 

  • NoExecute :  돌고 있던 Pod들을 evit 하고(다른 node로 옮김), 새것들은 못들어 오게 한다.

이 effect는 NoSchedule과 유사하지만, 새롭게 배포되는 Pod 뿐만 아니라, 이미 그 Node 에서 돌고 있는 Pod 들에게도 영향을 줘서, NoExecute로 Taint가 적용되면, 이에 해당하는 Toleration을 가지고 있지 않는 Pod는 모두 evict 되서 그 node에서 삭제 된다. 물론 ReplicaSet/Deployment 등 Controller에 의해서 관리되는 Pod의 경우에는 Taint 처리가 되어 있지 않은 다른 Node에서 새롭게 생성된다. 


이 effect에 대해서는 tolerationSeconds 라는 패러미터를 고려해야 하는데, 이 Taint가 적용된 Node에 맞는 toleration 을 가지고 있는 Pod의 경우, 이 Node에 영구적으로 남아 있지만, Pod의 toleration에 tolerationSeconds 패러미터가 정의되어 있으면 이 시간만큼만 남아 있다가 evit 된다. 즉 Pod 1, Pod 2,Pod 3가 Node 1에서 돌다가,  Node 1 에 NoExecute effect로 Taint가 적용되었다고 했을때, Pod 1은 이 Taint에 맞는 Toleration을 가지고 있고

Pod 2는 이 Taint에 맞는 Toleration을 tolerationSeconds=300(초) 패러미터와 함께 정의되어 가지고 있다면

Pod 3는 아마 Toleration이 없다면 

Pod 1은 계속 Node1에 남아 있게 되고,  Pod 2는 Node 2에 300초 동안 남아있다가 evit (강제 종료)되며, Pod 3는 바로 강제 종료가 된다. 


  • PreferNoSchedule : 가급적 Pod 배포하지 않는다. 


마지막으로 소개할 effect는 PerferNoSchedule인데, NoSchedule의 소프트 버전으로 생각히면 된다. NoSchedule로  Taint 처리가 되어 있는 Node 라면, 스케쥴시에, Toleration을 가지고 있지 않은 Pod는 무조건 배포가 불가능하지만, PreferNoSchedule의 경우에는 Toleration이 없는 Pod의 경우에는 되도록이면 배포되지 않지만 리소스가 부족한 상황등에는 우선순위를 낮춰서, Toleration이 없는 Pod도 배포될 수 있도록 한다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스를 위한 CD 툴, Skaffold #2

조대협 (http://bcho.tistory.com)

Skaffold 설정 파일의 구조

Skaffold의 개념과 기본적인 사용법을 이해하였으면, 다음으로 Skaffold 설정 파일에 대해서 알아보도록 하자.

Skaffold의 설정 파일은 아래와 같이 크게 두가지가 있다. 

  • Pipeline config
    우리가 앞에서 살펴본 skaffold.yaml 파일이 파이프라인 설정 파일에 해당한다.
    컨테이너 빌드 및 레지트리 등록, 테스트 및 컨테이너 배포 일련의 파이프라인에 대한 행동을 정의한다. 

  • Global config
    ~/.skaffold/config 파일에 저장되어 있는 정보로 skaffold의 기본 설정 정보를 정의한다. 예를 들어 디폴트 도커 레지스트리 경로등을 정의한다. 

pipeline config (skaffold.yaml)

파이프라인 설정은 앞에서 언급했듯이, 컨테이너 빌드, 빌드된 컨테이너 등록, 테스트 그리고 쿠버네티스 클러스터에 컨테이너와 쿠버네티스 리소스를 배포 하는 파이프라인 흐름을 정의하는데, 크게 build,test,deploy 그리고 profiles 4 가지 영역으로 구분된다. 개념적으로 표현해보면 아래와 같다. 


<그림 skaffold.yaml 파일에서 정의한 파이프라인 별 영역>


각 영역을 살펴보자

build

build는 컨테이너 빌드와, 빌드된 컨테이너에 대한 태깅 그리고 태그된 컨테이너 이미지를 컨테이너 레지트스리에 등록하는 역할을 정의한다


Builder

Skaffold는 각 행위를 정의 하기 위해서 단계별로 사용할 수 있는 컴포넌트들을 정의하는데, 빌드에 관련된 내용은 Builder에 의해서 처리된다.

Builder는 컨테이너 이미지를 빌드 하는 역할을 하는데, 다음 플랫폼 들을 지원한다.

  • 로컬 Docker 를 이용해서 Dockerfile을 기반으로 컨테이너를 빌드

  • Dockerfile을 기반으로 구글 클라우드의 Google Cloud Build를 이용해서 리모트로 빌드

  • Dockerfile을 기반으로 쿠버네티스 클러스터에서 빌드를  할 수 있는 Kaniko 를 이용한 빌드

  • 로컬에서 Bazel(구글이 만든 make와 같은 빌드 스크립트)

  • jlib를 이용하여 maven이나 gradle 프로젝트를 로컬에서 빌드 

  • jlib를 이용해서 Google Cloud Build 를 이용하여 리모트에서 빌드

  • 또는 커스텀 빌드 스크립트를 이용한 빌드

등이 있다. 


Tagger

이렇게 빌드된 이미지들은 컨테이너 레파지토리에 저장되는데, 컨테이너 레파지토리의 경로는 앞에서 언급한 global config 에 의해서 정의 된다.

레파지토리로 컨테이너가 푸쉬 되기 전에, 컨테이너 이미지의 이름에 태그(tag)를 부여해야 하는데, 이는 Tagger에 의해서 실행 된다.

컨테이너의 태그로 사용할 수 있는 것들은 다음과 같다.

  • dateTime : 날짜와 시간을 태그로 사용한다.

  • sha256 : 컨테이너 이미지의 sha256 해쉬 값을 추출하여 태그로 사용한다.

  • 환경변수 : 환경 변수로 정의된 값을 태그로 사용한다. 

  • gitcommit ID : git commit ID를 태그로 사용한다.


추천 하는 방법은 git를 사용하는 경우 가능하면 git commit ID를 사용하는 것이 코드 변경에 대한 컨테이너 이미지에 대한 추적성을 제공하기 때문에 이 방법이 가장좋고, 또는 dateTime을 사용하면, 일자/시간에 따라서 컨테이너 이미지 변경을 추적할 수 있기 때문에 이 방법도 추천한다. 


아래는 Tagger를 이용하여 빌드 과정에서 컨테이너 태그를 dateTime으로 하도록 하는 예제이다. 


apiVersion: skaffold/v1beta11

kind: Config

build:

 tagPolicy:

   dateTime:

     format: "2006-01-02_15-04-05.999_MST"

     timezone: "Local"

 artifacts:

 - image: gcr.io/terrycho-personal/node-example

     : (중략)

이렇게 skaffold.yaml 파일을 작성한 후에, skaffold run을 실행한 결과의 일부를 보면 다음과 같다.


       : (중략)

Successfully built 852d1d5a8f90

Successfully tagged gcr.io/terrycho-personal/node-example:2019-06-26_23-22-58.301_KST

The push refers to repository [gcr.io/terrycho-personal/node-example]

5ba8e7a42d0b: Preparing

520c8d5849cb: Prepari...


컨테이너의 태그 이름이 앞에서 지정한 날짜와 시간형식으로 지정된것을 확인할 수 잇다. 

test

테스트 단계는 그 위에 탑재되는 애플리케이션을 테스트 하지는 않다. 대신 컨테이너가 제대로 만들어 졌는지 특히 파일들이나 디렉토리들은 원하는 대로 제대로 배포되었는지 등을 확인하는데, skaffold에서는 이러한 컨테이너 테스트를 container-structure-test  라는 프레임웍을 사용해서 진행한다. 

deploy

이렇게 만들어지고 테스트된 컨테이너 이미지는 쿠버네티스로 배포하게 된다. 

배포는 Deployer 를 이용해서 쿠버네티스에 배포되는데,

쿠버네티스 디폴트 CLI인 kubectl 뿐만 아니라, helm, kustomize 를 모두 지원한다.  

Helm을 사용하게 되면, Helm Chart를 기반으로 새로운 릴리즈를 만들어서 배포를 한다. 

아래는 skaffold-helm 을 이용해서 배포한 결과인데, Helm 의 새로운 릴리즈가 생성된것을 확인할 수 있다. 


%helm list

NAME         REVISION UPDATED                 STATUS  CHART              APP VERSION NAMESPACE

skaffold-helm 1       Thu Jun 27 00:10:10 2019 DEPLOYED skaffold-helm-0.1.0           default  


Helm과 Skaffold를 이용하면 장점이 Helm의 버전 관리를 이용해서 버전 관리가 간편해지고, 리소스를 삭제할때도 Helm이 의존되는 리소스를 모두 같이 삭제해주기 때문에 깔끔한 배포 및 자원 관리가 가능하다. 



profile

일반적으로 하나의 애플리케이션을 개발하면, 여러환경을 함께 써야 하는 경우가 있다. 예를 들어 단일 코드를 개발,QA,운영 환경등 여러환경에 배포해야 할 경우가 있다. 

아래 예제 그림을 보면, git 의 소스코드를 개발 환경에서는 리파지토리에 저장하지 않고, 로컬의 minikube 클러스터에 배포하도록 해서 개발 테스트를 하고, 운영 환경에서는 빌드된 도커 컨테이너 이미지를 리파지토리에 저장한 다음, 다른 리소스들과 함께 Helm 으로 패키징해서 운영용 쿠버네티스 클러스터에 배포하도록 한다. 




이런 여러 종류의 배포 파이프라인을 하나의 skaffold 설정안에서 관리하기 위해서 skaffold는 profile이라는 개념을 가지고 있다. 

skaffold에서 profile 정의는, 디폴트 설정이 있고, 디폴트 설정과 다른 부분만 profiles 섹션에서 정의하면 된다. 아래는 dev 와 production 두개의 프로파일을 정의한 내용이다. 


apiVersion: skaffold/v1beta11

kind: Config

build:

 artifacts:

 - image: gcr.io/terrycho-personal/node-example

   context: backend

   sync:

     manual:

     # Sync all the javascript files that are in the src folder

     # with the container src folder

     - src: 'src/**/*.js'

       dest: .

profiles:

- name : dev

 build:

   local:

     push: false

- name : production


production은 디폴트 설정과 다르지 않게 설정하였기 때문에, profile 설정 부분에는 별다른 다른 내용을 넣지 않는다. dev에서 build 부분을 로컬 빌드로 하고, 도커 리파지토리로 이미지를 푸쉬하지 않도록 설정하였다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스용 Continuous Deployment 툴인 Skaffold

조대협 (http://bcho.tistory.com)

What is skaffold?

쿠버네티스 기반에서 개발을 하고 테스트를 하려면 일반적으로 다음과 같은 절차를 거쳐야 한다.

  • 소스 코드를 수정한 후, 

  • 수정한 코드를 컴파일 한 다음에

  • 컴파일한 소스 코드를 포함해서 Dockerfile을 이용해서 컨테이너로 패키징 한후에

  • 컨테이너를 레파지토리 새로운 버전 태그를 붙여서 업로드 하고

  • 쿠버네티스의 기존 Deployment나 Pod의 yaml 파일에 image 명을 바꾼후

  • kubectl -f apply 를 이용해서 변경된 파일을 반영하고,

  • 다음 public IP가 있는 서비스의 경우에는 public IP로 접속하고, 아닌 경우에는 SSH 터널링을 이용해서 해당 Pod나 Service에 접근해서 테스트를 한다.

한번의 소스 코드 수정을 하고 나서, 이러한 일련의 과정을 거쳐야 하는데, 코드 수정 하나를 반영하기 위해서 해야 할일들이 많다.

이러한 문제를 해결하기 위해서 코드 수정이 쿠버네티스에 반영되기 까지의 과정을 자동화를 통해서 단순화 해주는 프레임워크가 skaffold 이다. 

아래 그림은 skaffold의 개념인 워크 플로우를 도식화한 그림이다. 


<그림. Skaffold의 워크플로우>

출처 https://skaffold.dev/docs/concepts/


Skaffold가 실행되면, Skaffold는 소스 코드 리포지토리(ex git) 또는 로컬 디렉토리를 모니터링 한다. 소스 코드의 변화가 생기면 자동으로 코드를 빌드하고, 이를 도커 컨테이너로 패키징 한다. 

다음 컨테이너화된 이미지에 태그를 붙이는데, 단순하게 SHA256 해쉬를 생성하거나 또는 날짜나 Git commit ID를 태그로 사용한다. Git commit ID를 태그로 사용할 경우에는 코드 변경 내용이 어느 컨테이너 버전에 반영되어 있는지 추적할 수 있는 추적성을 제공한다.

태깅이된 컨테이너 이미지를 지정되어 있는 컨테이너 리파지토리에 저장하고, 미리 정의되어 있는 쿠버네티스 YAML 파일을 이용하여 변경 내용을 대상 쿠버네티스 클러스터에 반영한다. 이 과정은 개발자가 코드 변경만 하게 되면 전 과정이 자동으로 진행되기 때문에, 개발 환경으로의 코드 반영에 대해서 수동 작업이 필요없고, Skaffold를 종료 시키게 되면, 개발을 위해서 생성된 쿠버네티스 리소스 (Service, Pod 등)을 자동으로 삭제해주기 때문에, 매우 쾌적한 개발환경에서 개발이 가능하다.


원래 컨셉 자체는 CD(Continuous Deployment)의 용도이지만, 로컬 환경에서 변경된 소스코드를 바로 쿠버네티스에 배포 해주기 때문에 특히나 개발 환경 구축에 편리한 장점을 가지고 있다.

Hello Skaffold

상세한 개념 이해에 앞서서 먼저 간단한 예제를 통해서 어떻게 동작하는지 살펴보도록 하자

준비

예제 환경에는 쿠버네티스 클러스터가 이미 하나가 있어야 하고, kubectl이 로컬(랩탑)에 설치되어 쿠버네티스 클러스터와 연결이 되어 있는 상태이어야 한다. 

설치

설치 방법은 공식 문서 https://skaffold.dev/docs/getting-started/ 를 참고하면된다.

MAC의 경우에는 아래와 같이 curl 명령어를 이용해서 바이너리를 다운로드 받은 후 사용하면 된다.


curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-darwin-amd64

chmod +x skaffold

sudo mv skaffold /usr/local/bin


예제코드 다운로드

예제 코드는 GoogleContainerTools 깃허브에서 다운로드 받을 수 있다.


%git clone https://github.com/GoogleContainerTools/skaffold


예제코드는 examples 디렉토리에 있는데, 그 안에 node 폴더의 내용을 이용하도록 한다.

%cd examples/node


예제 디렉토리 안에는 다음과 같은 파일들이 있다.

  • backend/Dockerfile :

  • backend/package-lock.json , package.json

  • backend/src/ : node.js 소스 코드

  • k8s/ : 쿠버네티스 리소스 배포용 yaml 파일

  • skaffold.yaml : Skaffold 워크플로우를 정의한  yaml 파일


node.js 소스 코드

src/index.js 는 node.js 소스 코드로, node.js express 웹 프레임웍을 이용해서 “Hello World!” 문자열을 리턴하는 웹 서버를 3000 번 포트로 기동 시키는 코드이다.


const express = require('express')

const { echo } = require('./utils');

const app = express()

const port = 3000


app.get('/', (req, res) => res.send(echo('Hello World!')))


app.listen(port, () => console.log(`Example app listening on port ${port}!`))

소스 코드 이외에 필요한 패키지들을 package.json에 정의되어 있다.

Dockerfile

Dockerfile의 내용은 다음과 같다. 


FROM node:10.15.3-alpine


WORKDIR /app

EXPOSE 3000

CMD ["npm", "run", "dev"]


COPY package* ./

RUN npm install

COPY . .


알파인 리눅스 기반의 node.js 10.15-3 이미지를 베이스 이미지로 하고, /app 디렉토리에 package.json 과 기타 소스코드를 복사 한후에, npm install로 의존성 패키지를 설치하고, package.json에 지정된 nodemon src/index.js 명령을 이용해서 앞에서 작성한 index.js node.js 코드를 실행하도록 한다. 

그리고 EXPOSE 3000 명령어를 이용해서 도커 컨테이너에서 3000번 포트로 Listen을 하도록 한다.

쿠버네티스 설정 파일

그러면 앞에서 만들어진 컨테이너로 쿠버네티스에서 서빙을 하기 위해서 쿠버네티스 리소스를 정의해야 하는데, ./k8s/deployment.yaml에서  Service와, Deployment를 정의 하도록 하였다.


apiVersion: v1

kind: Service

metadata:

  name: node

spec:

  ports:

  - port: 3000

  type: LoadBalancer

  selector:

    app: node

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: node

spec:

  selector:

    matchLabels:

      app: node

  template:

    metadata:

      labels:

        app: node

    spec:

      containers:

      - name: node

        image: gcr.io/terrycho-personal/node-example

        ports:

        - containerPort: 3000


여기서 컨테이너 리포지토리의 경로는 자신이 사용하는 경로로 변경해야 한다. 이 예제에서는 구글 클라우드 리포지토리 (GCR)에 저장하도록  경로를 지정하였다.

Skaffold 설정 파일

다음으로 skaffold의 설정 파일을 보자.


apiVersion: skaffold/v1beta11

kind: Config

build:

  artifacts:

  - image: gcr.io/terrycho-personal/node-example

    context: backend

    sync:

      manual:

      # Sync all the javascript files that are in the src folder

      # with the container src folder

      - src: 'src/**/*.js'

        dest: .


컨테이너 이미지의 경로를 정해놓고, 소스 코드 파일을 어떻게 sync 할지를 지정한다. sync.manual 로 로컬 디렉토리의 src/**/*.js 파일이 변경이 되면 이를 반영하도록 지정하였다. 

실행

그러면 해당 파일을 실행해보도록 하자 skaffold dev 명령어를 실행하면 위의 파일들을 기반으로 컨테이너화를 하고, deployment.yaml 을 이용해서 쿠버네티스 리소스 (Service, Deployment)를 생성 한후, 컨테이너를 배포하여 서비스할 수 있는 형태로 준비한다. 

이때 --port-foward 옵션을 줄 수 있는데, 위의 예제의 경우에는 Service가 있고, Service 타입이 Load balancer이기 때문에, External IP를 가질 수 있어서, 쿠버네티스에서 배정된 External Service를 통해서 Pod에 접근해 서비스가 가능하지만, 일반적으로 개발을 할때는 Service 까지 배포할 필요가 없고 단순하게 Pod만 배포하고 싶은 경우가 있다. (단순하게 작업하기 위해서)

이때 --port-forward 옵션을 주면 Pod에서 외부로 오픈된 포트로 로컬 환경(랩탑) 포트 Forwarding을 해준다.

즉 이 예제에서는 로컬(랩탑)에서 localhost:3000번을 Pod의 3000번 포트로 포트 포워딩을 해주게 된다.  이 옵션을 추가해서 실행하게 되면 결과는 아래와 같이 된다. 


%skaffold dev --port-forward

Generating tags...

 - gcr.io/terrycho-personal/node-example -> gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a

Tags generated in 91.565177ms

Starting build...

Building [gcr.io/terrycho-personal/node-example]...

Sending build context to Docker daemon  101.4kB

Step 1/7 : FROM node:10.15.3-alpine

 ---> 56bc3a1ed035

Step 2/7 : WORKDIR /app

 ---> Running in d4579ab15f5a

Removing intermediate container d4579ab15f5a

 ---> 7c725818faae

Step 3/7 : EXPOSE 3000

 ---> Running in e05be7cb896b

Removing intermediate container e05be7cb896b

 ---> 49ac353388c6

Step 4/7 : CMD ["npm", "run", "dev"]

 ---> Running in 52b216a93e63

Removing intermediate container 52b216a93e63

 ---> 3b7878ac4c42

Step 5/7 : COPY package* ./

 ---> 457e460aae8d

Step 6/7 : RUN npm install

 ---> Running in ed391e236197


> nodemon@1.18.7 postinstall /app/node_modules/nodemon

> node bin/postinstall || exit 0


Love nodemon? You can now support the project via the open collective:

 > https://opencollective.com/nodemon/donate


npm WARN backend@1.0.0 No description

npm WARN backend@1.0.0 No repository field.

npm WARN backend@1.0.0 No license field.

npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules/fsevents):

npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})


added 265 packages from 161 contributors and audited 2359 packages in 5.381s

found 1 high severity vulnerability

  run `npm audit fix` to fix them, or `npm audit` for details

Removing intermediate container ed391e236197

 ---> f7a7217f7a60

Step 7/7 : COPY . .

 ---> 5b713462d7ac

Successfully built 5b713462d7ac

Successfully tagged gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a

The push refers to repository [gcr.io/terrycho-personal/node-example]

5723b6d27633: Preparing

4f0634afaa8a: Preparing

51a09ffa90b0: Preparing

4789f3ba672b: Preparing

28bf756b6f8e: Preparing

4c299e1e70d5: Preparing

f1b5933fe4b5: Preparing

4c299e1e70d5: Waiting

f1b5933fe4b5: Waiting

28bf756b6f8e: Layer already exists

4c299e1e70d5: Layer already exists

f1b5933fe4b5: Layer already exists

51a09ffa90b0: Pushed

4789f3ba672b: Pushed

5723b6d27633: Pushed

4f0634afaa8a: Pushed

v0.32.0-28-g6bd1d50a: digest: sha256:bccf087827dd536481974eb28465ce4ce69cf13121589e4a36264ef2279e9d1d size: 1786

Build complete in 27.759843507s

Starting test...

Test complete in 17.963µs

Starting deploy...

kubectl client version: 1.11

kubectl version 1.12.0 or greater is recommended for use with Skaffold

service/node created

deployment.apps/node created

Deploy complete in 1.606308148s

Watching for changes every 1s...

Port Forwarding node-6545db86c5-wkdc4/node 3000 -> 3000

[node-6545db86c5-wkdc4 node] 

[node-6545db86c5-wkdc4 node] > backend@1.0.0 dev /app

[node-6545db86c5-wkdc4 node] > nodemon src/index.js

[node-6545db86c5-wkdc4 node] 

[node-6545db86c5-wkdc4 node] [nodemon] 1.18.7

[node-6545db86c5-wkdc4 node] [nodemon] to restart at any time, enter `rs`

[node-6545db86c5-wkdc4 node] [nodemon] watching: *.*

[node-6545db86c5-wkdc4 node] [nodemon] starting `node src/index.js`

[node-6545db86c5-wkdc4 node] Example app listening on port 3000!


태그를 생성해서 gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a 이라는 이름으로 컨테이너를 만들어서 쿠버네티스에 배포하고 Port Forwarding node-6545db86c5-wkdc4/node 3000 -> 3000 에서 보는 것과 같이 Pod  node-6545db86c5-wkdc4의 3000 번 포트로 로컬 포트 3000번을 포워딩 하였다. 


kubectl 명령어를 이용해서 Deployment, Pod 그리고 Service들이 제대로 배포 되었는지를 확인해보면 다음과 같다. 


%kubectl get deploy

NAME                    DESIRED CURRENT UP-TO-DATE   AVAILABLE AGE

node                    1 1 1         1 3m


%kubectl get svc

NAME                               TYPE CLUSTER-IP EXTERNAL-IP      PORT(S) AGE

node                               LoadBalancer 10.23.254.227 35.187.196.76    3000:30917/TCP 3m


%kubectl get pod

NAME                                     READY STATUS RESTARTS AGE

node-6545db86c5-wkdc4                    1/1 Running 0 17m


모든 자원들이 제대로 올라온것을 확인했으면 해당 Pod에 localhost:3000 터널을 이용해서 접속해 보자. 



이 상태에서 src/index.js 내용을 에디터를 이용해서 변경해서 저장하면, skaffold 가 파일이 변경되었다는 것을 인지하고, 도커 컨테이너 빌드와 재 배포를 자동으로 수행한다. 아래는 index.js에서 “Hello World 2!”를 출력하도록 소스코드를 수정하여 저장한 후, 수정 내용이 반영된 결과이다. 


중지

개발을 끝내고 싶을 때는 실행중인 skaffold만 중지하면, 아래와 같이 개발을 위해서 생성했던 이미지와 쿠버네티스 리소스 (Deployment, Service, Pod)등을 클린업 해주기 때문에 깔끔하게 개발했던 환경을 정리할 수 있다. 


^CPruning images...

untagged image gcr.io/terrycho-personal/node-example@sha256:bccf087827dd536481974eb28465ce4ce69cf13121589e4a36264ef2279e9d1d

deleted image sha256:5b713462d7ac7ebb468a1f850fa470c34f7b3aaa91ecb9a98b768e892c7625af

deleted image sha256:164f2f7ad057816d45349ffc9e04c361018cb8ce135accdc437f9c7b0cec7460

untagged image gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a

untagged image gcr.io/terrycho-personal/node-example@sha256:608ceeae6724e84d30ea61fcfedbaf94aedd3fb6dfbf38c97d244af8c65cbe54

deleted image sha256:d13071d2094092d50236db037ac7e75efca479ed899ec09e3a9a0b61789d93da

deleted image sha256:f9ff5b561a440490345672fe892eeedcd28e05dc01c4675fea8cf5f465b87898

untagged image gcr.io/terrycho-personal/node-example:v0.32.0-28-g6bd1d50a-dirty

untagged image gcr.io/terrycho-personal/node-example@sha256:0ba775dee9f33dc74cea1158f2bc85190649b8102991d653bc73a8041c572600

deleted image sha256:18c830d66ee7c7d508e4edef2980bd786d039706e9271bd2f963a4ca27349950

deleted image sha256:98d168285f8df5c00d7890d1bb8fcc851e8a9d8f620c80f65c3f33aecb540d28

untagged image gcr.io/terrycho-personal/node-example@sha256:0bb49fc5b4ff6182b22fd1034129aed9d4ca5dde3f9ee2dd64ccd06d68d7c0f8

deleted image sha256:a63e7f73658f4f69d7a240f1d6c0fa9de6e773e0e10ab869d64d6aec37448675

deleted image sha256:7e59e5acb2511a5a5620cf552c40bdcba1613b94931fe2800def4c6c6ac6b2a6

deleted image sha256:f7a7217f7a60b23ee04cf50a9bac3524d45d2fd14b851feedd3dc6ffac8b953f

deleted image sha256:b3f0702d41a7df01932c3d3f0ab852ceae824d8acc14f70b7d38e694caa67dc1

deleted image sha256:457e460aae8d185410b47b0a62e027a7d09c7e876cdfaa0c06bcaf6069812974

deleted image sha256:f8e24fe06e6e2c04b5bf29f6eb748d99107781333dc229d03ff77865c1937f8c

deleted image sha256:3b7878ac4c42f541efb80af06415bec286254912f8c3a64e671e7f2f3808b91b

deleted image sha256:49ac353388c6c1e248ac41262ffef47a26ada4d28955e90308c575407f316c64

deleted image sha256:7c725818faaeb6db8d5650e037dc4fa0f38a95bc858ea396f729dfa9ccdc1e06

deleted image sha256:ea8f04b01ca5be4b4fda950557526c3a0707ebda06d0e9392501cba21cc17327

WARN[1877] builder cleanup: pruning images: Error: No such image: sha256:18c830d66ee7c7d508e4edef2980bd786d039706e9271bd2f963a4ca27349950 

Cleaning up...

deployment.apps "node" deleted

Cleanup complete in 2.677585866s

There is a new version (0.32.0) of Skaffold available. Download it at https://storage.googleapis.com/skaffold/releases/latest/skaffo


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 패키지 매니저 Helm

#2-5 Helm Chart 배포

조대협 (http://bcho.tistory.com)


Helm 차트를 작성했으면, 다른 사용자들이 사용하기 쉽게 차트를 차트 리포지토리 (Chart repository)에 배포할 수 있다. 

Helm 파일 패키징

파일을 배포하기 위해서는 먼저 차트 파일들을 *.tgz 파일 형태로 패키징해야 하는데, helm package 명령을 사용하면 된다. 

%helm package [차트 디렉토리] 

형태로 사용하면 된다. 앞의 예제인 helloworld 차트를 패키징 하려면 아래와 같은 명령을 사용하면 된다.

%helm package ./helloworld

Successfully packaged chart and saved it to: /home/terrychol/31.helm/helloworld-0.1.0.tgz


만약에 패키지된 파일에 대한 무결성을 보장하기 위해서(패키지된 파일이 변조되지 않음을 보장하는 방법) 패키지파일에 키로 사이닝을 하는 방법이 있다. helm package --sign … 옵션을 이용해서 사이닝을 한다. 패키지에 사이닝을 하면 *.prov 파일 (provenance file) 이 생성되고, 차트 패키지를 설치할때 helm install --verify 옵션을 이용하면 이 provenance 파일을 이용해서 파캐지의 무결성 (변조가 되었는지)을 확인한 후, 변조되지 않은 경우에만 설치를 한다. 

원리 자체는 PKI(비대칭키) 알고리즘을 이용해서 패키지에 사이닝 한후에, 차트를 인스톨할때 사이닝을 확인하여 패키지 변조 여부를 파악하는 방식이다. 

자세한 설정 방법은  https://helm.sh/docs/developing_charts/#helm-provenance-and-integrity

문서를 참고하기 바란다. 

Helm Chart repository server

패키징된 차트패키지 파일을 서버에 배포해야하는데, 서버는 일반적은 HTTP 서버면 모두 사용이 가능하다. github,일반 웹서버, AWS S3, Google Cloud Storage(aka GCS)등이 모두 가능한데, 디렉토리 구조면 repository server 구조에 맞춰서 저장해놓으면 된다.


리포지토리 서버의 디렉토리 구조는 다음과 같다.


charts/

  

  |- index.yaml

  

  |- alpine-0.1.2.tgz

  

  |- alpine-0.1.2.tgz.prov


  • *.tgz 파일은 차트 패키지 파일이고

  • *.prov 파일은 차트 패키지에 대한 provenance 파일이다.

  • 그리고 index.yaml에 리파지토리에 있는 패키지들에 대한 정보를 저장한다.


아래는 index.yaml 파일 샘플이다.


apiVersion: v1

entries:

  helloworld:

  - apiVersion: v1

    appVersion: "1.0"

    created: 2019-06-19T15:37:08.158097657+09:00

    description: A Helm chart for Kubernetes

    digest: f7fcd1078546939bd04b4f94282fb15b3d8d4c422e61b5b03b7e4061c1b61037

    name: helloworld

    urls:

    - http://127.0.0.1:8879/helloworld-0.1.0.tgz

    version: 0.1.0

  helloworld2:

  - apiVersion: v1

    appVersion: "1.0"

    created: 2019-06-19T15:37:08.158803299+09:00

    description: A Helm chart for Kubernetes

    digest: 69510159a58a1c5c228b6870546b67d852b09d299b36d4617bf9e9b971be01fd

    name: helloworld2

    urls:

    - http://127.0.0.1:8879/helloworld2-0.1.0.tgz

    version: 0.1.0

generated: 2019-06-19T15:37:08.15656402+09:00


helloworld와 helloworld2 가 포함되어 있고, 패키지 URL은 http://127.0.0.1:8879/ 이다.

간단하게 Helm 차트 리파지토리를 띄우는 방법은 패키지 (*.tgz)이 있는 디렉토리에서 helm serve 명령을 이용하면 디폴트로 현재 디렉토리에 있는 패키지들을 이용하여 index.yaml 파일을 자동으로 생성하고, 이를 8879 포트를 이용해서 서빙한다.

아래는 helm serve 명령을 이용해서 현재 디렉토리 “.”를 패키지 디렉토리로 해서 차트 리파지토리 서버를 기동한 결과이다.


%helm serve --repo-path .

Regenerating index. This may take a moment.

Now serving you on 127.0.0.1:8879



위의 명령을 사용하면 자동으로 현재 디렉토리에 있는 패키지 파일 (*.tgz)을 읽어서 index.yaml을 자동으로 생성해서 repository 서비스를 제공한다.

helm serve를 이용하는 것이 아니라 웹서버등을 이용할 경우에는 index.yaml을 별도로 생성해줘야 하는데, helm repo index 라는 명령을 이용하면 된다.


%helm repo index [디렉토리명]


을 실행하면, [디렉토리명]에 있는 helm 패키지 파일들에 대한 index.yaml을 생성한다. 이때 웹서버의 URL을 정해줄 수 있는데, 


%helm repo index [디렉토리명] --url [http or https URL for repository]


--url 옵션으로 웹서버의 URL을 주면, index.yaml에서 패키지 경로에 웹서버의 경로를 붙여준다.


%helm repo index . --url https://bwcho75.github.io/my-repo

apiVersion: v1

entries:

  helloworld:

  - apiVersion: v1

    appVersion: "1.0"

    created: 2019-06-19T18:16:29.31329401+09:00

    description: A Helm chart for Kubernetes

    digest: f7fcd1078546939bd04b4f94282fb15b3d8d4c422e61b5b03b7e4061c1b61037

    name: helloworld

    urls:

     https://bwcho75.github.io/my-repo/helloworld-0.1.0.tgz

    version: 0.1.0

  helloworld2:

  - apiVersion: v1

    appVersion: "1.0"

    created: 2019-06-19T18:16:29.314605303+09:00

    description: A Helm chart for Kubernetes

    digest: 69510159a58a1c5c228b6870546b67d852b09d299b36d4617bf9e9b971be01fd

    name: helloworld2

    urls:

     https://bwcho75.github.io/my-repo/helloworld2-0.1.0.tgz

    version: 0.1.0

generated: 2019-06-19T18:16:29.311471188+09:00


다음에 helm 클라이언트에서 이 repository를 사용하도록 하려면,이 repository를  리스트에 추가해야 한다. 

명령을 helm repo add [리파지토리 이름] [URL] 식으로 지정하면 된다.

리파지토리 이름은 사용자가 임의적으로 정하는 이름이고 URL은 Helm 리자지토리의 http URL 이다. 

아래는 myrepo라는 이름으로, http://localhost:8879 서버를 등록하는 방법이다. 

helm repo add myrepo http://localhost:8879

"myrepo" has been added to your repositories


팀내나 아니면 작은 시스템을 위한 Helm repository 라면, helm serv,git (or github) 또는 간단한 웹서버 정도로도 repository 운영이 가능하다. 새로운 차트의 등록은 위에 처럼  그러나 큰 규모로 운영을 하거나 외부에 까지 repository를 오픈할 경우에는 사용자 인증등 별도의 보안 기능이 있고, 매번 index.yaml을 재생성하는게 아니라, 추가 삭제할 수 있는 repository를 사용하는 것을 권장한다. 

ChartMuseum

Charmusem (https://chartmuseum.com) 은 오픈소스 Helm Chart Repository 서버이다. 인증 기능을 제공할 뿐만 아니라 파일 스토리지를 AWS S3, 구글 GCS등을 백앤드로 사용할 수 있다. 


기본 설치 및 사용은 도커로 패키징 되어있는 이미지를 사용하면 된다.

docker run --rm -it \

  -p 8080:8080 \

  -v $(pwd)/charts:/charts \

  -e DEBUG=true \

  -e STORAGE=local \

  -e STORAGE_LOCAL_ROOTDIR=/charts \

  chartmuseum/chartmuseum:v0.8.1


Helm repository 서버이외에도, Chartmuseum은 추가적으로 필요한 기능에 대해서 오픈소스로 제공하고 있다. 

이중에서 주의 깊게 볼만한것은 chartmuseum/ui 와 chartmuseum/helm-push 인데, ui는 chartmuseum 에 대한 웹 인터페이스를 제공한다.

<그림. Chartmuseum ui 웹 화면 >

Chartmuseum push는 CLI도구로, 로컬에 있는 Helm 차트 패키지를 Chartmuseum 에 설치할 수 있는 기능이다. Helm 클라이언트가 깔려 있는 로컬 환경(PC나 노트북)에 인스톨 해서 사용한다.

로컬환경에 설치를 한후에, 차트를 Chartmuseum repository에 차트를 저장하려면 

%helm push [차트디렉토리] [repository 서버명]

으로 실행하면 된다. [차트 디렉토리]는 차트 파일이 들어있는 디렉토리이고 [repository 서버명] 은 helm repo add로 등록한 repository 이다.  

%helm push ./helloworld chartmuseum

지금까지, Helm에 대해서 알아보았다. Helm 은 쿠버네티스를 사용할때, 같이 많이 사용되는 솔루션이고 특히 쿠버네티스에 애플리케이션 설정 및 배포 관점에서 매우 유용하다. 물론 전체 CI/CD 파이프라인을 모두 만들 수 는 없지만, Spinnaker나 Jenkins X 등의 툴과 함께 전체 CI/CD 파이프라인의 중요한 요소로서 사용된다. 



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 패키지 매니저 Helm

#2-4 Helm Chart Hook


조대협 (http://bcho.tistory.com)

Hook은 차트 설치나, 삭제와 같이 차트의 라이프 사이클 중에, 차크 개발자가 동작을 추가해줄 수 있도록 해주는 기능이다. mySQL을 차트로 설치한 후에, mySQL에 테이블을 생성하고 데이타를 로딩하거나, 차트로 Pod를 설치하기전에 Configmap이나 Secret 의 값을 세팅해놓는 것과 같은 작업을 예를 들 수 있다. 

등을 들 수 있다.


Hook 으로 실행되는 리소스는 따로 있는 것이 아니라, 기존의 쿠버네티스 리소스 (Job, Cron Job 등)에 metadata.annotations.”helm.sh/hook”  으로 태그를 달아주면, 이 리소스들은 Hook으로 정의되어 차트 인스톨 전후에 정해진 시점에 실행된다. 

아래는 차트 인스톨 전에  작업을 처리하도록 Hook을 정의한 예제이다. 


apiVersion: ...

kind: ....

metadata:

  annotations:

    "helm.sh/hook": "pre-install"

# ...


예를 들어 아래 그림과 같이 차트에, Deployment,Service,Job 등의 리소스를 차트에 정의해서 배포하고자 한다. 그중에, 인스톨 전과 후에 특정 작업을 Job을 이용해서 처리하고자 한다면, 그 Job들에 annotation으로, Helm Hook임을 명시해줘야 한다. 


<그림. Helm 차트내의 리소스와 Hook으로 정의된 리소스들>


이 차트를 실행하게 되면 다음과 같은 순서로 실행이 된다. 먼저 차트에 정의되어 있는 리소스 중에서 Hook 으로 정의된 리소스중, pre-install 로 정의된 리소스가 먼저 실행이 되고, 그 다음에 다른 리소스가 인스톨이 된다. 인스톨이 완료되고 나면 그후에 post-install 로 정의된 Hook이 실행되는 형태이다. 



<그림. Helm 차트에서 리소스들의 배포 순서>


Hook 종류

Hook 은 인스톨 전과 후 뿐만 아니라, 릴리즈를 삭제하거나 업그레이드 할때 등 다양한 시점에 Hook을 삽입할 수 있도록 정의되어 있다.  자세한 Hook 의 종류는 https://github.com/helm/helm/blob/master/docs/charts_hooks.md 문서를 참고하기 바란다. 정의된 Hook 중 몇가지를 보면 다음과 같다. 


  • pre-install: 리소스를 설치하기전에 실행된다. (정확히 이야기 하면 리소스를 설치하기 위한 템플릿이 렌더링 된 후에, 렌더링된 템플릿으로 리소스를 설치하기 전에 실행된다. )

  • post-install: Helm 차트에 의해서 리소스들이 모두 설치 된 후에 실행된다. 

  • pre-delete: 릴리즈의 리소스를 삭제할때, 삭제 전에 실행된다.

  • post-delete:릴리즈의 리소스를 삭제한 후에, 실행된다.  

  • pre-upgrade: 릴리즈를 업그레이드 하기 전,  ( 템플릿이 렌더링 된 후에) 리소스가 생성되기 바로 전에 실행된다.

  • post-upgrade: 릴리즈 업그레이드가 끝난 후에, 실행된다. 

  • pre-rollback: 릴리즈를 기존 버전으로 롤백 할때 실행된다. (템플릿이 렌더링 된 후에 )

  • post-rollback: 릴리즈에 대한 롤백이 완료된 후에 실행된다. 

  • crd-install:  CRD 리소스를 인스톨하는데, 다른 Hook이나 기타 모든 다른 태스크 보다 우선적으로 실행된다. 이 Hook은 다른 리소스에서 이 CRD를  참조하여사용할때 주로 사용된다. 

  • test-success: “helm test” 명령을 실행할때 수행되는데, 그 결과가 성공 (return code == 0)일때만 실행된다. 

  • test-failure: “helm test” 명령을 실행할때 수행되는데, 그 결과가 실패 (return code != 0)일때만 실행된다. 

Hook weight & policy

Hook에는 실행 시점을 정의하는 기능 뿐만 아니라, 여러개의 Hook이 정의되었을때 실행 순서를 정하거나 또는 Hook 실행이 끝났을때 Hook 리소스를 지울지 나둘지등의 정책을 결정하는 기능이 있다. 

Hook weight

Hook weight는 여러개의 Hook 이 있을때, Hook 들의 실행 순서를 정의하기 위해서 사용한다. 음/양수 모두를 사용할 수 있으며, 작은 수를 가지고 있는 Hook 부터 우선 실행된다.

Hook weight는 아래 그림과 같이 annotations 부분에 “helm.sh/hook-weight” 항목으로 지정할 수 있다. 

 annotations:

    "helm.sh/hook-weight": "5"

Hook delete policy

Hook 이 실행된 후에, Hook 리소스들을 지울지 말지를 설정할 수 있는데, Hook이 실행된 후에, Hook 리소스는 일반적으로는 남아 있다. 만약에 Hook 이 실행된 후에, Hook을 삭제 하고 싶으면 annotation에 “helm.sh/hook-delete-policy”에 아래와 같이 삭제 정책을 정하면 된다. 


annotations:

    "helm.sh/hook-delete-policy": hook-succeeded 

삭제 정책으로 사용할 수 있는 정책은 다음과 같다. 

  • "hook-succeeded" : Hook 이 성공적으로 실행이 되고 나면, 삭제하도록 한다. 

  • "hook-failed" : Hook 실행이 실패하였을 경우 삭제 한다. 

  • "before-hook-creation" : Hook 을 실행하기 전에, 기존의 Hook을 삭제하고 실행하도록 한다. 


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 패키지 매니저 Helm

#2-3. Charts (디렉토리 구조)

조대협 (http://bcho.tistory.com)

디렉토리 구조

Helm 차트의 디렉토리 구조는 다음과 같다. 직접 아래와 같은 디렉토리 구조에 파일을 각각 생성해도 되지만, 기본 템플릿을 helm create [차트명] 으로 생성할 수 있다.

아래는

%helm create mychart

명령으로 생성한 디렉토리의 구조이다.


mychart/

 Chart.yaml          # A YAML file containing information about the chart

 LICENSE             # OPTIONAL: A plain text file containing the license for the chart

 README.md           # OPTIONAL: A human-readable README file

 requirements.yaml   # OPTIONAL: A YAML file listing dependencies for the chart

 values.yaml         # The default configuration values for this chart

 charts/             # A directory containing any charts upon which this chart depends.

 templates/          # A directory of templates that, when combined with values,

                     # will generate valid Kubernetes manifest files.

 templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes

Chart.yaml 파일

Chart.yaml은 차트에 대한 기본적인 메타 정보를 정의 한다.

apiVersion: The chart API version, always "v1" (required)

name: The name of the chart (required)

version: A SemVer 2 version (required)

kubeVersion: A SemVer range of compatible Kubernetes versions (optional)

description: A single-sentence description of this project (optional)

keywords:

 - A list of keywords about this project (optional)

home: The URL of this project's home page (optional)

sources:

 - A list of URLs to source code for this project (optional)

maintainers: # (optional)

 - name: The maintainer's name (required for each maintainer)

   email: The maintainer's email (optional for each maintainer)

   url: A URL for the maintainer (optional for each maintainer)

engine: gotpl # The name of the template engine (optional, defaults to gotpl)

icon: A URL to an SVG or PNG image to be used as an icon (optional).

appVersion: The version of the app that this contains (optional). This needn't be SemVer.

deprecated: Whether this chart is deprecated (optional, boolean)

tillerVersion: The version of Tiller that this chart requires. This should be expressed as a SemVer range: ">2.0.0" (optional)

차트의 이름이나 제작자등이 들어가는데, 몇가지 중요한 필드들을 살펴보자.

  • name은 Helm 차트의 이름을 정의한다.

  • version을 정의하는 것은 두가지 필드가 있는데, version 필드가 있고, appversion 이라는 필드가 따로 있다. version 필드는 helm 차트 자체의 버전을 정의하고, appversion은 이 차트를 통해서 배포 되는 애플리케이션의 버전을 정의한다. 예를 들어 helm 차트를 처음 만들었으면, helm 차트 버전은 1.0.0이 되고, helm chart를 통해서 채팅 애플리케이션 1.2를 배포한다면 appVersion은 1.2가 된다. 버전에 대한 네이밍은 semver (https://semver.org/lang/ko/)을 따르도록 한다.

License 및 README 파일

License 파일에는 이 Helm 차트의 라이센스 그리고,README에는 간략한 설명들을 적어 놓는다.

requirement.yaml 파일

Requirement.yaml에는 이 helm 패키지를 설치하기 위해서 필요한 다른 차트들의 목록을 기술한다. 이렇게 기술된 차트들은 이 차트가 설치되기 전에 자동으로 설치된다.

requirement.yaml 파일의 구조는 다음과 같다.


dependencies:

 - name: apache

   version: 1.2.3

   repository: http://example.com/charts

 - name: mysql

   version: 3.2.1

   repository: http://another.example.com/charts


Requirement.yaml에는 이 차트에 대한 의존성을 정의하는데, 필요한 차트의 이름과 버전 그리고, 그 차트가 저장된 리파지토리 경로를 정의한다.

위의 예제를 보면 apache의 1.2.3 버전의 차트가 필요하고, 이 차트는 http://example.com/charts에 저장되어 있음을 알 수 있다. 리파지토리에 대해서는 뒤에서 다시 한번 자세히 설명하도록 한다.


참고로 의존성에 정의된 차트들을 모두 설치하게할 수 도 있지만, tag나 condition을 사용하여, 조건에 따라 설치를 조정할 수 있다. 예를 들어 node.js API서버와 MySQL로 되어 있는 2-tier 애플리케이션이 있을때, MySQL을 같이 설치하도록 의존성을 정해놓을 수 있지만, 이미 MySQL이 쿠버네티스 클러스터내에 설치되어 있을 경우에는 그것을 재활용하면 되기 때문에, 옵션에 따라서 MySQL을 설치하거나 설치하지 않도록 할 수 있다.

charts/ 디렉토리

의존성이 필요한 차트들을 requirement.yaml 에 정의해놓는 경우, requirement.yaml에 있는 경로에서 다운로드 받아서 설치하는데, 다운로드를 받게하지 않고,차트를 배포할때 아예 같이 묶어서 배포하고 싶을 경우에는  charts 디렉토리에 차트들의 파일을 저장해놓으면, 차트를 배포할때 charts 디렉토리에 있는 차트를 먼저 배포하게 된다.

templates/ 디렉토리

Template는 앞의 예제를 통해서  설명했듯이, Helm 차트의 탬플릿을 정의한다.  

values.yaml 파일

values.yaml에는 템플릿에 사용될 value 값들을 저장한다



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 패키지 매니저 HELM

#2-2. Chart 버전과 릴리즈

조대협 (http://bcho.tistory.com)

업그레이드와 롤백

Chart로 의해서 설치된 리소스들을 쿠버네티스에서 릴리즈라고 한다.

예를 들어 같은 차트로 MySQL을 쿠버네티스 클러스터 내에 여러번 설치 할 수 있다.  각각 설치된 MySQL들을 릴리즈라고 하고, 릴리즈에 설정이 변경된 경우에, 차트를 수정해서 변경을 반영할 수 있다. 변경이 반영될때 마다 새로운 버전이 생성된다.


처음 helm install로 설치를 할때 --name 옵션으로 저장한 설치 이름이 릴리즈 명이 되고, 이 릴리즈를 업데이트 하고 싶으면 helm upgrade {Helm 릴리즈명} {차트 디렉토리} 를 실행하면, 해당 릴리즈를 업데이트하고, 새로운 버전을 생성한다.

앞의 예제의 values.yaml 에서 replicaCount: 3 를 변경한 후, upgrade를 해보자

%helm upgrade helloworld ./helloworld/

명령을 실행하면 다음과 같이 기존 배포가 업데이트 된다.


Release "helloworld" has been upgraded. Happy Helming!

LAST DEPLOYED: Sun Jun  9 20:58:41 2019

NAMESPACE: default

STATUS: DEPLOYED


RESOURCES:

==> v1/Service

NAME            AGE

helloworld-svc  2d


==> v1beta2/Deployment

helloworld-deployment  2d


==> v1/Pod(related)


NAME                                    READY STATUS RESTARTS AGE

helloworld-deployment-696fc568f9-2dkpm  0/1 ContainerCreating 0 0s

helloworld-deployment-696fc568f9-fqqg8  1/1 Running 0 2d

helloworld-deployment-696fc568f9-r8qct  1/1 Running 0 2d


그리고 위와 같이 Deployment pod가 3개로 늘어난것을 확인할 수 있다.

업그레이드 한후 버전을 확인하려면  helm history {릴리즈 이름} 을 사용하면 되는데,

% helm history helloworld

를 실행하면 아래와 같이 2번 버전이 새로 생긴것을 확인할 수 있다.


REVISION UPDATED                  STATUS     CHART            DESCRIPTION     

1        Fri Jun  7 23:24:34 2019 SUPERSEDED helloworld-0.1.0 Install complete

2        Sun Jun  9 20:58:41 2019 DEPLOYED   helloworld-0.1.0 Upgrade complete


만약 예전 버전으로 돌리고 싶으면 rollback 명령을 사용하면 되는데, helm rollback {릴리즈 이름} {릴리즈 버전} 으로 실행하면 된다. helm rollback helloworld 1 는 helloworld 릴리즈를 1 버전으로 롤백 하는 명령어 이다.  명령어를 실행해보면 다음과 같이 롤백이 완료되는 것을 확인할 수 있고,

% helm rollback helloworld 1

Rollback was a success! Happy Helming!


helm history로 해당 릴리즈의 버전을 확인해보면, 3번 버전에서 1번으로 롤백을 한것을 확인할 수 있다.

%helm history helloworld

REVISION UPDATED                  STATUS     CHART            DESCRIPTION     

1        Fri Jun  7 18:24:34 2019 SUPERSEDED helloworld-0.1.0 Install complete

2        Sun Jun  9 20:58:41 2019 SUPERSEDED helloworld-0.1.0 Upgrade complete

3        Sun Jun  9 21:43:48 2019 DEPLOYED   helloworld-0.1.0 Rollback to 1


릴리즈

앞에서도 설명했듯이 차트 하나로 같은 클러스터에 같은 애플리케이션을 여러개를 설치할 수 있다. MySQL이나 Redis 메모리 서버들은 애플리케이션이 아니기 때문에  같은 이미지로 여러개의 릴리즈를 설치할 수 있다.

그런데 앞의 스크립트로 helloworld 차트를 한번 더 설치하면 에러가 날것이다. 이유는 차트에 정의된 Service와 Deployment 리소스의 이름이 동일하기 때문이다.

templates/helloworld.yaml 파일에서 Deployment 이름 정의 부분을 보면, 이름을 {{.Value.name}}을 사용하도록 하였다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Values.name }}-deployment

:


그래서 values.yaml에서 name을 변경하지 않는 이상, Deployment는 같은 이름을 가지게 된다.

이 문제를 해결하기 위해서 리소스들의 이름을 릴리즈 이름을 사용하도록 하면 된다. 릴리즈 이름은 {{ .Release.Name}} 을 사용하면, 릴리즈 이름을 리소스 이름으로 사용할 수 있다. helloworld.yaml을 아래와 같이 수정하면 된다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Release.Name }}

spec:


그런데, 이렇게 설치가 된 리소스들이 어떤 차트에 의해서 설치된 것인지 구별이 잘 안될 수 있기 때문에, 좀더 좋은 방법은 리소스 이름을 “차트 이름-릴리즈 이름" 형태로 하는 것이 좋다. 차트 이름은 {{ .Chart.Name }} 을 사용하면 된다. 아래는 “차트 이름-릴리즈 이름" 형태로 정의한 예제이다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Chart.Name }}-{{ .Release.Name }}


helm template 명령을 이용해서 테스트를 해보자. 같은 예제를  helloworld2 디렉토리에 새롭게 만들었다. (그래서 아래 명령을 보면 ./helloworld2 디렉토리를 차트 디렉토리로 실행하였다.) 그리고 릴리즈 이름을 --name을 이용해서 myrelease로 지정하였다.


% helm template --name myrelease ./helloworld2

---

# Source: helloworld2/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: helloworld2-myrelease

spec:

 replicas: 3

:


metadata:

 name: helloworld-deployment

spec:

 replicas: 10

 minReadySeconds: 5

삭제

그리고 설치된 차트는 간단하게 helm delete 명령으로 삭제가 가능하다.

%helm delete helloworld

release "helloworld" deleted



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 패키지 매니저 HELM

#2-1 .Chart
조대협 http://bcho.tistory.com

Helm Chart

차트는 helm의 패키지 포맷으로, 하나의 애플리케이션을 설치하기 위한 파일들로 구성되어 있다. 예를 들어 tomcat을 설치하기 위한 쿠버네티스의 pod,service,deployment를 위한 YAML 파일등을 포함한다.

템플릿과 밸류

Helm 은 기본적으로 템플릿의 개념을 사용한다. 템플릿 파일을 만들어놓은 후에, 밸류 값을 채워 넣어서 쿠버네티스 리소스를 정의한 YAML 파일을 생성한다. 예제를 살펴보자

Helm 은 기본적으로 템플릿의 개념을 사용한다. 템플릿 파일을 만들어놓은 후에, 밸류 값을 채워 넣어서 쿠버네티스 리소스를 정의한 YAML 파일을 생성한다. 예제를 살펴보자.


먼저 templates/helloworld.yaml 파일을 정의한다.

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: {{ .Values.name }}-deployment

spec:

 replicas: {{ .Values.replicaCount }}

 minReadySeconds: 5

 selector:

   matchLabels:

     app: {{ .Values.name }}

 template:

   metadata:

     name: {{ .Values.name }}-pod

     labels:

       app: {{ .Values.name }}

   spec:

     containers:

     - name: {{ .Values.name }}

       image: gcr.io/terrycho-sandbox/helloworlds:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

---

apiVersion: v1

kind: Service

metadata:

 name: {{ .Values.name }}-svc

spec:

 selector:

   app: {{ .Values.name }}

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer


그리고 ./values.yaml 파일을 아래와 같이 정의한다.


name: "helloworld"

replicaCount: 3


템플릿은 값을 채울 수 있는 말 그대로 템플릿이고, Value의 값을 이용해서 값을 채운다.

이를 개념적으로 표현해보면 다음과 같은 형태가 된다.



좌측은 템플릿 파일이고, 템플릿에서 밸류값은 별도의 파일에 정의한다.

정의한 키/밸류 형식으로 name:”helloworld”로 정의하였고, replicaCount: 2로 정의하였다

그리고 이 밸류값을 불러들이기 위해서는 {{.Value.키이름}} 식으로 템플릿내에 정의한다.

위의 예제에서 보면 name등에 {{.Value.name}} 으로 정의하였고, replicas 수는 {{ .Value.replicaCount }} 로 정의한다.

이렇게 정의된 템플릿에 밸류 내용을 정의하면 오른쪽 처럼 YAML 파일을 생성할 수 있다.


외부에서 Value를 받는 방법

Value값을 values,yaml에 지정해놨지만, 설치에 따라서 각 값을 변경하고 싶은 경우가 있다. 예를 들어 replicaCount를 3이 아니라 10으로 변경하고 싶을 경우에는 values.yaml 파일을 일일이 에디트 해야 한다. 나중에, 차트를 차트 리파지토리에 등록하기 위해서는 압축된 파일 형태를 사용하는데, 이 경우에는 그러면 차트 압축 파일을 다운로드 받은 후에 압축을 풀고나서, 내용을 수정하고 설치에 사용해야 하는 불편함이 있다.

이렇게 일일이 수정하지 않고 CLI에서 변경하고 싶은 인자만 간단하게 지정할 수 있는 방법이 없을까?

helm install 이나 upgrade시에  --set 옵션을 사용하면 된다. 예를 들어 values.yaml에 정의된 replicaCount를 10으로 변경하고자 하면 다음과 같이 하면 된다. %helm template --name myrelease --set replicaCount=10 ./helloworld


아래는 template 명령을 이용해서 테스트한 결과이다.   replicas가 10으로 변경된것을 볼 수 있다.

---

# Source: helloworld/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment


만약 설정값이 많아서 --set 을 이용해서 parameter로 넘기기가 어렵다면, 필요한 변수만 파일로 만들어서 넘길 수 있다.

예를 들어 myvalue.yaml 에 아래와 같이 name만 “fromValuefile” 로 정의를 한후에,


name: "fromValuefile"


helm install에서 -f 옵션으로 value 파일을 지정할 수 있다.

%helm install -f myvalues.yaml --name newrelease --dry-run --debug ./helloworld


결과를 보면 다음과 같다.

# Source: helloworld/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: fromValuefile-deployment

spec:

 replicas: 3

:

myvalues.yaml에 지정한 name에 의해서 Deployment name이  fromValuefile-deployment로 변경되고, replica 수는 원래 values.yaml에 지정한대로 3을 사용한 것을 확인할 수 있다.


디렉토리 구조

개념을 이해 하였으면, 파일을 어디에 저장하는지 디렉토리 구조를 살펴보자, helloworld라는 차트를 만들 것인데, helloworld라는 디렉토리를 만든다.

그리고 그 아래 템플릿들은 templates 라는 디렉토리에 yaml 로 정의한다. 여기에 채울 value들은 helloworld/values.yaml 파일내에 저장한다.



그리고 helloworlds/Chart.yaml 이라는 파일을 생성해야 하는데, 이 파일에는 이 헬름 차트에 대한 버전이나 작성자, 차트 이름과 같은 메타 정보를 정의한다.


다음은 Chart.yaml 의 내용이다.

apiVersion: v1

appVersion: "1.0"

description: A Helm chart for Kubernetes

name: helloworld

version: 0.1.0

테스트(검증)

헬름 차트 작성이 끝났으면 제대로 작동하는지 검증을 해볼 수 있다. 먼저 문법적인 오류가 없는지 확인 하는 명령은 helm lint 명령을 사용하면 된다. 명령어 실행은 차트 디렉토리 위에서 해야 한다. 이 예제에서는 ../helloworld 디렉토리가 된다.

실행하면 다음과 같은 결과를 볼 수 있다.


%helm linit ./helloworld

==> Linting ./helloworld/

[INFO] Chart.yaml: icon is recommended


1 chart(s) linted, no failures


문법적인 오류가 없는지 점검을 해준다. 다음으로 탬플릿에 밸류가 제대로 적용되서 원하는 YAML을 제대로 생성해나가는지 검증해야 하는데, helm template이라는 명령어를 사용하면 된다. helm lint 명령과 마찬가지로 차트가 저장된 디렉토리의 상위 디렉토리 (../helloworld)에서 실행한다.

다음은 helm template 명령을 실행한 결과이다.


%helm template ./helloworld

# Source: helloworld/templates/helloworld.yaml

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: helloworld-deployment

spec:

 replicas: 2

 minReadySeconds: 5

 selector:

   matchLabels:

     app: helloworld

 template:

   metadata:

     name: helloworld-pod

     labels:

       app: helloworld

   spec:

     containers:

     - name: helloworld

       image: gcr.io/terrycho-sandbox/helloworlds:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


내용 처럼 Value.name과  Value.replicaCount 값이 채워져서 Deployment 리소스에 대한 YAML 파일이 생성된것을 확인할 수 있다.

helm template 명령을 helm 클라이언트에서 tiller 서버 접속없이 template에 값이 채워지는지만 테스트를 하는 것이고, tiller 서버에 연결해서 테스트 할 경우에는 helm install --dry-run 옵션을 사용한다.  그리고, 내용을 확인하기 위해서 --debug 옵션을 추가한다.

helm install --name myrelease --dry-run --debug ./helloworld 이렇게 명령어를 사용하면 되는데, 장점은, 실제 서버에 연결해서 같은 릴리즈 버전이 있는지 등의 체크를 해주기 때문에, 실수를 막을 수 있다.

아래는 같은 릴리즈 버전으로 설치하는 것을 --dry-run으로 테스트한 결과이다.


% helm install --name myrelease --dry-run --debug ./helloworld

[debug] Created tunnel using local port: '56274'


[debug] SERVER: "127.0.0.1:56274"


[debug] Original chart version: ""

[debug] CHART PATH: /Users/terrycho/dev/workspace/kube/kubernetes-tutorial/31.helm/helloworld


Error: a release named myrelease already exists.

Run: helm ls --all myrelease; to check the status of the release

Or run: helm del --purge myrelease; to delete it


차트를 같은 이름(릴리즈명/뒤에서 다시 설명함)이 있기 때문에 설치할 수 없다 것을 테스트 단계에서 미리 확인할 수 있다.

실제 설치

그러면 생성한 헬름 차트를 이용해서 쿠버네티스에 리소스를 실제로 설치해보자. 설치 방법은 차트가 있는 디렉토리에서 helm install  명령을 이용해서 인스톨을 하면 된다. 이때 --name이라는 이름으로 설치된 차트 인스턴스의 이름을 설정해줘야 한다. 만약에 이름을 정해주지 않으면 임의의 이름이 자동으로 생성되어 사용된다.


%helm install --name helloworld ./helloworld/


명령을 실행하면 아래와 같이 deployment가 생성되는 것을 확인할 수 있다. 예제에서 설명은 하지 않았지만, 테스트에 사용된 코드에는 Service를 배포하는 부분이 함께 포함되어 있기 때문에 아래 실행결과를 보면 Service까지 같이 생성된것을 확인할 수 있다.


NAME:   helloworld

LAST DEPLOYED: Fri Jun  7 23:01:44 2019

NAMESPACE: default

STATUS: DEPLOYED


RESOURCES:

==> v1beta2/Deployment

NAME                   AGE

helloworld-deployment  1s


==> v1/Pod(related)


NAME                                    READY STATUS RESTARTS AGE

helloworld-deployment-696fc568f9-mc6mz  0/1 ContainerCreating 0 0s

helloworld-deployment-696fc568f9-nsw48  0/1 ContainerCreating 0 0s


==> v1/Service


NAME            AGE

helloworld-svc  1s


설치가 완료되었으면 리소스가 제대로 생성되었는지 확인을 해보기 위해서 kubectl get deploy 명령을 실행한다.


%kubectl get deploy

kubectl get deploy

NAME                    DESIRED CURRENT UP-TO-DATE   AVAILABLE AGE

helloworld-deployment   2 2 2         2 10m


위와 같이 Deployment 리소스가 생성 된것을 확인할 수 있다.

헬름을 통해서 설치된 차트들은 helm list 명령을 이용해서, 설치 상태를 확인할 수 있다.

helm list 명령을 실행해보면 아래와 같이 helloworld 차트가 설치된것을 확인할 수 있다.


%helm list

NAME               REVISION UPDATED                  STATUS   CHART            APP VERSION NAMESPACE

helloworld         1        Fri Jun  7 23:01:44 2019 DEPLOYED helloworld-0.1.0 1.0  




본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

쿠버네티스 패키지 매니저 HELM

Helm의 일반적인 개념

Helm은 리눅스의 apt 툴이나, node.js의 npm과 같은 쿠버네티스용 패키지 매니지먼트 도구 이다. 일반적으로 하나의 소프트웨어를 쿠버네티스에 배포하려면, 간단하게 컨테이너만을 배포해서는 사용하기 어려운 경우가 많다. 외부로 IP를 노출 시키기 위해서 쿠버네티스 서비스를 배포해야 하고,쿠버네티스 pod를 관리할 deployment가 필요하며, 디스크 볼륨과 기타 정책등 부가 적인 부분을 추가로 배포해야 한다.

이미 네트워크나 디스크 설정이 완료된 상태에서 애플리케이션을 업데이트 하는 경우에는 쿠버네티스 deployment나 다른 배포 도구를 이용해도 되지만, 처음부터 모든 것을 설치해야 하는 반복적인 작업이 있는 경우에는 배포 도구로 불가능하다.

그래서 Helm은 애플리케이션 컨테이너 배포는 물론이고, 이에 필요한 쿠버네티스 리소스를 모두 배포해주는 역할을 하는데, 이 배포를 패키지 형태로 한다.

Infrastructure as a code (이하 IaaC)를 구현하는 Terraform + 패키지 매니저인 npm 정도의 개념이 합쳐져 있다고 보면 된다.




<Helm 개념도>

Helm의 구성을 보면 위의 그림과 같다.

먼저 CLI 툴인 클라이언트로 helm이 있다. 이 클라이언트는  Helm 서버 모듈과 통신을 하는데, Helm 서버를 Tiller라고 하고, Tiller는 쿠버네티스 클러스터 내에 설치된다.

helm 을 통해서 인스톨하는 패키지를 Chart라고 한다. Chart는 템플릿으로 설치하고자 하는 쿠버네티스 리소스의 설치 스크립트가 된다. 이 Chart들은 Helm Chart Repository에 저장할 수 있는다. Helm Chart repository는 HTTP server를 지원하는 서버로 Google Cloud Storage나 Git Hub page 또는 웹서버등을 사용할 수 있다.


Helm 설치

Helm 설치는 먼저 helm 클라이언트를 설치해야 한다. https://github.com/helm/helm 에 설치가이드가 나와 있는데, 맥 사용자의 경우 brew로 쉽게 설치가 가능하고, Linux의 경우 scoop등의 패키지 인스톨러를 이용하면 된다.


다음 helm 서버인 Tiller를 설치해야 한다.

Tiller 설치는 간단하게 다음 명령을 클라이언트에서 실행하면 된다. (이때, 이미 클라이언트에 kubectl 명령과 쿠버네티스 연결을 위한 설정이 다 되어있어야 한다.)

% helm init ---history-max 200

(Helm 설치 히스토리의 수를 정의해준다. 최대 히스토리 수를 정하지 않으면 히스토리가 무한으로 증가하게 된다.)


그러면 테스트로 MySQL를 Helm을 이용해서 설치해보자 먼저 Chart 저장되는 repository 를 최신으로 업데이트 한다.

%helm repo update 명령을 실행하면 된다.


다음 mySQL을 설치할것인데,

%helm install stable/mysql

명령을 실행하면 다음과 같이 MySQL이 설치된다. 설치가 끝나면 아래 결과와 같이 mySQL 연결을 테스틑 하는 방법과 ROOT 패스워드를 구하는 방법이 나온다.



위의 그림에서와 같이

%  MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace default iron-kitten-mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo)

명령어를 실행하여, MySQL Root 패스워드를 알아낸다.

다음

% kubectl run -i --tty ubuntu --image=ubuntu:16.04 --restart=Never -- bash -il

을 실행해서, 우분투 이미지 기반의 컨테이너를 하나 만든후에, bash로 접속을 한다.

접속이 된 상태에서

% apt-get update && apt-get install mysql-client -y

명령을 이용해서 MySQL 클라이언트를 설치하고

% mysql -h iron-kitten-mysql -p

를 실행하고 앞에서 구한 비밀 번호를 넣으면 아래  그림과 같이 MySQL에 접속이 된다.



Helm에 의해서 설치된 차트 목록을 확인하려면 아래와 같이

% helm list

를 실행하면 된다. 아래 그림에서 보면 앞에서 설치한 mySQL이 iron-kitten이라는 이름으로 설치가 되어있는 것을 확인할 수 있다.


   

만약 설치된 helm chart를 삭제하기 위해서는

%helm delete {Chart명} 을 하면된다.

앞에서 설치한 iron-kitten mysql chart를 삭제하려면

%helm delete iron-kitten

을 실행하면 아래 그림과 같이 삭제되는 것을 확인할 수 있다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

Serveless를 위한 오픈소스 KNative #2 Eventing


조대협 (http://bcho.tistory.com)


knative의 다른 모듈로써는 비동기 메세지 처리를 위한 eventing 이라는 모듈이 있다. 카프카나, 구글 클라우드 Pub/Sub, AWS SQS와 같은 큐에서 메시지를 받거나 또는 Cron과 같은 타이머에서 이벤트가 발생하면 이를 받아서 처리할 수 있는 비동기 메커니즘을 제공하는 모듈이라고 보면 된다.


메시지 큐나 cron 과 같이 이벤트를 발생 시키는 자원들은 knative에 event source 라는 Custom Resource로 등록이 되고, 등록된 event source는 이벤트가 발생되면 지정된 knative 서비스로 이벤트 메시지를 HTTP로 전송한다. 이때 이벤트를 받는 knative 서비스는 앞에서 언급한 knative serving의 서비스이다. 이때 이벤트에 대한 스펙은 CNCF Serverless WG 에서 정의한 CloudEvents 스펙에 기반한다.

Hello Eventing

자세하게 Eventing에 대해서 알아보기 전에 간단한 예제를 살펴보자. 예제는 knative.dev의 cronjob  예제이다.  Crontab으로 이벤트를 생성하면, event-display 라는 서비스에서 이 이벤트를 받아서 이벤트의 내용을 간략하게 로그로 출력하는 예제이다.


먼저 이벤트를 읽어드릴 event-display 서비스를 배포하자. 해당 서비스는 HTTP post로 받은 이벤트의 내용을 log로 출력해주는 코드로 이벤트의 포맷은 앞에서 설명한 CloudEvent의 포맷을 따른다.

Go 로 구현된 코드이며, 코드 원본은 여기에 있다.

 해당 컨테이너를 배포하기 위해서 아래와 같이 service.yaml 파일을 만들고, kubectl apply -f service.yaml 을 이용해서 배포하면, crontab 에서 이벤트를 받는 serving 인스턴스가 준비된다.

apiVersion: serving.knative.dev/v1alpha1

kind: Service

metadata:

 name: event-display

spec:

 runLatest:

   configuration:

     revisionTemplate:

       spec:

         container:

           image: gcr.io/knative-releases/github.com/knative/eventing-sources/cmd/event_display

<그림. Event consumer용 knative 서비스 배포>


다음 Crontab event 소스를 아래와 같이 yaml로 정의한다.


apiVersion: sources.eventing.knative.dev/v1alpha1

kind: CronJobSource

metadata:

 name: test-cronjob-source

spec:

 schedule: "*/2 * * * *"

 data: '{"message": "Hello world!"}'

 sink:

   apiVersion: serving.knative.dev/v1alpha1

   kind: Service

   name: event-display

<그림. Crontab event source 정의>


spec>schedule 부분에 이벤트 주기에 대한 설정을 crontab 포맷을 따라서 하고, data 부분에 cron 이벤트가 발생할때 마다 보낼 데이타를 정의한다.

데이타를 보낼 목적지는 sink 부분에 지정하는데, kind에 타입을 정의하고 (여기서는 knative의 Service로 지정) 그리고 service 의 이름을 name에 정의한다. 앞에서 knative serving 서비스를 event-display로 지정하였기 때문에, 서비스명을 event-display로 정의한다.

yaml 파일 설정이 끝났으면 kubectl apply -f  명령을 이용해서 이벤트 소스를 등록하고, 동작을 하는지 확인해보도록 하자.


%kubectl logs -l serving.knative.dev/service=event-display -c user-container --since=10m


명령을 이용하면 앞에서 배포한 event-display 서비스의 로그를 볼 수 있는데, 결과를 보면 다음과 같다.



Data 부분에서 crontab 이벤트 소스에서 보내온 “message”:”Hello world!” 문자열이 도착한것을 확인할 수 있다.

Eventing detail

이벤트는 앞의 예제에서 본것과 같이 이벤트 소스에서 바로 Knative 서빙에서 받아서 처리하는 가장 기본적인 비동기 이벤트 처리 패턴이다.


Broker & Trigger

이러한 패턴이외에도 좀 더 다양한 패턴 구현이 가능한데, 두번째가 Broker와 Trigger이다. Broker는 이벤트 소스로 부터 메시지를 받아서 저장하는 버킷 역할을 하고, Broker에는 Trigger를 달 수 있는데, Trigger에는 메시지 조건을 넣어서, 특정 메시지 패턴만 서비스로 보낼 수 있다. 위의 패턴에서 필터를 추가한 패턴으로 보면 된다.



이해를 돕기 위해서 예제를 보자. 다음은 knative.dev 공식 사이트에 나와 있는 예제중에, Google Cloud Pub/Sub Source를 Broker로 연동하는 예제이다.


# Replace the following before applying this file:

#   MY_GCP_PROJECT: Replace with the GCP Project's ID.


apiVersion: sources.eventing.knative.dev/v1alpha1

kind: GcpPubSubSource

metadata:

 name: testing-source

spec:

 gcpCredsSecret:  # A secret in the knative-sources namespace

   name: google-cloud-key

   key: key.json

 googleCloudProject: MY_GCP_PROJECT  # Replace this

 topic: testing

 sink:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Broker

   name: default

<그림. github-pubsub-source.yaml>


위의 코드는 GCP Pub/Sub Source를 등록하는 부분인데, sink 부분은 이 소스에서 오는 메시지를 어디로 보낼지를 정하는 부분이다. 위에 보면 Broker로 보내는것을 볼 수 있다. Broker는 Default Broker로 보낸다.


다음은 Broker에서 받은 메시지를 Trigger 조건에 따라서 Knative Serving 서비스로 보내는 설정이다.


apiVersion: serving.knative.dev/v1alpha1

kind: Service

metadata:

 name: event-display

spec:

 template:

   spec:

     containers:

     - # This corresponds to

       # https://github.com/knative/eventing-sources/blob/release-0.5/cmd/event_display/main.go           

       image: gcr.io/knative-releases/github.com/knative/eventing-sources/cmd/event_display@sha256:bf45b3eb1e7fc4cb63d6a5a6416cf696295484a7662e0cf9ccdf5c080542c21d


---


# The GcpPubSubSource's output goes to the default Broker. This Trigger subscribes to events in the

# default Broker.


apiVersion: eventing.knative.dev/v1alpha1

kind: Trigger

metadata:

 name: gcppubsub-source-sample

spec:

 subscriber:

   ref:

     apiVersion: serving.knative.dev/v1alpha1

     kind: Service

     name: event-display


< 그림. Trigger와 이벤트 메시지를 수신하는 Service를 정의한 부분>


서비스는 event-display라는 서비스를 정의하였고, 그 아래 Trigger 부분을 보면 gcppubsub-source-sample 이라는 이름으로 Trigger를 정의하였다. Broker 명을 정의하지 않으면 이 Trigger는 default broker에 적용된다. 별다른 조건이 없기 때문에, Broker의 모든 메시지를 대상 서비스인 event-display로 전달한다.

Channel & subscription

다음 개념은 Channel과 subscription 이라는 개념인데, Channel을 메시지를 저장 후에, Channel에 저장된 메시지는 메시지를 수신하는 Subscription을 통해서 다른 Channel로 포워딩 되거나 또는 Service로 전달 될 수 있다.



<그림. Channel과 Subscription 개념도>


앞에서 Channel에서는 메시지를 저장한다고 했는데, 그러면 저장할 장소가 필요하다. 저장할 장소는 설정으로 다양한 메시지 저장소를 사용할 수 있는데, 현재 메모리, Apache Kafka 또는 NATS Streaming을 지원한다.


간단한 예제를 살펴보자 예제는 이 문서를 참고하였다

먼저 아래 설정을 보자


apiVersion: sources.eventing.knative.dev/v1alpha1

kind: GcpPubSubSource

metadata:

 name: testing-source

spec:

 gcpCredsSecret:  # A secret in the knative-sources namespace

   name: google-cloud-key

   key: key.json

 googleCloudProject: knative-atamel  # Replace this

 topic: testing

 sink:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Channel

   name: pubsub-test



< 그림. GCPPubSub Event Source 정의한 코드>


위 설정은 GCP Pub/Sub을 Event source로 등록하는 부분이다. 이벤트 소스로 등록 한후에, 이벤트를 sink 부분에서 pubsub-test라는 Channel로 전달하도록 하였다.

다음 아래는 Channel을 정의한 부분인데, pubsub-test 라는 이름으로 Channel을 정의하고 "provisioner” 부분에, 메시지 저장소를 "in-memory-channel” 로 지정해서 메모리에 메시지를 저장하도록 하였다.

apiVersion: eventing.knative.dev/v1alpha1

kind: Channel

metadata:

 name: pubsub-test

spec:

 provisioner:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: ClusterChannelProvisioner

   name: in-memory-channel

< 그림. Channel 정의한 코드>



apiVersion: serving.knative.dev/v1alpha1

kind: Service

metadata:

 name: message-dumper-csharp

spec:

 runLatest:

   configuration:

     revisionTemplate:

       spec:

         container:

           # Replace {username} with your actual DockerHub

           image: docker.io/{username}/message-dumper-csharp:v1

---

apiVersion: eventing.knative.dev/v1alpha1

kind: Subscription

metadata:

 name: gcppubsub-source-sample-csharp

spec:

 channel:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Channel

   name: pubsub-test

 subscriber:

   ref:

     apiVersion: serving.knative.dev/v1alpha1

     kind: Service

     name: message-dumper-csharp

< 그림. Serving과 subscription을 정의 코드>


Channel에 저장된 메시지를 다른 Channel로 보내거나 또는 Service로 보내려면 Subscription을 거쳐야 한다. 위에서 gcppubsub-source-sample-charp이라는 subscription을 정의하였고, 이 subscription이 연결되는 Channel은 spec > channel 부분에 아래와 같이 정의 하였다.


aspec:

 channel:

   apiVersion: eventing.knative.dev/v1alpha1

   kind: Channel

   name: pubsub-test

< 그림. 위의 Subscription 정의에서 Channel 정의 부분>


그리고 그 채널에서 받은 메시지를 subscriber > ref 부분에서 아래와 같이 message-dumper-charp이라는 서비스로 포워딩 하도록 하였다.

 subscriber:

   ref:

     apiVersion: serving.knative.dev/v1alpha1

     kind: Service

     name: message-dumper-csharp

< 그림.위의 Subscription 정의에서 Service 정의 부분>


전체적으로 Eventing 모듈을 이해하는데 시간이 많이 걸렸는데, Eventing 모듈은 Serving 모듈에 비해서 예제가 적고, 공식 문서에 아직 설명이 부족하다. 예를 들어서 소스 → 서빙으로 메시지를 보낼때 스케일링할 경우 문제가 없는지. Channel → subscription 으로 메시지를 보낼때 Trigger를 사용할 수 있는지 등 정보가 아직 부족해서 자세한 분석이 어려웠다. Knative는 현재 0.5 버전으로 버전이고, Event Source 들도 아직 개발 단계가 아니라 PoC (Proof Of Concept : 기술적으로 가능한지 테스트를 하는 단계) 단계 이기 때문에 제대로 사용하기에는 시간이 더 걸릴 듯 하다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

Serveless를 위한 오픈소스 KNative

조대협(http://bcho.tistory.com)

배경

근래에 들어서 컨테이너를 사용한 워크로드 관리는 쿠버네티스 de-facto 표준이 되어가고 있는데, 쿠버네티스 자체가 안정되어가고 있지만, 이를 현업에 적용하기 위해서는 아직까지 여러가지 챌린지가 있다.

컨테이너 기반의 쿠버네티스 서비스가 지향하는 바는, 셀프서비스 기반의 데브옵스 모델로 인프라와 이를 자동화하는 플랫폼을 인프라엔지니어가 개발하여 개발팀에 제공하고, 개발팀은 개발과 배포/운영을 스스로 하는 모델이다.

그런데 예를 들어 간단한 무상태(stateless) 웹서비스를 하나 구축한다 하더라도 Deployment,Ingress,Service 등의 쿠버네티스 리소스를 정의해서 배포해야 하고, 여기에 오토 스케일링이나, 리소스 (CPU,메모리)등의 설정을 따로 해줘야 한다. 그런데 이런 설정을 일일이 다 하기에는 일반 개발자들에게 부담이 된다. 또한 A/B 테스팅이나 카날리 배포등은 쿠버네티스 자체로 지원이 되지 않고 스피니커(spinnaker)등의 다른 솔루션을 부가해서 써야 하는데, 이런 모델은 컨테이너 기반의 셀프 서비스와는 거리가 멀어진다.

서버쪽에 복잡한 설정 없이 무상태 웹서비스나 간단한 이벤트 컨슈밍 서비스등을 구축하는 방법으로는 서버리스 서비스들이 있는다. 아마존 클라우드의 람다(Lambda)나, 구글 클라우드의 펑션(Function)등이 이에 해당한다. 그런데 이러한 서버리스 서비스들은 특정 클라우드 플랫폼에 의존성을 가지고 있는데, 이러한 문제를 해결하기 위해서 나온 오픈소스 서버리스 솔루션이 Knative 이다.

Knative

Knative는 구글의 주도하는 오픈소스 기반의 서버리스 솔루션으로 쿠버네티스 위에서 기동이 된다. 그래서 특정 클라우드 종속성이 없을뿐만 아니라 On-Prem에서도 설치가 가능하다. 지원되는 인프라 목록은 여기에 있는데, 레드헷 오픈 시프트, 피보탈, IBM 과 같은 On-Prem 쿠버네티스뿐만 아니라, 구글, Azure, IBM 클라우드등 다양한 클라우드를 지원한다.


Knative는 스테이트리스 웹서비스뿐만 아니라, 큐에서 이벤트를 받아서 처리하는 이벤트 핸들링을 위한 서버리스 모델을 지원하고, 거기에 더불어 컨테이너를 빌딩할 수 있는 빌드 기능을 제공한다. 그러면 각각을 살펴보자

Serving

서빙은 무상태 웹서비스를 구축하기 위한 프레임웍으로 간단하게 웹서비스 컨테이너만 배포하면, 로드밸런서의 배치, 오토 스케일링, 복잡한 배포 (롤링/카날리)등을 지원하고, 서비스 매쉬 솔루션인 istio와 통합을 통해서 다양한 모니터링을 제공한다.

Hello Serving

일단 간단한 예제를 보자. 아래는 미리 빌드된 간단한 웹서비스 컨테이너를 배포하는 YAML 스크립트이다.


apiVersion: serving.knative.dev/v1alpha1 # Current version of Knative

kind: Service

metadata:

 name: helloworld-go # The name of the app

 namespace: default # The namespace the app will use

spec:

 runLatest:

   configuration:

     revisionTemplate:

       spec:

         container:

           image: gcr.io/knative-samples/helloworld-go # The URL to the image of the app

           env:

             - name: TARGET # The environment variable printed out by the sample app

               value: "Go Sample v1"

<그림. service.yaml>


kind에 Service로 정의되었고, 서빙을 하는 컨테이너는 container>image에 container image URL이 정의되어 있다. 이 이미지를 이용해서 서빙을 하게 되며, 환경 변수를 컨테이너로 넘길 필요가 있을 경우에는 env에 name/value 식으로 정의하면 된다.


$kubectl apply -f service.yaml

<그림. 서비스 배포>

이렇게 정의된 서비스 yaml 파일은 다른 쿠버네티스의 yaml 파일과 같게 kubectl apply -f {파일명}을 이용하면 배포할 수 있게 된다.

쿠버네티스와 마찬가지로 yaml 파일을 정의해서 컨테이너를 정의하고 서비스를 정의해서 배포하는데, 그렇다면 쿠버네티스로 배포하는 것과 무슨 차이가 있을 것인가? 위의 설정 파일을 보면, 로드밸런서,Ingress 등의 추가 설정없이 간단하게 서비스 컨테이너 이름만 정의하고, 컨테이너만 정의하면 바로 배포가 된다. 서비스를 하는데 필요한 기타 설정을 추상화 시켜서 개발자가 꼭 필요한 최소한의 설정만으로 서비스를 제공할 수 있도록 해서, 복잡도를 줄여주는 장점이 있다.

그러면 배포된 서비스를 호출해보자


서비스를 호출하기 위해서는 먼저 서비스의 IP를 알아야 하는데, Knative serving 은 서비스 매쉬 솔루션인 istio 또는 apigateway인 Gloo 상에서 작동한다. 이 예제는 istio 위에 knative를 설치한것을 가정으로 설명한다.  istio에 대한 설명은 이링크와 이 링크 를 참고하기 바란다.

istio를 사용한 경우에는 istio의 gateway를 통해서 서비스가 되고,하나의 istio gateway가 몇개의 knative 서비스를 라우팅을 통해서 서비스한다. 이때 는 단일 IP이기 때문에 여러 knative 서비스를 서빙하기 위해서는 knative 서비스를 분류할 수 있어야 하는데 URI를 이용해서 구별을 하거나 또는 hostname 으로 구별을 한다. 이 예제에서는 hostname으로 구별하는 방법을 사용하였다.


그러면 실제로 서비스를 호출해보자. 먼저 istio gateway의 ip를 알아야한다.

Istio gateway ip는 다음 명령어를 이용하면 ip를 조회할 수 있다.


$kubectl get svc istio-ingressgateway --namespace istio-system

<그림. Istio gateway IP 조회>


다음으로 해야할일은 서비스의 domain 명을 알아야 하는데, 여기서 배포한 서비스는 helloworld-go 라는 서비스이다. 이 서비스가 배포되면 서비스에 대한 라우팅 정보가 정의되는데, kubectl get route 명령을 이용하면 라우팅 정보를 조회할 수 있고 그 중에서 domain 명을 조회하면 된다.


$kubectl get route helloworld-go  --output=custom-columns=NAME:.metadata.name,DOMAIN:.status.domain

<그림. Istio gateway IP 조회>


호스트명을 조회하면 아래와 같이 해당 서비스의 호스트명을 알 수 있다.

Domain 명은 {route name}.{kubernetes name space}.도메인명 으로 되어 있고, 도메인명은 디폴트로 example.com을 사용한다. helloworld-go 애플리케이션의 route 명은 helloworld-go이고, 쿠버네티스 네임 스페이스는 default 네임 스페이스를 사용하였기 때문에, helloworld-go.default.example.com 이 전체 서비스 호스트명이 된다.


그러면 조회한 호스트명과 ingress gateway의 IP 주소를 이용해서, curl 명령으로 테스트 호출을 실행해보자.

$curl -H "Host: helloworld-go.default.example.com" http://${IP_ADDRESS}

<그림. Istio gateway IP 조회>

 

IP_ADDRESS는 앞에서 조회한 ingress의  gateway 주소를 이용하면 된다.

실행을하고 나면 다음과 같은 결과를 얻을 수 있다.

Serving detail

간단하게, Serving 을 테스트 해봤다. 그럼 Serving이 어떻게 구성되어 있는지 조금 더 자세하게 살펴보도록 하자. Serving 은 쿠버네티스 CRD (Custom Resource Definition)으로 정의된 4개의 컴포넌트로 구성되어 있다.


  • Configuration
    Configuration은 knative serving으로 배포되는 서비스를 정의한다. 컨테이너의 경로, 환경 변수, 오토스케일링 설정, hearbeat 설정등을 정의한다. 재미있는것은 단순히 컨테이너 경로를 정의할 수 도 있지만, 컨테이너 빌드 설정을 정의할 수 있다. 즉 코드가 변경되었을때 Configuration에 있는 빌드 설정을 통해서 새로운 컨테이너를 빌드해서 자동으로 배포하고 새롭게 배포된 컨테이너를 이용해서 서비스를 할 수 있도록 한다.

  • Revision
    Configuration의 히스토리라고 보면 되는데, Configuration을 생성할때 마다 새로운 revision이 생성된다.(Revision은 현재 Configuration의 스냅샷이다.) 그래서, 이전 revision으로 롤백을 하거나 저장된 각각의 다른 버전으로 트래픽을 분할해서 서빙할 수 있다.

  • Route
    Route는 서비스로 들어오는 트래픽을 Revision으로 라우팅 하는 역할을 한다. 단순하게 최신 버전의 revision으로 라우팅할 수 도 있지만, 카날리 테스트와 같이 여러 revision으로 라우팅 하는 역할은 Route에서 정의된다.

  • Service
    Service는 Configuration과 Route를 추상화하여, 하나의 웹서비스를 대표하는 개념이라고 보면 된다. 쿠버네티스에서 Deployment가 ReplicaSet 등을 추상화 하는 개념으로 생각하면 된다.


Serving 컴포넌트의 내용을 추상화하여 그림으로 표현하면 아래 그림과 같다.


<그림. Knative serving의 개념도>


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

[팁] 쿠버네티스 StatefulSet에서 Headless 서비스를 이용한 Pod discovery


조대협 (http://bcho.tistory.com)


statefulset에서 데이타베이스와 같이 master,slave 구조가 있는 서비스들의 경우에는 service를 통해서 로드밸런싱을 하지 않고, service 를 통해서 로드 밸런싱을 하는 것을 잘 사용하지 않고 개별 pod의 주소를 알고 접속해야 한다. 그래서 개별 Pod의 dns 이름이나 주소를 알아야 한다.


Pod들은 DNS이름을 가질 수 는 있으나, {pod name}.{service name}.{name space}.svc.cluster.local 식으로 이름을 가지기 때문에, pod 를 DNS를 이용해서 접근하려면 service name이 있어야 한다. 그러나 statefulset에 의한 서비스들은 앞에서 언급하였듯이 쿠버네티스 service를  이용해서 로드밸런싱을 하는 것이 아니기 때문에, 로드밸런서의 역할은 필요가 없고, 논리적으로, pod들을 묶어줄 수 있는 service만 있으면 되기 때문에 headless 서비스를 활용한다. Headless 서비스를 이용하면, service 가 로드 밸런서의 역할도 하지 않고, 단일 IP도 가지지 않지만, 아래 그림처럼 nslookup을 이용해서, headless 서비스에 의해서 묶여진 Pod들의 이름도 알 수 있고




{pod name}.{service name}.{name space}.svc.cluster.local  이름으로, 각 pod 에 대한 접근 주소 역시 얻을 수 있다.




본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

도커 컨테이너 보안 취약점 스캔 도구 Anchore

조대협 (http://bcho.tistory.com)


근래에 쿠버네티스를 로컬환경에서  이것저것 테스트하다보니, 클라우드에 있는 기능 보다 오픈 소스 기능을 많이 보게 되는데, 빌드 파이프라인을 보다가 재미있는 오픈소스를 하나 찾아서 정리해놓는다.


컨테이너 이미지에 대한 보안 문제

쿠버네티스와 같은 컨테이너 오케스트레이션 솔루션에서 가장 보안 취약이 있는 곳 중의 하나는 컨테이너 이미지 인데, 도커허브와 같이 널리 알려진 컨테이너 리파지토리에 저장되서 배포되는 이미지도 보안적으로 문제가 있는 이미지가 많다. 그래서, 가급적이면 벤더들에서 보안적으로 문제가 없도록 관리하는 베이스 이미지를 사용하는 것이 좋다. (구글에서 제공하는 도커 컨테이너 베이스 이미지 https://cloud.google.com/container-registry/docs/managed-base-images)

컨테이너 이미지 스캐닝 툴 (Anchore)

다음 방법으로는 컨테이너 이미지를 구울때마다 보안적인 문제가 없는지, 알려진 보안 취약점을 스캔해서 분석하는 방법인데, 이러한 도구를 vulnerability scaning 툴이라고 한다.

이런 툴중에 Anchore 라는 툴이 있는데, 상업 버전도 있지만 오픈 소스 버전이 있어서 손쉽게 사용이 가능하다. Anchore는 보안 CVE (Common Vulnerabilities and Exposures) 목록을 기반으로 해서 스캔하는데, CVE는 미정부에서 후원하는 National Cybersecurity FFRDC 에 의해서 관리된다.  

이외에도 몇개의 보안 취약점 정보를 몇군데서 가지고 오는데 (https://anchore.freshdesk.com/support/solutions/articles/36000020579-feeds)

재미있는 것은 NPM과 루비젬에 대한 보안 취약점 피드도 가지고 온다는 점이다.

  • Linux Distributions

    • Alpine Linux

    • CentOS

    • Debian

    • Oracle Linux

    • Red Hat Enterprise Linux

    • Ubuntu

  • Software Package Repositories

    • RubyGems.org

    • NPMJS.org

  • NIST National Vulnerability Database (NVD)


간단하게 들여다 보기

설치 및 사용 방법은 공식 홈페이지를 참고하면 되고, https://anchore.freshdesk.com/support/solutions/articles/36000020729-install-with-docker-compose 에는 docker-compose를 이용해서 간단하게 설치 및 사용하는 방법이 나와 있다. 아래는 공식 홈페이지의 내용을 참고해서 설명한 내용이다.


  1. 컨테이너 이미지를 anchore에 추가한다.
    다음 예제 명령은 dokcer
    % docker-compose exec anchore-engine anchore-cli --u admin --p foobar image add docker.io/library/debian:7

  2. anchore는 등록된 이미지를 자동으로 스캔한다.
    다음 명령어를 이용하면 스캔 상태를 모니터링 할 수 있다.
    % docker-compose exec anchore-engine anchore-cli --u admin --p foobar image get docker.io/library/debian:7 | grep 'Analysis Status'

  3. 스캔이 끝나면, 발견된 보안 취약점과 상세 내용을 확인할 수 있다.
    % docker-compose exec anchore-engine anchore-cli --u admin --p foobar image vuln docker.io/library/debian:7 all
    Vulnerability ID        Package                 Severity Fix Vulnerability URL                                                 
    CVE-2005-2541           tar-1.26+dfsg-0.1+deb7u1                 Negligible None https://security-tracker.debian.org/tracker/CVE-2005-2541         
    CVE-2007-5686           login-1:4.1.5.1-1+deb7u1                 Negligible None https://security-tracker.debian.org/tracker/CVE-2007-5686         
    CVE-2007-5686           passwd-1:4.1.5.1-1+deb7u1                Negligible None https://security-tracker.debian.org/tracker/CVE-2007-5686         
    CVE-2007-6755           libssl1.0.0-1.0.1t-1+deb7u4              Negligible None https://security-tracker.debian.org/tracker/CVE-2007-6755         
    ...
    ...
    ...

여기서 CVE-XXX 식으로 나오는 것이 보안 취약점이며, 자세한 내용은 뒤에 나오는 링크에서 확인이 가능하다.

CI/CD 파이프라인 통합

실제 개발/운영 환경에서 사용하려면, 커맨드 라인 뿐만 아니라 CI/CD 빌드 파이프라인에 통합을 해야 하는데, Anchore는 젠킨스 플러그인으로 제공되서 빌드 파이프라인에 쉽게 통합이 된다.

아래와 같은 개념으로 젠킨스에서 이미지를 빌드한 후에, 쿠버네티스나 기타 컨테이너 솔루션에 배포전에 보안 취약점을 스캔할 수 있다.


<그림. Anchore를 젠킨스 빌드 파이프라인에 추가 한 그림 >

출처 : https://anchore.com/opensource/


아래는 Anchore를 젠킨스 플러그인으로 설치한후에, 컨테이너 보안 취약점을 스캔한 결과를 출력해준 화면이다.




국내에는 아직 많이 알려지지 않은것 같은데, 젠킨스와 통합해서 사용한다면 꽤나 좋은 효과를 볼 수 있지 않을까 한다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

minikube에서 서비스 테스트 하기

미니쿠베를 로컬환경에 설치하고 쿠버네티스 서비스를 로드 밸런서 타입으로 배포하면, External IP할당이 되지 않는다. 그래서 아래 그림과 같이 External-IP가 계속 <pending>으로 보이게 된다.


NAME         TYPE  CLUSTER-IP  EXTERNAL-IP PORT(S)          AGE

kubernetes   ClusterIP  10.96.0.1  <none> 443/TCP          7d2h

my-service   LoadBalancer  10.105.173.146  <pending> 8080:31203/TCP   4m10s


그러면 미니쿠베에서 서비스를 테스트하려면 어떻게 해야 할까? 미니쿠베는 서비스를 테스트하기 위해서 service라는 명령을 제공한다. 아래 그림과 같이 minikube service {쿠버네티스 서비스명} 을 입력하면, 로컬 브라우져에서 해당 서비스를 접속할 수 있도록 해준다.


% minikube service my-service

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

요즘 쿠버네티스를 로컬환경에서 이것 저것 테스트하고 있는데, 실행 방법은 다음과 같다.  

환경은 minikube를 인스톨하였다. (0.33.1 버전)


sudo -E minikube start --vm-driver=none --extra-config=kubelet.resolv-conf=/run/systemd/resolve/resolv.conf


쿠버네티스를 우분투에서 실행할때, 별도의 Virtual Machine 없이 실행이 가능하다. VM 없이 실행하려면 --vm-driver=none 옵션을 줘야 한다. 이때, Local DNS Pod 가 기동될때 문제가 생기는데, 이를 해결하기 위해서 --extra-config=kubelet.resolv-conf=/run/systemd/resolve/resolv.conf 옵션을 주면된다.


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

StatefulSet을 이용하여 상태가 유지되는 Pod 관리하기

조대협 (http://bcho.tistory.com)

ReplicaSet으로 Stateful Pod 관리하기

앞에서 쿠버네티스의 Pod를 관리하기 위한 여러가지 컨트롤러 (Replica Set, ReplicationController,Job 등)에 대해서 알아보았다.

이런 컨트롤러들은 상태가 유지되지 않는 애플리케이션(Stateless application)을 관리하기 위해 사용된다. Pod가 수시로 리스타트되어도 되고, Pod 내의 디스크 내용이 리스타트되어 유실되는 경우라도 문제가 없는 워크로드 형태이다. 웹서버나 웹애플리케이션 서버 (WAS)등이 그에 해당한다. 그러나 RDBMS나 NoSQL과 같은  분산 데이타 베이스등과 같이 디스크에 데이타가 유지 되어야 하는 상태가 유지되는 애플리케이션 (Stateful application)은 기존의 컨트롤러로 지원하기가 어렵다.

ReplicaSet (이하 RS)를 이용하여 데이타 베이스 Pod를 관리하게 되면 여러가지 문제가 발생한다.

Pod의 이름

RS 등, Stateless Pod를 관리하는 컨트롤러에 의해서 관리되는 Pod들의 이름은 아래 그림과 같이 그 이름이 불 규칙적으로 지정된다.



마스터/슬레이브 구조를 가지는 데이타 베이스등에서 마스터 서버의 이름을 특정 이름으로 지정할 수 가 없다.

Pod의 기동 순서

RS에 의해서 관리되는 Pod들은 기동이 될때 병렬로 동시에 기동이 된다. 그러나 데이타베이스의 경우에는 마스터 노드가 기동된 다음에, 슬레이브 노드가 순차적으로 기동되어야 하는 순차성을 가지고 있는 경우가 있다.

볼륨 마운트

Pod에 볼륨을 마운트 하려면, Pod는 PersistentVolume (이하 PV)를 PersistentVolumeClaim(이하 PVC)로 연결해서 정의해야 한다.

RS등의 컨트롤러를 사용해서 Pod를 정의하게 되면, Pod 템플릿에 의해서 PVC와 PV를 정의하게 되기 때문에, 여러개의 Pod들에 대해서 아래 그림과 같이 하나의 PVC와 PV만 정의가 된다. RS의 Pod 템플릿에 의해 정의된 Pod들은 하나의 PVC와 연결을 시도 하는데, 맨 처음 생성된 Pod가 이 PVC와 PV에 연결이 되기 때문에 뒤에 생성되는 Pod들은 PVC를 얻지 못해서 디스크를 사용할 수 없게 된다.   


아래 YAML 파일은 위의 내용을 테스트 하기 위해서 작성한 파일이다.


apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: helloweb-disk

spec:

accessModes:

  - ReadWriteOnce

resources:

  requests:

    storage: 30Gi

---

apiVersion: v1

kind: ReplicationController

metadata:

name: nginx

spec:

replicas: 3

selector:

  app: nginx

template:

  metadata:

    name: nginx

    labels:

      app: nginx

  spec:

    containers:

    - name: nginx

      image: nginx:1.7.9

      volumeMounts:

      - name: nginx-data

        mountPath: /data/redis

      ports:

      - containerPort: 8090

    volumes:

    - name: nginx-data

      persistentVolumeClaim:

        claimName: helloweb-disk



nginx Pod를 RC를 이용하여 3개를 만들도록 하고, nginx-data 라는 볼륨을 helloweb-disk라는 PVC를 이용해서 마운트 하는 YAML 설정이다. 이 설정을 실행해보면 아래 그림과 같이 nginx-2784n Pod 하나만 생성된다.




%kubectl describe pod nginx-6w9xf

명령을 이용해서 다른 Pod가 기동되지 않는 이유를 조회해보면 다음과 같은 결과를 얻을 수 있다.



내용중에 중요한 내용을 보면 다음과 같다.


“Multi-Attach error for volume "pvc-d930bfcb-2ec0-11e9-8d43-42010a920009" Volume is already used by pod(s) nginx-2784n”


앞에서 설명한 대로, 볼륨(PV)이 다른 Pod (nginx-2784n)에 의해 이미 사용되고 있기 때문에 볼륨을 사용할 수 없고, 이로 인해서, Pod 생성이 되지 않고 있는 상황이다.


RS로 이를 해결 하려면 아래 그림과 같이 Pod 마다 각각 RS을 정의하고, Pod마다 각기 다른 PVC와 PV를 바인딩하도록 설정해야 한다.



그러나 이렇게 Pod 마다 별도로 RS와 PVC,PV를 정의하는 것은 편의성 면에서 쉽지 않다.

StatefulSet

그래서 상태를 유지하는 데이타베이스와 같은 애플리케이션을 관리하기 위한 컨트롤러가 StatefulSet 컨트롤러이다. (StatefulSet은 쿠버네티스 1.9 버전 부터 정식 적용 되었다. )

StatefulSet은 앞에서 설명한 RS등의 Stateless 애플리케이션이 관리하는 컨트롤러로 할 수 없는 기능들을 제공한다. 대표적인 기능들은 다음과 같다.

Pod 이름에 대한 규칙성 부여

StatefulSet에 의해서 생성되는 Pod들의 이름은 규칙성을 띈다. 생성된 Pod들은 {Pod 이름}-{순번} 식으로 이름이 정해진다. 예를 들어 Pod 이름을 mysql 이라고 정의했으면, 이 StatefulSet에 의해 생성되는 Pod 명들은 mysql-0, mysql-1,mysql-2 … 가 된다.

배포시 순차적인 기동과 업데이트

또한 StatefulSet에 의해서 Pod가 생성될때, 동시에 모든 Pod를 생성하지 않고, 0,1,2,.. 순서대로 하나씩 Pod를 생성한다. 이러한 순차기동은 데이타베이스에서 마스터 노드가 기동된 후에, 슬레이브 노드가 기동되어야 하는 조건등에 유용하게 사용될 수 있다.

개별 Pod에 대한 디스크 볼륨 관리

RS 기반의 디스크 볼륨 관리의 문제는 하나의 컨트롤러로 여러개의 Pod에 대한 디스크를 각각 지정해서 관리할 수 없는 문제가 있었는데, StatefulSet의 경우 PVC (Persistent Volume Claim)을 템플릿 형태로 정의하여, Pod 마다 각각 PVC와 PV를 생성하여 관리할 수 있도록 한다.


그럼 StatefulSet 예제를 보자


apiVersion: apps/v1

kind: StatefulSet

metadata:

name: nginx

spec:

selector:

  matchLabels:

    app: nginx

serviceName: "nginx"

replicas: 3

template:

  metadata:

    labels:

      app: nginx

  spec:

    terminationGracePeriodSeconds: 10

    containers:

    - name: nginx

      image: k8s.gcr.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" ]

    storageClassName: "standard"

    resources:

      requests:

        storage: 1Gi


RS나 RC와 크게 다른 부분은 없다. 차이점은 PVC를 volumeClaimTemplate에서 지정해서 Pod마다 PVC와 PV를 생성하도록 하는 부분이다. 위의 볼드처리한 부분


이 스크립트를 실행하면 아래와 같이 Pod가 배포 된다.



pod의 이름은 nginx-0,1,2,... 식으로 순차적으로 이름이 부여되고 부팅 순서도 0번 pod가 기동되고 나면 1번이 기동되고 다음 2번이 기동되는 식으로 순차적으로 기동된다.


template에 의해서 PVC가 생성되는데, 아래는 생성된 PVC 목록이다. 이름은 {StatefulSet}-{Pod명} 식으로 PVC가 생성이 된것을 확인할 수 있다.


그리고 마지막으로 아래는 PVC에 의해서 생성된 PV(디스크 볼륨)이다.

기동 순서의 조작

위의 예제에 보는것과 같이, StatefulSet은 Pod를 생성할때 순차적으로 기동되고, 삭제할때도 순차적으로 (2→ 1 → 0 생성과 역순으로) 삭제한다. 그런데 만약 그런 요건이 필요 없이 전체가 같이 기동되도 된다면 .spec.podManagementPolicy 를 통해서 설정할 수 있다.

.spec.podManagementPolicy 는 디폴트로 OrderedReady 설정이 되어 있고, Pod가 순차적으로 기동되도록 설정이 되어 있고, 병렬로 동시에 모든 Pod를 기동하고자 하면  Parallel 을 사용하면 된다.

아래는 위의 예제에서 podManagementPolicy를 Parallel로 바꾼 예제이다.

apiVersion: apps/v1

kind: StatefulSet

metadata:

name: nginx

spec:

selector:

  matchLabels:

    app: nginx

serviceName: "nginx"

podManagementPolicy: Parallel

replicas: 3

template:

  metadata:

    labels:

      app: nginx

  spec:

    terminationGracePeriodSeconds: 10

    containers:

:

Pod Scale out and in

지금까지 StatefulSet에 대한 개념과 간단한 사용방법에 대해서 알아보았다. 그러면, StatefulSet에 의해 관리되는 Pod가 장애로 인하거나 스케일링 (In/out)으로 인해서 Pod의 수가 늘거나 줄면 그에 연결되는 디스크 볼륨은 어떻게 될까?


예를 들어 아래 그림과 같이 Pod-1,2,3 이 기동되고 있고, 이 Pod들은 StatefulSet에 의해서 관리되고 있다고 가정하자. Pod들은 각각 디스크 볼륨 PV-1,2,3 을 마운트해서 사용하고 있다고 하자.



이때, Pod-3가 스케일인이 되서, 없어지게 되면, Pod는 없어지지면, 디스크 볼륨을 관리하기 위한 PVC-3는 유지 된다. 이는 Pod 가 비정상적으로 종료되었을때 디스크 볼륨의 내용을 유실 없이 유지할 수 있게 해주고, 오토 스케일링이나 메뉴얼로 Pod를 삭제했을때도 동일하게 디스크 볼륨의 내용을 유지하도록 해준다.



그러면 없앴던 Pod가 다시 생성되면 어떻게 될까? Pod가 다시 생성되면, Pod 순서에 맞는 번호로 다시 생성이 되고, 그 번호에 맞는 PVC 볼륨이 그대로 붙게 되서, 다시 Pod 가 생성되어도 기존의 디스크 볼륨을 그대로 유지할 수 있다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.


PodDisruptionBudget

조대협 (http://bcho.tistory.com)


Pod의 개수는 컨트롤러가 붙어 있을 경우에는 컨트롤러 스펙에 정의된 replica 수 만큼을 항상 유지 하도록 되어 있다. Pod의 수가 replica의 수를 유지 하지 못하고 줄어드는 경우가 있는데, 애플리케이션이 크래쉬 나거나, VM이 다운되는 등의 예상하지 못한 사고로 인한 경우가 있고, 또는 시스템 관리자가 업그레이드등의 이슈로 노드를 인위적으로 다운 시키는 것과 같이 예상 가능한 상황이 있다.

예상 가능한 상황에서 Pod가 없어지는 것을 Voluntary disruptions 라고 하고, 커널 패닉이나 VM 크래쉬같은 예기치 못한 상황에서 Pod가 없어지는 것을 Involuntary disruptions 라고 한다.


노드를 인위적으로 줄일 경우(Voluntary disruptions) 그 노드에 Pod가 여러개 돌고 있을 경우 순간적으로 Pod의 수가 줄어들 수 있다. 예를 들어 웹 서버 Pod가 노드1에 5개, 노드 2에 8개가 돌고 있을 때 노드1을 다운 시키면 순간적으로 Pod의 총 수가 8개가 된다. replica수에 의해서 복귀는 되겠지만, 성능을 유지하기 위해서 일정 수의 Pod 수를 유지해야 하거나, NoSQL 처럼 데이타 저장에 대한 안정성을 확보하기 위해서 쿼럼값만큼  최소 Pod를 유지해야 하는 경우 , 이런 노드 다운은 문제가 될 수 있다.


그래서 인위적인 노드 다운등과 같이 volutary disruption 상황에도 항상 최소한의 Pod수를 유지하도록 해주는 것이 PodDistruptionBudget (이하 PDB)이라는 기능이다.  PDB를 설정하면 관리자가 노드 업그레이드를 위해서 노드를 다운 시키거나 또는 오토스케일러에 의해서 노드가 다운될 경우, Pod수를 일정 수 를 유지하지 못하면 노드 다운이나 오토스케일러에 의한 스케일 다운등을 막고, Pod 수를 일정 수준으로 유지할 수 있을때 다시 그 동작을 하도록 한다.


예를 들어 nginx-pod가 2개가 유지되도록 PDB를 설정했다고 가정하자


node-1

node-2