도커/쿠버네티스 컨테이너 개발 실전 입문 · · 7 min read

CH06. 쿠버네티스 클러스터 구축

1. Google Kubernetes Engine 환경 설정

  • 4장에서 구축한 TODO 어플리케이션을 쿠버네티스로 전환하고 GKE를 사용해서 배포할 것이다.

  • GCP 명령행 도구인 구글 클라우드 SDK를 설치하고 마지막으로 GKE에 새로운 쿠버네티스 클러스터를 구축할 것이다.

  • GCP 프로젝트 생성

    • 새로운 프로젝트 생성
      • ex ) joon-kuber
    • GCP를 명령행 도구로 사용하기 위해서 프로젝트 ID를 사용해야 한다.
  • 구글 클라우드 SDK(gcloud) 설치(ubuntu18.04에 설치함)

  • kubectl 설치(ubuntu18.04에 설치함)

    • curl로 최신 릴리즈 다운로드
      $ curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
      
    • curl로 특정 릴리즈 다운로드 -> curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt을 바꾼다.
      $ curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubectl
      
    • kubectl 바이너리를 실행 가능하게 만든다.
      $ chmod +x ./kubectl
      
    • 바이너리를 PATH가 설정된 디렉터리로 옮긴다.
      $ sudo mv ./kubectl /usr/local/bin/kubectl
      
    • 설치한 버전을 확인한다.
      $ kubectl version --client
      
  • gcloud 컴포넌트 업데이트

    $ gcloud components update
    
  • 인증 설정 및 project id 설정, 리전 설정

    $ gcloud auth login
    $ gcloud config set project [프로젝트 이름]
    $ gcloud config set compute/zone asia-northeast3-a
    
  • 클러스터 생성

    $ gcloud container clusters create [클러스터 이름] --cluster-version=[클러스터 버전] --machine-type=[인스턴스 타입] --num-nodes=[노드 수]
    
  • kubectl로 클러스터를 제어할 수 있도록 kubectl에 인증정보를 설정한다.

    $ gcloud container clusters get-credentials [클러스터 이름]
    $ kubectl get nodes
    
  • 프로젝트 이름 : joon

  • 환경 : ubuntu 18.04

  • kubectl 설치

    • 버전 : Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.3", GitCommit:"2e7996e3e2712684bc73f0dec0200d64eec7fe40", GitTreeState:"clean", BuildDate:"2020-05-20T12:52:00Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}

2. GKE에 TODO 어플리케이션 구축

  • 4장에서의 TODO 어플리케이션을 GKE에 구축한다.
  • [아키텍처]

