목차
- 도커의 역사
- 기술적 Overview
- 도커 설치와 실행
- 서드 파티 컨테이너 사용
- Demo 어플리케이션
- 컨테이너 이미지 만들기
- 컨테이너 레지스트리
- 컨테이너 돌리기
- 컨테이너 보안
- 도커 객체와 상호작용
- workflow 개발
- 컨테이너 배포

왜 컨테이너가 만들어졌고, 왜 도커는 성공했을까?
도커는 개발자의 환경과 사용자의 환경을 최대한 가깝게 만들어준다.
개발
역사적으로 개발자의 환경은 복잡한 프로제스를 포함시킨다. 도커는 이것을 개발자에게 단순 명령, 도커 compsed up, 그리고 윈도우, 맥, 리눅스에서 양립 가능하도록 허용하면서 간단하게 만들어준다.

배포
배포 어플리케이션은 주로 몇가지의 절차를 포함해왔다. 서버를 만들고 dependency를 설정한다음 어플리케이션 코드를 복사했다. 콘테이너는 단순 표준 패키지를 소개했고, 단순히 필요한 컨테이너와 컨테이너 이미지만 가지고 배포를 가능하게 한다.

컨테이너란?
도커 컨테이너 이미지란 가볍고, 독립적인 소프트웨어 실행패키지이다. 어플리케이션을 실행시키기 위한 모든 필요한것을 포함하고 있다.

컨테이너 이미지와 컨테이너의 차이점은 객체지향형 프로그램에서 클래스와 인스턴스의 차이 라고 생각하면 쉽다.
- 컨테이너 이미지 - class
- 컨테이너 - instance
Open Container Initiative (OCI)
OCI는 컨테이너 포멧에 대한 표준을 만들기 위해 만든 업계의 협력이다. Docker, Google, VMware, Microsoft, Dell, IBM, and Oracle 같은 회사들로부터 설립되었다. OCI는 세가지 주요한 스펙을 정의한다.
이미지 Specification: 이미지의 메타데이터와, 직렬 파일 시스템을 포함한 포멧을 정의한다.
런타임 Specification: 이미지 사양을 준수하는 이미지를 사용하여 컨테이너를 실행하는 방법을 설명한다.
분배 Specification: 이미지가 어떻게 분배되어야 하는가에 대한 개요, 예를 들어 레지스트리를 통해 이미지를 푸시하고 가져오는 등의 작업을 수행한다.

가상화의 발전
가상화가 발명되기 전에는 모든 프로그램이 호스트 시스템에서 직접 실행되었다. 사람들은 이것을 베어메탈(Bare Metal)이라고 부르는데, 베어 메탈에서 실행하는 것은 우리가 랩톱/데스크탑 컴퓨터에 프로그램을 설치할 때마다 수행하는 작업이다.


가상 머신은 호스트 리소스를 여러 개의 격리된 가상 하드웨어 구성으로 분할할 수 있는 "하이퍼바이저"라는 시스템을 사용한다. 그런 다음 이를 자체 시스템(각각 OS, 바이너리/라이브러리 및 애플리케이션 포함)으로 처리할 수 있다.

컨테이너는 바이너리/라이브러리 설치 및 구성을 위한 격리된 환경을 제공한다는 점에서 가상 머신과 유사하지만, 하드웨어 계층 컨테이너에서 가상화하는 대신 기본 Linux 기능(cgroups + 네임스페이스)을 사용하여 동일한 커널을 공유하면서 격리를 제공한다.

