kakasoo

Docker 소개 & 사용 방법 정리 본문

프로그래밍/devops

Docker 소개 & 사용 방법 정리

카카수(kakasoo) 2023. 1. 8. 22:51
반응형
docker run -it redis

install 하는 과정에서 wget 과 같은 커맨드 명령어를 사용해야 한다면, 그 전에 설치할 것들이 생겨난다.

그리고 그런 준비물들을 하나씩 설치하다보면 각각의 프로그램의 의존성으로 인해 에러가 발생하게 된다.

도커는 가상의 환경에 격리하여, 각각의 프로그램을 설치하기 때문에 이런 문제가 발생하지 않는다.

이 가상의 환경을 컨테이너라고 한다.

 

도커 및 용어 정리

도커란 컨테이너를 사용하여 응용 프로그램을 더 쉽게 만들고 배포하고 사용할 수 있도록 설계된 도구이며, 컨테이너 기반의 오픈소스 가상화 플랫폼이자 생태계.

컨테이너는 코드와 모든 종속성을 패키지화하여 응용 프로그램이 한 컴퓨팅 환경에서 다른 컴퓨팅 환경으로 빠르고 안정적으로 실행되도록 해주는 소프트웨어의 표준 단위이다.
부연 설명을 하자면, 프로그램이 실행될 수 있는 단위로 종속성을 모두 묶었다고 보면 된다.

컨테이너 이미지는 코드, 런타임, 시스템 도구, 시스템 라이브러리 및 설정과 같은 응용 프로그램을 실행하는 데에 필요한 모든 것을 포함하는, 가볍고 독립적이며 실행가능한 소프트웨어 패키지다.
마찬가지로 부연 설명하자면, 컨테이너 이미지는 컨테이너를 만들기 위한, 일종의 클래스고, 컨테이너는 이미지의 인스턴스라고 생각하면 된다.

 

도커 다운로드 방법

도커 사이트로 이동하여 Download for Mac을 클릭하여 설치한다.

설치 후에는 반드시 docker version 으로 도커 버전을 확인하고, 잘 설치되어 있는지를 확인하자.

설치가 됐더라도 환경 변수 경로가 저장되어 있지 않으면 docker command를 사용할 수 없으니,

 

export PATH="/usr/local/bin:$PATH"

/usr/local/bin 경로로 가서 docker가 설치되어 있는지 확인하고, 위 명령어를 따라쳐서 경로를 저장하자.

docker가 업데이트되며 기존과 설치 경로가 달라져, 명령어가 실행되지 않을 수도 있다.

단, 이 방법은 해당 터미널에서 일시적으로 PATH를 추가한 것이기 때문에 자신의 bash 환경에도 추가해야 한다.

 

~/.zprofile

 

 

도커 사용법

도커는 도커 클라이언트 ( CLI ) 와 도커 서버 ( Daemon ) 으로 나뉜다.

클라이언트에서 명령을 통해 데몬으로 돌아가고 있는 도커 서버들을 모두 관리할 수 있는 구조이다.

 

$ docker run hello-world 

# Unable to find image 'hello-world:latest' locally

 

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
7050e35b49f5: Pull complete
Digest: sha256:18a657d0cc1c7d0678a3fbea8b7eb4918bba25968d3e1b0adebfa71caddbc346
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm64v8)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

도커 클라이언트로 명령어를 쳐서, hello-world를 실행한다고 해보자.

아래처럼 로컬에서 hello-world를 찾지 못했다는 메시지가 출력되고, library/hello-world를 pull 받아온다.

이런 원격의 장소를 docker hub 라고 부르는데, dokcer hub는 하나가 아니고 여러 origin 저장소들이 있다.

바로 아래는 hello-world가 실행된 결과다.

도커에서는 테스트 용으로 hello-world라는 이미지를 가지고 있는데, 단순히 아래소개를 출력하는 이미지다.

한 번 더 docker run hello-world 를 입력하면 pull 받아야 한다는 메시지가 나오지 않는다.