3. GKE에 MySQL 마스터-슬레이브 구성으로 구축

  • 퍼시스턴트볼륨과 퍼시스턴트볼륨클레임

    • 퍼시스턴트볼륨 : 스토리지 자체, GCP의 GCEPersistentDisk
    • 퍼시스턴트볼륨클레임 : 추상화된 논리 리소스로, 퍼시스턴트볼륨과 달리 용량을 필요한 만큼 동적으로 확보할 수 있다.
    • 퍼시스턴트볼륨클레임의 매니페스트 파일
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
          name: pvc-example
      spec:
          accessModes:
              -   ReadWriteOnce
          storageClassName: ssd
          resources:
              requests:
                  storage: 4Gi
      
      • accessModes : 파드가 스토리지에 접근하는 방식
        • ReadWriteOnce : 마운트될 수 있는 노드를 하나로 제한한다.
      • storageClassName : StorageClass 리소스의 종류(어떤 스토리지를 사용할 것인지 정의)
      • 파드는 퍼시스턴트볼륨클레임을 직접 마운트할 수 있으며, 데이터를 볼륨에 넣고 나면 파드를 정지하거나 다시 생성해도 어플리케이션의 상태가 유지된다.
  • StorageClass

    • 퍼시스턴스볼륨으로 확보한 스토리지의 종류를 정의하는 리소스
    • GCP 스토리지 종류 : 표준, SSD
    • storage-class-ssd.yaml 작성
      kind: StorageClass
      apiVersion: storage.k8s.io/v1
      metadata:
          name: ssd
          annotations:
              storageclass.kubernetes.io/is-default-class: "false"
          labels:
              kubernetes.io/cluster-service: "true"
      provisioner: kubernetes.io/gce-pd
      parameters:
          type: pd-ssd
      
      • 스토리지클래스의 name을 ssd로 지정
      • provisioner는 GCP의 퍼시스턴트 스토리지인 GCEPersistentDisk에 해당하는 gcd-pd로 지정
      • 파라미터의 type 속성을 pd-ssd로 지정
    • 스토리지 클래스 생성
      $ kubectl apply -f storage-class-ssd.yaml
      
  • 스테이트풀세트(StatefulSet)

    • 디플로이먼트는 함께 포함된 파드 정의를 따라 파드를 생성하는 리소스로, 하나만 있으면 되는 파드 혹은 퍼시스턴스 데이터를 갖지 않는 상태가 없는(stateless) 어플리케이션을 배포하는데 적합하다.
    • 스테이트풀세트는 데이터 스토어처럼 데이터를 계속 유지하는, 상태가 있는(stateful) 어플리케이션을 관리하는 데 적합한 리소스다.
    • 마스터용 설정 :: mysql-master.yaml
      apiVersion: v1
      kind: Service
      metadata:
          name: mysql-master
          labels:
              app: mysql-master
      spec:
          ports:
              -   port: 3306
                  name: mysql
          clusterIP: None
          selector:
              app: mysql-master
      
      ---
      apiVersion: apps/v1
      kind: StatefulSet
      metadata:
          name: mysql-master
          labels:
              app: mysql-master
      spec:
          serviceName: "mysql-master"
          selector:
              matchLabels:
                  app: mysql-master
          replicas: 1
          template:
              metadata:
                  labels:
                      app: mysql-master
              spec:
                  terminationGracePeriodSeconds: 60
                  containers:
                      -   name: mysql
                          image: gihyodocker/tododb:latest
                          imagePullPolicy: Always
                          args:
                              -   "--ignore-db-dir=lost+found"
                          ports:
                              -   containerPort: 3306
                          env:
                              -   name: MYSQL_ROOT_PASSWORD
                                  value: "gihyo"
                              -   name: MYSQL_DATABASE
                                  value: "tododb"
                              -   name: MYSQL_USER
                                  value: "gihyo"
                              -   name: MYSQL_PASSWORD
                                  value: "gihyo"
                              -   name: MYSQL_MASTER
                                  value: "true"
                          volumeMounts:
                              -   name: mysql-data
                                  mountPath: /var/lib/mysql
          volumeClaimTemplates:
              -   metadata:
                      name: mysql-data
                  spec:
                      accessModes: [ "ReadWriteOnce" ]
                      storageClassName: ssd
                      resources:
                          requests:
                              storage: 4Gi
      
    • 슬레이브용 설정 :: mysql-slave.yaml
      apiVersion: v1
      kind: Service
      metadata:
          name: mysql-slave
          labels:
              app: mysql-slave
      spec:
          ports:
              -   port: 3306
                  name: mysql
          clusterIP: None
          selector:
              app: mysql-slave
      
      ---
      apiVersion: apps/v1
      kind: StatefulSet
      metadata:
          name: mysql-slave
          labels:
              app: mysql-slave
      spec:
          serviceName: "mysql-slave"
          selector:
              matchLabels:
                  app: mysql-slave
          replicas: 2
          updateStrategy:
              type: OnDelete
          template:
              metadata:
                  abels:
                      app: mysql-slave
              spec:
                  terminationGracePeriodSeconds: 60
                  containers:
                      -   ame: mysql
                          image: gihyodocker/tododb:latest
                          imagePullPolicy: Always
                          args:
                              -   "--ignore-db-dir=lost+found"
                          ports:
                              -   containerPort: 3306
                          env:
                              -   name: MYSQL_MASTER_HOST
                                  value: "mysql-master"
                              -   name: MYSQL_ROOT_PASSWORD
                                  value: "gihyo"
                              -   name: MYSQL_DATABASE
                                  value: "tododb"
                              -   name: MYSQL_USER
                                  value: "gihyo"
                              -   name: MYSQL_PASSWORD
                                  value: "gihyo"
                              -   name: MYSQL_REPL_USER
                                  value: "repl"
                              -   name: MYSQL_REPL_PASSWORD
                                  value: "gihyo"
                          volumeMounts:
                              -   name: mysql-data
                                  mountPath: /var/lib/mysql
          volumeClaimTemplates:
              -   metadata:
                      name: mysql-data
                  spec:
                  accessModes: [ "ReadWriteOnce" ]
                  storageClassName: ssd
                  resources:
                      requests:
                          storage: 4Gi
      
    • 마스터-슬레이브 배포
      $ kubectl apply -f mysql-master.yaml
      $ kubectl apply -f mysql-slave.yaml
      
    • 실행 결과 확인 및 데이터 삽입, 확인
      $ kubectl get pod
      $ kubectl exec -it mysql-master-0 -- init-data.sh
      $ kubectl exec -it mysql-slave-0 bash
      mysql-slave-0# mysql -u root -pgihyo tododb -e "show tables;"
      

