컨테이너 가상화

2025. 3. 18. 17:47Docker

위와 같은 특성들로

VM은 격리성과 보안이 중요한 환경에 적합하고

컨테이너는 빠른 배포, 높은 확장성, 리소스 효율성이 중요한 환경에서 유리하다

 


컨테이너 가상화에 필요한 대표적인 리눅스 기반 기술 두 가지

- 네임스페이스(Namespaces)

- cgroups(Control Groups)

 

1. 네임스페이스 (Namespaces)

컨테이너를 서로 독립된 환경에서 실행할 수 있도록 리눅스 커널에서 제공하는 격리 기술이다.

리눅스에서 실행되는 모든 프로세스는 기본적으로 커널의 자원(파일 시스템, 프로세스 ID, 네트워크 등)을 공유하지만 Namespaces를 사용하면 특정 리소스를 다른 프로세스와 격리할 수 있다.

Namespaces 사용 예시

1) PID Namespace 를 사용한 프로세스 격리

unshare --pid --fork --mount-proc bash

--pid : 새로운 PID Namespace 생성

--fork: Namespace 안에 새로운 프로세스 실행

--mount-proc: /proc directory를 새로운 Namespace에 맞게 마운트

위 명령어를 통해 새로운 Shell이 실행되었고 기존 프로세스들과 격리되었다.


PID Namespace가 격리되었는 지 확인하는 방법

ps aux

기존 시스템에서는 여러 프로세스가 보이지만 새로운 Namespace 내에서는 단 몇 개의 프로세스(현재 실행 중인 bash 등)만 보여야 한다.

2) Network Namespace를 사용하여 네트워크 격리

독립적인 네트워크 interface 를 생성하기 위해 사용

ip netns add mynetns  # 새로운 네트워크 네임스페이스 생성
ip netns list          # 생성된 네임스페이스 확인

mynetns 라는 Network Namespace 생성

ip netns exec mynetns ip addr # 새로운 Network Namespace Interface 확인

 

 

2. cgroups (Control Groups)

CPU, 메모리, 디스크 I/O, 네트워크 대역폭 등의 시스템 리소스를 프로세스 그룹별로 제한하고 할당하는 기능이다.

컨테이너 환경에서 cgroups 를 사용하여 각 컨테이너가 사용 가능한 리소스를 제한하고, 특정 컨테이너가 과도한 리소스를 사용하지 않도록 제어할 수 있다.

cgroups는 컨테이너가 리눅스 시스템 전체를 과도하게 사용하는 것을 방지하는 역할을 한다.

예를 들어 컨테이너 하나가 무제한으로 CPU를 점유하지 못하도록 특정 비율만 사용하게 설정하거나 메모리 사용량을 초과하면 컨테이너를 강제 종료할 수도 있다.

 

+ chroot ???

chroot는 리눅스에서 특정 프로세스의 루트 디렉토리 ( / ) 를 변경하는 기능이다. 특정 프로세스가 파일 시스템을 접근할 때, 지정된 디렉토리를 루트 ( / )로 인식하게 만들어 파일 시스템을 격리하는 효과를 준다. 이를 활용해 격리된 환경을 만들어 시스템의 나머지 부분에 접근하지 못하도록 격리시킬 수 있다.

하지만 chroot를 사용하면 파일 시스템 레벨에서만 격리되고, 프로세스, 네트워크, 사용자 권한 등 다른 리소스들은 여전히 공유되는 상태이다. 그렇기 때문에 파일 시스템 격리 목적으로 사용할 수 있지만 Namespaces와 같은 수준의 격리는 불가능하다.

 

chroot 는 Namespace 와 비교 했을 때

- 프로세스 및 네트워크 격리가 없고, 보안성이 낮으며, 리소스 제한이 불가능하다는 단점이 있다.

 

 

Docker 는  Namespaces + cgroups + OverlayFS(파일 시스템) + 기타 기능으로 구현된 도구이다.

Docker는 리눅스 커널의 기능(Namespaces, cgroups, OverlayFS 등)을 활용하여 컨테이너를 구현했다.

사용자가 쉽게 컨테이너를 만들고 관리할 수 있도록 UI(Command Line), API, 오케스트레이션 기능 등을 추가했다.