이전의 명령어에 대해서 도커 클라이언트가 캐시하고 있기 때문이다.

 

가상화 기술의 출현 전후

가상화 기술이 나오기 전

  • 한 대의 서버는 하나의 용도로만 사용
  • 남는 서버 공간은 그대로 방치
  • 하나의 서버에 하나의 운영체제, 하나의 프로그램만을 운영
  • 안정적이지만 비효율적

하이퍼 바이저 기반의 가상화 출현

  • 논리적으로 공간을 분할하여 VM 이라는 독립적인 가상 환경의 서버 이용 가능
  • 하이퍼 바이저는 호스트 시스템에서 다수의 게스트 OS를 구동하는 소프트웨어, 하드웨어를 가상화
  • 하이퍼 바이저는 각각의 VM을 모니터링하는 중간 관리자 역할을 수행

일반적으로 말하는 하이퍼 바이저는 호스트형 하이퍼 바이저로,

OS 위에 게스트 OS를 통제하기 위한 가운데 계층으로 하이퍼 바이저를 두는 것을 말한다.

 

하이퍼 바이저에 의해 구동되는 VM은 각 VM마다 독립된 가상 하드웨어 자원을 할당 받는다.

논리적으로 분리되어 있어 한 VM에서 오류가 발생해도 다른 VM으로 퍼지지 않는다는 장점이 있다.

 

도커는 기존 가상화 기술과 비교할 때, 가상화 정도의 차이가 있다.

컨테이너는 게스트 OS를 필요로 하지 않기 때문에 더 가볍다.

VM 기술은 게스트 OS를 부팅한 후 어플리케이션을 실행하기 때문에 더 무겁다.

또한 도커는 VM 기술과 달리 컨테이너가 격리되어 있음에도 여전히 같은 호스트의 동일한 커널을 공유한다.

결과적으로 내부 컨테이너에서 실행되는 포르세스는 호스트 시스템에서도 볼 수 있게 된다.

어떻게 컨테이너는 격리되는가?

먼저 Cgroup ( Control Groups ) 과 네임스페이스 ( namespace ) 를 알아야 한다.

이 둘은 컨테이너와 호스트에서 실행되는 다른 프로세스 사이에 벽을 만드는 리눅스 커널 기능들이다.

Cgroup은 CPU, 메모리, 네트워크 대역 등 프로세스 그룹의 시스템 리소스 사용량을 관리하는 기능이다.

어떤 어플리케이션의 사용량이 너무 많다면, 그 어플리케이션을 Cgroup에 넣어서 리소스 사용 제한이 가능하다.

namespace는 하나의 시스템에서 프로세스를 격리시킬 수 있는 가상화 기술이다.

별개의 독립된 공간을 사용하는 것처럼, 격리된 환경을 제공하는 경량 프로세스 가상화 기술이다.

그런데 왜 MacOS나 Windows에서 도커를 사용할 수 있을까?

 

Client:
 Cloud integration: v1.0.25
 Version:           20.10.16
 API version:       1.41
 Go version:        go1.17.10
 Git commit:        aa7e414
 Built:             Thu May 12 09:20:34 2022
 OS/Arch:           darwin/arm64
 Context:           default
 Experimental:      true

Server: Docker Desktop 4.9.1 (81317)
 Engine:
  Version:          20.10.16
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.10
  Git commit:       f756502
  Built:            Thu May 12 09:14:19 2022
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.6.4
  GitCommit:        212e8b6fa2f44b9c21b2798135fc6fb7c53efc16
 runc:
  Version:          1.1.1
  GitCommit:        v1.1.1-0-g52de29d
 docker-init:
  Version:          0.19.0

docker version 을 입력해서 도커의 버전을 확인해보면, 이상하게도 OS가 리눅스라고 나온다.

도커는 리눅스 VM이 설치되고, 그 안에서 리눅스 커널을 이용해서 컨테이너를 다룬다.

