원래는 react와 node js, mysql을 이용한 웹 어플리케이션을 돌리기로 마음을 먹었었지만,
1. 가상머신의 고정 용량으로 설정한 하드디스크의 용량 부족..
2. 메인 컴퓨터의 docker 오류와 docker image build가 안됨
이러이러한 이유들로 kubernetes에서 제공해주는 image들을 사용하기로 마음먹고
공식문서에 있는 Redis와 PHP로 방명록 애플리케이션을 만드는 것으로
쿠버네티스의 고가용성을 테스트하기로 마음먹었습니다.
전체적인 틀
1. 방명록을 저장하는 단일 인스턴스 Redis 마스터 (pod 한 개)
2. 읽기를 제공하는 여러 개의 복제된 Redis 인스턴스 (pod 두 개)
3. 여러 개의 웹 프론트엔드 인스턴스 (pod 세 개)
까지가 공식문서에 나와있는 것입니다.
저는 웹 프론트엔드 인스턴스의 pod를 네 개로 늘리고 각각의 pod는 다른 node에서 실행이 되고,
Redis 마스터와 Redis 인스턴스가 실행되고 있는 노드에서는 실행이 안되도록 하겠습니다.
또한 Redis가 저장되고 읽는 저장소는 하나의 저장소를 사용할 것 입니다.
저는 lustre를 이용한 대용량 저장소가 구축이 되어있기 때문에 lustre를 mount해서 그 저장소를 사용할 것 입니다.
방명록을 저장하는 Redis 마스터 생성하고 배포하기
redis-master-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: redis-master
labels:
app: redis
spec:
selector:
matchLabels:
app: redis
role: master
tier: backend
replicas: 1
template:
metadata:
labels:
app: redis
role: master
tier: backend
spec:
containers:
- name: master
image: k8s.gcr.io/redis:e2e # or just image: redis
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
Redis Master의 Deployment입니다.
name: redis-master
이름은 redis-master
replicas: 1
복제본은 없이 하나의 파드만 생성하고
resources:
requests:
cpu: 100m
memory: 100Mi
cpu와 memory의 요구량을 적어놓았습니다.
image: k8s.gcr.io/redis:e2e # or just image: redis
쿠버네티스에서 제공하는 redis 이미지를 사용합니다.
ports:
- containerPort: 6379
컨테이너의 port는 6379 포트를 사용합니다.
redis-master-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-master
labels:
app: redis
role: master
tier: backend
spec:
ports:
- port: 6379
targetPort: 6379
selector:
app: redis
role: master
tier: backend
우리가 만들고자 하는 방명록 애플리케이션에서 데이터를 쓰려면 Redis 마스터와 통신을 해야합니다.
Redis 마스터 파드로 트래픽을 프록시하기 위해 서비스 오브젝트를 생성합니다.
ports:
- port: 6379
targetPort: 6379
이 서비스가 노출되는 포트는 6379
그리고 서비스로 들어온 트래픽을 targetPort인 6379번 포트로 리디렉션해줍니다.
targetPort의 6379번 포트는 pod의 컨테이너 포트를 의미합니다.
selector:
app: redis
role: master
tier: backend
그리고 pod selector가 명시되어 있습니다.
app: redis
role: master
tier: backend
이 서비스는 위의 레이블을 가지는 파드로 트래픽을 라우팅합니다.
읽기를 제공하는 여러 개의 복제된 Redis 인스턴스 생성하고 배포하기
Redis 마스터는 단일 파드이지만, 복제된 Redis 슬레이브를 추가해서 트래픽 요구 사항을 충족시킬 수 있습니다.
redis-slave-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: redis-slave
labels:
app: redis
spec:
selector:
matchLabels:
app: redis
role: slave
tier: backend
replicas: 2
template:
metadata:
labels:
app: redis
role: slave
tier: backend
spec:
containers:
- name: slave
image: gcr.io/google_samples/gb-redisslave:v3
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
# Using `GET_HOSTS_FROM=dns` requires your cluster to
# provide a dns service. As of Kubernetes 1.3, DNS is a built-in
# service launched automatically. However, if the cluster you are using
# does not have a built-in DNS service, you can instead
# access an environment variable to find the master
# service's host. To do so, comment out the 'value: dns' line above, and
# uncomment the line below:
# value: env
ports:
- containerPort: 6379
name: redis-slave
redis-slave의 이름을 갖는 deployment를 생성합니다.
spec:
selector:
matchLabels:
app: redis
role: slave
tier: backend
pod 셀렉터를 명시해줍니다.
app: redis
role: slave
tier: backend
의 레이블을 가지는 pod를 생성하겠다는 의미입니다.
replicas: 2
복제본은 두 개입니다.
template:
metadata:
labels:
app: redis
role: slave
tier: backend
파드 템플릿에서의 레이블은 위에서 정의해준 selector의 레이블과
동일하게 진행이 됩니다. (그래야 deployment가 이 pod 템플릿을 보고 pod를 생성)
image: gcr.io/google_samples/gb-redisslave:v3
kubernetes에서 제공해준 이미지를 사용합니다.
env:
- name: GET_HOSTS_FROM
value: dns
환경변수로 GET_HOSTS_FROM 이라는 변수를 사용하는데
dns를 사용하겠다는 의미입니다. kubernetes1.3버전부터는
dns가 자동적으로 설치가 되어있다고 합니다.
ports:
- containerPort: 6379
컨테이너가 노출되는 포트는 6379번 포트입니다.
redis-slave-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-slave
labels:
app: redis
role: slave
tier: backend
spec:
ports:
- port: 6379
selector:
app: redis
role: slave
tier: backend
우리가 만들고자하는 방명록 애플리케이션은 Redis 슬레이브와 통신하여서 데이터를 읽습니다.
그러면 이 파드에 접근할 수 있도록 서비스를 설정해주어야 합니다.
ports:
- port: 6379
이 서비스 또한 6379번 포트로 노출시켜줍니다.
selector:
app: redis
role: slave
tier: backend
그리고 위의 레이블과 동일한 파드로 트래픽을 라우터시켜 줍니다.
지금까지 적용한 Redis 마스터와 슬레이브를 확인합니다.
방명록 프론트엔드를 설정하고 노출하기
방명록 애플리케이션에는 PHP로 작성된 HTTP 요청을 처리하는 웹 프론트엔드가 있습니다.
쓰기 요청을 위한 redis-master 서비스와 읽기 요청을 위한 redis-slave 서비스에 연결하도록 설정합니다.
frontend-deployment.yaml 파일을 통해 프론트엔드의 디플로이먼트에 적용합니다.
frontend-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: frontend
labels:
app: guestbook
spec:
selector:
matchLabels:
app: guestbook
tier: frontend
replicas: 3
template:
metadata:
labels:
app: guestbook
tier: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google-samples/gb-frontend:v4
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
# Using `GET_HOSTS_FROM=dns` requires your cluster to
# provide a dns service. As of Kubernetes 1.3, DNS is a built-in
# service launched automatically. However, if the cluster you are using
# does not have a built-in DNS service, you can instead
# access an environment variable to find the master
# service's host. To do so, comment out the 'value: dns' line above, and
# uncomment the line below:
# value: env
ports:
- containerPort: 80
labels:
app: guestbook
tier: frontend
이 디플로이먼트의 파드 셀렉터는 위의 레이블을 사용하고 있는 pod를 선택합니다.
image: gcr.io/google-samples/gb-frontend:v4
php 이미지 또한 kubernetes에서 제공하는 image를 사용하였습니다.
kubectl get pods -o wide
생성중인것을 확인할 수 있습니다.
정상 실행 됐을 때의 모습입니다. 현재는 node selector가 없고,
스케줄링 방식에 대해서 아무것도 정의해주지 않았기 때문에 아무 worker node에 배치가 되는 것을 알 수 있습니다.
frontend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
app: guestbook
tier: frontend
spec:
# comment or delete the following line if you want to use a LoadBalancer
type: NodePort
# if your cluster supports it, uncomment the following to automatically create
# an external load-balanced IP for the frontend service.
# type: LoadBalancer
ports:
- port: 80
selector:
app: guestbook
tier: frontend
위에서 작성한 redis-master 와 redis-slave 서비스는 서비스 타입이 ClusterIP(default입니다.)이기 때문에
클러스터 내에서만 접근이 가능합니다. (그래서 clusterIP라는 이름입니다.)
게스트가 방명록을 작성하려면 외부에서 볼 수 있도록 프론트엔드 서비스를 구성해서
클라이언트가 컨테이너 클러스터 외부에서 서비스를 요청할 수 있도록 해야합니다.
외부에서 노출하는 서비스의 종류는 여러가지가 있는데
클라우드 공급자가 로드 밸런서를 지원해서 클라우드 공급자를 사용한다면, LoadBalancer type을
저희는 NodePort를 사용해서 서비스를 외부에 노출시켜보도록 하겠습니다.
type: NodePort
서비스의 타입은 NodePort 입니다.
ports:
- port: 80
이 서비스는 80번 포트로 노출시켜줍니다.
selector:
app: guestbook
tier: frontend
이 서비스는 위와 같은 레이블을 가진
파드들로 트래픽을 라우팅해줍니다.
공식 홈페이지 결과물
NodePort는 30398번으로 모든 노드에 같은 포트를 사용하기 때문에
192.168.100.61~65:30398의 주소로 접속할 수 있습니다.
전체적인 조직도를 간단히 그려보았습니다.
1. 사용자는 외부에 노출된 프론트엔드 서비스의 NodePort를 통해서 들어옵니다.
2. 프론트엔드 서비스는 각각의 pod에 대해서 임의의 pod로 트래픽을 할당해주고
3. 프론트엔드 컨테이너에서 읽기 또는 쓰기에 따라 각각의 서비스에 트래픽을 받고
4. 각각의 서비스는 pod로 트래픽을 할당해줍니다.
요기까지는 공식 홈페이지에서 따라한 그대로 입니다.
아래부터는 제가 개인적으로 확장하고 추가한 방명록 애플리케이션을 만들어 보도록 하겠습니다.
방명록 애플리케이션 확장 및 변경하기
구상
1. Frontend 파드 하나 더 늘리기
우선 제가 사용하는 worker node가 7개 이므로 각각의 노드에
하나의 pod가 실행되도록 하기위해서 프론트엔드 파드를 하나 더 늘려 총 4개로 진행을 할 것입니다.
2. 스케줄링 방식 정의하기
각각의 노드에 하나의 파드만 실행되도록 할 것입니다.
어피니티를 사용하여 노드와 파드에 대한 의존성을 정의합니다.
3. slave와 master의 저장소는 하나의 저장소를 공유
PV와 PVC를 생성해서 PV가 Lustre로 구축해놓은 저장소를 공유하도록 합니다.
그렇게 완성된 구상도는 이렇게 될 것입니다.
Frontend replicas 늘리기
frontend-deployment.yaml 파일에서 replicas의 개수를 4개로 늘려주었습니다.
kubectl apply -f frontend-deployment.yaml
명령어로 디플로이먼트를 적용시켜줍니다.
정상적으로 돌아가는 것을 볼 수 있습니다!
스케줄링 방식 정의하기
모든 파드가 각각의 노드에서 하나씩만 동작하도록 하려면 어피니티를 사용해야 합니다.
그리고 지금은 노드에 대한 의존성을 표현하는것이 아닌
파드에 영향을 받아서 노드에 스케줄링이 되는 것이기 때문에
파드 어피니티 & 파드 안티어피니티를 사용해야합니다.
frontend-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: frontend
labels:
app: guestbook
spec:
selector:
matchLabels:
app: guestbook
tier: frontend
replicas: 4
template:
metadata:
labels:
app: guestbook
tier: frontend
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- frontend
topologyKey: "kubernetes.io/hostname"
containers:
- name: php-redis
image: gcr.io/google-samples/gb-frontend:v4
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
fronted deployment에 우선 파드 안티 어피니티를 적용해주었습니다.
kubernetes에 모든 노드들은 기본적으로 kubernetes.io/hostname의 토폴로지키와 값을 가지고 있기 때문에
kubernetes.io/hostname (쿠버네티스에서 자동으로 미리 저장하는 label로 Node의 이름을 정의하는 label 이다.)
로 되어 있기 때문에, node의 kubernetes.io/hostname label 값이 이 Node와 다른 Node를 배포 타겟으로 설정한다.
출처: https://bcho.tistory.com/1346
app : frontend 의 레이블을 가진 파드를 가진 노드에는 배치를 안한다는 얘기입니다.
다시 디플로이먼트를 적용시키면 node에 겹쳐서 생성되지 않습니다.
그렇지만 redis 파드와는 겹치는 것을 볼 수 있습니다.
frontend 디플로이먼트에 redis에 대한 내용도 넣어주어야 합니다.
app: redis의 레이블을 가지는 pod가 배치된 node에는 배치를 안한다는 얘기입니다.
다시 적용을 시키면 redis가 있는 node를 피하는것을 볼 수 있습니다.
지금은 redis가 노드에 겹치지않게 잘 배치가 되어있지만
이것은 우연입니다.
redis에도 파드 안티어피니티를 적용해줍시다.
frontend에 적용해준 어피니티를 그대로 똑같이 적용시켜주면 됩니다.
(redis 파드 또한 frontend와 redis가 있는 node에는 스케줄이 안되도록)
최종적으로 모든 노드에 각각하나의 파드가 배치되도록 하였습니다.
이렇게 한 이유는 한 노드에 너무 많은 서비스를 배치하면 부하가 걸릴 가능성을 생각하였습니다.
제 개인적인 프로젝트이지만 그런 가정을 하고 실습을 하면서 직접 어피니티를 적용해보고 싶었습니다.
PV, PVC 생성하고 Lustre 저장소에 바인드하기
이제 PV와 PVC를 먼저 생성하고
Redis의 디플로이먼트에 PVC를 요청해서 바인드를 시켜주면 됩니다.
redis-volumes.yaml
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: redis-data-pv-volume
labels:
app: redis
tier: storage
spec:
storageClassName: manual
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/mnt/Lustre/redis-data"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: redis-data-pv-claim
labels:
app: redis
tier: storage-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
PV
PV는 물리적인 공간을 bind해줘야 하기 때문에
러스터와 마운트되어 있는 /mnt/Lustre 라는 폴더에
/redis-data 라는 폴더를 생성해서 거기에 저장을하고 읽을 수 있도록 했습니다.
방명록 데이터라서 10G만 넉넉하게 잡아서 할당해주었습니다.
그리고 storageClassName이 동일해야 서로 binding이 됩니다.
PVC
PVC는 파드가 요청을하면 PVC가 PV로 바인드를 해서 연결이 되는 형식입니다.
스토리지클래스와 할당량은 똑같이 해주었습니다.
kubectl apply -f redis-volumes.yaml
명령어로 PV, PVC를 생성해줍니다.
kubectl get pv,pvc
STATUS가 서로 Bound가 되었으면 정상적으로 연결이 된겁니다.
그럼 이제 파드에서 요청만하면 정상적으로 저장소가 연결이 되는겁니다.
redis-master-deployment.yaml 와 redis-slave-deployment.yaml에
동일하게 적용시켜줍니다.
디플로이먼트에서는 PVC를 요청합니다.
아까 PVC 이름을 redis-data-pv-claim 으로 적어주었기 때문에
PVC를 요청하면 컨테이너 안에서 /redis-data 폴더에
PVC와 바운드 되어있는 PV가 마운트해놓은 폴더와 연결이 되는 것입니다.
두 YAML파일을 다시 적용시키면 저장소가 정상적으로 마운트가 된 것을 볼 수 있습니다.
kubectl describe pod <pod-name>
방명록에 저장되는 데이터를 저기 저장시키고 싶었지만..
redis master와 slave의 코드를 바꿔야합니다.
그래서 파일을 한번 임의로 생성해서 잘 공유하고 있는지 테스트 해보았습니다.
현재 redis는 node 1,2,3에서 구동중이기 때문에 node1,2,3로 직접들어가서 컨테이너 안으로 들어갔습니다.
그리고 파일을 간단히 만들어본다음 잘 bind가 되었는지 확인해보겠습니다.
ssh로 node2에 접속을 했습니다. (redis-master가 구동중)
그리고 구동중인 컨테이너를 확인합니다.
redis가 정상적으로 구동되고있는 것을 볼 수 있습니다.
그럼 컨테이너 쉘을 직접 실행하기 위해서 명령어를 입력해줍니다.
#docker exec -it <컨테이너 id> /bin/bash
docker exec -it 80ef85d4e4fe /bin/bash
그리고 저희가 마운트해준 컨테이너안의 폴더는 /redis-data 이므로
cd /redis-data로 들어가줍니다.
(컨테이너 안에 /redis-data 라는 폴더가 없어도 자동으로 생성해줍니다.)
그리고 a, b, c라는 임의의 폴더를 생성해줍니다.
그리고 같은 PV를 바라보고있는 노드1 에도 컨테이너의 쉘을 실행해서
생성해 준 a, b, c 파일이 있는지 확인해봤더니,
잘 공유하고 있는것을 볼 수 있습니다.
이렇게 하나의 저장소를 공유하고
이 저장소는 외부에 계속 저장되고있는 클라우드같은 느낌이라서
pod가 종료되어도 이 데이터는 끝까지 남아있을 것 입니다.
그리고 다른 pod에서 사용을 하고 싶다면
PVC를 통해서 PV에 요청만 보내면 됩니다.
지금까지 우리가 목적한
이 그림의 구조를 완성했습니다.
Frontend를 React나 Vue.js 같은 프레임워크를 사용하고
웹 서버를 다른 Nginx 혹은 Node.js 같은 프레임워크,
데이터베이스를 MySQL같은 다른 저장소를 사용해도
똑같은 방식으로 진행이 될 것입니다.
참고
https://kubernetes.io/ko/docs/tutorials/stateless-application/guestbook/
'Kubernetes & Docker' 카테고리의 다른 글
[Kubernetes]쿠버네티스 컨트리뷰터 되기 : 쿠버네티스 문서에 기여하기 (0) | 2020.08.19 |
---|---|
[Kubernetes 공식문서 파헤치기] 고가용성 테스트(pod 부하분산, auto scaler 적용하기) (2) | 2020.08.13 |
[O'REILLY] 쿠버네티스 패턴 (Kuerbernetes Patterns) (0) | 2020.08.10 |
Kubespray와 Ansible을 이용한 Kubernetes 설치하기 - 4편 (2) | 2020.08.05 |
Kubespray와 Ansible을 이용한 Kubernetes 설치하기 - 3편 (0) | 2020.07.31 |