따라서, Docker는 리눅스의 컨테이너 기능을 편리하게 패키징한 도구일 뿐, Namespace 자체를 새로 개발한 것은 아니다.

 

 


Docker

Docker 는 컨테이너 기반 가상화를 쉽게 사용할 수 있도록 도와주는 플랫폼이자 도구이다.

애플리케이션과 그 실행에 필요한 환경을 패키징하여 실행 할 수 있게 만들어 주는 도구이다.

 

Docker 의 핵심 개념

1) Image 

 

컨테이너를 실행하기 위한 불편(Immutable)한 패키지이다. (변하지 않는 패키지)

애플리케이션을 실행하는 데 필요한 모든 구성 요소(코드, 런타임, 라이브러리, 환경 설정, OS 패키지 등)을 포함하고 있다. Docker Container은 이 Image를 기반으로 실행된다.

경량성 ( 기존 VM 과 비교했을 때 OS 자체를 포함하지 않기 때문에 가볍다)

 

Image는 layer 구조로 되어있어 효율적인 저장과 배포가 가능하다. (변경된 부분만 새로운 Layer로 추가되므로 효율적인 저장과 배포가 가능하다)

Layer Structure?

Docker Image 는 여러 개의 Read Only Layer 로 구성되어있다. 각 Layer 는 파일 시스템의 변경 사항 (추가, 수정, 삭제 등)을 저장하고 이전 Layer 위에 Overwrite 되는 방식으로 동작하게 된다.

 

예시: ubuntu:20.04 이미지를 기반으로 새로운 이미지를 만들 때

FROM ubuntu:20.04  # (레이어 1)
RUN apt-get update && apt-get install -y python3  # (레이어 2)
COPY app.py /app/  # (레이어 3)
CMD ["python3", "/app/app.py"]  # (레이어 4)

각 레이어는 변경된 부분만 저장하고 기존 데이터는 그대로 유지하게 된다.

그래서 Layer 2 = Layer 1 + apt 실행결과  와 같은 방식이다.  점점 쌓이는 구조 

각 Layer는 Caching 되기 떄문에 변경되지 않은 부분은 재사용된다.

컨테이너를 실행하면 Layer 1~4 위에 추가적인 Read/Write (Writable) Layer 가 생성된다.

이는 컨테이너가 실행되는 동안 내부에서 파일을 생성, 수정 및 삭제할 수 있도록하기 위해서다.

 

Docker 이미지를 실행하게 되면 아래와 같은 주로가 된다.

Writable Layer  (컨테이너 실행 중 생성, 읽기/쓰기 가능)
-----------------------------------
Layer 4: CMD 설정 (이미지의 일부, Read-Only)
Layer 3: app.py 파일 추가 (Read-Only)
Layer 2: apt-get install python3 (Read-Only)
Layer 1: ubuntu:20.04 (베이스 이미지, Read-Only)

컨테이너 실행 시 추가되는 Writable Layer만 수정이 가능하고 컨테이너가 삭제되면 Writable Layer도 사라지게 된다.

docker run -it ubuntu:20.04 bash
echo "Updated" >> /etc/hostname

위와 같이 기존 파일을 수정하려고 하면 Docker 는 이 파일을 Writable Layer로 복사하고 복사본을 수정하게 된다.

(Copy on Write 원리) 결과적으로 원래 이미지 파일은 그대로 남아 있고 컨테이너 내부에서만 변경이 된다.

 

Writable Layer vs Docker Volume

Writable Layer 은 컨테이너가 삭제되면 사라지지만 Volume은 컨테이너가 삭제되어도 데이터를 유지하게 된다. 데이터를 영구적으로 저장해야하는 경우에는 Writable Layer 대신 Volume 을 사용해야한다.

단, 원본 Docker 이미지는 절대 변경되지 않는다는 것을 생각해야한다.

 

 

# Docer Image 관련 명령어

docker images           # 현재 시스템에 저장된 이미지 목록 확인
docker pull nginx       # Docker Hub에서 nginx 이미지 다운로드
docker rmi nginx        # Docker 이미지 삭제
docker build -t my-app .  # Dockerfile을 이용해 새로운 이미지 빌드

 

