본문 바로가기

카테고리 없음

[클라우드응용SW개발] 도커와 쿠버네티스 한 번에 정리

컨테이너 하나를 직접 빌드해 띄우는 법부터, 그게 수천 개가 됐을 때 쿠버네티스로 굴리는 법까지

 

"제 자리에선 분명히 잘 됐는데요?" 개발하다 보면 한 번쯤은 하게 되는 변명이다. 원인은 대부분 같다. 개발 환경과 운영 환경이 다르기 때문이다. 내 노트북은 파이썬 3.12인데 서버는 3.1이거나, 패키지 버전이 어긋나거나, 클라이언트엔 있고 서버엔 없는 환경 변수가 있거나. 심지어 서버 한 대 안에서도 앱마다 요구하는 파이썬 버전이 다를 수 있다. 이 환경 복잡성을 정리하려고 나온 게 컨테이너 기술이고, 그 중심에 도커가 있다.

도커가 푸는 문제 — 실행에 필요한 걸 통째로 담는다

도커는 부두에서 쓰는 물류 컨테이너에서 이름과 개념을 그대로 빌려왔다. 컨테이너 안에 쌀이 들었든 전자제품이 들었든 운송하는 쪽은 내용물에 신경 쓰지 않고 컨테이너 단위로만 옮긴다. 도커도 똑같다. 운영체제, 실행 환경, 라이브러리까지 실행에 필요한 모든 걸 컨테이너 하나에 담아 어디서든 같은 상태로 돌린다. 그래서 "내 컴퓨터에선 됐는데"가 사라진다.

도커를 다룰 때 헷갈리지 말아야 할 세 단어가 있다. 이게 곧 작업 흐름이다.

Dockerfile 환경을 적은 설계서(스크립트) 도면
Docker Image 설계서로 빌드한 파일 결과물(저장장치에 존재) 완성된 제품(박스)
Docker Container 이미지를 메모리에 올려 실제로 실행한 상태 박스를 풀어 돌아가는 것

 

 Dockerfile 작성 → build로 이미지 생성 → run으로 컨테이너 실행 순서다. 트래픽이 몰리면 이미지 하나로 컨테이너를 수천, 수만 개씩 찍어내 묶어서 서비스할 수도 있다.

VM과 뭐가 다른가

기존 가상화는 물리 인프라 위에 하이퍼바이저를 얹고, 그 위에 게스트 OS를 여러 개 올린다. OS가 매번 통째로 반복되니 그만큼 오버헤드가 크다. 컨테이너는 OS를 하나만 두고 그 위에 가벼운 도커 엔진이 돌면서 앱들을 바로 올린다.

가상 머신(VM)컨테이너

구조 인프라 → 하이퍼바이저 → 게스트 OS 여러 개 인프라 → OS 하나 → 도커 엔진
무게 무겁다(OS 반복) 가볍다
속도 느림 빠릿함

 

도커의 모체가 리눅스라 실행 환경은 리눅스에 최적화돼 있다. 엔진은 하나지만 우분투 20.04, 22.04, CentOS 같은 서로 다른 환경을 동시에 돌리는 효과를 낼 수 있다.

레이어와 레지스트리

이미지가 통짜였다면 작은 변화에도 전체를 다시 옮겨야 한다. 도커는 그래서 필요한 요소를 층층이 쌓는 레이어(Layer) 구조로 만든다. 베이스 이미지(예: Ubuntu) 위에 웹 서버(NGINX) 레이어를 얹고, 그 위에 내 소스 코드를 올리는 식이다. 서버에 이미 우분투 레이어가 있다면 NGINX 컨테이너를 받을 때 추가된 레이어만 내려받으면 되니 가볍다.

 

이렇게 만든 이미지를 용도별·버전별로 쌓아두는 주차장 같은 공간이 레지스트리(Registry)다. 그중 인터넷에서 깃허브처럼 공개로 운영되는 게 도커 허브(Docker Hub)다. 이미지 경로는 docker.io/네임스페이스/이미지이름:태그 형태이고, 맨 뒤 태그(Tag)가 버전이다. latest로 두면 항상 최신을 가져온다.

실습: 우분투 VM에 도커 올리기

윈도우에도 설치되지만 환경 따라 오류가 잦아서, 애저에 작은 우분투 VM을 띄워 실습했다.

1. VM 생성 & SSH 접속

