서버 환경을 구축하기 위한 Kubernetes 도입기

profile
BE Developer - 라이언
July 31, 2023
GPT 요약
Thumbnail

Kubernetes reborn with Midjourney

안녕하세요.
덴티움 IT팀의 BE Developer 라이언입니다.

오늘은 IT팀에서 서버 환경을 구축하면서 여러 이슈를 어떻게 해결해 나갔는지 공유하고자 합니다.

이번 글은 서버 환경을 처음 구축해 보시거나 더 확장하고자 하시는 분들께 도움이 될 것이라고 생각합니다. 글을 시작하기에 앞서 서버 환경이 무엇이고 왜 필요한 것인가에 대해서 먼저 알아보도록 하겠습니다.

Thumbnail

서버 환경이란?

서버 환경은 크게 개발 서버(Development Server), 스테이징 서버(Staging Server), 운영 서버(Production Server)로 나눌 수 있습니다. (Local과 QA와 같은 세부 환경은 생략했습니다.)

개발 서버는 개발자들의 개인(Local) 개발 환경이 아닌 통합된 환경으로 테스트를 할 수 있는 서버를 말합니다. 대부분의 프로젝트에서 개발 서버(Development Server)는 스테이지 서버의 환경과 비슷하게 구축하려는 경향이 있습니다.

스테이징 서버는 운영 서버(Production Server) 환경과 거의 동일하게 구성하고 실제 운영 데이터를 적용하여 실질적으로 운영 서버에 반영하기 전, 최종 테스트하는 서버라고 할 수 있습니다.

운영 서버는 실질적으로 서버를 운영하기 위한 서버입니다. 스테이징 서버에서 정상적으로 작동되는 기능들을 운영 서버에 반영하게 됩니다.

프로젝트를 시작하진 얼마 되지 않는 경우, 대부분 위에서 말씀드린 3가지 환경을 모두 구축하는 경우는 극히 드뭅니다. 그래서 저희 IT팀에서 처음에는 아주 작은 규모의 서버 환경을 구축했습니다. 해당 서버 환경은 개발 서버라고 할 수 있겠습니다.

초기 프로젝트에 필요한 개발 서버

웹 개발 특성상 프로젝트 초기에도 Front-end와 Back-end 간의 협업을 위해 안정된 서버 환경이 필요했습니다. 하지만 API 개발에 소요되는 시간 때문에, 서버 환경을 구축하기 위한 시간을 충분히 가질 수 없었습니다.

그래서 프로젝트 초기에는 WSL2기반의 Docker Desktop과 Dockerfile을 활용하여 아주 작은 규모의 서버 환경을 구축했습니다.

초기 서버 환경에서는 아래와 같이 PostgreSQL 서버, API 서버 이렇게 2개의 서버가 필요했습니다.

docker run --name postgres -p 5000:5000 --restart=unless-stopped -d api:latest

docker run --name api -p 5432:5432 --restart=unless-stopped -d postgres:latest
Tip

--restart=unless-stopped 속성은 사용자가 직접 Container를 Stop하지 않는 이상, 계속해서 서버를 항상 재시작하는 옵션입니다.

이렇게 아주 작았던 서버 환경은 시간이 지나면서 발전을 거듭해, 아래와 같이 docker-compose를 활용하는 방식으로 변경되었습니다.

version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: /docker/api.Dockerfile
    container_name: api_server
    restart: on-failure
    environment:
      NODE_ENV: ${NODE_ENV}
    ports:
      - ${APP_PORT}:${APP_PORT}
    depends_on:
      - pg

  pg:
    image: postgres:alpine
    container_name: chart_db_server
    restart: on-failure
    environment:
      POSTGRES_USER: ${POSTGRES_USERNAME}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DATABASE}
    ports:
      - 5432
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  pg_data:
  pgadmin-data:

규모가 커진다는 것은 그 만큼 많은 취약점에 노출될 수 있다는 것을 의미합니다. 그리고 규모가 커진 서버 환경을 운영하던 중에 한 가지 보안 취약점을 찾을 수 있었습니다. 그것은 바로 PostgreSQL 서버를 외부에서 쉽게 접근할 수 있다는 문제였습니다.

