DevOps/Kubernetes

[구글 클라우드 스터디 잼] Docker 소개

  • -
728x90

해당 글은 https://www.cloudskillsboost.google/quests/29Docker 소개 실습을 완료하고 작성하는 글입니다.

Intro: 도커에 대해

 

Docker는 애플리케이션을 개발, 출시, 실행하는 데 사용하는 개방형 플랫폼입니다. Docker를 사용하면 인프라에서 애플리케이션을 분리하고 인프라를 관리형 애플리케이션처럼 취급할 수 있습니다. Docker는 코드를 더욱 빠르게 출시, 테스트, 배포하고 코드 작성과 실행 주기를 단축하는 데 도움이 됩니다.

 

이는 Docker가 커널 컨테이너화 기능을 애플리케이션 관리 및 배포를 지원하는 워크플로 및 도구와 결합하기 때문입니다.
Docker 컨테이너는 Kubernetes에서 직접 사용할 수 있으므로 Kubernetes Engine에서 쉽게 실행할 수 있습니다. Docker의 핵심 내용을 학습하면 Kubernetes 및 컨테이너 애플리케이션 개발을 시작하는 데 필요한 기술을 갖출 수 있습니다.

 

위 설명은 학습 페이지에서 소개하는 Docker의 정의이다. 하지만 실제 docker를 이해하기에 위 설명은 충분치 못한 것 같다.

 

보통 도커를 설명할 때에는 VM(가상 머신) 과 도커를 비교하여 설명하는 글이 가장 많은 것 같다. 가장 확실하고 직관적으로 이해하기 쉬워서인것 같다. 그럼, 실제로 VM과 도커를 비교한 아래 그림을 통해 알아보자.

