본문 바로가기

Docker & Kubernetes

도커 & 쿠버네티스 스터디 1주차 : Docker 소개 & Docker 활용

개요 : 컨테이너란?

  • 컨테이너는 애플리케이션(프로세스) 동작에 필요한 파일들만 패키징된 이미지를 실행하여 동작
  • 컨테이너는 컨테이너 환경이 조성된곳 어디에서나(온프레미스/클라우드 환경)에서도 실행할 수 있음
  • —> 그렇기 때문에 요즘은 컨테이너가 개발 표준 환경으로 자리잡음

실습 환경 설정 : AWS (window)

  • MAC, WINDOW 등 다양한 환경의 사용자들의 환경을 맞추기 위해 서버를 하나 올리고 접속하여 명령어를 실행하고자 함.
  1. AWS 로그인 후 오른쪽 위의 리전은 서울 리전으로 맞추기

 

2. 검색창에 EC2 검색, EC2 들어가기

2-1. 왼쪽 네트워크 및 보안 → 키페어 들어가기

 

2-2. 키페어 생성하기

키페어는 생성하여 다운로드하여 가지고 있는다.

 

3. 주소창에 myip.com 입력하여 접속하기

Your IP address is : 다음에 나와있는 IP를 통하여

내 pc가 사용하고 있는 공인 IP 주소를 확인할 수 있다.

 

공인 IP 주소는 추후 사용되므로 이를 복사해둔다.

 

4. 다시 AWS에 돌아와서 cloudFormation 검색

스택 생성 → 새 리소스 사용을 누른다.

4-1 . Amazon S3 URL 부분에 공유해주신 url을 넣기

 

 

그리고 다음을 누른다.

 

4-2. 스택 이름 입력하고, KeyName 에는 2번에서 생성한 key 를 선택한다.

SgIngressSshCidr 에는 위에서 확인한 내 공인 IP주소를 입력하고 /32라고 입력한다.

나머지는 그대로 둔다.

 

그리고 다음, 다음, 전송을 누르면 서버가 만들어진다.

 

 💡
위에서 한 작업 부연설명 - AWS에서는 VPC라고 가상 네트워크가 있고, VPC 내에 subnet이라고 네트워크 격리된 레이어가 있다. EC2를 배포하기 위해서는 그 격리된 네트워크 구간에 또 한번 더 배포를 해야 하는데, 그러한 일련의 작업들을 한번에 다 해주는 템플릿 파일이다.

생성된 서버 안에는 스크립트가 미리 실행되어 배포된다.

 

5. CREATE_COMPLETE가 된 것을 확인하면 다시 EC2쪽으로 들어간다.

 

 

6. EC2에 들어가서 인스턴스를 클릭하면, “MyServer”라는 이름의 인스턴스가 있고, 이 서버를 클릭하면 퍼블릭 IP 주소가 뜬다. 이 주소를 복사하고, 이 주소를 통해 ssh에 접속한다.

 

 

7. 윈도우 사용자이므로, SSH 터미널 프로그램(MobaXterm)을 미리 설치하였다.

 

MobaXterm을 사용해서 EC2에 접근하려면 먼저 왼쪽 위의 Session을 클릭한 후, Remote host에 퍼블릭 IP를 입력한다. Specify username에는 ubuntu를 입력하고, Use Private key에 체크한 후 위에서 다운로드 받은 private key를 선택한 후 OK를 클릭한다.

 


도커란?

  • 가상 실행 환경을 제공해주는 오픈소스 플랫폼 (지금은 유료화를 진행하려 하고 있다)
  • 도커의 가상 실행 환경을 컨테이너라고 부른다.
  • 컨테이너 = 도커는 아니지만, 컨테이너 생태계를 거의 도커가 차지하고 있기 때문에 도커와 컨테이너를 동일선상으로 부르기는 한다.
  • 더 정확한 용어는 → 컨테이너화된 프로세스
  • 도커 플랫폼이 설치된 곳이라면 애플리케이션을 어디서든 실행할 수 있는 장점을 가진다.
  • VM과 컨테이너의 차이점 → VM에 있는 Hypervisor는 뜨는 데 시간이 오래 걸린다. 컨테이너는 빠르게 실행할 수 있다. (컨테이너는 프로세스이기 때문에)