그러므로 MacOS, Windows 유저들도 컨테이너를 다룰 때는 Linux를 사용하는 것과 같은 환경이다.

 

이미지로 컨테이너 만들기

이미지는 컨테이너를 만들기 위한 설계도, 일종의 프로그래밍 언어에서의 클래스와 같다.

이미지는 자기 자신을 실행시켜 컨테이너를 만들기 위한 명령어를 스스로 가지고 있어야 하는데,

이게 도커에서의 run 명령어와 같다.

즉, 이미지는 명령어와 명령으로 실행될 프로그램 ( 종속성을 모두 지닌 ) 스냅샷 의 집합이라고 볼 수 있다.

이미지를 도커 명령어로 실행하게 되면,

컨테이너에 할당된 하드디스크에 도커 이미지의 스냅샷으로 프로그램이 생성되어 들어가게 되고,

컨테이너에 할당된 명령어들은 컨테이너로 들어가 자동으로 실행된다.

명령어는 커널을 통해 컨테이너 내부의 실행 파일을 실행시켜 컨테이너 내에서 동작하게 된다.

 

기본적인 도커 클라이언트 명령어 알아보기

docker run image_name

# docker     : docker 클라이언트를 호출
# run        : docker 컨테이너 생성 및 실행을 의미
# image_name : 이 컨테이너를 위한 이미지 이름

 

docker run image_name command

# command    : image_name이 가지고 있는 실행 명령어를 무시하고, option으로 받은 command 실행

 

docker run alpine ls

# 아래는 실행 후 나오는 메시지들로, ls가 실행된 걸 알 수 있다.
# 아래는 hostOS 에서의 파일이 아니라, 컨테이너 내부에 있는 파일들로, alpine 컨테이너의 내부 하드디스크 파일들이다.

Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
9b18e9b68314: Pull complete
Digest: sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad
Status: Downloaded newer image for alpine:latest
bin
dev
etc
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

 

docker run hello-world ls

# hello-world로 생성된 컨테이너에는 ls 명령어를 실행하게 해주는 파일이 없기 때문에 에러 발생

docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "ls": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled

 

컨테이너 나열하기

컨테이너를 생성하고, 어떤 컨테이너가 생성되어 있는지 보자.

 

docker ps

# docker     : docker 클라이언트를 호출
# ps         : process statu s

두 개의 터미널을 열고, 하나에서는 아래처럼 입력한다.

 

docker run ping localhost

# localhost로 ping을 계속 보낸다.

다른 터미널에서는 docker ps 명령어로, 컨테이너 상태를 확인한다.

 

docker ps --format 'table{{.Names}} \t table{{.Image}}'

원하는 항목만 보고자 할 때는 다음과 같이 입력한다.

 

docker ps -a # all

과거에 종료된 이미지를 포함, 모든 컨테이너를 보고 싶다면 위 명령어를 사용한다.

 

도커 컨테이너의 생명 주기

  • 생성 (create) # docker create
  • 시작 (start) # docker start
  • 실행 (running) # docker run ( create, start가 합쳐진 명령어 )
  • 중지 (stopped) # docker stop
  • 삭제 (deleted) # docker rm

create는 컨테이너 공간을 생성하는 명령어로, 생성 후에는 이미지의 파일 스냅샷이 들어가게 된다.

start는 생성된 컨테이너에 이미지의 명령어를 실행한다.

 

docker create hello-world

# 611374b3fa4bb15b14dd9eeb269eda3c93d1a26ce42f5f305f6e6c84005ae068

docker start 611374b3

# 도커를 Gracefully 하게 중지시킨다.
# 작업 중인 게 완료가 된 다음에 컨테이너를 중지시킨다.
docker stop 611374b3

# 작업 중인 걸 기다리지 않고 바로 컨테이너를 중지시킨다.
docker kill 611374b3

docker stop은 SIGTERM을 날린 다음, SIGKILL로 이어지지만, docker kill은 바로 SIGKILL 신호를 날린다.

 

도커 컨테이너의 삭제

