안녕하세요. 바른호랑이입니다.
이번 게시글에서는 Docker 이미지에 대해서 알아볼 예정입니다.
게시글은 '시작하세요! 도커/쿠버네티스 친절한 설명으로 쉽게 이해하는 컨테이너 관리'를 기반으로 작성하였으니 참고 바랍니다.
모든 컨테이너는 이미지를 기반으로 생성되기에 이미지를 다루는 방법을 아는 것을 Docker 관리에 있어서 필수적이라고 할 수 있습니다. 이미지의 이름을 구성하는 저장소, 이미지 이름, 태그와 같은 기초적인 부분에서부터 이미지의 생성과 삭제, 구조 등과 같은 부분들까지 아는 것 또한 매우 중요합니다. 도커 엔진과 도커 허브의 작동원리를 간단하게 표현해 보면 아래 그림과 같습니다.
Docker는 기본적으로 도커 허브(Docker가 공식적으로 제공 중인 이미지 저장소)라는 중앙 이미지 저장소에서 이미지를 내려받으며, 도커 계정을 가지고 있다면 누구든지 이미지를 올리고 내려받을 수 있기에 다른 사람들에게 이미지를 쉽게 공유할 수 있습니다. docker create, docker run, docker pull 명령어로 이미지를 내려받을 때에는 도커 허브에서 해당 이미지를 검색한 뒤 내려받게 되고, ubuntu, centos와 같이 필요한 대부분의 이미지의 경우 도커 허브에서 공식적으로 제공하거나 다른 사람들이 도커 허브에 이미 올려놓은 경우가 대부분이기에 애플리케이션 이미지를 직접 만들지 않아도 손쉽게 사용할 수 있다는 장점이 있습니다.
단, 도커 허브는 누구나 이미지를 올릴 수 있기 때문에 공식(Official) 라벨이 없는 이미지는 사용법을 찾을 수 없거나 제대로 동작하지 않을 수 있습니다. 또한 이미지 저장소를 비공개(Private)로 사용하기 위해서는 비공개 저장소의 수에 따라 요금을 지불해야 합니다.
도커 허브에 존재하는 이미지의 종류를 살펴보기 위해서는 도커 허브 사이트를 직접 접속하여 찾아볼 수도 있고, docker serach 명령어를 사용할 수도 있습니다. 이를 실제로 알아보기 위해 ubuntu와 관련된 이미지를 검색해 보았습니다.
sudo docker search ubuntu
출력된 결과물을 통해 ubuntu 이미지도 다양하게 존재하는 것을 확인할 수 있었고, STARS의 수치를 통해 해당 이미지가 얼마나 사용자들로부터 즐겨찾기로 추가되어 있는지도 확인할 수 있었습니다.
이미 개발되어 업로드되어 있는 이미지를 docker search 명령어로 내려받아 사용할 수도 있지만 개별 사용목적에 맞는 이미지를 사용하기 위해서는 직접 컨테이너에 애플리케이션을 위한 특정 개발환경을 구축한 뒤 이미지를 생성해야 합니다. 컨테이너 안에서 작업한 내용을 이미지로 만드는 방법을 알아보기 위해 아래의 명령어를 통해 컨테이너를 만들고 테스트 파일을 생성함으로써 기존의 이미지로부터 변경사항을 만들어보았습니다.
# create container
sudo docker run -i -t --name commit_test ubuntu:24.04
# create file
echo test >> test
변경사항을 만들어준 후에는 해당 컨테이너에서 빠져나온 다음 commit 명령어를 통해 이미지를 생성해 주었습니다.
# commit container
sudo docker commit \
-a "righttiger" -m "commit test" \
commit_test \
commit_test:first
# check images
sudo docker images
저장소 이름은 입력하지 않아도 상관없으나 이미지 태그를 입력해주지 않으면 자동으로 latest로 설정됩니다. 위의 예시에서는 이미지 이름을 commit_test로 태그를 first로 설정해 주었고, -a 옵션으로 작성자를, -m 옵션으로 커밋 메시지를 설정해 주었습니다. 이후에 docker images 명령어를 통해 정상적으로 생성되었는지 확인해 주었습니다.
이미지가 정상적으로 생성된 것을 확인해 준 후에는 실제로 해당 이미지로 컨테이너가 잘 생성되는지 확인해 보기 위해 아래의 명령어로 컨테이너를 생성한 후 first 파일의 내용을 살펴보았습니다.
# create container
sudo docker run -i -t --name commit_test2 commit_test:first
# check file
cat test
컨테이너를 이미지로 만드는 작업은 commit 명령어로 쉽게 수행할 수 있으나 이미지를 보다 효율적으로 다루기 위해서는 컨테이너가 어떠한 방식으로 이미지로 만들어지고 구조는 어떻게 되어 있는지 아는 것이 필요합니다. 이미지의 구조를 보다 자세히 살펴보기 위해 docker inspect 명령어를 통해 확인해 보았습니다.
sudo docker inspect ubuntu:24.04
sudo docker inspect commit_test:first
출력결과에는 다양한 정보들이 존재하지만 주의 깊게 살펴봐야 하는 항목은 Layers 항목으로, commit을 통해 변경 사항을 저장할 때 이미지를 이루는 파일들을 전부 복사해서 새롭게 만들어 놓는 것이 아니라 기반이 되는 이미지에 레이어를 추가하는 형식으로 작성되게 됩니다. 즉, ubuntu:24.04을 기반으로 test_commit:first라는 이미지를 만들어서 ubuntu:24.04라는 이미지와 test_commit:first라는 이미지가 존재한다고 해도 전체 이미지 크기는 ubuntu:24.04의 크기 + test_commit:first의 크기가 아니라 ubuntu:24.04의 크기 + first파일의 크기가 되게 됩니다. 이는 docker commit 명령어가 수행될 경우 변경사항에 대해서는 layer형태로 저장을 해놓기 때문입니다. 이를 그림으로 표현해 보면 아래와 같습니다.
세부적인 레이어 구조가 궁금하면 docker history 명령어를 통해 알아볼 수 있습니다.
sudo docker history commit_test:first
지금까지 docker 이미지는 layer 형태로 추가 및 변경 정보들을 저장한다는 것을 알아보았습니다. 이는 이미지들 간의 의존성이 발생한다는 것을 의미하며, 의존성이 있는 경우 이미지가 삭제될 경우 이슈가 발생할 수 있기에 삭제 시에 어떤 방식으로 작동하는지를 알고 있는 것이 중요합니다. 이에 대해 보다 명확하게 알아보기 위해 docker rmi 명령어로 ubuntu:24.04 이미지를 삭제해 보았습니다.
sudo docker rmi ubuntu:24.04
만약 이미지를 사용 중인 컨테이너가 존재한다면 위와 같이 해당 이미지를 삭제할 수 없다는 메시지가 출력됩니다. 이와 같은 경고문구를 무시하고 강제로 삭제하는 것도 가능은 하지만 이는 실제로 레이어 파일을 삭제하는 것이 아니라 이미지 이름만 삭제하기에 의미가 없습니다. 만약 강제 삭제를 하게 되면 어떻게 되는지 알아보기 위해 rm -f 옵션을 주어 강제 삭제 후 결과를 확인해 보았습니다.
# delete image using force option
sudo docker rmi -f ubuntu:24.04
# check docker images using dangling option
sudo docker images -a
사용 중인 이미지를 강제로 삭제하게되면 위와 같은 <none>으로 표기되며, 사용중이지 않은 <none>으로 표기되는 댕글링(dangling) 이미지는 docker image prune 명령어로 삭제하는 것이 가능합니다. 정상적으로 물리적인 이미지 파일까지 삭제하기 위해서는 현재 해당 이미지를 사용중인 컨테이너를 전부 삭제하고, rmi 명령어 또는 prune 명령어를 실행해야 합니다. 실제 동작여부를 확인하기 위해 아래 코드를 통해 컨테이너와 이미지를 순차적으로 삭제해 보았습니다.
# delete container
sudo docker rm -f $(sudo docker ps -a -q)
# delete image
sudo docker rmi commit_test:first
sudo docker image prune
만약 위와 같이 Deleted가 출력된다면 물리적인 이미지 파일들이 정상적으로 삭제된 것입니다.
만약 도커 이미지를 별도로 저장하거나 마이그레이션을 해야 하는 경우 필요에 따라 바이너리 파일로 저장해야 할 때가 있습니다. 이 경우 docker save 명령어를 통해 컨테이너의 커맨드, 이미지 이름과 태그 등 이미지의 모든 메타데이터를 포함해 하나의 파일로 추출할 수 있으며 -o 옵션으로 추출될 파일명을 입력할 수 있습니다. 이를 보다 명확하게 알아보기 위해 아래의 코드로 ubuntu 이미지를 추출해 보았습니다.
sudo docker save -o ubuntu_24_04.tar ubuntu:24.04
추출한 이미지는 load 명령어를 통해 도커에 다시 로드할 수 있으며, save 명령어로 추출된 이미지는 이미지의 모든 메타데이터를 포함하기 때문에 load 명령어로 이미지를 로드하면 이전 이미지와 완전히 동일한 이미지가 도커 엔진에 생성됩니다.
sudo docker load -i ubuntu_24_04.tar
save, load 명령어와 유사한 명령어로는 export와 import 명령어가 있습니다. 하지만 docker commit 명령어로 컨테이너를 이미지로 만들면 컨테이너에서 변경된 사항뿐만 아니라 detached 모드, 컨테이너 커맨드와 같은 설정 등도 이미지에 함께 저장되는 것과 달리 export 명령어로 컨테이너를 이미지로 만들면 파일시스템만을 추출할 뿐 컨테이너 및 이미지에 대한 설정 정보를 저장하지 않기 때문에 이 점을 유의해야 합니다. 실제로 export, import 명령어를 사용해 보기 위해 아래의 명령어로 컨테이너를 만들고 export 명령어로 파일 시스템을 추출한 후 import 명령어로 이미지화해보았습니다.
# create container
sudo docker run -d --name test_export ubuntu:24.04
# export container file system
sudo docker export -o rootFS.tar test_export
# import container file system
sudo docker import rootFS.tar myimage:0.0
다만 이미지를 단일 파일로 저장하는 것은 효율적인 방법이 아니며, 추출된 이미지는 레이어 구조의 파일이 아닌 단일 파일이기 때문에 여러 버전의 이미지를 추출할 경우 이미지 용량을 각기 차지하게 됩니다. 즉, ubuntu:24.04 이미지와 ubuntu:24.04 이미지를 기반으로 일부 수정하고 추출한 이미지가 있다면 ubuntu:24.04 이미지 2개 크기 이상의 용량을 차지하게 된다고 볼 수 있습니다.
이미지를 생성한 후에는 이를 다른 도커 엔진에 배포할 방법이 필요하며, save와 export와 같은 단일 파일로 추출하여 배포하는 것도 가능하지만 위에서 언급한 이유와 이미지 파일의 크기가 너무 크거나 도커 엔진의 수가 많다면 이미지를 파일로 배포하기는 어렵고 비효율적입니다.
이를 해결할 첫 번째 방법은 Docker에서 공식적으로 제공하는 도커 허브(도커 이미지를 저장하기 위한 클라우드 저장소) 이미지 저장소를 이용하는 방법으로 사용자는 docker push 명령어로 이미지를 올리고 docker pull 명령어로 이미지를 내려받으면 되므로 매우 간단하게 사용할 수 있습니다. 다만 비공개(private) 저장소를 일정량이상 사용하기 위해서는 결제를 해야 한다는 단점이 있습니다.
두 번째 방법은 Docker Private Registry를 사용하는 방법으로 이 방법을 이용하면 사용자가 직접 이미지 저장소를 만들 수 있습니다. 직접 이미지 저장소 및 사용되는 서버, 저장 공간등을 관리해야 하기 때문에 도커 허브보다 사용법이 까다롭다는 단점이 있으나 회사 내부망과 같은 곳에서 도커 이미지를 배포해야 한다면 이는 더 적합한 방법이 될 수도 있습니다.
첫 번째 방법을 실제로 진행해 보기 위해 우선 도커 허브 공식 홈페이지에 접속해 보았습니다.
※ Docker Hub
홈페이지에 접속 후 나타나는 검색창에 원하는 이미지를 입력한 후 검색하면 해당 검색어와 관련된 이미지들을 살펴볼 수 있으며, 이미지 저장소를 클릭하면 보다 세부적인 정보를 확인할 수 있습니다. 도커허브에 이미지를 올리기 위해서는 저장소 생성이 필요하며, 저장소 생성을 위해서는 계정 생성이 필요합니다.(계정 생성 후 이메일 인증을 해줘야 정상적으로 기능이용이 가능합니다.)
계정 생성 후 로그인을 완료한 후에는 Image Repository를 생성해 보기 위해 Hub 메뉴를 눌러 Docker Hub관련 페이지로 넘어갔고, 좌측 중앙에 위치한 Create a Repository 메뉴를 클릭하여 저장소 생성을 진행해 보았습니다.
저장소를 생성하기 위해서는 저장소에 저장할 이미지의 이름과 설명을 입력해주어야 하며, Visibility를 설정하여 공개여부를 결정해주어야 합니다. public으로 설정하면 다른 사용자가 docker search와 pull 명령어로 이미지를 사용할 수 있고, private으로 설정하면 저장소의 접근 권한이 있는 계정으로 로그인해야 저장소를 사용할 수 있습니다. private 저장소는 일정 개수가 넘어가면 결제가 필요하기에 public으로 설정하여 테스트를 진행해 보았습니다.
저장소 생성을 완료한 후에는 실제로 이미지를 해당 저장소에 업로드해 보기 위해 아래의 코드를 통해 테스트 컨테이너를 생성하고 docker commit 명령어를 활용하여 이미지를 추출하였습니다.
# create container
sudo docker run -i -t --name commit_container1 ubuntu:24.04
# create file
echo test >> test
# commit container
sudo docker commit commit_container1 repo_test:0.0
이미지까지 정상적으로 추출된 것을 확인한 후에는 특정 이름의 저장소에 이미지를 올리기 위해서는 저장소 이름을 이미지 앞에 접두어로 추가해야 하기에 docker tag 명령어를 활용하여 접두어를 추가해 주었습니다. tag 명령어의 형식은 docker tag [기존의 이미지 이름] [새롭게 생성될 이미지 이름]입니다.
sudo docker tag testimage:0.0 righttiger/repo_test:0.0
여기서 유의해야 할 점은 tag 명령어로 이미지 이름을 변경했다고 해서 기존의 이름이 삭제되는 것이 아니라 같은 이미지를 가리키는 새로운 이름이 추가될 뿐이라는 점입니다. 이미지 이름을 변경한 후에는 지정한 저장소에 업로드를 하기 위해 아래의 명령어로 도커 허브 서버에 로그인해 주었습니다. 로그인 시에는 계정을 생성할 당시 입력한 계정명과 비밀번호를 입력해 주면 되며, 로그인한 정보는 /[계정명]/.docker/config.json에 파일에 저장됩니다. 만약 로그인 정보를 삭제하고 싶다면 docker logout 명령어를 입력해주면 됩니다.
sudo docker login
로그인까지 완료한 후에는 실제로 이미지를 저장소에 올려보기 위해 docker push 명령어를 사용해 주었습니다.
sudo docker push righttiger/repo_test:0.0
출력결과를 보면 전체 레이어가 repository에 전송된 것이 아니라 1개만 전송된 것을 알 수 있는데 이는 해당 이미지가 ubuntu:24.04를 기반으로 만들어졌기 때문입니다. 즉 ubuntu:24.04 이미지의 레이어는 허브의 우분투 이미지 저장소에 존재하므로 거기에서 가져오고 수정된 사항이 기록된 layer만 전송한 것을 확인할 수 있습니다. 업로드를 완료한 후에는 생성했던 저장소의 Tags메뉴에 들어가 보면 정상적으로 업로드된 것을 확인할 수 있으며, 업로드가 완료된 이미지는 pull명령어로 다운로드할 수 있습니다. 실제로 다운로드가 잘 되는지 보기 위해 기존의 컨테이너와 이미지들을 삭제해 주고 해당 이미지를 다운로드해보았습니다.
# delete container / images
sudo docker rm -f $(sudo docker ps -a -q)
sudo docker rmi righttiger/repo_test:0.0 repo_test:0.0
# pull image
sudo docker pull righttiger/repo_test:0.0
위에서 알아본 저장소 사용방법은 단일 계정에서 저장소를 생성 후 이미지를 저장하는 방법이었습니다. 만약 다른 개발자들과 함께 이미지를 개발하는 상황이라면, 저장소의 이미지 읽기, 쓰기, 수정 및 관리와 같은 권한과 조직 구성을 통한 체계화가 필요할 수 있기에 DockerHub에서는 조직과 팀 단위로 이미지 저장소를 이용할 있게 Organizations, Teams로 구성하는 것도 가능합니다. 실제로 조직을 생성하고 사용해 보기 위해 도커허브 홈페이지의 상단 메뉴에서 [Organizations]를 선택한 후 [Create Organization] 버튼을 클릭해 보았습니다. 무료로 이용이 가능했던 과거와 달리 정책이 변경되어 조직 및 팀으로 저장소를 사용하기 위해서는 일정 금액을 지불해야 하는 것을 확인할 수 있었습니다.(2024년 10월 2일 기준)
직접 실습을 할 수 없는 상태라 교재에서 나와 있는 내용을 순차적으로 읽어보면서 내용을 확인을 보았고, 기본적인 틀은 Organization을 생성한 후 내부에 Team을 생성하여 각각의 Team마다 권한을 다르게 설정하는 방식을 통해 권한관리를 보다 유동적이고 원활하게 진행할 수 있는 것을 확인했습니다. 기본적인 개념은 결국 Azure Entra ID(과거 Azure Active Directory)와 유사한 형태로 관리가 진행된다는 것을 알 수 있었습니다.
지금까지 알아본 도커허브를 활용한 이미지 관리방법은 결국 웹사이트 서비스를 제공하는 외부 서버를 활용하여 처리하는 구조를 취하고 있습니다. DockerHub를 이용하는 방법 말고도 도커에서는 Docker Private Registry라는 이미지를 제공하여 개인 서버에 이미지를 저장하는 저장소를 만들 수 있게 해주고 있습니다. 실제로 Docker Private Registry를 사용해 보기 위해 아래의 코드를 통해 컨테이너를 생성해 보았습니다.
sudo docker run -d --name myregistry \
-p 5000:5000 \
--restart=always \
registry:2.6
--restart 옵션은 컨테이너가 종료되었을 경우 재시작에 대한 정책을 설정하는 옵션으로 컨테이너가 정지되면 자동으로 재시작시켜줘야 하기에 always라는 파라미터를 설정하여 주었습니다. 만약 종료코드가 0이 아닐 때 컨테이너 재시작 횟수를 5번으로 주고 싶다면 해당 옵션에 on-failure:5로 파라미터를 주면 되며, stop 명령어로 컨테이너가 종료되었을 경우에는 별도로 컨테이너를 자동 재시작하게 하고 싶지 않다면 unless-stopped를 파라미터로 주면 됩니다.
레지스트리 컨테이너는 기본적으로 5000번 포트를 사용하기에 포트는 5000번으로 연결해 주었으며, 해당 포트로 registry container의 RESTful API를 사용할 수 있습니다. 실제로 잘 동작하는지 확인해 보기 위해 http 요청을 보낼 수 있는 아래의 명령어로 확인해 보았습니다.
curl localhost:5000/v2/
정상적으로 컨테이너가 생성되고, http 통신도 잘 작동하는 것을 확인했으므로 실제로 이미지를 해당 컨테이너에 실제로 올려보기 위해 아래의 명령어를 사용해 보았습니다.
# tag명 변경
sudo docker tag repo_test:0.0 192.168.91.151:5000/repo_test:0.0
# push 테스트
sudo docker push 192.168.91.151:5000/repo_test:0.0
레지스트리 컨테이너에 이미지를 올리기 위해서는 이미지의 접두어를 레지스트리 컨테이너가 존재하는 호스트의 IP와 레지스트리 컨테이너의 5000번 포트와 연결된 호스트의 포트로 설정해주어야 합니다. 즉, 호스트의 IP가 192.168.91.151이고 레지스트리 컨테이너의 포트가 5000번이라면 192.168.91.151:5000/{image name}:{image tag}가 되어야 하기에 tag 명령어로 해당 내용을 적용한 이미지를 작성해 주었습니다. 만약 Docker host에서 별도로 사용 중인 도메인이 있다면 IP주소가 아닌 해당 도메인 이름으로 설정해 주어도 됩니다.
정상적으로 이미지의 접두사를 수정했음에도 HTTPS관련 오류가 위와 같이 발생한 것을 확인할 수 있는데 이는 기본적으로 도커 데몬은 HTTPS를 사용하지 않은 레지스트리 컨테이너에 접근할 수 없기 때문입니다. HTTPS를 사용하려면 인증서를 적용해 별도로 설정해야 하며, 지금은 간단하게 테스트만 해보기 위해 HTTPS를 사용하지 않아도 push, pull이 가능하게 아래의 옵션을 Docker 시작 옵션에 추가한 뒤 도커를 재시작하였습니다.(ubuntu 24.04 버전 기준)
# move to docker configuration file directory
cd /etc/docker/
# create file
sudo nano daemon.json
# insert options
{
"insecure-registries": ["192.168.91.151:5000"]
}
# restart docker
sudo systemctl restart docker
# push 테스트
sudo docker push 192.168.91.151:5000/repo_test:0.0
# check registry
curl http://192.168.91.151:5000/v2/_catalog
정상적으로 image가 push 된 것을 확인하였으므로 호스트 머신에서 repo_test:0.0 이미지를 삭제하고 pull 명령어로 이미지를 가져올 수 있는지 아래의 명령어로 확인해 보았습니다.
# remove image from host
sudo docker rmi 192.168.91.151:5000/repo_test:0.0
# check docker images
sudo docker images
# pull image
sudo docker pull 192.168.91.151:5000/repo_test:0.0
# check docker images
sudo docker images
레지스트리 컨테이너는 생성됨과 동시에 컨테이너 내부 디렉터리에 마운트 되는 도커 볼륨을 생성하며, push 된 이미지 파일은 이 볼륨에 저장됩니다. 만약 레지스트리 컨테이너를 삭제할 때 볼륨도 삭제하고 싶다면 아래의 명령어처럼 docker rm 명령어에 --volumnes 옵션을 추가하면 됩니다.(추가 학습을 위해 생성한 이미지 및 컨테이너들은 전부 삭제해 준 후 다음 실습을 진행했습니다.)
sudo docker rm -f --volumes myregistry
위에서는 도커 시작 옵션을 수정하여 별도의 인증 절차 없이 레지스트리 컨테이너에서 이미지를 push, pull 할 수 있게 작성을 해보았는데, https 절차도 없이 레지스트리 컨테이너에서 접근할 수 있게 하면 보안성이 취약해지는 문제점이 발생합니다. 이 경우 도커 허브에서 docker login 명령어를 통해 접근을 제한하여 보안성을 확보한 것처럼 레지스트리 컨테이너 또한 미리 정의한 계정들만 레지스트리 컨테이너에 접근할 수 있도록 작성하여 보안성을 강화할 수 있습니다. 레지스트리 컨테이너 자체에서 인증 정보를 설정하거나 별도의 서버 컨테이너를 만듦으로써 해당 내용 적용이 가능하며, 이에 대해 보다 자세히 알아보기 위해 Nginx 서버 컨테이너를 생성하여 연동하는 방법을 진행해 보았습니다. 일반적으로 로그인 인증 기능은 insecure 레지스트리 컨테이너에서는 사용할 수 없기에 자체적으로 인증서와 키를 만들어서 TLS를 적용하는 방법으로 실습을 진행해보았습니다.
# create folder
mkdir certs
# create RSA prviate key
# 2048 비트의 RSA 개인 키를 생성하여 저장하는 명령어
openssl genrsa -out ./certs/ca.key 2048
# create ca cert
# 10000일 동안 유효한 자체 서명된 CA 인증서를 생성하는 명령어(ca.key를 기반으로 생성됨)
# 인증서 정보는 전부 공백으로 입력해도 상관없음.
openssl req -x509 -new -key ./certs/ca.key -days 10000 -out ./certs/ca.crt
위와 같이 ROOT 인증서를 생성한 후에는 ROOT 인증서를 활용하여 레지스트리 컨테이너에 사용될 인증서를 생성해 주었습니다. 인증서 서명 요청 파일인 CSR(Certificate signing request) 파일을 생성하고 ROOT 인증서로 새로운 인증서를 발급해 주었는데, ${DOCKER_HOST_IP}에는 레지스트리 컨테이너가 존재하는 도커 호스트 서버의 IP나 도메인 이름을 입력해 주면 됩니다.
# create RSA Key
# 2048 비트 RSA 개인 키를 생성
openssl genrsa -out ./certs/domain.key 2048
# create csr file
# 192.168.91.151 IP에 대한 인증서 서명 요청을 생성하고 저장
openssl req -new -key ./certs/domain.key -subj /CN=192.168.91.151 -out ./certs/domain.csr
# create extfile.cnf
# SSL인증서에 IP주소를 추가
echo subjectAltName = IP:192.168.91.151 > extfile.cnf
# ca.crt와 ca.key를 사용하여 domain.csr을 서명하고 인증서를 발급
openssl x509 -req -in ./certs/domain.csr -CA ./certs/ca.crt -CAkey ./certs/ca.key \
-CAcreateserial -out ./certs/domain.crt -days 10000 -extfile extfile.cnf
레지스트리 컨테이너에 사용할 인증 생성까지 마무리한 후에는 레지스트리에 로그인할 때 사용할 계정과 비밀번호를 저장하는 파일을 아래의 명령어로 생성해 주었습니다.
# install htpassword package
sudo apt install apache2-utils
# create htpasswd file
# htpasswd 파일은 Apache http 서버에서 사용되는 사용자 이름과 암호의 해시값을 저장하여 http 기본 인증을 구현할 대 사용되는 파일
htpasswd -c htpasswd righttiger
# change file location
mv htpasswd certs/
로그인할 계정을 생성해 준 후에는 nginx.conf 파일을 아래와 같이 내용을 작성해 준 후에 certs 디렉터리에 저장해 주었습니다.
# creat nginx.conf
sudo nano certs/nginx.conf
# contents in nginx.conf
upstream docker-registry {
server registry:5000;
}
server {
listen 443 ssl;
server_name 192.168.91.151;
ssl_certificate /etc/nginx/conf.d/domain.crt;
ssl_certificate_key /etc/nginx/conf.d/domain.key;
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
return 404;
}
auth_basic "registry.localhost";
auth_basic_user_file /etc/nginx/conf.d/htpasswd;
add_header 'Docker-Distribution-Api-Version' 'registry/2.0' always;
proxy_pass http://docker-registry;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
}
}
nginx.conf 파일까지 작성을 완료하고 나서는 아래의 명령어로 레지스트리 컨테이너와 Nginx 서버 컨테이너를 생성해 주었습니다.
# create registry container
# 해당 레지스트리 컨테이너는 Nginx 서버를 통해 접근하기에 별도로 포트 바인딩을 하지 않음
sudo docker run -d --name myregistry --restart=always registry:2.6
# create Nginx container
sudo docker run -d --name nginx_frontend \
-p 443:443 \
--link myregistry:registry \
-v $(pwd)/certs/:/etc/nginx/conf.d \
nginx:1.27
# check container status
sudo docker ps -a
컨테이너를 생성한 후에는 도커 허브와 동일하게 docker login 명령어로 레지스트리 컨테이너로 로그인이 되는지 확인해 보았습니다.
sudo docker login https://192.168.91.151
계정 정보를 올바르게 입력했지만 직접 만들어준 Self-signed 인증서를 신뢰할 수 있는 인증서 목록에 추가하지 않았기에 docker에서는 에러를 출력하는 것을 확인했습니다. 직접 만든 인증서를 신뢰할 수 있는 인증서 목록에 추가하기 위해 아래의 명령어를 사용해 주었습니다.
# 인증서 복사 후 ca 인증서 디렉터리에 저장
sudo cp certs/ca.crt /usr/local/share/ca-certificates/
# 인증서 목록 갱신
sudo update-ca-certificates
# 도커 및 Nginx 서버 컨테이너 시작
sudo service docker restart
sudo docker start nginx_frontend
sudo docker ps -a
갱신을 완료한 후에는 다시 한번 아래의 명령어로 로그인 테스트를 진행해 보았습니다.
sudo docker login 192.168.91.151
로그인에 성공했으므로 실제로 레지스트리 컨테이너에 이미지를 push, pull 할 수 있는지 확인하기 위해 아래의 명령어로 테스트를 해보았습니다.
# create image
# 포트 설정을 따로 안했으므로 별도로 포트를 이미지에 적어줄 필요 없음.
sudo docker tag righttiger/repo_test:0.0 192.168.91.151/repo_test:0.0
# push image to registry container
sudo docker push 192.168.91.151/repo_test:0.0
# delete image(192.168.91.151/repo_test:0.0)
sudo docker rmi 192.168.91.151/repo_test:0.0
# pull image from registry container
sudo docker pull 192.168.91.151/repo_test:0.0
일반적으로 도커 엔진의 경우 docker ps와 같은 명령어를 통해 다양한 정보를 확인하는 것이 가능하지만 사설 레지스트리 컨테이너의 경우 별도의 인터페이스를 제공하지 않기에 제어를 위해서는 RESTful API를 사용해야 합니다. 기본적으로 RESTful API는 5000번 포트를 사용하기에 호스트와 포트바인딩을 하는 것이 보편적입니다. 레지스트리 컨테이너를 제어하기 위한 RESTful API의 종류는 매우 다양하며, 모든 목록을 확인하고 싶다면 도커 공식문서의 레지스트리 항목을 참고하면 됩니다. 이를 보다 자세히 알아보기 위해 위에서 작성한 nginx 컨테이너와 registry 컨테이너를 활용하여 실습을 진행해 보았습니다.
# curl 명령어를 사용하여 Docker registry API로부터 /v2/_catalog 엔드포인트에 요청을 보냄
# Basic Authentication command
# curl -u ${User}:${Password} https://${registry container ip}/v2/_catalog
curl -u righttiger:1q2w3e4r https://192.168.91.151/v2/_catalog
정상적으로 이미지를 읽어오는 것을 확인한 후에는 아래의 명령어로 특정 이미지의 태그 리스트를 확인해 보았습니다.
curl -u righttiger:1q2w3e4r https://192.168.91.151/v2/repo_test/tags/list
보다 자세한 정보를 확인해 보기 위해 아래의 명령어로 확인해 보았습니다.
# 저장된 이미지의 자세한 정보를 확인하기 위해서 /v2/(image name)/manifests/(tag)를 사용
# -i 옵션은 http 응답의 헤더를 출력하도록 설정
# --header는 http 요청의 헤더에 입력된 값을 추가
# manifests는 레지스트리 컨테이너에 저장된 이미지 정보의 묶음을 의미함.
curl -u righttiger:1q2w3e4r -i \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://192.168.91.151/v2/repo_test/manifests/0.0
도커 이미지는 이미지에 대한 정보를 저장하는 매니페스트와 이미지에 레이어 파일을 저장하는 바이너리 파일로 나뉘며, 매니페스트와 각 레이어에 해당하는 바이너리 파일들을 구별하기 위해 고유한 ID로 Digest 값을 가지고, 이를 도식화해 보면 아래의 그림과 같습니다.
매니페스트와 레이어에 부여된 다이제스트는 레지스트리 컨테이너에 저장된 이미지를 제어할 때 사용되며, 이미지의 삭제를 위해서는 매니페스트와 레이어 파일을 따로 삭제해줘야 합니다. 실제로 해당 내용이 어떻게 작동하는지 확인해 보기 위해 아래의 명령어를 활용하여 매니페스트를 삭제해 보았습니다.
# 삭제 URL: DELETE /v2/(image name)/manifests/(manifest digest)
# delete image manifest
curl -u righttiger:1q2w3e4r \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-X DELETE \
https://192.168.91.151/v2/repo_test/manifests/sha256:2e7a13c4c8d45806e72173960c950994b2c627bc7c1d23439ec6ca4f9dc671cb
위의 명령어로 삭제를 진행해 보면 지원하지 않는 작업이라는 오류가 발생하는데 이는 레지스트리 컨테이너가 각종 설정을 컨테이너 환경변수로 저장하여 이미지 저장 기능을 설정하는데, 그중에 이미지를 삭제하는 기능을 활성화할지가 포함되기 때문입니다. 즉 이미지 삭제 기능 활성화에 대한 환경변수를 지정하지 않으면 해당 환경 변수를 false 인식하므로 기존의 레지스트리 컨테이너를 삭제 후 환경변수를 적용하여 재생성해 준 후에 이미지를 다시 push 해준 후 해당 명령어를 재실행해보았습니다.
# delete container
sudo docker rm -f $(sudo docker ps -a -q)
# create registry container
sudo docker run -d --name myregistry \
--restart=always \
-e REGISTRY_STORAGE_DELETE_ENABLED=true \
registry:2.6
# create Nginx container
sudo docker run -d --name nginx_frontend \
-p 443:443 \
--link myregistry:registry \
-v $(pwd)/certs/:/etc/nginx/conf.d \
nginx:1.27
# check container status
sudo docker ps -a
# push image to registry container
sudo docker push 192.168.91.151/repo_test:0.0
# check manifests digest
curl -u righttiger:1q2w3e4r -i \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://192.168.91.151/v2/repo_test/manifests/0.0
# delete image manifest
curl -u righttiger:1q2w3e4r \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-X DELETE -v \
https://192.168.91.151/v2/repo_test/manifests/sha256:2e7a13c4c8d45806e72173960c950994b2c627bc7c1d23439ec6ca4f9dc671cb
202 Accepted 메시지가 반환되면 해당 이미지의 매니페스트가 정상적으로 삭제된 것입니다. 다만 이는 실제 이미지 레이어 파일이 아닌 매니페스트 파일만 삭제된 것이기 때문에 같은 이미지를 다시 push하면 Layer Already Exist라는 출력 결과를 확인할 수 있으며, 이를 실제로 확인해보기 위해 다시 push 해보았습니다. push 결과 해당 메세지가 출력되는 것과 삭제되었던 매니페스트도 다시 생성된 것을 확인할 수 있었습니다.
# push image to registry container
sudo docker push 192.168.91.151/repo_test:0.0
# check manifests digest
curl -u righttiger:1q2w3e4r -i \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://192.168.91.151/v2/repo_test/manifests/0.0
이미지 레이어 파일도 실제로 삭제하려면 각 레이어 파일의 다이제스트를 입력하여 삭제 API를 호출해야 하며, 보다 확실히 알아보기 위해 아래의 명령어를 통해 매니페스트와 레이어를 직접 삭제해 보았습니다.
# delete image manifest
curl -u righttiger:1q2w3e4r \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-X DELETE -i \
https://192.168.91.151/v2/repo_test/manifests/sha256:2e7a13c4c8d45806e72173960c950994b2c627bc7c1d23439ec6ca4f9dc671cb
# delete layers
curl -u righttiger:1q2w3e4r \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-X DELETE \
-i https://192.168.91.151/v2/repo_test/blobs/sha256:eb993dcd6942ffcb7633f2cb271bd4b0a275fc9bdc8f5100c5b4d24694cacf50
curl -u righttiger:1q2w3e4r \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-X DELETE \
-i https://192.168.91.151/v2/repo_test/blobs/sha256:4b2ff5f28f4ee3921bd656f222babe34870943d0c4bc0e62dfd828f64a35b438
위의 실습을 통해 매니페스트를 삭제하더라도 레이어는 자동으로 삭제되지 않기에 삭제 시 2가지 사항 모두 직접 삭제를 해줘야 한다는 것을 확인할 수 있었습니다.
앞서서 레지스트리 컨테이너의 환경변수를 설정하기 위해 -e 옵션을 적용하였는데, 적용해야 할 환경변수의 개수가 많아지게 되면 컨테이너를 생성할 때 복잡한 코드를 작성해줘야 하는 문제가 있습니다. 이를 해결하기 위해 docker에서는 yml파일을 활용하여 환경변수를 설정할 수 있게 구성되어 있으며, 기본적으로 yml 파일을 사용하지 않으면 기본 yml파일을 이용합니다. 기본 yml 파일의 내용을 확인해 보기 위해서 아래의 코드를 통해 새로운 컨테이너를 생성하고 yml파일을 확인해 보았습니다.
# create container
sudo docker run -d -p 5001:5000 --name yml_basic --restart=always \
registry:2.6
# check yml file contents
sudo docker exec yml_basic cat /etc/docker/registry/config.yml
yml 파일의 구조를 확인한 후에는 실제로 yml 파일을 활용하여 환경변수를 적용해 보기 위해 실습을 진행해 보았습니다. 작성한 파일의 주된 내용은 로그 출력 레벨을 info로 설정하고, 이미지 파일이 저장되는 디렉터리를 컨테이너 내부의 /registry_data로 설정(따로 설정하지 않으면 /var/lib/registry에 저장됨.)하며, 이미지 삭제 API를 활성화하는 것이었습니다. 아래의 내용을 yml 파일로 작성하였습니다.(http 항목의 addr은 레지스트리 서비스를 바인딩할 주소를 나타냄.)
# sudo nano config.yml
# storage-filesystem-rootdirectory: /registry_data 옵션은 아래처럼 REGISTRY를 접두사로 하는 -e 옵션으로 처리가능
# -e REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/registry_data
version: 0.1
log:
level: info
storage:
filesystem:
rootdirectory: /registry_data
delete:
enabled: true
http:
addr: 0.0.0.0:5000
yml 파일을 작성한 후에는 실제로 코드를 활용하여 신규 컨테이너를 생성해 주었습니다.
# create container
sudo docker run -d --name yml_basic2 \
-p 5002:5000 \
--restart=always \
-v $(pwd)/config.yml:/etc/docker/registry/config.yml \
registry:2.6
적용한 내용이 기본 yml 파일과 다르게 잘 적용되었는지 확인해 보기 위해 우분투 이미지를 복제한 이미지를 접두사에 맞춰서 생성해 주고 push 해준 후 확인해 보았습니다.
# copy image
sudo docker tag ubuntu:24.04 localhost:5001/ubuntu:24.04
sudo docker tag ubuntu:24.04 localhost:5002/ubuntu:24.04
# push image
sudo docker push localhost:5001/ubuntu:24.04
sudo docker push localhost:5002/ubuntu:24.04
# check image location
sudo docker exec yml_basic ls /var/lib/registry
sudo docker exec yml_basic2 ls /registry_data
'IT & 데이터 사이언스 > Data Engineering' 카테고리의 다른 글
[Docker] 도커파일의 개념과 기본 (1) | 2024.12.11 |
---|---|
[Docker] 컨테이너 자원 할당 (1) | 2024.08.29 |
[Docker] 컨테이너 로깅 (0) | 2024.08.26 |
[Docker] Docker 네트워크 (1) | 2024.08.08 |
[Docker] Docker 볼륨 (5) | 2024.08.05 |
댓글