Linux Building Blocks
컨테이너는 세 가지 Linux 기능을 활용하여 기능을 발휘한다.
네임스페이스
네임스페이스는 전역 시스템 리소스를 추상화로 래핑하여 네임스페이스 내의 프로세스에 전역 리소스의 격리된 인스턴스가 있는 것처럼 보이게 한다.
전역 리소스에 대한 변경 사항은 네임스페이스의 구성원인 다른 프로세스에 표시되지만 다른 프로세스에는 표시되지 않는다.
네임스페이스를 사용하면 컨테이너 런타임은 컨테이너 외부의 프로세스를 컨테이너 내에서 보이지 않게 유지하거나 컨테이너 내부의 사용자를 호스트의 다른 사용자에게 매핑할 수 있다.
cgroup
Cgroup은 프로세스를 계층적 그룹으로 구성하여 다양한 유형의 리소스 사용을 제한하고 모니터링할 수 있는 Linux 커널 기능이다
cgroup을 사용하면 컨테이너 런타임은 컨테이너가 다음을 사용할 수 있도록 지정할 수 있다.
유니온 파일 시스템
통합 파일 시스템을 사용하면 분기라고 알려진 별도의 파일 시스템의 파일과 디렉터리를 투명하게 오버레이하여 일관된 단일 파일 시스템을 형성할 수 있다.
병합된 분기 내에서 동일한 경로를 갖는 디렉터리의 내용은 새로운 가상 파일 시스템 내의 단일 병합 디렉터리에 함께 표시된다.
이 접근 방식을 사용하면 공통 레이어를 공유할 수 있으므로 공간을 효율적으로 사용할 수 있다.
예를 들어, 동일한 이미지의 여러 컨테이너가 단일 호스트에 생성된 경우 컨테이너 런타임은 각 컨테이너에 특정한 씬 오버레이만 할당하면 되며 기본 이미지 레이어는 공유될 수 있다.
각각 리소스 제약 조건과 애플리케이션 격리를 제공하는 Cgroup 및 네임스페이스. 또한 공통 레이어에 이미지를 구축할 수 있는 통합 파일 시스템을 사용하여 이미지를 빠르고 효율적으로 구축하고 공유할 수 있다.

Docker Desktop Architecture

A client application
A Linux virtual machine containing
Registries
설치를 완료하면 이제 간단한 컨테이너를 구동해보자.
docker run --env POSTGRES_PASSWORD=ansadmin --publish 5432:5432 postgres:15.7-alpine


컨테이너 작동이 잘 된다....!
pgAdmin4 서버 connection에서도 연결이 된다...!

데이터의 지속성

우리가 컨테이너 이미지로 부터 컨테이너를 만들떄, 이미지에 있는 모든것은 read-only로 취급된다. 그리고 맨위에 새로운 layer가 read/wirte로 쒸워진다.
dependency 설치 데이터 보존
이제 컨테이너가 runtime할때 무언가를 설치하는 방법에 대해서 실습을 해보자. 물론 컨테이너가 실행될때 content를 수정하는 것은 주된 행동은 아니지만 학습 목적으로 해보자
# 우분투 이미지로부터 컨태이너 만들기
docker run --interactive --tty --rm ubuntu:22.04
-it (또는 --interactive와 --tty): 컨테이너와 상호 작용하는 대화형 모드로 컨테이너 내부의 터미널에 접속할수 있음. 이 옵션을 사용하면 컨테이너 내부에서 명령어를 실행하고 터미널 세션을 유지가능할 수 있게 해준다.
--rm 옵션은 컨테이너를 일회성으로 실행할 때 주로 쓰이는데 컨테이너가 종료될 때 컨테이너와 관련된 리소스(파일 시스템, 볼륨)까지 깨끗이 제거해준다.
# ping 설치
apt update
apt install iputils-ping --yes
# google.comd에 ping 시도
ping google.com -c 1

이제 다시 시도 해보자.
docker run --interactive --tty --rm ubuntu:22.04
ping google.com -c 1
위에 코드가 될지 안될지 데이터의 지속성에 대해 생각하면서 추측해보자.
.
.
.
.
.
.
.
.
.
답은 당연히 실행이 안된다. 이미지를 삭제하는 순간 원래 있는 데이터는 모조리 날라간다. 그렇다면 데이터를 유지하고 싶다면 어떻게 해결 할 수 있을까?
# 우분투 이미지로부터 컨테이너 생성 (--rm flag 없이)
docker run -it --name my-ubuntu-container ubuntu:22.04
핑을 설치하고 다시 exit 해보자
# Install & use ping
apt update
apt install iputils-ping --yes
ping google.com -c 1
exit

