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


Archive»


 
 

쿠버네티스 #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:

       matchLabels:

         role: monitoring




네트워크 정책을 정의하기 위한 전체 스키마는 다음과 같다.

apiVersion: networking.k8s.io/v1

kind: NetworkPolicy

metadata:

 name: test-network-policy

 namespace: default

spec:

 podSelector:

   matchLabels:

     role: db

 policyTypes:

 - Ingress

 - Egress

 ingress:

 - from:

   - ipBlock:

       cidr: 172.17.0.0/16

       except:

       - 172.17.1.0/24

   - namespaceSelector:

       matchLabels:

         project: myproject

   - podSelector:

       matchLabels:

         role: frontend

   ports:

   - protocol: TCP

     port: 6379

 egress:

 - to:

   - ipBlock:

       cidr: 10.0.0.0/24

   ports:

   - protocol: TCP

     port: 5978


자 그럼, 간단하게 네트워크 정책을 정의해서 적용하는 테스트를 해보자

app:shell 이라는 라벨을 가지는 pod와 app:apiserver 라는 라벨을 가지는 pod 를 만든후에, app:shell pod에서 app:apiserver pod로 HTTP 호출을 하는 것을 테스트 한다.

다음 app:apiserver pod에 label이 app:loadbalancer 인 Pod만 호출을 받을 수 있도록 네트워크 정책을 적용한 후에, app:shell pod에서 app:apiserver로 호출이 되지 않는 것을 확인해보도록 하겠다.


테스트 환경은 구글 클라우드 쿠버네티스 엔진 ( GKE : Google cloud Kubernetes engine) 를 사용하였다.

GKE의 경우에는 NetworkPolicy가 Default로 Disable 상태이기 때문에, GKE 클러스터를 만들때 또는 만든 후에, 이 기능을 Enabled 로 활성화 해줘야 한다.

아래는 GKE 클러스터 생성시, 이 기능을 활성화 하는 부분이다.


클러스터 설정이 끝났으면, 이제 테스트에 사용할 Pod 를 준비해보자.

apiserver는 아래와 같이 server.js 의 node.js 파일을 가지고 8080 포트를 통해서 서비스하는 pod가 된다.

var os = require('os');


var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

 response.end("Hello World! I'm API Server  "+os.hostname() +" \n");


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);

이 서버로 컨테이너 이미지를 만들어서 등록한후에, 그 이미지로 아래와 같이 app:apiserver 라벨을 가지는

Pod를 생성해보자.


apiVersion: v1

kind: Pod

metadata:

 name: apiserver

 labels:

   app: apiserver

spec:

 containers:

 - name: apiserver

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

   ports:

   - containerPort: 8080

마찬가지로, app:shell 라벨을 가진 Pod도 같은  server.js 파일로 생성한다.

app:apiserver와 app:shell 라벨을 가진 pod를 생성하기 위한 코드와 yaml 파일은 https://github.com/bwcho75/kube101/tree/master/10.security/3.%20networkpolicy 를 참고하기 바란다.


두 개의 Pod를 생성하였으면 shell pod 에 kubectl exec -it {shell pod 명} -- /bin/bash를 이용해서 로그인한후에

apiserver의 URL인 10.20.3.4:8080으로 curl 로 요청을 보내보면 아래와 같이 호출되는 것을 확인할 수 있다.



이번에는 네트워크 정책을 정의하여, app:apiserver pod에 대해서 app:secure-shell 라벨을 가진 pod로 부터만 접근이 가능하도록 정책을 정해서 정의해보자


아래는 네트워크 정책을 정의한 accept-secureshell.yaml 파일이다.

kind: NetworkPolicy

apiVersion: networking.k8s.io/v1

metadata:

 name: accept-secureshell

spec:

 policyTypes:

 - Ingress

 podSelector:

   matchLabels:

     app: apiserver

 ingress:

 - from:

   - podSelector:

       matchLabels:

         app: secureshell


이 설덩은 app:apiserver 라벨이 설정된 Pod로의 트래픽은 라벨이 app:secureshell에서 보내는 트래픽만 받도록 설정한 정책이다.

%kubectl create -f accept-secureshell.yaml

명령어를 이용해서 앞에서 만든 정책을 적용한후에, 앞에서와 같이 app:shell → app:apiserver로 curl 호출을 실행하면 다음과 같이 연결이 막히는 것을 확인할 수 있다.



이외에도 다양한 정책으로, 트래픽을 컨트롤할 수 있는데, 이에 대한 레시피는 https://github.com/ahmetb/kubernetes-network-policy-recipes 문서를 참고하면 좋다.


쿠버네티스 #16


보안 1/4 - 사용자 계정 인증 및 권한 인가

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


이번글 부터는 몇회에 걸쳐 쿠버네티스 계쩡 인증,인가, 네트워크등 보안에 관련된 부분을 알아보도록 하겠다.

모든 시스템이 그렇듯이, 쿠버네티스 역시 보안이 매우 중요하다. 쿠버네티스는 보안에 관련된 여러가지 기능을 제공하는데, 각각에 대해서 살펴 보도록 하자

사용자 인증 및 권한 관리

인증과 인가 (Authentication & Authorization)

먼저 인증과 인가에 대한 개념에 대해서 이해 하자



인증(Authentication)은 사용자가 누구인지를 식별하는 것이다. 흔히 생각하는 사용자 로그인을 생각하면 된다.  인가는 인증된 사용자가 해당 기능을 실행할 수 있는 권한이 있는지를 체크하는 기능이다.

인증 (Authentication)

쿠버네티스는 계정 체계를 관리함에 있어서 사람이 사용하는 사용자 어카운트와, 시스템이 사용하는 서비스 어카운트 두가지 개념을 제공한다.

사용자 어카운트

사용자 어카운트는 우리가 일반적으로 생각하는 사용자 아이디의 개념이다.

쿠버네티스는 자체적으로 사용자 계정을 관리하고 이를 인증(Authenticate)하는 시스템을 가지고 있지 않다. 반드시 별도의 외부 계정 시스템을 사용해야 하며, 계정 시스템 연동을 위해서 OAuth나 Webhook가 같은 계정 연동 방식을 지원한다.

서비스 어카운트

서비스 어카운트가 다소 낮설 수 있는데, 예를 들어 클라이언트가 쿠버네티스 API를 호출하거나, 콘솔이나 기타 클라이언트가 쿠버네티스 API를 접근하고자 할때, 이는 실제 사람인 사용자가 아니라 시스템이 된다. 그래서, 쿠버네티스에서는 이를 일반 사용자와 분리해서 관리하는데 이를 서비스 어카운트 (service account)라고 한다.

서비스 어카운트를 생성하는 방법은 간단하다.

%kubectl create sa {서비스 어카운트명}

을 실행하면 된다. 아래는 foo 라는 이름으로 서비스 어카운트를 생성하는 예이다.



인증 방법

그러면 계정이 있을때, 이 계정을 이용해서 쿠버네티스의 API에 어떻게 접근을 할까? 쿠버네티스는 사용자 인증을 위해서 여러가지 메커니즘을 제공한다.

용도에 따라서 다양한 인증 방식을 제공한다.


  • Basic HTTP Auth

  • Access token via HTTP Header

  • Client cert

  • Custom made


Basic HTTP Auth는 HTTP 요청에 사용자 아이디와 비밀번호를 실어 보내서 인증하는 방식인데, 아이디와 비밀번호가 네트워크를 통해서 매번 전송되기 때문에 그다지 권장하지 않는 방법이다.

Access token via HTTP Header는 일반적인 REST API 인증에 많이 사용되는 방식인데, 사용자 인증 후에, 사용자에 부여된 API TOKEN을 HTTP Header에 실어서 보내는 방식이다.

Client cert는 클라이언트의 식별을 인증서 (Certification)을 이용해서 인증하는 방식이다. 한국으로 보자면 인터넷 뱅킹의 공인 인증서와 같은 방식으로 생각하면 된다. 보안성은 가장 높으나, 인증서 관리에 추가적인 노력이 필요하다.

그러면 쉽지만 유용하게 사용할 수 있는 Bearer token 방식의 인증 방식을 살펴보도록 하자

Bearer token을 사용하는 방법

인증 메커니즘 중에서 상대적으로 가장 간단한 방법은 API 토큰을 HTTP Header에 넣는 Bearer token 인증 방식이 있다.