도커 아키텍처

  • 도커는 클라이언트(Client), 도커 호스트(Docker Host), 레지스트리(Registry) 3가지로 분류가 된다.
  • 도커 cli (명령어)를 치는 부분은 Client로, Client가 Host로 명령어를 전달해준다.
  • Docker Host는 명령어를 받아서 일련의 작업들을 수행해주는 서버이다.
  • 레지스트리는 도커 이미지(프로세스를 격리해 놓은 단위들)가 올라가는 저장소이다.

https://docs.docker.com/get-started/docker-overview/#docker-architecture

  • 클라이언트에서 docker run 이라는 명령어를 수행했을 때, Docker Host에서 Docker daemon이 그 명령어를 받아주고, 로컬에 해당 이미지가 있으면 바로 가져오고, 이미지가 없으면 입력된 레지스트리에 가서 이미지를 pull 해서 다시 실행시켜주는 작업을 수행한다.

리눅스 프로세스의 이해

  • 프로세스는 실행 중인 프로그램의 인스턴스를 의미
  • OS에서 프로세스를 관리
  • 각 프로세스는 고유한 ID(PID)를 가짐
  • 프로세스는 CPU와 메모리를 사용하는 기본 단위로, OS 커널에서 각 프로세스의 자원을 관리
  • 도커는 프로세스를 격리하는 것, 이 프로세스를 관리하는 것은 커널에서 진행하기 때문에 커널단에서 격리된다고 보면 된다.

 

# 프로세스 정보 확인
ps

# root 계정으로 들어가기
sudo su - 

# /sbin/init 1번 프로세스 확인
# 프로세스별 CPU 차지율, Memory 점유율, 실제 메모리 사용량 등 확인 >> 비율로 표현되는 이유는?
ps -ef

# 실시간 프로세스 정보 출력
top -d 1
htop

# 특정 프로세스 정보 찾기
pgrep -h

# [터미널1]
sleep 10000

# [터미널2]
pgrep sleep
pgrep sleep -u root
pgrep sleep -u ubuntu
  • 리눅스에서는 프로세스를 동적으로 관리하게 되는데, proc라는 디렉토리를 참고한다.
  • ls /proc를 하면 프로세스 ID가 보인다.
  • 하나를 지정해서 들어가보면 (ls /proc/171) 이 프로세스가 사용하고 있는 cpu, 메모리 등의 정보가 다 파일로 관리되는 것을 볼 수 있다.
  • 도커 데몬은 이런 프로세스를 보고, 관리한다.
# proc 마운트 정보
mount -t proc

#
ls /proc

# 커널이 동적으로 생성하는 정보
cat /proc/cpuinfo
cat /proc/meminfo
cat /proc/uptime
cat /proc/loadavg
cat /proc/version
cat /proc/filesystems
cat /proc/partitions

# 실시간(갱신) 정보
cat /proc/uptime
cat /proc/uptime
cat /proc/uptime

# 프로세스별 정보
ls /proc > 1.txt

# [터미널1]
sleep 10000

# [터미널2]
## 프로세스별 정보
ls /proc > 2.txt
ls /proc
diff 1.txt 2.txt
ps -C sleep

## sleep 프로세스 디렉터리 확인
tree /proc/$(pgrep sleep) -L 1
tree /proc/$(pgrep sleep) -L 2 | more

## 해당 프로세스가 실행한 명령 확인
cat /proc/$(pgrep sleep)/cmdline ; echo

## 해당 프로세스의 Current Working Directory 확인
ls -l /proc/$(pgrep sleep)/cwd

## 해당 프로세스가 오픈한 file descriptor 목록 확인
ls -l /proc/$(pgrep sleep)/fd

## 해당 프로세스의 환경 변수 확인
cat /proc/$(pgrep sleep)/environ ; echo
cat /proc/$(pgrep sleep)/environ | tr '\\000' '\\n'

## 해당 프로세스의 메모리 정보 확인
cat /proc/$(pgrep sleep)/maps

## 해당 프로세스의 자원 사용량 확인
cat /proc/$(pgrep sleep)/status

## 기타 정보
ls -l /proc/$(pgrep sleep)/exe
cat /proc/$(pgrep sleep)/stat
  • 프로세스에 대한 심화 명령어