애저 포털에서 리소스 그룹을 만들고 Ubuntu Server 22.04 LTS로 VM을 생성한다. 실습용이라 CPU 1~2개 최소 사양, 인증은 암호 방식으로 잡고 인바운드 포트를 열어둔다. 배포가 끝나면 공용 IP를 확인해 CMD에서 접속한다.

ssh azureuser@52.231.xxx.xxx

2. 도커 설치 & 확인

설치 명령은 외우기보다 docker.com → Docs → Engine 설치 가이드에서 우분투를 골라 그대로 복사하는 게 정확하다. 클라우드 명령은 자주 바뀌니 공식 문서가 정답이다.

# 공식 가이드의 핵심 한 줄(요약)
sudo apt-get install docker-ce docker-ce-cli containerd.io

# 설치 확인
sudo docker run hello-world

 

hello-world 이미지를 받아 실행하고 소개 메시지가 뜨면 성공이다.

3. sudo 없이 쓰도록 권한 설정

이대로 docker ps를 치면 Permission denied가 뜬다. 매번 sudo를 붙이는 건 귀찮으니 도커 그룹에 사용자를 넣는다. (이것도 공식 문서의 Post-installation steps에 그대로 있다.)

sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker        # 그룹 변경 즉시 적용

docker ps            # 이제 sudo 없이 동작

도커 기본 명령어

핵심 명령은 개수가 많지 않다. 표로 한 번에 정리한다.

명령어하는 일

docker pull ubuntu:18.04 레지스트리에서 이미지 다운로드 (명령은 대소문자 구별)
docker images 받아둔 이미지 목록·태그·용량 확인
docker ps / docker ps -a 실행 중 컨테이너 / 종료된 것까지 전체 확인
docker run -it --name demo1 ubuntu:18.04 /bin/bash 컨테이너 실행 + 인터랙티브로 셸 접속
docker run -d ... 데몬(백그라운드) 모드로 실행
docker exec -it demo1 /bin/bash 이미 떠 있는 컨테이너 내부로 접속
docker logs -f demo1 컨테이너 출력 로그 확인(-f는 실시간)
docker stop demo1 실행 중 컨테이너 중단(→ Exited)
docker rm demo1 중단된 컨테이너를 메모리에서 완전 삭제
docker rmi ubuntu:18.04 이미지 삭제(태그 생략 시 latest)

 

run -it로 컨테이너에 들어가면 프롬프트가 $(일반)에서 #(root)로 바뀐다. 컨테이너 안에서는 최고 권한이라는 뜻이라 명령은 신중히 쳐야 한다.

한 번 막혔던 부분. 이미지를 docker rmi로 지우려는데 오류가 났다. 그 이미지로 만든 컨테이너가 메모리에 남아 있어서다. docker rm으로 컨테이너부터 다 지운 뒤 docker rmi를 해야 깔끔하게 삭제된다.

busybox로 로그 테스트

로그 확인용으로 가벼운 busybox를 자주 쓴다. 1초마다 날짜를 찍는 반복문을 백그라운드로 돌려놓고 logs -f로 들여다봤다.

docker run -d --name logtest busybox \
  sh -c "while true; do echo $(date); sleep 1; done"

docker logs -f logtest      # Ctrl+C로 모니터링 중단

Dockerfile로 내 이미지 직접 빌드하기

여기까지는 남이 만든 이미지를 쓴 거고, 이제 직접 만든다. 새 디렉토리에서 touch Dockerfile로 파일을 만들고 vi로 작성한다. 파일 이름은 반드시 Dockerfile이어야 한다.

명령역할

FROM 베이스 이미지·버전 지정 (시작점)
WORKDIR 컨테이너 내부 기본 작업 디렉토리
COPY 호스트의 소스/데이터를 컨테이너로 복사
RUN 빌드 시 실행할 명령(패키지 설치 등), 여러 번 가능
ENV 환경 변수 설정(언어, 키 값 등)
EXPOSE 외부와 통신할 네트워크 포트 개방
CMD 컨테이너가 처음 실행될 때의 기본 동작
FROM ubuntu:18.04
WORKDIR /app
COPY . /app
RUN apt-get update
RUN apt-get install -y python3
ENV LANG=C.UTF-8
EXPOSE 8080
CMD ["echo", "Hello Docker"]

 

 

작성했으면 빌드한다. -t로 이름과 태그(버전)를 주고, 마지막 .은 "현재 디렉토리의 Dockerfile을 써라"는 뜻이다.