서비스 어카운트의 경우에는 인증 토큰 정보를 secret에 저장을 한다. 이 토큰 문자열을 가지고, HTTP 헤더에 “Authorization: Bearer {토큰문자열}로 넣고 호출하면 이 토큰을 이용해서 쿠버네티스는 API 호출에 대한 인증을 수행한다.


서비스 어카운트에서 토큰 문자열을 가지고 오는 방법은

%kubectl describe sa {service account 이름}

을 실행하면 아래와 같이 Token 항목에 토큰을 저장하고 있는 secret 이름이 나온다.


위의 그림에서는 foo-token-zvnzz 이다. 이 이름으로 secret을 조회해보면,

%kubectl describe secret {시크릿명}

명령을 실행하면 아래와 같이 token이라는 항목에, 토큰이 문자열로 출력이 된다.



이 토큰을 HTTP Header 에 “Authorization: Bearer {토큰문자열}” 식으로 넣고 호출하면 된다.


간단한 스크립트를 통해서 API를 호출하는 것을 테스트 해보자

% APISERVER=$(kubectl config view | grep server | cut -f 2- -d ":" | tr -d " ")
명령을 수행하면 환경 변수 APISERVER에 현재 쿠버네티스 클러스터의 API SERVER IP가 저장된다.


다음 APISERVER의 주소를 알았으니

% curl $APISERVER/api

명령을 이용해서 HTTP GET의 /api를 호출해보자. 호출을 하면 SSL 인증서에 대한 인증 에러가 발생한다.


이는 API를 호출할때 인증에 필요한 정보를 기재하지 않았기 때문에, 디폴트로 Client cert를 이용한 인증을 시도하게 되고, 인증서를 지정하지 않았기 때문에 에러가 나게 되는것이다.


그러면 인증 정보를 제대로 지정하기 위해서 서비스 어카운트 default의 토큰을 얻어서 호출해보도록 하자.

다음 스크립트는 서비스 어카운트 default의 secret에서 토큰을 추출해서 저장하는 스크립트이다.

%TOKEN=$(kubectl describe secret $(kubectl get secrets | grep default | cut -f1 -d ' ') | grep -E '^token' | cut -f2 -d':' | tr -d '\t')

스크립트를 실행한후 TOKEN의 내용을 찍어 보면 아래와 같이 API TOKEN이 저장된것을 확인할 수 있다.




다음 이 토큰을 이용해서 API를 호출하면 된다.

%curl https://35.189.143.107/api --header "Authorization: Bearer $TOKEN" --insecure




Kubectl proxy를 이용한 API 호출

앞에서는 HTTP Header에 토큰을 직접 입력하는 방식을 사용했지만, 이렇게 사용하는 경우는 드물다. curl을 이용해서 호출할 경우에는 kubectl proxy 명령어를 이용해서 proxy를 설정하고 proxy로 API URL을 호출하면, 자동으로 이 Proxy가 현재 클라이언트의 kubeconfig file에 저장되어 있는 Credential (인증 정보)를 채워서 자동으로 보내준다.


%kubectl proxy --port=8080

을 실행하게 되면, localhost:8080을 프록시로 하여 쿠버네티스 API서버로 요청을 자동으로 포워딩 해준다.


그리고 curl localhost:8080/api 를 호출하면 {쿠버네티스 API Server}/api 를 호출해주게 된다.




SDK를 이용한 호출

일반적으로 간단한 테스트가 아닌 이상, curl 을 이용해서 직접 API를 호출하는 경우는 드물고, SDK를 사용하게 된다.  쿠버네티스에는 Go/Python/Java/Javascript 등 다양한 프로그래밍 언어를 지원하는 SDK가 있다.

https://kubernetes.io/docs/reference/using-api/client-libraries/#officially-supported-kubernetes-client-libraries


이들 SDK 역시, kubectl proxy 처럼, 로컬의 kubeconfig file의 Credential 정보를 이용해서 API를 인증하고 호출 한다.

권한 관리 (Authorization)

계정 체계와 인증에 대한 이해가 끝났으면, 이번에는 계정 권한에 대해서 알아보자. 쿠버네티스의 권한 처리 체계는 기본적으로 역할기반의 권한 인가 체계를 가지고 있다. 이를 RBAC (Role based access control)이라고 한다.


권한 구조를 도식화 해보면 다음과 같이 표현할 수 있다.


  • 사용자의 계정은 개개별 사용자인 user, 그리고 그 사용자들의 그룹은 user group, 마지막으로 시스템의 계정을 정의하는 service account로 정의된다.

  • 권한은 Role이라는 개념으로 정의가 되는데, 이 Role에는 각각의 리소스에 대한 권한이 정의된다. 예를 들어 pod 정보에대한 create/list/delete등을 정의할 수 있다. 이렇게

  • 이렇게 정의된 Role은 계정과 RoleBinding 이라는 정의를 통해서, 계정과 연결이 된다.


예제를 살펴보자, 아래는 Role을 정의한 yaml 파일이다.

pod-reader라는 Role을 정의하였고, pods에 대한 get/watch/list를 실행할 수 있는 권한을 정의하였다.



다음 이 Role을 사용자에게 부여하기 위해서 RoleBinding 설정을 아래와 같이 정의하자.

아래 Role-Binding은 read-pods라는 이름으로 jane이라는 user에서 Role을 연결하였고, 앞에서 정의한 pod-reader를 연결하도록 정의하였다.




이 예제를 그림으로 표현하면 다음과 같다.



Role vs ClusterRole

Role은 적용 범위에 따라 Cluster Role과 일반 Role로 분리 된다.

Role의 경우 특정 네임스페이스내의 리소스에 대한 권한을 정의할 수 있다.

반면 ClusterRole의 경우, Cluster 전체에 걸쳐서 권한을 정의할 수 있다는 차이가 있다.

또한 ClusterRole의 경우에는 여러 네임스페이스에 걸쳐 있는 nodes 와 같은 리소스스나 /heathz와 같이 리소스 타입이 아닌 자원에 대해서도 권한을 정의할 수 있다.

Role과 ClusterRole은 각각 RoleBinding과 ClusterRoleBinding 을 통해서 사용자에게 적용된다.

Predefined Role

쿠버네티스에는 편의를 위해서 미리 정해진 롤이 있다.


Default ClusterRole

Default ClusterRoleBinding

Description

cluster-admin

system:masters group

쿠버네티스 클러스터에 대해서 수퍼사용자 권한을 부여한다.
ClusterRoleBinding을 이용해서 롤을 연결할 경우에는 모든 네임스페이스와 모든 리소스에 대한 권한을 부여한다. RoleBinding을 이용하여 롤을 부여하는 경우에는 해당 네임 스페이스에 있는 리소스에 대한 모든 컨트롤 권한을 부여한다.

admin

None

관리자 권한의 억세스를제공한다. RoleBinding을 이용한 경우에는 해당 네임스페이스에 대한 대부분의 리소스에 대한 억세스를 제공한다.  새로운 롤을 정의하고 RoleBinding을 정의하는 권한을 포함하지만, resource quota에 대한 조정 기능은 가지지 않는다.

edit

None

네임스페이스내의 객체를 읽고 쓰는 기능은 가지지만, role이나 rolebinding을 쓰거나 수정하는 역할은 제외된다.

view

None

해당 네임스페이스내의 객체에 대한 읽기기능을 갔는다. role이나 rolebinding을 조회하는 권한은 가지고 있지 않다.


미리 정해진 롤에 대한 자세한 정보는  https://kubernetes.io/docs/reference/access-authn-authz/rbac/

를 참고하기 바란다.

권한 관리 예제

이해를 돕기 위해서 간단한 예제를 하나 테스트 해보자. 작성하는 예제는 Pod를 하나 생성해서 curl 명령으로 API를 호출하여, 해당 클러스터의 Pod 리스트를 출력하는 예제를 만들어보겠다.

Pod가 생성될때는 default 서비스 어카운트가 할당이 되는데, 이 서비스 어카운트는 클러스터의 정보를 호출할 수 있는 권한을 가지고 있지 않다. 쿠버네티스에 미리 정의된 ClusterRole중에 view 라는 롤은 클러스터의 대부분의 정보를 조회할 수 있는 권한을 가지고 있다.

이 롤을 sa-viewer 라는 서비스 어카운트를 생성한 후에, 이 서비스 어카운트에 ClusterRole view를 할당한후, 이 서비스 어카운트를 만들고자 하는 Pod에 적용하도록 하겠다.


apiVersion: v1

