Docker와 Kubernetes를 직접 다뤄보며 첫걸음을 내딛는다.
아주 간단한 애플리케이션을 만들어 도커 이미지 빌드, 컨테이너 실행, docker hub로 배포까지 실습해본다.
busybox는 여러 개의 리눅스 명령어들을 모아놓은 단일 실행 파일이다. 용량이 몇 백 KB밖에 안되지만 필수적인 리눅스 기능들이 들어있어 임베디드 환경에서 자주 쓰인다. busybox를 이용해서 Hello World 컨테이너를 돌렸다.
~$ docker run busybox echo "Hello World"
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
97e70d161e81: Pull complete
Digest: sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f
Status: Downloaded newer image for busybox:latest
Hello World
별 건 없다. 로컬에서 busybax가 없었기때문에 해당 이미지를 pull했다.
매우 간단해 보이지만 뒤에선 어떤 게 벌어지는지 알아보자.

우선 도커는 busybox:latest이미지가 로컬에 있는지 확인 한 후, 없으면 도커 허브에서 가져온다. 그러곤 컨테이너를 만들고 명령어를 실행한다.
같은 이미지에도 서로 다른 태그를 붙여 버전을 관리할 수 있다
$ docker run <image>:<tag>
Node.js app을 만들어 실습해보자. 만들 앱은 간단하다. HTTP request를 받은 후 현재 돌아가고 있는 머신의 hostname을 반환하는 앱이다.
const http = require('http');
const os = require('os');
console.log("Kubia server starting...");
var handler = function(request, response) {
console.log("Received request from " + request.connection.remoteAddress);
response.writeHead(200);
response.end("You've hit " + os.hostname() + "\n");
};
var www = http.createServer(handler);
www.listen(8080);
로컬에 Node.js를 설치할 필요는 없다. 어차피 컨테이너화할 것이므로.
도커 이미지 생성을 위해선 동 디렉토리내에 Dockerfile을 만들어 다음의 내용을 넣자.
FROM node:7
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]
base image로 tag 7의 node 이미지를 쓰고, 로컬의 app.js를 이미지의 root dir로 복사한다. ENTRYPOINT는 이미지를 실행시킬 때 같이 실행되어야 하는 커맨드다.
이제 이미지를 빌드하면 된다. 현재 디렉토리의 컨텐츠를 바탕으로 kubia 라는 이름의 이미지를 만든다.
$ docker build -t kubia .
failed to fetch metadata: fork/exec /usr/local/lib/docker/cli-plugins/docker-buildx: no such file or directory
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM node:7
7: Pulling from library/node
ad74af05f5a2: Pulling fs layer
2b032b8bbe8b: Pulling fs layer
a9a5b35f6ead: Pulling fs layer
...
Status: Downloaded newer image for node:7
---> d9aed20b68a4
Step 2/3 : ADD app.js /app.js
---> 9652b5cfdcd1
Step 3/3 : ENTRYPOINT ["node", "app.js"]
---> Running in 8d3c67540720
---> Removed intermediate container 8d3c67540720
---> 6579cad9c117
Successfully built 6579cad9c117
Successfully tagged kubia:latest
legacy builder가 deprecated 된단다. buildx를 설치하라고 하는데…
Docker version 20이상이면 buildx가 포함되어있다고 하는데, 뭔가 꼬였는지 로컬에서 작동에 문제가 있었다. 그래서 다시 설치함.
mkdir -p ~/.docker/cli-plugins
curl -L https://github.com/docker/buildx/releases/download/v0.22.0/buildx-v0.22.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod 755 ~/.docker/cli-plugins/docker-buildx
docker buildx version
version이 잘 나오면 성공적으로 설치된 것. deprecated 메세지를 받고 싶지 않다면
export DOCKER_BUILDKIT=1
이제 BuildKit 엔진을 사용해서 Dockerfile을 빌드한다. 다시 본론으로..

도커 클라이언트와 도커 데몬은 서로 하는 일이 다르다. 도커 클라이언트가 Dockerfile과 app.js를 데몬에게 넘기면 거기서 이미지를 만든다. 클라이언트와 데몬은 꼭 같은 머신에 있어야 할 필요는 없다. 리눅스가 아닌 경우, 클라이언트는 host OS에 있지만 데몬은 VM에서 돌아갈 것이다.
이미지는 마치 페스츄리처럼 겹겹의 레이어로 쌓여있다. 레이어들은 중복 저장되지 않는다. 그래서 이미 레이어가 있다면 굳이 다시 다운로드하지 않는다. 도커는 레이어 개별로 다운로드하기 때문에 가능하다. 최신 레이어일수록 top에 쌓이며 :latest 태그가 불여진다.