docker build -t myapp:1.0 .
docker images          # 베이스 위에 레이어가 얹혀 용량이 커진 게 보인다
docker run myapp:1.0   # CMD가 실행돼 Hello Docker 출력

 

컨테이너 10,000개를 실행하기

예전엔 서버 한 대를 시골집 소처럼 이름까지 붙여가며 애지중지 관리했다. 그런데 컨테이너는 적게는 수십 개, 많게는 수만 개가 돈다. 이건 광활한 농장에서 가축을 떼로 관리하는 일에 가깝다. 한 마리씩 들여다보며 고치는 건 불가능하니, 죽은 컨테이너는 자동으로 도태시키고 새로 채워 넣는 관리 기술이 필요하다. 그게 쿠버네티스다.

 

쿠버네티스는 컨테이너의 배포·확장·관리를 자동화하는 오케스트레이션 플랫폼이다. 오케스트라 지휘자가 여러 악기를 조율하듯 여러 컨테이너를 한꺼번에 조율한다. 구글이 만들어 오픈소스로 풀었고 지금은 CNCF가 관리하는 사실상 업계 표준이다. 핵심 기능은 셋으로 추릴 수 있다.

  • 자동 배포·스케일링 — 여러 노드(서버)를 묶어 컨테이너를 분산 배치하고, 필요한 개수만 말하면 알아서 만들고 지워 개수를 맞춘다.
  • 네트워크·스토리지 오케스트레이션 — 도메인/IP로 외부에 노출하고 트래픽을 분산하며, 여러 컨테이너가 저장소를 공유하게 연결한다.
  • 셀프 힐링 — 응답 없는 컨테이너를 죽이고 재시작한다. 노드 하나가 통째로 죽어도 그 안의 컨테이너를 다른 노드로 옮겨 되살린다.

아키텍처와 배포판

전체를 관리하는 마스터(Master)와 실제 컨테이너가 도는 워커 노드(Worker Node)로 나뉜다. 사용자는 CLI 도구인 kubectl로 명령을 내리고, kubectl이 마스터와 통신해 노드에 일을 시킨다. 그리고 쿠버네티스에서 컨테이너를 다루는 최소 단위는 컨테이너가 아니라 그걸 감싼 파드(Pod)다.

배포판특징

바닐라 쿠버네티스 CNCF가 관리하는 공식 배포판. 그냥 "쿠버네티스"라 하면 보통 이것
레드햇 오픈시프트 CI/CD·헬름 등 추가 도구 + 유료 기술 지원. 책임이 필요한 기업용(RHEL 기반)
EKS / GKE / AKS AWS·구글·애저가 각각 제공하는 매니지드 쿠버네티스

마스터·노드를 직접 세팅하고 자원을 배분하는 건 워낙 어려워서, 쿠버네티스 그 자체를 "하나의 클라우드"라고 부를 정도다. 그래서 실무에선 클라우드 매니지드 서비스를 많이 쓴다. 학습용으로는 노드 하나로 도는 미니쿠베(Minikube)가 적당하다.

실습: Minikube로 단일 노드 클러스터 띄우기

미니쿠베는 마스터·노드가 하나씩인 단일 노드 클러스터지만 주요 기능은 거의 다 지원한다. 단, 사양이 받쳐줘야 한다. 기본 권장이 vCPU 4 / 메모리 16GB라, 애저 포털의 "크기" 메뉴에서 VM을 Standard D4s 정도로 키웠다.

VM 크기를 키우는 순간부터 비용이 확 뛴다. 실습 끝나면 반드시 중단하거나 삭제해둬야 비용 폭탄을 피한다. 이건 진짜 중요하다.

1. Minikube & kubectl 설치

# Minikube
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

# kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

# 확인
minikube version
kubectl version --client

2. 도커 드라이버로 시작

minikube start --driver=docker   # 필요한 이미지 자동 다운로드 (CPU 2, 메모리 4GB 할당)
minikube status                  # 서비스 정상 여부

kubectl get pod                  # 아직 내 파드는 없음
kubectl get pod -n kube-system   # 쿠버네티스 자체를 돌리는 시스템 파드들이 보인다

다 쓰고 나면 minikube delete로 환경을 통째로 내린다.

YAML과 Pod — 모든 건 파드 단위로

쿠버네티스의 모든 객체와 설정은 YAML로 정의한다. 키-값 구조에 들여쓰기로 계층을 표현하고, 항목이 여럿이면 앞에 대시(-)를 붙여 리스트로 쓴다. 들여쓰기가 한 칸만 어긋나도 오류가 나니 신경 써야 한다.