kind: ServiceAccount

metadata:

 name: sa-viewer

---

kind: ClusterRoleBinding

apiVersion: rbac.authorization.k8s.io/v1

metadata:

 name: default-view

subjects:

- kind: ServiceAccount

 name: sa-viewer

 namespace: default

roleRef:

 kind: ClusterRole

 name: view

 apiGroup: rbac.authorization.k8s.io


먼저 위와 같이 sa-viewer 라는 서비스 어카운트를 생성한후, ClusterRoleBiniding 을 이용하여, default-view라는 ClusterRolebinding 을 생성하고, sa-viewer 서비스 어카운트에, view 롤을 할당하였다.


다음 Pod를 생성하는데, 아래와 같이 앞에서 생성한 서비스 어카운트 sa-viewer를 할당한다.

apiVersion: v1

kind: Pod

metadata:

 name: pod-reader

spec:

 serviceAccountName: sa-viewer

 containers:

 - name: pod-reader

   image: gcr.io/terrycho-sandbox/pod-reader:v1

   ports:

   - containerPort: 8080


Pod 가 생성된 후에, kubectl exec 명령을 이용하여 해당 컨테이너에 로그인해보자

% kubectl exec -it pod-reader -- /bin/bash


로그인 후에 아래 명령어를 실행해보자


$ CA_CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

$ curl --cacert $CA_CERT -H "Authorization: Bearer $TOKEN" "https://35.200.91.132/api/v1/pods/"


CA_CERT는 API를 HTTPS로 호출하기 위해서 인증서를 저장한 파일의 위치를 지정하는 것이다. Pod의 경우에는 일반적으로 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt  디렉토리에 인증서가 자동으로 설치 된다. 다음은 API TOKEN을 얻기 위해서 TOKEN 값을 가지고 온다. TOKEN은 cat /var/run/secrets/kubernetes.io/serviceaccount/token 에 디폴트로 저장이 된다.

다음 curl 명령으로 https:{API SERVER}/api/v1/pods 를 호출하면 클러스터의 Pod 리스트를 다음 그림과 같이 리턴한다.


\



사용자 관리에 있어서, 계정에 대한 정의와 권한 정의 그리고 권한의 부여는 중요한 기능이기 때문에, 개념을 잘 잡아놓도록하자.


쿠버네티스 #11

ConfigMap


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



애플리케이션을 배포하다 보면, 환경에 따라서 다른 설정값을 사용하는 경우가 있다. 예를 들어, 데이타베이스의 IP, API를 호출하기 위한 API KEY, 개발/운영에 따른 디버그 모드, 환경 설정 파일들이 있는데, 애플리케이션 이미지는 같지만, 이런 환경 변수가 차이가 나는 경우 매번 다른 컨테이너 이미지를 만드는 것은 관리상 불편할 수 밖에 없다.

이러한 환경 변수나 설정값들을 변수로 관리해서 Pod가 생성될때 이 값을 넣어줄 수 있는데, 이러한 기능을 제공하는 것이 바로 Configmap과 Secret이다.


아래 그림과 같이 설정 파일을 만들어놓고, Pod 를 배포할때 마다 다른 설정 정보를 반영하도록 할 수 있다.



Configmap이나 secret에 정의해놓고, 이 정의해놓은 값을 Pod로 넘기는 방법은 크게 두가지가 있다.

  • 정의해놓은 값을 Pod의 환경 변수 (Environment variable)로 넘기는 방법

  • 정의해놓은 값을 Pod의 디스크 볼륨으로 마운트 하는 방법

ConfigMap

configmap은 앞서 설명한것과 같이 설정 정보를 저장해놓는 일종의 저장소 역할을 한다.

configmap은 키/밸류 형식으로 저장이된다.

configmap을 생성하는 방법은 literal (문자)로 생성하는 방법과 파일로 생성하는 방법 두가지가 있다.

Literal

먼저 간단하게 문자로 생성하는 방법을 알아보자

키가 “language”로 하고 그 값이 “java”인 configMap을 생성해보자

Kubectl create configmap [configmap 이름] --from-literal=[키]=[값] 식으로 생성하면 된다.

아래 명령을 이용하면, hello-cm 이라는 이름의 configMap에 키는 language, 값은 java인 configMap이 생성된다.

% kubectl create configmap hello-cm --from-literal=language=java


또는 아래와 같이 YAML파일로도 configMap을 생성할 수 있다.

hello-cm.yaml

apiVersion: v1

kind: ConfigMap

metadata:

 name: hello-cm

data:

 language: java


데이타 항목에 [키]:[값] 형식으로 라인을 추가하면 여러개의 값을 하나의 configMap에 저장할 수 있다.

configmap이 생성되었으면 이 값을 Pod에서 환경 변수로 불러서 사용해보도록 하자.

node.js로 간단한 웹 애플리케이션을 만든후에 “LANGUAGE”라는 환경 변수의 값을 읽어서 출력하도록 할것이다.

아래와 같이 server.js node.js 애플리케이션을 만든다.


var os = require('os');


var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

 response.end(" my prefered language is "+process.env.LANGUAGE+ "\n");


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


이 파일을 컨테이너로 패키징한 후에, 아래와 같이 Deployment를 정의한다

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: cm-deployment

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: cm-literal

 template:

   metadata:

     name: cm-literal-pod

     labels:

       app: cm-literal

   spec:

     containers:

     - name: cm

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

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       env:

       - name: LANGUAGE

         valueFrom:

           configMapKeyRef:

              name: hello-cm

              key: language


configMap에서 데이타를 읽는 부분은 맨 아래에 env 부분인데, env 부분에 환경 변수를 정의하는데, name은 LANGUAGE라는 이름으로 정의하고 데이타는 valueFrom을 이용해서 configMap에서 읽어오도록 하였다. name에는 configMap의 이름인 hello-cm을, 그리고 읽어오고자 하는 데이타는 키 값이 “language”인 값을 읽어오도록 하였다. 이렇게 하면, LANGUAGE 환경 변수에, configMap에 “language” 로 저장된 “java”라는 문자열을 읽어오게 된다.


이 스크립트를 이용하여 Deployment를 생성한 후에, 이 Deployment 앞에 Service (Load balancer)를 붙여 보자.


apiVersion: v1

kind: Service

metadata:

 name: cm-literal-svc

spec:

 selector:

   app: cm-literal

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer


서비스가 생성이 되었으면 웹 브라우져에서 해당 Service의 URL을 접속해보자.



위와 같이 환경 변수에서 “java”라는 문자열을 읽어와서 출력한것을 확인할 수 있다.

File

위와 같이 개개별 값을 공유할 수 도 있지만, 설정을 파일 형태로 해서 Pod에 공유하는 방법도 있다.

예제를 보면서 이해하도록 하자.

profile.properties라는 파일이 있고 파일 내용이 아래와 같다고 하자

myname=terry

email=myemail@mycompany.com

address=seoul


파일을 이용해서 ConfigMap을 만들때는 아래와 같이 --from-file 을 이용해서 파일명을 넘겨주면 된다.

kubectl create configmap cm-file --from-file=./properties/profile.properties

이렇게 파일을 이용해서 configMap을 생성하면, 아래와 같이 키는 파일명이 되고, 값은 파일 내용이 된다.


환경변수로 값을 전달하기

생성된 configMap 내의 값을 Pod로 전달하는 방법은,앞에서 예를 든것과 같이 환경 변수로 넘길 수 있다.

아래 Deployment 예제를 보면


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: cm-file-deployment

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: cm-file

 template:

   metadata:

     name: cm-file-pod

     labels:

       app: cm-file

   spec:

     containers:

     - name: cm-file

       image: gcr.io/terrycho-sandbox/cm-file:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       env:

       - name: PROFILE

         valueFrom:

           configMapKeyRef:

              name: cm-file

              key: profile.properties


cm-file configMap에서 키가 “profile.properties” (파일명)인 값을 읽어와서 환경 변수 PROFILE에 저장한다. 저장된 값은 파일의 내용인 아래 문자열이 된다.

myname=terry

email=myemail@mycompany.com

address=seoul


혼동하지 말아야 하는 점은, profile.properties 파일안에 문자열이 myname=terry 처럼 키/밸류 형식으로 되어 있다고 하더라도, myname 을 키로 해서 terry라는 값을 가지고 오는 것처럼 개개별 문자열을 키/밸류로 인식하는 것이 아니라 전체 파일 내용을 하나의 문자열로 처리한다는 점이다.


