無知

갈 길이 먼 공부 일기

기술 공부/쿠버네티스

쿠버네티스 (2) | 도커와 쿠버네티스 튜토리얼

moozii 2022. 2. 6. 11:59
앞으로의 스터디 내용은 <Kubernetes in Action>을 기반으로 진행합니다.
자세한 내용은, 해당 책을 확인해주세요! 
http://www.yes24.com/Product/Goods/89607047 

 

도커를 사용한 컨테이너 이미지 생성, 실행, 공유하기

도커 설치와 Hello World 컨테이너 실행하기

  1. 리눅스 머신에 도커 설치하기
    1. 도커 설치 + 가상머신 생성
    2. 가상 머신 내 도커 데몬 구동
    3. 호스트 OS에서 도커 클라이언트 실행 파일 사용
    4. 가상머신 내 도커 데몬과 도커 클라이언트 간 통신
  2. Busybox 이미지로 Hello World 컨테이너 실행하기
    1. busybox는 표준 UNIX 명령줄 도구를 합쳐놓은 단일 실행파일
    2. 터미널을 통한 실행 > 별도 설치 없이 전체 애플리케이션이 다운로드되어 실행됨.
      $ docker run busybox echo "Hello World"​

 

docker run 명령 동작의 백그라운드 이해하기

  1. $ docker run busybox echo "Hello World" [$ docker run <image>]
    1. 이미지 실행 시 명령 지정하지 않고 이미지 내부에 패키징하여 넣을 수 있다 (원할 경우 오버라이드 가능)
  2. 로컬 머신에 busybox:latest 이미지가 존재하는지 확인
  3. 존재하지 않을 경우 도커 허브 레지스트리에서 이미지 다운로드 (이미지 Pull)
  4. 이미지로부터 컨테이너를 생성
  5. 컨테이너 내부에서 명령어를 실행 (echo “Hello World”)

 

컨테이너 이미지 버전 지정하기

도커 이미지는 동일한 이름의 여러 개의 버전 생성이 가능하다.
각 버전은 고유한 태그를 가져야 한다.
이미지 참조 시 명시적으로 태그를 지정하지 않을 경우 latest로 간주한다.

$ docker run <image>:<tag>

 

애플리케이션 생성부터 이미지 빌드, 실행까지

  1. Node.js 웹 애플리케이션 만들기 (app.js)
    const http = require('http');
    const os = require('os');
    const fs = require('fs');
    
    const version = "0.1";
    const listenPort = 8080;
    
    function sendResponse(status, contentType, encoding, body, response) {
        response.writeHead(status, {'Content-Type': contentType});
        response.write(body, encoding);
        response.end();
    }
    
    function renderFile(req, res, path, contentType) {
        let template = fs.readFileSync(path, 'utf8');
    
        let map = Object.assign({
            "{{hostname}}": os.hostname(),
            "{{clientIP}}": req.connection.remoteAddress,
            "{{version}}": version,
        });
    
        let body = template.replace(
            new RegExp(Object.keys(map).join('|'), 'g'),
            function (matched) {
                return map[matched];
            });
    
        sendResponse(200, contentType, 'utf8', body, res);
    }
    
    function sendFile(req, res, path, contentType) {
        let body = fs.readFileSync(path, 'binary');
        sendResponse(200, contentType, 'binary', body, res);
    }
    
    // this function guesses if the client that sent the request is a full-fledged
    // graphical web browser and not a text-based tool such as curl
    // graphical browsers typically send accept: text/html,application/xhtml+xml,...
    // curl sends accept: */*
    function isGraphicalWebBrowser(req) {
        let accept = req.headers.accept || "*/*";
        return accept.startsWith("text/html");
    }
    
    function handler(req, res) {
        let clientIP = req.connection.remoteAddress;
        console.log("Received request for " + req.url + " from " + clientIP);
        switch (req.url) {
            case '/':
                if (isGraphicalWebBrowser(req)) {
                    res.writeHead(302, {"Location": "html"});
                    res.write("Redirecting to the html version...");
                    res.end();
                    return;
                }
            // text-based clients fall through to the '/text' case
            case '/text':
                return renderFile(req, res, "html/index.txt", "text/plain");
            case '/html':
                return renderFile(req, res, "html/index.html", "text/html");
            case '/stylesheet.css':
                return sendFile(req, res, "html/stylesheet.css", "text/css");
            case '/javascript.js':
                return sendFile(req, res, "html/javascript.js", "text/javascript");
            case '/favicon.ico':
                return sendFile(req, res, "html/favicon.ico", "image/x-icon");
            case '/cover.png':
                return sendFile(req, res, "html/cover.png", "image/png");
            default:
                return sendResponse(404, "text/plain", "utf8", req.url + " not found", res)
        }
    }
    
    console.log("Kiada - Kubernetes in Action Demo Application");
    console.log("---------------------------------------------");
    console.log("Kiada " + version + " starting...");
    console.log("Local hostname is " + os.hostname());
    console.log("Listening on port " + listenPort);
    
    let server = http.createServer(handler);
    server.listen(listenPort);
  2. 컨테이너 이미지로 패키징하기
    1. 도커파일 생성하기 (app.js와 동일한 디렉터리)
      FROM node:16
      COPY app.js /app.js
      COPY html/ /html
      ENTRYPOINT ["node", "app.js"]
    2. 컨테이너 이미지 빌드 >> 로컬에 이미지 저장
      $ docker build -t kubia .
  3. 컨테이너 이미지 실행
    $ docker run --name kubia-container -p 8080:8080 -d kubia
  4. 애플리케이션 접근하기
    $ curl localhost:8080
    1. 데몬을 가상 머신 내부에서 실행 중인 경우 (맥/윈도우)
    2. 로컬 머신에서 도커 데몬이 실행 중이지 않으므로, localhost 대신 데몬이 실행 중인 가상 머신의 호스트 이름이나 IP를 사용해야 한다. DOCKER_HOST 환경변수로 확인 가능하다.
  5. 실행 중인 모든 컨테이너 조회하기
    $ docker ps
  6. 컨테이너 추가 정보 얻기 (컨테이너 상세 정보를 JSON 형태로 출력)
    $ docker inspect kubia-container

 