+ Docker Tag

Docker Tag는 Image Version을 식별하고 관리하는 방법이다. Docker Image는기본적으로

이름:태그(repository:tag) 와 같은 형식으로 식별되며 Tag를 이용하면 같은 이미지라도 여러 버전으로 관리할 수 있다.

 

Tag가 없는 경우 (latest 기본 적용)

Tag를 지정하지 않으면 기본적으로 latest Tag가 자동으로 붙게 된다.

docker build -t mynginx . 은 mynginx:latest 와 동일하다

 그리고 

docker run -d mynginx 

는 자동으로 mynginx:latest 를 실행하게 된다.

 

 

2) Container

Docker Container 는 실행 중인 Docer Instace를 의미한다.

Image 기반으로 생성되며 독립적인 환경에서 애플리케이션을 실행 할 수 있게 해주는 기술이다. 기본적으로 Host OS의 KErnel을 공유하면서도 파일 시스템, 네트워크, 프로세스 등을 격리해서 실행하게 된다. VM 과 달리 가벼우며, 빠르게 생성 및 삭제를 할 수 있다.

대표적인 특징으로 경량성, 독립된 실행환경, 일관된 배포 환경, 즉시 실행 가능이 있다.

 

Docker 컨테이너 관련 명령어

docker run -d -p 8080:80 --name my-nginx nginx        # 컨테이너 실행
docker ps                                                                     # 실행 중인 컨테이너 목록 확인
docker stop my-nginx                                                  # 컨테이너 중지
docker rm my-nginx                                                     # 컨테이너 삭제
docker logs my-nginx                                                  # 컨테이너 로그 확인
docker exec -it my-nginx bash                                     # 실행 중인 컨테이너에 접속

-it 컨테이너 접속을 위한 명령어

-i : interactive 상호작용

-t : terminal 

로 해당 컨테이너와 터미널로 상호작용하겠다는 의미이다.

 

3) Dockerfile

 

Dockerfile은 Docker Image를 만들기 위한 설정 파일을 말한다. 이 파일을 통해 애플리케이션 환경을 코드로 정의하고 일관된 방식으로 Image를 생성할 수 있게 된다.

Dockerfile을 기반으로 docker build 를 실행하면 새로운 Docker Image 가 생성된다.

 

- Dockerfile 기본 구조

# 1. 베이스 이미지 선택 (Python 3.9 사용)
FROM python:3.9

# 2. 작업 디렉토리 설정
WORKDIR /app

# 3. 필요한 파일 복사
COPY app.py .

# 4. 필요한 패키지 설치
RUN pip install flask

# 5. 실행할 명령어 설정
CMD ["python", "app.py"]

From : Docker Image의 기반(Base Image)를 지정하는 명령어이다.

위에서는 Python 3.9가 설치된 공식 Docker Image를 사용한다고 명시 되어있다.

 

WORKDIR : 컨테이너 내에서 작업할 Base Directory 를 설정

/app : 컨테이너 내부에서 사용할 Directory를 의미한다.

이를 통해 컨테이너 내부에서 모든 작업은 /app Directory 에서 이루어지며 이후 COPY 명령어 등을 사용할 때 상대 경로로 /app/ 기준으로 파일이 복사되게 된다.

 

cd 대신에 WORKDIR을 사용하라!

=> 설치 위치나 작업 디렉토리를 변경할 때마다 새로운 WORKDIR을 사용하라는 의미이다.

cd vs WORKDIR 차이점

# cd를 사용하는 경우 

RUN mkdir /app
RUN cd /app && touch file.txt

첫 번째 RUN에서 /app을 만들지만, 두 번째 RUN에서 cd /app을 해도 레이어가 독립적이기 때문에 유지되지 않는다.

두 번째 RUN이 끝나면 다시 원래 디렉토리로 돌아가게 된다.

 

# WORKDIR 사용 예시

WORKDIR /app
RUN touch file.txt

WORKDIR은 이후 모든 명령어가 해당 디렉토리를 기준으로 실행되므로 한번 설정하며 계속 유지된다.

 

 

COPY <Host file> <컨테이너 file>

