순식간에 Kubernetes에 대해 알고가자.


1. 소개

Kubernetes란 무엇인지 알아보자.

쿠버네티스라고 부른다. 혹은 큐브(Kube)라고 부르기도 한다. 그리스어로 조타수, 항해사라는 뜻을 가지고 있다.


2. 의미

마이크로 서비스 아키텍처는 비즈니스 민첩성(Agility), 유연한 확장성(Scalability), 강한 복원력(Resiliency) 등을 제공한다. 하지만 단지 응용 프로그램을 마이크로 단위로 구현한다고 해서 이러한 이점을 얻을 수 있는 것은 아니다. 각 마이크로 서비스를 위한 독립된 실행 환경 확보 및 자동화된 배포(CI/CD) 환경이 뒷받침되어야 한다.

우선 각 마이크로 서비스들을 실행할 수 있는 독립된 환경이 필요하다. 또 마이크로 서비스 단위로 부하에 따라 빠르고 유연하게 확장/축소할 수 있어야 한다. 하지만 물리 서버 기반에서는 각 서비스별로 리소스 할당이 자유롭지 못하기 때문에 가상화 기법을 사용하는 것이 일반적이다. 가상화는 VM 가상화와 컨테이너 가상화로 구분할 수 있는데, 컨테이너 가상화는 VM 가상화에 비해 Guest OS 영역에 대한 오버헤드가 존재하지 않아 가볍고, 작고 독립적인 단위로 분할할 수 있기 때문에 확장성 측면에서도 장점이 있다.

여기서 잠깐,

(1) 각 컨테이너에는 자체 운영 체제 인스턴스가 있습니다.

컨테이너는 가상 머신보다 훨씬 빠르게 시작하고 더 적은 리소스를 사용합니다. 각 컨테이너에는 자체 운영 체제 인스턴스가 없기 때문입니다.

(2) 컨테이너는 환경에 느슨하게 연결됩니다.

  • 컨테이너화된 애플리케이션을 배포하면 가상 머신에 애플리케이션을 배포하는 것보다 리소스를 덜 소비하고 오류가 발생하기 쉽습니다.
  • 컨테이너는 이동하기 쉽습니다.
  • 컨테이너는 환경의 중요하지 않은 세부 정보를 추상화합니다.

컨테이너 가상화는 Docker가 대표적이다. 그런데 서버의 수와 컨테이너의 수가 증가하게 되면 그 관리가 쉽지 않으며, 서비스 단위 관리, 서비스 간 연결 방안도 필요하다. 이러한 요구사항을 충족하기 위하여 컨테이너 오케스트레이션 도구가 등장하게 되었는데 춘추전국시대를 거쳐 현재는 Kubernetes가 사실상의 표준으로 자리잡았다.


3. 역사

2014년 발표되었으니 역사가 그렇게 오래된 것은 아니다. 처음 만든 곳은 구글이다. 원래 구글에는 Borg라는 솔루션(?)이 있었는데 이것이 발전했다고 보면 된다. 위키에 따르면 첫 코드 네임은 "Seven"이었다고 하는데 이는 스타트랙의 1등 항해사임. 하지만 쿠버네티스가 정말 단기간에 만들어진 것은 아니다. 10년 이상의 구글 노하우가 집약되어 있다고 보면 된다.

Docker 등장 이후 시장에는 여러 Orchestration Tool들이 등장하였는데 2018년 정도에 쿠버네티스가 천하통일했다고 봐도 무방하다.

리눅스에서 컨테이너 기술은 namespace와 cgroup을 통해 가능해졌다고 할 수 있다.


4. 특징

4.1. 특징

  • 요구사항에 맞게 컨테이너를 배치
  • 컨테이너가 종료되면 자동 복구 (Desired State)
  • 자동으로 컨테이너 확장
  • 로드 밸런싱
  • 알아서 롤아웃, 롤백
  • 설정 관리

4.2. VM과 비교

흔히 VM에 대비한 컨테이너 기술은 Pet(애완동물) vs. Cattle(가축)으로 비유된다. VM(애완동물)에는 이름이 있고 생명주기가 길지만 컨테이너(가축)는 이름도 없고 일찍 죽는다..

멀티 테넌시를 구현할 수 있는데 1) namespace를 이용하거나, 2) 클러스터 분리를 통해 가능하다.

