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


Archive»


 
 

쿠버네티스 #7

서비스 (service)


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


Service

쿠버네티스 서비스에 대해서 자세하게 살펴보도록 한다.

Pod의 경우에 지정되는 Ip가 랜덤하게 지정이 되고 리스타트 때마다 변하기 때문에 고정된 엔드포인트로 호출이 어렵다, 또한 여러 Pod에 같은 애플리케이션을 운용할 경우 이 Pod 간의 로드밸런싱을 지원해줘야 하는데, 서비스가 이러한 역할을 한다.

서비스는 지정된 IP로 생성이 가능하고, 여러 Pod를 묶어서 로드 밸런싱이 가능하며, 고유한 DNS 이름을 가질 수 있다.


서비스는 다음과 같이 구성이 가능하며, 라벨 셀렉터 (label selector)를 이용하여, 관리하고자 하는 Pod 들을 정의할 수 있다.


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer

멀티 포트 지원

서비스는 동시에 하나의 포트 뿐 아니라 여러개의 포트를 동시에 지원할 수 있다. 예를 들어 웹서버의 HTTP와 HTTPS 포트가 대표적인 예인데,  아래와 같이 ports 부분에 두개의 포트 정보를 정의해주면 된다.

apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

   - name: https

     port: 443

     protocol: TCP

     targetPort: 8082

 type: LoadBalancer

로드 밸런싱 알고리즘

서비스가 Pod들에 부하를 분산할때 디폴트 알고리즘은 Pod 간에 랜덤으로 부하를 분산하도록 한다.

만약에 특정 클라이언트가 특정 Pod로 지속적으로 연결이 되게 하려면  Session Affinity를 사용하면 되는데, 서비스의 spec 부분에 sessionAffinity: ClientIP로 주면 된다.




웹에서 HTTP Session을 사용하는 경우와 같이 각 서버에 각 클라이언트의 상태정보가 저장되어 있는 경우에 유용하게 사용할 수 있다.

Service Type

서비스는 IP 주소 할당 방식과 연동 서비스등에 따라 크게 4가지로 구별할 수 있다.

  • Cluster IP

  • Load Balancer

  • Node IP

  • External name


ClusterIP

디폴트 설정으로, 서비스에 클러스터 IP (내부 IP)를 할당한다. 쿠버네티스 클러스터 내에서는 이 서비스에 접근이 가능하지만, 클러스터 외부에서는 외부 IP 를 할당  받지 못했기 때문에, 접근이 불가능하다.

Load Balancer

보통 클라우드 벤더에서 제공하는 설정 방식으로, 외부 IP 를 가지고 있는 로드밸런서를 할당한다. 외부 IP를 가지고 있기  때문에, 클러스터 외부에서 접근이 가능하다.

NodePort

클러스터 IP로만 접근이 가능한것이 아니라, 모든 노드의 IP와 포트를 통해서도 접근이 가능하게 된다. 예를 들어 아래와 같이 hello-node-svc 라는 서비스를 NodePort 타입으로 선언을 하고, nodePort를 30036으로 설정하면, 아래 설정에 따라 클러스터 IP의  80포트로도 접근이 가능하지만, 모든 노드의 30036 포트로도 서비스를 접근할 수 있다.


hello-node-svc-nodeport.yaml


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 type: NodePort

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

     nodePort: 30036


아래 그림과 같은 구조가 된다.




이를 간단하게 테스트 해보자.

아래는 구글 클라우드에서 쿠버네티스 테스트 환경에서 노드로 사용되고 있는 3개의 VM 목록과 IP 주소이다.


현재 노드는 아래와 같이 3개의 노드가 배포되어 있고 IP 는 10.146.0.8~10이다.

내부 IP이기 때문에, VPC 내의 내부 IP를 가지고 있는 서버에서 테스트를 해야 한다.


같은 내부 IP를 가지고 있는 envoy-ubuntu 라는 머신 (10.146.0.18)에서 각 노드의 30036 포트로 curl을 테스트해본 결과 아래와 같이 모든 노드의 IP를 통해서 서비스 접근이 가능한것을 확인할 수 있다.



ExternalName

ExternalName은 외부 서비스를 쿠버네티스 내부에서 호출하고자할때 사용할 수 있다.

쿠버네티스 클러스터내의 Pod들은 클러스터 IP를 가지고 있기 때문에 클러스터 IP 대역 밖의 서비스를 호출하고자 하면, NAT 설정등 복잡한 설정이 필요하다.

특히 AWS 나 GCP와 같은 클라우드 환경을 사용할 경우 데이타 베이스나, 또는 클라우드에서 제공되는 매지니드 서비스 (RDS, CloudSQL)등을 사용하고자할 경우에는 쿠버네티스 클러스터 밖이기 때문에, 호출이 어려운 경우가 있는데, 이를 쉽게 해결할 수 있는 방법이 ExternalName 타입이다.

아래와 같이 서비스를 ExternalName 타입으로 설정하고, 주소를 DNS로  my.database.example.com으로 설정해주면 이 my-service는 들어오는 모든 요청을 my.database.example.com 으로 포워딩 해준다. (일종의 프록시와 같은 역할)

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

다음과 같은 구조로 서비스가 배포된다.



DNS가 아닌 직접 IP를 이용하는 방식

위의 경우 DNS를 이용하였는데, DNS가 아니라 직접 IP 주소를 이용하는 방법도 있다.

서비스 ClusterIP 서비스로 생성을 한 후에, 이 때 서비스에 속해있는 Pod를 지정하지 않는다.

apiVersion: v1

kind: Service

metadata:

 name: external-svc-nginx

spec:

 ports:

 - port: 80



다음으로, 아래와 같이 서비스의 EndPoint를 별도로 지정해주면 된다.

apiVersion: v1

kind: Endpoints

metadata:

 name: external-svc-nginx

subsets:

 - addresses:

   - ip: 35.225.75.124

   ports:

   - port: 80


이 때 서비스명과 서비스 EndPoints의 이름이 동일해야 한다. 위의 경우에는 external-svc-nginx로 같은 서비스명을 사용하였고 이 서비스는 35.225.75.124:80 서비스를 가르키도록 되어 있다.