4. GKE에 TODO API 구축

  • todo-api.yaml 작성
    apiVersion: v1
    kind: Service
    metadata:
        name: todoapi
        labels:
            app: todoapi
    spec:
        selector:
            app: todoapi
        ports:
            -   name: http
                port: 80
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
        name: todoapi
        labels:
            name: todoapi
    spec:
        replicas: 2
        selector:
            matchLabels:
                app: todoapi
        template:
            metadata:
                labels:
                    app: todoapi
            spec:
                containers:
                    -   name: nginx
                        image: gihyodocker/nginx:latest
                        imagePullPolicy: Always
                        ports:
                            -   containerPort: 80
                        env:
                            -   name: WORKER_PROCESSES
                                value: "2"
                            -   name: WORKER_CONNECTIONS
                                value: "1024"
                            -   name: LOG_STDOUT
                                value: "true"
                            -   name: BACKEND_HOST
                                value: "localhost:8080"
                    -   name: api
                        image: gihyodocker/todoapi:latest
                        imagePullPolicy: Always
                        ports:
                            -   containerPort: 8080
                        env:
                            -   name: TODO_BIND
                                value: ":8080"
                            -   name: TODO_MASTER_URL
                                value: "gihyo:gihyo@tcp(mysql-master:3306)/tododb?parseTime=true"
                            -   name: TODO_SLAVE_URL
                                value: "gihyo:gihyo@tcp(mysql-slave:3306)/tododb?parseTime=true"
    
  • 적용 및 확인
    $ kubectl apply -f todo-api.yaml
    $ kubectl get pod -l app=todoapi
    