도커 이미지가 잘 생성되었는지 확인하자.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
kubia latest d22321e37c67 12 minutes ago 660MB
<none> <none> 23361e1dd1b6 3 months ago 1.11GB
python 3.11 e729f76087ee 4 months ago 1.01GB
busybox latest ff7a7936e930 6 months ago 4.28MB
hello-world latest d2c94e258dcb 23 months ago 13.3kB
node 7 d9aed20b68a4 7 years ago 660MB
이미지가 잘 만들어졌으니, 컨테이너로 실행시키자.
$ docker run --name kubia-containter -p 8080:8080 -d kubia
c8bc4c2e07a56a55986ae7ce2871aee831e67f24de32d693a2c7051f38ed2549
—name 은 생성할 컨테이너의 이름을 지정해주고, -p는 port number를 지정한다. 8080:8080 는 머신의 포트 8080과 컨테이너 포트 8080을 연결하는 것인데, https://localhost:8080 에서 반환하는 값을 볼 수 있게 한다. -d 옵션은 detach from console인데, 백그라운드 실행을 하는 것이다.
위 주소에 접속하거나 혹은 curl을 날리면,
$ curl localhost:8080
You've hit c8bc4c2e07a5
hostname이 출력되는데, 이건 실제 host machine의 호스트네임이 아니고, 컨테이너의 ID이다.
현재 실행중인 컨테이너를 확인하려면
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c8bc4c2e07a5 kubia "node app.js" 4 minutes ago Up 4 minutes 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp kubia-containter
(오타가 났네. containter라고 저장해버렸다.. ) 좀 더 자세히 알고 싶다면
$ docker inspect kubia-container
실행중인 컨테이너 내부에 접속할 방법이 없을까? 그렇게 된다면 디버깅하기도 수월할 텐데.
방법은 다음과 같다
$ docker exec -it kubia-containter bash
root@c8bc4c2e07a5:/#
실행중인 kubia-containter 에 bash를 실행실 수 있다. 위 명령어를 입력하면 컨테이너 내부에 bash shell이 실행된다. 참고로 -i 옵션은 STDIN open, -t 옵션은 pseudo terminal을 할당한다.
그럼 컨테이너 내부의 프로세스 리스트를 보자.
root@c8bc4c2e07a5:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.3 813608 25256 ? Ssl 15:06 0:00 node app.js
root 12 0.0 0.0 20252 3216 pts/0 Ss 15:14 0:00 bash
root 18 0.0 0.0 17508 2048 pts/0 R+ 15:15 0:00 ps aux
토탈 세개만 돌아가고 있다. host OS에서 다음 명령어를 입력하면
ps aux | grep app.js
root 253594 0.0 0.3 813608 25256 ? Ssl 00:06 0:00 node app.js
여기서도 app.js가 돌아가는 걸 확인할 수 있다. 컨테이너에서 실행되는 프로세스가 실은 host OS에서 돌아가는 것을 보여준다. 그런데 잘 살펴보면 컨테이너에서 얻은 PID와 host OS에서 얻은 PID가 다르다. 컨테이너는 독자적인 PID linux namespace를 쓰기 때문이다.
컨테이너는 호스트와는 분리된 파일 시스템을 가진다.
root@c8bc4c2e07a5:/# ls /
app.js bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
컨테이너를 멈추고 삭제하려면
$ docker stop kubia-containter
kubia-containter
$ docker rm kubia-containter
kubia-containter
사실 내 로컬에서만 도커 이미지 만들고 실행하면 무슨 의미가 있는가. 배포해서 다른 머신에서 쓸 수 있어야 한다. 그걸 위해선 이미지를 Docker Hub로 push 해야한다.
Docker hub는 이미지 레지스트리다. 이외에도 GHCR, ECR, GCR등 여러 레지스트리가 있다. ECR에 대해서 좀 더 알아봐야 하는데.. 일단 나중에
도커 허브에 회원가입하고 로컬에 로그인하자. 도커 허브에 이미지를 푸쉬하려면 tag를 붙여줘야 한다. 내 도커 허브 아이디를 앞에 붙여서
$ docker tag kubia anandashin/kubia
이렇게 태그를 붙이고 이미지 리스트를 확인해 보면 kubia 와 anandashin/kubia 가 별개로 뜨는데, 복사본이 생긴 게 아니라, 그저 하나의 이미지에 서로 다른 태그가 달린 것 뿐이다. 용량을 더 잡아먹진 않는다.
이제 docker hub에 이미지를 푸쉬하자.
$ docker push anandashin/kubia
도커 허브에 들어가 내 레포에 잘 올라갔는지 확인도 해보자.
도커 실습은 이제 끝. 이제 k8s cluster로.
두 가지를 배운다:
minikube(미니쿠브)는 k8s를 로컬 환경에서 사용할 수 있게 만든 쿠버네티스의 가벼운 구현체다. 싱글 노드로 이루어져 있다. 설치부터 차근차근.
$ curl -Lo minikube https://github.com/kubernetes/minikube/releases/download/v1.35.0/minikube-linux-amd64
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 119M 100 119M 0 0 19.9M 0 0:00:05 0:00:05 --:--:-- 26.4M
$ chmod 755 minikube
$ sudo mv minikube /usr/local/bin/
다운받아서 권한 준 다음 bin 디렉토리로 옮긴다. 이제 미니쿠브 VM을 실행하자.
$ minikube start
😄 minikube v1.35.0 on Ubuntu 22.04 (amd64)
✨ Automatically selected the docker driver. Other choices: ssh, none
📌 Using Docker driver with root privileges
👍 Starting "minikube" primary control-plane node in "minikube" cluster
🚜 Pulling base image v0.0.46 ...
...
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
VM을 실행시켰으니, 소통할 수 있는 창구(CLI)도 설치하자 — kubectl
$ curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 53.7M 100 53.7M 0 0 26.8M 0 0:00:02 0:00:02 --:--:-- 26.8M
$ chmod 755 kubectl
$ sudo mv kubectl /usr/local/bin/
이제 미니쿠브와 얘기를 나눌수 있다~
$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:32771
CoreDNS is running at https://127.0.0.1:32771/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
책이랑 살짝 다르게 나오긴 하지만, k8s 컴포넌트들의 url을 확인할 수 있다.
미니쿠브를 끄는(중지하는) 방법은 간단하다.
$ minikube stop
✋ Stopping node "minikube" ...
🛑 Powering off "minikube" via SSH ...
🛑 1 node stopped.
만약 미니쿠브와 GKE를 동시에 사용한다면, 적절한 kubectl context switch가 필요하다.
$ gcloud container clusters get-credentials my-gke-clusterkubectl이 my-gke-cluster라는 GKE cluster를 쓸 수 있다.
구글 클라우드 프로젝트 세팅이 필요하다. 여기서 시간 좀 잡아먹는다. 튜토리얼은 여기서 잘 설명한다.
아마 모든 사람들이 구글 계정이 있으므로, 바로 google cloud platform console에 가서 새 프로젝트를 만든다. 그리고 결제 수단을 활성화한다. 90일 무료라고 하니 기간 내에 잘 써야할듯.
Artifact Registry와 k8s engine API를 활성화한다. gcloud CLI를 쓰기 위해 로컬에 google cloud sdk를 다운받고 설치.
# Ubuntu에 GCloud SDK 설치를 위한 APT 저장소를 등록하는 명령어
$ echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \
| sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
# GPG 키 가져오기
# gpg --dearmor 방식으로 .gpg 키링을 직접 만들어서 사용
$ curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
# update
$ sudo apt-get update
# gcloud CLI 설치
$ sudo apt-get install google-cloud-sdk
# gcloud CLI 초기화
$ gcloud init
gcloud init 시 이것 저것 많이 물어본다. 프로젝트 생성한 계정으로 로그인 해서 권한 부여해주고 region (asia-northeast1-a)잘 설정해주면 된다. kubectl이 없다면 sudo apt-get install kubectl명령어로 설치하자. 나는 앞서 minikube 설치 때 kubectl을 설치했으므로 이건 패스.
본격 실습하면서 클러스터 만들긴 할 건데, 미리 클러스터 생성 방법을 알아보자.
# my-gke-cluster 생성
$ gcloud container clusters create-auto my-gke-cluster \
--region asia-northeast3
정상적으로 생성되었다면 cluster의 정보가 뜰 것이다. 정상적으로 작동했지만 만약 다음 경고문이 나왔다면
CRITICAL: ACTION REQUIRED: gke-gcloud-auth-plugin, which is needed for continued use of
kubectl, was not found or is not executable. Install gke-gcloud-auth-plugin for use with
kubectl by following
https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl#install_plugin
gke-gcloud-auth-plugin이 설치되지 않아서 kubectl이 동작하지 않을 수 있으므로 이것도 설치해주자.
$ sudo apt-get install google-cloud-sdk-gke-gcloud-auth-plugin
이후 kubectl을 연결시키면 일단 세팅 끝.
$ gcloud container clusters get-credentials my-gke-cluster --region asia-northeast3
Fetching cluster endpoint and auth data.
kubeconfig entry generated for my-gke-cluster.
3개 워커 노드를 가진 cluster를 만들자.
$ gcloud container clusters create kubia --num-nodes 3 --machine-type e2-micro
...
NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS
kubia asia-northeast1-a 1.31.6-gke.1020000 34.84.83.154 e2-micro 1.31.6-gke.1020000 3 RUNNING
—num-nodes로 워커 노드의 개수를 지정할 수 있다. —machine-type으로 원래는 f1-micro 를 했는데, 메모리가 너무 작대서 e2-micro (RAM 1GB)로 설정하였다.

