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


Archive»


 
 

[팁] 쿠버네티스 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를 젠킨스 플러그인으로 설치한후에, 컨테이너 보안 취약점을 스캔한 결과를 출력해준 화면이다.




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

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

node-3

nginx-pod-1

nginx-pod-2

nginx-pod-3


위와 같은 상태에서 시스템 관리자가 node-1에 대한 패치를 위해서 node-1을 다운 시키려고 node draining 명령을 실행했다고 하자. 그럼 node-1에 있는 nginx-pod-1은 다른 node로 옮겨진다.(이를 eviction이라고 한다. ) 구체적으로는 nginx-pod-1 은 terminate 되고, node-2나 node-3에 새로운 pod가 생기게 된다. 여기서는 node-2에 nginx-pod-4라는 이름으로 새로운 pod가 생성된다고 가정하자


node-1 (draining)

node-2

node-3

nginx-pod-1(terminating)

nginx-pod-2

nginx-pod-4 (starting)

nginx-pod-3


이때 마음급한 관리자가 nginx-pod-4가 기동되는 것을 기다리지 않고, node-2를 패치하기 위해서 node-3 를 다운 시키려고 draining 명령을 내렸다고 하자.

이 경우 nginx-pod-4가 아직 기동중인 상태이기 때문에, 현재 사용 가능한 pod는 nginx-pod-2 와 nginx-pod-3 2개 뿐인데, node-3를 다운 시키면 nginx-pod-2 하나 밖에 남지 않기 때문에, PDB에서 정의한 pod의 개수 2개를 충족하지 못하기 때문에, node-3에 대한 다운 작업은 블록 된다.


PDB는 YAML 파일을 이용해서 리소스로 정의될 수 있다.

PDB에서 정의 하는 방법은 minAvailable 정의할 경우 앞의 예제에서 설명한 바와 같이 최소한 유지해야 하는 Pod의 수를 정의할 수 있고 또는 maxUnavailable 을 정의해서 최대로 허용할 수 있는 Unhealthy한 Pod 의 수를 정의할 수 있다. 이 두 값은 Pod의 숫자로 정의할 수 도 있고 또는 %를 사용할 수 있다.


예를 들어

  • minAvailable 가 5이면, 최소한 5개의 Pod 는 항상 정상 상태로 유지하도록 한다.

  • minAvailable 가 10%이면,  Pod의 replica 수를 기준으로 10%의 Pod 는 항상 정상 상태로 유지하도록 한다.

  • maxUnavailable 가 1이면, 최대 한개의 Pod만 비정상 상태를 허용하도록 한다.


그러면 이렇게 정의한 값들은 어떻게 Pod에 적용할까, PDB를 만든후에, 이 PDB안에 label selector를 정의해서 PDB를 적용할 Pod를 선택할 수 있다.


완성된 예제는 다음과 같다.

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
 name: zk-pdb
spec:
 minAvailable: 2
 selector:
   matchLabels:
     app: zookeeper


< 출처 : https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget >


이 예제는 app: zookeeper라고 되어 있는 Pod 들을 최소 2개는 항상 유지할 수 있도록 하는 설정이다.

지금까지 간단하게 PDB에 대한 개념을 설명하였다. 일반적인 Stateless 애플리케이션에는 크게 문제가 없겠지만, 일정 성능 이상을 요하는 애플리케이션이나, NoSQL과 같은 일정 수의 쿼럼 유지가 필요한 애플리케이션등의 경우에는 PDB를 정의하는 것이 좋다.


참고로 현재 PDB는 베타 상태이고 정식 릴리즈 상태는 아니기 때문에, 운영 환경에는 아직 사용하지 않는 것이 좋다.

PodPreset


애플리케이션을 배포하는데 있어서, 애플리케이션 실행 파일과과 설정 정보를 분리해서 환경에 따라 설정 정보만 변경해서 애플리케이션을 재배포하는 방법이 가장 효율적이다.  


쿠버네티스에서는 ConfigMap을 이용하여 이러한 정보를 환경 변수로 넘기거나, 또는 파일이나 디렉토리로 마운트하는 방법을 제공해왔다. ConfigMap을 이용하는 경우 ConfigMap에서 여러 정보를 꺼내오도록 해야 한다. 또한 반복적으로 유사한 애플리케이션을 배포하고자 할때, 디스크가 필요한 경우에는 볼륨과 그 마운트에 대한 정의를 해야하기 때문에, 애플리케이션을 배포하는 Pod의 설정 파일이 다소 복잡해질 수 있다.


그래서 새롭게 소개되는 개념이 PodPreset 이다.

PodPreset에는 환경 변수 이외에도, 볼륨과 볼륨 마운트 정보를 정의할 수 있다.

이렇게 정의된 PodPreset은 label selector를 통해서 적용할 Pod를 지정할 수 있다.

label selector에 의해서 지정된 Pod는 Pod가 생성될때, PodPreset에 지정된 정보가 추가되서 Pod가 생성된다.


이해가 잘 안될 수 있는데, 예제를 살펴보는게 가장 빠르다. 예제는 쿠버네티스 공식 홈페이지에 있는 예제를 사용하였다.

https://kubernetes.io/docs/tasks/inject-data-application/podpreset/#simple-pod-spec-example



아래는 PodPreset  예제이다.

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
 name: allow-database
spec:
 selector:
   matchLabels:
     role: frontend
 env:
   - name: DB_PORT
     value: "6379"
 volumeMounts:
   - mountPath: /cache
     name: cache-volume
 volumes:
   - name: cache-volume
     emptyDir: {}


env 부분에서 DB_PORT 환경 변수에  데이타 베이스 접속 포트를 정해주었고, /cache 디록토리에 emptyDir을 마운트 하도록 하는 설정이다. 이 PodPreset을 role: frontend 인 Pod 들에 적용하도록 하였다.


아래는 Pod 정의 예제이다.


apiVersion: v1
kind: Pod
metadata:
 name: website
 labels:
   app: website
   role: frontend
spec:
 containers:
   - name: website
     image: nginx
     ports:
       - containerPort: 80

내용을 보면 알겠지만, 간단하게 nginx 웹서버를 실행하는 Pod 설정 파일이다.

그런데 이 Pod를 배포하게 되면 앞에 PodPreset에서 Selector로 role:frontend 를 적용하도록 하였기 때문에, 이 Pod에는 앞에서 정의한 PodPreset이 적용되서 실제 배포 모양은 아래와 같이 된다.


apiVersion: v1
kind: Pod
metadata:
 name: website
 labels:
   app: website
   role: frontend
 annotations:
   podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
 containers:
   - name: website
     image: nginx
     volumeMounts:
       - mountPath: /cache
         name: cache-volume
     ports:
       - containerPort: 80
     env:
       - name: DB_PORT
         value: "6379"
 volumes:
   - name: cache-volume
     emptyDir: {}


원래 Pod 설정 파일에 있는 내용에 추가해서 env, volumeMount, volumes 부분이 PodPreset에 정의된데로 추가 적용된것을 확인할 수 있다.


Configmap이 이미 있는데, 이런게 왜 필요할까 하고 잠깐 살펴봤는데, 편하다. 쿠버네티스에서 디스크 볼륨 설정이 그다지 간략하다고 말하기가 어렵고 그래서, 개발자 입장에서 애플리케이션을 간략하게 배포하고 싶은데, 디스크에 대한 설정을 해야 하는데, 이 부분을 모두 PodPreset에서 관리자가 해줄 수 있는 구조로 빠질 수 있다. 아직 알파단계 API이기 때문에 환경 정보와 볼륨 관련 정보만 지정하는데, 정식 버전에서는 좀 더 많은 설정 정보를 제공했으면 한다.



Ksonnet

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


쿠버네티스의 리소스 배포는 YAML 스크립트를 기반으로 한다.

하나의 마이크로 서비스를 배포하기 위해서는 최소한 Service, Deployment 두개 이상의 배포 스크립트를 작성해야 하고, 만약에 디스크를 사용한다면 Persistent Volume (aka PV)와 Persistent Volume Claim (PVC)등 추가로 여러 파일을 작성해서 배포해야 한다.

그런데 이러한 배포 작업을 보면, 사실 비슷한 성격의 마이크로 서비스간에는 중복 되는 부분이 많다.


예를 들어 간단한 웹서비스 (node.js나 springboot)를 배포할때는 Service의 타입을 지정하고, Deployment에 의해서 관리되는 Pod의 수, 그리고 컨테이너 이미지 정도만 지정하면 기본적인 배포는 가능하다. 여기에 필요하다면, 요구되는 리소스 (CPU, 메모리) 정도만 지정하면 된다.

그러나 이를 위해서는 Service와 Deployment를 정의한 긴 YAML 파일을 작성해야 한다.

그렇다면 이러한 반복작업을 단순화하고, 환경(운영/개발, 또는 미국/유럽과 같은 다른 리전등)에도 반복적으로 재활용할 수 있는 방법은 없을까?


이런 목적에서 여러가지 솔루션이 소개되는데, 그중에 널리 사용되고 있는 것은 패키지 매니저인 Helm이 있다. Helm은 node.js 의 npm이나, 리눅스의 apt 유틸리티와 유사하게, 패키지 형태로 쿠버네티스 리소스를 배포할 수 있도록 해준다. 이외에도 여러가지 솔루션이 있는데, 오늘 이야기하는 솔루션은 ksonnet이라는 오픈소스로, 템플릿엔진 형태의 특징을 가지고 있는 솔루션이다.


Ksonnet