5. GKE에 TODO 웹 어플리케이션 구축

  • 정적파일(애셋)은 Node.js를 거치지 않고 Nginx에서 바로 제공한다.
  • todo-api.yaml 작성
    apiVersion: v1
    kind: Service
    metadata:
        name: todoweb
        labels:
            app: todoweb
    spec:
        selector:
            app: todoweb
        ports:
            -   name: http
                port: 80
        type: NodePort
    
    ---
    apiVersion:  apps/v1
    kind: Deployment
    metadata:
        name: todoweb
        labels:
            name: todoweb
    spec:
        replicas: 2
        selector:
            matchLabels:
                app: todoweb
        template:
            metadata:
                labels:
                    app: todoweb
            spec:
                volumes: # (1)
                    -   name: assets
                        emptyDir: {}
                containers:
                    -   name: nginx
                        image: gihyodocker/nginx-nuxt:latest
                        imagePullPolicy: Always
                        ports:
                            -   containerPort: 80
                        env:
                            -   name: WORKER_PROCESSES
                                alue: "2"
                            -   name: WORKER_CONNECTIONS
                                value: "1024"
                            -   name: LOG_STDOUT
                                value: "true"
                            -   name: BACKEND_HOST
                                value: "localhost:3000"
                        volumeMounts: # (2)
                            -   mountPath: /var/www/_nuxt
                                name: assets
                    -   name: web
                        image: gihyodocker/todoweb:latest
                        imagePullPolicy: Always
                        lifecycle: # (3)
                            postStart:
                                exec:
                                    command:
                                        -   cp
                                        -   -R
                                        -   /todoweb/.nuxt/dist
                                        -   /
                        ports:
                            -   containerPort: 3000
                        env:
                            -   name: TODO_API_URL
                                value: http://todoapi
                        volumeMounts: # (4)
                            -   mountPath: /dist
                                name: assets
    
    • (1) : assets라는 이름으로 볼륨을 생성한다. emptyDir:로 설정해 파드 단위로 할당되는 가상 볼륨을 생성한다.
    • (2), (4) : 생성된 볼륨을 컨테이너에 마운트한다. mountPath 속성값으로 경로를 결정한다.
    • (3) : web 컨테이너의 애셋 파일을 Nginx 컨테이너에 복사하는 과정(lifecycle 이벤트를 이용하는 것이 좋다.)
      @web $ cp -R /todoweb/.nuxt/dist /
      
      • /dist로 애셋 파일을 복사한다.
      • entrykit의 prehook을 사용해도 되지만 lifecycle을 사용하여 Dockerfile을 수정하지 않고 사용할 수 있다.
  • 적용 및 확인
    $ kubectl apply -f todo-web.yaml
    $ kubectl get service todoweb
    

