# 16장. 스토리지 솔루션과 쿠버네티스의 연계 상태 비저장 방식으로 마이크로서비스를 구축하면 신뢰성를 확보하고 관리가 용이하다.
하지만 서비스 운영 시 어느 시점에는 어딘가에 저장된 데이터가 필요하다.
컨테이너 오케스트레이션 솔루션 상의 애플리케이션은 불변하고 선언형인 상태로 관리될 필요가 있다.
기존 애플리케이션은 데이터 중력(data gravity)로 인해 컨테이너로 운영되더라도
격리된 상태로 구축되지 않고 기존 VM과 데이터를 공유할 수 있어서 분리가 필요한 복잡성이 요구된다. 15장에서는 쿠버네티스 환경에서 스토리지를 컨테이너형 마이크로서비스로
연계하기 위한 다양한 접근 방법을 살펴본다. ## 외부 서비스 가져오기 데이터베이스가 실행되고 있는 기존 시스템과 통합하는 방법이다.
레거시 서버와 서비스를 쿠버네티스에 포함시킬 경우
쿠버네티스에서 제공하는 모든 내장 네이밍 및 서비스 탐색의 기본 기능 같은 이점을 활용할 수 있다. ```yaml kind: Service metadata: name: my-database # note 'test' namespace here namespace: test ``` 위와 같은 서비스는 아래와 같은 포인터를 가진다. - my-database.test.svc.cluster.internal 네임스페이스를 prod로 설정할 경우 아래와 같은 포인터를 가진다. - my-database.prod.svc.cluster.internal ### 셀렉터가 없는 서비스 데이터베이스가 실행 중인 특정 서버를 가리키는 DNS를 서비스와 연결할 수 있다. *예제 15-1. dns-service.yaml* ```yaml kind: Service apiVersion: v1 metadata: name: external-database spec: type: ExternalName externalName: database.company.com ``` ExternalName 타입의 서비스를 생성하면 사용자가 지정한 외부 서비스를 가리키는 CNAME 레코드를 쿠버네티스 DNS 서비스에 등록한다. 클러스터 내에서 호스트 이름 external-database.svc.default.cluster의 DNS를 조회하면 DNS 프로토콜은 database.company.com의 이름으로 별칭을 부여한다.
그후 이 이름을 외부 데이터베이스 서버의 IP 주소로 해석한다. IP 주소로 외부 서비스를 쿠버네티스로 가져오는 것도 가능하다.
이 경우 서비스와 엔드포인트 객체를 직접 등록해주어야한다.
또한 서버의 IP 주소의 최산화 책임을 가지게 되기에 고정 IP 사용 또는 자동으로 엔드포인트를 업데이트해주는 절차를 마련해야한다. *예제 15-2. external-ip-service.yaml* ```yaml kind: Service apiVersion: v1 metadata: name: external-ip-database ``` *예제 15-3. external-ip-endpoints.yaml* ```yaml kind: Endpoints apiVersion: v1 metadata: name: external-ip-database subsets: - addresses: - ip: 192.168.0.1 ports: - port: 3306 ``` ### 외부 서비스의 제약 사항: 상태 검사 외부 서비스는 health check를 수행하지 않아서 별도로 신뢰성을 확인해야한다. ## 신뢰할 수 있는 싱글톤 실행 쿠버네티스 기본 객체를 활용하되 복제본을 생성하지 않으면 관리가 간단하다. 신뢰할 수 있는 분산 시스템 구축 원칙에 반하는 것으로 보일 수 있지만
기존 시스템이 단일 가상머신이나 단일 물리머신인 경우가 많고
소규모 애플리케이션의 경우 제한된 다운타임은 복잡성을 줄일 수 있는 합리적인 트레이드오프 관계로 볼 수 있다. 다음절에서 설명하는 것과 같이 레플리카셋을 사용하면 싱글톤이어도 장애를 어느 정도 대비할 수 있다. ### MySQL 싱글톤 실행 MySQL을 쿠버네티스 내에서 싱글톤으로 실행하기 위해 다음과 같은 세 가지 기본 객체를 생성한다. - MySQL 애플리케이션과 독립적인 영구 볼륨 - MySQL 애플리케이션 파드 - MySQL 파드를 노출하기 위한 서비스 *예제 15-4. nfs-volume.yaml* ```yaml apiVersion: v1 kind: PersistentVolume metadata: name: database labels: volume: my-volume spec: accessModes: - ReadWriteMany capacity: storage: 1Gi nfs: # nfs server: 192.168.0.1 path: "/exports" ``` 아래와 같이 영구 볼륨을 생성 가능하다. ```sh kubectl apply -f nfs-volume.yaml ``` ```{note} [Network File System(NFS)](https://en.wikipedia.org/wiki/Network_File_System)란 1984년 Sun Microsystems에서 개발한 분산 파일 시스템 프로토콜로 클라이언트 컴퓨터의 사용자가 로컬 저장소에 액세스하는 것과 매우 유사하게 컴퓨터 네트워크를 통해 파일에 액세스할 수 있도록 한다. ``` *예제 15-5. nfs-volume-claim.yaml* ```yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: database spec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi selector: matchLabels: v olume: my-volume ``` 위와 같이 PVC를 통해 영구 볼륨 사용 요청을 할 수 있다.
굳이 PV가 아닌 PVC를 통해 영구 볼륨을 연결하는 이유는 파드에 대한 정의를 스토리지의 정의에서 분리하기 위함이다(일종의 프록시 느낌). *예제 15-6. mysql-replicaset.yaml* ```yaml apiVersion: extensions/v1 kind: ReplicaSet metadata: name: mysql # labels so that we can bind a Service to this Pod labels: app: mysql spec: replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: database image: mysql resources: requests: cpu: 1 memory: 2Gi env: # 보다 나은 보안은 11장 참고 - name: MYSQL_ROOT_PASSWORD value: some-password-here livenessProbe: tcpSocket: port: 3306 ports: - containerPort: 3306 volumeMounts: - name: database # /var/lib/mysql가 MySQL이 데이터를 저장하는 장소 mountPath: "/var/lib/mysql" volumes: - name: database persistentVolumeClaim: claimName: database ``` 레플리카셋을 사용해서 파드가 다운될 경우 정상 노드에 스케쥴링을 보장할 수 있다.
단일 머신 장애 등을 방지하기 위함이다. *예제 15-7. mysql-service.yaml* ```yaml apiVersion: v1 kind: Service metadata: name: mysql spec: ports: - port: 3306 protocol: TCP selector: app: mysql ``` ### 동적 볼륨 프로비저닝 StorageClass 객체를 통해 동적 볼륨 프로비저닝을 사용할 수 있다. 클러스터는 여러 개의 다른 스토리지 클래스가 설치될 수 있다.
NFS 서버용과 iSCSI 블록 저장소용 등 스토리지 클래스가 함께 존재할 수 있다. *예제 15-8. storageclass.yaml* ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: default annotations: storageclass.beta.kubernetes.io/is-default-class: "true" labels: kubernetes.io/cluster-service: "true" provisioner: kubernetes.io/azure-disk ``` *예제 15-9. dynamic-volume-claim.yaml* ```yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: my-claim annotations: volume.beta.kubernetes.io/storage-class: default # default 스토리지 클래스 연결 spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi ``` ## 스테이트풀셋을 통한 쿠버네티스 네이티브 스토리지 상태 비저장 애플리케이션이 비식별성을 가지는 것과 달리
상태 저장 애플리케이션은 식별성을 필요로 한다. 쿠버네티스 1.5에서 스테이트풀셋이 소개됐다. ### 스테이트풀셋 속성 스테이트풀셋은 레플리카셋과 유사하게 복제된 파드의 그룹으로 구성된다.
하지만 아래와 같은 차이점이 있다. - 각 복제본은 고유한 인덱스와 함께 영구 호스트 이름을 갖는다. (예. database-0, database-1 등) - 각 복제본은 인덱스 번호가 가장 낮은 순서부터 높은 순서로 생성되고, 이전 인덱스 번호의 파드가 안정적이고 이용 가능한 상태가 될 때까지 복제본은 생성되지 않는다. 이러한 특성은 확장(scale up) 시에도 동일하게 적용된다. - 스테이트풀셋이 삭제될 때 각각의 관리되는 복제본 파드는 가장 높은 번호에서 낮은 순으로 삭제된다. 이는 축소(scale down)하는 데도 적용된다. ### 스테이트풀셋을 통한 몽고DB 수동 복제 *예제 15-10. mongo-simple.yaml* ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: mongo spec: serviceName: "mongo" replicas: 3 template: metadata: labels: app: mongo spec: containers: - name: mongodb image: mongo:3.4.1 command: - mongod - --replSet - rs0 # 복제본 세트 이름 지정 ports: - containerPort: 27017 name: peer ``` *예제 15-11. mongo-service.yaml* ```yaml apiVersion: v1 kind: Service metadata: name: mongo spec: ports: - port: 27017 name: peer clusterIP: None selector: app: mongo ``` 아래와 같이 DNS 항목을 확인할 수 있다. ```console $ kubectl run -it --rm --image busybox busybox ping mongo-1.mongo ``` 아래 처럼 첫번째 파드에 접속해 replicaset을 수동 설정할 수 있다. ```console $ kubectl exec -it mongo-0 mongo > rs.initiate( { _id: "rs0", members:[ { _id: 0, host: "mongo-0.mongo:27017" } ] }); OK > rs.add("mongo-1.mongo:27017"); > rs.add("mongo-2.mongo:27017"); ``` ### 몽고DB 클러스터 생성 자동화 컨피그맵 객체에 스크립트를 저장하여 몽고DB 클러스터 생성 자동화에 서용할 수 있다. ```yaml ... - name: init-mongo image: mongo:3.4.1 command: - bash - /config/init.sh volumeMounts: - name: config mountPath: /config volumes: - name: config configMap: name: "mongo-init" ``` *Example 15-12. mongo-configmap.yaml* ```yaml apiVersion: v1 kind: ConfigMap metadata: name: mongo-init data: init.sh: | #!/bin/bash # Need to wait for the readiness health check to pass so that the # mongo names resolve. This is kind of wonky. until ping -c 1 ${HOSTNAME}.mongo; do echo "waiting for DNS (${HOSTNAME}.mongo)..." sleep 2 done until /usr/bin/mongo --eval 'printjson(db.serverStatus())'; do echo "connecting to local mongo..." sleep 2 done echo "connected to local." HOST=mongo-0.mongo:27017 until /usr/bin/mongo --host=${HOST} --eval 'printjson(db.serverStatus())'; do echo "connecting to remote mongo..." sleep 2 done echo "connected to remote." if [[ "${HOSTNAME}" != 'mongo-0' ]]; then until /usr/bin/mongo --host=${HOST} --eval="printjson(rs.status())" \ | grep -v "no replset config has been received"; do echo "waiting for replication set initialization" sleep 2 done echo "adding self to mongo-0" /usr/bin/mongo --host=${HOST} \ --eval="printjson(rs.add('${HOSTNAME}.mongo'))" fi if [[ "${HOSTNAME}" == 'mongo-0' ]]; then echo "initializing replica set" /usr/bin/mongo --eval="printjson(rs.initiate(\ {'_id': 'rs0', 'members': [{'_id': 0, \ 'host': 'mongo-0.mongo:27017'}]}))" fi echo "initialized" while true; do sleep 3600 done ``` *Example 15-13. mongo.yaml* ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: mongo spec: serviceName: "mongo" replicas: 3 template: metadata: labels: app: mongo spec: containers: - name: mongodb image: mongo:3.4.1 command: - mongod - --replSet - rs0 ports: - containerPort: 27017 name: web # This container initializes the mongodb server, then sleeps. - name: init-mongo image: mongo:3.4.1 command: - bash - /config/init.sh volumeMounts: - name: config mountPath: /config volumes: - name: config configMap: name: "mongo-init" ``` ### 영구 볼륨과 스테이트풀셋 ```yaml ... volumeMounts: - name: database mountPath: /data/db ``` ```yaml volumeClaimTemplates: - metadata: name: database annotations: volume.alpha.kubernetes.io/storage-class: anything spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Gi ``` ### 마지막 단계: 준비 프로브 ```yaml ... livenessProbe: exec: command: - /usr/bin/mongo - --eval - db.serverStatus() initialDelaySeconds: 10 timeoutSeconds: 10 ```