실습 가이드로 생성한 웹 애플리케이션 설명

HTTP 요청을 받으면 실행 중인 머신의 호스트 이름을 응답으로 반환하는 웹애플리케이션이다.

도커를 통해 올린 프로세스는, 호스트 머신에서 실행되고 있지만,
호스트 머신의 호스트 이름이 아니라, 격리된 컨테이너 내부의 호스트 이름을 바라본다는 것을 확인할 수 있다.

프로세스가 올라간 16진수 도커 컨테이너의 ID가 응답으로 돌아온다.

 

애플리케이션을 이미지로 패키징하기

이미지로 패키징하기 위해 먼저 도커 파일을 생성해야 한다.

도커 파일은, 도커가 이미지를 생성하기 위해 수행해야 할 지시사항이 담겨 있다.

FROM node:16   # 이미지 생성의 기반이 되는 기본 이미지로 사용할 이미지를 정의
COPY app.js /app.js   # 로컬 디렉터리 app.js 파일을 이미지의 루트 디렉터리 내 동일한 이름으로 추가하기
(중략)
ENTRYPOINT ["node", "app.js"]   # 이미지 실행 시 수행되어야 할 명령어 정의
더보기
더보기

기본 이미지로 지정된 node:16은?

node 컨테이너 이미지의 태그 16.
Node.js 애플리케이션 실행 시 필요한 node 바이너리 실행파일을 포함한 이미지.

 

이미지 빌드 이해하기

$ docker build -t kubia .

현재 디렉터리의 콘텐츠를 기반으로 kubia라는 이미지를 빌드하라고 요청했다.
요청을 받은 도커는 도커파일의 지시 사항을 기반으로 이미지를 빌드한다.
빌드된 이미지는 로컬에 저장된다.

  1. 도커 클라이언트가, 빌드 디렉터리의 콘텐츠를 도커 데몬에 업로드한다 (도커 파일과 애플리케이션 코드)
  2. 도커파일에 명시된 이미지가 아직 로컬에 저장되어 있지 않은 경우 레포지토리에서 이미지를 풀한다
  3. 준비된 기본 이미지를 기반으로 새로운 이미지를 도커 데몬 상에 빌드한다

도커파일을 통해 이미지 빌드를 진행하면, 반복 가능하고, 자동화가 가능하다는 장점이 있다.