근데 컨테이너가 모든 것이 좋은 것은 아니다.

  • Scale out : VM의 경우는 노드를 수동으로 추가해야 하나, 쿠버네티스는 StatefulSet의 replica 구성을 통해 자동 적용 가능함
  • 장애 복구 : VM은 수동 복구하지만 쿠버네티스는 자체적으로 자동 복구
  • 자원 사용 측면 : 쿠버네티스는 자체 관리 기능들을 위한 추가 자원이 필요함
  • 모니터링 : 쿠버네티스는 VM과 Pod 측면의 모니터링을 위한 추가 구성이 필요

5. Node

노드에서는 마스터 노드와 워커 노드가 있다. 마스터 노드는 전체적으로 관장하는 노드이고 워커 노드는 일을 하는 노드이다. (당연한 말..)

5.1. 마스터 노드

클러스터의 상태를 관리하고, API 서비스를 제공하는 노드이다.

API Server, etcd, Scheduler(kube-scheduler), Controller 등으로 구성된다.

5.1.1. API Server

  • HTTP나 HTTPS 기반으로 통신하며 REST API를 제공한다.
  • 여러 구성 요소 간 허브 역할을 한다. 따라서 고가용성을 위한 복제구조를 추천한다.

5.1.2. etcd

  • etcd는 클러스터의 상태를 관리하는 State machine이라고 보면 되며 etcd에 read/write라는 주체는 무조건 API Server이다.
  • 고가용성을 위해서는 마스터 노드와 분리된 외부(external) etcd 구성을 추천한다.

5.1.3. Scheduler

  • Scheduler는 Pod를 어느 노드에서 돌릴지 결정한다.
  • Pod의 스케줄링 요건을 충족하는 클러스터 노드를 Pod에 feasible한 노드라고 하는데 Scheduler는 Pod에 적합한 노드를 찾고 기능 셋을 실행하여 노드의 점수를 측정한다.
  • 측정 지표는 CPU, 메모리, 실행 중인 컨테이너 수 등이다.
  • 그리고 적합한 노드들 중 가장 높은 점수를 가진 노드를 선택한다. 이후 Scheduler는 바인딩이라는 프로세스로 API 서버에 결정사항을 통지한다.

5.1.4. Contoller

컨트롤러는 노드 관리, 내부 정보 생성 및 업데이트, 상태 변경 등을 수행한다.

5.1.5. High Availability

  • 마스터의 다중화는 etcd의 동기화 문제를 고민한다.
  • 워커가 많으면 etcd를 외부로 분리하고 다중화로 설계할 수 있다.

5.2. 워커 노드

kubelet, kube-proxy 등으로 구성된다. 

5.2.1. kubelet

kubelet은 각 워커 노드에서 실행된다. 노드 에이전트 역할이라고 보면 된다. Pod를 생성하기 위해서는 kubelet에 컨테이너 런타임 환경이 필요한데 기본적으로는 도커를 많이 사용한다. 물론 다른 컨테이너 런타임 환경을 사용할 수도 있다. 이와 관련해서는 이 문서를 확인한다.

워커 노드 설계 시에는 적정 수량을 고민해야 하는데 워커 노드가 너무 많으면 마스터의 API Server가 부담이 되고, 소수의 워커 노드에 Pod가 너무 많으면 kubelet이 부담이 된다.

5.2.2. kube-proxy

kube-proxy는 호스트의 NIC를 컨테이너에 브리지한다.


6. 클러스터 접근

6.1. kubectl

kubectl이 사용하는 위치 정보, 인증 정보는 kubectl config view 로 확인 가능하다.

6.2. REST API

HTTP 클라이언트로 REST API에 접근하고자 한다면, 위치 정보와 인증을 위한 몇가지 방법이 있다.

6.2.1. kubectl proxy

kubectl proxy --port=8080 처럼 하면 curl http://localhost:8080/api/ 처럼 접근 가능하다.

6.2.2. kubectl describe secret

grep과 cut의 조합으로 얻어낼 수 있다.

APISERVER=$(kubectl config view --minify | grep server | cut -f 2- -d ":" | tr -d " ")
SECRET_NAME=$(kubectl get secrets | grep ^default | cut -f1 -d ' ')
TOKEN=$(kubectl describe secret $SECRET_NAME | grep -E '^token' | cut -f2 -d':' | tr -d " ")

curl $APISERVER/api --header "Authorization: Bearer $TOKEN" --insecure

혹은 jsonpath를 사용할 수도 있다.

APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
SECRET_NAME=$(kubectl get serviceaccount default -o jsonpath='{.secrets[0].name}')
TOKEN=$(kubectl get secret $SECRET_NAME -o jsonpath='{.data.token}' | base64 --decode)