도커 컨테이너의 삭제는, 종료된 컨테이너에게만 가능하다.

docker ps -a

docker rm <container_name or id>
docker rm `docker ps -a -q` # 모든 컨테이너의 삭제, '이 아니라 `인 것에 유의한다.

 

docker rmi <image_id> # 도커 이미지의 삭제

# 컨테이너, 이미지, 네트워크를 한 번에 삭제한다.
# 실행 중인 컨테이너에 영향을 주지 않으며, 사용하지 않는 도커를 모두 지울 때 사용한다.
docker system prune

 

도커 컨테이너에 명령어 전달하기

docker exec <container_id> command

# container_id에 해당하는 컨테이너에게 command 명령어를 실행시킨다.

 

레디스를 이용한 컨테이너의 이해

docker run redis # redis pull 받은 다음 실행하기

redis-cli # redis가 컨테이너 안에 있기 때문에 에러 발생, redis-cli를 실행하려면 어떻게 해야 하는가?

 

docker exec -it <redis_container_id> redis-cli # redis 서버가 있는 컨테이너에서 cli 실행시키기

# -it 명령어는 interactive 와 terminal의 약자로, 명령 실행 후에도 컨테이너 접속을 유지하라는 의미.
  • docker # 명령어
  • exect # execute, 실행하라.
  • -it # 연결을 유지하라.

명령을 내리고 싶으면, 명령하고자 하는 대상 ( = 클라이언트 ) 도 클라이언트 안에 들어와야 한다는 걸 알 수 있다.

 

컨테이너에서 터미널 실행시키기

docker exec -it <container_id or name> sh # zsh, bash도 가능하지만 sh는 어디에나 있으므로

터미널에서 빠져 나올 때에는 컨트롤 + D 로 해야한다.

 

컨테이너 이미지 생성하기

flowchart LR

a[Ddocker file 생성] --> b[도커 클라이언트] --> c[도커 서버] --> d[이미지 생성]

docker file은 도커 이미지를 만들기 위한 설정 파일이다.

컨테이너가 어떻게 행동해야 하는지에 대한 설정들을 정의한 곳을 의미한다.

도커 클라이언트에는 도커 파일에 입력한 것들이 전달될 것이고, 도커 클라이언트는 그 명령어를 서버에 넘긴다.

서버는 이미지를 생성한다.

 

도커파일 (docker file) 만들기

  1. 베이스 이미지를 명시한다. ( 파일 스냅샷에 해당 )
  2. 추가적으로 필요한 파일을 다운받기 위한 몇 가지 명령어를 명시해준다. ( 파일 스냅샷에 해당 )
  3. 컨테이너 시작 시 실행될 명령어를 명시해준다.
  4. 이미지 완성

베이스 이미지란?

도커 이미지는 여러 개의 레이어로 되어 있다.

그 중에서 베이스 이미지는 이 이미지의 기반, 근간이 되는 부분이다. ( = OS에 해당 )

레이어는 중간 단계의 이미지이다.

도커파일 작성하기

# 베이스 이미지를 명시한다.
FROM baseImage

# 추가적으로 필요한 파일들을 다운로드한다.
RUN command

# 컨테이너 시작 시 실행할 명령어를 명시한다.
CMD ["executable"]

도커파일은 위와 같이 베이스 이미지, 추가 파일 다운로드, 명령어 명시의 3 단계로 작성한다.

FROM은 이미지 이름을 명시하면 되며, 태그를 명시하지 않으면 최신 버전으로 다운로드한다.

RUN은 도커 이미지 생성 전 수행할 쉘 명령어이며, CMD는 DockerFile 내 1번만 사용 가능하다.

“hello world”를 출력해주는 docker file을 만들어보자.

 

FROM alpine

# 추가로 설치할 게 없으므로 RUN은 생략한다.
# RUN command

CMD ["echo", "hello"]

 

도커 파일로 도커 이미지 만들기

실제로 작성한 도커 파일로 이미지를 만들어보자.

docker build .