그에 반해 수동 빌드의 경우에는, 기존 이미지에서 컨테이너를 실행하고, 컨테이너 내부에서 명령어를 수행한 후 최종 상태를 새로운 이미지로 커밋하여 진행할 수 있으나, 명령어를 수동으로 입력해야 한다는 단점이 있다.

 

이미지 레이어 이해하기

  • 이미지는 여러 개의 레이어로 구성된다.
  • 서로 다른 이미지가 여러 개의 레이어를 공유할 수 있다.
  • 이미지를 가져올 때 도커는 각 레이어를 개별적으로 다운로드한다.
  • 도커 파일은 이미지 빌드 시 여러 개의 새로운 레이어를 추가한다.
    • app.js 파일을 추가하는 레이어를 생성한다.
    • 이미지 실행 시 수행돼야 할 명령을 지정하는 또다른 레이어를 추가한다.
      • kubia:latest라는 태그를 지정하는 것을 마지막으로 포함한다.

 

이미지 실행 명령어 이해하기

$ docker run --name kubia-container -p 8080:8080 -d kubia

--name kubia-container kubia-container라는 이름의 새로운 컨테이너를 실행한다.

-d 컨테이너는 콘솔에서 분리되어 백그라운드로 실행된다.

-p 8080:8080 로컬 머신의 8080 포트가 컨테이너 내부의 8080 포트와 매핑된다.
이를 통해 localhost:8080으로 애플리케이션 접근이 가능하다.

데몬을 가상 머신 내부에서 실행 중인 경우 (맥/윈도우)

로컬 머신에서 도커 데몬이 실행 중이지 않으므로, localhost 대신 데몬이 실행 중인 가상 머신의 호스트 이름이나 IP를 사용해야 한다. DOCKER_HOST 환경변수로 확인 가능하다.

 

 

실행 중인 컨테이너 내부 탐색하기

실행 중인 컨테이너 내부에서 셸 실행하기

$ docker exec -it kubia-container bash

kubia-container 내부에서 bash 셸을 실행한다.
컨테이너의 메인 프로세스와 동일한 리눅스 네임스페이스를 가진다 (즉, 컨테이너 내부를 탐색할 수 있다).

-it 옵션 :

  • -i 옵션은 STDIN(표준입력)을 오픈 상태로 유지한다는 의미. (셸 명령어 입력에 필요)
  • -t 옵션은 pseudo TTY (의사 터미널)을 할당한다는 의미. (명령어 프롬프트 표시)

 

내부에서 컨테이너 탐색하기

앞에서 생성할 셸을 통해 컨테이너 내부 프로세스, 파일 시스템 조회하기

root@'ContainerID':/# ps aux
root@'ContainerID':/# ls

호스트 운영체제 상에서 컨테이너 프로세스 조회하기
(도커 데몬이 실행 중인 가상머신에 로그인해 조회)

$ ps aux | grep app.js

컨테이너 내부 프로세스 ID와 호스트 운영체제 상 프로세스 ID가 다른 이유

컨테이너는 자체 리눅스 PID 네임스페이스를 사용하며, 고유의 시퀀스 번호를 가지고 완전히 분리된 트리를 가진다.

 

 

컨테이너 중지 및 삭제

$ docker stop kubia-container   [컨테이너 중지(실행 중인 메인 프로세스 중지)]
$ docker rm kubia-container     [컨테이너 완전 삭제]

 

 

 

2.1.8 이미지 레지스트리 이미지 푸시

외부 이미지 저장소 푸시 작업. 도커 허브 규칙에 따라야 한다.

도커 허브의 규칙에 따른 이미지 태그 지정법 : 도커 허브는 이미지 리포지터리 이름이 도커 허브 ID로 시작해야 한다

$ docker tag kubia 'dockerhubID'/kubia  [도커 이미지에 도커 허브 규칙에 맞는 태그 추가]
$ docker images | head                  [시스템에 저장된 이미지를 조회해 추가된 태그 확인]
$ docker login                          [도커 로그인]
$ docker push 'dockerhubID'/kubia       [도커 이미지 푸시]

이를 통해 다른 머신에서도 이미지를 실행할 수 있게 되었다.

 

 