curl $APISERVER/api --header "Authorization: Bearer $TOKEN" --insecure

다만 --insecure 옵션은 MITM 공격의 여지가 있다. 

6.2.3. 프로그래밍 접근

Go, Python 용 클라이언트 라이브러리가 지원된다.

6.2.4. Pod에서 접근

Pod에서 API 접근 시에는 apiserver의 위치 지정과 인증이 다르다.

Pod에서 apiserver 위치 지정은 kubernetes.default.svc DNS 사용이 권장된다. 이 DNS 이름은 apiserver로 라우팅되는 서비스 IP가 resolve된다.

apiserver 인증에 추천하는 방식은 서비스 어카운트 인증을 사용하는 것이다.

  • kube-system에 의해 Pod는 서비스 어카운트와 연계
    -> 해당 서비스 어카운트의 인증 정보(토큰)  파일 : Pod 내 각 컨테이너 파일시스템의 /var/run/secrets/kubernetes.io/serviceaccount/token
  • apiserver의 인증서 제공을 검증하는 사용할 인증서 번들 파일 : 각 컨테이너 파일시스템의 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  • 네임스페이스의 한정된 API 조작에 사용할 기본 네임스페이스 파일 : 각 컨테이너 파일시스템의 /var/run/secrets/kubernetes.io/serviceaccount/namespace

접근 방식은 다음과 같다.

  • Pod의 sidecar 컨테이너에서 kubectl proxy를 실행하거나 컨테이너 내부에서 백그라운드 프로세스로 실행한다. 이는 쿠버네티스 API를 Pod의 localhost 인터페이스로 proxy하여 해당 Pod 컨테이너에서 다른 프로세스가 API에 접근할 수 있게 한다.
  • Go 클라이언트 라이브러리를 통해 rest.InClutserConfig(), kubernetes.newForConfig() 함수를 사용하도록 클라이언트를 만든다. 이는 apiserver의 위치지정과 인증을 처리한다.

7. Pods

  • 쿠버네티스를 다룬다면 정말 많이 듣게 되는 말이다. Pod들은 단일 노드(물리 머신 혹은 VM) 상에서 실행되며 하나 이상의 컨테이너 그룹이다. 
  • 하나의 Pod에 여러 앱을 넣을 수는 있지만 실제 그렇게 하지 않는다. 무조건 하나의 Pod에는 하나의 앱만 넣자. 
  • Pod 안에는 pause가 있는데 여러 컨테이너의 stop/start를 관리하고 namespace를 생성한다. (** 쿠버네티스 자체의 namespace와는 다른 개념이다)
  • Pod는 컨트롤러에 의해 관리되며 어느 특정 위치에 고정되어 실행되지 읺으며 이리저리 떠돌아다닌다. 이렇게 동적인 생명체인 Pod에 접근하기 위해서 사용하는 것이 Service이다. Service를 사용함으롷서 Pod에 어디에 있든 고정된 주소를 통해 접근이 가능해진다. 좀 더 자세히 알아보자.

8. Service

각 Pod들이 고유 IP를 갖고 있지만 Service의 도움 없이는 클러스터 외부로 노출될 수 없다.

Service는 크게 4종류가 있다.

8.1. ClusterIP

가장 기본적인 Service이고, 클러스터 내부에서 사용 가능하다. 다시 말해 클러스터 외부에서는 사용할 수 없다.

apiVersion: v1
kind: Service
metadata:
  name: my-cip-service
spec:
  type: ClusterIP
  selector:
    app: metrics
    department: sales
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

8.2. NodePort

각 노드의 지정된 포트를 할당한다. 노드의 포트를 사용하기 때문에 클러스터 내부와 외부에서도 접근 가능하다. 특이한 점이 하나 있는데 예를 들어 포트가 8080이고 Pod가 node1에만 있고 node2에는 없을 때 node2:8080으로 접근하더라도 node1의 Pod에 연결된다.

apiVersion: v1
kind: Service
metadata:
  name: my-np-service
spec:
  type: NodePort
  selector:
    app: metrics
    department: engineering
  ports:
  - protocol: TCP
    port: 80
    targetPort: 50000

8.3. LoadBalancer

GCP, AWS 등 퍼블릭 클라우드 서비스를 사용할 때 가능한 옵션이다. Pod를 클라우드가 제공하는 로드 밸런서를 통해 외부로부터 접근이 가능하게 해준다.

apiVersion: v1
kind: Service
metadata:
  name: my-lb-service