COPY <Host Directory> <Container Directory>

app.py : 호스트에 있는 파일을 컨테이너 내부의 /app/app.py 위치에 복사한다.

이 단계에서 호스트 시스템의 app.py가 컨테이너 안으로 복사된다. 컨테이너 내부에서 app.py 를 실행할 수 있다.

파일 -> 파일. 디렉토리 -> 디렉토리로 하는 것이 좋다.

파일 -> 디렉토리로 복사헤된다면 컨테이너 내부에 해당 디렉토리가 존재하는 지를 확인해야한다.

만약 해당 디렉토리가 존재하지 않는다면 Error 가 발생할 수 있다.

COPY . . 은 ?

Host의 현재 Directory를 Container 의 현재 Directory로 복사한다.

COPY . . 는 현재 디렉토리의 모든 파일과 폴더를 복사하기 때문에 불필요한 파일은 (node_moudles, .git 등)을 .dockerignore 로 제외해야한다.

WORKDIR 이 잘 설정되어 있는 지 확인해야한다.

 

+ ADD

Dockerfile 에서 파일을 컨테이너 내부로 복사하는 명령어다. COPY 와 비슷하지만 추가적인 기능이 있다.

(압축 파일 자동 해체, URL에서 직접 다운로드 등)

 

ADD는 압축 해제 기능이 있지만, 대부분의 경우 RUN tar -xzf로 직접 압축을 해제하는 것이 더 명확하다

ADD는 URL 다운로드 기능이 있지만, RUN wget이나 RUN curl을 사용하는 것이 더 직관적이다

 

그리고 COPY는 단순히 파일을 복사하는 용도로만 사용되기 때문에 의도가 더 명확하다.

따라서 일반적으로 COPY를 사용하고 ADD는 특수한 경우에서만 사용하는 것이 좋다.

 

 

RUN : Docker Image Build 시 실행되는 명령어

pip install flast: Flask web framework를 설치한다.

RUN은 Image Build 단계에서 실행되며 결과는 Cache 된다.

 

CMD : 컨테이너가 실행될 때 기본적으로 사용할 명령어들이다.

["python", "app.py"] → python app.py를 실행하게 된다.

CMD는 컨테이너가 시작될 때 실행되는 기본 명령어를 뜻하며, ENTRYPOINT와 다르게 CMD는 컨테이너 실행 시 다른 명령어로 대체할 수 있다.

 

+ 추가적인 개선 사항

requirements.txt  (여러 패키지 설치)

flask
requests
gunicorn

파일안에 위와 같이 생성하면 Flask, Requests, Gunicorn 설치

# Dockerfile 수정
COPY requirements.txt .
RUN pip install -r requirements.txt

이를 통해 여러 개의 패키지를 한 번에 설치할 수 있다.

 

CMD vs ENTRYPOINT

둘 다 모두 컨테이너가 실행될 때 기본적으로 실행될 명령어를 지정한다.

1. CMD 

컨테이너 실행 시 기본적으로 실행할 명령어를 지정한다. 하지만 사용자가 컨테이너 실행 시 명령어를 입력하면 CMD는 덮어씌워진다.

FROM centos:7
CMD ["sleep", "infinity"]

예를 들어 위와 같은 Dockerfile 이 있을 때

docker run centos:7

이와 같이 기본적인 방식으로 컨테이너를 실행하면

-> 정상적으로 CMD에서 설정한 sleep infinity 가 실행된다.

하지만 사용자가 실행 시 명령어를 입력한다면 

docker run centos:7 ls -l

-> CMD가 무시되고 ls -1이 실행된다.

 

하지만 ENTRYPOINT는 사용자가 실행 시 명령어를 입력해도 ENTRYPOINT는 유지된다.

FROM centos:7
ENTRYPOINT ["sleep", "infinity"]

위와 같은 Dockerfile에 아래와 같은 명령어를 입력하면 

docker run centos:7 10

이 때 실행되는 명령어는 sleep 10 이 된다. 명령어를 입력해도 ENTRYPOINT는 유지된다고 했는 데 왜 infinity가 10으로 바뀌었을까?

그 이유는 ENTRYPOINT의 동작 방식 때문이다. 