build 명령어는 디렉토리 내 dockerfile 이라는 파일을 찾아서 알아서 도커 클라이언트에게 전달시켜준다.

docker는 이미지를 통해서 컨테이너를 만들지만,

만들어지는 과정을 보면 base image로 만든 컨테이너에 새로운 기능들을 붙여 이미지로 만드는 것이기에,

이미지 역시 컨테이너를 통해서 만들어진다고 볼 수 있다.

명확하게 이름을 지정해야 다루기 편하므로 아래처럼 사용하자.

 

docker build -t kakasoo/hello:latest ./
# docker build -t 유저이름/프로젝트명/버전 ./

 

Node.js 앱 만들기

server.js 라는 파일 이름으로 node.js application code가 만들어져 있고,
package.json에 그 코드를 실행시키기 위한 script ( ex. npm run start ) 가 작성되어 있다고 해보자.

# filename : Dockerfile

# 베이스 이미지 명시
# FROM baseImage

# 추가적으로 필요한 파일 다운로드
# RUN command

# 컨테이너 생성 직후 실행할 명령어
# CMD ["executable"]

 

# 베이스 이미지 명시
FROM node:10

# 추가적으로 필요한 파일 다운로드
RUN npm install

# 컨테이너 생성 직후 실행할 명령어
CMD ["node","server.js"]

alpine이 아니라 node로 해야 하는 이유는, npm 과 같은 node 종속성 관리 도구를 사용해야 하기 때문이다.

alpine은 가장 최소화된 경량화된 파일들의 집합이라, npm install과 같은 명령어를 사용할 수 없다.

그렇다고 npm이 꼭 node 이미지에만 있는 것은 아니지만, node가 대표적이다.

 

npm install 에서의 에러 발생

# 베이스 이미지 명시
FROM node:10

# 추가적으로 필요한 파일 다운로드
RUN npm install

# 아래가 발생한 에러 내용
npm WARN saveError ENOENT: no such file or directory, open '/package.json'

package.json은 컨테이너 바깥에 존재하기 때문에 에러가 발생한다.

이러한 이유 때문에 COPY 명령을 통해 컨테이너 내부로 파일을 공유해야 한다.

 

# package.json을 도커 컨테이너의 ./ 경로로 복사한다.
COPY package.json ./

 

# 베이스 이미지 명시
FROM node:10

COPY package.json ./

# 추가적으로 필요한 파일 다운로드
RUN npm install

# 아래가 발생한 에러 내용
npm WARN saveError ENOENT: no such file or directory, open '/package.json'

도커 파일을 ./ 경로에 만들었으면 이제 이 이미지를 생성, 실행시켜보자.

 

docker build -t kakasoo/nodejs ./

# docker
# build
# -t : 이름을 정하기 위해
# kakasoo/nodejs : 정하고자 하는 컨테이너의 이름
# ./ : dockerfile이 있는 경로

이번에는 server.js가 없어서 에러가 발생할 것이다.

마찬가지로 server.js도 COPY 명령으로 컨테이너에 넣고 재실행해보자.

모든 파일을 복사해서 옮기라는 의미로, 아래처럼 작성할 수 있다.

 

# 베이스 이미지 명시
FROM node:10

# ./ 경로의 모든 파일을 컨테이너 내부 ./로 옮긴다.
COPY ./ ./

# 추가적으로 필요한 파일 다운로드
RUN npm install

# 아래가 발생한 에러 내용
npm WARN saveError ENOENT: no such file or directory, open '/package.json'

하지만 브라우저로 해당 포트에 접근해보면, 연결이 안 된다?

 

생성한 이미지로 서버 실행 시 접근이 안 되는 이유

docker run -p 49160 : 8080 imageName

파일만 COPY 명령어로 전달한다고 끝이 아니고,

이와 비슷하게 네트워크도 외부의 것과 내부의 것을 연결해줄 필요가 있다.

이 명령어는 49160번 포트를, 도커 컨테이너의 8080과 연결하란 의미가 된다.

 