@RedHat&nbsp;https://developers.redhat.com/blog/2020/11/20/kubectl-developer-tips-for-the-kubernetes-command-line

 

쿠버네티스 클러스터 설치

설치 옵션 - 추가적인 설명은 생략합니다

  1. 로컬 머신에 단일 노드 쿠버네티스 클러스터 실행하기
  2. 구글 쿠버네티스 엔진에 실행중인 클러스터 접근하기
  3. kubeadm 도구 사용해 설치하기 [부록]
  4. AWS에 설치하기 [kops]

 

클러스터의 개념 이해하기

  1. 개발자가 로컬 머신에서 kubectl 클라이언트 명령어를 입력한다
  2. 마스터 노드의 쿠버네티스 API 서버로 REST 요청을 보낸다
  3. 요청을 받은 마스터 노드 API 서버는 클러스터와 상호작용한다

클러스터 노드 조회해서 동작 상태 확인하기

$ kubectl get nodes

오브젝트 세부 정보 가져오기

$ kubectl describe node 'node-name'

 

 

kubectl의 alias와 명령줄 자동완성 설정하기

별칭 설정하기

alias k=kubectl

명령줄 자동완성 설정하기

https://kubernetes.io/ko/docs/tasks/tools/included/optional-kubectl-configs-zsh/

책의 설명은 bash 중심이라 쿠버네티스 문서 가이드 링크로 대체함.

 

 

쿠버네티스로 애플리케이션 실행하기

Node.js 애플리케이션 구동하기

kubectl run 명령어를 사용해 매니페스트 없이 간단히 구성 요소 생성하기

$ kubectl run kubia --image=luksa/kubia --port=8080 --generator=run/v1

--image=luksa/kubia : 실행하고자 하는 컨테이너 이미지를 명시

--port=8080 : 애플리케이션이 8080 포트를 수신 대기해야 함

--generator=run/v1 : 쿠버네티스에서 디플로이먼트 대신 레플리케이션 컨트롤러를 생성하기 때문에 붙은 플래그

더보기
더보기