ENTRYPOINT를 ["command", "arg1", "arg2"] 형식으로 설정하면, 컨테이너 실행 시 기본적으로 "command"는 고정이된다 하지만 사용자가 추가적인 인자를 입력하면 CMD나 ENTRYPOINT의 기본 인자 (arg1, arg2)가 대체되는 형식이다.

그래서 Docker file 에서 고정하고 싶은 command 는 ENTRYPOINT 로 설정하고 명령어를 통해 변경하고 싶은 부분은 CMD 로 작성할 수도 있다.

 

Dockerfile 의 주요 명령어

Dockerfile을 이용한 이미지 생성 및 실행

docker build -t my-python-app .
docker run -p 8080:8080 my-python-app

 

+ EXPOSE 는 컨테이너에서 사용할 포트를 명시적으로 문서화하는 역할을 한다.

하지만 실제로 포트를 개방하거나 바인딩하는 기능은 없다.

FROM nginx:latest
EXPOSE 80

컨테이너 80번 포트를 사용할 것임을 나타내는 "문서화" 용도이다.

실제 80번 네트워크 포트가 개방되지는 않는다.

- P 옵션을 사용하면 컨테이너가 EXPOSE 한 포트를 호스트의 랜텀한 포트에 자동으로 패밍한다.

docker run -d -P my-nginx


CONTAINER ID   PORTS
abc12345       0.0.0.0:32768->80/tcp

이처럼 컨테이너의 80번포트가 호스트의 32768번 포트로 연결된 것을 볼 수 있다.

EXPOSE 80 이 선언되어서  - P 옵션이 해당 포트를 호스트에게 랜덤하게 할당하여 연결한 것이다.

결과적으로 - P 를 사용할 때만 EXPOSE가 의미가 있다.

 

- p 옵션은 호스트 포트를 컨테이너의 특정 포트에 수동으로 매핑하는 역할이기 때문에 EXPOSE 에 영향을 받지 않기 때문이다.

 

4) Registry 

Docker Registry는 Docker Image를 저장하고 배포하기 위한 공간이다.

Local에서 만든 Image를 공유하거나 배포할 때 사용한다.

대표적인 Docker Registry Service:

 - Docker Hub

- AWS Elastic Container Registry (ECR)

등이 있다.

 

Docker Hub에서 이미지 가져오기


docker pull nginx       # nginx 최신 이미지 다운로드
docker pull ubuntu:20.04  # 특정 버전(20.04)의 ubuntu 이미지 다운로드

 

5) Volume

Volume은 컨테이너의 데이터를 영구적으로 저장하는 기능이다. 기본적으로 Docker Container는 종료되면 내부 데이터도 사라지자만 Volume을 사용하면 Container가 삭제되어도 Data를 유지할 수 있따. 일반적으로 DataBase Container 에서 많이 사용된다.

Volume은 일반적으로 Database, Log Storage, File Share 등의 목적으로 사용된다.

Volume의 특징

- 데이터 영구 저장

- 호스트와 공유 가능

- 컨테이너 간 공유 가능

이다.

 

docker volume create my-volume                                                           # 새로운 Volume 생성

docker run -d -v my-volume:/app/data --name my-container ubuntu     # Container 에서 Volume 사용

-v my-volume:/app/data → my-volume 볼륨을 컨테이너의 /app/data에 마운트됨
컨테이너 내부에서 /app/data 디렉토리에 저장된 데이터는 컨테이너가 삭제되더라도 유지된다.

docker run -d -v my-volume:/app/data --name container1 ubuntu   # 컨테이너 간 데이터 공유
docker run -d -v my-volume:/app/data --name container2 ubuntu

docker volume ls                                                                             # 볼륨 목록 확인
docker volume rm my-volume                                                         # 특정 볼륨 삭제

 

 

 

Docker 에서 데이터를 저장하는 방식에는 3 가지 방법이 있다.

- Volumes

Docker 가 자체적으로 관리하는 데이터 저장 방식으로 Volume의 데이터는 Host File System 이 아니라 Docker 내부 경로에 저장된다. (/var/lib/docker/volumes/)

여러 개의 컨테이너가 동일한 Volume을 공유할 수도 있다.