VM vs Docker Container 출처(https://seosh817.tistory.com/345)

VM을 통한 가상화의 경우 Host OS와 완전히 분리된다는 장점을 가지지만, OS 위 OS를 올리는 형태로(Host OS - Guest OS), 무겁고 느리게 된다. 또한 하이퍼바이저를 이용해서 여러 운영체제를 하나의 호스트에서 생성하는 방식이기 떄문에 일반 호스트에 비해 성능의 손실이 발생한다. 더불어 게스트 운영체제를 사용하기 위한 라이브러리 커널 등을 포함하여 배포를 위한 이미지로 만들었을 때 이미지의 크기가 커진다.

 

반면, 컨테이너 기반 가상화는프로세스 단위의 격리 환경을 만들기 때문에 성능 손실이 거의 없다. 컨테이너에 필요한 커널을 공유해서 사용하고, 컨테이너 안에는 어플리케이션을 구동하는 데 필요한 라이브러리 및 실행 파일만 존재하기 때문에 컨테이너를 이미지로 만들었을 때 이미지의 용량도 작다. Host OS(그림에서 Operating System 부분) - Docker 엔진위에서 바로 동작하며 Host의 커널을 공유하기 떄문에 io 처리가 쉽게 되어 가상화된 공간을 사용할 때의 성능 손실도 거의 없다는 장점이 있다.

하지만 Docker가상화가 VM보다 뛰어나다는 것은 아니다.

 

OS가상화는 컨테이너기반 가상화보다 더 높은 격리 레벨을 지원한다. 보안 측면에서 더 뛰어난 것이며, OS가상화의 커널을 공유하지 않는 장점 또한 있다. 커널을 공유하지 않는 만큼 멀티 OS가 가능하다는 의미이다. (멀티 OS의 예: Linux위에 Window를 올리는 것)

그럼에도 Docker를 쓰는 이유는 성능향상, 뛰어난 이식성 등을 꼽을 수 있을 것이다.

Ref.

https://seosh817.tistory.com/345

https://khj93.tistory.com/entry/Docker-Docker-%EA%B0%9C%EB%85%90

실습

시작하려면 Cloud Shell에서

docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
9db2ca6ccae0: Pull complete
Digest: sha256:4b8ff392a12ed9ea17784bd3c9a8b1fa3299cac44aca35a85c90c5e3c7afacdc
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
...

이 명령어는 Docker 데몬이 hello-world 이미지를 검색했으나 로컬에서 이미지를 찾지 못했고, Docker Hub라는 공개 레지스트리에서 이미지를 가져오고, 가져온 이미지에서 컨테이너를 생성하고, 컨테이너를 실행하는 과정을 보여준다.

이제 Docker Hub에서 가져온 컨테이너 이미지를 확인할 수 있다.

REPOSITORY     TAG      IMAGE ID       CREATED       SIZE
hello-world   latest    feb5d9fea6a5   14 months ago   13.3kB

도커의 이미지는 도커 허브에서 오며, 이미지의 id는 SHA256 해시 형식으로되어 있어, 도커 허브에서 이 값을 가지고 이미지를 풀 받을 수도 있다. Docker 데몬이 로컬에서 이미지를 찾을 수 없으면 기본적으로 공개 레지스트리에서 이미지를 검색할 수도 있다.

 

또, 이미지는 레이러들로 구성되어, 이미지를 풀 받을때에는 독립적으로 저장된다. 명령어를 다시 실행했을때는 도커 허브에서 받아온 이미지가 이미 존재하기 때문에 빠르게 출력되는 모습도 확인할 수 있다.

 

도커 이미지와 관련된 더 자세한 내용은 아래 글에서 살펴볼 수 있다.

도커 이미지는 어디에서 오나요?: 도커 허브(Docker Hub)

 

docker ps는 현재 실행 중인 컨테이너 목록만 출력하기 떄문에 종료된 hello-world 컨테이너를 확인 할 수 없다. 실행이 완료된 컨테이너를 포함하여 모든 컨테이너를 보려면 docker ps -a를 사용해야 한다.

docker ps # 현재 실행 중인 컨테이너만 출력
docker ps -a # 실행이 완료된 컨테이너까지 전체 출력

이전에 학교에서 리눅스를 사용할 때 ps 명령어를 통해 프로세스를 확인했었다. 근데 왜 컨테이너를 생성하는 docker에서도 ps를 사용하는지 궁금하여 구글링해보니 Docker 컨테이너가 바로 프로세스라고 한다. 더 자세한 내용은 아래 링크를 참조하자.

도커 컨테이너는 가상머신인가요? 프로세스인가요?

 

아래 링크는 Docker가 컨테이너를 만들때 사용하는 프로세스의 개념을 쉽게 정리했으니, 더 자세한 프로세스 작동 과정이 궁금하다면 참고하지 좋을 것 같다.

Docker? 그 이전에 Process가 먼저

 

이제 만들어진 이미지가 아닌, Docker 이미지를 직접 빌드해보자. 다음 명령어를 터미널에서 실행하면 test라는 폴더를 만들고, 그 안에 Dockerfile를 생성할 수 있다.

# 1. test라는 이름의 폴더를 만들고 이 폴더로 전환합니다.
mkdir test && cd test

# 2. Dockerfile 만들기
cat > Dockerfile <<EOF
# Use an official Node runtime as the parent image
FROM node:lts
# Set the working directory in the container to /app
WORKDIR /app
# Copy the current directory contents into the container at /app
ADD . /app
# Make the container's port 80 available to the outside world
EXPOSE 80
# Run app.js using node when the container launches
CMD ["node", "app.js"]
EOF

이때 Dockerfile은 Docker 데몬에 이미지를 빌드하는 방법을 안내한다. 각 줄은 아래의 내용을 담고 있다.

  • FROM node:lts는 기본 상위 이미지를 지정한다. 이 경우 기본 상위 이미지는 노드 버전 장기적 지원(LTS)의 공식 Docker 이미지이다.
  • WORKDIR /app에서 컨테이너의 작업 디렉터리 위치를 설정한다.
  • ADD . /app에서는 현재 디렉터리의 콘텐츠("."는 현재 디렉터리를 의미)를 컨테이너에 추가한다.
  • EXPOSE 80에서는 컨테이너의 포트를 공개하여 해당 포트에서의 연결을 허용한다.
  • 마지막으로 CMD ["node", "app.js"]를 통해 터미널에서 노드 명령어를 실행하여 애플리케이션을 시작한다.

 

이제 노드 애플리케이션을 작성한 다음 이미지를 빌드해 보아야 한다. 다음 명령어를 실행하여 노드 애플리케이션을 생성할 수 있다.

cat > app.js <<EOF
const http = require('http');
const hostname = '0.0.0.0';
const port = 80;
const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello World\n');
});
server.listen(port, hostname, () => {
    console.log('Server running at http://%s:%s/', hostname, port);
});
process.on('SIGINT', function() {
    console.log('Caught interrupt signal and will exit');
    process.exit();
});
EOF

해당 코드는 포트 80에서 수신 대기하고 'Hello World'를 반환하는 간단한 HTTP 서버이다. 이제 이미지를 빌드하는데, 다음 명령어를 반드시 Dockerfile이 있는 디렉터리에서 시행해야 한다. 현재 디렉터리를 의미하는 .를 꼭 유의하며 명령어를 작성한다.

docker build -t node-app:0.1 .

-t는 name:tag 문법을 사용하여 이미지의 이름과 태그를 생성해주는 역할을 한다. 즉, 이미지의 이름은 node-app이 되고, 태그는 0.1이 된다(Docker 이미지를 빌드할 때는 태그를 사용하는 것이 좋다고 한다. ).

 

태그를 지정하지 않으면 태그가 기본값인 latest로 지정되어 최신 이미지와 기존 이미지를 구분하기 어려워진다. 이미지를 빌드할 때 위 Dockerfile의 각 행을 통해 중간 컨테이너 레이어가 만들어지는 방식을 확인하자.

 

이제, 아래 명령어를 통해 빌드한 이미지를 확인해볼 수 있다.

다음과 유사한 결과가 출력된다. (캡처해둔 것이 없어, 실습 페이지의 내용을 그대로 가져왔다.)

REPOSITORY     TAG      IMAGE ID        CREATED            SIZE
node-app       0.1      f166cd2a9f10    25 seconds ago     656.2 MB
node           lts      5a767079e3df    15 hours ago       656.2 MB
hello-world    latest   1815c82652c0    6 days ago         1.84 kB

node는 기본 이미지이고 node-app은 빌드한 이미지이다. node를 삭제하려면 우선 node-app을 삭제해야 하며, 이미지의 크기는 VM에 비해 상대적으로 작다. node:slim 및 node:alpine과 같은 노드 이미지의 다른 버전을 사용하면 더 작은 이미지를 제공하여 이식성을 높일 수 있다.

 

실행 작업에서는 아래의 명령어를 통해 빌드한 이미지를 기반으로 하는 컨테이너를 실행해 보자.

docker run -p 4000:80 --name my-app node-app:0.1

이때 각 명령어의 뜻은 아래와 같다.

  • --name: 원하는 컨테이너 이름을 지정한다.
  • -p: Docker가 컨테이너의 포트 80에 호스트의 포트 4000을 매핑하도록 지시하는 플래그이다. 이제 http://localhost:4000에서 서버에 접속할 수 있다. 포트 매핑이 없으면 localhost에서 컨테이너에 접속할 수 없다.

다른 터미널을 열고 (Cloud Shell에서 + 아이콘을 클릭) 서버를 테스트한다

curl http://localhost:4000

정상적으로 실행된다면, 아래처럼 출력된다.

Hello World

터미널을 두 개 열어서 진행하는 것이 번거롭다면, 컨테이너를 터미널 세션에 종속시키지 않고 백그라운드에서 실행해야 한다. 이때는 -d 플래그를 지정해야 한다. 초기 터미널을 닫고, 다음 명령어를 실행하여 컨테이너를 중지하고 삭제한 후 백그라운드에서 컨테이너를 시작한다.

# 컨테이너를 중지하고 삭제
docker stop my-app && docker rm my-app

# 백그라운드에서 컨테이너 시작
docker run -p 4000:80 --name my-app -d node-app:0.1
docker ps

docker ps의 출력된 결과에서 컨테이너가 실행 중임을 확인할 수 있다.

 

이제 앞서 만들었던 어플리케이션을 수정하자. 앞서 실습에서 만든 테스트 디렉터리를 열고, 원하는 텍스트 편집기(예: nano 또는 vim)로 app.js 파일을 연 후 'Hello World'를 다른 문자열로 바꾸면 된다.

# 디렉토리 변경
cd test

# 택스트 편집기 실행(vi 기준)
vi app.js

# Hello World -> Welcome to Cloud
....
const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Welcome to Cloud\n');
});
....

이제 이 새로운 어플리케이션을 빌드하고 0.2로 태그를 지정해야 한다.(명령어는 아래와 같다.)

docker build -t node-app:0.2 .

아래 명령어 실행 결과를 보면, 2단계에서는 기존 캐시 레이어를 사용하고 있음을 확인할 수 있지만, 3단계 이후부터는 app.js를 변경했기 때문에 레이어가 수정되었다. (추가적으로, 기존 캐시를 사용했기 때문에 빌드가 비교적 짧게 끝난다.)

Step 1/5 : FROM node:lts
 ---> 67ed1f028e71
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> a39c2d73c807
Step 3/5 : ADD . /app
 ---> a7087887091f
Removing intermediate container 99bc0526ebb0
Step 4/5 : EXPOSE 80
 ---> Running in 7882a1e84596
 ---> 80f5220880d9
Removing intermediate container 7882a1e84596
Step 5/5 : CMD node app.js
 ---> Running in f2646b475210
 ---> 5c3edbac6421
Removing intermediate container f2646b475210
Successfully built 5c3edbac6421
Successfully tagged node-app:0.2

새 이미지 버전으로 다른 컨테이너를 실행한다. 이때 호스트 포트를 80 대신 8080으로 매핑하는 방법을 확인해야 한다. (호스트 포트 4000은 이미 사용 중이므로 사용할 수 없다.)

docker run -p 8080:80 --name my-app-2 -d node-app:0.2
docker ps

마지막으로 컨테이너를 테스트해보자.

curl http://localhost:8080
Welcome to Cloud
curl http://localhost:4000
Hello World

컨테이너 빌드와 실행을 숙지했으니 이제 디버깅 사례를 살펴보자.

  • docker logs \[container\_id\]를 사용하여 컨테이너의 로그를 볼 수 있다. 컨테이너가 실행 중일 때 로그 출력을 확인하려면 -f 옵션을 사용한다. 실행 중인 컨테이너 안에 직접 들어가 어플리케이션의 로그를 확인할 수도 있다. 실행중인 컨테이너에서 대화형 Bash 세션 을 시작해야 하는 경우, docker exec를 사용한다. 이때 -it 플래그는 pseudo-tty를 할당하고 stdin을 열린 상태로 유지하여 컨테이너와 표준입출력으로 상호작용할 수 있도록 한다. 세션을 나가고 싶을 때는 exit을 입력하면 된다.마지막으로, docker inspect를 통해 Docker에서 컨테이너의 메타데이터를 검토할 수 있다.

디버깅에 대한 자세한 내용은 다음 Docker 문서 리소스를 확인하자

 

다음으로 게시 단계이다. 이제 이미지를 Google Artifact Registry로 푸시하자. 그런 다음 모든 컨테이너와 이미지를 삭제하여 새로운 환경을 시뮬레이션하고 컨테이너를 가져와서 실행한다.

 

Artifact Registry에서 호스팅하는 비공개 레지스트리에 이미지를 푸시하려면 이미지에 레지스트리 이름으로 태그를 지정해야 한다. 형식은 -docker.pkg.dev/my-project/my-repo/my-image이다.

 

대상 Docker 저장소를 만들어보자. 이미지를 푸시하려면 먼저 저장소를 만들어야 한다(당연하다, 이미지가 저장되려면 저장소가 먼저 있어야 하니까). Aritifact Registry는 직접 만들어주어야 한다. 다음 절차를 거쳐서 저장소를 만들어주면 된다.

  1. 탐색 메뉴의 CI/CD에서 Artifact Registry > 저장소로 이동
  2. 저장소 만들기 클릭
  3. 저장소 이름으로 my-repository를 지정
  4. 형식으로 Docker를 선택
  5. 위치 유형에서 리전을 선택한 후 us-central1 (Iowa) 위치를 선택
  6. 만들기 클릭

 

다음으로 인증 구성이다. 이미지를 푸시하거나 가져오려면 먼저 Docker가 Artifact Registry에 대한 인증 정보를 가지게 만들어 주어야 한다. us-central1 리전의 Docker 저장소에 인증을 설정하려면 Cloud Shell에서 다음 명령어를 실행하면 된다.

gcloud auth configure-docker us-central1-docker.pkg.dev

 

다음으로 컨테이너를 Artifact Registry로 푸시해보자. 저장소가 생성되었고, 저장소에 접근할 수 있는 인증도 구성하였으니 이미지를 푸시할 수 있다. 먼저 명령어를 실행하여 프로젝트 ID를 설정하고 Dockerfile이 포함된 디렉터리로 변경한다.

export PROJECT_ID=$(gcloud config get-value project)
cd ~/test

0.2 태그를 단 이미지를 빌드한 후, Artifact Registry로 푸시한다.

# 이미지 빌드
docker build -t us-central1-docker.pkg.dev/$PROJECT_ID/my-repository/node-app:0.2 .

# 저장소로 푸시
docker push us-central1-docker.pkg.dev/$PROJECT_ID/my-repository/node-app:0.2

푸시가 완료되면 탐색 메뉴의 CI/CD에서 Artifact Registry > 저장소my-repository에서 `node-app` Docker 컨테이너가 생성된 것을 확인할 수 있다.

다음으로, 이미지를 테스트해보자. 새로운 VM을 시작하고 SSH로 새 VM에 접속한 다음 gcloud를 설치할 수도 있지만, 간단하게 모든 컨테이너와 이미지를 삭제하여 새로운 환경을 시뮬레이션하고, 새로운 컨테이너를 올려서 빌드한 이미지가 잘 작동하는지 테스트한다.

# 모든 컨테이너 중지 후 삭제
docker stop $(docker ps -q)
docker rm $(docker ps -aq)

# 모든 Docker 이미지 삭제
docker rmi node:lts
docker rmi -f $(docker images -aq) # remove remaining images
docker images

이미지를 가져와서 실행해보자. pull을 통해서 이미지를 가져오고, run으로 이미지를 컨테이너로 실행시키고, curl 명령어를 통해 작동을 테스트할 수 있다.

docker pull us-central1-docker.pkg.dev/$PROJECT_ID/my-repository/node-app:0.2
docker run -p 4000:80 -d us-central1-docker.pkg.dev/$PROJECT_ID/my-repository/node-app:0.2
curl http://localhost:4000
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.