해당 문제에 대응하기 위해서는 아래와 같이 두 가지 이슈를 해결해야 했습니다.

  1. PostgreSQL 서버에 대한 외부 접근을 어떻게 차단할 것인가?
  2. 외부 접근이 차단되 PostgreSQL 서버에 데이터를 어떻게 확인할 수 있는가? (API를 활용하여 데이터를 추가하고 제대로 저장되었는지 여부를 DB에 직접 접근해서 확인하는 프로세스를 통해 테스트를 진행하고 있었습니다.)

해결 방법

첫 번째 이슈는 아래와 같이 외부에서 PostgreSQL 서버에 접근할 수 있었던 ports 속성을 제거하고 expose, networks 속성을 활용하여 docker-compose 내부 네트워크 포함되어 있는 API 서버만 접근하는 방식으로 해결했습니다.

version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: /docker/api.Dockerfile
    container_name: api_server
    restart: on-failure
    environment:
      NODE_ENV: ${NODE_ENV}
    networks:
      - api_net
    ports:
      - ${APP_PORT}:${APP_PORT}
    depends_on:
      - pg

  pg:
    image: postgres:alpine
    container_name: db_server
    restart: on-failure
    environment:
      POSTGRES_USER: ${POSTGRES_USERNAME}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DATABASE}
    networks:
      - api_net
    expose:
      - 5432
    volumes:
      - pg_data:/var/lib/postgresql/data

networks:
  api_net:

volumes:
  pg_data:
  pgadmin-data:

두 번째 이슈는 아래와 같이 PostgreSQL 서버에 직접 접근해서 데이터를 확인하는 것이 아니라 pgAdmin4라는 DB Admin 서버를 통해서 데이터를 확인하는 방법으로 해결했습니다.


  ...

  pgadmin:
    build:
      context: .
      dockerfile: /docker/pgadmin4.Dockerfile
    restart: on-failure
    container_name: db_viewer
    environment:
      - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
      - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
      - PGADMIN_LISTEN_PORT=${PGADMIN_LISTEN_PORT}
    networks:
      - api_net
    ports:
      - ${PGADMIN_PORT}:${PGADMIN_LISTEN_PORT}
    volumes:
      - pgadmin-data:/var/lib/pgadmin
      - ./docker/config/servers.json:/pgadmin4/servers.json
    depends_on:
      - pg

  ...

이렇게 프로젝트 초기, Front-end와 Back-end 간의 협업을 위한 개발 서버 환경 1호기가 구축되었습니다. 하지만 개발 서버 환경 1호기는 여러 효율적이지 못한 문제를 직면하게 되면서 오래 살아남지 못했습니다...

Kubernetes 도입 배경

docker-compose 기반으로 구축된 개발 서버 환경에도 마찬가지로 여러 문제가 산재하고 있었습니다.

문제를 정리하면 아래와 같습니다.

  1. API 업데이트를 진행할 때마다 서버를 수동으로 중지하고 다시 실행해야 했습니다.
  2. 서버를 수동으로 업데이트하는 동안 서버가 중지되어 Front-end 개발에 딜레이가 발생했습니다.
  3. 다중 API 서버 환경에 대한 테스트를 할 수 없었습니다.

이외에도 여러 문제가 있었지만 위 3가지 문제가 제일 시급했습니다.

기나긴 고민 끝에 위 3가지 문제를 한번에 해결하기 위해서는 Kubernetes를 도입하는 방법 밖에 없다는 결론을 내렸습니다. 하지만, 그 당시 저는 Kubernetes란 기술에 능숙하지 못했기 때문에 Kubernetes를 도입한다는 것은 그렇게 쉬운일이 아니였습니다.

물론, AWS, Azure, Google Cloud의 Kubernetes 서비스를 활용하면 빠르고 쉽게 구축할 수 있습니다. 그렇지만, 운영 서버가 아닌 불안전한 개발 서버이기 때문에 예기치 못한 큰 비용 발생에 대한 부담이 있었습니다.

이러한 이유로 Kubernetes 환경을 직접 구축하기로 결정하였습니다.

Kubernetes 환경을 직접 구축하기

Kubernetes 환경을 직접 구축하기 위해 먼저 Ubuntu 운영 체제 서버가 필요합니다.