ksonnet은 JSON 기반의 템플릿 엔진인 jsonnet (https://jsonnet.org/)을 기반으로 한다. 기본적으로 템플릿 엔진의 특성을 가지고 있는데, 특이점이 인프라를 지원하는데 있어서 일종의 프로그래밍 언어적인 컨셉을 지원한다.

그러면 기본적인 개념을 이해 해보도록 하자


Prototype, Parameter & Component



먼저 Prototype 이라는 개념을 이해 해야하는데, prototype의 템플릿의 개념으로, 객체 지향형 프로그램에서 클래스의 개념으로 생각하면 된다. 이 prototype을 이용해서 component를 만들어 낸다. component는 객체 지향형에서 객체의 개념으로 생각하면 된다. 이렇게 만들어진 컴포넌트에 속성(변수)를 설정하면 되는데, Deployment 이름이나, 컨테이너 이미지등 실배포에서 애플리케이션 서비스별로 필요한 구체적인 값을 입력한다. 이 값을 입력하는 방법은 parameter 라는 리소스로 정의해서, 필요한 값을 전달한다.

이렇게 값이 채워진 component는 각 서비스 배포에 필요한 쿠버네티스 리소스들을 정의한 쿠버네티스 manifest 파일로 생성된다. (참고 : YAML이 아니라 JSON으로 생성되는데, 쿠버네티스의 manifest는 YAML뿐만 아니라 JSON으로도 설정이 가능하다. )


prototype에서 component를 생성하는 방식은 앞에서도 설명하였듯이, 하나의 prototype에서 parameter만 바꿔서 생성하면 다른 서비스에 대한 component를 반복적으로 생성할 수 있다. 아래 개념 그림은 하나의 prototype에서 도커 이미지와 서비스 타입만 변경해서 Guestbook과 BookStore 서비스에 대해서 각각의 컴포넌트를 생성하는 개념이다.



Environment

이렇게 생성된 component는 쿠버네티스에 적용해서, 실제 쿠버네티스 리소스를 만드는데, ksonnet은 동시에 여러개의 쿠버네티스 환경을 지원할 수 있다. 이를 ksonnet 안에서는 environment 라는 리소스로 정의하는데, 여러개의 쿠버네티스 클러스터를 하나의 ksonnet 프로젝트 안에 등록해놓고, 생성된 component를 등록된 쿠버네티스 환경중 하나에 배포한다. ksonnet의 environment는 쿠버네티스 클러스터에 대한 정보 (예를 들어 마스터 서버의 IP)를 가지고 있다.



Part

그런데 Prototype도 유사점이 있다. 예를 들어 redis 배포를 예를 들었을때, 어떤 redis 배포는 외장 디스크 (PV)를 붙여서 배포할 수 도 있고, 아니면 내장 디스크 (PV 없이) 배포할 수 있다. 비슷한 타입의 prototype의 반복적인 작업을 막기 위해서 prototype은 재사용이 가능한 자원으로 조합해서 정의 될 수 있다. 이를 ksonnet에서는 part라고 정의한다.




위의 그림을 보면 우측에 두가지 프로토 타입이 정의된것을 볼 수 있다. 하나는 PVC(Persistent Volume Claim)이 있는 것이고 하나는 없는 것이데, 이외에 차이가 없고 Service나 Deployment는 정의는 동일 하기 때문에 이를 part로 정의한 후에, 이 part를 조합하여 다른 prototype을 구성할 수 있다.

Application

하나의 애플리케이션은 여러개의 마이크로 서비스로 구성이 된다. 그래서 하나의 애플리케이션을 배포하기 위해서는 여러개의 Prototype과 component들이 필요하게 되는데, 하나의 쿠버네티스 클러스터에는 열개의 애플리케이션이 설치될 수 있기 때문에, 이 애플케이션 단위로, 리소스를 묶을 수 있는 바운더리 개념이 필요하다. ksonnet에서는 이 개념을 application이라는 단위로 정의해서, 이 애플리케이션 안에, 여러개의 prototype, part, component, environment를 묶을 수 있게 한다.

그래서 이 애플리케이션 단위로 설치도 가능하고 반대로 애플리케이션 단위로 설치된 애플리케이션을 삭제할 수 있다.

Package

Part와 Prototype은 다른 애플리케이션에서도 재 사용될 수 있다. 예를 들어 redis 설치 스크립트나, nginx 설치 스크립트 역시 어느 애플리케이션에서나 재 사용될 수 있고, 머신러닝 패키지인 kueflow와 같은 복잡한 애플리케이션의 경우에는 이 prototype과 part들만 있으면 손쉽게 설치될 수 있다. 이렇게 각 모듈들을 재사용 가능하도록 묶어놓은 것을 ksonnet에서는 package라고 한다. Package 안에는 일반적으로 아래와 같이 재 사용이 가능한 part와 prototype들이 들어가 있다.


Registry

그러면 이 패키지들 어디에 저장하고 어떻게 내 애플리케이션에 반영할까? ksonnet에서는 npm이나 docker repository처럼 이러한 패키지들을 저장할 수 있는 레파지토리 개념을 제공하는데, 이것이 바로 registry다.

Github repository나, 파일 시스템 Helm repository를 registry로 사용할 수 있다.


작성한 package는 이 registry에 저장할 수 있으며, 저장된 package들은 package install 명령어를 이용해서 설치한다.



Hello ksonnet

ksonnet의 개념을 이해했으면, 실제 테스트를 통해서 구체적은 사용법을 알아보도록 하자. 테스트를 위해서는 쿠버네티스 클러스터가 설치되어 있어야 하는데, 쿠버네티스 환경이 없는 경우에는 온라인에서 테스트할 수 있는 환경이 https://ksonnet.io/tour/welcome/ 에 있으니,이를 활용하는 것도 하나의 방법이다.

여기서 사용한 예제는 ksonnet 공식 홈페이지에 나와 있는 Guestbook 예제이다. https://ksonnet.io/docs/tutorial/guestbook/ 함축적으로 설명을 해놓은 간단한 예제라서 디테일한 내용을 이해하기는 어렵지만 대략적인 컨셉과 흐름을 이해하는데는 큰 무리가 없다.

애플리케이션 생성

먼저 애플리케이션을 생성한다. ks init 명령을 이용하면 애플리케이션 디렉토리가 생성되고, 필요한 초기 리소스들이 생성된다. 일종의 boiler plate와 같은 개념으로 보면 된다.

%ks init guest book

생성이 되면 아래와 같은 디렉토리 구조로 생성이 된다.

components에는 컴포넌트 파일과 parameter 등이 저장되는데, 정의된 컴포넌트가 없기 때문에, 컴포넌트 관련 파일은 없이 비어 있다.

Environment 디렉토리는 쿠버네티스 환경에 대한 정보가 들어있는데, 기본 환경값은 defaults라는 디렉토리에 저장된다.



Environment 생성

현재 환경을 cloud 라는 이름으로 등록해보자. 환경 등록은 ks add env라는 명령을 사용하면 된다. 등록해야하는 대상 쿠버네티스 클러스터가 많을 경우, 다른 환경을 추가로 등록하면 된다.

%ks add env cloud

Component 생성

다음으로 컴포넌트를 생성하는데, 등록된 prototype을 기반을 component를 생성해야 하는데, ks application을 생성하면 디폴트로 등록되어 있는 prototype이 있다. 등록되어 있는 prototype 목록은 ks prototype list 명령으로 조회가 가능하다.


%ks prototype list




여기서 사용할 prototype은 deployed-service를 사용하는데, image와 type 만을 지정한다.

이 prototype은 웹서비스를 위해서 쿠버네티스 리소스인 Service 와 Deployment를 생성해준다.

deployed-service prototype을 이용해서, 컴포넌트를 생성하기 위해서는 ks generate deployed-service {컴포넌트명} 으로 생성하고, 필요한 parameter는 --{parameter name}={parameter value) 식으로 지정한다.


% ks generate deployed-service guestbook-ui \

 --image gcr.io/heptio-images/ks-guestbook-demo:0.1 \

 --type ClusterIP


Component 생성이 끝나면 ~/components/ 디렉토리에 컴포넌트 파일이 생성된다.


생성된 컴포넌트 파일의 일부를 보면 다음과 같다.  컴포넌트들이 생성되고, parameter 처리되어 있는 부분은 아래 마크한바와 같이 parameter value를 각각 항목마다 지정되어 있다.


~/components/guestbook-ui.libsonnet

중략

:

 {

   "apiVersion": "apps/v1beta2",

   "kind": "Deployment",

   "metadata": {

     "name": params.name

   },

   "spec": {

     "replicas": params.replicas,

     "selector": {

       "matchLabels": {

         "app": params.name

       },

     },

     "template": {

       "metadata": {

         "labels": {

           "app": params.name

         }

       },

       "spec": {

         "containers": [

           {

             "image": params.image,

             "name": params.name,

             "ports": [

               {

                 "containerPort": params.containerPort

               }

             ]

           }

         ]

       }



     "name": params.name

   },

   "spec": {

     "ports": [

       {

         "port": params.servicePort,

         "targetPort": params.containerPort

       }

     ],

     "selector": {

       "app": params.name

     },

     "type": params.type

   }

 },


이하 생략

그리고 parameter로 지정한 내용들은 별도의 param.libsonnet 이라는 파일에 정의된다. 아래 그림과 같이 image명이나 type들이 앞에서 ks generate 단계에서 지정한 내용으로 정의된것을 확인할 수 있다. 다른 값들은 prototype에서 정의된 디폴트 값으로 채워진다.


~/components/param.libsonnet

local env = std.extVar("__ksonnet/environments");

local params = std.extVar("__ksonnet/params").components["guestbook-ui"];

[

중략

 components: {

   // Component-level parameters, defined initially from 'ks prototype use ...'

   // Each object below should correspond to a component in the components/ directory

   "guestbook-ui": {

     containerPort: 80,

     image: "gcr.io/heptio-images/ks-guestbook-demo:0.1",

     name: "guestbook-ui",

     replicas: 1,

     servicePort: 80,

     type: "ClusterIP",

   },

중략

생성된 Manifest 확인하기

생성된 컴포넌트가 각 쿠버네티스 환경에 어떻게 적용되는지를 살펴보기 위해서는  ks show {환경이름} 명령을 실행하면, 쿠버네티스에 적용된 YAML manifest 파일을 먼저확인해볼 수 있다.

%ks show cloud


다음은 “cloud” 라는 환경에 반영할 수 있는 YAML manifest 파일이다.

아래 그림은 YAML 파일중에서 Deployment 파트인데,  아래 그림고 같이 name이나 label등 구체적인 설정 값들이 앞에서 지정한 parameter 값으로 채워진것을 확인할 수 있다.

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 labels:

   ksonnet.io/component: guestbook-ui

 name: guestbook-ui

spec:

 replicas: 1

 selector:

   matchLabels:

     app: guestbook-ui

 template:

   metadata:

     labels:

       app: guestbook-ui

   spec:

     containers:

     - image: gcr.io/heptio-images/ks-guestbook-demo:0.1

       name: guestbook-ui

       ports:

       - containerPort: 80



중략

Manifest 적용

이렇게 컴포넌트가 정의되었으면, 대상 쿠버네티스를 선택해서 ks apply 명령으로 적용 하면 된다. 아래 명령어는 “cloud” 라는 환경에 생성된 컴포넌트들을 적용하는 명령이다.

%ks apply cloud

적용된 서비스 확인 및 테스트

쿠버네티스에 적용된 리소스를 확인해보자. kubectl get svc와 kubectl get pod를 실행해보면 아래와 같이 Service와 Pod들이 생성된것을 확인할 수 있다.

%kubectl get svc




%kubectl get pod




생성된 서비스를 테스트해보자. 서비스를 ClusterIP로 생성하였기 때문에, 외부 IP를 통해서 접속은 불가능하고, kubectl proxy를 이용해서 프록시를 세팅해서 접속해야 한다. 아래는 kubectl proxy 명령어를 이용해서 proxy를 생성하고 접속을 하는 예제이다.

%kubectl proxy > /dev/null &

%KC_PROXY_PID=$!

%SERVICE_PREFIX=http://localhost:8001/api/v1

%GUESTBOOK_URL=$SERVICE_PREFIX/namespaces/default/services/guestbook-ui/proxy/


%open $GUESTBOOK_URL



프록시를 통해서 접속한 웹사이트에 대한 결과이다.


간단하게 컨셉과 예제를 통해서 실제 사용 방법을 확인해보았다.

ksonnet은 쿠버네티스의 다른 YAML 파일 배포 시스템과 다르게 prototype,component,application,environment 등과 같이 잘 추상화된 개념을 가지고 있으며, 특히 배포를 위해서 쿠버네티스에 무엇인가를 설치할 필요가 없이 클라이언트에 ks 툴만 설치하면 되기 때문에, 쿠버네티스 설치에 대한 의존성이 없이 깔끔하게 설치 및 운영이 가능하다.

또한 파일 형태로 모든 리소스를 관리할 수 있기 때문에 gitOps와 같은 형상 관리 시스템을 통해서 리소스를 관리할 수 있기 때문에, 변경 추적이나 코드 재 사용등에 많은 장점을 가질 수 있다.



컨테이너 기반의 워크플로우 솔루션 argo

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


argo는 컨테이너 워크플로우 솔루션이다.

컨테이너 기반으로 빅데이타 분석, CI/CD, 머신러닝 파이프라인을 만들때 유용하게 사용할 수 있는 오픈 소스 솔루션으로 개념은 다음과 같다.


워크플로우를 정의하되 워크플로우의 각각의 스텝을 컨테이너로 정의한다.

워크플로우 스펙은 YAML로 정의하면, 실행할때 마다 컨테이너를 생성해서, 작업을 수행하는 개념이다.


기존에 아파치 에어플로우 (https://airflow.apache.org/)등 많은 워크 플로우 솔루션이 있지만, 이러한 솔루션은 컴포넌트가 VM/컨테이너에서 이미 준비되서 돌고 있음을 전제로 하고, 각각의 컴포넌트를 흐름에 따라서 호출하는데 목적이 맞춰서 있다면, argo 의 경우는 워크플로우를 시작하면서 컨테이너를 배포하고, 워크플로우 작업이 끝나면 컨테이너가 종료되기 때문에, 실행할때만 컨테이너를 통해서 컴퓨팅 자원을 점유하기 때문에 자원 활용면에서 장점이 있다고 볼 수 있다.


argo 설치는 쿠버네티스 클러스터가 있는 상태라면 https://argoproj.github.io/docs/argo/demo.html 를 통해서 간단하게 설치가 가능하다. 설치와 사용법은 위의 문서링크를 활용하기 바란다.

HelloWorld

간단한 워크플로우 예제를 살펴보자. 워크 플로우를 실행하기 위해서는 워크플로우 스펙을 yaml 파일로 정의해야 한다. 아래는 helloworld 의 간단한 예제이다.


apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: hello-world-

spec:

 entrypoint: whalesay

 templates:

 - name: whalesay

   container:

     image: docker/whalesay:latest

     command: [cowsay]

     args: ["hello world"]


워크플로우의 이름은 metadata 부분에 generateName에서 워크플로우 JOB의 이름을 정의할 수 있다. 여기서는 hello-world-로 정의했는데, 작업이 생성될때 마다 hello-world-xxx 이라는 이름으로 작업이 생성된다.

Templates 부분에 사용하고자 하는 컨테이너를 정의한다. 위의 예제에서는 docker/whalesay:latest 이미지로 컨테이너를 생성하도록 하였고, 생성후에는 “cosway”라는 명령어를 “hello world” 라는 인자를 줘서 실행하도록 하였다.

Template 부분에는 여러개의 컨테이너를 조합하여, 어떤 순서로 실행할지를 정의한다. 이 예제에서는 하나의 컨테이너만 실행하도록 정의하였다.

다음에 어느 컨테이너 부터 시작하게 할것인지는 sepc 부분에 정의하고, 시작 부분은 entrypoint라는 구문을 이용해서 정의할 수 있다.이 예제에서는 template에 정의한 whalesay라는 컨테이너부터 실행하도록 한다.

이렇게 생성된 워크플로우 스펙은 “argo submit”이라는 CLI 명령을 이용해서 실행한다.


%argo submit --watch {yaml filename}


워크플로우가 실행되면 각 단계별로 쿠버네티스 Pod 가 생성되고, 생성 결과는 argo logs {pod name}으로 확인할 수 있다.


%argo list

명령을 이용하면 argo 워크플로우의 상태를 확인할 수 있다.



위의 그림과 같이 hello-world는 hello-world-smjxq 라는 작업으로 생성되었다.

Pod 명은 이 {argo 작업이름}-xxx 식으로 명명이 된다.

%kubectl get pod

명령으로 확인해보면 아래 그림과 같이 hello-world-smjxq 라는 이름으로 pod가 생성된것을 확인할 수 있다.


이 pod의 실행 결과를 보기 위해서

%argo logs hello-world-smjxq

명령을 실행하면 된다.


위의 그림과 같이 고래 그림을 결과로 출력한것을 확인할 수 있다.

ArgoUI

워크플로우의 목록과 실행결과는 CLI뿐 아니라 웹 기반의 GUI에서도 확인이 가능하다.

argo ui는 argo라는 이름의 deployment에 생성이 되어 있는데, clusterIP (쿠버네티스 내부 IP)로 생성이 되어 있기 때문에 외부에서 접근이 불가능하다. 포트포워딩 기능을 이용해서 argo deployment의 8001 포트를 로컬 PC로 포워딩해서 접속할 수 있다.


% kubectl -n argo port-forward deployment/argo-ui 8001:8001


다음에 http://localhost:8001을 이용해서 접속해보면 다음과 같이 현재 등록되어 있는 워크플로우 목록을 확인할 수 있다.




이 목록에서 아까 수행한 hello-world-xxx 워크플로우를 확인해보자. 아래 그림과 같이 워크플로우의 구조를 보여준다.



hello-world-xxx 노드를 클릭하면 각 노드의 상세 내용을 볼 수 있다.


그림에서 Summary > Logs 부분을 선택하면 아래 그림과 같이 각 단계별로 실행한 결과 로그를 볼 수 있다.


연속된 작업의 실행

앞에서 간단한 설치 및 사용법에 대해서 알아봤는데, 앞에서 살펴본 예제는 하나의 태스크로 된 워크플로우이다. 워크플로우는 좀더 복잡하게 여러개의 태스크를 순차적으로 실행하거나 또는 병렬로 실행이 된다.


예제 원본 https://argoproj.github.io/docs/argo/examples/README.html


다음 워크플로우 정의를 보자

apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: steps-

spec:

 entrypoint: hello-hello-hello


 # This spec contains two templates: hello-hello-hello and whalesay

 templates:

 - name: hello-hello-hello

   # Instead of just running a container

   # This template has a sequence of steps

   steps:

   - - name: hello1            #hello1 is run before the following steps

       template: whalesay

       arguments:

         parameters:

         - name: message

           value: "hello1"

   - - name: hello2a           #double dash => run after previous step

       template: whalesay

       arguments:

         parameters:

         - name: message

           value: "hello2a"

     - name: hello2b           #single dash => run in parallel with previous step

       template: whalesay

       arguments:

         parameters:

         - name: message

           value: "hello2b"


 # This is the same template as from the previous example

 - name: whalesay

   inputs:

     parameters:

     - name: message

   container:

     image: docker/whalesay

     command: [cowsay]

     args: ["{{inputs.parameters.message}}"]


구조를 살펴보면 다음과 같다.



  • Metadata 부분에 generateName으로 이 워크플로우의 이름을 정의했다. 워크플로우 작업이 실행될때마다 steps-xxx  라는 이름으로 생성이 된다.

  • 워크 플로우는 templates 부분에 spec 부분에 정의하는데, hello1 작업을 수행한 후에, hello2a,hello2b 를 동시에 실행한다. 동시 실행인지 순차 실행인지는 steps의 인덴트(탭으로 띄워쓰기 한부분)을 확인하면 되는데, hello1은 - -name: hello1으로 정의 하였고 다음 단계는 - -name: hello2a와, - name:hello2b로 지정하였다. 잘 보면, hello2b는 “-”가 두개가 아니고 한개로 되어 있고 hello2a와 같은 띄어 쓰기로 되어 있는 것을 볼 수 있다.

  • 마지막으로 이 워크플로우에서 사용되는 컨테이너 이미지를 정의하면 된다.


실행시에 --watch 옵션을 주면, 각 단계별 실행 상태와, 워크플로우 그래프의 구조를 볼 수 있다.


%argo submit --watch helloworld-seq.yaml


위의 그림을 보면 step-h44qf라는 이름으로 작업이 수행되는데, hello1이 먼저 실행되고 다음에 hello2a,hello2b가 동시에 실행된것을 확인할 수 있다.


실행후에 UI에서 실행 내역을 확인해보면 다음과 같이 hello1이 먼저 실행 된 후에,  hello2a,hello2b가 병렬로 동시에 실행된것을 확인할 수 있다.



DAG를 이용한 워크플로우 정의

Yaml 파일 형식이 워크플로우의 실행 순서를 정의할 경우 명시성이 떨어져서 가독성 측면에서 읽기에 불편할 수 있는데, DAG(Directed acyclic graph)를 이용하면 조금 더 명시적으로 워크플로우 정의가 가능하다.



apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: dag-diamond-

spec:

 entrypoint: diamond

 templates:

 - name: echo

   inputs:

     parameters:

     - name: message

   container:

     image: alpine:3.7

     command: [echo, "{{inputs.parameters.message}}"]

 - name: diamond

   dag:

     tasks:

     - name: A

       template: echo

       arguments:

         parameters: [{name: message, value: A}]

     - name: B

       dependencies: [A]

       template: echo

       arguments:

         parameters: [{name: message, value: B}]

     - name: C

       dependencies: [A]

       template: echo

       arguments:

         parameters: [{name: message, value: C}]

     - name: D

       dependencies: [B, C]

       template: echo

       arguments:

         parameters: [{name: message, value: D}]


entrypoint는 diamond dag를 실행하도록 한다.

dag 정의 부분을 보면 맨 앞에 name: A인 task를 실행하도록 하고, 다음 B,C는 A에 의존성을 가지도록 한다.D는 B,C의 의존성을 가지게 해서 실행 순서는 A→ B,C → D형태가 된다.

다음과 같은 순서로 실행이 된다.



입력/출력값 전달

argo의 개념과 워크플로우의 개념을 이해했으면, 워크플로우에서 태스크간의 데이타를 어떻게 전달하는지 살펴보도록 하자.

입력값의 전달

argo에서 변수를 입력값으로 사용하는 방법은 간단하다. 먼저 변수를 정의한 다음에, 정의된 변수를 입력이나 출력으로 사용할지 워크플로우의 태스크에서 정의한후, 그 변수를 사용하면 된다.


apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: hello-world-parameters-

spec:

 # invoke the whalesay template with

 # "hello world" as the argument

 # to the message parameter

 entrypoint: whalesay

 arguments:

   parameters:

   - name: message

     value: hello world


 templates:

 - name: whalesay

   inputs:

     parameters:

     - name: message       #parameter declaration

   container:

     # run cowsay with that message input parameter as args

     image: docker/whalesay

     command: [cowsay]

     args: ["{{inputs.parameters.message}}"]


위의 코드를 살펴보면 먼저 spec.arguments 부분에서 message라는 변수를 선언 하였고, 그 값은 “hello world”로 초기화를 했다.

그리고 워크플로우의 whalesay 태스크에서 message 변수를 input 변수로 사용하도록 선언하였다. 그 후에, args에서 input.parameters.message를 참조하여 message변수의 값을 도커 컨테이너의 실행 변수로 넘기도록 하였다.


만약의 변수의 값을 CLI에서 바꾸고자 한다면 다음과 같이 argo submit시에 -p 옵션을 주면 된다. argo submit {workflow yaml file name} -p {parameter name}={value}

아래는 message=”hello terry”로 바꿔서 실행한 예이다.

ex) argo submit argument.yaml -p message=”hello terry”

출력값 사용

아래 코드를 보자.

아래 코드는 whalesay 컨테이너의 결과를 print-message 컨테이너로 넘기는 코드이다.


apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: output-parameter-

spec:

 entrypoint: output-parameter

 templates:

 - name: output-parameter

   steps:

   - - name: generate-parameter

       template: whalesay

   - - name: consume-parameter

       template: print-message

       arguments:

         parameters:

         # Pass the hello-param output from the generate-parameter step as the message input to print-message

         - name: message

           value: "{{steps.generate-parameter.outputs.parameters.hello-param}}"


 - name: whalesay

   container:

     image: docker/whalesay:latest

     command: [sh, -c]

     args: ["echo -n hello world > /tmp/hello_world.txt"]  #generate the content of hello_world.txt

   outputs:

     parameters:

     - name: hello-param       #name of output parameter

       valueFrom:

         path: /tmp/hello_world.txt    #set the value of hello-param to the contents of this hello-world.txt


 - name: print-message

   inputs:

     parameters:

     - name: message

   container:

     image: docker/whalesay:latest

     command: [cowsay]

     args: ["{{inputs.parameters.message}}"]


구조를 보면 다음과 같은데


먼제 whalesay 정의 부분을 보면 outputs.parameters에 hello-param이라는 이름으로 output 변수를 정의하였고, output의 내용은 /tmp/hello_world.txt 파일 내용으로 채워진다.


다음 print-message 컨테이너 정의 부분을 보면 input param으로 message라는 변수를 정의하였다.

steps를 보면, print-message를 실행할때, message 변수의 값을 {{steps.generate-parameter.outputs.parameters.hello-param}} 로 정의하여, print-message의 이전 스탭인 generate-parameter의 output param중에 hello-param이라는 변수의 값으로 채우는 것을 볼 수 있다.


이 흐름을 그림으로 도식화 해보면 다음과 같다.


Whalesay 컨테이너에서 /tmp/hello_world.txt 파일 내용을 hello-param이라는 output param으로 전달하고, print-message 컨테이너는 입력값으로 message라는 param을 받는데, 이 값을 앞단계의 hello-param의 값을 받도록 한것이다.

Artifact

워크플로우 태스크에 대한 입/출력값을 parameter로 전달할 수 도 있지만, CI/CD 빌드 파이프라인에서는 소스코드, 빌드 바이너리가 될 수 도 있고, 빅데이타 파이프라인에서는 데이타 파일과 같이 큰 사이즈의 파일이나 데이타가 될 수 있다. 이 경우 parameter 를 이용해서 넘기기에는 부담이 되는데, 이런 요구 사항을 위해서 제공되는 것이 artifact라는 기능이다. 태스크의 결과값을 로컬 스토리지가 아니라, AWS S3나 GCP GCS와 같은 외부 스토리지에 쓸 수 있게 하고, 반대로 태스크에 대한 입력 값을 외부 스토리지에서 읽어올 수 있게 하는 기능이다.


예를 들어 텐서플로우로 학습을 시키는 파이프라인이 있을때, 학습된 모델을 S3나 GCS에 저장하도록 하는 등의 작업을 할 수 있다.


아래 예제를 보면 앞에서 소개한 generate-artifact → consume-artifact 워크플로우에서 parameter로 값을 넘기는 방식이 artifact 방식으로 바뀐것을 확인할 수 있다. 단순하게 parameter 로 선언한 부분을 artifact로만 변경해주었다.


apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: artifact-passing-

spec:

 entrypoint: artifact-example

 templates:

 - name: artifact-example

   steps:

   - - name: generate-artifact

       template: whalesay

   - - name: consume-artifact

       template: print-message

       arguments:

         artifacts:

         # bind message to the hello-art artifact

         # generated by the generate-artifact step

         - name: message

           from: "{{steps.generate-artifact.outputs.artifacts.hello-art}}"


 - name: whalesay

   container:

     image: docker/whalesay:latest

     command: [sh, -c]

     args: ["cowsay hello world | tee /tmp/hello_world.txt"]

   outputs:

     artifacts:

     # generate hello-art artifact from /tmp/hello_world.txt

     # artifacts can be directories as well as files

     - name: hello-art

       path: /tmp/hello_world.txt


 - name: print-message

   inputs:

     artifacts:

     # unpack the message input artifact

     # and put it at /tmp/message

     - name: message

       path: /tmp/message

   container:

     image: alpine:latest

     command: [sh, -c]

     args: ["cat /tmp/message"]


기타

간단하게 기본 개념만 설명했지만, 이외에도 여러가지 기능을 이용하여 좀 더 복잡한 워크플로우를 구현할 수 있다. 예를 들어서 Condition 기능을 이용해서, 조건에 따라서 워크플로우를 분기하거나 조건에 따라 재귀호출을 하는 Recursive 호출, 호출중에 조건에 따라 워크플로우를 종료할 수 있는 기능등을 이용할 수 있다.

이외에도 컨테이너 배포시 사이드카(http://bcho.tistory.com/1256) 를 삽입하여 컨테이너 배포시 로그 수집이나 기타 기능등을 동시에 수행하도록 할 수 있다.


Istio #4 - 설치 및 BookInfo 예제

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

Istio 설치

그러면 직접 Istio 를 설치해보자, 설치 환경은 구글 클라우드의 쿠버네티스 환경을 사용한다. (쿠버네티스는 오픈소스이고, 대부분의 클라우드에서 지원하기 때문에 설치 방법은 크게 다르지 않다.)

쿠버네티스 클러스터 생성

콘솔에서 아래 그림과 같이 istio 라는 이름으로 쿠버네티스 클러스터를 생성한다. 테스트용이기 때문에, 한존에 클러스터를 생성하고, 전체 노드는 3개 각 노드는 4 CPU/15G 메모리로 생성하였다.



다음 작업은 구글 클라우드 콘솔에서 Cloud Shell내에서 진행한다.

커맨드 라인에서 작업을 할것이기 때문에, gCloud SDK를 설치(https://cloud.google.com/sdk/gcloud/) 한후에,

%gcloud auth login

gcloud 명령어에 사용자 로그인을 한다.


그리고 작업을 편리하게 하기 위해서 아래와 같이 환경 변수를 설정한다. 쿠버네티스 클러스터를 생성한 리전과 존을 환경 변수에 아래와 같이 설정한다. 예제에서는 asia-southeast1 리전에 asia-southeast1-c 존에 생성하였다. 그리고 마지막으로 생성한 쿠버네티스 이름을 환경 변수로 설정한다. 예제에서 생성한 클러스터명은 istio이다.

export GCP_REGION=asia-southeast1
export GCP_ZONE=asia-southeast1-c
export GCP_PROJECT_ID=$(gcloud info --format='value(config.project)')
export K8S_CLUSTER_NAME=istio

다음 kubectl 명령어를 사용하기 위해서, 아래과 같이 gcloud 명령어를 이용하여 Credential을 세팅한다

% gcloud container clusters get-credentials $K8S_CLUSTER_NAME \
   --zone $GCP_ZONE \
   --project $GCP_PROJECT_ID

Credential 설정이 제대로 되었는지

% kubectl get pod -n kube-system

명령어를 실행하여, 쿠버네티스 시스템 관련 Pod 목록이 잘 나오는지 확인한다.

Istio 설치

쿠버네티스 클러스터가 준비되었으면, 이제 Istio를 설치한다.

Helm 설치

Istio는 Helm 패키지 매니져를 통해서 설치 한다.

% curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
% chmod 700 get_helm.sh
% ./get_helm.sh

Istio 다운로드

Istio 를 다운로드 받는다. 아래는 1.0.4 버전을 다운 받는 스크립트이다.

% cd ~

% curl -L https://git.io/getLatestIstio | sh -

% cd istio-1.0.4

% export PATH=$PWD/bin:$PATH

Helm 초기화

Istio를 설치하기 위해서 Helm용 서비스 어카운트를 생성하고, Helm을 초기화 한다.

% kubectl create -f install/kubernetes/helm/helm-service-account.yaml

% helm init --service-account tiller

Istio 설치

다음 명령어를 이용하여 Istio를 설치한다. 설치시 모니터링을 위해서 모니터링 도구인 kiali,servicegraph 그리고 grafana 설치 옵션을 설정하여 아래와 같이 추가 설치 한다.

% helm install install/kubernetes/helm/istio \

--name istio \

--namespace istio-system \

--set tracing.enabled=true \

--set global.mtls.enabled=true \

--set grafana.enabled=true \

--set kiali.enabled=true \

--set servicegraph.enabled=true


설치가 제대로 되었는지 kubectl get pod명령을 이용하여, istio 네임스페이스의 Pod 목록을 확인해보자

% kubectl get pod -n istio-system




BookInfo 샘플 애플리케이션 설치

Istio 설치가 끝났으면, 사용법을 알아보기 위해서 간단한 예제 애플리케이션을 설치해보자, Istio에는 BookInfo (https://istio.io/docs/examples/bookinfo/)  라는 샘플 애플리케이션이 있다.

BookInfo 애플리케이션의 구조

아래 그림과 같이 productpage 서비스 안에, 책의 상세 정보를 보여주는 details 서비스와 책에 대한 리뷰를 보여주는 reviews 서비스로 구성이 되어 있다.  


시스템의 구조는 아래와 같은데, 파이썬으로 개발된 productpage 서비스가, 자바로 개발된 review 서비스과 루비로 개발된 details 서비스를 호출하는 구조이며, review 서비스는 v1~v3 버전까지 배포가 되어 있다. Review 서비스 v2~v3는 책의 평가 (별점)를 보여주는  Rating 서비스를 호출하는 구조이다



< 그림 Book Info 마이크로 서비스 구조 >

출처 : https://istio.io/docs/examples/bookinfo/

BookInfo 서비스 설치

Istio의 sidecar injection 활성화

Bookinfo 서비스를 설치하기 전에, Istio의 sidecar injection 기능을 활성화 시켜야 한다.

앞에서도 설명하였듯이 Istio는 Pod에 envoy 를 sidecar 패턴으로 삽입하여, 트래픽을 컨트롤 하는 구조이 다. Istio는 이 sidecar를 Pod 생성시 자동으로 주입 (inject)하는 기능이 있는데, 이 기능을 활성화 하기 위해서는 쿠버네티스의 해당 네임스페이스에 istio-injection=enabled 라는 라벨을 추가해야 한다.

다음 명령어를 이용해서 default 네임 스페이스에 istio-injection=enabled 라벨을 추가 한다.

% kubectl label namespace default istio-injection=enabled


라벨이 추가되었으면

% kubectl get ns --show-labels

를 이용하여 라벨이 제대로 적용이 되었는지 확인한다.


Bookinfo 애플리케이션 배포

Bookinfo 애플리케이션의 쿠버네티스 배포 스크립트는 samples/bookinfo 디렉토리에 들어있다. 아래 명령어를 실행해서 Bookinfo 앺ㄹ리케이션을 배포하자.

% kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml


배포를 완료한 후 kubectl get pod 명령어를 실행해보면 다음과 같이 productpage, detail,rating 서비스가 배포되고, reviews 서비스는 v1~v3까지 배포된것을 확인할 수 있다.



Kubectl get svc 를 이용해서 배포되어 있는 서비스를 확인하자



Prodcutpcage,rating,reviews,details 서비스가 배포되어 있는데, 모두 ClusterIP 타입으로 배포가 되어 있기 때문에 외부에서는 접근이 불가능하다.


Istio gateway 설정

이 서비스를 외부로 노출 시키는데, 쿠버네티스의 Ingress나 Service는 사용하지 않고, Istio의 Gateway를 이용한다.

Istio의 Gateway는 쿠버네티스의 커스텀 리소스 타입으로, Istio로 들어오는 트래픽을 받아주는 엔드포인트 역할을 한다. 여러 방법으로 구현할 수 있으나, Istio에서는 디폴트로 배포되는 Gateway는 Pod 형식으로 배포되어 Load Balancer 타입의 서비스로 서비스 된다.


먼저 Istio Gateway를 등록한후에, Gateway를 통해 서비스할 호스트를 Virtual Service로 등록한다.


아래는 bookinfo에 대한 Gateway를 등록하는 Yaml 파일이다.


apiVersion: networking.istio.io/v1alpha3

kind: Gateway

metadata:

 name: bookinfo-gateway

spec:

 selector:

   istio: ingressgateway # use istio default controller

 servers:

 - port:

     number: 80

     name: http

     protocol: HTTP

   hosts:

   - "*"


selector를 이용해서 gateway 타입을 istio에서 디폴트로 제공하는 Gateway를 사용하였다. 그리고, HTTP프로토콜을 80 포트에서 받도록 하였다.

다음에는 이 Gateway를 통해서 트래픽을 받을 서비스를 Virtual Service로 등록해야 하는데, 그 구조는 다음과 같다.


apiVersion: networking.istio.io/v1alpha3

kind: VirtualService

metadata:

 name: bookinfo

spec:

 hosts:

 - "*"

 gateways:

 - bookinfo-gateway

 http:

 - match:

   - uri:

       exact: /productpage

   - uri:

       exact: /login

   - uri:

       exact: /logout

   - uri:

       prefix: /api/v1/products

   route:

   - destination:

       host: productpage

       port:

         number: 9080


spec에서 gateways 부분에 앞에서 정의한 bookinfo-gateway를 사용하도록 한다. 이렇게 하면 앞에서 만든 Gateway로 들어오는 트래픽은 이 Virtual Servivce로 들어와서 서비스 디는데, 여기서 라우팅 룰을 정의 한다 라우팅룰은 URL에 때해서 어느 서비스로 라우팅할 지를 정하는데 /productpage,/login,/lougout,/api/v1/products URL은 productpage:9080 으로 포워딩해서 서비스를 제공한다.


Gateway와 Virtual service 배포에 앞서서, Istio에 미리 설치되어 있는 gateway를 살펴보면, Istio default gateway는 pod로 배포되어 있는데, istio=ingressgateway 라는 라벨이 적용되어 있다. 확인을 위해서 kubectl get 명령을 이용해서 확인해보면 다음과 같다.

%kubectl get pod -n istio-system -l istio=ingressgateway



이 pod들은 istio-ingressgateway라는 이름으로 istio-system 네임스페이스에 배포되어 있다. kubectl get svc로 확인해보면 다음과 같다.

%kubectl get svc istio-ingressgateway -n istio-system --show-labels



그러면 bookinfo를 istio gateway에 등록해서 외부로 서비스를 제공해보자

% istioctl create -f samples/bookinfo/networking/bookinfo-gateway.yaml


게이트 웨이 배포가 끝나면, 앞에서 조회한 Istio gateway service의 IP (여기서는 35.197.159.13)에 접속해서 확인해보자

브라우져를 열고 http://35.197.159.13/productpage 로 접속해보면 아래와 같이 정상적으로 서비스에 접속할 수 있다.



모니터링 툴

서비스 설치가 끝났으면 간단한 테스트와 함께 모니터링 툴을 이용하여 서비스를 살펴보자

Istio를 설치하면 Prometheus, Grafana, Kiali,Jaeger 등의 모니터링 도구가 기본적으로 인스톨 되어 있다. 각각의 도구를 이용해서 지표들을 모니터링 해보자

Grafana를 이용한 서비스별 지표 모니터링

Grafana를 이용해서는 각 서비스들의 지표를 상세하게 모니터링할 수 있다.

먼저 아래 스크립트를 사용해서 간단하게 부하를 주자. 아래 스크립트는 curl 명령을 반복적으로 호출하여 http://35.197.159.13/productpage 페이지를 불러서 부하를 주는 스크립이다.


for i in {1..100}; do

curl -o /dev/null -s -w "%{http_code}" http://35.197.159.13/productpage

done


다음 Grafana 웹 콘솔에 접근해야 하는데, Grafana는 외부 서비스로 노출이 안되도록 설정이 되어 있기 때문에 kubectl을 이용해서 Grafana 콘솔에 트래픽을 포워딩 하도록 하자. Grafana는 3000번 포트에서 돌고 있기 때문에, localhost:3000 → Grafana Pod의 3000 번 포트로 트래픽을 포워딩 하도록 설정하자


kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app=grafana -o jsonpath='{.items[0].metadata.name}') 3000:3000 &


다음 localhost:3000 번으로 접속해보면 다음과 같은 화면을 볼 수 있다.

각 서비스 productpage,review,rating,detail 페이지의 응답시간과 OPS (Operation Per Sec : 초당 처리량)을 볼 수 있다.




각 서비스를 눌러보면 다음과 같이 서비스별로 상세한 내용을 볼 수 있다. 응답 시간이나 처리량에 대한 트렌드나, Request의 사이즈등 다양한 정보를 볼 수 있다.



Jaeger를 이용한 분산 트렌젝션 모니터링

다음은 Jaeger 를 이용해 개별 분산 트렌젝션에 대해서 각 구간별 응답 시간을 모니터링 할 수 있다.

Istio는 각 서비스별로 소요 시간을 수집하는데, 이를 Jaeger 오픈소스를 쓰면 손쉽게 모니터링이 가능하다.

마찬가지로 Jaeger 역시 외부 서비스로 노출이 되지 않았기 때문에, kubectl 명령을 이용해서 로컬 PC에서 jaeger pod로 포트를 포워딩하도록 한다. Jaerger는 16686 포트에서 돌고 있기 localhost:16686 → Jaeger pod:16686으로 포워딩한다.


kubectl port-forward -n istio-system $(kubectl get pod -n istio-system -l app=jaeger -o jsonpath='{.items[0].metadata.name}') 16686:16686 &


Jaeger UI에 접속해서, 아래는 productpage의 호출 기록을 보는 화면이다. 화면 상단에는 각 호출별로 응답시간 분포가 나오고 아래는 개별 트렉젝션에 대한 히스토리가 나온다.



그중 하나를 선택해보면 다음과 같은 그림을 볼 수 있다.



호출이 istio-ingressgateway로 들어와서 Productpage를 호출하였다.

productpage는 순차적으로 productpage → detail 서비스를 호출하였고, 다음 productpage→ reviews → ratings 서비스를 호출한것을 볼 수 있고, 많은 시간이 reviews 호출에 소요된것을 확인할 수 있다.


Servicegraph를 이용한 서비스 토폴로지 모니터링

마이크로 서비스는 서비스간의 호출 관계가 복잡해서, 각 서비스의 관계를 시각화 해주는 툴이 있으면 유용한데, 대표적인 도구로는 service graph라는 툴과 kiali 라는 툴이 있다. BookInfo 예제를 위한 Istio 설정에는 servicegraph가 디폴트로 설치되어 있다.


마찬가지로 외부 서비스로 노출 되서 서비스 되지 않고 클러스터 주소의 8088 포트를 통해서 서비스 되고 있기 때문에, 아래와 같이 kubectl 명령을 이용해서 localhost:8088 → service graph pod의 8088포트로 포워딩하도록 한다.


kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app=servicegraph -o jsonpath='{.items[0].metadata.name}') 8088:8088 &


그 후에, 웹 브루우져에서 http://localhost:8088/dotviz 를 접속해보면 서비스들의 관계를 볼 수 있다.



다음 글에서는 예제를 통해서 Istio에서 네트워크 경로 설정하는 부분에 대해서 더 자세히 알아보도록 하겠다.



Istio #2 - Envoy Proxy


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

그럼 앞에서 설명한 서비스 매쉬의 구조를 구현한 Istio를 살펴보기전에, Istio에 사용되는 envoy 프록시에 대해서 먼저 알아보자.
(이 글은 예전에 포스팅한 내용이지만, Istio 글의 흐름상 다시 포스팅 한다.)

Envoy Proxy

먼저 istio에 사용되는 envory proxy를 살펴보자. Envoy 프록시는 Lyft사에서 개발되었으면 오픈소스로 공개되었다.

기존 프록시 L4기능 뿐 아니라 L7 기능도 지원하면서 HTTP 뿐아니라 HTTP 2.0,TCP,gRPC까지 다양한 프로토콜을 지원한다.


성능 지표를 보면 아래 Twillo에서 2017년에 테스트 한 자료를 참고할만 한데, (원본 https://www.twilio.com/blog/2017/10/http2-issues.html) HAProxy 보다 약간 느린것을 확인할 수 있다. 아무래도 L4가 아닌 L7단의 로드밸런서이다 보니 다소 성능 감소는 부담해야 한다.




(참고. 위의 문서를 보면 Envoy HTTP2 의 성능이 낮게 나오는데, 이는 Envory 자체 문제라가 보다는 HTTP/2가 Connection을 reuse하는 특성에서 온다고 볼 수 있는데, 성능에 대한 이슈가 있는 만큼 HTTP/2를 사용할 경우에는 별도의 검증 등이 필요하리라 본다.)


주요 기능적인 특성을 보면 다음과 같다.


  • HTTP, TCP, gRPC 프로토콜을 지원

  • TLS client certification 지원

  • HTTP L7 라우팅 지원을 통한 URL 기반 라우팅, 버퍼링, 서버간 부하 분산량 조절등

  • HTTP2 지원

  • Auto retry, circuit breaker, 부하량 제한등 다양한 로드밸런싱 기능 제공

  • 다양한 통계 추적 기능 제공 및 Zipkin 통합을 통한 MSA 서비스간의 분산 트렌젝션 성능 측정 제공함으로써 서비스에 대한 다양한 가시성 (visibility)을 제공

  • Dynamic configuration 지원을 통해서, 중앙 레파지토리에 설정 정보를 동적으로 읽어와서 서버 재시작없이 라우팅 설정 변경이 가능함

  • MongoDB 및 AWS Dynamo 에 대한 L7 라우팅 기능 제공


등 매우 다양한 기능을 제공한다.

Envoy 배포 아키텍처

Envoy 프록시는 배포 위치에 따라서 다양한 기능을 수행할 수 있는데, 크게 다음과 같이 4가지 구조에 배포가 가능하다.


<그림. Envoy 배포 방식>

Front envoy proxy

특정 서비스가 아니라, 전체 시스템 앞의 위치하는 프록시로, 클라이언트에서 들어오는 호출을 받아서 각각의 서비스로 라우팅을 한다. URL 기반으로 라우팅을 하는 기능 이외에도, TLS(SSL) 처리를 하는 역할들을 할 수 있다. 통상적으로 nginx나 apache httpd가 리버스프록시로 이 용도로 많이 사용되었다.

Service to service ingress listener

특정 서비스 앞에 위치하는 배포 방식으로 서비스로 들어오는 트래픽에 대한 처리를 하는데, 트래픽에 대한 버퍼링이나 Circuit breaker 와 같은 역할을 수행한다.

Service to service egress listener

특정 서비스 뒤에서 서비스로부터 나가는 트래픽을 통제 하는데, 서비스로 부터 호출 대상이 되는 서비스에 대한 로드 밸런싱, 호출 횟수 통제 (Rate limiting)와 같은 기능을 수행한다.

External service egress listener

내부서비스에서 외부 서비스로 나가는 트래픽을 관리하는 역할인데, 외부 서비스에 대한 일종의 대행자(Delegator)와 같은 역할을 한다.


시스템 앞 부분이나 또는 시스템을 구성하는 서비스의 앞뒤에 배치할 수 있는 구조지만, 서비스 앞뒤로 붙는다고 실제로 배포를 할때 하나의 서비스 앞뒤로 두개의 envoy proxy를 배치하지는 않는다.

다음과 같이 하나의 서비스에 하나의 Envoy를 배치 한후, ingress/egress 두 가지 용도로 겸용해서 사용한다.



Envoy 설정 구조

다음은 Envoy 설정 파일을 살펴 보자

Envoy의 설정은 크게 아래 그림과 같이 크게 Listener, Filter, Cluster 세가지 파트로 구성된다.



  • Listener
    Listener는 클라이언트로 부터 프로토콜을 받는 부분으로, TCP Listener, HTTP Listener 등이 있다.

  • Filter
    Filter는 Listener 로 부터 많은 메시지를 중간 처리하는 부분으로, 압축이나 들어오는 Traffic 에 대한 제한 작업등을 한후, Router를 통해서 적절한 클러스터로 메시지를 라우팅 하는 역할을 한다.

  • Cluster
    Cluster는 실제로 라우팅이 될 대상 서버(서비스)를 지정한다.


이렇게 Listener를 통해서 메시지를 받고, Filter를 이용하여 받은 메시지를 처리한 후에, 라우팅 규칙에 따라서 적절한 Cluster로 라우팅을 해서 적절한 서비스로 메시지를 보내는 형식이다.


Envoy 설치

Envoyproxy를 빌드하고 설치하는 방법은 여러가지가 있다. 소스코드로 부터 빌드를 하는 방법이나 이미 빌드된 바이너리를 사용해서 설치하는 방법 그리고 이미 빌딩된 도커 이미지를 사용하는 방법이 있다.

소스코드로 빌드하는 방법의 경우에는 bazel (make와 같은 빌드 도구) 빌드를 이용해서 빌드해야 하고, 빌드된 바이너리는 특정 플랫폼에 대해서만 미리 빌드가 되어 있기 때문에, 모든 플랫폼에 사용하기가 어렵다.

마지막으로는 도커 이미지 방식이 있는데, 이 방식이 배포면에서 여러모로 편리하기 때문에 도커 이미지를 이용한 배포 방식을 설명하도록 하겠다.


다음 명령어 처럼

docker pull을 이용하여 envoyproxy 도커 이미지 최신 버전을 가지고 오고, 다음 docker run 명령을 이용하여, 해당 이미지  (envoyproxy/envoy:latest)를 기동한다. 이때 -p 10000:10000 포트를 도커의 10000번 포트를 VM의 10000포트로 포워딩하도록 설정한다.


$ docker pull envoyproxy/envoy:latest
$ docker run --rm -d -p 10000:10000 envoyproxy/envoy:latest
$ curl -v localhost:10000


배포가 끝났으면, curl을 이용하여 localhost:10000번에 호출 하는 테스트를 하도록 한다.

설정에는 디폴트로, 10000 번 포트로 들어오는 모든 트래픽을 *.google.com으로 라우팅 하도록 설정되어 있다.


원본 설정 파일은 https://github.com/envoyproxy/envoy/blob/master/configs/google_com_proxy.v2.yaml 에 있고,  상세 내용을 보면 아래와 같다.


  • admin:
    이 부분은 envoyproxy의 admin 서버를 기동하는 부분으로, envoy 서버의 각종 설정이나 상태 정보를 127.0.0.1:9901로 들어오는 요청은 admin 기능으로 라우팅하도록 한다.

  • static_resources:
    Listener와 Filter 설정에 해당하는 부분으로, 아래 부면, listeners로 정의가 되어 있고 socket_address 부분에 0.0.0.0에 포트 10000 으로 들어오는 요청을 처리하도록 하였다.

    다음 filter_chain 부분에 filter들을 연속해서 정의하는데, http_connection_manager를 이용하여 모든 트래픽을 service_google이라는 클러스터로 라우팅 하도록 설정하였다.

  • clusters:
    마지막으로 clusters 부분에는 “service_google”이라는 클러스터를 정의했으며, 이 호스트의 URL은 google.com 443 포트로 정의하였다.


admin:

access_log_path: /tmp/admin_access.log

address:

  socket_address: { address: 127.0.0.1, port_value: 9901 }


static_resources:

listeners:

- name: listener_0

  address:

    socket_address: { address: 0.0.0.0, port_value: 10000 }

  filter_chains:

  - filters:

    - name: envoy.http_connection_manager

      config:

        stat_prefix: ingress_http

        route_config:

          name: local_route

          virtual_hosts:

          - name: local_service

            domains: ["*"]

            routes:

            - match: { prefix: "/" }

              route: { host_rewrite: www.google.com, cluster: service_google }

        http_filters:

        - name: envoy.router

clusters:

- name: service_google

  connect_timeout: 0.25s

  type: LOGICAL_DNS

  # Comment out the following line to test on v6 networks

  dns_lookup_family: V4_ONLY

  lb_policy: ROUND_ROBIN

  hosts: [{ socket_address: { address: google.com, port_value: 443 }}]

  tls_context: { sni: www.google.com }


다음글에서는 Istio에 대해서 알아보도록 하겠다.


Istio #1

마이크로 서비스 아키텍처와 서비스 매쉬

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


마이크로 서비스 아키텍쳐는 여러가지 장점을 가지고 있는 아키텍쳐 스타일이기는 하지만, 많은 단점도 가지고 있다. 마이크로 서비스는 기능을 서비스라는 단위로 잘게 나누다 보니, 전체 시스템이 커질 수 록 서비스가 많아지고, 그로 인해서 서비스간의 연결이 복잡해지고 여러가지 문제를 낳게 된다



<그림. 넷플릭스의 마이크로 서비스 구조 >

출처 : https://www.slideshare.net/BruceWong3/the-case-for-chaos?from_action=save


서비스간의 전체 연결 구조를 파악하기 어려우며 이로 인해서 장애가 났을때, 어느 서비스에서 장애가 났는지 추적이 어려워진다.

또한 특정 서비스의 장애가 다른 서비스에 영향을 주는 문제들을 겪을 수 있다.



예를 들어 클라이언트→ 서비스 A → 서비스 B의 호출 구조가 있다고 하자. 만약 서비스 B가 느려지거나 응답이 없는 상태가 되어 버리면, 서비스 B를 호출 하는 서비스 A 안의 쓰레드는 서비스 B로 부터 응답을 기다리기 위해 대기 상태가 되고, 이 상태에서 클라이언트에서 호출이 계속 되면, 같은 원리로 서비스 A의 다른 쓰레드들도 응답을 받기 위해서 대기 상태가 된다. 이런 상태가 반복되면, 서비스 A에 남은 쓰레드는 없어지고 결과적으로 서비스 A도 응답을 할 수 없는 상태가 되서 장애 상태가 된다. 이런 현상을 장애 전파 현상이라고 한다.  

마이크로 서비스 아키텍쳐 패턴

이런 문제들이 패턴화 되고 이를 풀어내기 위한 방법이 디자인 패턴으로 묶이기 시작하였다.

예를 들어 앞의 문제와 같은 장애 전파의 예는 써킷 브레이커 (Circuit breaker)라는 디자인 패턴으로 해결할 수 있다.



<그림, 써킷 브레이커(Circuit breaker) 패턴 >


서비스 A와 서비스 B에 써킷 브레이커라는 개념을 정의해서, 네트워크 트래픽을 통과 시키도록 하고, 서비스 B가 장애가 나거나 응답이 없을 경우에는 그 네트워크 연결을 끊어서 서비스 A가 바로 에러를 받도록 하는 것이다. 이렇게 하면 서비스 B가 응답이 느리거나 또는 응답을 할 수 없는 상태일 경우에는 써킷 브레이커가 바로 연결을 끊어서, 서비스 A내에서 서비스 B를 호출한 쓰레드가 바로 에러를 받아서 더 이상 서비스 B로 부터 응답을 기다리지 않고, 쓰레드를 풀어주서 서비스 A가 쓰레드 부족으로 장애가 되는 것을 막는다.

이 외에도 분산 시스템에 대한 로그 수집등 다양한 패턴들이 있는데, https://microservices.io/ 를 보면 잘 정리가 되어 있다.

이런 패턴은 디자인 패턴일 뿐이고, 이를 사용하기 위해서는 시스템에서 구현을 해야 하는데, 당연히 구현에 대한 노력이 많이 들어서 구체화 하기가 어려웠는데, 넷플릭스에서 이러한 마이크로 서비스 아키텍쳐 패턴을 오픈소스화 하여 구현하여 공개하였다. 예를 들어 위에서 언급한 써킷 브레이커 패턴의 경우에는 Hystrix (https://github.com/Netflix/hystrix/wiki)라는 오픈 소스로 공개가 되어 있다.

Hystrix 이외에도, 서비스 디스커버리 패턴은 Eureka, 모니터링 서비스인 Turbine 등 다양한 오픈 소스를 공개했다.



<그림. 넷플릭스의 마이크로 서비스 프레임웍 오픈소스 >

출처 : https://jsoftgroup.wordpress.com/2017/05/09/micro-service-using-spring-cloud-and-netflix-oss/


문제는 이렇게 오픈소스로 공개를 했지만, 여전히 그 사용법이 복잡하다는 것이다. Hystrix 하나만을 적용하는데도 많은 노력이 필요한데, 여러개의 프레임웍을 적용하는 것은 여간 어려운 일이 아니다.

그런데 여기서 스프링 프레임웍이 이런 문제를 풀어내는 기여를 한다. 스프링 프레임웍에 넷플릭스의 마이크로 서비스 오픈 소스 프레임웍을 통합 시켜 버린것이다. (http://spring.io/projects/spring-cloud-netflix)

복잡한 부분을 추상화해서 스프링 프레임웍을 적용하면 손쉽게 넷플릭스의 마이크로 서비스 프레임웍을 사용할 수 있게 해줬는데, 마지막 문제가 남게 된다. 스프링은 자바 개발 프레임웍이다. 즉 자바에만 적용이 가능하다.

서비스 매쉬

프록시

이러한 마이크로 서비스의 문제를 풀기 위해서 소프트웨어 계층이 아니라 인프라 측면에서 이를 풀기 위한 노력이 서비스 매쉬라는 아키텍쳐 컨셉이다.

아래와 같이 서비스와 서비스간의 호출이 있을때


이를 직접 서비스들이 호출을 하는 것이 아니라 서비스 마다 프록시를 넣는다.


이렇게 하면 서비스로 들어오거나 나가는 트래픽을 네트워크 단에서 모두 통제가 가능하게 되고, 트래픽에 대한 통제를 통해서 마이크로 서비스의 여러가지 문제를 해결할 수 있다.

예를 들어 앞에서 설명한 써킷 브레이커와 같은 경우에는 호출되는 서비스가 응답이 없을때 프록시에서 이 연결을 끊어서 장애가 전파되지 않도록 하면된다.


또는 서비스가 클라이언트 OS에 따라서 다른 서비스를 호출해야 한다면, 서비스가 다른 서비스를 호출할때, 프록시에서 메세지의 헤더를 보고 “Client”라는 필드가 Android면, 안드로이드 서비스로 라우팅을 하고, “IOS”면 IOS 서비스로 라우팅 하는 지능형 라우팅 서비스를 할 수 있다.


이런 다양한 기능을 수행하기 위해서는 기존의 HA Proxy,nginx, Apache 처럼 TCP 기반의 프록시로는 한계가 있다. 예를 들어서 위에서 언급한 HTTP 헤더 기반의 라우팅이나 조금더 나가면 메세지 본문을 기반으로 하는 라우팅들이 필요하기 때문에, L7 계층의 지능형 라우팅이 필요하다.

서비스 매쉬

그러면 이러한 마이크로 서비스에 대한 문제를 소프트웨어 계층이 아니라, 프록시를 이용해서 인프라 측면에서 풀어낼 수 있다는 것을 알았다. 그렇지만 마이크로 서비스는 한두개의 서비스가 아니라 수백, 수천의 서비스로 구성된다. 프록시를 사용해서 여러 기능을 구성할 수 있지만 문제는 서비스 수에 따라 프록시 수도 증가하기 때문에, 이 프록시에 대한 설정을 하기가 어려워진다는 것이다.



그래서 이런 문제를 해결하기 위해서, 각 프록시에 대한 설정 정보를 중앙 집중화된 컨트롤러가 통제하는 구조를 취할 수 있다. 아래 구조와 같이 되는데,

각 프록시들로 이루어져서 트래픽을 설정값에 따라 트래픽을 컨트롤 하는 부분을 데이타 플레인(Data Plane)이라고 하고, 데이타 플레인의 프록시 설정값들을 저장하고, 프록시들에 설정값을 전달하는 컨트롤러 역할을 하는 부분을 컨트롤 플레인(Control Plane) 이라고 한다.


다음 글에서는 이러한 서비스 매쉬 구조를 구현한 오픈 소스 솔루션인 Istio에 대해서 알아보도록 하겠다.



쿠버네티스 리소스(CPU/Memory)할당과 관리

조대협

리소스 관리


쿠버네티스에서 Pod를 어느 노드에 배포할지를 결정하는 것을 스케쥴링이라고 한다.

Pod에 대한 스케쥴링시에, Pod내의 애플리케이션이 동작할 수 있는 충분한 자원 (CPU,메모리 등)이 확보되어야 한다. 쿠버네티스 입장에서는 애플리케이션에서 필요한 자원의 양을 알아야, 그 만한 자원이 가용한 노드에 Pod를 배포할 수 있다.


쿠버네티스에서는 이런 컨셉을 지원하기 위해서 컨테이너에 필요한 리소스의 양을 명시할 수 있도록 지원하고 있다.  현재(1.9 버전) 지원되는 리소스 타입은 CPU와 메모리이며, 아직 까지는 네트워크 대역폭이나 다른 리소스 타입은 지원하고 있지 않다.

리소스 단위

리소스를 정의하는데 사용되는 단위는 CPU의 경우에는 ms(밀리 세컨드)를 사용한다. 해당 컨테이너에 얼마만큼의 CPU 자원을 할당할것인가인데, 대략 1000ms가 1 vCore (가상 CPU 코어) 정도가 된다. 클라우드 벤더에 따라 또는 쿠버네티스를 운영하는 인프라에 따라서 약간씩 차이가 있으니 참고하기 바란다.

메모리의 경우에는 Mb를 사용한다.

Request & Limit

컨테이너에 적용될 리소스의 양을 정의하는데 쿠버네티스에서는 request와 limit이라는 컨셉을 사용한다.

request는 컨테이너가 생성될때 요청하는 리소스 양이고, limit은 컨테이너가 생성된 후에 실행되다가 리소스가 더 필요한 경우 (CPU가 메모리가 더 필요한 경우) 추가로 더 사용할 수 있는 부분이다.


예를 들어 CPU request를 500ms로 하고, limit을 1000ms로 하면 해당 컨테이너는 처음에 생성될때 500ms를 사용할 수 있다. 그런데, 시스템 성능에 의해서 더 필요하다면 CPU가 추가로 더 할당되어 최대 1000ms 까지 할당될 수 있다.


리소스를 정의하는 방법은 아래와 같이 Pod spec 부분에서 개별 컨테이너 마다. Resources 파트에 request와 limit으로 필요한 리소스의 최소/최대양을 정의하면 된다.


apiVersion: v1

kind: Pod

metadata:

 name: frontend

spec:

 containers:

 - name: db

   image: mysql

   resources:

     requests:

       memory: "64Mi"

       cpu: "250m"

     limits:

       memory: "128Mi"

       cpu: "500m"

 - name: wp

   image: wordpress

   resources:

     requests:

       memory: "64Mi"

       cpu: "250m"

     limits:

       memory: "128Mi"

       cpu: "500m"


위의 예제에 따라서 정의된 Pod내의 컨테이너 CPU 리소스의 할당은 다음과 같이 된다.


db라는 이름과 wp라는 이름의 컨테이너는 생성시 250ms 만큼의 CPU 리소스를 사용할 수 있도록 생성이 되고, 필요시 최대 CPU를 500ms 까지 늘려서 사용할 수 있다.

모니터링 리소스

그러면 사용할 수 있는 리소스의 양과 현재 사용되고 있는 리소스의 양을 어떻게 모니터링할 수 있을까?

사용할 수 있는 리소스의 양은 쿠버네티스 클러스터를 생성하는데 사용된 node의 스펙을 보면 알 수 있다. 예를 들어 2 코어 VM 5대로 node를 만들었다면 그 총량은 10 코어 = 10,000ms가 된다.

그러나 이 자원을 모두 사용자 애플리케이션에 사용할 수 있는 것이 아니다. 쿠버네티스 클러스터를 유지하는 시스템 자원이나 또는 모니터링등에 자원이 소비되기 때문에 실제로 사용할 수 있는 자원의 양을 확인하는게 좋은데 “kubectl describe nodes” 명령을 이용하면 된다.

아래 예제는 kubectl describe nodes 명령으로 node들의 상세정보중에서 한 node의 자원 상태를 모니터링한 내용이다.



아래 붉은 박스를 보면 총 4 코어 머신으로 현재 request된 CPU는 1081m이고 limit으로 296m를 확보하고 있다. 메모리는 request 된것은 685M가 requested 되었고, 약 1G가 limit으로 확보되어 있다.

실제 사용량은 붉은 박스 위를 보면 되는데, default 네임 스페이스의 client-6bcxxx Pod는 현재 110m의 CPU를 request해서 사용중인것을 확인할 수 있다.


확보된 리소스와 현재 실제로 사용되는 리소스의 양은 다른데, “kubectl top nodes” 명령을 이용하면 실제로 사용되고 있는 리소스의 상태를 확인할 수 있다. 아래는 4개의 노드에서 실제로 사용되고 있는 리소스의 양이다. 붉은 색으로 표시된 노드가 위의 예제와 같은 노드인데, 위에서 requested 된 양은 1081m이었는데, 실제 사용된 cpu는 151m가 사용되고 있다.


Pod들의 리소스 사용량은 “kubectl top pods” 명령으로 확인이 가능하다.


ResourceQuota & LimitRange

이제까지 컨테이너 운영에 필요한 리소스의 양을 명시하여 요청하는 방법을 알아보았다.

만약에 어떤 개발자나 팀이 불필요하게 많은 리소스를 요청한다면, 쿠버네티스 클러스터를 운영하는 입장에서 자원이 낭비가 되고, 다른 팀이 피해를 볼 수 있는 상황이 될 수 있다. 그래서, 쿠버네티스에서는 네임스페이스별로 사용할 수 있는 리소스의 양을 정하고, 컨테이너마다 사용할 수 있는 리소스의 양을 지정할 수 있는 기능을 제공한다.

Resource Quota

Resource Quota는 네임스페이스별로 사용할 수 있는 리소스의 양을 정한다.

아래는 예는 demo 네임스페이스에, CPU 는 500m ~ 700m 까지, 메모리는 100M~500M까지 할당한 예제이다.



이 용량안에서 demo 네임스페이스내에 컨테이너를 자유롭게 만들어서 사용할 수 있다.

Limit Range

Resource Quota가 네임 스페이스 전체의 리소스양을 정의한다면, Limit Range는 컨테이너 개별 자원의 사용 가능 범위를 지정한다.

아래 예제를 보자.



  • default 로 정의된 부분은 컨테이너에 limit을 지정하지 않았을 경우 디폴트로 지정되는 limit이다. 여기서는 cpu 600m, 메모리 100m로 정의되었다.

  • defaultRequest 로 정의된 부분은 컨테이너의 request를 지정하지 않았을 경우 디폴트로 지정되는 request의 양이다.

  • max : 컨테이너에 limit을 지정할 경우, 지정할 수 있는 최대 크기이다.

  • min : 컨테이너에 limit을 지정할 경우, 지정할 수 있는 최소 크기이다.  

Overcommitted 상태

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

컨테이너의 총 Limit의 양이 실제 시스템이 가용한 resource의 양보다 많을 수 있는 경우가 발생한다. 이를 overcommitted 상태라고 한다.

Overcommitted 상태가 발생하면, CPU의 경우에는 실제 사용량을 requested 에 정의된 상태까지 낮춘다. 예를 들어 limit이 500, request가 100인 경우, 현재 500으로 가동되고 있는 컨테이너의 CPU할당량을 100으로 낮춘다. 그래도 Overcommitted 상태가 해결되지 않는 경우, 우선 순위에 따라서 운영중인 컨테이너를 강제 종료 시킨다.  

메모리의 경우에는 할당되어 사용중인 메모리의 크기를 줄일 수 는 없기 때문에, 우선 순위에 따라서 운영 중인 컨테이너를 강제 종료 시킨다.  Deployment,RS/RC에 의해 관리되고 있는 컨테이너는 다시 리스타트가 되고 초기 requested 상태의 만큼만 자원 (메모리/CPU)를 요청해서 사용하기 때문에, overcommitted  상태가 해제된다.

Best practice

구글 문서에 따르면 데이타 베이스등 아주 무거운 애플리케이션이 아니면, 일반적인 경우에는 CPU request를 100m 이하로 사용하기를 권장한다.

또한 세밀하게 클러스터를 운영하기 어려운 경우에는 request와 limit의 사이즈를 같게 하는 것을 권장한다. limit이 request보다 클 경우 overcommitted 상태가 발생할 수 있는데, 이때 CPU가 throttle down 되면, 실제 필요한 CPU양 보다 작은 CPU양으로 줄어들기 때문에 성능저하가 발생할 수 있다.  




쿠버네티스 보안 Best Practice


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


지금까지 여러가지 보안 기능에 대해서 알아보았다. 그러면 이러한 보안 기능을 어떻게 잘 사용할지 베스프 프렉틱스에 대해서 알아보자. 쿠버네티스 보안 베스트 프렉틱스는 쿠버네티스 공식 블로그 https://kubernetes.io/blog/2016/08/security-best-practices-kubernetes-deployment/ 에 2016년 8월에 포스팅과 https://kubernetes.io/docs/tasks/administer-cluster/securing-a-cluster/ 에 2018년 7월에 포스팅된 내용을 기반으로 한다. 쿠버네티스는 새버전 릴리즈가 빠른 편이고 버전마다 기능이나 아키텍쳐의 변화가 심하기 때문에, 항상 새로운 베스트 프렉틱스를 찾아서 참고하기 바란다.

이 글에서는 쉽지만 중요한 보안 정책을 위주로 설명한다.

Control plan security

TLS (SSL) 사용

쿠버네티스를 설치하면 디폴트로 API 통신은 TLS(SSL) 암호화를 이용하도록 되어 있으나, 일부 재배포판의 경우는 REST API통신을 HTTPS를 사용하지 않고, HTTP를 사용하는 경우가 있기 때문에, 이를 확인할 필요가 있다.


인증

쿠버네티스는 앞에서 설명한데로 여러가지 인증 방식을 제공하고 있는데, 그중에 BASIC_AUTH를 사용하는 방식등은, 비밀 번호가 그냥 네트워크를 통해서 전송되기 때문에, 중간에 패킷을 가로 채는 방식등으로 탈취가 가능하다.

쿠버네티스는 이외에도 Bootstrap token, static token, X509 인증서, Open ID 연동등 다양한 인증 방식이 있는데, 가급적이면 Open ID 인증 방식을 사용하는 것이 안전하다.

RBAC 사용

쿠버네티스의 기본 인증 방법은 ABAC(Attirbute-Based control) 이다.  사용자마다 기능에 대해서 권한을 배정하는 방법인데,

RBAC은 1.6에서 소개되었고, 1.8 부터는 디폴트이다. 1.6~1.8 버전은 RBAC 설정을 따로 하기 바라고, 1.6 이하 버전은 1.8 이상 버전으로 업그레이드 하는 것을 권장한다.

대쉬 보드 사용 금지

쿠버네티스 대쉬 보드는 편리하고 강력한 기능을 가지고 있지만, 별도의 접근 통제 기능이 디폴트로 탑재되어 있지 않다. 1.8 이전 버전에는 클러스터에 대한 모든 억세스가 가능한 서비스 어카운트가 바인딩되어 있기 때문에, 클러스터에 대한 모든 접근이나 보안 정보(토큰)등의 탈취가 가능하다.

실제로 테슬라의 경우에 쿠버네티스 대쉬보드 접근을 통해서, 해킹을 당한 사례가 유명하다. https://redlock.io/blog/cryptojacking-tesla

가급적이면 쿠버네티스 대쉬보드를 사용하지 않도록 하는 것이 좋고 (인스톨하지 않는다.), 사용한다고 해도, 계정 인증과, 내부 인터넷망을 통한 접근만을 허용하는등의 추가적인 보안 조치가 반드시 필요하다.

kubectl 억세스 통제 (마스터 노드 억세스 통제)

kubectl은 쿠버네티스를 통제할 수 있는 매우 강력한 툴이다. 일종의 어드민툴이기 때문에 접근 제어를 하는 것이 좋다. 쿠버네티스 방화벽 설정등을 해서 특정 머신에서만 오는 트래픽만 마스터 노드가 받아 드리도록 설정하는 방법이다.  태그 기반으로 k8s-controller로 가는 트래픽을 특정 머신에서 오는 트래픽만을 수용하게 하거나 또는 bastion을 놓고, bastion에서 들어온 API 호출만 수용하도록 하는 방법이 있다.

서비스 어카운트 토큰 마운트을 자동으로 마운트 하지 않게 한다.

Pod는 기본적으로 서비스 어카운트를 사용하게 되어 있다. 만약 서비스 어카운트를 지정하지 않으면 디폴트로 정의된 서비스 어카운트를 사용하게 되는데, 쿠버네티스에서는 디폴트로 서비스 어카운트를 사용하게 되면 서비스 어카운트의 API 토큰을 볼륨으로 마운트 한다.

서비스 어카운트 볼륨은 /var/run/secrets/kubernetes.io/serviceaccount 디렉토리에 마운트 되는데, 이 디렉토리 안에는 API인증을 위한 인증서와, 토큰이 들어 있다.

만약 이 토큰을 탈취당하게 되면, 토큰을 이용하여 쿠버네티스 API 접근이 가능하다.

일반적인 Pod의 경우에는 애플리케이션을 운영하기 위한 목적으로 사용될뿐, 쿠버네티스 API를 접근할 일이 없기 때문에 사용하지 않는 토큰을 마운트 하는 것은 위험하다. 해커가 컨테이너를 해킹해서  /var/run/secrets/kubernetes.io/serviceaccount 디렉토리를 접근한다면 토큰을 탈취할 수 있다.

이를 막기 위해서, 서비스 어카운트를 사용시 디폴트로 서비스 어카운트 토큰을 마운트하지 않게 하는 것이 좋다.


아래와 같이 서비스 어카운트를 생성할때, 자동으로 토큰을 마운트 하지 않는 옵션을 주거나,

apiVersion: v1

kind: ServiceAccount

metadata:

 name: nonroot-sa

automountServiceAccountToken: false


또는 아래와 같이 Pod 정의 부분에서 서비스 어카운트에 디폴트로 토큰을 마운트 하지 않게 정의하면 된다.


apiVersion: apps/v1

kind: Deployment

metadata:

 name: nonroot-deploy

spec:

 replicas: 3

 selector:

   matchLabels:

     app: nonroot

 template:

   metadata:

     name: nonroot-pod

     labels:

       app: nonroot

   spec:

     serviceAccountName: nonroot-sa

     automountServiceAccountToken: false

     securityContext:

       runAsUser: 1001

       fsGroup: 2001

     containers:

     - name: nonroot

       image: gcr.io/terrycho-sandbox/security-context:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


반대로 토큰을 사용하고 싶을때는 Pod 정의 부분에서 automountServiceAccountToken: true 옵션을 주면된다.


감사 로깅 (Audit)

쿠버네티스 클러스터에 대한 각종 명령어 (Pod 생성, Service 생성 삭제 등)에 대한 내용을 추적하기 위해서 모든 로그를 남겨서 정상적인 접근 여부를 판단하고, 비정상적인 접근이 발생하였을때, 이를 감지하고 추적할 수 있는 기반을 마련해야 한다. 모니터링/로깅 시스템 구축시 감사 로깅은 별도로 분리해서 감사에 대한 내용만을 따로 추적할 수 있도록 하는것이 좋다.

노드 시큐리티

컨테이너를 호스팅하는 노드에 대해서도 보안 조치가 필요하다. 다음 항목은 필수적으로 적용하기를 권장되는 항목이다.

노드에 Private IP 만 사용 (Public IP 사용 금지)

노드 서버의 IP를  Private IP만을 사용하여, 외부 인터넷으로 부터 노드 서버를 접속할 수 있는 경로를 원천적으로 차단한다.

Minimal OS 사용

노드 서버의 OS는 필요한 기능만을 가지고 있는 OS만을 설치하는 것이 좋다. OS에 따라서 디폴트로 메일 서버, FTP등 사용하지 않는 서비스가 디폴트로 제공됨으로써, 노드 서버로 접근할 수 있는 채널을 제공할 수 있다.

정기적인 패치

또한 노드 OS에 대해서 정기적인 보안 패치를 적용함으로써, 새롭게 발견되는 보안 위협에 대해 사전 봉쇄 조치를 취해야 한다

컨테이너 시큐리티

마지막으로 컨테이너에 대한 보안을 강화하는 방법이다.

컨테이너 이미지 관리

가장 중요한 것중의 하나가 컨테이너 이미지를 잘 관리하는 것인데, 일반적으로 도커로 이미지를 만들때, 베이스 이미지 (OS등이 깔려있는)를 외부 컨테이너 레파지토리에서 가져다가 사용하는 경우가 많다. 이 경우 공인되지 않는 이미지등을 사용해서, 해킹 프로그램이 깔려있는 이미지가 베이스 이미지로 사용되거나, 또는 최신 보안 패치가 되어 있지 않은 이미지로 전환하지 않고 계속 오래된 이미지를 사용해서, 보안에 헛점을 들어내는 경우가  많다.

베이스 이미지 사용은 반드시, 보안이 검증된 이미지를 사용하되, 지속적으로 최신 OS 패치를 적용한 이미지를 사용해야 한다.

베이스 이미지를 포함하여 실제 쿠버네티스에 배포되는 애플리케이션 컨테이너 이미지는 신뢰할 수 있는 이미지 저장 서비스를 이용하고, 해당 클러스터 및 허가된 IP나 사용자만 접근할 수 있도록 하는 것이 좋다.


또는 상용 서비스 중에는 컨테이너 저장소에 저장된 이미지를 스캔해서 보안에 위협이 되는 항목을 자동으로 검출하여 알려주는 서비스들이 있다.

아래 그림은 구글 클라우드의 컨테이너 저장소 서비스로, 컨테이너에 저장된 이미지에 대해서 보안 위협을 자동으로 스캔해서 리포팅 해주는 기능이다.




Security Context 사용

Pod를 정의할때, 불필요한 root나 커널 접근 권한을 최대한 제외하는 것이 좋다.

Security context를 이용해서 이런 권한을 통제할 수 있다.

  • 컨테이너는 꼭 필요하지 않는 이상 root 사용자 권한이 아니라 일반 사용자로 실행하도록 한다. securityContext에서 runAsUser와 fsGroup을 이용해서 사용자와 그룹을 지정할 수 있다.

  • Root 권한으로 실행할 수 없도록 securityContext에서 runAsNonRoot 를 true로 설정한다.

  • 꼭 필요한 경우가 아니라면 root 권한으로 생성된 파일이나 디렉토리에 대해서는 읽기만을 할 수 있도록 SecurityContext에서 readOnlyRootFilesystem 를 true로 설정한다.

  • 마지막으로 필요한 경우가 아니라면 호스트 커널에 대한 접근을 막기 위해서 securityContext에서 privileged를 false로 설정한다.

PodSecurityPolicy를 이용한 Pod Security Context 통제

SecurityContext를 위와 같이 설정하도록 권고하지만, 이를 빼먹을 수 있기 때문에,  PodSecurityPolicy (PSP)를 정의하여, Security context를 강제할 필요가 있다.

Network Policy를 이용한 트래픽 통제

마지막으로 Network policy를 정의해서, Pod로의 네트워크 접근을 통제하여 불필요한 접근을 막는다. 예를 들어 MySQL DB서버로의 접근은 label이 app=apiserver인 서버들만 3306으로 inbound 트래픽만 들어올 수 있도록 통제하는 등의 예를 들 수 있다.




구글 클라우드 해커톤 세션으로 진행한 강의 내용입니다

50분 정도 인데, 짧게 쿠버네티스에 대한 설명과, 마이크로서비스에 대한 설명

그리고 쿠버네티스 에코 시스템인 Spinnaker, Istio, KNative 등에 대해서 설명합니다



쿠버네티스 #19

보안 4/4 - Pod Security Policy

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



SecurityContext가 컨테이너나 Pod의 보안 기능을 정의 하는 것이라면, Pod Security Policy (이하 PSP)는 보안 기능에 대한 정책을 정의 하는 것이다.

예를 들어, 정책으로 Pod를 생성할때는 반드시 root 사용자를 사용하지 못하도록 강제한다던지, Privileged 모드를 사용못하도록 강제할 수 있다. 현재는 (2018년9월1일) 베타 상태이기 때문에 다소의 기능 변경이 있을 수 있음을 염두하고 사용하도록 하자.

개념

개념이 복잡하기 때문에 먼저 기본적인 개념을 이해한 후에, 각 상세를 살펴보도록 하자.

먼저 아래 그림을 보자 PSP는 생성후에, 사용자에게 지정이 된다.

그리고 Pod를 생성할때, Pod의 보안 요건을 SecurityContext를 이용해서 Pod 설정에 정의한다.

Pod를 생성하려고 할때, 생성자(사용자)의 PSP를 레퍼런스 하는데, Pod의 보안 요건이 사용자에게 정의되어 있는 PSP 요건을 만족하면, Pod가 생성된다.



반대로, Pod를 생성할때, Pod의 보안 요건 (SecurityContext)가 Pod를 생성하고자하는 사용자의 PSP요건을 만족하지 않으면, Pod 생성이 거부된다. 아래 그림은 사용자의 PSP에서 Privileged 모드를 사용할 수 없도록 설정하였으나, Pod를 생성할때 Privileged 모드를 Pod 가 사용할 수 있도록 설정하였기 때문에, Pod를 생성에 실패하는 흐름이다.




Pod Security Policy

Pod Security Policy는 Security Context와 달리 클러스터 리소스 (Cluster Resource)이다.

즉 적용하는 순간 클러스터 전체에 적용이 된다는 이야기이다.


정책 종류

Pod Security Policy를 통해서 통제할 수 있는 정책은 다음과 같다.

(출처 https://kubernetes.io/docs/concepts/policy/pod-security-policy/) 자세한 내용은 원본 출처를 참고하기 바란다.


Control Aspect

Field Names

Running of privileged containers

privileged

Usage of host namespaces

hostPID, hostIPC

Usage of host networking and ports

hostNetwork, hostPorts

Usage of volume types

volumes

Usage of the host filesystem

allowedHostPaths

White list of Flexvolume drivers

allowedFlexVolumes

Allocating an FSGroup that owns the pod’s volumes

fsGroup

Requiring the use of a read only root file system

readOnlyRootFilesystem

The user and group IDs of the container

runAsUser, supplementalGroups

Restricting escalation to root privileges

allowPrivilegeEscalation, defaultAllowPrivilegeEscalation

Linux capabilities

defaultAddCapabilities, requiredDropCapabilities, allowedCapabilities

The SELinux context of the container

seLinux

The AppArmor profile used by containers

annotations

The seccomp profile used by containers

annotations

The sysctl profile used by containers

annotations



포맷

PSP의 포맷을 이해하기 위해서 아래 예제를 보자

apiVersion: extensions/v1beta1

kind: PodSecurityPolicy

metadata:

 name: nonroot-psp

spec:

 seLinux:

   rule: RunAsAny

 supplementalGroups:

   rule: RunAsAny

 runAsUser:

   rule: MustRunAsNonRoot

 fsGroup:

   rule: RunAsAny

 volumes:

 - '*'


nonroot-psp 라는 이름으로 PSP를 정의하였고, seLinux,supplementalGroup,fsGroup과 volumes(디스크)에 대한 권한은 모두 허용하였다. runAsUser에 rule (규칙)을 MustRunAsNonRoot로 지정해서, 이 정책을 적용 받은 사용자는 Pod를 생성할때 Pod가 반드시 root 사용자가 아닌 다른 사용자를 지정하도록 정의했다.

PSP 사용자 적용

PSP 를 정의하고 실행한다고 해도, 실제로 적용되지 않는다. PSP를 적용하기 위해서는 생성한 PSP를 RBAC을 이용하여 ClusterRole을 만들고, 이 ClusterRole을 사용자에게 부여해야 실제로 정책이 적용되기 시작한다. 사용자에게 PSP를 적용하는 부분은 뒤의 예제에서 살펴보자

이때 주의할점은 사용자의 정의인데, 쉽게 생각하면 사용자를 사람으로만 생각할 수 있는데, 쿠버네티스의 사용자는 사람이 될 수 도 있지만 서비스 어카운트 (Service account)가 될 수 도 있다.

쿠버네티스에서 Pod를 생성하는 주체는 사용자가 kubectl 등으로 Pod를 직접생성할 경우, 사람이 사용자가 되지만, 대부분의 경우 Pod의 생성과 관리는 Deployment나 ReplicaSet과 같은 컨트롤러를 이용하기 때문에, 이 경우에는 컨트롤러들이 사용하는 서비스 어카운트가 사용자가 되는 경우가 많다.

그래서, PSP를 적용하는 대상은 일반 사용자가 될 수 도 있지만 서비스 어카운트에 PSP를 적용해야 하는 경우가 많다는 것을 반드시 기억해야 한다.

PSP 활성화

PSP는 쿠버네티스 클러스터에 디폴트로는 비활성화 되어 있다. PSP 기능을 사용하기 위해서는 이를 활성화 해야 하는데, PSP는 admission controller에 의해서 컨트롤 된다.

구글 클라우드

구글 클라우드에서 PSP를 활성화 하는 방법은 아래와 같이 gcloud 명령을 이용하면 된다.


%gcloud beta container clusters update {쿠버네티스 클러스터 이름} --enable-pod-security-policy --zone={클러스터가 생성된 구글 클라우드 존}


만약에 활성화된 PSP 기능을 비활성화 하고 싶으면 아래와 같이 gcloud 에서 --no-enable-pod-security-policy  옵션을 사용하면 된다.


gcloud beta container clusters update {쿠버네티스 클러스터 이름}  --no-enable-pod-security-policy --zone={클러스터가 생성된 구글 클라우드 존}

Minikube

minikube start --extra-config=apiserver.GenericServerRunOptions.AdmissionControl=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds,PodSecurityPolicy


주의할점은 PSP 기능이 활성화된후에, PSP가 적용되지 않은 사용자(사람과, 서비스어카운트 모두)의 경우에는 Pod를 생성할 수 없기 때문에, 기존에 잘 생성되던 Pod가 갑자기 생성되지 않는 경우가 많기 때문에, 반드시 기능을 활성화하기 전에 반드시, 사용자마다 적절한 PSP를 생성해서 적용하기 바란다. (PSP기능을 활성화하지 않더라도 기본적으로 PSP 정의및, PSP를 사용자에게 적용하는 것은 가능하다.)

예제

개념에 대한 이해가 끝났으면 이제 실제 예제를 통해서 어떻게 PSP를 생성 및 적용하는지를 알아보도록 하자. 예제는 다음 순서로 진행하도록 한다.

  1. PSP 정의 : Root 권한을 사용이 불가능한 PSP를 생성한다.

  2. 서비스 어카운트 생성 : PSP를 생성할 서비스 어카운트를 생성한다. Pod를 바로 생성하는 것이 아니라 Deployment를 통해서 생성할것이기 때문에 Deployment에서 이 서비스 어카운트를 사용할것이다.

  3. ClusterRole 생성 : 다음 1에서 만든 PSP를 2에서 만든 서비스 어카운트에 적용하기 위해서, PSP를 가지고 있는 ClusterRole을 생성한다.

  4. ClusterRoleBinding을 이용하여 서비스 어카운트에 PSP 적용 : 3에서 만든 ClusterRole을 2에서 만든 서비스 어카운트에 적용한다.

  5. Admission controller 활성화 : PSP를 사용하기 위해서 Admission controller를 활성화 한다.

  6. Pod 정의 및 생성 : 2에서 만든 서비스 어카운트를 이용하여 Deployment 를 정의한다.

  7. 테스트 : 테스트를 위해서, root user를 사용하는 deployment와, root user를 사용하지 않는 deployment 두개를 각각 생성해서 psp 가 제대로 적용되는지를 확인한다.

PSP 정의

PSP를 정의해보자. 아래와 같이 nonroot-psp.yaml 을 작성한다. 이 PSP는 runAsUser에서 MustRunAsNotRoot 규칙을 추가해서, Root 권한으로 컨테이너가 돌지 않도록 하는 정책이다.


# nonroot-psp.yaml

apiVersion: extensions/v1beta1

kind: PodSecurityPolicy

metadata:

 name: nonroot-psp

spec:

 seLinux:

   rule: RunAsAny

 supplementalGroups:

   rule: RunAsAny

 runAsUser:

   rule: MustRunAsNonRoot

 fsGroup:

   rule: RunAsAny

 volumes:

 - '*'


파일을 nonroot-psp.yaml 파일로 저장한후에,

%kubectl create -f nonroot-psp.yaml

명령어를 이용하여 PSP를 생성한후에,

%kubectl get psp

명령을 이용하여, PSP가 생성된것을 확인하자




서비스 어카운트 생성

서비스 어카운트 생성을 위해서 아래 yaml 파일을 작성하고, 서비스 어카운트를 생성하여 확인하자


#nonroot-sa.yaml

apiVersion: v1

kind: ServiceAccount

metadata:

 name: nonroot-sa



ClusterRole 생성 및 적용

서비스 어카운트를 생성하였으면, 앞에 만든 PSP nonroot-psp 를 사용하는 ClusterRole nonroot-clusterrole을 생성하고, 이 롤을 nonroot-clusterrole-bindings를 이용하여, 앞서 만든 서비스 어카운트 nonroot-sa 에 연결한다.


아래와 같이 ClusterRole을 생성하는데, resouces 타입을 podsecuritypolicies 로 정의하고, 리소스 이름은 앞에서 생성한 PSP인 nonroot-psp로 지정한다. 그리고, 이 psp를 사용하기 위해서 verb는 “use”로 지정한다

#nonroot-clusterbinding.yaml

apiVersion: rbac.authorization.k8s.io/v1

kind: ClusterRole

metadata:

 name: nonroot-clusterrole

rules:

- apiGroups:

 - policy

 resources:

 - podsecuritypolicies

 resourceNames:

 - nonroot-psp

 verbs:

 - use


%kubectl create -f nonroot-clusterrole.yaml

명령어를 이용하여 위의 ClusterRole을 생성한후에, 이 ClusterRole을 서비스 어카운트 nonroot-sa 에 적용하자.

아래와 같이 nonroot-clusterrolebinding.yaml 를 생성한후,


#nonroot-clusterrolebinding.yaml

apiVersion: rbac.authorization.k8s.io/v1

kind: ClusterRoleBinding

metadata:

 name: nonroot-clusterrole-bindings

subjects:

- kind: ServiceAccount

 name: sa-nonroot

 namespace: default

roleRef:

 apiGroup: rbac.authorization.k8s.io

 kind: ClusterRole

 name: nonroot-clusterrole


%kubectl create -f nonroot-clusterrolebinding.yaml

명령어를 이용하여 ClusterRole nonroot-clusterrole을 서비스 어카운트 sa-nonroot에 적용한다.

도커 컨테이너 생성

이제 PSP가 생성되었고, 이 PSP를 사용하는 서비스 어카운트 nonroot-sa 가 완성되었으면, 이를 실제로 배포에 적용해보자. 배포에 앞서서 컨테이너 이미지를 만든다.

아래는 Docker 파일인데, 앞의 보안 컨텍스트 설명때 사용한 컨테이너와 동일하다.


#Dockerfile

FROM node:carbon

EXPOSE 8080

RUN groupadd -r -g 2001 appuser && useradd -r -u 1001 -g appuser appuser

RUN mkdir /home/appuser && chown appuser /home/appuser

USER appuser

WORKDIR /home/appuser

COPY --chown=appuser:appuser server.js .

CMD node server.js > /home/appuser/log.out

생성된 도커이미지를 gcr.io/terrycho-sandbox/nonroot-containe:v1 이름으로 docker push 명령을 이용해서  컨테이너 레지스트리에 등록한다.

PSP 기능 활성화

이미지까지 준비가 되었으면, 이제 Pod를 생성할 모든 준비가 되었는데, PSP를 사용하려면, 쿠버네티스 클러스터에서 PSP 기능을 활성화 해야 한다.

다음 명령어를 이용해서 PSP를 활성화한다.

%gcloud beta container clusters update {쿠버네티스 클러스터 이름} --enable-pod-security-policy --zone={클러스터가 생성된 구글 클라우드 존}


아래 그림과 같이 PSP 기능이 활성화 되는 것을 확인한다.


Deployment 생성

기능 활성화가 끝났으면, 이제 Pod를 deploy해보자.

아래는 nonroot-deploy.yaml 파일이다.


#nonroot-deploy.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

 name: nonroot-deploy

spec:

 replicas: 3

 selector:

   matchLabels:

     app: nonroot

 template:

   metadata:

     name: nonroot-pod

     labels:

       app: nonroot

   spec:

     serviceAccountName: nonroot-sa

     securityContext:

       runAsUser: 1001

       fsGroup: 2001

     containers:

     - name: nonroot

       image: gcr.io/terrycho-sandbox/security-context:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


우리가 nonroot-psp를 사용하기 위해서, 이 psp가 정의된 서비스 어카운트 nonroot-sa를 사용하도록 하였다. 그래고 nonroot-psp에 정의한데로, 컨테이너가 root 권한으로 돌지 않도록 securityContext에 사용자 ID를 1001번으로 지정하였다.

%kubectl create -f nonroot-deploy.yaml

을 실행한후,

%kubectl get deploy 명령어를 실행해보면 아래와 같이 3개의 Pod가 생성된것을 확인할 수 있다.


보안 정책에 위배되는 Deployment 생성

이번에는 PSP 위반으로, Pod 가 생성되지 않는 테스트를 해보자.

아래와 같이 root-deploy.yaml 이라는 이름으로, Deployment 스크립트를 작성하자.


#root-deploy.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

 name: root-deploy

spec:

 replicas: 3

 selector:

   matchLabels:

     app: root

 template:

   metadata:

     name: root-pod

     labels:

       app: root

   spec:

     serviceAccountName: nonroot-sa

     containers:

     - name: root

       image: gcr.io/terrycho-sandbox/nonroot-containe:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


이 스크립트는 앞에서 작성한 nonroot-deploy.yaml 과 거의 유사하지만 Security Context에서 사용자 ID를 지정하는 부분이 없기 때문에, 디폴트로 root로 컨테이너가 기동된다. 그래서 PSP에 위반되게된다.


%kubectl create -f root-deploy.yaml

을 실행하면 결과가 아래와 같다.



맨 아래 root-deploy-7895f57f4를 보면, Current 가 0으로 Pod가 하나도 기동되지 않았음을 확인할 수 있다.

원인을 파악하기 위해서 Pod를 만드는 ReplicaSet을 찾아보자

%kubectl get rs

명령을 아래와 같이 ReplicaSet 리스트를 얻을 수 있다.

%kubectl describe rs root-deploy-7895f57f4

명령을 실행해서 ReplicaSet의 디테일과 로그를 확인해보면 다음과 같다.



그림과 같이 Pod 생성이 정책 위반으로 인해서 실패한것을 확인할 수 있다.



쿠버네티스 #18

보안 3/4 - Security Context

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

보안 컨택스트

보안 컨택스트 (Security context)는 쿠버네티스의 Pod나 컨테이너에 대한 접근 제어 설정(Access Control Setting)이나, 특수 권한 (Privilege)를 설정하는 기능을 제공한다. 단어가 추상적이기 때문에 바로 이해하기 약간 어려울 수 있는데, 몇가지 예를 들어보면, 컨테이너 내에서 동작하는 프로세스의 사용자 ID (UID)나, 그룹 ID (GID)를 설정하거나, 프로세스에 커널에 대한 접근 권한을 부여하는 것과 같은 기능을 수행할 수 있다.


구체적으로 보안 컨택스트가 지원하는 기능은 다음과 같다. 예제와 병행해서 살펴보도록 하자

예제에 사용된 코드는 https://github.com/bwcho75/kube101/tree/master/10.security/4.securityContext 에 있다.

프로세스 사용자 ID와 그룹 ID 지정

Pod나 컨테이너에서 구동되는 프로세스의 사용자 ID와 그룹 ID를 지정한다.

디폴트로 컨테이너에서 구동되는 모든 프로세스는 root 권한으로 실행이 된다. 이 경우 컨테이너 이미지가 오염되어, 악성적인 코드를 가지고 있을 경우에는 root 권한으로 컨테이너의 모든 기능을 장악할 수 있기 때문에, 이를 방지하기 위해서는 컨테이너 내에서 구동되는 사용자 애플리케이션 프로세스의 사용자 ID와 그룹 ID를 지정하여, 특정 자원 (파일이나 디렉토리)에 대한 액세스만을 허용하게 할 필요가 있다.

또한 프로세스의 사용자 ID와 그룹 ID를 지정하면, 생성되는 파일 역시 지정된 사용자 ID와 그룹 ID 를 통해서 생성된다.


간단한 예제를 하나 보자.  우리가 계속 사용해왔던 server.js 로 node.js 서버를 하나 올리는 예제이다.

이 예제를 변경하여, 사용자 ID를 1000으로, 그리고 그룹 ID를 1000으로 지정해서 Pod를 올려보도록 하자.


몇가지 수정이 필요한데, 먼저 기존에 아래와 같이 사용했던 Dockerfile을

FROM node:carbon

EXPOSE 8080

COPY server.js .

CMD node server.js > log.out


log.out 파일 경로를 /log.out에서 아래와 같이 /home/node/log.out 으로 변경한다.

기존의 예제들의 경우에는 컨테이너와 Pod를 root 권한으로 수행했지만 이 예제에서는 runAs를 이용하여 사용자 ID가 1000인 사용자로 돌리기 때문에, 루트 (“/”) 디렉토리에 파일을 생성하려면 권한 에러가 난다.

사용자 ID 1000은 node:carbon 이미지에서 정의되어 있는 node 라는 사용자로, 디폴트로 /home/node 라는 사용자 디렉토리를 가지고 있기 때문에, 이 디렉토리에 파일을 쓰도록 아래와 같이 변경한다.


FROM node:carbon

EXPOSE 8080

COPY server.js .

CMD node server.js > /home/node/log.out


다음 yaml 파일을 작성한다. (1.runas.yaml)

apiVersion: v1

kind: Pod

metadata:

 name: runas

spec:

 securityContext:

   runAsUser: 1000

   fsGroup: 1000

 volumes:

 - name: mydisk

   emptyDir: {}

 containers:

 - name: runas

   image: gcr.io/terrycho-sandbox/security-context:v1

   imagePullPolicy: Always

   volumeMounts:

   - name: mydisk

     mountPath: /mydisk

   ports:

   - containerPort: 8080


위와 같이 securityContext에 runAsUser에 사용자 ID 1000을 그리고 fsGroup에 그룹 ID 1000을 지정하여 Pod를 생성한다. 그리고, mydisk 디스크 볼륨을 생성하여, /mydisk 디렉토리에 마운트 하였다.

생성후 결과를 보자.


생성된 Pod에

%kubectl exec -it runas /bin/bash

명령을 이용하여 로그인 한후 다음 그림과 같이 권한을 체크해본다.




먼저 ps -ef로 생성된 컨테이너들의 사용자 ID를 보면 위와 같이 node (사용자 ID가 1000임)으로 생성되어 있는것을 볼 수 있다.

다음 ls -al /home/node 디렉토리를 보면 컨테이너 생성시 지정한 로그 파일이 생성이 되었고 마찬가지로 사용자 ID와 그룹 ID가 node로 지정된것을 확인할 수 있다.

다음 마운트된 디스크의 디렉토리인 /mydisk 에 myfile이란 파일을 생성해도 파일의 사용자 ID와 그룹 ID가 node로 설정되는것을 확인할 수 있다.


앞의 예제의 경우에는 사용자를 이미지에서 미리 정해진 사용자(node)를 사용하였는데, 만약에 미리 정해진 사용자가 없다면 어떻게 해야 할까?

여러가지 방법이 있겠지만, 도커 이미지를 생성하는 단계에서 사용자를 생성하면 된다. 아래는 사용자를 생성하는 도커 파일 예제이다.


FROM node:carbon

EXPOSE 8080

RUN groupadd -r -g 2001 appuser && useradd -r -u 1001 -g appuser appuser

RUN mkdir /home/appuser && chown appuser /home/appuser

USER appuser

WORKDIR /home/appuser

COPY --chown=appuser:appuser server.js .

CMD node server.js > /home/appuser/log.out


RUN 명령을 이용하여 useradd와 groupadd로 사용자를 생성하고, mkdir로 사용자 홈디렉토리 생성을 한후, 해당 디렉토리의 사용자를 생성한 사용자로 변경한다

그 후에 명령을 실행하기 위해서 명령어를 실행하는 사용자를 변경해야 하는데, USER 명령을 이용하면 사용자를 변경할 수 있다. 이후 부터 생성되는 사용자는 USER에 의해서 지정된 사용자로 실행이 된다.

그 다음은 디렉토리를 WORKDIR을 이용해서 홈디렉토리로 들어가서 COPY와 CMD 명령을 순차로 실행한다.


실행 결과 디렉토리를 확인해보면



와 같이 모든 파일이 앞에서 생성한 appuser 라는 사용자 ID로 생성이 되어 있고, 그룹 역시 appuser로 지정되어 있는 것을 확인할 수 있다.


프로세스를 확인해보면 아래와 같이 앞에서 생성한 appuser라는 사용자로 프로세스가 기동됨을 확인할 수 있다.




SecurityContext for Pod & Container

보안 컨택스트의 적용 범위는 Pod 에 적용해서 Pod 전체 컨테이너에 적용되게 할 수 도 있고, 개발 컨테이너만 적용하게 할 수 도 있다.


아래 예제의 경우에는 컨테이너에 보안 컨택스트를 적용한 예이고,

pods/security/security-context-4.yaml  

apiVersion: v1

kind: Pod

metadata:

 name: security-context-demo-4

spec:

 containers:

 - name: sec-ctx-4

   image: gcr.io/google-samples/node-hello:1.0

   securityContext:

     capabilities:

       add: ["NET_ADMIN", "SYS_TIME"]


아래 예제는 Pod 전체의 컨테이너에 적용한 예이다.

apiVersion: v1

kind: Pod

metadata:

 name: runas

spec:

 securityContext:

   runAsUser: 1000

   fsGroup: 1000

 volumes:

 - name: mydisk

   emptyDir: {}

 containers:

 - name: runas

   image: gcr.io/terrycho-sandbox/security-context:v1

   imagePullPolicy: Always

   volumeMounts:

   - name: mydisk

         : (중략)


노드 커널에 대한 억세스 권한을 제어

쿠버네티스에서 동작하는 컨테이너는 호스트 OS와 가상적으로 분리된 상태에서 기동된다. 그래서, 호스트 커널에 대한 접근이 제한이 된다. 예를 들어 물리머신에 붙어 있는 디바이스를 접근하거나 또는 네트워크 인터페이스에 대한 모든 권한을 원하거나 호스트 머신의 시스템 타임을 바꾸는 것과 같이 호스트 머신에 대한 직접 억세스가 필요한 경우가 있는데, 컨테이너는 이런 기능에 대한 접근을 막고 있다. 그래서 쿠버네티스는 이런 기능에 대한 접근을 허용하기 위해서 privilege 모드라는 것을 가지고 있다.

도커 컨테이너에 대한 privilege 모드 권한은 https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities 문서를 참고하기 바란다.

NFS 디스크 마운트나 FUSE를 이용한 디스크 마운트나, 로우 레벨 네트워크 모니터링이나 통제가 필요할때 사용할 수 있다.

privilege 모드를 true로 하면 커널의 모든 권한을 사용할 수 있는데,

아래 예제는 https://github.com/kubernetes/examples/blob/master/staging/volumes/nfs/nfs-server-rc.yaml 의 일부로 NFS 볼륨을 마운트 하는 리소스 컨트롤러 설정의 일부이다. NFS로 마운트를 하기 위해서 컨테이너의 privileged: true 로 설정하여 privileged 모드로 컨테이너가 실행하게 한것을 확인할 수 있다.


containers:

- name: nfs-server

 image: k8s.gcr.io/volume-nfs:0.8

 ports:

   - name: nfs

     containerPort: 2049

   - name: mountd

     containerPort: 20048

   - name: rpcbind

     containerPort: 111

 securityContext:

   privileged: true


Privileged 모드가 커널의 모든 기능을 부여한다면, 꼭 필요한 기능만 부여할 수 있게 세밀한 컨트롤이 가능하다. 이를 위해서 Linux capability 라는 기능이 있는데, 이 기능을 이용하면 커널의 기능을 선별적으로 허용할 수 있다. 자세한 설명은 https://linux-audit.com/linux-capabilities-hardening-linux-binaries-by-removing-setuid/ 문서를 참고하기 바란다.


아래 예제는 https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container 중의 일부로, 컨테이너 생성시에, NET_ADMIN과 SYS_TIME 권한 만을 컨테이너에 부여한 내용이다.


pods/security/security-context-4.yaml  

apiVersion: v1

kind: Pod

metadata:

 name: security-context-demo-4

spec:

 containers:

 - name: sec-ctx-4

   image: gcr.io/google-samples/node-hello:1.0

   securityContext:

     capabilities:

       add: ["NET_ADMIN", "SYS_TIME"]

기타

Security context는 이 이외에도 다양한 보안 관련 기능과 리소스에 대한 접근 제어가 가능하다.


  • AppAmor
    는 리눅스 커널의 기능중의 하나로 애플리케이션의 리소스에 대한 접근 권한을 프로필안에 정의하여 적용함으로써, 애플리케이션이 시스템에 접근할 수 있는 권한을 명시적으로 정의 및 제한 할 수 있다.
    (https://wiki.ubuntu.com/AppArmor)

  • Seccomp
    Security computing mode 의 약자로, 애플리케이션의 프로세스가 사용할 수 있는 시스템 콜의 종류를 제한할 수 있다. (https://en.wikipedia.org/wiki/Seccomp)

SELinux
보안 리눅스 기능으로 AppAmor와 유사한 기능을 제공하는데, 리소스에 대한 접근 권한을 정책으로 정의해서 제공한다.


쿠버네티스 #17

보안 2/4 - 네트워크 정책

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

네트워크 정책 (Network Policy)

쿠버네티스의 보안 기능중의 하나가 네트워크 정책을 정의함으로써 Pod로 부터 들어오거나 나가는 트래픽을 통제할 수 있다. Network Policy라는 기능인데, 일종의 Pod용 방화벽정도의 개념으로 이해하면 된다.

특정 IP나 포트로 부터만 트래픽이 들어오게 하거나 반대로, 특정 IP나 포트로만 트래픽을 내보내게할 수 있는 등의 설정이 가능한데, 이 외에도 다음과 같은 방법으로 Pod에 대한 Network Policy를 설정할 수 있다.

Ingress 트래픽 컨트롤 정의

어디서 들어오는 트래픽을 허용할것인지를 정의하는 방법은 여러가지가 있다.

  • ipBlock
    CIDR IP 대역으로, 특정 IP 대역에서만 트래픽이 들어오도록 지정할 수 있다.

  • podSelector
    label을 이용하여, 특정 label을 가지고 있는 Pod들에서 들어오는 트래픽만 받을 수 있다. 예를 들어 DB Pod의 경우에는 apiserver 로 부터 들어오는 트래픽만 받는것과 같은 정책 정의가 가능하다.

  • namespaceSelector
    재미있는 기능중 하나인데, 특정 namespace로 부터 들어오는 트래픽만을 받을 수 있다. 운영 로깅 서버의 경우에는 운영 환경 namespace에서만 들어오는 트래픽을 받거나, 특정 서비스 컴포넌트의 namespace에서의 트래픽만 들어오게 컨트롤이 가능하다. 내부적으로 새로운 서비스 컴포넌트를 오픈했을때, 베타 서비스를 위해서 특정 서비스나 팀에게만 서비스를 오픈하고자 할때 유용하게 사용할 수 있다.

  • Protocol & Port
    받을 수 있는 프로토콜과 허용되는 포트를 정의할 수 있다.

Egress 트래픽 컨트롤 정의

Egress 트래픽 컨트롤은 ipBlock과 Protocol & Port 두가지만을 지원한다.

  • ipBlock
    트래픽이 나갈 수 있는 IP 대역을 정의한다. 지정된 IP 대역으로만 outbound 호출을할 수 있다.

  • Protocol & Port
    트래픽을 내보낼 수 있는 프로토콜과, 포트를 정의한다.

예제

예제를 살펴보자. 아래 네트워크 정책은 app:apiserver 라는 라벨을 가지고 있는 Pod들의 ingress 네트워크 정책을 정의하는 설정파일로, 5000번 포트만을 통해서 트래픽을 받을 수 있으며, role:monitoring이라는 라벨을 가지고 있는 Pod에서 들어오는 트래픽만 허용한다.


kind: NetworkPolicy

apiVersion: networking.k8s.io/v1

metadata:

 name: api-allow-5000

spec:

 podSelector:

   matchLabels:

     app: apiserver

 ingress:

 - ports:

   - port: 5000

   from:

   - podSelector: