개요: Docker란?
Google Cloud Skills Boost의 설명에 따르면, Docker는 다음과 같다.
Docker는 애플리케이션을 개발, 출시, 실행하는 데 사용하는 개방형 플랫폼입니다. Docker를 사용하면 인프라에서 애플리케이션을 분리하고 인프라를 관리형 애플리케이션처럼 취급할 수 있습니다. Docker는 코드를 더욱 빠르게 출시, 테스트, 배포하고 코드 작성과 실행 주기를 단축하는 데 도움이 됩니다. 이는 Docker가 커널 컨테이너화 기능을 애플리케이션 관리 및 배포를 지원하는 워크플로우 및 도구와 결합하기 때문입니다.
위와 같은 일반적인 설명으로는 아직 Docker가 무엇인지 이해가 가지 않는다. Docker에 대해 간단하게 더 알아보자!
Docker에 대해 소개하는 이미지로 위의 그림이 정말 많이 쓰인다. 그림에서 왼쪽은 가상 머신, 오른쪽은 Docker를 사용한 경우를 보여주는 그림인데, 딱 봐도 Docker를 사용했을 때가 더욱 가벼워보인다. 이 둘의 차이는 무엇일까?
가상 머신 vs 도커
전통적인 가상 머신은 새로운 컴퓨터 하나를 기존의 Infrastructure에 설치한다고 생각하면 간단하다. 가상 머신을 현재 사용하고 있는 컴퓨터에 설치하면, 컴퓨터를 위해 필요한 리소스가 전부 할당되어야 한다. 즉, 디스크 용량, 운영체제, 메모리와 CPU 등이 모두 필요하다. 이 리소스는 가상 머신이 설치되고 있는 호스트에서 가상으로 가져온다. 하이퍼바이저(Hypervisior)는 바로 리소스를 가상 머신에 할당해 주기 위해서 리소스를 가상으로 분할하여 마치 2개의 서로 다른 컴퓨터가 있는 것처럼 만들어준다. 정리하자면 가상 머신은 물리적인 호스트-호트스의 운영 체제-하이퍼바이저-게스트의 운영 체제 위에 비로소 어플리케이션이 돌아간다.
이런 식으로 새로운 리소스 및 운영 체제가 모두 필요한 가상 머신은 상당히 무겁다는 단점이 있다. 이 단점을 극복하기 위해 나온 것이 바로 Docker다. 도커는 운영 체제 수준의 가상화 기술을 가지고 컨테이너라는 격리된 공간을 만들고, 그 안에서 어플리케이션을 운영한다. 즉 리눅스 커널만 빌려서 호스트 운영 체제 위에 독립된 공간을 만드는 것이다.
도커에 대해 더 자세하게 다루는 것은 다른 글에서 하도록 하고, 실습으로 넘어가겠다.
작업 1. Hello World
Cloud Shell을 열고 다음 명령어를 입력하면 hello world 컨테이너를 실행할 수 있다. 도커는 docker run [도커 이미지 이름]
이라는 명령어를 통해 해당 이미지를 실행시킬 수 있다.
이 간단한 컨테이너는 화면에 Hello from Docker!
를 반환해야 하지만, 처음부터 바로 반환하지 못하고 어떤 작업을 거친 다음에 반환한다. 이때 에러 출력 결과를 단계별로 살펴보자. Docker 데몬이 hello-world
이미지를 검색했으나 로컬에서 이미지를 찾지 못했고, 따라서 Docker Hub라는 공개 레지스트리에서 이미지를 가져왔다. 그 후 가져온 이미지에서 컨테이너를 생성하여 실행했다.
사실 이 결과는 당연하다. Docker가 실행시킬 이미지가 로컬에 없는데 실행할 수는 없다. 따라서 Docker Hub라는 공개 레지스트리에 이 이미지가 있는지를 검색해보고 이미지를 가져와야 한다(Docker 데몬이 로컬에서 이미지를 찾을 수 없으면 기본적으로 공개 레지스트리에서 이미지를 검색한다). 실제로 Docker Hub를 가 보면 해당 이미지가 존재하는 것을 확인할 수 있다.
만약 Docker Hub에도 이미지가 없다면 어떻게 될까? 당연히 컨테이너 실행에 실패한다. 따라서 Docker를 사용하기 위해서는 이미지가 필수적이다. 다음 명령어를 실행하면 Docker Hub 공개 레지스트리에서 가져온 컨테이너 이미지를 확인할 수 있다.
docker images
이때 이미지 ID는 SHA256
해시 형식이라는 점만 알아두고, 컨테이너를 다시 실행해보자. 두 번째 실행했을 때 실행 결과를 살펴 보면 처음과는 다르게 바로 Hello from Docker
가 출력되는 것을 알 수 있다. 이는 위에서 hello-world
이미지를 이미 다룬도르 받아 이제 로컬 레지스트리에 존재하기 때문에 Docker Hub에서 이미지를 가져올 필요가 없기 때문이다.
마지막으로 다음 명령어를 실행하여 실행 중인 컨테이너를 확인해보자. docker ps
는 현재 실행 중인 컨테이너 목록만 출력해주는데, 이 명령어를 실행하면 아무 것도 뜨지 않는 것을 확인할 수 있다. 이는 전에 실행한 hello-world
컨테이너가 이미 종료되었기 때문이다. 과거에 실행했던 목록까지 모두 출력해주는 docker ps -a
명령어를 쓰면 컨테이너가 과거에 실행되었던 목록을 모두 확인할 수 있다.
docker ps # 현재 실행 중인 컨테이너만 출력
docker ps -a # 현재 실행 중인 컨테이너와 과거에 실행했던 컨테이너 목록 모두 출력
그런데 이때 사용하는 명령어 ps
는 process
의 줄임말이다. 앞서 Docker는 컨테이너를 생성한다고 했었는데, 그럼 왜 container의 줄임말인 ct 같은 단어를 명령어로 쓰는 게 아니라 ps
를 쓰는 것일까? 그 이유는 Docker 컨테이너가 바로 프로세스이기 때문이다. 이에 대해서 자세하게 작성한 좋은 글이 있으므로 첨부하고, 우선은 Docker 컨테이너가 프로세스이기 때문에 ps
명령어를 쓴다는 점만 알고 넘어가자.
작업 2. 빌드
위에서는 이미 만들어져 있는 이미지를 실행시켜 보았으니, 이제 간단한 노드 애플리케이션을 기반으로 한 Docker 이미지를 직접 빌드해보자. 다음 명령어를 터미널에서 실행하면 test라는 폴더를 만들고, 그 안에 Dockerfile를 생성할 수 있다.
# test라는 이름의 폴더를 만들고 이 폴더로 전환
mkdir test && cd test
# 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"]
를 통해 터미널에서 노드 명령어를 실행하여 애플리케이션을 시작한다.
Dockerfile을 통해 어떻게 애플리케이션을 빌드할지를 설정했으니, 이제 실제로 노드 애플리케이션을 작성한 다음 이미지를 빌드해 보아야 한다. 다음 명령어를 터미널에서 실행하여 노드 애플리케이션을 생성할 수 있다.
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
로 지정되고, 따라서 최신 이미지와 기존 이미지를 구분하기 어려워진다.
빌드 과정을 몇 분 정도 소요될 수 있다. 빌드 과정에서 출력되는 화면은 다음과 같다.
빌드 후 다음 명령어를 통해 빌드된 이미지를 출력해보자.
docker images
다음과 유사한 결과가 출력되면 정상적으로 빌드된 것이다.
작업 3. 실행
이제 방금 빌드한 이미지를 기반으로 하는 컨테이너를 실행해 보자. 다음 명령어를 통해서 이미지를 실행할 수 있다.
docker run -p 4000:80 --name my-app node-app:0.1
이때 각 명령어의 뜻은 다음과 같다.
- -
name
: 원하는 컨테이너 이름을 지정한다. -p
: Docker를 호스트의 포트에 매핑한다. 예를 들어,4000:80
으로 매핑한 경우에는 호스트의 포트 4000에 Docker 컨테이너 포트 80이 매핑된 것이다. 따라서http://localhost:4000
으로 들어가면 서버에 접속이 가능하다. 포트 매핑을 하지 않으면 localhost에서 컨테이너에 접속할 수 없다.
다른 터미널을 열고 (Cloud Shell에서 +
아이콘을 클릭) 서버에 curl
을 때려서 정상적으로 작동하고 있는지를 테스트하자. Hello World
가 출력되면 정상적으로 실행되고 있는 것이다.
만약 이렇게 터미널을 두 개 열어서 진행하는 것이 번거롭다면, 컨테이너를 터미널 세션에 종속시키지 않고 백그라운드에서 실행해야 한다. 그러기 위해서는 -d
플래그를 지정해 실행시키면 된다. 초기 터미널을 닫고, 다음 명령어를 실행하여 컨테이너를 중지 & 삭제한 후 -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
의 출력된 결과에서 컨테이너가 실행 중임을 확인할 수 있다(앞서 말했듯이, -a
옵션이 없으면 현재 실행 중인 컨테이너만 보여준다). docker logs [container_id]
를 실행하면 로그를 확인할 수도 있다.
💡 참고: 앞부분에 입력한 문자들로 컨테이너를 고유하게 식별할 수 있다면 전체 컨테이너 ID를 입력할 필요는 없다. 예를 들어 컨테이너 ID가 17bcaca6f....인 경우 docker logs 17b
만 입력해도 된다. 하지만 Google cloud shell을 사용하는 있다면 터미널 상의 문자를 복사하고 싶을 때 마우스로 드래그만 하면 자동 복사되므로, 컨테이너 ID를 전부 입력하는 게 어렵지는 않다.
이제 앞서 만들었던 어플리케이션을 수정하고, 다시 빌드해보자. 앞서 실습에서 만든 테스트 디렉터리를 열고, 원하는 텍스트 편집기(예: nano 또는 vim)로 app.js
파일을 연 후 'Hello World'
를 다른 문자열로 바꾸면 된다.
# 디렉토리 변경
cd test
# 문자열 변경: 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로 태그를 지정해야 한다. 빌드 과정을 살펴보면 2단계에서는 기존 캐시 레이어를 사용하고 있음을 확인할 수 있지만, 3단계 이후부터는 app.js
를 변경했기 때문에 레이어가 수정된 것을 알 수 있다. 기존 캐시를 사용했기 때문에 빌드가 비교적 짧게 끝난다.
docker build -t node-app:0.2 .
새 이미지 버전으로 다른 컨테이너를 실행하자. 이때 앞서 4000번 호스트 포트를 사용했으므로, 4000 대신 8080으로 매핑해보자.
docker run -p 8080:80 --name my-app-2 -d node-app:0.2
docker ps
이렇게 만들어진 컨테이너를 curl을 때려서 확인해보면 4000포트는 Hello World
가, 8080 포트는 Welcome to Cloud
가 출력되는 것을 알 수 있다!
작업 4. 디버그
컨테이너 빌드와 실행을 숙지했으니 이제 디버깅 방식을 살펴보자.
docker logs [container_id]
를 사용하여 컨테이너의 로그를 볼 수 있다. 컨테이너가 실행 중일 때 로그 출력을 확인하려면-f
옵션을 사용한다.
- 또한 실행 중인 컨테이너 안에 직접 들어가 어플리케이션의 로그를 확인할 수도 있다.
docker exec
명령어를 통해 컨테이너 안의 bash 세션을 생성해보고, 로그를 확인한다. 이때-it
플래그는 stdin을 열린 상태로 유지하여 컨테이너와 표준입출력으로 상호작용할 수 있도록 한다. 세션을 나가고 싶을 때는exit
을 입력하면 된다.
- 마지막으로,
docker inspect
를 통해 Docker에서 컨테이너의 메타데이터를 검토할 수 있다.
작업 5. 게시
이제 이미지를 Google Artifact Registry로 푸시해보자. 그런 다음 모든 컨테이너와 이미지를 삭제하여 새로운 환경을 시뮬레이션하고 컨테이너를 가져와서 실행한다.
Artifact Registry에서 호스팅하는 비공개 레지스트리에 이미지를 푸시하려면 이미지에 레지스트리 이름으로 태그를 지정해야 한다. 형식은 <regional-repository>-docker.pkg.dev/my-project/my-repo/my-image
이다.
대상 Docker 저장소 만들기
이미지를 푸시하려면 먼저 저장소를 만들어야 한다(당연하다, 이미지가 저장되려면 저장소가 먼저 있어야 하니까). Aritifact Registry는 직접 만들어주어야 한다. 다음 절차를 거쳐서 저장소를 만들어주면 된다.
- 탐색 메뉴의 CI/CD에서 Artifact Registry > 저장소로 이동
- 저장소 만들기 클릭
- 저장소 이름으로 my-repository를 지정
- 형식으로 Docker를 선택
- 위치 유형에서 리전을 선택한 후 us-central1 (Iowa) 위치를 선택
- 만들기 클릭
인증 구성하기
이미지를 푸시하거나 가져오려면 먼저 Google Cloud CLI를 통해 Docker가 Artifact Registry에 대한 인증 정보를 가지게 만들어 주어야 한다. us-central1
리전의 Docker 저장소에 인증을 설정하려면 Cloud Shell에서 다음 명령어를 실행하면 된다.
gcloud auth configure-docker us-central1-docker.pkg.dev
참고로 CLI란 명령줄 인터페이스(command-line interface)라는 뜻으로, 터미널에서 명령어를 쳐서 어떤 작업을 할 수 있도록 제공하는 인터페이스라는 의미이다. 구글 클라우드에서는 gcloud
라는 CLI를 통해 이렇게 간단하게 명령어만 입력하면 인증을 할 수 있도록 제공하고 있다. 인증이 완료되고 나면 Google Cloud 프로젝트의 Artifact Registry와 연결하여 이미지를 푸시하거나 가져올 수 있다.
컨테이너를 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 때리기
curl http://localhost:4000
아주 잘 작동한다. Welcome to Cloud! ☁️