파드(Pod)는 쿠버네티스가 만들고 관리하는 가장 작은 컴퓨팅 단위다. 컨테이너 하나 이상을 감싸는 형태이고, 같은 파드 안의 컨테이너들은 네트워크·스토리지를 공유한다. 그래서 한 컨테이너가 자원을 너무 먹으면 같은 파드의 다른 컨테이너도 영향을 받는다.

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
    - name: mycontainer
      image: busybox
      args: ["sh", "-c", "while true; do echo $(date); sleep 1; done"]

pod.yaml

kubectl apply -f pod.yaml          # 파드 생성
kubectl get pod                    # 이름·상태(Running)·재시작 횟수·Age 확인
kubectl get pod -w                 # 상태 변화를 실시간 추적
kubectl describe pod mypod         # 노드·이미지·포트 등 상세 스펙
kubectl logs -f mypod              # 로그 실시간
kubectl exec -it mypod -- sh       # 파드 내부 셸 접속
kubectl delete pod mypod           # 삭제 (또는 kubectl delete -f pod.yaml)

Deployment — 파드를 떼로 관리하기

파드를 직접 만들면 죽었을 때 끝이다. 그래서 실제로는 디플로이먼트(Deployment)로 파드를 관리한다. 디플로이먼트는 리플리카셋(ReplicaSet)으로 "파드 몇 개를 유지할지"를 정하고, 그 개수를 항상 맞춘다. 여기서 앞서 말한 기능들이 다 나온다.

  • 셀프 힐링 — 파드가 죽으면 디플로이먼트가 즉시 새 파드를 띄워 개수를 복구한다.
  • 스케일링 — 트래픽이 몰리면 파드 개수를 동적으로 늘린다.
  • 롤아웃(무중단 업데이트) — 컨테이너를 갱신할 때 파드를 한 번에 하나씩 순차 교체해, 밖에서는 서비스가 안 끊긴 것처럼 보인다.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.14.2
          ports:
            - containerPort: 80

deployment.yaml — replicas: 3 이면 nginx 파드 3개를 항상 유지한다.

적용하고 셀프 힐링을 직접 확인해봤다. 파드 하나를 강제로 지워도 곧바로 새 파드가 끼워 들어온다.

kubectl apply -f deployment.yaml
kubectl get pods -w                          # Pending → ContainerCreating → Running 추적

kubectl delete pod nginx-deployment-xxxxx    # 하나 강제 삭제
# → get pods 다시 보면 3개를 맞추려고 새 파드가 자동 생성됨 (오토 힐링)

kubectl scale deployment nginx-deployment --replicas=5   # 동적으로 5개로 확장
kubectl delete deployment nginx-deployment               # 삭제 → 파드들이 Terminating

Service — 죽었다 살아나도 주소는 그대로

파드는 기본적으로 독립·격리돼 있어 외부에서 바로 못 들어간다. 게다가 파드는 언제든 죽고 새로 뜨면서 IP가 바뀐다. 그때마다 주소를 따라다닐 수는 없다.

서비스(Service)는 이 문제를 푼다. 고정된 IP를 들고 있으면서 뒤의 파드들과 매칭해, 파드가 죽었다 살아나 IP가 바뀌어도 외부 연결을 그대로 유지해준다. 하나의 서비스가 여러 파드와 매칭되니, 디플로이먼트로 찍어낸 다수의 파드에 트래픽을 고르게 흘려보낼 수 있다. 다만 서비스를 제대로 세팅하려면 네트워크 기본 지식이 받쳐줘야 한다는 게 솔직한 소감이다.

정리하면

이번 주를 한 줄로 줄이면 이렇다. 도커는 컨테이너 한 개를 어디서든 똑같이 실행하는 기술이고, 쿠버네티스는 그 컨테이너가 수천 개가 됐을 때 떼로 굴리는 기술이다. 도커에서 Dockerfile → 이미지 → 컨테이너 흐름을 손에 익히고, 쿠버네티스에서 Pod → Deployment → Service 순서로 올라가니 큰 그림이 잡혔다.

설치 명령이나 옵션은 자주 바뀌니 외우려 하기보다 공식 문서를 보는 습관이 맞다는 것, 그리고 미니쿠베 돌릴 땐 VM을 꼭 꺼두자는 것. 이 두 개가 이번 주에 제일 실용적으로 남은 교훈이다.