디스크 볼륨으로 마운트하기

configMap의 정보를 pod로 전달하는 방법은 앞에 처럼 환경 변수를 사용하는 방법도 있지만, Pod의 디스크 볼륨으로 마운트 시키는 방법도 있다.

앞의 cm-file configMap을 /tmp/config/에 마운트 해보도록 하자.

아래와 같이 Deployment 스크립트를 작성한다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: cm-file-deployment-vol

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: cm-file-vol

 template:

   metadata:

     name: cm-file-vol-pod

     labels:

       app: cm-file-vol

   spec:

     containers:

     - name: cm-file-vol

       image: gcr.io/terrycho-sandbox/cm-file-volume:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       volumeMounts:

         - name: config-profile

           mountPath: /tmp/config

     volumes:

       - name: config-profile

         configMap:

           name: cm-file


configMap을 디스크 볼륨으로 마운트해서 사용하는 방법은 volumes 을 configMap으로 정의하면 된다. 위의 예제에서 처럼 volume을 정의할때, configMap으로 정의하고 configMap의 이름을 cm-file로 정의하여, cm-file configMap을 선택하였다. 이 볼륨을 volumeMounts를 이용해서 /tmp/config에 마운트 되도록 하였다.

이때 중요한점은 마운트 포인트에 마운트 될때, 파일명을 configMap내의 키가 파일명이 된다.


다음 테스트를 위해서 server.js 애플리케이션에 /tmp/config/profile.properties 파일을 읽어서 출력하도록 아래와 같이 코드를 작성한다.

var os = require('os');

var fs = require('fs');


var http = require('http');