spec:
  type: LoadBalancer
  selector:
    app: products
    department: sales
  ports:
  - protocol: TCP
    port: 60000
    targetPort: 50001

8.4. ExternalName

Service를 externalName의 값과 매치한다. 주로 클러스터 내부에서 외부로 접근할 때 사용한다. 외부로 접근 시 주로 사용하기 때문에 설정할 때 셀렉터가 필요 없다. 이 서비스로 접근 시에는 설정된 CNAME 값으로 연결되어 클러스터 외부에서 접근할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name: my-xn-service
spec:
  type: ExternalName
  externalName: example.com

아래 설명할 Ingress를 사용하지 않는 경우에도 NodePort 등을 사용하면 외부 요청을 처리할 수 있음을 알 수 있다.


9. Ingress

쿠버네티스 외부 -> 내부로 들어오는 트래픽을 처리하기 위한 규칙 세트로 Layer 7의 요청을 처리할 수 있다.

기능은 다음과 같다.

  • 로드 밸런스
  • SSL/TLS 인증서 처리
  • 외부에서 접근 가능한 URL 제공
  • 도메인 기반 가상 호스팅 제공

외부 요청 처리 측면에서 어느 정도는 NodePort로도 가능은 하지만, 세부적인 기능을 애플리케이션 레벨에서 구현 시에는 꽤나 복잡하게 될 것이다.

Ingress가 위와 같은 개념이라고 하면 실제 구현체는 Ingress Controller이다. 쿠버네티스가 공식적으로 제공하는 Ingress Controller는 ingress-gce, ingress-nginx (https://github.com/kubernetes/ingress-nginx)이다.

다음과 같은 yaml 파일을 한번 보자.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: sarcio.com
    http:
      paths:
      - path: /top
        backend:
          serviceName: svc1
          servicePort: 80
      - path: /main
        backend:
          serviceName: svc2
          servicePort: 80
  - host: iosarc.com
    http:
      paths:
      - backend:
          serviceName: svc2
          servicePort: 80

해석하면 다음과 같다.

  • sarcio.com/top : svc1 서비스 연결
  • sarcio.com/main : svc2 서비스 연결
  • iosarc.com : svc2 서비스 연결

10. CI/CD

여러가지 방안이 있다.

  • VSCode에서 쿠버네티스 플러그인 설치한 후 VSCode로 개발한 코드를 Skaffold에서 이미지로 빌드하여 쿠버네티스로 배포
  • Unit Test를 위해 Jenkins가 제공하는 플러그인을 이용하여 쿠버네티스 Pod를 동적으로 생성하여 Jenkins의 slave로 사용
    -> 로컬에서 개발하여 Github로 푸시하고, Github는 Jenkins로 Webhook 날리고, Jenkins는 Pod 생성하여 Unit Test하고, Docker hub로 푸시

10.1. 쿠버네티스에서 Jenkins 구축

10.1.1. 환경

  • Persistent Volume을 사용할 수 있는 쿠버네티스 환경 
  • Helm 사용 가능 환경

kubeadm와 테라폼으로 구성한 3개 워커 노드 환경을 만들었다고 하자. 쿠버네티스 클러스터는 AWS EBS와 연동이 된다고 하자.

(작성중)


11. EKS

AWS가 제공하는 관리형 Kubernetes 서비스이다.

11.1. 소개

  • Kubernetes 제어부(Control Plane, API 서버 + etcd)는 3개 AZ를 통해 고가용성으로 실행된다.
  • EKS 인증에는 Heptio Authenticator를 사용한다. Authenticator를 사용하면 kubectl이 Kubernetes 클러스터에 액세스 시 IAM 인증을 사용할 수 있다.
  • PersistentVolumes에는 EBS 볼륨을 사용한다.

11.2. 전환 전략

  • 기존에 AWS에서 kops 기반으로 Kubernetes를 구성하여 운영 중이었는데 이를 EKS로 전환하고자 한다.
  • 도메인을 Route 53의 가중치 기반으로 설정하고 구 NLB(to kops), 신 NLB(to EKS)를 바라보게 한다. 
  • 구 NLB의 가중치는 100 -> 0으로, 신 NLB의 가중치는 0 -> 100으로 점차 조정한다.
  • 단 DNS는 시간차가 있으므로 일부 클라이언트는 여전히 구 NLB를 바라볼 수 있다. 따라서 일정 기간 구 NLB에 신규 리소스(EKS)를 붙여 처리하도록 한다.
  • 구 리소스의 경우 Terraform으로 설치된 경우 terraform destroy하여 날린다.