그림으로 구조를 표현해보면 다음과 같다.




35.225.75.124:80 은 nginx 웹서버가 떠 있는 외부 서비스이고, 아래와 같이 간단한 문자열을 리턴하도록 되어 있다.



이를 쿠버네티스 내부 클러스터의 Pod 에서 curl 명령을 이용해서 호출해보면 다음과 같이 외부 서비스를 호출할 수 있음을 확인할 수 있다.

Headless Service

서비스는 접근을 위해서 Cluster IP 또는 External IP 를 지정받는다.

즉 서비스를 통해서 제공되는 기능들에 대한 엔드포인트를 쿠버네티스 서비스를 통해서 통제하는 개념인데, 마이크로 서비스 아키텍쳐에서는 기능 컴포넌트에 대한 엔드포인트 (IP 주소)를 찾는 기능을 서비스 디스커버리 (Service Discovery) 라고 하고, 서비스의 위치를 등록해놓는 서비스 디스커버리 솔루션을 제공한다. Etcd 나 hashcorp의 consul (https://www.consul.io/)과 같은 솔루션이 대표적인 사례인데, 이 경우 쿠버네티스 서비스를 통해서 마이크로 서비스 컴포넌트를 관리하는 것이 아니라, 서비스 디스커버리 솔루션을 이용하기 때문에, 서비스에 대한 IP 주소가 필요없다.

이런 시나리오를 지원하기 위한 쿠버네티스의 서비스를 헤드리스 서비스 (Headless service) 라고 하는데, 이러한 헤드리스 서비스는 Cluster IP등의 주소를 가지지 않는다. 단 DNS이름을 가지게 되는데, 이 DNS 이름을 lookup 해보면, 서비스 (로드밸런서)의 IP 를 리턴하지 않고, 이 서비스에 연결된 Pod 들의 IP 주소들을 리턴하게 된다.


간단한 테스트를 해보면


와 같이 기동중인 Pod들이 있을때, Pod의 IP를 조회해보면 다음과 같다.


10.20.0.25,10.20.0.22,10.20.0.29,10.20.0.26 4개가 되는데,

다음 스크립트를 이용해서 hello-node-svc-headless 라는 헤드리스 서비스를 만들어보자


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc-headless

spec:

 clusterIP: None

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080


아래와 같이 ClusterIP가 할당되지 않음을 확인할 수 있다.



다음 쿠버네티스 클러스터내의 다른 Pod에서 nslookup으로 해당 서비스의 dns 이름을 조회해보면 다음과 같이 서비스에 의해 제공되는 pod 들의 IP 주소 목록이 나오는 것을 확인할 수 있다.




Service discovery

그러면 생성된 서비스의 IP를 어떻게 알 수 있을까? 서비스가 생성된 후 kubectl get svc를 이용하면 생성된 서비스와 IP를 받아올 수 있지만, 이는 서비스가 생성된 후이고, 계속해서 변경되는 임시 IP이다.

DNS를 이용하는 방법

가장 쉬운 방법으로는 DNS 이름을 사용하는 방법이 있다.

서비스는 생성되면 [서비스 명].[네임스페이스명].svc.cluster.local 이라는 DNS 명으로 쿠버네티스 내부 DNS에 등록이 된다. 쿠버네티스 클러스터 내부에서는 이 DNS 명으로 서비스에 접근이 가능한데, 이때 DNS에서 리턴해주는 IP는 외부 IP (External IP)가 아니라 Cluster IP (내부 IP)이다.


아래 간단한 테스트를 살펴보자. hello-node-svc 가 생성이 되었는데, 클러스터내의 pod 중 하나에서 ping으로 hello-node-svc.default.svc.cluster.local 을 테스트 하니, hello-node-svc의 클러스터 IP인 10.23.241.62가 리턴되는 것을 확인할 수 있다.



External IP (외부 IP)

다른 방식으로는 외부 IP를 명시적으로 지정하는 방식이 있다. 쿠버네티스 클러스터에서는 이 외부 IP를 별도로 관리하지 않기 때문에, 이 IP는 외부에서 명시적으로 관리되어야 한다.


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 externalIPs:

 - 80.11.12.11

 

외부 IP는 Service의 spec 부분에서 externalIPs 부분에 IP 주소를 지정해주면 된다.

구글 클라우드의 경우

퍼블릭 클라우드 (AWS, GCP 등)의 경우에는 이 방식 보다는 클라우드내의 로드밸런서를 붙이는 방법을 사용한다.


구글 클라우드의 경우를 살펴보자.서비스에 정적인 IP를 지정하기 위해서는 정적 IP를 생성해야 한다. 구글 클라우드 콘솔내의 VPC 메뉴의 External IP 메뉴에서 생성해도 되고, 아래와 같이 gcloud CLI 명령어를 이용해서 생성해도 된다.


IP를 생성하는 명령어는 gcloud compute addresses create [IP 리소스명] --region [리전]

을 사용하면 된다. 구글 클라우드의 경우에는 특정 리전만 사용할 수 있는 리저널 IP와, 글로벌에 모두 사용할 있는 IP가 있는데, 서비스에서는 리저널 IP만 사용이 가능하다. (글로벌 IP는 후에 설명하는 Ingress에서 사용이 가능하다.)

아래와 같이

%gcloud compute addresses create hello-node-ip-region  --region asia-northeast1

명령어를 이용해서 asia-northeast1 리전 (일본)에 hello-node-ip-region 이라는 이름으로 Ip를 생성하였다. 생성된 IP는 describe 명령을 이용해서 확인할 수 있으며, 아래 35.200.64.17 이 배정된것을 확인할 수 있다.



이 IP는 서비스가 삭제되더라도 계속 유지되고, 다시 재 사용이 가능하다.

그러면 생성된 IP를 service에 적용해보자

다음과 같이 hello-node-svc-lb-externalip.yaml  파일을 생성하자


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer

 loadBalancerIP: 35.200.64.17


타입을 LoadBalancer로 하고, loadBalancerIP 부분에 앞에서 생성한 35.200.64.17 IP를 할당한다.

다음 이 파일을 kubectl create -f hello-node-svc-lb-externalip.yaml 명령을 이용해서 생성하면, hello-node-svc 가 생성이 되고, 아래와 같이 External IP가 우리가 앞에서 지정한 35.200.64.17 이 지정된것을 확인할 수 있다.




쿠버네티스 #6

Replication Controller를 이용하여 서비스 배포하기

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


1. 도커 파일 만들기

node.js로 간단한 웹서버를 만들어서 도커로 패키징 해보자.

실습을 진행하기 위해서 로컬 환경에 도커와, node.js 가 설치되어 있어야 한다. 이 두 부분은 생략하도록 한다.

여기서 사용한 실습 환경은 node.js carbon 버전 (8.11.3), 도커 맥용 18.05.0-ce, build f150324 을 사용하였다.

node.js 애플리케이션 준비하기

node.js로 간단한 웹 애플리케이션을 제작해보자 server.js라는 이름으로 아래 코드를 작성한다.

var os = require('os');

 

var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

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

 

 //log

 console.log("["+

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

               "] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


이 코드는 8080 포트로 웹서버를 띄워서 접속하면 “Hello World!” 문자열과 함께, 서버의 호스트명을 출력해준다. 그리고 stdout에 로그로, 시간과 서버의 호스트명을 출력해준다.

코드 작성이 끝났으면, 서버를 실행해보자

%node server.js


다음 브라우저로 접속하면 다음과 같은 결과를 얻을 수 있다.


그리고 콘솔화면에는 아래와 같이 시간과 호스트명이 로그로 함께 출력된다.

도커로 패키징하기

그러면 이 node.js 애플리케이션을 도커 컨테이너로 패키징 해보자

Dockerfile 이라는 파일을 만들고 아래 코드를 작성한다.

FROM node:carbon

EXPOSE 8080

COPY server.js .

CMD node server.js > log.out


이 코드는 node.js carborn (8.11.3) 컨테이너 이미지를 베이스로 한후에,  앞서 작성한 server.js 코드를 복사한후에, node server.js > log.out 명령어를 실행하도록 하는 컨테이너를 만드는 설정파일이다.

설정 파일이 준비되었으면,  도커 컨테이너 파일을 만들어보자


% docker build -t gcr.io/terrycho-sandbox/hello-node:v1 .


docker build  명령은 컨테이너를 만드는 명령이고, -t는 빌드될 이미지에 대한 태그를 정하는 명령이다.

빌드된 컨테이너 이미지는 gcr.io/terrycho-sandbox/hello-node로  태깅되는데, 이는 향후에 구글 클라우드 컨테이너 레지스트리에 올리기 위해서 태그 명을 구글 클라우드 컨테이너 레지스트리의 포맷을 따른 것이다. (참고 https://cloud.google.com/container-registry/docs/pushing-and-pulling)

포맷은 [HOST_NAME]/[GOOGLE PROJECT-ID]/[IMAGE NAME]


gcr.io/terrycho-sandbox는 도커 이미지가 저장될 리파지토리의 경로를 위의 규칙에 따라 정의한 것인데,

  • gcr.io는 구글 클라우드 컨테이너 리파지토리 US 리전을 지칭하며,

  • terrycho-sandbox는 본인의 구글 프로젝트 ID를 나타낸다.

  • 이미지명을 hello-node 로 지정하였다.

  • 마지막으로 콜론(:) 으로 구별되어 정의한 부분은 태그 부분으로, 여기서는 “v1”으로 태깅을 하였다.


이미지는 위의 이름으로 지정하여 생성되어 로컬에 저장된다.




빌드를 실행하면 위와 같이 node:carbon 이미지를 읽어와서 필요한 server.js 파일을 복사하고 컨테이너 이미지를 생성한다.

컨테이너 이미지가 생성되었으면 로컬 환경에서 이미지를 기동 시켜보자


%docker run -d -p 8080:8080 gcr.io/terrycho-sandbox/hello-node:v1


명령어로 컨테이너를 실행할 수 있다.

  • -d 옵션은 컨테이너를 실행하되, 백그라운드 모드로 실행하도록 하였다.

  • -p는 포트 맵핑으로 뒤의 포트가 도커 컨테이너에서 돌고 있는 포트이고, 앞의 포트가 이를 밖으로 노출 시키는 포트이다 예를 들어 -p 9090:8080 이면 컨테이너의 8080포트를 9090으로 노출 시켜서 서비스 한다는 뜻이다. 여기서는 컨테이너 포트와 서비스로 노출 되는 포트를 동일하게 8080으로 사용하였다.


컨테이너를 실행한 후에, docker ps 명령어를 이용하여 확인해보면 아래와 같이 hello-node:v1 이미지로 컨테이너가 기동중인것을 확인할 수 있다.



다음 브라우져를 통해서 접속을 확인하기 위해서 localhost:8080으로 접속해보면 아래와 같이 Hello World 와 호스트명이 출력되는 것을 확인할 수 있다.


로그가 제대로 출력되는지 확인하기 위해서 컨테이너 이미지에 쉘로 접속해보자

접속하는 방법은


% docker exec -i -t [컨테이너 ID] /bin/bash

를 실행하면 된다. 컨테이너 ID 는 앞의 docker ps 명령을 이용하여 기동중인 컨테이너 명을 보면 처음 부분이 컨테이너 ID이다.

hostname 명령을 실행하여 호스트명을 확인해보면 위에 웹 브라우져에서 출력된 41a293ba79a7과 동일한것을 확인할 수 있다. 디렉토리에는 server.js 파일이 복사되어 있고, log.out 파일이 생성된것을 볼 수 있다.  

cat log.out을 이용해서 보면, 시간과 호스트명이 로그로 출력된것을 확인할 수 있다.



2. 쿠버네티스 클러스터 준비

구글 클라우드 계정 준비하기

구글 클라우드 계정 생성은 http://bcho.tistory.com/1107 문서를 참고하기 바란다.

쿠버네티스 클러스터 생성하기

쿠버네티스 클러스터를 생성해보자, 클러스터 생성은 구글 클라우드 콘솔의 Kubernetes Engine > Clusters 메뉴에서 Create 를 선택하면 클러스터 생성이 가능하다.



클러스터 이름을 넣어야 하는데, 여기서는 terry-gke-10 을 선택하였다. 구글 클라우드에서 쿠버네티스 클러스터는 싱글 존에만 사용가능한 Zonal 클러스터와 여러존에 노드를 분산 배포하는 Regional 클러스터 두 가지가 있는데, 여기서는 하나의 존만 사용하는 Zonal 클러스터를 설정한다. (Regional은 차후에 다루도록 하겠다.)

다음 클러스터를 배포한 존을 선택하는데, asia-northeast1-c (일본)을 선택하였다.

Cluster Version은 쿠버네티스 버전인데, 1.10.2 버전을 선택한다.

그리고 Machine type은 쿠버네티스 클러스터의 노드 머신 타입인데, 간단한 테스트 환경이기 때문에,  2 CPU에 7.5 메모리를 지정하였다.

다음으로 Node Image는 노드에 사용할 OS 이미지를 선택하는데, Container Optimized OS를 선택한다. 이 이미지는 컨테이너(도커)를 운영하기 위해 최적화된 이미지이다.

다음으로는 노드의 수를 Size에서 선택한다. 여기서는 3개의 노드를 운용하도록 설정하였다.


아래 부분에 보면  Automatic node upgrades 라는 기능이 있다.


구글 클라우드의 재미있는 기능중 하나인데, 쿠버네티스 버전이 올라가면 자동으로 버전을 업그레이드 해주는 기능으로, 이 업그레이드는 무정지로 진행 된다.


gcloud 와 kubectl 설치하기

클러스터 설정이 끝났으면 gloud (Google Cloud SDK 이하 gcloud)를 인스톨한다.

gcloud 명령어의 인스톨 방법은 OS마다 다른데, https://cloud.google.com/sdk/docs/quickstarts 문서를 참고하면 된다.

별다른 어려운 작업은 없고, 설치 파일을 다운 받아서 압축을 푼후에, 인스톨 스크립트를 실행하면 된다.


kubectl은 쿠버네티스의 CLI (Command Line Interface)로, gcloud를 인스톨한후에,

%gcloud components install kubectl

명령을 이용하면 인스톨할 수 있다.

쿠버네티스 클러스터 인증 정보 얻기

gcloud와 kubectl 명령을 설치하였으면, 이 명령어들을 사용할때 마다 쿠버네티스에 대한 인증이 필요한데, 인증에 필요한 인증 정보는 아래 명령어를 이용하면, 자동으로 사용이 된다.

gcloud container clusters get-credentials CLUSTER_NAME

여기서는 클러스터명이 terry-gke10이기 때문에,

%gcloud container clusters get-credentials terry-gke-10

을 실행한다.


명령어 설정이 끝났으면, gcloud 명령이 제대로 작동하는지를 확인하기 위해서, 현재 구글 클라우드내에 생성된 클러스터 목록을 읽어오는 gcloud container clusters list 명령어를 실행해보자



위와 같이 terry-gke-10 이름으로 asia-northeast1-c 존에 쿠버네티스 1.10.2-gke.3 버전으로 클러스터가 생성이 된것을 볼 수 있고, 노드는 총 3개의 실행중인것을 확인할 수 있다.

3. 쿠버네티스에 배포하기

이제 구글 클라우드에 쿠버네티스 클러스터를 생성하였고, 사용을 하기 위한 준비가 되었다.

앞에서 만든 도커 이미지를 패키징 하여, 이 쿠버네티스 클러스터에 배포해보도록 하자.

여기서는 도커 이미지를 구글 클라우드내의 도커 컨테이너 레지스트리에 등록한 후, 이 이미지를 이용하여 ReplicationController를 통해 총 3개의 Pod를 구성하고 서비스를 만들어서 이 Pod들을 외부 IP를 이용하여 서비스를 제공할 것이다.

도커 컨테이너 이미지 등록하기

먼저 앞에서 만든 도커 이미지를 구글 클라우드 컨테이너 레지스트리(Google Container Registry 이하 GCR) 에 등록해보자.

GCR은 구글 클라우드에서 제공하는 컨테이너 이미지 저장 서비스로, 저장 뿐만 아니라, CI/CD 도구와 연동하여, 자동으로 컨테이너 이미지를 빌드하는 기능, 그리고 등록되는 컨테이너 이미지에 대해서 보안적인 문제가 있는지 보안 결함을 스캔해주는 기능과 같은 다양한 기능을 제공한다.


컨테이너 이미지를 로컬환경에서 도커 컨테이너 저장소에 저장하려면 docker push라는 명령을 사용하는데, 여기서는 GCR을 컨테이너 이미지 저장소로 사용할 것이기 때문에, GCR에 대한 인증이 필요하다.

인증은 한번만 해놓으면 되는데

%gcloud auth configure-docker

명령을 이용하면, 인증 정보가 로컬 환경에 자동으로 저장된다.



인증이 완료되었으면, docker push 명령을 이용하여 이미지를 GCR에 저장한다.

%docker push gcr.io/terrycho-sandbox/hello-node:v1


명령어를 실행하면, GCR에 hello-node 이미지가 v1 태그로 저장된다.


이미지가 GCR에 잘 저장되었는지를 확인하기 위해서 구글 클라우드 콘솔에 Container Registry (GCR)메뉴에서 Images라는 메뉴를 들어가보자




아래와 같이 hello-node 폴더에 v1이라는 태그로 이미지가 등록된것을 확인할 수 있다.

ReplicationController 등록

컨테이너 이미지가 등록되었으면 이 이미지를 이용해서 Pod를 생성해보자,  Pod 생성은 Replication Controller (이하 rc)를 생성하여, rc가 Pod 생성 및 컨트롤을 하도록 한다.


다음은 rc 생성을 위한 hello-node-rc.yaml 파일이다.


apiVersion: v1

kind: ReplicationController

metadata:

 name: hello-node-rc

spec:

 replicas: 3

 selector:

   app: hello-node

 template:

   metadata:

     name: hello-node-pod

     labels:

       app: hello-node

   spec:

     containers:

     - name: hello-node

       image: gcr.io/terrycho-sandbox/hello-node:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


hello-node-rc 라는 이름으로 rc를 생성하는데, replica 를 3으로 하여, 총 3개의 pod를 생성하도록 한다.

템플릿 부분에 컨테이너 스팩에 컨테이너 이름은 hello-node로 하고 이미지는 앞서 업로드한 gcr.io/terrycho-sandbox/hello-node:v1 를 이용해서 컨테이너를 만들도록 한다. 컨테이너의 포트는 8080을 오픈한다. 템플릿 부분에서 app 이라는 이름의 라벨을 생성하고 그 값을 hello-node로 지정하였다. 이 라벨은 나중에 서비스 (service)에 의해 외부로 서비스될 pod들을 선택하는데 사용 된다.


여기서 imagePullPolicy:Always  라고 설정한 부분이 있는데, 이는 Pod를 만들때 마다 매번 컨테이너 이미지를 확인해서 새 이미지를 사용하도록 하는 설정이다.  컨테이너 이미지는 한번 다운로드가 되면 노드(Node) 에 저장이 되어 있게 되고, 사용이 되지 않는 이미지 중에 오래된 이미지는 Kublet이 가비지 컬렉션 (Garbage collection) 정책에 따라 이미지를 삭제하게 되는데, 문제는 노드에 이미 다운되어 있는 이미지가 있을 경우 컨테이너 생성시 노드에 이미 다운로드 되어 있는 이미지를 사용한다. 컨테이너 리파지토리에 같은 이름으로 이미지를 업데이트 하거나 심지어 그 이미지를 삭제하더라도 노드에 이미지가 이미 다운로드 되어 있으면 다운로드된 이미지를 사용하기 때문에, 업데이트 부분이 반영이 안된다.

이를 방지하기 위해서 imagePullPolicy:Always로 해주면 컨테이너 생성시마다 이미지 리파지토리를 검사해서 새 이미지를 가지고 오기 때문에, 업데이트된 내용을 제대로 반영할 수 있다.


%kubectl create -f hello-node-rc.yaml


명령어를 실행해서 rc와 pod를 생성한다.




위의 그림과 같이 3개의 Pod가 생성된것을 확인할 수 있는데, Pod가 제대로 생성되었는지 확인하기 위해서 hello-node-rc-rsdzl pod에서 hello-node-rc-2phgg pod의 node.js 웹서버에 접속을 해볼 것이다.

아직 서비스를 붙이지 않았기 때문에, 이 pod들은 외부 ip를 이용해서 서비스가 불가능하기 때문에, 쿠버네티스 클러스터 내부의 pod를 이용하여 내부 ip (private ip)간에 통신을 해보기 위해서 pod에서 pod를 호출 하는 것이다. kubectl describe pod  [pod 명] 명령을 이용하면, 해당 pod의 정보를 볼 수 있다. hello-node-rc-2hpgg pod의 cluster ip (내부 ip)를 확인해보면 10.20.1.27 인것을 확인할 수 있다.


kubectl exec 명령을 이용하면 쉘 명령어를 실행할 수 있는데, 다음과 같이 hello-node-rc-rsdzl pod에서 첫번째 pod인 hello-node-rc-2phgg의 ip인 10.20.1.27의 8080 포트로 curl 을 이용해 HTTP 요청을 보내보면 다음과 같이 정상적으로 응답이 오는 것을 볼 수 있다.


Service 등록

rc와 pod 생성이 끝났으면 이제 서비스를 생성해서 pod들을 외부 ip로 서비스 해보자

다음은 서비스를 정의한 hello-node-svc.yaml 파일이다.


hello-node-svc.yaml

apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer


Selector 부분에 app:hello-node 로 지정하여, pod들 중에 라벨의 키가 app이고 값이 hello-node인 pod 들만 서비스에 연결하도록 지정하였다. 다음 서비스의 포트는 80으로 지정하였고, pod의 port는 8080으로 지정하였다.


서비스가 배포되면 위와 같은 구조가 된다.

%kubectl create -f hello-node-svc.yaml

명령을 이용하면 서비스가 생성이 된다.


다음 생성된 서비스의 외부 ip를 얻기 위해서 kubectl get svc 명령을 실행해보자

아래 그림과 같이 35.200.40.161 IP가 할당된것을 확인할 수 있다.


이 IP로 접속을 해보면 아래와 같이 정상적으로 응답이 오는 것을 확인할 수 있다.


RC 테스트

rc는 pod의 상태를 체크하다가 문제가 있으면 다시, pod를 기동해주는 기능을 한다.

이를 테스트하기 위해서 강제적으로 모든 pod를 제거해보자. kubectl delete pod --all을 이용하면 모든 pod를 제거할 수 있는데, 아래 그림을 보면, 모든 pod를 제거했더니 3개의 pod가 제거되고 새롭게 3개의 pod가 기동되는 것을 확인할 수 있다.



운영중에 탄력적으로 pod의 개수를 조정할 수 있는데, kubectl scale 명령을 이용하면 된다.

kubectl scale --replicas=[pod의 수] rc/[rc 명] 식으로 사용하면 된다. 아래는 pod의 수를 4개로 재 조정한 내용이다.



자원 정리

테스트가 끝났으면 서비스, rc,pod를 삭제해보자.

  • 서비스 삭제는 kubectl delete svc --all 명령어를 이용한다.

  • rc 삭제는 kubectl delete rc --all

  • pod 삭제는 kubectl delete pod --all

을 사용한다.

삭제시 주의할점은 pod를 삭제하기 전에 먼저 rc를 삭제해야 한다. 아니면, pod가 삭제된 후 rc에 의해서 다시 새로운 pod가 생성될 수 있다.


쿠버네티스 #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 에 대해서는 뒤에 다른 글에서 조금 더 자세하게 설명하도록 한다.


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

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




Zipkin을 이용한 MSA 환경에서 분산 트렌젝션의 추적 #2 

 Spring Sleuth를 이용한 Zipkin 연동


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



앞글에 이어서 이번에는 실제로 어플리케이션에서 분산 로그를 추적해보도록 한다.

스프링 부트 애플리케이션을 Zipkin과 연동하기 위해서는 Sleuth라는 라이브러리를 사용하면 된다.

구조

우리가 구현하고자 하는 예제의 구조는 다음과 같다.


API Client는 User 서비스를 호출하고, User 서비스는 Item 서비스를 호출하여 사용자의 Item 정보를 리턴 받아서 리턴 받은 내용을 API Client에 호출한다.

User와 Item 서비스는 모두 Spring Boot 1.5 버전으로 개발하였다. Spring 2.0은 아직 나온지가 얼마되지 않아서 Zipkin 이 지원되지 않는다.

이 예제에 대한 전체 코드는 https://github.com/bwcho75/zipkin-spring-example 에 있다.

User 서비스 코드

User 서비스 코드를 살펴보도록 하자

maven pom.xml

먼저 maven 빌드 스크립트인 pom.xml에는, zipkin 연동을 위해서 sleuth 라이브러리를 사용하기 위해서 이에 대한 의존성을 추가한다. 아래와 같이 zipkin과 sleuth 라이브러리의 버전은 1.3.2.RELEASE 버전을 사용하였다. 참고로 스프링 부트의 버전은 1.5.5.RELEASE 버전을 사용하였다.


<dependency>

   <groupId>org.springframework.cloud</groupId>

   <artifactId>spring-cloud-starter-zipkin</artifactId>

   <version>1.3.2.RELEASE</version>

</dependency>

<dependency>

   <groupId>org.springframework.cloud</groupId>

   <artifactId>spring-cloud-starter-sleuth</artifactId>

   <version>1.3.2.RELEASE</version>

</dependency>


Controller 클래스

다음은 /users URL을 처리하는  Rest Controller 부분의 코드를 살펴보자, 코드는 다음과 같다.


@RestController

@RequestMapping("/users")

public class UserController {

   @Autowired

   RestTemplate restTemplate;

   

   @Bean

   public RestTemplate getRestTemplate() {

       return new RestTemplate();

   }

   

   @Bean

   public AlwaysSampler alwaysSampler() {

       return new AlwaysSampler();

   }

private static final Logger logger = LoggerFactory.getLogger(UserController.class);

@RequestMapping(value="/{name}",method=RequestMethod.GET)

public List<User> getUsers(@PathVariable String name){

logger.info("User service "+name);

List<User> usersList = new ArrayList<User>();

List<Item> itemList = (List<Item>)restTemplate.exchange("http://localhost:8082/users/"+name+"/items"

,HttpMethod.GET,null

,new ParameterizedTypeReference<List<Item>>() {}).getBody();

usersList.add(new User(name,"myemail@mygoogle.com",itemList));

return usersList;

}


}


getUsers() 함수에서 /users/{name}으로 들어오는 요청을 받아서 RestTemplate을 이용하여 localhost:8082/users/{name}/items로 호출하는 코드이다.

여기서 중요한것이 RestTemplate 객체를 생성하는 방법은데, restTeamplte을 @AutoWrire로 하게 하고, getRestTemplate을 @Bean으로 정해줘야 한다. (아래 문서 참조 내용 참고)


https://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/1.2.1.RELEASE/#_baggage_vs_span_tags

그리고 @Bean으로 정의된 alwaysSampler()를 정의하는데, Sampler란 zipkin으로 트레이싱 하는 트렌젝션을 100%를 다할것인지 일부만 할것인지를 결정하는 것이다. 여기서는 100%를 다하도록 하였다.

100%를 샘플링하면 정확하게 트렌젝션을 추적할 수 있지만, 반대 급부로 매번 샘플링 및 로그를 서버에 전송해야하기 때문에 성능 저하를 유발할 수 있기 때문에 이 비율을 적절하게 조정할 수 있다. 비율 조정은 뒤에 설명할 설정파일에서 조정이 가능하다.

applicaiton.yml

Zipkin 서버의 URL과, 샘플링 비율등을 설정하기 위해서는 src/main/resources/application.yml에 이 설정 정보를 지정해놓는다. 아래는  application.yml 파일이다.


server:

 port: 8081

spring:

 application:

   name: zipkin-demo-server1

 zipkin:

   baseUrl: http://127.0.0.1:9411/

 sleuth:

   enabled: true

   sampler:

     probability: 1.0

sample:

 zipkin:

   enabled: true


port는 이 서비스가 listen할 TCP 포트로 8081로 listen을 하도록 하였다.

spring.zipkin에 baseUrl 부분에 zipkin 서버의 URL을 지정한다. 이 예제에서는 zipkin 서버를 localhost(127.0.0.1):9411 에 기동하였기 때문에 위와 같이 URL을 지정하였다.

다음은 sleuth 활성화를 위해서 spring.sleuth.enabled를 true로 하고 sampler에서 probability를 1.0으로 지정하였다.

Item 서비스 코드

Item 서비스 코드는 User 서비스 코드와 크게 다르지 않다. 전체 코드는 https://github.com/bwcho75/zipkin-spring-example/tree/master/zipkin-service2 를 참고하기 바란다.

Item 서비스는 8082 포트로 기동되도록 설정하였다.

테스트

서비스 개발이 끝났으면 컴파일을 한 후에 User 서비스와 Item  서비스를 기동해보자.

Zipkin 서버 구동

Zipkin 서버를 설치하는 방법은 https://zipkin.io/pages/quickstart 를 참고하면 된다. 도커 이미지를 사용하는 방법등 다양한 방법이 있지만 간단하게 자바 jar 파일을 다운 받은 후에, java -jar로 서버를 구동하는게 간편하다.

wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
java -jar zipkin.jar

이때 주의할점은 zipkin 서버를 통해서 HTTP로 Trace 로그를 받을때, 별도의 보안이나 인증 메커니즘이 없기 때문에, zipkin 서버는 반드시 방화벽 안에 놓고, 서비스 서버로부터만 HTTP 호출을 받을 수 있도록 해야 한다.

부하주기

모든 서버가 기동 되었으면 부하를 줘서 로그를 수집해보자. 부하 발생은 간단하게 apache ab 툴을 이용하였다.

%ab -n 1000 http://localhost:8081/users/terry

위의 명령어는  localhost:8081/users/terry로 HTTP GET 요청을 1000번 보내는 명령이다.

결과 확인

부하 발생이 끝난후에 http://localhost:9411 화면으로 들어가서 Find Traces 버튼을 눌러보면 다음과 같은 트레이스 화면을 볼 수 있다. 개개별 트렌젝션 결과가 나오고,


개별 트렌젝션을 눌러보면 다음과 같은 결과가 나오는 것을 볼 수 있다. 아래를 보면 /users/terry가 전체 58.944 ms가 소요되고, users/terry/items는 2 ms가 소요되는 것을 확인할 수 있다. 앞에는 서비스 명인데, 첫번째 서비스는 zipkin-demo-server1, 두번째 서버는 zipkin-demo-server2 로 출력이 된다. 이 서버명은 application.yml 파일에서 지정하면 된다.



재미있는 기능중 하나는 각 서비스의 의존성을 시각화 해주는 기능이 있는데, 화면 위쪽에 dependency 버튼을 누르면 아래 그림과 같이 로그 기반으로하여 서비스간의 호출 의존성을 보여준다.



지금까지 간략하게 Spring Sleuth와 Zipkin을 이용한 분산 로그 추적 기능을 구현해보았다.

여기서 구현한 내용은 어디까지나 튜토리얼 수준이다. Zipkin 서버의 스토리지 구성이 메모리로 되어 있기 때문에 실 운영환경에서는 적합하지 않다. 다음 글에서는 클라우드 환경을 이용하여 운영 수준의 Zipkin 서비스를 구성하는 방법에 대해서 알아보도록 하겠다.


참고 자료

https://howtodoinjava.com/spring/spring-boot/spring-boot-tutorial-with-hello-world-example/

https://howtodoinjava.com/spring/spring-cloud/spring-cloud-zipkin-sleuth-tutorial/




구글 클라우드의 대용량 메세지 큐 Pub/Sub 소개

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




구글 클라우드의 Pub/Sub 은 클라우드 기반의 대용량 메세지 큐이다.

흔히들 사용하는 RabbitMQ, JMS나 Kafka의 클라우드 버전으로 보면 된다. Rabbit MQ와 같은 설치형 큐가 작은 메세지에 대해서 세심한 컨트롤을 제공한다고 하면, Kafka나 Pub/Sub은 대용량 스케일의 메세지를 처리하기 위해서 설계 되었고, 자잘한 기능보다는 용량에 목적을 둔다.

그중에서 Pub/Sub은 클라우드 기반의 서비스로 비동기 메세징이 필요한 기능을 매니지드 서비스 형태로 제공함으로써, 별도의 설치나 운영이 필요 없이 손쉽게, 사용이 가능하다.

보통 특정 클라우드 벤더의 매지니드 솔루션은 Lock in 이슈 (한번 개발하면 다른 플랫폼으로 옮기기가 어려운)가 있어서 쉽사리 권하기는 어렵지만, 사용법이 간단하고 Lock in을 감수하고도 기능이 막강한 서비스나, 타 서비스로 전환이 쉬운 서비스일 경우에는 적극적으로 권장하는 편이다.


Pub/Sub의 경우 대용량 큐 서비스이기 때문에 Kafka 처럼 설치나 운영이 필요없음에도 불구하고 대용량 처리를 지원하면서 사용이 매우 쉽고, 코딩 양이 매우 적어서 차후에 다른 솔루션으로 교체가 용이하고 또한 대용량 장점과, 운영 대행의 장점으로 Lock in에 대한 단점을 충분히 커버하리라고 본다.

특징

주요 특징을 살펴보면, 글로벌 스케일 큐로, 전세계 어느 데이타 센터에서 접속하던지 구글 자체 광케이블망을 이용하여 빠른 접근을 제공한다.

메세지 전달 보장을 기능이 있으며, 큐에서 메세지를 PULLING 하는 기능뿐만 아니라, 큐가 메세지를 받는 쪽으로 HTTP를 이용하여 PUSH 해줄 수 있다.

토폴로지

구글 Pub/Sub 은 Message Provider (보내는쪽)과 Message Consumer (받는쪽)이 1:1 관계가 아니라. 1:N 관계이다.


Pub/Sub에는 Topic과 Subscription이라는 개념이 존재하는데,  Topic 을 큐로 생각하면 된다.

Message Provider가 Topic으로 메세지를 보내게 되고, 메세지를 읽으려면 Subscription 이라는 구독 채널을 설정해야 한다. Subscription은 하나의 Topic에 대해서 1..N개가 생성될 수 있다.

클라이언트는 각각의 Subscription에 붙어서 메세지를 받을 수 있다.

예를 들어서 하나의 메세지를 로그 시스템과 데이타 베이스 양쪽에 저장하고 싶을때는 Topic을 만든 후에, 로그 시스템용 Subscription, 데이타 베이스용 Subscription을 각각 만들어서 데이타를 읽으면 된다.


클라이언트 인터페이스

구글 Pub/Sub의 연동은 크게 다음과 같이 3가지 방법으로 접근이 가능하다.

메세지 구조와 생명 주기

Pub/Sub에 넣을 수 있는 메세지는 간단하다. String 형태의 메세지를 넣을 수 있으며, 메세지의 크기는 base64-encoding이 된 기준으로 최대 10M까지 지원이 된다.

메세지는 Message 와, Message Attribute  두가지 블럭으로 구분된다. 비교해서 이해하자면 Message 는 HTTP BODY, Message Attribute는 HTTP Header와 같은 개념으로 생각하면 되는데, Message는 통째로 TEXT가 들어가고, Message Attribute는 Key/Value 형태로 각각의 필드가 들어간다.  

생명주기 및 재처리 정책

메세지 생명 주기가 재미있는데, 먼저 Push로 받거나 Pull로 받은 메세지는 큐에서는 일단은 보이지 않는다. (다시 가지고 올 수 없다는 이야기). 메세지 처리가 끝난 후에는 클라이언트는 Pub/Sub으로 Acknowlege를 보내야 하는데, 만약에 정해진 시간 (이를 message acknowlegement deadline이라고 하고 디폴트는 10초)내에 ack를 주지 않으면, 그 메세지는 다시 Pub/Sub으로 들어간다.  이 acknowlegement를 통해서 메세지 전달 보장이 가능하다.


다시 Pub/Sub으로 돌아간 메세지는 Publishing time으로 부터 최대 7일까지 보관이 되서 클라이언트에서 다시  읽어드릴 수 있다.


https://cloud.google.com/pubsub/subscriber#ack_deadline


순서 보장

Pub/Sub 큐에 들어온 메세지는 Consumer에서 읽어드릴때, Pub/Sub에서 보낸 순서대로 읽을 수 없고, 랜덤한 순서로 전달된다. 즉 전달 순서 보장이 되지 않는다. 이는 Pub/Sub이 기본적으로 분산형 아키텍쳐를 띄고 있기 때문에, 내부에 어떤 노드로 데이타가 전달되는지, 그리고 각 노드중 어느 노드에서 데이타를 읽는지 예측이 불가능하기 때문이다.

메세지 전달 방식 (Message delivery type)

Pub/Sub은 일반적은 큐 시스템과 다르게 메세지를 Subscriber가 읽어오는 Pull 방식 이외에, Pub/Sub이 직접 Subscriber에게 메세지를 쏴주는 Push 방식을 같이 지원한다.

Pub/Sub 테스트 하기

대략적인 개념 이해가 끝났으면, 이제 실제 테스트를 해보자

Topic 생성하기

구글 클라우드 콘솔에서 Pub/Sub을 선택한 후, 아래 그림과 같이 메뉴에 들어오면, Create Topic 메뉴를 선택하여 Pub/Sub Topic을 생성한다.


여기서는 아래 그림과 같이 “mytopic”이라는 이름으로 토픽을 생성하겠다.


토픽명은 “projects/{프로젝트명}/topcis/{토픽명}” 식으로 생성된다. 이 예제에서 사용한 프로젝트명은 terrycho-sandbox이고, 지정한 토픽명은 “mytopic”이기 때문에, topic의 전체 이름은 “projects/terrycho-sandbox/topcis/mytopic”이 된다.

Subscription 생성하기

이제 앞에서 생성한 Topic으로 부터 메세지를 읽어드리기 위해서 Subscription을 생성해보자.

Pub/Sub 메뉴에서 아래와 같이 앞서 생성한 Topic을 확인할 수 있다. 이 메뉴에서 생성한 Topic의 “+New subscrition”이라는 버튼을 선택하면 새로운 Subscription을 생성할 수 있다.


아래 그림과 같이 subscription 생성화면에서 subscription 이름을  mysubscription으로 지정하자.

topic과 마찬가지로 subscription의 full name 역시 “projects/{프로젝트명}/subscriptions/{서브스크립션명}” 이 된다.


그리고 Delivery type (메세지 전달 방식)은 Pull을 선택한다.

아래 그림과 같이 Advanced option에서  Acknowlegement deadline을 설정할 수 있는데, 건들지 말고 디폴트 10초로 놔둔다.



메시지 보내보기

메세지를 보내는 테스트를 하기 위해서는 클라우드 콘솔 Pub/Sub 메뉴에서 앞에서 생성한 Topic을 선택하면 아래 그림과 같이 “Publish” 버튼이 나온다.


Publish 버튼을 누르면 아래와 같이 메세지를 직접 입력할 수 있는 창이 나온다.


위의 그림과 같이 Message 창에 보내고 싶은 메세지를 적고 Publish버튼을 누르면 Pub/Sub에 메세지가 퍼블리슁 된다.

보낸 메세지 읽어드리기

이제 퍼블리슁된 메세지를 읽어보자. 메세지는 gcloud라는 구글 클라우드 클라이언트를 이용해서 할것인데, 설치 방법은 https://cloud.google.com/sdk/gcloud/ 를 참고하면된다.

설치가 귀찮은 경우에는 아래 그림과 같이 구글 클라우드 콘솔의 상단 부분에 우측에 “>.” 이렇게 생긴 아이콘을 누르면 된다.



클라우드 쉘이라는 것인데, 구글 클라우드에 대해서 Command Line으로 명령을 내릴 수 있는 기본 명령어들이 미리 깔려있는 Linux 접속창이다.



pub/sub 은 아직 알파 버전이기 때문에, gcloud를 업그레이드 해서 alpha 버전 명령어가 수행이 가능하도록 해야 한다.

다음 명령어를 실행하자

%gcloud components install alpha

이제 gcloud 명령어가 업데이트 되었다. 이제 Pub/Sub topic에서 데이타를 읽어와보자

다음 명령어를 실행하면 mysubscrition에서 메세지를 읽어올 수 있다.

gcloud alpha pubsub subscriptions pull projects/terrycho-sandbox/subscriptions/mysubscription

다음은 명령을 실행해서 데이타를 읽어온 결과 이다.




10초 정도 후에 같은 명령어 실행해보면 같은 메세지가 리턴되는 것을 볼 수 있는데, 이는 ack를 주지 않았기 때문이다. gcloud에서 자동으로 ack를 보내는 방법은 명령어에 --auto-ack라는 옵션을 추가하면 된다.

옵션을 추가하고 명령을 실행해보자

gcloud alpha pubsub subscriptions pull projects/terrycho-sandbox/subscriptions/mysubscription --auto-ack

아래 결과와 같이, 첫번째 실행에서는 메세지가 도착하지만, 두번째 실행에서는 같은 메세지가 도착 하지 않는 것을 확인할 수 있다.


이 밖에도 gcloud 명령으로 하나의 메세지 뿐 아니라 한번에 여러개의 메세지를 리턴받을 수 도 있고, 여러개의 메세지를  pagination을 통해서 리턴 받을 수 도 있다. 자세한 옵션은 https://cloud.google.com/sdk/gcloud/reference/alpha/pubsub/subscriptions/pull 를 참고하기 바란다.


클라우드 웹 콘솔과, gcloud 명령어를 이용해서, 메세지를 퍼블리슁하고 읽어들이 것을 알아보았다. 다음 글에서는 실제로 SDK를 이용해서 메세지를 퍼블리슁하고 읽어들이는 예제를 소개하도록 하겠다.