Working Directory 명시하기

FROM node:10

WORKDIR /usr/src/app

이미지 안에서 어플리케이션 소스 코드를 갖고 있을 디렉토리를 생성하는 것이다.

이게 있어야 하는 이유는,

COPY 명령으로 복사한 파일과 이름이 같은 파일이 base image 안에 있을 경우 덮어 씌우기 때문이다.

 

# 베이스 이미지 명시
FROM node:10

# 시작 디렉토리 정의
WORKDIR /usr/src/app

# ./ 경로의 모든 파일을 컨테이너 내부 ./로 옮긴다.
COPY ./ ./

# 추가적으로 필요한 파일 다운로드
RUN npm install

# 컨테이너 생성 직후 실행할 명령어
CMD ["node","server.js"]
# 컨테이너 생성 후 
docker run -it kakasoo/nodejs sh

시작 디렉토리는 /usr/src/app이 되어 있고, 물론 나갈 수도 있다.

 

어플리케이션 소스 변경으로 인해 다시 빌드하는 방법

코드를 한 줄 수정하면…

docker stop kakasoo/hello:latest

docker build -t kakasoo/hello:latest ./

docker run -p 49160 : 8080 kakasoo/hello:latest

하나의 코드를 수정해서 저 정도의 명령어를 쳐야 하는 것도 번거로운데,

docker run 같은 경우에는 npm install과 같은 무거운 명령어들도 수행하게끔 되어 있다.

좀 더 효율적인 방법이 없을까?

 

효율적으로 캐시를 이용하는 방법

# 베이스 이미지 명시
FROM node:10

# 시작 디렉토리 정의
WORKDIR /usr/src/app

# ./ 경로의 모든 파일을 컨테이너 내부 ./로 옮긴다.
COPY ./ ./

# 추가적으로 필요한 파일 다운로드
RUN npm install

# 컨테이너 생성 직후 실행할 명령어
CMD ["node","server.js"]
# 베이스 이미지 명시
FROM node:10

# 시작 디렉토리 정의
WORKDIR /usr/src/app

COPY ./package.json

# 추가적으로 필요한 파일 다운로드
RUN npm install

# ./ 경로의 모든 파일을 컨테이너 내부 ./로 옮긴다.
COPY ./ ./

# 컨테이너 생성 직후 실행할 명령어
CMD ["node","server.js"]

docker run 명령어의 경우에는 각 명령어에 대한 캐시가 있기 때문에,

좌측을 두 번 실행할 때 npm install로 새로운 파일들을 설치하는 것을 두 번 반복하지 않는다.

그래서 캐시 범위를 넓혀주기 위해, 우측처럼 명령어를 수정할 수 있다.

좌측에서는 코드를 조금만 변경해도 COPY ./ ./ 에 대한 npm install이 진행되어 무조건 재실행되는 반면,

우측에서는 COPY ./package.json 에 의해 돌아가는 거기 때문에 좀 더 캐시 적중률이 높아진다.

 

도커 Volumn을 이용하는 방법

docker run -d -p 5000:8080 -v .usr/src/app/node_modules -v $(pwd):/usr/src/app imageId

# -d : detect mode
# -v .usr/src/app/node_modules : 호스트 디렉토리에서 node_modules는 매핑하지 말라 하는 것
# -v $(pwd):/usr/src/app : pwd 경로에 있는 디렉토리 혹은 파일을 /usr/src/app 경로에서 참조

변화된 파일을 추적하게끔 하는 방법이지만, 재실행은 필요하다.

그래서 아직은 완벽한 방법은 아니라는 생각이 든다.

반응형

'프로그래밍 > devops' 카테고리의 다른 글

AWS beanstalk과 ECS의 차이  (0) 2023.02.12
테라폼 작동 원리와 CLI 실습  (0) 2023.01.05
AWS-CLI 및 terraform 설치  (0) 2023.01.01
[Mac OS] ZSH 및 OH-MY-ZSH  (0) 2023.01.01