쿠버네티스 삽질기 (1) - 개요
2024년 4월 30일최근 컨테이너를 넘어 컨테이너 오케스트레이션 개념을 이해하기 위해 쿠버네티스를 공부하고 있습니다. 이전에도 쿠버네티스에 대한 호기심이 있었지만, 쿠버네티스를 사용할 적절한 환경의 부재로 파고들지 않았습니다. 인프라 세계에서 컨테이너 오케스트레이션은 빠질 수 없는 개념이 되었고 docker와 마찬가지로 필수 도구가 되었습니다. 쿠버네티스를 사용하면서 처음 컨테이너를 배운 거 처럼 새로운 차원을 만난 느낌이 들었습니다. 기존 컨테이너 구조와는 다르게 적용해야 할 문제가 많아 생소하면서도 쿠버네티스의 설계에 놀라웠습니다.
하지만 쿠버네티스를 사용하면 할수록 ‘쿠버네티스의 장점을 살리기 위해서는 인프라 도구뿐만 아니라 서비스 또한 그에 맞춰 설계가 되어야 하지 않는가?’에 대한 의문이 들었습니다. 보통은 서비스 규모가 커짐에 따라 복잡해지는 코드와 배포 과정을 해소하기 위해 컨테이너 오케스트레이션을 도입하는데(서비스 설계 → 서비스가 커짐 → 쿠버네티스 도입), 저는 인프라를 먼저 고려하다 보니 위와 같은 의문이 생긴 거 같습니다(쿠버네티스 선 도입 → 서비스 설계). 쿠버네티스 환경에서 서비스를 운영한다면 다음과 같은 이점을 기대할 수 있습니다.
- 고가용성 유지
- 오토 스케일링의 자유로움
- self-healing 기능
- 롤링 업데이트 관리
위의 이점은 쿠버네티스가 container를 추상화한 pod를 사용함으로서 얻는 이점이지 않을까 생각됩니다. Pod는 여러 container가 실행할 수 있도록 환경을 제공하며 쿠버네티스의 기본 배포 단위로 스케줄링 됩니다. 이를 통해 스케일링, self-healing, 롤링 업데이트를 수행할 수 있는 구조를 가집니다. 쿠버네티스는 pod와 다른 쿠버네티스 리소스를 활용하여 다양한 컨테이너 구성에서도 유연하게 서비스를 설계할 수 있습니다.
하지만 쿠버네티스를 도입한다고 해서 이런 많은 이점을 얻을 수 있는 걸까요? 단순히 서비스를 pod로 만들고 replicaset으로 pod 수를 늘리고 service로 외부 통신을 한다고 해서 쿠버네티스의 장점을 누릴 수 있는 걸까요? 제가 내린 결론은 ‘아니다’입니다. 운영(production) 환경에서 쿠버네티스를 관리해보거나 인프라 경험이 부족하기에 틀린 내용이 많을 수 있습니다. ‘쿠버네티스의 장점을 살리기 위해서는 인프라 도구뿐만 아니라 서비스 또한 그에 맞춰 설계가 되어야 하지 않는가?’라는 의문에 대한 답을 머릿속이 아닌 코드와 글로 풀어내보고 싶어 정리해보았습니다.
‘쿠버네티스에 맞는 서비스 구조’라는 말은 모호합니다. 서비스의 형태, 개발자의 생각에 따라 정의하기 나름입니다. 쿠버네티스를 공부하면서 만난 문제와 어떤 과정을 거쳤는지에 대한 한 사람의 글이라 생각해 주시면 좋겠습니다.
처음에는 가볍게 쿠버네티스를 공부하자는 마음으로 출발했습니다. Kubernetes core concepts부터 시작하여 쿠버네티스 컴포넌트, 리소스를 살펴보았습니다. 기본적인 쿠버네티스의 개념을 알아보고 이를 적용해보기 위해 쿠버네티스 환경을 구축하였습니다. 쿠버네티스 환경을 구축하기 위해서는 크게 2가지의 방식이 있습니다. 서버에 직접 쿠버네티스를 구축하는 방식과 public cloud가 제공하는 쿠버네티스 서비스(EKS, GKS, AKS 등)를 이용하는 방식이 있습니다. 저는 자체 서버를 보유하고 있고 온프레미스 환경이든 클라우드 환경이든 쿠버네티스를 사용하기에 큰 차이가 없다고 판단하여 서버에 쿠버네티스를 구축하는 방식을 선택했습니다.
쿠버네티스 환경 구축에도 여러 가지 방법이 있습니다. Kind, K3s, Minikube로 쿠버네티스 환경을 구축할 수 있지만 쿠버네티스를 가볍게 쓰기 위해 기능을 줄인 도구입니다. 이는 개발 환경에 적합해 보였기에 로컬에서 테스트하는 용도로 사용했습니다. 운영 환경에서의 쿠버네티스 환경 구축을 위해 Kubespray 도구를 이용해 쿠버네티스 환경을 구축하였습니다. Kubeadm 도구를 이용해 쿠버네티스를 구축할 수도 있지만, Ansible을 사용하여 여러 서버에 노드를 설치하고 자동으로 연결해주는 Kubespray 도구를 선택했습니다. Kubespray를 통해 발생한 몇 가지 이슈만 공유하고 설치 과정은 아래의 정리를 참고해주세요.
쿠버네티스 환경이 생겼으니 쿠버네티스에 올릴 서비스가 필요합니다. 서비스 구축을 위해 2가지 방법을 고민했습니다.
- 기존 컨테이너 환경 서비스를 쿠버네티스 환경으로 옮긴다
- 쿠버네티스 환경을 위한 서비스를 새롭게 만든다
쿠버네티스 사용에 익숙해지기 위해 새로운 서비스를 만들기보다는 1번 방법을 택하였습니다. 이전에 컨테이너 환경으로 설계한 동아리 홈페이지를 쿠버네티스 환경으로 구현해 보았습니다. 동아리 홈페이지는 app(Spring Boot), nginx, redis, db(MySQL) 4개의 컨테이너로 돌아가며 docker compose로 관리하고 있었습니다. 컨테이너에서 pod으로 바꾸고 내/외부 통신을 위한 service, ingress를 붙여보는 식으로 구성해 보았습니다.
컨테이너 환경에서 쿠버네티스 환경으로 이전 했지만 큰 의문점이 남아있었습니다. ‘기존 컨테이너 환경이랑 차이가 뭐지?’ 단순히 컨테이너에서 pod로 바뀌어 구조에 차이가 없었으며 오히려 쿠버네티스 지식을 알아야 서버 운영할 수 있어 진입장벽만 높아졌습니다. 더군다나 쿠버네티스의 장점을 활용할 수 있는 것도 아니였습니다. 쿠버네티스의 self-healing으로 pod가 죽어도 자동으로 재시작되는 것은 유용했지만, 기존 구조에서 컨테이너 수를 늘리거나 쿠버네티스 구조에서 pod 수를 늘리는 것 사이에 큰 차이가 있지 않다고 생각했습니다.
‘왜 차이가 없는걸까?’에 대해서 고민해보았습니다. 컨테이너 기술이 등장하기 전에는 서버에 서비스를 운영하기 위해 필요한 도구를 모두 직접 설치해야 했습니다. 컨테이너 기술의 도입으로 서비스에 필요한 도구를 서버로부터 ‘독립적’으로 관리 할 수 있게 되었습니다. 또한 컨테이너 오케스트레이션으로 ‘여러 컨테이너의 생명주기’를 쉽게 관리할 수 있게 되었습니다. 동아리 홈페이지를 운영하기 위해 Spring Boot, NGINX, Redis, MySQL 4개의 도구를 서버에 설치해야 했지만, 컨테이너로 각각의 도구를 따로 관리할 수 있게 되었습니다. 하지만 쿠버네티스 환경으로 옮기면서 서비스를 단일 pod(= Spring Boot 서비스를 단일 컨테이너로 운영)로만 관리하여 컨테이너 환경과 차이가 없었습니다.
여러 고민 끝에 ‘서비스 컨테이너의 역할을 여러 컨테이너로 쪼개서 관리’하면 쿠버네티스의 장점을 극대화할 수 있다고 기대할 수 있다고 생각했습니다. 즉, app 컨테이너(Spring Boot 컨테이너)의 기능을 잘게 쪼개 여러 컨테이너로 관리하면 쿠버네티스의 장점을 살릴 수 있다고 생각했습니다. 여러 기능으로 나뉜 컨테이너를 각각의 pod로 관리하면 독립적으로 배포가 가능하니 기존 컨테이너 환경에서 하지 못하던 스케일링, 롤링 업데이트와 같은 기능을 쉽게 활용할 수 있게 됩니다. 예를 들어 회원 관리(member), 인증 관리(auth), 게시글 관리(post)를 수행하는 서비스를 한 컨테이너가 아니라 3개의 컨테이너로 관리한다면 특정 컨테이너 수를 늘린다든지, 사양을 조절한다든지, 설정을 추가하는 식으로 유연하게 관리할 수 있습니다.
위의 상황을 적용한다면 한 개의 Spring Boot 서버를 여러 Spring Boot 서버로 나누어 기능을 분리하는 것으로 계획하였습니다. 분리된 Spring Boot 서버를 docker image로 만들고 pod로 띄워 쿠버네티스가 관리한다면 쿠버네티스의 이점을 얻을 수 있을 거라 생각했습니다. 동아리 홈페이지 서비스로 진행하고 싶었지만 덩치가 커진 서비스이기도 하고 Spring Boot 프레임워크에 익숙하지 않았던 터라 작은 서비스를 만들기로 결정했습니다.
한 개의 컨테이너에서 여러 개의 컨테이너로 나누면 그에 따라 구조가 복잡하게 됩니다. 서비스 설계부터 git 전략, 배포 전략, 환경 변수 관리, 버전 관리, 모니터링 관리 등 전체적으로 인프라 구조가 커지게 되어 관리해야 할 대상이 많아집니다.
전체적인 서비스 설계부터 시작하였습니다. 이왕 새롭게 서비스를 만들 겸 경험해보고 싶은 도구를 선택했습니다. 구현할 서비스 기능과 배포 전략을 다음과 같이 간단하게 정리했습니다.
- 서비스
- 회원 관리(member) - Spring Boot(java)
- 인증 관리(auth) - Spring Boot(java)
- 게시글 관리(post) - Spring Boot(java)
- 토큰 인증(jwt) - Gin(golang)
- 배포 전략
- 한 repository에 서비스 코드가 모여있는 monorepo 사용
- 배포 순서
- GitLab push → (GitLab CI) → Jenkins → (docker registry & edit yaml) → ArgoCD → k8s pod 교체
서비스를 크게 회원 관리, 인증 관리, 게시글 관리, 토큰 인증 4개로 구성하였습니다. 참고로 인증 관리는 로그인, 회원가입을 처리하고 토큰 인증은 토큰 유효성을 판단합니다. 토큰 인증 같은 경우는 단순히 토큰 유효성만 판단하므로 Spring Boot 프레임워크보다 가벼운 golang 기반의 Gin 프레임워크를 사용했습니다.
서비스를 개발하고 배포 시스템을 구축하면서 사소한 문제부터 어떻게 해결해야 할지 감이 안 오는 문제까지 여러 문제가 있었습니다.
- 다른 서비스의 DB 데이터가 필요한 경우 어떻게 해결할 것인가
- 서비스 간 의존성을 어떻게 줄일 것인가?
- 서비스마다 필요한 DB 정의 코드를 일일이 정의해야 하는가?
- Monorepo에서 특정 서비스에 대해서만 배포를 어떻게 진행할 것인가
- 어떻게 특정 서비스 코드 변경을 감지하고 배포를 진행해야 하는가?
- 어떤 방식으로 Jenkins pipeline을 구성해야 하는가?
- 매번 요청 인증은 어떻게 처리할 것인가
- 서비스에 인증 처리 코드를 작성하기엔 중복이 많고 관리가 힘들다
- 인프라적으로 해결할 수 없을까?
- 만약 여러 DB pod를 운영한다면 동기화 문제는 어떻게 해결할 것인가
…
쿠버네티스를 사용하기 위해 서비스를 생각하다보니 인프라 뿐만 아니라 서버 개발까지 건드리는 복잡한 프로젝트가 되었습니다. 여러 문제를 만나고 해결한 과정을 다음 글에 이어서 소개하도록 하겠습니다.
Kubespray 환경
- Intel NUC8I3BEH (i3-8109, 32gb)
- VirtualBox 7
- master-node (2 cpu, 8gb, Ubuntu 22.04)
- worker-node1 (2 cpu, 8gb, Ubuntu 22.04)
- worker-node2 (2 cpu, 8gb, Ubuntu 22.04)
- Bridged network 설정
- Kubespray 2.24
- argocd, helm, argocd, metallb 등 addon 설정은 따로 진행하지 않음
- 필요한 외부 도구는 kubectl로 설치로 했습니다
Kubespray를 통한 Kubernetes 설치 과정
-
VirutalBox로 환경 준비
-
VM 고정 ip 설정을 위해
/etc/netplan/00-installer-config.yaml
파일 수정 (선택)YAMLnetwork: ethernets: enp0s3: dhcp4: no addresses: [172.30.1.20/24] routes: - to: default via: 172.30.1.254 nameservers: addresses: - 8.8.8.8 version: 2
-
모든 node의
/etc/hosts
파일에 node 정보 추가TEXTk8s-master 172.30.1.20 k8s-slave1 172.30.1.21 k8s-slave2 172.30.1.22
-
master node에 ssh 키 생성 후 worker node에 ssh 키 전달
BASHssh-keygen ssh-copy-id [ACCOUNT]@[IP] # ssh-copy-id k8s@172.30.1.20
-
모든 node에 비밀번호 없이
sudo
명령어 사용 가능하도록 설정BASHsudo visudo [ACCOUNT] ALL=(ALL) NOPASSWD: ALL # k8s ALL=(ALL) NOPASSWD: ALL
-
모든 node에 swap, 방화벽 해제
BASHswapoff -a systemctl stop firewalld systemctl disable firewalld
-
kubespray git 가져오기
BASHgit clone https://github.com/kubernetes-sigs/kubespray.git # git checkout release-[VERSION]
-
pip 의존성 설치
BASHcd kubespray sudo apt install python3-pip pip install -r requirements.txt
-
inventory 디렉터리 복사
BASHcp -rpf inventory/sample inventory/mycluster
-
inventory.ini
수정BASHvi inventory/mycluster/inventory.ini [all] k8s-master ansible_host=172.30.1.20 ip=172.30.1.20 k8s-slave1 ansible_host=172.30.1.21 ip=172.30.1.21 k8s-slave2 ansible_host=172.30.1.22 ip=172.30.1.22 [kube_control_plane] k8s-master [etcd] k8s-master [kube_node] k8s-slave1 k8s-slave2
-
addon 설정 (선택)
BASH# ingress-nginx, metallb, argocd, helm 등 addon 설치 가능 vi inventory/mycluster/group_vars/k8s_cluster/addons.yml
-
ansible로 서버 상태 확인
BASHansible all -m ping -i inventory/mycluster/inventory.ini
-
k8s 설치
BASH# 설치 ansible-playbook -v -i inventory/mycluster/inventory.ini --become --become-user=root cluster.yml # 제거 ansible-playbook -v -i inventory/mycluster/inventory.ini --become --become-user=root reset.yml
-
root 계정이 아닌 계정도
kubectl
명령어를 사용할 수 있도록 설정BASHmkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config
-
node 확인
BASHkubectl get nodes
Kubespray 사용하면서 만난 이슈
-
ARM64 아키텍처보다 AMD64 환경이 더 안정적이다
- ARM64 환경에 제대로 설치가 안되는 거 같다 (ansible을 통해 다른 node에 설치 괴정에서 오류가 뜨는데 이유를 모르겠다)
-
하드웨어 스펙이 충분해야 설치가 잘된다
- 하드웨어 사양 차이가 있겠지만 램 8GB 정도는 설정해야 설치가 여유롭게 되는 거 같다
-
Kubespray 설치 시 오류가 나면 reset을 하고 재설치하자
- 이유없이 특정 설치 과정에서 오류가 나는데 reset하고 재설치 하는 것이 깔끔하다
-
VirtualBox를 사용한다면 snapshot을 잘 활용하자
- Kubespray를 사용하면 20분 정도 시간이 소요되므로 시간 절약을 위해 쿠버네티스 설치 전후의 snapshot을 찍자
-
addon 설치 시 오류가 나면 원인 파악하기 힘들어 kubectl로 외부 도구를 설치하는 방법을 선택했습니다
-
ingress nginx (1.10.1, baremetal)
BASH# baremetal 버전 설치 kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/baremetal/deploy.yaml # ingress-nginx-controller 서비스를 LoadBalancer 타입으로 변경 (선택) kubectl -n ingress-nginx patch service ingress-nginx-controller -p '{"spec":{"type":"LoadBalancer"}}'
-
metallb (0.14.5)
BASH# strictARP true로 수정 kubectl edit configmap -n kube-system kube-proxy # metallb 설치 kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml # Load balancer ip 대역폭 설정 cat metallb.yml apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: default namespace: metallb-system spec: addresses: - 172.30.1.100-172.30.1.200 autoAssign: true --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: default namespace: metallb-system spec: ipAddressPools: - default kubectl apply -f metallb.yml
-
argocd (2.10.7, non HA)
BASHkubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.10.7/manifests/install.yaml # argocd ingress 설정 (선택) cat argocd-ingress.yml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: argocd-server-ingress namespace: argocd annotations: cert-manager.io/cluster-issuer: letsencrypt-prod kubernetes.io/tls-acme: "true" nginx.ingress.kubernetes.io/ssl-passthrough: "true" nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" spec: ingressClassName: nginx rules: - host: [ADDRESS] http: paths: - path: / pathType: Prefix backend: service: name: argocd-server port: name: https tls: - hosts: - [ADDRESS] secretName: argocd-secret kubectl apply -f argocd-ingress.yml
-