Ubuntu 운영 체제 서버가 준비되었다면 본격적으로 Kubernetes 환경을 구축하기 위한 준비가 된 것입니다.

종속성 설치

먼저 추후에 사용할 Command를 위해 시스템에 패키지를 설치합니다.

$ apt-get update

$ apt-get install -y apt-transport-https ca-certificates curl

컨테이너 런타임 설치

다양한 컨테이너 런타임이 있지만 저는 containerd를 사용하기로 하였습니다. containerd를 설치하기 전에 먼저 구성 파일을 생성합니다.

$ curl -fsSLo containerd-config.toml https://gist.githubusercontent.com/oradwell/31ef858de3ca43addef68ff971f459c2/raw/5099df007eb717a11825c3890a0517892fa12dbf/containerd-config.toml

$ mkdir /etc/containerd

$ mv containerd-config.toml /etc/containerd/config.toml

구성 파일을 생성했다면 다음 Command를 사용하여 공식 GitHub 저장소를 통해 containerd를 설치합니다.

$ curl -fsSLo containerd-1.6.14-linux-amd64.tar.gz https://github.com/containerd/containerd/releases/download/v1.6.14/containerd-1.6.14-linux-amd64.tar.gz

$ tar Cxzvf /usr/local containerd-1.6.14-linux-amd64.tar.gz

$ curl -fsSLo /etc/systemd/system/containerd.service https://raw.githubusercontent.com/containerd/containerd/main/containerd.service

$ systemctl daemon-reload

$ systemctl enable --now containerd

컨테이너 CLI 툴 설치

정상적으로 컨테이너 런타임을 설치했다면 컨테이너를 컨트롤 할 수 있는 CLI를 설치해야 합니다.

$ curl -fsSLo runc.amd64 https://github.com/opencontainers/runc/releases/download/v1.1.3/runc.amd64

$ install -m 755 runc.amd64 /usr/local/sbin/runc

CNI 네트워크 플러그인 설치

CNI(Container Network Interface)는 컨테이너 간의 네트워킹을 제어할 수 있는 플러그인을 만들기 위한 표준으로 쿠버네티스에서 Pod 간의 통신을 위해서 사용합니다.

Talk

CNCF(Cloud Native Computing Foundation)는 2015년 12월 리눅스 재단 소속의 비영리 단체입니다. 첫 번째 프로젝트로 Kubernetes를 Google에서 기증하였습니다. 클라우드네이티브 컴퓨팅 환경에서 필요한 다양한 오픈소스 프로젝트를 추진하고 관리하고 있습니다. CNCF맴버로는 인텔, Arm, 알리바바클라우드, 에저, 구글, 레드헷, SAP, vmware 등등 500개 이상의 글로벌 기업들이 활동하고 있습니다.

$ curl -fsSLo cni-plugins-linux-amd64-v1.1.1.tgz https://github.com/containernetworking/plugins/releases/download/v1.1.1/cni-plugins-linux-amd64-v1.1.1.tgz

$ mkdir -p /opt/cni/bin

$ tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.1.1.tgz

$ cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

$ modprobe -a overlay br_netfilter

$ cat <<EOF | tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

$ sysctl --system

kubeadm, kubelet & kubectl 설치

다음은 Kubernetes에서 제공하는 기본 도구인 kubeadm과 Pod에서 Container가 제대로 동작하도록 관리하는 kubelet, 그리고 Kubernetes의 CLI인 kubectl을 설치합니다.

$ curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg

$ echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | tee /etc/apt/sources.list.d/kubernetes.list

$ apt-get update

$ apt-get install -y kubelet kubeadm kubectl

$ apt-mark hold kubelet kubeadm kubectl

Cluster 생성

kubeadm 도구를 활용하여 Cluster를 생성합니다.

$ kubeadm init --pod-network-cidr=10.244.0.0/16

kubectl 설정

생성된 Cluster에 접근하려면 kubectl을 아래와 같이 설정해야 합니다.

$ mkdir -p $HOME/.kube

$ cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

$ chown $(id -u):$(id -g) $HOME/.kube/config

Untaint node

Pod를 단일 Node Cluster에 배포할 수 있도록 Node를 Untaint해야 합니다.