3 node cluster와 소통하는 방법을 그림으로 보자. 유저는 kubectl command line으로 클러스터에게 명령어를 날리면, kubectl이 REST requests를 마스터 노드의 k8s API 서버로 보낸다. 각 워커노드에는 Docker, kubelet, kube-proxy를 가진다.
클러스터 노드들을 리스팅해보자.
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-kubia-default-pool-236147ea-3ns1 Ready <none> 3m28s v1.31.6-gke.1020000
gke-kubia-default-pool-236147ea-r5lz Ready <none> 3m28s v1.31.6-gke.1020000
gke-kubia-default-pool-236147ea-xrft Ready <none> 3m28s v1.31.6-gke.1020000
특정 노드의 자세한 정보를 보고 싶다면
$ kubectl describe node gke-kubia-default-pool-236147ea-3ns1
이쯤되면 매번 kubectl을 쳐야 하는게 슬슬 짜증이 난다. 그러므로 우린 alias와 command-line completion을 설정하려 한다.
$ echo "alias k=kubectl" >> ~/.bashrc
$ source ~/.bashrc k get nodes 명령어를 치면 정상 작동하는 것을 확인할 수 있다.$ source <(kubectl completion bash | sed 's/kubectl/k/g')질문: kubectl get contaonters 명령어는 유효한가?
그렇지 않다. kubectl은 개별 컨테이너를 리스팅하지 못한다. 컨테이너들은 pod에 담겨 격리되어 있기 때문이다. Pod은 하나 이상의 긴밀히 연관된 컨테이너들의 그룹이다. 그것들은 언제나 함께 실행되고 각 pod은 분리된 logical machine로 생각할 수 있다. 각 팟은 각자의 IP, hostname, process를 가진다. 같은 팟에서 돌아가는 컨테이너는 마치 하나의 머신에서 돌아가는 것처럼 보이며 다른 팟에 있는 컨테이너와는 분리된 것 처럼 보인다. 같은 워커 노드에 있더라도!
그래서 pod을 리스팅하는 건 가능하다.