컨테이너 삭제 후에도 데이터가 유지되며, Docker 가 데이터를 관리하기 때문에 보안성이 높다. 여러 개의 컨테이너가 같은Volume 을 공유할 수 있고 백업과 복원이 쉽다.

 

- Bind Mounts 

호스트 OS의 특정 디렉토리를 컨테이너 내부로 직접 Mount 하는 방식이다.

Volume 과 다르게 데이터가 Docker 가 직접 관리하는 디렉토리가 아니기 때문에 호스트의 특정 디렉토리에 저장된다.

따라서 Host OS에서 직접 접근하여 파일을 수정할 수 있다.

 

- Tmpfs Mounts (임시 파일 시스템 마운트)

컨테이너가 데이터를 RAM(메모리)에 저장하는 방식이다. 디스크에 저장되지 않고 컨테이너가 종료되면 데이터가 사라진다. 읽기 / 쓰기 속도가 빠르기 때문에 임시 데이터 저장, 캐싱, 성능이 중요한 애플리케이션에서 유용하다.

docker run -d --tmpfs /app/cache:size=100m --name temp-container ubuntu

/app/cache 에 메모리를 Mount 하고 크기그를 100 MB로 설정했다.

 

호스트 파일을 직접 접근할 수 있기 때문에 컨테이너가 호스트 파일을 변경할 위험성이 있다. ( 보안 위험) 

docker run -d -v /home/user/data:/app/data --name my-container ubuntu

Host의 /home/user/data 디렉토리를 컨테이너의 /app/data에 Mount

이제 호스트와 컨테이너가 동일한 데이터를 공유하게 된다.

컨테이너 내부에서 데이터를 저장하면 호스트에서 확인이 가능하며, 컨테이너를 삭제해도 호스트 파일 안에 데이터가 여전히 존재하는 것을 확인할 수 있다.

 

 

 

 

 


# 컨테이너가 잘 동작하려면 반드시 foregorund 상태로 프로세스가 존재해야 한다 ?

Foreground Process ?

터미널에서 실행되고 사용자가 직접 제어할 수 있는 Process를 의미한다. 반대는 Backgorund Process 가 있다.

 

Cotainer 에서 Foreground Process 가 필요한 이유

컨데이터 내부에서 실행되는 프로세스가 종료되면 컨테이너도 종료된다. 다른 말로 컨테이너가 실행되려면 종료되지 않는 foreground process 가 필요하다.

docker run --name test-container ubuntu echo "Hello, World!"

echo 는 한 번 실행되고 종료되므로 컨테이너도 즉시 종료된다.

 

그래서 지속적으로 컨테이너를 올바르게 유지하는 방법은

docker run -it ubuntu bash

와 같이 bash를 foreground 상태에서 실행시켜 컨테이너가 종료되지 않고 유지되게 만드는 것이다.

 

 

문제 1 # 호스트에 파일이 존재하고 컨테이너 내부에는 없는 경우!

호스트에 특정 파일이 존재하지만 컨테이너 내부에는 해당 파일이 존재하지 않아야 한다.

이를 확인하기 위해 컨테이너 내부에서 해당 파일을 검색하고 출력할 것이다.

docker run --rm ubuntu ls /home/user/testfile.txt

호스트의  /home/user/testfile.txt 파일이 컨테이너 내부에도   /home/user/testfile.txt  존재하는 지 확인하고 해당 파일이 없는 경우에 다음과 같은 메세지를 출력한다.

ls: cannot access '/home/user/testfile.txt': No such file or directory

 

문제 2 # 호스트에 파일이 존재하고 컨테이너 내부에도 있는 경우

호스트에 있는 파일을 컨테이너 내부에서도 사용할 수 있도록 Bind Mount 또는 COPY 명령어를 사용해야 한다.

컨테이너 내부에서 해당 파일을 검색하고 내용이 동일한지 확인해야 한다.

 

방법 1: Bind Mount 

방법 2: Dockerfile을 사용하여 이미지에 포함시키기

 

 


 

'Docker' 카테고리의 다른 글

Docker Compose  (0) 2025.03.25
Docker Architecture  (0) 2025.03.19