Error: unknown flag: --generator

  • kubectl run은 파드 생성과 관련이 없는 플래그와 함께, 이전에 사용 중단된 생성기(generator)를 삭제했다. kubectl run은 이제 파드만 생성한다. 파드 이외의 오브젝트를 만드려면, 특정 kubectl create 하위 커맨드를 참조한다. (#87077, @soltysh) [SIG Architecture, CLI 및 테스트]
    https://kubernetes-docsy-staging.netlify.app/ko/docs/setup/release/notes/
  • https://kubernetes.io/ko/docs/reference/kubectl/conventions/
    모든 kubectl run의 생성기(generator)는 더 이상 사용 할 수 없다. 생성기 목록 및 사용 방법은 쿠버네티스 v1.17 문서를 참고한다.
    • As the author of the problem let me explain a little bit the intention behind this deprecation. Just like Brendan explains in his answer, kubectl run per se is not being deprecated, only all the generators, except for the one that creates a Pod for you.The vast majority of input parameters for kubectl run command is overwhelming for newcomers, as well as for the old timers. It's not that easy to figure out what will be the result of your invocation. You need to take into consideration several passed options as well as the server version.That's why we're trying to move people away from using kubectl run for their daily workflows and convince them that using explicit kubectl create commands is more straightforward. Finally, we want to make the newcomers that played with docker or any other container engine, where they run a container, to have the same experience with Kubernetes where kubectl run will just run a Pod in a cluster. Sorry for the initial confusion and I hope this will clear things up.
      https://stackoverflow.com/questions/52890718/kubectl-run-is-deprecated-looking-for-alternative
  • 생성기 옵션을 입력하지 않고 kubectl run kubia --image={dockerhubid}/kubia --port=8080 로 실행함
    • “pod/kubia” created.
  • 참고: ReplicaSet을 구성하는 Deployment가 현재 권장하는 레플리케이션 설정 방법이다.

 

 

파드

쿠버네티스는 개별 컨테이너를 직접 다루는 대신,
“함께 배치된 다수의 컨테이너”라는 개념의 컨테이너 그룹, 파드를 사용한다.

파드 = 하나 이상의 밀접하게 연관된 컨테이너 그룹. 같은 워커 노드, 같은 리눅스 네임스페이스에서 실행됨.

각 파드는 자체 IP, 호스트 이름, 프로세스 등이 있는, 논리적으로 분리된 머신이다.
즉, 같은 워커 노드에 있더라도 다른 파드에서 실행 중이라면, 다른 머신에서 실행 중인 것으로 나타난다.

각 파드는 고유한 IP와 애플리케이션 프로세스를 실행하는 하나 이상의 컨테이너를 가진다.

 

 

파드 조회하기

$ kubectl get pods
NAME         READY   STATUS    RESTARTS  AGE
kubia-4jfyf  0/1     Pending   0         1m

$ kubectl get pods
NAME         READY   STATUS    RESTARTS  AGE
kubia-4jfyf  1/1     Running   0         5m

READY 0/1, Pending의 의미

워커노드가 아직 컨테이너 이미지를 다운로드 하는 중이므로 컨테이너를 실행하기 전임.
Pending 상태가 지속된다면 이미지를 가져오는 데에 문제가 있는 것일 수 있으니 Public 이미지인지 확인해보고 다른 머신으로 docker pull을 통해 이미지 풀링을 시도하는 것을 권함.

 

백그라운드 이해하기

  1. 로컬 개발 머신을 통해 이미지를 빌드 해 도커 허브에 푸시
    docker push luksa/kubia
    로컬 빌드 이미지는 로컬에서만 사용 가능므로 도커 데몬이 있는 다른 워커 노드에서 사용하기 위해 허브에 올려야 한다
  2. kubectl 명령어를 실행
    kubectl run kubia --image=luksa/kubia --port=8080
  3. 로컬 개발 머신의 kubectl이 컨트롤플레인 쿠버네티스 API 서버로 REST HTTP 요청을 전달 (REST 호출)
  4. 마스터 노드는 레플리케이션컨트롤러 오브젝트를 생성
  5. 레플리케이션컨트롤러는 새로운 파드를 생성
  6. 컨트롤플레인 스케줄러가 워커노드에 할당(스케줄링)
  7. 할당받은 워커노드의 kubelet은 API 서버로부터 정보를 확인하고 도커에 이미지 실행을 지시
  8. 워커노드의 도커는 이미지가 로컬에 없을 확인하고 이미지를 풀함
  9. 이미지 다운로드 후 도커가 컨테이너를 생성하고 실행함

 

웹 애플리케이션에 접근하기

서비스 오브젝트 생성하기

레플리케이션컨트롤러를 노출하도록 명령하면 서비스가 생성된다

$ kubectl expose rc kubia --type=LoadBalancer --name kubia-http

rc = replicationcontroller

minikube의 경우 로드밸런서 서비스 생성이 어려우니, minikube service kubia-http를 이용해보자

  • minikube 상에서 아래 코드로 먼저 시도했는데 15min 넘게 pending 상태가 지속됨. minikube의 경우 로드밸런서 서비스를 지원하지 않아 외부 IP를 얻지 못한다는 설명을 책에서 확인함.
    kubectl expose po kubia --type=LoadBalancer --name kubia-http
    rc 대신 po로 사용함 (레플리케이션 컨트롤러 사용하지 않았기 때문)
  • 즉, 먼저 위의 명령어로 파드의 로드밸런서 형태 서비스를 먼저 생성하여 노출시킨뒤, 외부 IP를 얻기를 대기하는 것이 아니라 minikube명령어를 입력하면 해결된다. 이렇게 하면 서비스 접근이 가능하다.

서비스를 조회하여 생성된 kubia-http 서비스를 확인해보자.

$ kubectl get svc
NAME         CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
kubernetes   10.3.240.1     <none>          443/TCP          35m
kubia-http   10.3.246.185   104.155.74.57   8080::31348/TCP  1m

외부 IP와 포트를 통해 파드에 요청을 보내 접근이 가능함을 확인할 수 있다

$ curl 104.155.74.57:8080

각 파드는 클러스터 내부에 있어 외부 접근이 어렵다.

외부에서 접근하려면 서비스 오브젝트를 통해 노출해야 한다.
(단 일반적인 서비스 오브젝트는 클러스터 내부 접근만 가능하므로 Load Balancer 유형의 특별한 서비스를 생성해야 한다)

 

서비스란 클러스터 IP 서비스

서비스 = 파드 집합에서 실행중인 애플리케이션을 네트워크 서비스로 노출하는 추상화 방법

쿠버네티스를 사용하면 익숙하지 않은 서비스 디스커버리 메커니즘을 사용하기 위해 애플리케이션을 수정할 필요가 없다. 쿠버네티스는 파드에게 고유한 IP 주소와 파드 집합에 대한 단일 DNS 명을 부여하고, 그것들 간에 로드-밸런스를 수행할 수 있다.

https://kubernetes.io/ko/docs/concepts/services-networking/service/

로드밸런서 유형의 서비스 생성 시 외부 로드밸런서가 생성되어 해당 로드밸런서 퍼블릭 IP로 파드에 연결한다

 

 

시스템의 논리적인 구성

레플리케이션컨트롤러, 파드, 서비스의 동작 방식

  1. kubectl 명령어 실행
  2. 레플리케이션 컨트롤러 생성
  3. 레플리케이션 컨트롤러가 파드를 생성
  4. 클러스터 외부의 서비스 노출 명령(요청)
  • 파드 : 고유 IP 주소와 호스트이름을 갖는 1개 이상의 컨테이너 그룹.
    • 파드에 포함된 컨테이너는 프로세스가 있고, 포트에 바인딩되어 요청을 대기 중.
    • 파드는 일시적이다. 실패 / 삭제 / 교체의 가능성을 고려해야 한다. IP주소도 그에 따라 변경될 수 있다.
  • 레플리케이션컨트롤러 : 파드를 복제해 여러 개의 파드 복제본을 만들어 실행 상태로 만든다.
    • 정확히 하나의 파드 인스턴스를 실행하도록 지정한다.
    • 파드의 레플리카가 지정되지 않아 레플리케이션 컨트롤러가 예시에서는 파드를 하나만 생성하였다.
    • 파드가 사라질 경우 새로운 파드를 생성해 대체하는 역할을 수행한다.
  • 서비스 : 동일한 서비스를 제공하는 하나 이상의 파드 그룹의 정적 위치를 나타낸다.
    • 항상 변경되는 파드의 IP 주소 문제를 해결한다
    • 여러 개의 파드를 단일 IP와 포트의 쌍으로 노출시켜준다
    • 변경되지 않는 정적 IP를 할당받아 파드와 클라이언트 사이를 연결한다

 

애플리케이션 수평 확장

파드의 개수, 인스턴스의 수를 늘리는 수평 확장이 가능하다.

쿠버네티스는 파드를 관리하는 레플리케이션컨트롤러의 DESIRED 레플리카 수와 CURRENT 파드 수를 일치시킨다.

레플리케이션 컨트롤러 관련 설명 및 실습은 이론적인 이해만 하고 넘어가도록 한다. 현재 버전에 맞는 deployment 의 경우 책의 뒷부분에 설명이 포함되는 것으로 확인된다.

 

의도하는 레플리카 수 늘리기

$ kubectl scale rc kubia --replicas=3

DESIRED 값을 3으로 증가시켜, 쿠버네티스가 알아서 원하는 인스턴스 수 설정 값에 맞게 액션을 스스로 수행한다.

스케일링이 간단하지만, 애플리케이션 자체에서도 수평 확장을 지원하도록 만들어야 한다.

이렇게 수평 확장되어 늘어난 파드는, 하나의 서비스로 묶여있으므로,
서비스 URL을 호출하면 무작위하게 실행 중인 파드들 중 하나가 호출된다.
서비스는 로드밸런서 역할을 수행하여 연결해준다.

 

 

애플리케이션 속 노드 검사하기

-o wide 옵션을 활용해 get pods 를 입력하면 추가 열을 요청하여 노드가 표시된다.

$ kubectl get pods -o wide

pod에 대한 describe 명령어로도 파드 상세 정보 확인을 통해 노드를 조회할 수 있다.

$ kubectl describe pod kubia-hzcji

 

쿠버네티스 대시보드 소개

쿠버네티스 대시보드는, GUI 형태 웹 대시보드를 제공한다.

파드, 레플리케이션컨트롤러, 서비스 등 클러스터 오브젝트의 조회 / 생성 / 수정 / 삭제가 가능하다.

Minikube로 대시보드 접근하기

$ minikube dashboard