docker ps -a를 통해확인해보니 컨테이너는 stopped 되긴 했지만, 아직 read/write 영역에 존재하고 있는것으 알 수 있다. 다시 컨테이너를 시작해서 ping명령어를 실행해보자
# Restart the container and attach to running shell
docker start my-ubuntu-container
docker attach my-ubuntu-container
# Test ping
ping google.com -c 1 # 성공! 🎉
exit
근데 사실 우리가 원하는건 이런식으로 데이터를 보존하는 것이 아니라 아예 처음부터 이미지 자체에 데이터들이 포함되는 것을 원한다. 즉 우리는 dependency가 포함된 우리만의 이미지를 만들어야한다.
# Build a container image with ubuntu image as base and ping installed
docker build --tag my-ubuntu-image -<<EOF
FROM ubuntu:22.04
RUN apt update && apt install iputils-ping --yes
EOF
# Run a container based on that image
docker run -it --rm my-ubuntu-image
# Confirm that ping was pre-installed
ping google.com -c 1 # Success! 🥳
애플리케이션에서 생성된 지속 데이터

이번에는 어플리케이션에 의해 데이터를 보존하는 실습을 해보자.
# 콘테이너 생성
docker run -it --rm ubuntu:22.04
# 디렉토리를 만드고 파일을 그 안에 저장
mkdir my-data
echo "Hello from the container!" > /my-data/hello.txt
# 파일 확인
cat my-data/hello.txt
exit

# 우분투 이미지로 부터 콘테이너 생성
docker run -it --rm ubuntu:22.04
# 파일이 아직 남아있는지 확인
cat my-data/hello.txt # Produces error: `cat: my-data/hello.txt: No such file or directory`

짐작했듯이, 새 컨테이너를 만드는 순간 데이터가 싹 날아간다
이를 방지 하기 위해서 volume을 이용해보자.
- 볼륨 마운트
# 이름을 지정한 볼륨 생성
docker volume create my-volume
# 콘테이너를 생성하고 아까 생성한 볼륨을 콘테이너 파일 시스템에 마운트
docker run -it --rm --mount source=my-volume,destination=/my-data/ ubuntu:22.04
# 의 명령으로도 같은 작업을 실행할 수 있음.
docker run -it --rm -v my-volume:/my-data ubuntu:22.04
# 이제 마운트한 볼륨에 파일을 저장 할 수 있음.
echo "Hello from the container!" > /my-data/hello.txt
cat my-data/hello.txt
exit

# 원래 컨테이너를 지우가 다른 컨테이너를 다시 생성한후 다시 mount
docker run -it --rm --mount source=my-volume,destination=/my-data/ ubuntu:22.04
cat my-data/hello.txt # This time it succeeds!
exit