var handleRequest = function(request, response) {

 fs.readFile('/tmp/config/profile.properties',function(err,data){

   response.writeHead(200);

   response.end("Read configMap from file  "+data+" \n");

 });


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


이 server.js를 도커로 패키징해서 배포한후, service를 붙여서 테스트해보면 다음과 같은 결과를 얻을 수 있다.



파일 내용이 출력되는 것을 확인할 수 있다

디스크에 마운트가 제대로 되었는지를 확인하기 위해서 Pod에 쉘로 로그인해서 확인해보자


그림과 같이 /tmp/config/profile.properties 파일이 생성된것을 확인할 수 있다.


쿠버네티스 #2

개념 이해 (1/2)


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


쿠버네티스를 공부하면서 가장 헷갈리는 부분이 용어와 컨셉이다. 이 컨셉만 잘 이해하면 쿠버네티스를 쉽게 이해하고 사용할 수 있지만, 적어도 내 기준에서는 문서들의 용어나 개념 설명이 다소 어려웠다.

쿠버네티스의 개념은 크게 오브젝트 두개의 개념에서 출발한다. 각각을 살펴보도록 하자

마스터와 노드

쿠버네티스를 이해하기 위해서는 먼저 클러스터의 구조를 이해할 필요가 있는데, 구조는 매우 간단하다. 클러스터 전체를 관리하는 컨트롤러로써 마스터가 존재하고, 컨테이너가 배포되는 머신 (가상머신이나 물리적인 서버머신)인 노드가 존재한다.


오브젝트

쿠버네티스를 이해하기 위해서 가장 중요한 부분이 오브젝트이다. 가장 기본적인 구성단위가 되는 기본 오브젝트(Basic object)와, 이 기본 오브젝트(Basic object) 를 생성하고 관리하는 추가적인 기능을 가진 컨트롤러(Controller) 로 이루어진다. 그리고 이러한 오브젝트의 스펙(설정)이외에 추가정보인 메타 정보들로 구성이 된다고 보면 된다.

오브젝트 스펙 (Object Spec)

오브젝트들은 모두 오브젝트의 특성 (설정정보)을 기술한 오브젝트 스펙 (Object Spec)으로 정의가 되고, 커맨드 라인을 통해서 오브젝트 생성시 인자로 전달하여 정의를 하거나 또는 yaml이나 json 파일로 스펙을 정의할 수 있다.

기본 오브젝트 (Basic Object)

쿠버네티스에 의해서 배포 및 관리되는 가장 기본적인 오브젝트는 컨테이너화되어 배포되는 애플리케이션의 워크로드를 기술하는 오브젝트로 Pod,Service,Volume,Namespace 4가지가 있다.


간단하게 설명 하자면 Pod는 컨테이너화된 애플리케이션, Volume은 디스크, Service는 로드밸런서 그리고 Namespace는 패키지명 정도로 생각하면 된다. 그러면 각각을 자세하게 살펴보도록 하자.

Pod

Pod 는 쿠버네티스에서 가장 기본적인 배포 단위로, 컨테이너를 포함하는 단위이다.

쿠버네티스의 특징중의 하나는 컨테이너를 개별적으로 하나씩 배포하는 것이 아니라 Pod 라는 단위로 배포하는데, Pod는 하나 이상의 컨테이너를 포함한다.


아래는 간단한 Pod를 정의한 오브젝트 스펙이다. 하나하나 살펴보면


apiVersion: v1

kind: Pod

metadata:

 name: nginx

spec:

 containers:

 - name: nginx

   image: nginx:1.7.9

   ports:

   - containerPort: 8090


  • apiVersion은 이 스크립트를 실행하기 위한 쿠버네티스 API 버전이다 보통 v1을 사용한다.

  • kind 에는 리소스의 종류를 정의하는데, Pod를 정의하려고 하기 때문에, Pod라고 넣는다.

  • metadata에는 이 리소스의 각종 메타 데이타를 넣는데, 라벨(뒤에서 설명할)이나 리소스의 이름등 각종 메타데이타를 넣는다

  • spec 부분에 리소스에 대한 상세한 스펙을 정의한다.

    • Pod는 컨테이너를 가지고 있기 때문에, container 를 정의한다. 이름은 nginx로 하고 도커 이미지 nginx:1.7.9 를 사용하고, 컨테이너 포트 8090을 오픈한다.


Pod 안에 한개 이상의 컨테이너를 가지고 있을 수 있다고 했는데 왜 개별적으로 하나씩 컨테이너를 배포하지 않고 여러개의 컨테이너를 Pod 단위로 묶어서 배포하는 것인가?


Pod는 다음과 같이 매우 재미있는 특징을 갖는다.


  • Pod 내의 컨테이너는 IP와 Port를 공유한다.
    두 개의 컨테이너가 하나의 Pod를 통해서 배포되었을때, localhost를 통해서 통신이 가능하다.
    예를 들어 컨테이너 A가 8080, 컨테이너 B가 7001로 배포가 되었을 때, B에서 A를 호출할때는 localhost:8080 으로 호출하면 되고, 반대로 A에서 B를 호출할때에넌 localhost:7001로 호출하면 된다.

  • Pod 내에 배포된 컨테이너간에는 디스크 볼륨을 공유할 수 있다.
    근래 애플리케이션들은 실행할때 애플리케이션만 올라가는것이 아니라 Reverse proxy, 로그 수집기등 다양한 주변 솔루션이 같이 배포 되는 경우가 많고, 특히 로그 수집기의 경우에는 애플리케이션 로그 파일을 읽어서 수집한다. 애플리케이션 (Tomcat, node.js)와 로그 수집기를 다른 컨테이너로 배포할 경우, 일반적인 경우에는 컨테이너에 의해서 파일 시스템이 분리되기 때문에, 로그 수집기가 애플리케이션이 배포된 컨테이너의 로그파일을 읽는 것이 불가능 하지만, 쿠버네티스의 경우 하나의 Pod 내에서는 컨테이너들끼리 볼륨을 공유할 수 있기 때문에 다른 컨테이너의 파일을 읽어올 수 있다.


위와 같이 애플리케이션과 애플리케이션에서 사용하는 주변 프로그램을 같이 배포하는 패턴을 마이크로 서비스 아키텍쳐에서 사이드카 패턴(Side car pattern)이라고 하는데, 이 외에도 Ambassador, Adapter Container 등 다양한 패턴이 있는데, 이는 나중에 다른 글에서 상세하게 설명하도록 한다.

Volume

Pod가 기동할때 디폴트로, 컨테이너마다 로컬 디스크를 생성해서 기동되는데, 이 로컬 디스크의 경우에는 영구적이지 못하다. 즉 컨테이너가 리스타트 되거나 새로 배포될때 마다 로컬 디스크는 Pod 설정에 따라서 새롭게 정의되서 배포되기 때문에, 디스크에 기록된 내용이 유실된다.

데이타 베이스와 같이 영구적으로 파일을 저장해야 하는 경우에는 컨테이너 리스타트에 상관 없이 파일을 영속적으로 저장애햐 하는데, 이러한 형태의 스토리지를 볼륨이라고 한다.

볼륨은 컨테이너의 외장 디스크로 생각하면 된다. Pod가 기동할때 컨테이너에 마운트해서 사용한다.


앞에서 언급한것과 같이 쿠버네티스의 볼륨은 Pod내의 컨테이너간의 공유가 가능하다.


웹 서버를 배포하는 Pod가 있을때, 웹서비스를 서비스하는 Web server 컨테이너, 그리고 컨텐츠의 내용 (/htdocs)를 업데이트하고 관리하는 Content mgmt 컨테이너, 그리고 로그 메세지를 관리하는 Logger라는 컨테이너이가 있다고 하자

  • WebServer 컨테이너는 htdocs 디렉토리의 컨테이너를 서비스하고, /logs 디렉토리에 웹 억세스 기록을 기록한다.

  • Content 컨테이너는 htdocs 디렉토리의 컨텐트를 업데이트하고 관리한다.

  • Logger 컨테이너는 logs 디렉토리의 로그를 수집한다.

이 경우 htdocs 컨텐츠 디렉토리는 WebServer와 Content 컨테이너가 공유해야 하고 logs 디렉토리는 Webserver 와 Logger 컨테이너가 공유해야 한다. 이러한 시나리오에서 볼륨을 사용할 수 있다.


아래와 같이 htdocs와 logs 볼륨을 각각 생성한 후에, htdocs는 WebServer와, Contents management 컨테이너에 마운트 해서 공유하고, logs볼륨은 Logger와 WebServer 컨테이너에서 공유하도록 하면된다.  



쿠버네티스는 다양한 외장 디스크를 추상화된 형태로 제공한다. iSCSI나 NFS와 같은 온프렘 기반의 일반적인 외장 스토리지 이외에도, 클라우드의 외장 스토리지인 AWS EBS, Google PD,에서 부터  github, glusterfs와 같은 다양한 오픈소스 기반의 외장 스토리지나 스토리지 서비스를 지원하여, 스토리지 아키텍처 설계에 다양한 옵션을 제공한다.

Service

Pod와 볼륨을 이용하여, 컨테이너들을 정의한 후에, Pod 를 서비스로 제공할때, 일반적인 분산환경에서는 하나의 Pod로 서비스 하는 경우는 드물고, 여러개의 Pod를 서비스하면서, 이를 로드밸런서를 이용해서 하나의 IP와 포트로 묶어서 서비스를 제공한다.


Pod의 경우에는 동적으로 생성이 되고, 장애가 생기면 자동으로 리스타트 되면서 그 IP가 바뀌기 때문에, 로드밸런서에서 Pod의 목록을 지정할 때는 IP주소를 이용하는 것은 어렵다. 또한 오토 스케일링으로 인하여 Pod 가 동적으로 추가 또는 삭제되기 때문에, 이렇게 추가/삭제된 Pod 목록을 로드밸런서가 유연하게 선택해 줘야 한다.

그래서 사용하는 것이 라벨(label)과 라벨 셀렉터(label selector) 라는 개념이다.


서비스를 정의할때, 어떤 Pod를 서비스로 묶을 것인지를 정의하는데, 이를 라벨 셀렉터라고 한다. 각 Pod를 생성할때 메타데이타 정보 부분에 라벨을 정의할 수 있다. 서비스는 라벨 셀렉터에서 특정 라벨을 가지고 있는 Pod만 선택하여 서비스에 묶게 된다.

아래 그림은 서비스가 라벨이 “myapp”인 서비스만 골라내서 서비스에 넣고, 그 Pod간에만 로드밸런싱을 통하여 외부로 서비스를 제공하는 형태이다.



이를 스펙으로 정의해보면 대략 다음과 같다.


kind: Service
apiVersion: v1
metadata:
 name: my-service
spec:
 selector:
   app: myapp
 ports:
 - protocol: TCP
   port: 80
   targetPort: 9376


  • 리소스 종류가 Service 이기 때문에, kind는 Service로 지정하고,

  • 스크립트를 실행할 api 버전은 v1으로 apiVersion에 정의했다.

  • 메타데이타에 서비스의 이름을 my-service로 지정하고

  • spec 부분에 서비스에 대한 스펙을 정의한다.

    • selector에서 라벨이 app:myapp인 Pod 만을 선택해서 서비스에서 서비스를 제공하게 하고

    • 포트는 TCP를 이용하되, 서비스는 80 포트로 서비스를 하되, 서비스의 80 포트의 요청을 컨테이너의 9376 포트로 연결해서 서비스를 제공한다.


Name space

네임스페이스는 한 쿠버네티스 클러스터내의 논리적인 분리단위라고 보면 된다.

Pod,Service 등은 네임 스페이스 별로 생성이나 관리가 될 수 있고, 사용자의 권한 역시 이 네임 스페이스 별로 나눠서 부여할 수 있다.

즉 하나의 클러스터 내에, 개발/운영/테스트 환경이 있을때, 클러스터를 개발/운영/테스트 3개의 네임 스페이스로 나눠서 운영할 수 있다. 네임스페이스로 할 수 있는 것은

  • 사용자별로 네임스페이스별 접근 권한을 다르게 운영할 수 있다.

  • 네임스페이스별로 리소스의 쿼타 (할당량)을 지정할 수 있다. 개발계에는 CPU 100, 운영계에는 CPU 400과 GPU 100개 식으로, 사용 가능한 리소스의 수를 지정할 수 있다.

  • 네임 스페이스별로 리소스를 나눠서 관리할 수 있다. (Pod, Service 등)


주의할점은 네임 스페이스는 논리적인 분리 단위이지 물리적이나 기타 장치를 통해서 환경을 분리(Isolation)한것이 아니다. 다른 네임 스페이스간의 pod 라도 통신은 가능하다.

물론 네트워크 정책을 이용하여, 네임 스페이스간의 통신을 막을 수 있지만 높은 수준의 분리 정책을 원하는 경우에는 쿠버네티스 클러스터 자체를 분리하는 것을 권장한다.


참고 자료 네임 스페이스에 대한 베스트 프랙틱스 : https://cloudplatform.googleblog.com/2018/04/Kubernetes-best-practices-Organizing-with-Namespaces.html

https://kubernetes.io/blog/2016/08/kubernetes-namespaces-use-cases-insights/

라벨

앞에서 잠깐 언급했던 것 중의 하나가 label 인데, 라벨은 쿠버네티스의 리소스를 선택하는데 사용이 된다. 각 리소스는 라벨을 가질 수 있고, 라벨 검색 조건에 따라서 특정 라벨을 가지고 있는 리소스만을 선택할 수 있다.

이렇게 라벨을 선택하여 특정 리소스만 배포하거나 업데이트할 수 있고 또는 라벨로 선택된 리소스만 Service에 연결하거나 특정 라벨로 선택된 리소스에만 네트워크 접근 권한을 부여하는 등의 행위를 할 수 있다.

라벨은 metadata 섹션에 키/값 쌍으로 정의가 가능하며, 하나의 리소스에는 하나의 라벨이 아니라 여러 라벨을 동시에 적용할 수 있다.


"metadata": {
 "labels": {
   "key1" : "value1",
   "key2" : "value2"
 }
}


셀렉터를 사용하는 방법은 오브젝트 스펙에서 selector 라고 정의하고 라벨 조건을 적어 놓으면 된다.

쿠버네티스에서는 두 가지 셀렉터를 제공하는데, 기본적으로 Equaility based selector와, Set based selector 가 있다.

Equality based selector는 같냐, 다르냐와 같은 조건을 이용하여, 리소스를 선택하는 방법으로

  • environment = dev

  • tier != frontend

식으로, 등가 조건에 따라서 리소스를 선택한다.

이보다 향상된 셀렉터는 set based selector로, 집합의 개념을 사용한다.

  • environment in (production,qa) 는 environment가 production 또는 qa 인 경우이고,

  • tier notin (frontend,backend)는 environment가 frontend도 아니고 backend도 아닌 리소스를 선택하는 방법이다.

다음 예제는 my-service 라는 이름의 서비스를 정의한것으로 셀렉터에서 app: myapp 정의해서 Pod의 라벨 app이 myapp 것만 골라서 이 서비스에 바인딩해서 9376 포트로 서비스 하는 예제이다.


kind: Service
apiVersion: v1
metadata:
 name: my-service
spec:
 selector:
   app: myapp
 ports:
 - protocol: TCP
   port: 80
   targetPort: 9376



컨트롤러

앞에서 소개한 4개의 기본 오브젝트로, 애플리케이션을 설정하고 배포하는 것이 가능한데 이를 조금 더 편리하게 관리하기 위해서 쿠버네티스는 컨트롤러라는 개념을 사용한다.

컨트롤러는 기본 오브젝트들을 생성하고 이를 관리하는 역할을 해준다. 컨트롤러는 Replication Controller (aka RC), Replication Set, DaemonSet, Job, StatefulSet, Deployment 들이 있다. 각자의 개념에 대해서 살펴보도록 하자.

Replication Controller

Replication Controller는  Pod를 관리해주는 역할을 하는데, 지정된 숫자로 Pod를 기동 시키고, 관리하는 역할을 한다.

Replication Controller (이하 RC)는 크게 3가지 파트로 구성되는데, Replica의 수, Pod Selector, Pod Template 3가지로 구성된다.

  • Selector : 먼저 Pod selector는 라벨을 기반으로 하여,  RC가 관리한 Pod를 가지고 오는데 사용한다.

  • Replica 수 :  RC에 의해서 관리되는 Pod의 수인데, 그 숫자만큼 Pod 의 수를 유지하도록 한다.예를 들어 replica 수가 3이면, 3개의 Pod만 띄우도록 하고, 이보다 Pod가 모자르면 새로운 Pod를 띄우고, 이보다 숫자가 많으면 남는 Pod를 삭제한다.

  • Pod를 추가로 기동할 때 그러면 어떻게 Pod를 만들지 Pod에 대한 정보 (도커 이미지, 포트,라벨등)에 대한 정보가 필요한데, 이는 Pod template이라는 부분에 정의 한다.




주의할점은 이미 돌고 있는 Pod가 있는 상태에서 RC 리소스를 생성하면 그 Pod의 라벨이 RC의 라벨과 일치하면 새롭게 생성된 RC의 컨트롤을 받는다. 만약 해당 Pod들이 RC에서 정의한 replica 수 보다 많으면, replica 수에 맞게 추가분의 pod를 삭제하고, 모자르면 template에 정의된 Pod 정보에 따라서 새로운 Pod를 생성하는데, 기존에 생성되어 있는 Pod가 template에 정의된 스펙과 다를지라도 그 Pod를 삭제하지 않는다. 예를 들어 기존에 아파치 웹서버로 기동중인 Pod가 있고, RC의 template은 nginx로 Pod를 실행하게 되어 있다하더라도 기존에 돌고 있는 아파치 웹서버 기반의 Pod를 삭제하지 않는다.


아래 예를 보자.


이 예제는 ngnix라는 이름의 RC를 정의한 것으로, label이 “app:ngnix”인 Pod들을 관리하고 3개의 Pod가 항상 운영되도록 설정한다.

Pod는 app:ngix 라는 라벨을 가지면서 이름이 ngnix이고 nginx 이미지를 사용해서 생성하고 컨테이너의 포트는 80 번 포트를 이용해서 서비스를 제공한다.

ReplicaSet

ReplicaSet은 Replication Controller 의 새버전으로 생각하면 된다.

큰 차이는 없고 Replication Controller 는 Equality 기반 Selector를 이용하는데 반해, Replica Set은 Set 기반의 Selector를 이용한다.

Deployment

Deployment (이하 디플로이먼트) Replication controller와 Replica Set의 좀더 상위 추상화 개념이다. 실제 운영에서는 ReplicaSet 이나 Replication Controller를 바로 사용하는 것보다, 좀 더 추상화된 Deployment를 사용하게 된다.

쿠버네티스 배포에 대한 이해

쿠버네티스의 Deployment 리소스를 이해하기 위해서는 쿠버네티스에서 Deployment 없이 어떻게 배포를 하는지에 대해서 이해를 하면 Deployment 를 이해할 수 있다.


다음과 같은 Pod와 RC가 있다고 하자


애플리케이션이 업데이트되서 새로운 버전으로 컨테이너를 굽고 이 컨테이너를 배포하는 시나리오에 대해서 알아보자. 여러가지 배포 전략이 있겠지만, 많이 사용하는 블루/그린 배포와 롤링 업데이트 방식 두가지 방법에 대해서 설명한다.

블루/그린 배포

블루/그린 배포 방식은 블루(예전)버전으로 서비스 하고 있던 시스템을 그린(새로운)버전을 배포한 후, 트래픽을 블루에서 그린으로 한번에 돌리는 방식이다.

여러가지 방법이 있지만 가장 손쉬운 방법으로는 새로운 RC을 만들어서 새로운 템플릿으로 Pod를 생성한 후에, Pod 생성이 끝나면, 서비스를 새로운 Pod로 옮기는 방식이다.


후에, 배포가 완료되고 문제가 없으면 예전 버전의 RC 와 Pod를 지워준다.

롤링 업그레이드

롤링 업그레이드 방식은 Pod를 하나씩 업그레이드 해가는 방식이다.

이렇게 배포를 하려면 먼저 새로운 RC를 만든후에, 기존 RC에서 replica 수를 하나 줄이고, 새로운 RC에는 replica 수를 하나만 준다.


라벨을 같은 이름으로 해주면 서비스는 자연히 새로운 RC에 의해 생성된 Pod를 서비스에 포함 시킨다.

다음으로 기존 RC의 replica를 하나 더 줄이고, 새로운 RC의  replica를 하나 더 늘린다.


그러면 기존 버전의 Pod가 하나더 서비스에서 빠지게 되고 새로운 버전의 Pod가 서비스에 추가된다.

마찬가지 작업을 반복하게 되면, 아래 그림과 같이 예전 버전의 Pod가 모두 빠지고 새 버전의 Pod만 서비스 되게 된다.


만약에 배포가 잘못되었을 경우에는 기존 RC의 replica 수를 원래대로 올리고, 새버전의 replicat 수를 0으로 만들어서 예전 버전의 Pod로 롤백이 가능하다.

이 과정은 kubectl rolling-update라는 명령으로 RC 단위로 컨트롤이 가능하지만, 그래도 여전히 작업이 필요하고, 배포 과정을 모니터링 해야 한다. 그리고 가장 문제는 kubectl rolling-update 명령은 클라이언트에서 실행 하는 명령으로, 명령어 실행중에 클라이언트의 연결이 끊어 지면 배포작업이 비정상적으로 끊어질 수 있는 문제가 있다.

그리고 마지막으로, 롤백과정 역시 수동 컨트롤이 필요할 수 있다.

그래서 이러한 과정을 자동화하고 추상화한 개념을 Deployment라고 보면 된다.

Deployment는 Pod 배포를 위해서 RC를 생성하고 관리하는 역할을 하며, 특히 롤백을 위한 기존 버전의 RC 관리등 여러가지 기능을 포괄적으로 포함하고 있다.



Deployment 에 대해서는 뒤에 다른 글에서 조금 더 자세하게 설명하도록 한다.


이글에서는 쿠버네티스를 이루는 기본적인 오브젝트와 이를 생성 제어하기 위한 기본적인 컨트롤러에 대해서 알아보았다.

다음 글에서는 조금 더 발전된 형태의 컨트롤러에 대해서 알아보기로 한다.







Kubernetes #1 - 소개

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

배경

도커와 쿠버네티스를 알게 된건 수년전인데, 근래에 들어서 다시 쿠버네티스를 보기 시작하였다.

컨테이너 기반의 환경은 배포에 장점이 있고 마이크로 서비스 아키텍쳐 구조에 잘 맞아들어가는 듯 싶지만, 컨테이너가 약간 빠르다는 장점은 있지만, 가상 머신으로도 충분히 패키징이 가능하고, 로컬의 개발환경을 동기화 시키는 장점은 vagrant 로도 충분하다는 생각을 가지고 있었다.


그리고 결정적으로 도커 컨테이너를 운용하기 위한 컨테이너 관리 환경이 그다지 성숙하지 못했었다. Mesosphere, Swarm, Kubernetes 등 다양한 환경이 나오기는 하였지만 기능적으로 부족한 부분도 많았고, 딱히 어떤 플랫폼이 대세라고 정해진것도 없었다.


마이크로 서비스 아키텍쳐 발전

그러나 근래에 들어서 재미있어지는 현상이 마이크로 서비스 아키텍쳐가 단순 개념에서 부터 점점 더 발전하기 시작하였고, 디자인 패턴과 이를 구현하기 위한 다양한 인프라 플랫폼들이 소개되기 시작하였다.

또한 서비스가 점점 작아지면서, 1~2 코어로도 운영할 수 있는 작은 서비스들이 다수 등장하게 되었고 이런 작은 서비스는 VM 환경으로 운영하기에는 낭비가 너무 심하다. (VM 이미지 크기도 너무 크고, 다양한 이미지를 VM으로 관리 배포하기에는 배포 속도등 다양한 문제가 발생한다.)


솔루션의 발전

배포 방식도 예전에 서버에 계속해서 애플리케이션 코드만 업데이트 하는 방식이 아니라, VM이나 컨테이너 단위로 배포하는 피닉스 서버 패턴과 이를 구현하기 위한 Spinnaker  와 같은 솔루션이 나오고 있고, 지능형 라우팅과 분산 트렌젝션 로그 추적을 하는 기능들이 Envoy 라는 솔루션으로 나오고 이를 중앙 통제하기 위한 Istio.io 와 같은 서비스 메쉬 솔루션 까지 나오기에 이르렀다.


데브옵스 모델의 성숙화

데브옵스 모델도 나온지는 오래되었지만, 운영을 데브옵스라는 이름으로 바꾼 것일뿐 실제적인 변화가 없는 팀들이 많았고, 또는 데브옵스라는 이름아래에서 개발팀이 개발과/운영 역할을 병행해서 하는 사례가 오히려 많았다.

이런 데브옵스의 개념도 근래에 들어서 정리가 되어가고 있는데, 개발팀이 개발과 시스템에 대한 배포/운영을 담당한다면, 데브옵스팀은 개발팀이 이를 쉽게할 수 있는 아랫단의 플랫폼과 자동화를 하는데 목표를 두는 역할로 역할이 명확해지고 있다.


이러한 배경에서 슬슬 컨테이너 기반의 환경이 실질적으로 적용될만하다는 것으로 판단하였고, 다시 컨테이너 환경에 대해서 살펴보기 시작하였다.

왜 하필이면 쿠버네티스인가?

그렇다면 Swarm,Mesosphere 가 아니라 왜 하필이면 쿠버네티스인가? 컨테이너 운용 환경은 여러 오픈소스에 의해서 표준이 없이 혼돈이었다가 작년말을 기점으로 해서 쿠버네티스가 de-facto 표준으로 되어가는 형국이다. 아래 트랜드 그래프에서 보면 알 수 있듯이 쿠버네티스의 트랜드가 지속적으로 올라가서 가장 높은 것을 확인할 수 있다.



또한 주요 클라우드 벤더인 아마존,구글,애저 모두 컨테이너 관리 환경을 쿠버네티스를 지원하는 정책으로 변화된것은 물론이고 IBM이나 시스코와 같은 온프렘(on-premise) 솔루션 업체들도 경쟁적으로 쿠버네티스를 지원하고 있다.

컨테이너 운영환경이 무엇인데?

컨테이너 (도커)에 필요성과 마이크로 서비스의 관계등에 대해서는 워낙 소개된 글들이 많아서 생략한다. 그렇다면 쿠버네티스가 제공하는 컨테이너 운영환경이란 무엇인가? 이를 이해하기 위해서는 먼저 컨테이너에 대해서 이해할 필요가 있는데, 컨테이너의 가장 대표적인 예로는 도커가 있다. 도커에 대한 자세한 설명은 링크를 참고하기 바란다.


그러면 단순하게 도커 컨테이너를 하드웨어나 VM에 배포하면 사용하면 되지 왜 컨테이너 운영환경이 필요한가?


작은 수의 컨테이너라면 수동으로 VM이나 하드웨어에 직접 배포하면 되지만, VM이나 하드웨어의 수가 많아지고 컨테이너의 수가 많아지면, 이 컨테이너를 어디에 배포해야 하는지에 대한 결정이 필요하다.

16 CPU, 32 GB 메모리 머신들에 컨테이너를 배포할때 컨테이너 사이즈가 2 CPU, 3 CPU, 8 CPU등 다양할 수 있기 때문에, 자원을 최대한 최적으로 사용하기 위해서 적절한 위치에 배포해야 하고, 애플리케이션 특성들에 따라서, 같은 물리 서버에 배포가 되어야 하거나 또는 가용성을 위해서 일부러 다른 물리서버에 배포되어야 하는 일이 있다. 이렇게 컨테이너를 적절한 서버에 배포해주는 역할을 스케쥴링이라고 한다.


이러한 스케쥴링 뿐만이 아니라 컨테이너가 정상적으로 작동하고 있는지 체크하고 문제가 있으면 재 기동등을 해주고, 모니터링, 삭제관리등 컨테이너에 대한 종합적인 관리를 해주는 환경이 필요한데, 이를 컨테이너 운영환경이라고 한다.

쿠버네티스란?

이런 컨테이너 운영환경중 가장 널리 사용되는 솔루션이 쿠버네티스 (Kubernetes, 약어로 k8s)라고 한다.

구글은 내부 서비스를 클라우드 환경에서 운영하고 있으며, 일찌감치 컨테이너 환경을 사용해왔다. 구글의 내부 컨테이너 서비스를 Borg라고 하는데, 이 구조를 오픈소스화한것이 쿠버네티스이다.

GO 언어로 구현이되었으며, 특히 재미있는 것은 벤더나 플랫폼에 종속되지 않기 때문에, 대부분의 퍼블릭 클라우드 (구글,아마존,애저)등에 사용이 가능하고 오픈 스택과 같은 프라이빗 클라우드 구축 환경이나 또는 베어메탈 (가상화 환경을 사용하지 않는 일반 서버 하드웨어)에도 배포가 가능하다.

이런 이유 때문에 여러 퍼블릭 클라우드를 섞어서 사용하는 환경이나 온프렘/퍼블릭 클라우드를 혼용해서 쓰는 환경에도 동일하게 적용이 가능하기 때문에 하이브리드 클라우드 솔루션으로 많이 각광 받고 있다.


흔히들 컨테이너를 이야기 하면 도커를 떠올리기 쉬운데, 도커가 물론 컨테이너 엔진의 대표격이기는 하지만 이 이외도 rkt나 Hyper container(https://hypercontainer.io/) 등 다양한 컨테이너 엔진들이 있으며, 쿠버네티스는 이런 다양한 컨테이너 엔진을 지원한다.

컨테이너 환경을 왜 VM에 올리는가?

온프렘 환경(데이타센터)에서 쿠버네티스를 올릴때 궁금했던점 중의 하나가, 바로 베어메탈 머신위에 쿠버네티스를 깔면 되는데, 보통 배포 구조는 VM(가상화 환경)을 올린 후에, 그 위에 쿠버네티스를 배포하는 구조를 갖는다. 왜 이렇게 할까 한동안 고민을 한적이 있었는데, 나름데로 내린 결론은 하드웨어 자원 활용의 효율성이다. 컨테이너 환경은 말그대로 하드웨어 자원을 컨테이너화하여 isolation 하는 기능이 주다. 그에 반해 가상화 환경은 isolation 기능도 가지고 있지만, 가상화를 통해서 자원 , 특히 CPU의 수를 늘릴 수 있다.


예를 들어 설명하면, 8 CPU 머신을 쿠버네티스로 관리 운영하면, 8 CPU로밖에 사용할 수 없지만, 가상화 환경을 중간에 끼면, 8 CPU를 가상화 해서 2배일 경우 16 CPU로, 8배일 경우 64 CPU로 가상화 하여 좀 더 자원을 잘게 나눠서 사용이 가능하기 때문이 아닌가 하는 결론을 내렸다.

이 이외에도 스토리지 자원의 활용 용이성이나 노드 확장등을 유연하게 할 수 있는 장점이 있다고 한다.


다음 글에서는 쿠버네티스를 구성하는 컴포넌트들의 구성과 개념에 대해서 설명하도록 한다.



Docker란 무엇인가?

개념 잡기

Docker Linux 기반의 Container RunTime 오픈소스이다. 처음 개념을 잡기가 조금 어려운데, Virtual Machine과 상당히 유사한 기능을 가지면서, Virtual Machine보다 훨씬 가벼운 형태로 배포가 가능하다. 정확한 이해를 돕기 위해서, VM Docker Container의 차이를 살펴보자.

아래는 VM 에 대한 컨셉이다. Host OS가 깔리고, 그 위에 Hypervisor (VMWare,KVM,Xen etc)가 깔린 후에, 그위에, Virtual Machine이 만들어진다. Virtual Machine은 일종의 x86 하드웨어를 가상화 한 것이라고 보면된다. 그래서 VM위에 다양한 종류의 Linux, Windows등의 OS를 설치할 수 있다.



DockerContainer 컨셉은 비슷하지만 약간 다르다. Docker VM 처럼 Docker Engine Host위에서 수행된다. 그리고, Container Linux 기반의 OS만 수행이 가능하다.

Docker VM처럼 Hardware를 가상화 해주는 것이 아니라, Guest OS (Container) Isolation해준다.무슨 말인가 하면, Container OS는 기본적으로 Linux OS만 지원하는데, Container 자체에는 Kernel등의 OS 이미지가 들어가 있지 않다. Kernel Host OS를 그대로 사용하되, Host OS Container OS의 다른 부분만 Container 내에 같이 Packing된다. 예를 들어 Host OS Ubuntu version X이고, Container OS CentOS version Y라고 했을때, Container에는 CentOS version Y full image가 들어가 있는 것이 아니라, Ubuntu version X CentOS version Y의 차이가 되는 부분만 패키징이 된다. Container 내에서 명령어를 수행하면 실제로는 Host OS에서 그 명령어가 수행된다. Host OS Process 공간을 공유한다.



실제로 Container에서 App을 수행하고 ps –ef 를 이용하여 process를 보면, “lxc”라는 이름으로 해당 App이 수행됨을 확인할 수 있다. 아래는 docker를 이용해서 container에서 bash 를 수행했을때는 ps 정보이다. lxc 프로세스로 bash 명령어가 수행되었음을 확인할 수 있다.

root      4641   954  0 15:07 pts/1    00:00:00 lxc-start -n 161c56b9284ffbad0477bd04875c4277be976e2032f3ffa35395950ea05f9bd6 -f /var/lib/docker/containers/161c56b9284ffbad0477bd04875c4277be976e2032f3ffa35395950ea05f9bd6/config.lxc -- /.dockerinit -g 172.17.42.1 -e TERM=xterm -e HOME=/ -e PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -e container=lxc -e HOSTNAME=161c56b9284f -- /bin/bash

LXC (LinuX Container), 자세한 정보는 http://linuxcontainers.org/ 에서 얻을 수 있다.

lxc container를 실행시켜주는 runtime으로, 앞에서 설명한것과 같이 VM과 비슷한 기능을 제공하지만, 실제 수행에 있어서, guest os (container)를 마치 VM처럼 isolate해서 수행해주는 기능을 제공한다.

이와 같이 Docker LXC라는 Linux에 특화된 feature를 사용하기 때문에, 제약 사항을 가지고 있는데, 현재까지 Docker Ubuntu 12.04 이상(Host OS)에서만 사용이 가능하다.

Performance에 대해서는 당연히 Host OS에서 직접 application 을 돌리는 것보다 performance 감소가 있는데, 아래 표와 같이 performance 감소가 매우 적은 것을 볼 수 있다.



출처: http://www.slideshare.net/modestjude/dockerat-deview-2013

Repository 연계

다음으로 Docker의 특징중의 하나는 repository 연계이다.Container Image를 중앙의 Repository에 저장했다가, 다른 환경에서 가져다가 사용할 수 있다. 마치 git와 같은 VCS (Version Control System)과 같은 개념인데, 이를 통해서 Application들을 Container로 패키징해서 다른 환경으로 쉽게 옮길 수 있다는 이야기다.



예를 들어 local pc에서 mysql, apache, tomcat등을 각 컨테이너에 넣어서 개발을 한 후에, 테스트 환경등으로 옮길때, Container repository에 저장했다가 테스트환경에서 pulling(당겨서) 똑같은 테스트환경을 꾸밀 수 있다는 것이다. Container에는 모든 application과 설치 파일, 환경 설정 정보 등이 들어 있기 때문에, 손쉽게 패키징 및 설치가 가능하다는 장점을 가지고 있다.

여기서 고려해야할점은 Docker는 아쉽게도 아직까지 개발중이고, 정식 릴리즈 버전이 아니다. 그래서, 아직까지는 production(운영환경)에 배포를 권장하고 있지 않다. 단 개발환경에서는 모든 개발자가 동일한 개발환경을 사용할 수 있게 할 수 있고, 또한 VM 보다 가볍기 때문에, 개발환경을 세팅하는데 적절하다고 볼 수 있다.

Base Image vs Dockerfile

Docker Container Image packing하기 위해서, Docker Base Image Docker file이라는 두가지 컨셉을 이용한다. 쉽게 설명하면, Base Image는 기본적인 인스톨 이미지, Docker file은 기본적인 인스톨 이미지와 그 위에 추가로 설치되는 스크립트를 정의한다.

예를 들어 Base Image Ubuntu OS 이미지라면, Docker FileUbuntu OS + Apache, MySQL을 인스톨하는 스크립트라고 보면 된다. 일반적으로 Docker Base Image는 기본 OS 인스톨 이미지라고 보면 된다. 만약에 직접 Base Image를 만들고 싶으면  http://docs.docker.io/en/latest/use/baseimages/ 를 참고하면 된다. docker에서는 미리 prebuilt in image들을 제공하는데, https://index.docker.io/ 를 보면 public repository를 통해서 제공되는 이미지들을 확인할 수 있다. 아직까지는 Ubuntu 많이 공식적으로 제공되고, 일반 contributor들이 배포한 centos 등의 이미지들을 검색할 수 있다. (2013.10.22 현재 redhat 등의 이미지는 없다.)

아래는 docker file 샘플이다. (출처 : http://docs.docker.io/en/latest/use/builder/)

# Nginx

#

# VERSION               0.0.1

 

FROM      ubuntu

MAINTAINER Guillaume J. Charmes <guillaume@dotcloud.com>

 

# make sure the package repository is up to date

RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list

RUN apt-get update

 

RUN apt-get install -y inotify-tools nginx apache2 openssh-server

위의 이미지는 Ubuntu OS 베이스 이미지에 apt-get을 이용해서, inotify-tools nginx apache2 openssh-serverf 를 인스톨하는 스크립트이다.

Docker 실행해보기

그럼 간단하게 docker를 테스트해보자, 윈도우즈 환경을 가정한다. 앞서 말한바와 같이 Docker Unbuntu 위에서만 작동한다. (참고 : http://docs.docker.io/en/latest/installation/windows/)

그래서, 윈도우즈 위에서 Ubuntu VM을 설치한후, Ubuntu VM에서 Docker를 실행할 것이다. 이를 위해서 VM을 수행하기 위한 환경을 설치한다.

l  Hypervisor Virtual Box를 설치한다. https://www.virtualbox.org 

l  VM을 실행하기 위한 vagrant를 설치한다. http://www.vagrantup.com 

l  Docker 코드를 다운받기 위해서 git 클라이언트를 설치한다. http://git-scm.com/downloads 

여기까지 설치했으면, docker를 실행하기 위한 준비가 되었다.

다음 명령어를 수행해서, docker 코드를 git hub에서 다운로드 받은 후에, vagrant를 이용해서 Ubuntu host os를 구동한다.

git clone https://github.com/dotcloud/docker.git

cd docker

vagrant up

Virtual Box를 확인해보면, Docker Host OS가 될 Ubuntu OS가 기동되었음을 확인할 수 있다.



그러면, 기동된 Ubuntu OS SSH를 이용해서 log in을 해보자. Putty를 이용해서 127.0.0.1:2222 포트로, SSH를 통해서 로그인한다. (기본 id,passwd vagrant/vagrant 이다.)

이제 Docker를 이용해서, public repository에서 “busybox”라는 Ubuntu OS Container로 설치하고, Container에서 “echo hello world” 명령어를 수행해보자

sudo su

docker run busybox echo hello world

Docker public repository에서 busybox image를 다운로드 받아서 설치하고, 아래와 같이 명령어를 수행했음을 확인할 수 있다.



※ 참고 : 현재 docker에 설치된 이미지 리스트 docker images, 설치된 이미지를 삭제할려면 docker rmi {image id}. {image id} docker images에서 hexa로 나온 id를 사용하면 된다.