도커 설치

# [터미널1] 관리자 전환
sudo su -
whoami

# 도커 설치
curl -fsSL <https://get.docker.com> | sh

# 도커 정보 확인 : Client 와 Server , Storage Driver(overlay2), Cgroup Version(2), Default Runtime(runc)
docker info
docker version

# 도커 서비스 상태 확인
systemctl status docker -l --no-pager

# 도커 루트 디렉터리 확인 : Docker Root Dir(/var/lib/docker)
tree -L 3 /var/lib/docker

# ubuntu 사용자를 docker 그룹에 추가 (docker socket 접근 권한)
sudo usermod -aG docker ubuntu

# 위 명령어 입력 후 SSH 재접속
exitsu
  • 도커 설치 후 정보를 보면 Server와 Client가 같이 설치된 것을 확인할 수 있다.

도커를 루트 계정 아닌 사용자로 사용하기

💡
유닉스 도메인 소켓(Unix Domain Socket)이라는 개념 - 도커는 유닉스 도메인 소켓을 통하여 클라이언트와 서버 간에 통신을 하게 된다. tcp 통신이 아니다 보니 속도가 빠르다. (통신 상태 효율이 좋음)

 

  • 도커를 설치할 때 docker라는 사용자 그룹이 생성이 된다.
  • 이 그룹에 해당 유저를 추가시켜 주어야 한다.
  • root에서 ubuntu 사용자로 돌아오기 → logout 두번
  • 이후 docker ps를 해보면 해당 유저는 도커 데몬 소켓에 대한 접근 권한이 없기 때문에, 에러가 뜬다.

  • 해당 유저를 도커 소켓에 접근할 수 있도록 권한을 부여한다.
  • sudo usermod -aG docker $USER 로 권한을 추가한 다음 exit으로 한번 터미널 밖으로 나갔다가 들어와야 한다.
  • 그러면 docker ps를 했을 때 명령어가 실행된다.

 

[참고]

# [터미널2] 일반 유저 ubuntu 로 실습 진행
whoami

# Create the docker group : 도커 스크립트 생성 시 자동 생성되어 그룹 확인만 진행
sudo groupadd docker
getent group | tail -n 3

# Add your user to the docker group.
echo $USER
sudo usermod -aG docker $USER

# ssh logout
exit

# ssh 재접속 후 확인

#
docker info

# 컨테이너 실행
docker run hello-world

#
docker ps
docker ps -a
docker images

# 중지된 컨테이너 삭제
docker ps -aq
docker rm -f $(docker ps -aq)
docker ps -a

도커 기본 명령어

# 현재 컨테이너 프로세스 조회
docker ps
docker ps -a # 한번 실행했다 죽은 프로세스 조회

# 이미지 다운로드
docker pull hello-world:latest

latest: Pulling from library/hello-world
e6590344b1a5: Pull complete
Digest: sha256:424f1f86cdf501deb591ace8d14d2f40272617b51b374915a87a2886b2025ece
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

# 로컬 이미지 조회
docker images

# 도커 실행
docker run --help
docker run --name first-docker hello-world:latest # 실행되었다 바로 죽음
docker run --name nginx-docker -p 3000:80 -d nginx:latest
# 로컬에 이미지가 없어서 Unable to find image 'nginx:latest' locally 메시지가 뜨고, 
# 도커 허브에서 이미지를 당겨와 실행한다. 
# -p 3000:80 옵션은 호스트(내 컴퓨터)의 3000번 포트를 도커 컨테이너 내의 80번 포트와 매핑(mapping)

# 네트워크 상태 조회
netstat -ntlp
# 호스트의 3000번 포트가 열려있는 것을 확인할 수 있음

# 컨테이너 프로세스 재조회 -> nginx 실행 조회 가능
docker ps --help
docker ps
docker ps -a

# 컨테이너 로그 조회
docker logs --help
docker logs first-docker
docker logs -f nginx-docker
# -f 옵션은 터미널에서 로그를 계속 지켜보는 것

# 컨테이너 내부에서 sh 실행
docker exec --help
# exec -> 지금 실행되고 있는 컨테이너 안으로 들어가는 옵션 

