CI 과정 중에서나 여러 다른 과정에서 kubernetes 클러스터의 파드 컨테이너 안에서 docker build로 이미지를 빌드하는 경우가 많습니다.
하지만 파드 안에서 이미지를 빌드할 때 docker를 사용하는 건 권장되는 방식이 아닌데요, 여러 가지 이유가 있습니다. 그리고 docker 대신 권장되는 방식이 있는데, 곧 소개할 Kaniko라는 친구입니다.
본격적으로 들어가기 전에, 기본적인 docker Host와 Client의 통신방식에 대해서 짚고 넘어갑시다.
컨테이너 이미지는 Dockerfile로 지정됩니다. 아시다시피 Dockerfile은 애플리케이션, 그리고 리소스를 기반으로 이미지를 빌드하는 방법을 설명한 설명서이죠. docker build 명령어로 설명서를 기반으로 컨테이너 이미지를 빌드할 수 있습니다. 이 이미지만 있으면 docker run 명령어로 워크로드를 실행할 수 있죠.
하지만 도커의 문제점은 사용자의 시스템에서 직접 도커를 사용할 수 없다는 것입니다. 대부분의 경우, Docker CLI와만 상호 작용합니다. 하지만 도커로 애플리케이션을 실행한다는 것은 root 권한으로 도커 데몬을 실행해야 한다는 것을 의미합니다. 실제로 이 데몬은 TCP 포트 대신 Unix 소켓에 바인딩됩니다. 기본적으로 사용자는 사용자 루트가 소유한 sudo 명령으로만 Unix 소켓에 액세스 할 수 있습니다.
정리해 보자면, 컨테이너 내에서 docker daemon과 통신이 필요한 경우에선 일반적으로 소켓을 이용해 통신해야 합니다. 이 방법을 Docker In Docker(DinD)라고 합니다. 하지만 sudo 명령으로만 Unix 소켓에 액세스 할 수 있다는 것이죠. 이 점이 DinD의 심각한 보안적 결함을 불러옵니다. 실제 예제를 보며 설명을 이어가겠습니다.
쿠버네티스 환경에서 파드 안에서 docker build 커맨드로 이미지를 빌드하려고 하는 상황을 가정해봅시다.
apiVersion: v1
kind: Pod
metadata:
name: docker
spec:
containers:
- name: docker
image: docker
args: ["sleep", "10000"]
restartPolicy: Never
다음 구성으로 Pod를 생성합니다. 생성된 파드에 쉘로 접속해서 docker build 명령어를 사용하면 어떻게 될까요?
보나 마나 에러를 뱉을 겁니다. 당연하게도 파드 안에는 docker daemon과 통신할 소켓이 없기 때문이죠. docker 커맨드를 사용한다고 소켓이라 docker daemon이 있는 건 아닙니다. docker client가 설치된 것뿐이죠.
두 번째 예제로 Pod를 생성해 봅시다.
apiVersion: v1
kind: Pod
metadata:
name: docker
spec:
containers:
- name: docker
image: docker
args: ["sleep", "10000"]
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-socket
restartPolicy: Never
volumes:
- name: docker-socket
hostPath:
path: /var/run/docker.sock
역시 이전 방법과 동일하게 파드의 셀로 접속해 docker build 커맨드를 날려봅시다.
잘 동작하는 것을 알 수 있습니다. 쿠버네티스에서 DinD 구조를 사용한다는 것은 일반적으로 파드가 속한 노드의 소켓을 볼륨으로 마운트 해서 사용한다는 것인데 잘 동작은 하지만 여기서 여러 가지 문제점이 생깁니다.
먼저 말씀드린 것처럼 해당 컨테이너가 어떤 커맨드로 어떤 명령을 내릴지도 모르는데, 노드의 소켓을 공유시킨다는 건 상당히 위험한 일입니다. 전체 클러스터에 영향을 미칠 수도 있기 때문에 심각한 보안 문제를 야기할 수도 있기 때문이죠.
두 번째는 더 큰 이슈입니다.
도커는 기본적으로 파드 안에서 실행되기 위해선 당연히 노드에 도커와 통신할 런타임이 필요합니다. 하지만 그 쿠버네티스와 파드 사이에 연결을 담당하는 docker shim은 Kubernetes 1.20에 공식적으로 삭제되었습니다.(1.22까지는 지원)
docker shim이 사라지고 containerd를 사용해야 하는데, 얘는 도커가 아닙니다.
컨테이너 런타임은 노드에서 컨테이너를 실행하고 컨테이너 이미지를 관리하는 소프트웨어인데, 대다수가 알고 있는 컨테이너 런타임이 Docker입니다. 하지만 "Docker=컨테이너"가 아니듯이 rkt, lxd 등 다른 컨테이너 런타임의 종류도 다양합니다.
이 컨테이너 런타임이 다양해지면서 표준화가 필요해졌습니다. 이에 CRI(Container Runtime Interface)라는 규격을 도입했고 CRI를 통해 Kubernetes는 다시 컴파일할 필요 없이 다양한 컨테이너 런타임을 사용할 수 있게 되었습니다.
이에 Google, Docker, IBM, ZTE 및 ZJU의 엔지니어들이 containerd용 CRI를 구현하기 위해 노력했습니다. 이 프로젝트를 cri-containerd라고 합니다. 이 cri-containerd를 사용하면 Docker를 설치하지 않고도 기본 런타임으로 containerd를 사용하여 Kubernetes 클러스터를 실행할 수 있습니다.
다시 돌아와서, 현재 1.22~ 기준 쿠버네티스 환경에서 docker는 사라지고, containerd 런타임 환경만 남은 상태입니다. 이 말은 즉슨 docker sock를 볼륨으로 넣는 방법이 통하지 않는다는 거죠.
이 두 가지 이유면 이제 kubernetes 클러스터의 파드 컨테이너 안에서 docker build로 이미지를 빌드하는 것이 얼마나 끔찍한 일인지 아시겠나요?
"아니 그럼 그냥 로컬 환경에서 빌드하라는 거야?"라고 하시겠지만 이제 소개해드릴 친구가 있습니다.
바로 Kaniko라는 구글 컨테이너 툴 프로젝트에서 관리하는 오픈소스입니다. Kaniko는 Docker 데몬에 대한 액세스 없이 컨테이너 내에서 컨테이너 이미지를 빌드하도록 도와줍니다. 이렇게 하면 호스트 파일 시스템에 대한 액세스 권한을 부여하지 않고도 컨테이너 내에서 빌드 작업을 실행할 수 있습니다.
sudo 권한으로 인한 보안적 위협도 없고, DinD 구조보다 간단하기까지 합니다. 속도도 빠르고요. kubernetes 환경 기반으로 이미지를 빌드할 때 최고의 선택일 수 있습니다.
그럼 Kaniko는 docker daemon도 없이 어떻게 이미지를 빌드하는 것일까요?
Kaniko executor는 이미지를 실행시킬 때, Dockerfile에 정의되어 있는 베이스 이미지로부터 파일시스템을 뽑아냅니다. 그러고 Dockerfile의 커맨드를 하나씩 순차적으로 실행시키면서 파일시스템의 스냅샷을 생성합니다.
각 커맨드가 실행된 후에는, 그 베이스 이미지로 변경된 파일의 레이어를 추가시키고 이미지의 메타데이터를 업데이트하는 방식으로 진행됩니다. 베이스 이미지 위에 레이어를 쌓고 쌓고 하는 방식으로 구현되었죠.
apiVersion: v1
kind: Pod
metadata:
name: kaniko
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:debug
args: ["--context=git://github.com/vfarcic/kaniko-demo",
"--destination=vfarcic/devops-toolkit:1.0.0"]
volumeMounts:
- name: kaniko-secret
mountPath: /kaniko/.docker
restartPolicy: Never
volumes:
- name: kaniko-secret
secret:
secretName: regcred
items:
- key: .dockerconfigjson
path: config.json
예제 파일을 보면 더 잘 이해할 수 있습니다. kaniko executor 이미지를 가져와 args를 상황에 맞게 추가한 후 빌드에 필요한 docker 사용자 비밀번호 (ECR 사용 시에는 AWS 유저의 key와 secret)를 시크릿으로 볼륨을 추가해 주면 됩니다.
보통은 CI 도구를 쿠버네티스에서 운영하면서 각 파이프라인의 스텝들을 파드로 실행시키면서 D in D 구조를 많이 사용하는데, Kaniko를 사용하면 안전하면서도 안정적이게 운영할 수 있습니다.
특히 Drone CI를 사용할 경우엔 제공되는 kaniko 플러그인을 사용해 더욱 간편하게 파이프라인을 구성할 수 있죠.
- name: build-image-push
image: plugins/kaniko
settings:
repo: newdeal/isgod
tags: latest
build_args:
- VERSION=newdeal
- COMMIT=${DRONE_COMMIT_SHA:0:8}
username:
from_secret: dockerhub_username
password:
from_secret: dockerhub_password
depends_on:
- build-and-test
when:
branch:
- main
event:
- push
특히 회사에서 CI 도구를 운영하면서 이미지 빌드하는 과정에 아주 가끔씩 발생하는 Is the docker daemon running? 에러가 아주 스트레스였는데, kaniko 도입으로 완벽히 줄어들었습니다.
더불어 이미지 캐싱 기법도 기똥차서 빌드 시간도 제법 줄어들었습니다. (캐시 레이어마다 저장소에 푸시하는 방법이라 저장소의 용량은 조금 더 커지지만 그만큼 효율적입니다.)
쿠버네티스에서 CI 도구를 운영 중이거나 docker client를 자주 워크로드로 운용하시는 분은 적극적으로 고려해 볼 만한 좋은 도구라고 생각합니다. 궁금한 점이 있으시거나 의견이 있으신 분은 언제든 의견을 달아주시길 바랍니다. 감사합니다.
출처
https://www.youtube.com/watch?v=26bYo-0e9tM
https://www.youtube.com/watch?v=EgwVQN6GNJg
https://kubernetes.io/blog/2018/05/24/kubernetes-containerd-integration-goes-ga/
https://medium.com/geekculture/create-docker-images-without-docker-daemon-kaniko-847a688155a6
'🏋️♀️ DevOps, SRE' 카테고리의 다른 글
DevOps vs SRE , 차이점과 공통점에 대해 (0) | 2023.08.22 |
---|---|
짧은 생각 모음집 - crossplane, eBPF 편 (2) | 2023.07.06 |
쿠버네티스의 4가지 설계 원칙 : Understand the "Why?" (1) | 2023.06.04 |
신입 데브옵스 (DevOps) 엔지니어 되기 - (2) Drone CI 마이그레이션 하기 (2) | 2023.04.16 |
[k8s] 당신의 pod에서 nslookup이 실패하는 이유 (0) | 2023.02.18 |