k8s에서 컨테이너를 실행하면 무슨 일이 벌어지는지 자세히 알아보자.

pod의 IP 주소는 internal이다. 외부에서 접속하려면 이를 노출시켜야 한다. 여기서 LoadBalancer가 필요하다. LoadBalancer는 외부(인터넷)에서 클러스터 내부의 애플리케이션에 접근할 수 있게 해주는 서비스다. 로드 발란서의 public IP를 통해 각 pod에 접근할 수 있다.
pod은 변동이 잦다. 삭제될 수도 있고, node에서 방출될 수도 있다. 이렇게 pod의 ip는 상당히 가변적이기에 클라이언트가 접속을 하려면 하나의 고정적인 IP가 필요하다. 그 역할을 LoadBalancer가 해 주는 것이다.
원래 책에는 —generator 옵션으로 Replication Controller을 만드는데, 최신 버전에는 그게 사라진 모양이다. 그래서 그냥 Deployment로 생성하였다.
# pod 3개 생성
$ k create deployment kubia --image=anandashin/kubia --replicas=3
# 외부로 포트 포워딩 - Loadbalancer 이용
# --port : 사용자가 접근할 서비스 포트
# --target-port : 컨테이너가 사용하는 포트
$ k expose deployment kubia --type=LoadBalancer --target-port=8080 --port=80
$ k get pod
NAME READY STATUS RESTARTS AGE
kubia-645987d6c5-96j4d 1/1 Running 0 2m31s
kubia-645987d6c5-lr5vl 1/1 Running 0 2m8s
kubia-645987d6c5-tn8qg 1/1 Running 0 3m
$ k get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 34.118.224.1 <none> 443/TCP 147m
kubia LoadBalancer 34.118.228.183 34.84.80.7 80:32047/TCP 7m59s
# hit different pods randomly
$ curl 34.84.80.7:80
You've hit kubia-645987d6c5-lr5vl
$ curl 34.84.80.7:80
You've hit kubia-645987d6c5-trl62
$ curl 34.84.80.7:80
You've hit kubia-645987d6c5-trl62
$ curl 34.84.80.7:80
You've hit kubia-645987d6c5-lr5vl
$ curl 34.84.80.7:80
You've hit kubia-645987d6c5-lr5vl
# pod scaling (개수 조절) 가능
$ k scale deployment kubia --replicas=1
# pod의 IP 확인 가능
$ k get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kubia-645987d6c5-lr5vl 1/1 Running 0 8m30s 10.60.0.8 gke-kubia-default-pool-236147ea-xrft <none> <none>
kubia-645987d6c5-trl62 1/1 Running 0 2m17s 10.60.1.8 gke-kubia-default-pool-236147ea-3ns1 <none> <none>
kubia-645987d6c5-v2wdk 1/1 Running 0 2m17s 10.60.2.7 gke-kubia-default-pool-236147ea-r5lz <none> <none>
ECR (Elastic Container Registry) : 어디서나 애플리케이션 이미지 및 아티팩트를 안정적으로 배포할 수 있도록 뛰어난 성능 호스팅을 제공하는 완전관리형 컨테이너 레지스트리.
AWS에서 제공하는 도커 이미지 레지스트리다. 다음 특징들이 있다.
AWS 서비스다 보니 관련 서비스와 연동이 쉽고 IAM 정책으로 권환 부여를 세밀하게 조정할 수 있다. Docker hub는 퍼블릭한 반면, ECR은 프라이빗 설정이 가능해 보안적 측면에서 우수하다. 이미 많은 AWS 서비스를 쓰고 있다면, 외부 서비스인 Docker Hub보다 ECR이 통합적 측면에서 더 좋은 선택일 수 있다.
ECR 레포지토리를 프라이빗으로 만들면 image push 할 때 AWS credential 설정이 필요하다. AWS CLI를 이용한다면 로컬에서 다음을 설정해야 한다.
AWS Access Key ID [None]: 액세스 키를 입력한다. (IAM 생성 때 발급)
AWS Secret Access Key [None]: 시크릿 키를 입력한다. (IAM 생성 때 발급)
Default Region name [None]: Seoul 리전을 뜻하는 ap-northeast-2를 입력한다.
Default output format [None]: 비워둔다.(json, text, table을 사용할 수 있다.)
ECR 접근 인증을 위해서 도커 로그인을 한다.
aws ecr get-login-password \
| docker login \
--username AWS \
--password-stdin 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
아래와 같이 이미지 태깅을 하고
# docker tag ${위에서 생성한 도커 이미지 이름} ${ecr url} / ${ecr repository 이름}:${버전 정보}
docker tag myapp:latest 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest
Push한다.
# docker push ${ecr url} / ${ecr repository 이름} : ${버전 정보}
docker push 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest
만약 EC2에서 ECR에 있는 이미지를 받아오고 싶다면 일단 ec2 인스턴스의 IAM 역할을 연결한다. 그리고 로그인 한다.
aws ecr get-login-password --region ap-northeast-2 \
| docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
이제 이미지를 pull 하면 된다.
docker pull 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest
자료출처: Kubernetes In Action