컨테이너를 지웠다가 다시 만들어지만 외부에 있는 볼륨이 마운트 되니까 파일은 그대로다!!!
그렇다면 대체 이 데이터와 볼륨은 어디에 위치 하고 있는걸까?
리눅스 같은 경우, /var/lib/docker/volumes에 위치하고 있지만, 나같은 윈도우나 맥에서 도커 데스크탑을 사용하고 있는 사람들은 Docker가 리눅스 가상 머신안에서 만든다.
VM안에서 파일 시스템을 볼 수 있는 방법중에서 하나는 우리에게 PID 1의 네임스페이스 내에서 컨테이너를 만들수 있도록 허락해주는 justincormat에 의해 생성되는 컨테이너 이미지를 사용하는 것이다.
# 도커 리눅스 vm에 접속할 수 있는 컨테이너 생성
# Pinning to the image hash ensures it is this SPECIFIC image and not an updated one helps minimize the potential of a supply chain attack
docker run -it --rm --privileged --pid=host justincormack/nsenter1@sha256:5af0be5e42ebd55eea2c593e4622f810065c3f45bb805eaacf43f08f3d06ffd8
# 내 볼륨이 존재하는곳으로:
ls /var/lib/docker/volumes/my-volume/_data
cat /var/lib/docker/volumes/my-volume/_data/hello.txt
내 볼륨이 잘 존재하고 있다....!
이러한 접근방식은 프로그램이 데이터를 유지하려고 알려진 경로에 위치하고 있는 볼륨을 마운트 하는데에 사용 할 수 있다.
#postgres 컨테이너 이미지로부터 컨테이너를 만들고, pgdata라고 알려진 볼륨을 알려진 저장 장소로 사용하기 위해 마운트
docker run -it --rm -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=foobarbaz postgres:15.1-alpine
- 바인드 마운트
# host filesystem에서 컨테이너로 디렉토리를 마운트하는 컨테이너를 생성
docker run -it --rm --mount type=bind,source="${PWD}"/my-data,destination=/my-data ubuntu:22.04
# 위의 작업과 같은 명령을 해주는 코드
docker run -it --rm -v ${PWD}/my-data:/my-data ubuntu:22.04
echo "Hello from the container!" > /my-data/hello.txt
# 내 host 파일 시스템에서도 hello.txt를 확인 할 수 있음.
cat my-data/hello.txt
exit
저장되는 데이터를 쉽게 확인하고 싶다면 바인드 마운트가 좋을 수 있지만 링크에 볼륨이 선호되는 여러 가지 이유가 설명되어 있습니다(Windows나 맥에서 Docker Desktop을 실행하는 경우 속도 문제 등)
Using 3rd Party Containers
지금까지 데이터 스토리지가 어떻게 작동하는지 알아봤다. 이제 3rd party 컨테이가 구동되는 다양한 usecase 예시를 알아보자...!
databases, interactive test environments, and CLI utilities.
데이터베이스는 설치와 설정이 계속 변하는 것으로 악명이 높다.
데이터 베이스들의 지침은 복잡하고 버전과 운영체제 별로 상당히 다르다.
개발을 진행할 경우 단일 데이터베이스의 여러 버전을 실행해야 하거나 컨테이너에서 실행되는 테스트 목적으로 새로운 데이터베이스를 생성해야 하는것이 큰 도움이 될 수 있다.
설정과 설치가 컨테이너 이미지에의새 조작되므로, 내가 제공해야 할것은 그냥 몇가지의 설정값 뿐이다. 데이터베이스의 버전을 바꾸려면 그냥 이미지의 tag값을 바꿔주면된다.(e.g. postgres:14.6 vs postgres:15.1 ).
컨테이너에서 데이터 베이스를 구동하는데에 중요한 몇가지 사항들이있다.
Postgres
docker run -d --rm \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=foobarbaz \
-p 5432:5432 \
postgres:15.1-alpine
# With custom postresql.conf file
docker run -d --rm \
-v pgdata:/var/lib/postgresql/data \
-v ${PWD}/postgres.conf:/etc/postgresql/postgresql.conf \
-e POSTGRES_PASSWORD=foobarbaz \
-p 5432:5432 \
postgres:15.1-alpine -c 'config_file=/etc/postgresql/postgresql.conf'
Mongo
docker run -d --rm \
-v mongodata:/data/db \
-e MONGO_INITDB_ROOT_USERNAME=root \
-e MONGO_INITDB_ROOT_PASSWORD=foobarbaz \
-p 27017:27017 \
mongo:6.0.4
# With custom mongod.conf file
docker run -d --rm \
-v mongodata:/data/db \
-v ${PWD}/mongod.conf:/etc/mongod.conf \
-e MONGO_INITDB_ROOT_USERNAME=root \
-e MONGO_INITDB_ROOT_PASSWORD=foobarbaz \
-p 27017:27017 \
mongo:6.0.4 --config /etc/mongod.conf
Redis
docker run -d --rm \
-v redisdata:/data \
redis:7.0.8-alpine
# With custom redis.conf file
docker run -d --rm \
-v redisdata:/data \
-v ${PWD}/redis.conf:/usr/local/etc/redis/redis.conf \
redis:7.0.8-alpine redis-server /usr/local/etc/redis/redis.conf
MySQL
docker run -d --rm \
-v mysqldata:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=foobarbaz \
mysql:8.0.32
# With custom conf.d
docker run -d --rm \
-v mysqldata:/var/lib/mysql \
-v ${PWD}/conf.d:/etc/mysql/conf.d \
-e MYSQL_ROOT_PASSWORD=foobarbaz \
mysql:8.0.32
Elasticsearch
docker run -d --rm \
-v elasticsearchdata:/usr/share/elasticsearch/data
-e ELASTIC_PASSWORD=foobarbaz \
-e "discovery.type=single-node" \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:8.6.0
Neo4j
docker run -d --rm \
-v=neo4jdata:/data \
-e NEO4J_AUTH=neo4j/foobarbaz \
-p 7474:7474 \
-p 7687:7687 \
neo4j:5.4.0-community
Operating systems
# https://hub.docker.com/_/ubuntu
docker run -it --rm ubuntu:22.04
# https://hub.docker.com/_/debian
docker run -it --rm debian:bullseye-slim
# https://hub.docker.com/_/alpine
docker run -it --rm alpine:3.17.1
# https://hub.docker.com/_/busybox
docker run -it --rm busybox:1.36.0 # small image with lots of useful utilities
Programming runtimes
# https://hub.docker.com/_/python
docker run -it --rm python:3.11.1
# https://hub.docker.com/_/node
docker run -it --rm node:18.13.0
# https://hub.docker.com/_/php
docker run -it --rm php:8.1
# https://hub.docker.com/_/ruby
docker run -it --rm ruby:alpine3.17
가끔 특정한 유틸리티가 현재 내 시스템에 설치되어있지 않거나, 버전별로 큰차이가 있는 경우, 호스트에 아무것도 설치할 필요 없이 컨테이너 안에서 특정한 버전의 유틸리티를 구동 할 수 있다.
jq (json command line utility)
docker run -i stedolan/jq <sample-data/test.json '.key_1 + .key_2'
yq (yaml command line utility)
docker run -i mikefarah/yq <sample-data/test.yaml '.key_1 + .key_2'
sed
docker run -i --rm busybox:1.36.0 sed 's/file./file!/g' <sample-data/test.txt
base64
# Pipe input from previous command
echo "This string is just long enough to trigger a line break in GNU base64." | docker run -i --rm busybox:1.36.0 base64
# Read input from file
docker run -i --rm busybox:1.36.0 base64 </sample-data/test.txt
Amazon Web Services CLI
docker run --rm -v ~/.aws:/root/.aws amazon/aws-cli:2.9.18 s3 ls
Google Cloud Platform CLI
# Bind mount the credentials into the container
docker run --rm -v ~/.config/gcloud:/root/.config/gcloud gcr.io/google.com/cloudsdktool/google-cloud-cli:415.0.0 gsutil ls
# Why is the container image so big 😭?! 2.8GB
만약 너가 이러한 유틸리티를 컨테이너 안에서 자주 사용할 계획이 있다면 쉘 기능이나 alia를 사용하여 프로그램이 내 호스트 안에 설치되어있는듯한 느낌을 주도록 설계하는것이 효율적이다....!
Here are examples of this for yq:
# Shell function
yq-shell-function() {
docker run --rm -i -v ${PWD}:/workdir mikefarah/yq "$@"
}
yq-shell-function <sample-data/test.yaml '.key_1 + .key_2'
---
# Alias
alias 'yq-alias=docker run --rm -i -v ${PWD}:/workdir mikefarah/yq'
yq-alias <sample-data/test.yaml '.key_1 + .key_2'