# [터미널 2] ------------------------------------------------------------
docker exec -it nginx-docker /bin/bash # 컨테이너 내부로 들어감
curl  # nginx 호출 -> 터미널 1에서 보면 127.0.0.1 이라는 IP가 찍혀 있음

#
exit
curl  # 3000번 포트 호출 -> 터미널 1에서 보면 172.17.0.1이라는 IP가 찍혀 있음

curl <https://ipinfo.io/ip> # -> 결과로 내 공인 IP를 알 수 있음 
# 웹 브라우저를 열고 공인 IP의 3000번 포트로 접속하기
# -> 터미널 1의 로그에는 106.250.183.178 이라는 IP가 찍힘

# IP 가 다른 이유는?? -> 컨테이너 네트워크에서 다룰 내용
127.0.0.1 - - [10/Apr/2025:10:53:31 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
172.17.0.1 - - [10/Apr/2025:10:53:57 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.81.0" "-"
221.143.247.216 - - [10/Apr/2025:10:54:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.7.1" "-"

# 컨테이너 정보 조회
docker inspect nginx-docker

... 생략 ...
"Networks": {
    "bridge": {
        "Gateway": "172.17.0.1",
        "IPAddress": "172.17.0.2",
    }
}
... 생략 ...

# 컨테이너 네트워크 조회
docker network ls
docker network inspect bridge
docker network inspect bridge | grep 'Containers' -A8

## 172.17.0.1 은 어디서 온걸까?
ip addr
ip addr show dev docker0

# 컨테이너 중지
docker stop nginx-docker
docker stop $(docker ps -aq)

# 컨테이너 제거
docker rm nginx-docker
docker rm $(docker ps -aq)

# 컨테이너 이미지 제거
docker rmi nginx:latest hello-world:latest
  • docker network ls

  • 컨테이너 네트워크를 조회하는 명령어, bridge, host, null이라는 종류가 있다.
  • nginx가 bridge 네트워크를 사용하고 있다.
  • docker network inspect bridge: bridge 상세 정보 조회

 

ip addr : 해당 우분투 서버가 가지고 있는 네트워크 인터페이스를 조회하는 명령어

 

  • docker-0라는 인터페이스에 이더넷 172.17.0.1이라는 IP를 가지고 있다.
  • 내가 현재 배포했던 nginx container는 bridge라는 네트워크를 통해서 ubuntu host vm과 통신을 하고 있다, ip는 이런 식으로 구성이 되어있다는 것을 알 수 있다.

자주 사용하는 docker run 명령어

docker run --help

# 필수로 알아야 하는 옵션
-d        : [Detach] 백그라운드에서 컨테이너 프로세스 실행 (nohup &)
-i        : [Interactive] 표준입력 연결 (입력) (-it 는 세트)
-t        : [tty] 가상 터미널 생성 - 상호작용 (출력) (-it 는 세트)
--name    : 컨테이너 이름 지정
-p        : [Publish] 호스트 포트와 컨테이너 포트 매핑
--restart : 재시작 정책 [always 항상, on-failure 에러 코드 0이 아닌 경우, unless-stopped 수동으로 중지하지 않는 한]
# 이런 옵션을 통해 컨테이너가 항상 실행된다는 가용성을 어느 정도 보장한다.
-v        : [volume] 호스트 볼륨 마운트
# 호스트의 볼륨과 컨테이너 볼륨을 연결.
# 컨테이너가 죽으면 데이터가 날아가는데, 데이터의 영속성을 유지할 수 있는 옵션

# 알아두면 좋은 옵션
-u        : [user] 컨테이너 내부 실행 유저
--rm      : 컨테이너 종료 시 자동 삭제, 임시 사용 [restart 와는 동시 사용 불가]

## 참고
# --restart 와 --rm 은 같이 사용할 수 없다. (내용상 상호 충돌이 일어남)
# -d 와 -it 는 함께 사용할 수 없다. 
# (이렇게 하고 싶을 때는 -d를 통해 백그라운드 실행을 하고 docker exec -it를 활용한다)

# 예시
mkdir ~/volume-mount && cd ~/volume-mount && touch hello.txt

docker run \\
	--name nginx-docker \\
	-d \\
	-p 3000:80 \\
	-v $(pwd):/tmp-dir \\
	--restart always \\
	nginx:latest
	
docker exec -it nginx-docker /bin/bash

 

 

  • 예시 실습 및 설명
  • mkdir ~/volume-mount && cd ~/volume-mount && touch hello.txt
    • 호스트 내의 volume-mount 디렉토리 생성 및 파일 생성
  • docker run \ --name nginx-docker \ -d \ -p 3000:80 \ -v $(pwd):/tmp-dir \ --restart always \ nginx:latest
    • 호스트의 volume-mount 디렉토리와 컨테이너 내부의 tmp-dir를 연결함
  • docker exec -it nginx-docker /bin/bash
    • 직접 nginx 내부로 들어가서 해당 tmp-dir 디렉토리로 들어가면 hello.txt 파일을 확인 가능하다.
  • 그렇기 때문에 컨테이너가 죽어도, volume-mount는 살아있으므로 컨테이너가 죽었을 때 유지되어야 하는 파일들을 유지할 수 있다.

Docker Hub와 Amazon ECR Public Gallery

  • Docker Hub : 부분 유료화, 6시간 기준 로그인 유저 200회 / 비로그인 유저 100회
  • 실제 운영상에 CICD 파이프라인에 컨테이너 이미지 빌드 단계가 들어간다면 ECR Public Gallery / Private Repository 사용 권장

컨테이너 네트워크

도커 네트워크 모델 (기본)

  • Bridge
    • 사실상 도커 네트워크를 쓸 때 브릿지 이외를 사용하는 일은 거의 없다.
  • Host
  • None

Bridge 모드

  • 도커에서 기본적으로 쓸 수 있는 네트워크
  • 컨테이너 기본 생성 시 자동으로 docker0 브리지를 사용
  • 기본 172.17.0.0/16 대역을 컨테이너가 사용, 대역 변경 설정 가능
  • 도커 컨테이너 간 상호 통신 허용, 호스트 컨테이너 간 통신 허용

컨테이너 통신 테스트

# 터미널1 (kn) : kn 이름의 busybox 컨테이너 생성
docker run -it --name=kn --rm busybox
ip addr # 결과 172.17.0.3

# 터미널2 (ou) : ou 이름의 busybox 컨테이너 생성
docker run -it --name=ou --rm busybox
ip addr # 결과 172.17.0.4
# 이를 통해 알 수 있는 것 -> 172.17 대역에서 1, 2, 3, 4.. 이렇게 네트워크가 형성이 되는구나

# 터미널3 (호스트)
docker ps
# 터미널3 (호스트)
sudo tcpdump -i docker0 -n # 네트워크 패킷 캡쳐하여 가져올 수 있는 리눅스 명령어

# 터미널1 (kn)
# Ping (kn -> ou)
ping -c 4 172.17.0.4

# 터미널2 (ou)
# Ping (ou -> kn)
ping -c 4 172.17.0.3

터미널 1 실습

 

터미널 2 실습

 

터미널 3 실습

  • 컨테이너 내부의 통신은 브릿지 네트워크를 통해서 한다는 것을 알 수 있다
  • 컨테이너 외부의 통신은?

컨테이너 외부 통신 테스트

# 터미널3 (호스트)
sudo tcpdump -i any icmp # any : 모든 인터페이스를 캡쳐하기
# icmp : ping을 의미하는 프로토콜, icmp 패킷을 전부 캡쳐

# 터미널1 (kn)
# Ping (kn -> google.com)
ping -c 1 8.8.8.8

# 터미널2 (ou)
# Ping (ou -> kn)
ping -c 1 8.8.8.8

터미널 3 실습

결과에 대한 설명

  • vethae24e06 → docker0 → ens5 →docker0 → vethae24e06
  • ip a로 확인해보면 9번에 연결되어있다.
  • 터미널 1에서 ip a 해보면 컨테이너 내부에서 9번 인터페이스와 연결되어 있음을 알 수 있다.
  • 이 컨테이너는 결국 호스트 vm의 가상 이더넷 인터페이스인 9번과 연결되어있다.
  • 컨테이너에서 나온 네트워크 트래픽은 이 9번 인터페이스 통해서 호스트 vm까지 나오고, docker0 브릿지 인터페이스 게이트웨이를 통하여 밖으로 나온다. 외부와 통신할 때는 호스트 vm의 ip를 타고 나온다. (들어올 때는 역순)
  • 결론은 컨테이너에도 네트워크 인터페이스가 있는데, 컨테이너 네트워크 인터페이스에서 나와서 vm의 ip를 달고 외부와 통신을 한다. 들어올 때는 다시 vm을 타고 들어온다.
  • 컨테이너가 2개가 떠있고 각각 ip는 다르지만, 외부와 통신할 때의 ip는 똑같다.

Host 모드

  • 호스트 네트워크 환경을 컨테이너에서 그대로 사용 (동기화)
  • 브릿지 → 네트워크 대역이 따로 있었는데, 호스트에서는 이러한 대역이 없다. ubuntu의 ip 대역을 그대로 쓴다. 외부와 통신할 때도 컨테이너 ip가 우분투의 ip와 동일하다.
# 컨테이너 실행
docker run --rm -d --network host --name my_nginx nginx

# HostConfig.NetworkMode "host" , Config.ExposedPorts "80/tcp" , NetworkSettings.Networks "host" 확인
docker inspect my_nginx
# 이전과 비교했을때 Networks가 "host"이고 IP도 나오지 않음

# 네트워크 상태 확인
netstat -ntlp

# curl 접속 확인
curl -s localhost | grep -o '<title>.*</title>'

# 추가 실행 시도
docker run -d --network host --name my_nginx_2 nginx

# 확인
docker ps -a

# 삭제
docker container stop my_nginx
  • 사실상 호스트 모드는 도커에서는 사용하지 않는데, 쿠버네티스에서는 사용하는 경우가 있다.

None 모드

  • 컨테이너에 네트워크 설정을 부여하지 않는 모드
  • 컨테이너가 ip를 안 달고 있다 → 외부, host와 통신을 할 수 없다. 완전히 격리된 private한 환경
# None 네트워크로 Nginx 실행
docker run --rm -d --network none --name my_nginx nginx
 
# Docker Inspect 로 정보 확인
docker inspect my_nginx | grep NetworkMode

# 네트워크 정보 조회
netstat -ntlp

# 컨테이너 내부 환경 확인
docker exec -it my_nginx /bin/bash

ip addr
ping 8.8.8.8
curl localhost
  • none 모드 사용 - 배치성 컨테이너
  • none 모드 실습


컨테이너 이미지 빌드

Dockerfile

  • Docker Image 생성을 위한 일련의 설계도
  • 어떤 OS, 파일 복사 내용, 어떤 패키지 설치, 어떤 명령어 수행을 순서대로 적어둔 스크립트 파일
  • docker build 명령어 수행 시 옵션으로 주입하여 사용

Dockerfile 요소

  • FROM : 베이스 이미지 지정 (필수)
  • WORKDIR : 작업 디렉토리 지정 (cd 명령어)
  • COPY : 호스트에서 컨테이너로 파일 및 디렉토리 복사
    • 소스코드를 컨테이너 안에 넣어서 빌드를 실행한다든지 할 때 사용
  • RUN : 쉘 명령어 수행
  • ENV : 환경 변수 설정
  • ARG : 빌드 시 인자 설정 (빌드 후 제거)
    • 같은 도커 파일이더라도 인자값이 달라질 때 사용
  • CMD : 컨테이너 시작 시 실행할 기본 명령어
  • ENTRYPOINT : CMD 와 비슷하나, 고정 명령

Dockerfile 테스트

# 실습을 위해 기존 이미지 제거
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
docker rmi $(docker images -aq)

# 테스트 디렉토리 생성
mkdir ~/python && cd ~/python

cat << EOF >> app.py
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type','text/plain')
        self.end_headers()
        self.wfile.write(b"Hello, Docker!")

if __name__ == '__main__':
    server = HTTPServer(('0.0.0.0', 8000), Handler)
    print("Server running on port 8000...")
    server.serve_forever()
EOF

cat << EOF >> hello.txt
Hello World
EOF

cat << EOF >> dockerfile
# 베이스 이미지
FROM python:3.10-slim

# 컨테이너 안에 디렉토리 만들고 이동
WORKDIR /app

# 전체 디렉토리 파일 복사
COPY . .

# 컨테이너가 실행할 기본 명령
CMD ["python", "app.py"]
EOF

# Docker build 명령어 수행 (파일명이 dockerfile 일 경우 -f 생략 가능
docker build -t my-first-python -f dockerfile .

#
docker images

# 터미널 1
docker run --rm -p 8000:8000 my-first-python

# 터미널 2
netstat -ntlp
curl <http://localhost:8000>; echo
  • app.py는 컨테이너를 호출했을 때 출력값이 나오는 프로그램
  • dockerfile
    • 이 도커 이미지는 python:3.10-slim 이미지에서 시작해서
    • /app 디렉토리로 가서
    • 내 호스트 현재 위치에 있는 파일을 /app 디렉토리에 복사해라
    • 그리고 이미지가 끝나고 컨테이너를 실행했을 때 python app.py를 실행하라는 도커 파일

.dockerignore

  • .gitignore 처럼 docker build 시 Container 내부에 복사하지 않도록 지정하는 파일
  • dockerfile 과 같은 경로에 위치
  • 민감 정보 유출 방지
  • 불필요한 파일을 제외하여 빌드 속도 상승
# 터미널1 - 기존 컨테이너 실행
docker run -d -p 8000:8000 --name origin my-first-python
docker exec -it origin /bin/bash
ls -lah

# 터미널2
# .dockerignore 파일 생성 
# **dockerfile 과 같은 경로**에 위치해야 함
cat << EOF >> .dockerignore
# hello.txt 제외
hello.txt
EOF

#
docker build -t my-second-python -f dockerfile .

#
docker run -d -p 8001:8000 --name modify my-second-python
docker exec -it modify /bin/bash

#
ls -lah

# 실습 후 컨테이너 제거
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
docker rmi $(docker images -aq)

 

dockerignore 적용 뒤 hello.txt 제외된 것 확인


이미지 빌드 시 고려사항

이미지 크기 단축

  • 컨테이너 이미지가 크면 클수록 빌드 & 배포 & 가동 시간이 길어짐
  • 클라우드 환경일 경우 네트워크 트래픽이 증가하여 비용 상승

Alpine Image

  • 경량화된 이미지
mkdir ~/alpine && cd ~/alpine

# 예제 파일 생성
cat << EOF >> hello.py
print("Hello from Docker!")
EOF

# 기본 파이썬 이미지
cat << EOF >> normal-dockerfile
FROM python:3.10

WORKDIR /app
COPY hello.py .
CMD ["python", "hello.py"]
EOF

# Alpine 파이썬 이미지
cat << EOF >> alpine-dockerfile
FROM python:3.10-alpine

WORKDIR /app
COPY hello.py .
CMD ["python", "hello.py"]
EOF

# 이미지 빌드
docker build -t normal -f normal-dockerfile .
[+] Building 19.3s (8/8) FINISHED

docker build -t alpine -f alpine-dockerfile .
[+] Building 3.5s (8/8) FINISHED

# 이미지 확인
docker images
SITORY   TAG       IMAGE ID       CREATED                  SIZE
alpine       latest    2cdffb617a98   About a minute ago   51.1MB
normal       latest    8c7ec72061f5   2 minutes ago        1GB
  • CICD 파이프라인의 시간을 줄이는 데 효과적이다.

Multi Stage Build

  • 도커 공식문서에서도 권장하는 방법
  • 빌드 만을 위한 파일과 어플리케이션 실행을 위한 파일을 분리하여 실행 컨테이너 크기 축소
    • ex) Java - 코드 파일(빌드 파일) / Jar - 실행 파일
mkdir ~/multi && cd ~/multi

cat << EOF >> main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF

# 기존 빌드
cat << EOF >> origin-dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.23
WORKDIR /src
COPY main.go .
RUN go build -o /bin/hello ./main.go
CMD ["/bin/hello"]
EOF

# Multi Stage Build
cat << EOF >> multi-dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.23 AS build
WORKDIR /src
COPY main.go .
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=build /bin/hello /bin/hello
CMD ["/bin/hello"]
EOF

# 이미지 빌드
docker build -t origin -f origin-dockerfile .
docker build -t multi -f multi-dockerfile .

# 컨테이너 실행 확인
docker run origin
docker run multi

# 이미지 확인
docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
multi        latest    b1288266668c   21 seconds ago   2.13MB
origin       latest    a75c44b075a8   31 seconds ago   870MB
  • 용량, 빌드 속도 면에서 유의미한 차이를 보임

멀티 플랫폼 빌드

  • 도커는 라이브러리가 패키징되어 어디에서나 실행될 수 있다고 알려있지만, CPU 아키텍처가 달라지므로 멀티 플랫폼 빌드가 필요하다.
  • CPU Architecture가 다르면 호환이 안 된다.
  • 한 마디로 하나의 빌드 이미지와 태그를 통해서 각기 다른 CPU 아키텍처에서 사용할 수 있는 이미지를 생성하는 것
  • ARM CPU Architecture 를 가진 서버와 컴퓨터가 인기를 끌면서 사용 (EC2 Graviton, Mac Apple Silicon)
  • linux/amd64, linux/arm64, windows/amd64

컨테이너 보안

컨테이너 유저 변경

  • root 유저로 실행
mkdir ~/non-root && cd ~/non-root

cat << EOF >> hello.sh
#!/bin/sh
echo "Hello from multi-architecture Docker image!"
EOF
chmod +x hello.sh

cat << EOF >> root-dockerfile
FROM ubuntu
COPY hello.sh /hello.sh
CMD ["/hello.sh"]
EOF

# 빌드
docker build -t root -f root-dockerfile .
docker run -it root /bin/bash

#
whoami
id
apt update -y && apt install nginx -y
  • 컨테이너가 탈취되었을 때 관리자 권한으로 악성 프로그램을 설치할 수도 있고, 호스트에 공격을 가할 수도 있다.
  • root 유저가 아닌 non-root 유저로 컨테이너 실행
cat << EOF >> non-root-dockerfile
FROM ubuntu
RUN useradd -m -u 1001 appuser
USER appuser
COPY hello.sh /home/appuser/hello.sh
EOF

# 빌드
docker build -t non-root -f non-root-dockerfile .
docker run -it non-root /bin/bash

#
whoami
id
apt update -y
  • 해당 사용자는 root 권한을 얻을 수 없다.

실습 결과

클라이언트 인증 활성화

  • Docker 명령어를 아무나 수행할 수 없도록 인증 단계 추가 - OPA라는 곳에서 제공
    • OPA는 쿠버네티스에서도 사용된다.
  • Docker에도 플러그인을 많이 설치할 수 있다.
docker plugin ls # 도커 플러그인 조회

# 정책 디렉토리 생성
sudo mkdir -p /etc/docker/policies
sudo touch /etc/docker/policies/authz.rego

# 모든 사용자에게 모두 허용
echo "package docker.authz
allow = true" | sudo tee -a /etc/docker/policies/authz.rego

# Docker Plugin Install - OPA
## 수락 필요
sudo docker plugin install openpolicyagent/opa-docker-authz-v2:0.4 opa-args="-policy-file /opa/policies/authz.rego"

# Docker Daemon 설정
if [ ! -f /etc/docker/daemon.json ]; then
  sudo touch /etc/docker/daemon.json
fi
echo "{
  \\"authorization-plugins\\": [\\"openpolicyagent/opa-docker-authz-v2:0.4\\"]
}" | sudo tee -a /etc/docker/daemon.json

# 설정 확인
cat /etc/docker/daemon.json

# 플러그인 활성화 적용 (데몬 재시작)
sudo systemctl restart docker

# 정책 변경 (모두 사용 불가)
sudo vim /etc/docker/policies/authz.rego
allow = false 변경

# Docker 명령어 수행 -> 수행할 수 없음
docker ps

Docker Compose

  • 멀티 컨테이너 실행을 위한 애플리케이션 도구
  • 여러 서비스를 같이 구성할 때 사용 - 컨테이너 간 의존 관계가 있을 때 등
  • YAML 문서로 정의 - 선언형
  • Docker CLI 가 아닌 YAML 선언된 구성을 실행하기 때문에 이력 관리에도 도움

실습 환경 삭제

  • AWS는 사용한 만큼 돈이 나오기 때문에 실습 완료 시 CloudFormation을 삭제하여 한번에 실습 환경을 삭제해야 한다.