6. 인그레스로 웹 어플리케이션 노출하기

  • GCP의 Cloud Load Balancing을 사용
  • ingress.yaml 작성
    apiVersion: extensions/v1beta1
    kind: Ingress
    metadata:
        name: ingress
    spec:
        rules:
            - http:
                paths:
                    -   path: /*
                        backend:
                            serviceName: todoweb
                            servicePort: 80
    
  • 적용 및 확인
    $ kubectl apply -f ingress.yaml
    $ kubectl get ingress
    
    • 인그레스 생성후 1분 정도가 지나면 글로벌 IP를 할당받는다.
    • 할당된 IP 주소로 접근해보면 GKE에 배포된 TODO 어플리케이션이 브라우저에 표시된다.

7. 온프레미스 환경에서 쿠버네티스 클러스터 구축

  • kubespray를 사용해 독단적인 온프레미스 환경에서 클러스터를 스크래치 빌드하는 방법

8. kubespray를 사용한 쿠버네티스 클러스터 구축

  • 클러스터를 구축할 서버 준비하기

    • 작업용 서버(ops) * 1
    • 쿠버네티스 마스터 서버(master) * 3
    • 쿠버네티스 노드 서버(node) * 1
    • 위 서버의 운영 체제는 모두 우분투 16.04 TLS
    • 모든 서버는 폐쇄된 로컬 네트워크에 구축되며 모두 사설 IP 주소를 갖는다.
    이름사설 IP역할
    master0110.90.65.11마스터
    master0210.90.65.12마스터
    master0310.90.65.13마스터
    node0110.90.65.21노드
    ops10.90.65.90작업용(ops)
    • 마스터 서버 : 어플리케이션이 배포될 노드 서버나, 서비스 혹은 파드 등의 리소스를 관리하는 사령탑 역할을 한다.
    • 노드 서버 : 어플리케이션 파드가 실제로 배포되는 서버이다.
    • 작업용 서버 : kubespray를 실행하는 작업용 서버로, 쿠버네티스가 직접 가동되지 않는다.
    • 작업용 서버에서 앤서블을 통해 kubespray를 싱행해 마스터 서버와 노드 서버로 쿠버네티스 클러스터를 구성해 볼 것이다.
  • 작업용 서버의 SSH 공개키 등록

    (root@ops) $ ssh-keygen -t rsa
    (root@ops) $ ssh-copy-id root@master01
    (root@ops) $ ssh-copy-id root@master02
    (root@ops) $ ssh-copy-id root@master03
    (root@ops) $ ssh-copy-id root@node01
    

    또는

    (root@master01) echo "ssh-ras ..." >> ~/.ssh/authorized_keys
    (root@master02) echo "ssh-ras ..." >> ~/.ssh/authorized_keys
    (root@master03) echo "ssh-ras ..." >> ~/.ssh/authorized_keys
    (root@node01) echo "ssh-ras ..." >> ~/.ssh/authorized_keys
    
  • IPv4 포트 포워딩 활성화

    • 일회성 : 즉시 적용
      $ sysctl -w net.ipv4.ip_forward=1
      
    • 영구적 : 재부팅 후 적용
      $ cat /etc/sysctl.conf
      # Uncomment the next line to enable packet forwarding for IPv4
      net.ipv4.ip_forward=1
      
  • 작업용 서버에 kubespray 설치

    (root@ops) $ pip install ansible netaddr
    (root@ops) $ git clone https://github.com/kubernetes-incubator/kubespray
    (root@ops) $ cd kubespray && git checkout v2.5.0
    (root@ops ~/kubespray) $ pip install -r requirements.txt
    (root@ops ~/kubespray) $ cp -rfp inventory/sample incentory/mycluster
    (root@ops ~/kubespray) $ declare -a IPS=(10.90.65.11 10.90.65.12 10.90.65.13 10.90.65.21)
    (root@ops ~/kubespray) $ CONFIG_FILE=inventory/mycluster/hosts.ini python3
    (root@ops ~/kubespray) $ contrib/inventory_builder/inventory.py ${IPS[0]}
    
  • 클러스터 설정

    • 클러스터 구성 서버 설정 : inventory/mycluster/host.ini
      [all]
      master01 ansible_host=10.90.65.11 ip=10.90.65.11
      master02 ansible_host=10.90.65.12 ip=10.90.65.12
      master03 ansible_host=10.90.65.13 ip=10.90.65.13
      node01 ansible_host=10.90.65.21 ip=10.90.65.21
      
      [kube-master]
      master01
      master02
      master03
      
      [kube-node]
      node01
      
      [etcd]
      master01
      master02
      master03
      
      [k8s-cluster:chidren]
      kube-node
      kube-master
      
      [calico-rr]
      
      [vault]
      
    • 쿠버네티스 설정 : inventory/mycluster/group_vars/k8s-cluster.yml
      ...
      ## Change this to use another Kubernetes version, e.g. a current beta release
      kube_version: v1.9.5
      
      # Kubernetes dashboard
      # RBAC required. see docs/getting-started.md for access details.
      dashboard_enable: true
      ...
      
  • 클러스터 구축

    • 실제 클러스터 구축
      (root@ops ~/kubespray) $ ansible-playbook -i inventory/mycluster/hosts.ini cluster.yml
      
    • 구축 대상 서버의 사양 등의 요소에 따라 달라지지만, 20~30분 정도면 클러스터 구축이 완료된다.
    • master 서버에 kubectl 명령이 실행되는지 확인한다.
      (root@master01) $ kubectl get nodes
      
    • 직접 구축한 쿠버네티스 클러스터는 매니지드 서비스가 아니기 때문에 마스터 서버의 가용성 확보나 쿠버네티스 버전업 등의 유지보수 작업도 직접 수행해야 한다.
    • 운영 환경이라면 이러한 클러스터 유지 보수 체계를 갖추거나 GKE같은 매니지드 서비스를 사용해야 한다.