$ kubectl taint nodes --all node-role.kubernetes.io/master-

$ kubectl taint nodes --all node-role.kubernetes.io/control-plane-

CNI 플러그인 flannel 설치

네트워킹이 본격적으로 작동되려면 flannel을 설치해야 합니다.

$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

Metrics 서버 설치

Kubernetes의 리소스를 모니터링하기 위해 Metrics 서버를 설치합니다.

$ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

$ kubectl edit deploy -n kube-system metrics-server
apiVersion: apps/v1
kind: Deployment

  ...

    spec:
      containers:
      - args:
        - --cert-dir=/tmp
        - --secure-port=4443
        - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
        - --kubelet-use-node-status-port
        - --metric-resolution=15s
        - --kubelet-insecure-tls                                         ==> 추가 요소
        image: registry.k8s.io/metrics-server/metrics-server:v0.6.3
        imagePullPolicy: IfNotPresent

  ...

  unavailableReplicas: 1
  updatedReplicas: 1

@@@
⌨ Esc => :wq + Enter

MetalLB-LoadBalancer + Nginx-Ingress

마지막으로 LoadBalancer와 Ingress를 설치하게 되면 Kubernetes 환경 구축이 마무리 됩니다.

Talk

LoadBalancer는 하나의 인터넷 서비스가 발생하는 트래픽이 많을 때 여러 대의 서버가 분산처리하여 서버의 로드율 증가, 부하량, 속도저하 등을 고려하여 적절히 분산처리하여 해결해주는 서비스입니다.

Talk

Ingress는 Cluster 외부에서 내부로 접근하는 요청들을 어떻게 처리할 지 정의해둔 규칙들의 모음입니다.

$ kubectl cluster-info
Kubernetes control plane is running at https://10.0.0.8:6443
CoreDNS is running at https://10.0.0.8:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

$ git clone https://github.com/metallb/metallb.git

$ cd metallb/config/manifests

$ kubectl apply -f metallb-native.yaml

$ kubectl get all -n metallb-system

$ nano ipaddress-pools.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.0.0.8-10.0.0.8
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-advert
  namespace: metallb-system

$ kubectl apply -f ipaddress-pools.yaml
ipaddresspool.metallb.io/first-pool created
l2advertisement.metallb.io/l2-advert created

$ kubectl get ipaddresspools.metallb.io -n metallb-system
NAME         AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
first-pool   true          false             ["10.0.0.8-10.0.0.8"]

$ kubectl get l2advertisements.metallb.io -n metallb-system
NAME        IPADDRESSPOOLS   IPADDRESSPOOL SELECTORS   INTERFACES
l2-advert

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/baremetal/deploy.yaml

$ kubectl edit svc ingress-nginx-controller -n ingress-nginx
service/ingress-nginx-controller edited

kubectl -n ingress-nginx get svc
NAME                                 TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.103.25.76     10.0.0.8      80:30568/TCP,443:30856/TCP   100s
ingress-nginx-controller-admission   ClusterIP      10.109.146.111   <none>        443/TCP                      100s

마치며

Kubernetes라는 기술이 빠르게 발전함에 따라 설치와 구성하는 방법 또한 수시로 업데이트되고 있어 Kubernetes 환경을 구축하기란 여전히 쉽지 않은 일입니다.

이러한 이유로 큰 비용을 지불할 수 있는 환경적 여유가 된다면 앞서 말씀드린 AWS, Azure, Google Cloud의 서비스를 활용하여 Kubernetes 환경을 빠르고 쉽게 구축하는 것이 좋을 것 같습니다.

하지만, 수학에서 무작정 외운 공식을 사용하는 것보다 원리를 이해하고 사용하는 공식이 더 정확하게 답을 찾을 수 있는 것처럼, Kubernetes라는 기술도 무작정 유료 서비스를 사용하는 것 보다 직접 구축해보면서 원리를 알고 체화하는 것이 더 현명한 방식이라 생각합니다.

빠른 Kubernetes 구축을 위해 다소 짧게 요약했지만 공식문서를 통해 좀 더 자세하게 알아보며 구축하는 것을 권장드립니다.

감사합니다!

profile
안녕하세요 👏
BE Developer 라이언입니다.