Certified Kubernetes Application Developer (CKAD) #4 컨테이너 이미지: Dockerfile, 멀티스테이지, 시험에서 직접 빌드
#3 Multi-container 패턴에서 하나의 Pod 안에 여러 컨테이너를 배치하는 설계를 다뤘다면, 이번 글은 그 컨테이너의 재료인 이미지를 직접 만드는 작업으로 내려갑니다. CKAD는 매니페스트만 작성하는 시험이 아닙니다. 일부 작업은 주어진 소스로 이미지를 직접 빌드해 레지스트리에 푸시한 뒤, 그 이미지를 참조하는 Pod를 띄우는 흐름까지 한 번에 요구합니다.
그래서 이 글은 Dockerfile의 기본 명령부터 멀티스테이지 빌드, 그리고 시험 환경에서 자주 쓰이는 podman,buildah 빌드 절차와 imagePullPolicy의 함정까지, 이미지를 만들고 참조하는 한 사이클을 실기 관점으로 정리하겠습니다. Docker가 익숙하다면 대부분 그대로 통하지만, 시험 환경의 차이를 짚어 두는 것이 핵심입니다.
왜 CKAD에서 이미지를 직접 빌드하는가 #
CKAD의 첫 번째 도메인은 Application Design and Build 입니다. 이름 그대로 애플리케이션을 빌드하는 능력을 포함하므로, 시험에는 “주어진 디렉터리의 Dockerfile로 이미지를 빌드해 태그 myapp:1.0으로 만들고, 로컬 레지스트리에 푸시한 뒤, 그 이미지로 Pod를 실행하라” 같은 작업이 나올 수 있습니다.
이 작업이 까다로운 이유는 한 작업 안에 빌드 도구,태깅,푸시,매니페스트가 모두 섞여 있어서, 어느 한 단계라도 어긋나면 Pod가 ErrImagePull 또는 ImagePullBackOff로 떨어지기 때문입니다. 그래서 빌드 한 번이 아니라, 이미지가 클러스터에서 정상적으로 당겨지는지까지 확인하는 습관이 필요합니다. Docker 입문 시리즈를 봤다면 Dockerfile 자체는 익숙하겠지만, 여기서는 시험에서 점수로 직결되는 부분만 압축해 다루겠습니다.
Dockerfile 기본 명령 #
이미지는 Dockerfile이라는 텍스트 파일의 명령을 위에서 아래로 실행해 만들어집니다. 각 명령은 하나의 레이어를 만들고, 이 레이어들이 쌓여 최종 이미지가 됩니다.
FROM node:20-alpine # 베이스 이미지 (항상 첫 명령)
WORKDIR /app # 이후 명령의 기준 경로
COPY package*.json ./ # 의존성 파일만 먼저 복사 (캐시 최적화)
RUN npm ci --omit=dev # 빌드 시점 셸 명령 실행
COPY . . # 나머지 소스 복사
EXPOSE 3000 # 노출 포트 문서화 (실제 개방 아님)
CMD ["node", "server.js"] # 시작 시 기본 명령 (덮어쓰기 가능)RUN은 빌드 시점에 셸 명령을 실행하고, CMD와 ENTRYPOINT는 컨테이너가 시작될 때 무엇을 실행할지를 정합니다. 이 둘의 구분이 핵심입니다.
CMD와 ENTRYPOINT의 차이 #
이 둘의 차이는 시험에서 command,args 매핑을 묻는 문제로 이어지므로 정확히 알아둬야 합니다.
ENTRYPOINT는 컨테이너를 무엇으로 실행할지를 고정합니다.docker run또는 Pod의args가 이 뒤에 인자로 붙습니다.CMD는 기본 인자 또는 기본 명령입니다. 실행 시 인자를 주면 통째로 대체됩니다.
권장 패턴은 ENTRYPOINT로 실행 파일을 고정하고 CMD로 기본 인자를 주는 조합입니다.
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]이렇게 두면 기본 실행은 python app.py --port 8080이고, 실행 시 --port 9090을 넘기면 python app.py --port 9090이 됩니다.
레이어 캐시: 명령 순서가 빌드 속도를 가른다 #
빌드는 변경되지 않은 레이어를 캐시에서 재사용합니다. 어떤 레이어가 바뀌면 그 아래 모든 레이어는 다시 빌드됩니다. 그래서 위 예제처럼 자주 바뀌는 소스(COPY . .)보다 거의 바뀌지 않는 의존성 파일(COPY package*.json)을 먼저 복사하고 설치하면, 소스만 고쳤을 때 의존성 설치 레이어를 재사용해 빌드가 빨라집니다.
시험에서 빌드 시간이 길어지면 그만큼 시간을 잃으므로, 캐시가 잘 듣는 순서를 알아두는 것이 실전에서 유리합니다.
멀티스테이지 빌드로 이미지 경량화 #
빌드에 필요한 도구(컴파일러, 패키지 매니저)는 실행 시점에는 필요 없습니다. 그런데도 한 단계로만 빌드하면 이 도구들이 최종 이미지에 그대로 남아 이미지가 무거워집니다. 멀티스테이지 빌드는 빌드 단계와 실행 단계를 분리해, 실행 단계로는 결과물만 복사합니다.
Go 예제: 빌드 스테이지 + distroless 런타임 #
# build stage: Go 컴파일러로 바이너리 생성
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server
# runtime stage: 셸도 없는 최소 이미지
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]핵심은 FROM ... AS builder로 단계에 이름을 붙이고, 마지막 단계에서 COPY --from=builder로 이전 단계의 결과물만 가져오는 부분입니다. Go 컴파일러가 들어 있던 첫 단계는 최종 이미지에 포함되지 않으므로, 수백 MB 이미지가 수십 MB로 줄어듭니다. Node 라면 빌드 단계에서 npm run build로 번들을 만든 뒤 런타임 단계의 node:20-alpine으로 dist와 node_modules만 복사하는 식으로 같은 패턴을 적용합니다. 빌드 도구와 소스가 최종 이미지에 남지 않으므로 이미지 크기뿐 아니라 보안(공격 표면)에서도 유리합니다.
빌드,태그,푸시 #
이제 만든 Dockerfile로 이미지를 빌드해 레지스트리에 올리는 절차입니다. 시험 환경에 따라 빌드 도구가 docker가 아니라 podman 또는 buildah 인 경우가 있습니다. 어느 명령으로도 같은 작업을 할 수 있어야 하므로 둘 다 정리하겠습니다.
podman으로 빌드,태그,푸시 #
podman은 docker와 명령 호환성이 높아 거의 그대로 옮겨 쓸 수 있습니다. 빌드 → 태그 → 푸시 순서는 다음과 같습니다.
podman build -t myapp:1.0 . # 빌드
podman tag myapp:1.0 registry.example.com/team/myapp:1.0 # 레지스트리 경로 태그
podman push registry.example.com/team/myapp:1.0 # 푸시
podman images # 로컬 이미지 확인buildah,docker 병기 #
buildah는 이미지 빌드에 특화된 도구로, Dockerfile 빌드는 buildah bud로 합니다. docker가 설치된 환경이라면 docker 명령도 그대로 통합니다.
# buildah (bud = build-using-dockerfile)
buildah bud -t myapp:1.0 .
buildah push myapp:1.0 docker://registry.example.com/team/myapp:1.0
# docker
docker build -t myapp:1.0 .
docker tag myapp:1.0 registry.example.com/team/myapp:1.0
docker push registry.example.com/team/myapp:1.0세 도구 모두 build -t <이름:태그> . → push 흐름은 동일합니다. 시험에서는 문제 지문이 지정한 도구와 태그를 정확히 따르는 것이 채점의 전제입니다.
이미지 참조와 imagePullPolicy #
빌드한 이미지를 Pod에서 참조하는 방식과, 클러스터가 그 이미지를 언제 다시 당기는지를 결정하는 imagePullPolicy를 짚겠습니다.
태그와 digest #
이미지는 이름:태그(예: myapp:1.0) 또는 이름@digest(예: myapp@sha256:abc123...)로 참조합니다. 태그는 같은 이름에 다른 내용을 다시 푸시할 수 있어 가변적이지만, digest는 이미지 내용의 해시라 한 번 지정하면 항상 같은 이미지를 가리킵니다.
imagePullPolicy의 세 값 #
| 값 | 동작 |
|---|---|
Always | 매번 레지스트리에서 다시 당김 |
IfNotPresent | 노드에 이미지가 없을 때만 당김 |
Never | 절대 당기지 않음. 노드에 미리 있어야 함 |
기본값은 태그에 따라 달라집니다. 태그가 :latest이거나 태그를 생략하면 기본값이 Always, 그 외 구체적 태그면 IfNotPresent 입니다.
latest 태그의 함정 #
:latest는 항상 최신을 가리키는 특별한 태그가 아니라, 이름이 그냥 latest 인 평범한 태그입니다. 그런데도 기본 풀 정책이 Always가 되어 노드마다 다른 시점의 이미지를 당길 수 있고, 어떤 버전이 떠 있는지 추적하기 어렵습니다. 그래서 실무에서도 시험에서도 컨테이너에 image: ...myapp:1.0처럼 구체적 버전 태그를 명시하고 imagePullPolicy: IfNotPresent를 함께 두는 것이 안전합니다. 로컬에서 방금 빌드해 노드에 이미 있는 이미지라면 Never 또는 IfNotPresent로 두어, 존재하지 않는 레지스트리에서 당기려다 실패하는 상황을 막을 수 있습니다.
프라이빗 레지스트리: imagePullSecrets #
인증이 필요한 프라이빗 레지스트리의 이미지를 쓰려면, 자격 증명을 담은 docker-registry 타입 Secret을 만들고 Pod가 그것을 참조해야 합니다.
k create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=ckad \
--docker-password=<비밀번호> \
--docker-email=ckad@example.com만든 Secret은 Pod의 spec.imagePullSecrets에 연결합니다.
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/team/myapp:1.0이 연결을 빠뜨리면 인증 실패로 ImagePullBackOff가 나므로, 프라이빗 이미지를 다루는 작업에서는 Secret 생성과 imagePullSecrets 연결을 한 쌍으로 기억해야 합니다.
Pod에서 명령 덮어쓰기 (시험 단골) #
Pod 매니페스트의 command와 args는 Dockerfile의 ENTRYPOINT와 CMD를 덮어씁니다. 이 매핑을 묻는 문제가 자주 나오므로 표로 정리하겠습니다.
| Pod 필드 | Dockerfile 대응 | 동작 |
|---|---|---|
command | ENTRYPOINT | 실행 파일을 덮어씀 |
args | CMD | 인자를 덮어씀 |
즉 command를 지정하면 이미지의 ENTRYPOINT가 무시되고, args를 지정하면 CMD가 무시됩니다. 둘 다 생략하면 이미지의 ENTRYPOINT,CMD가 그대로 쓰입니다.
apiVersion: v1
kind: Pod
metadata:
name: cmd-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c"] # ENTRYPOINT를 덮어씀
args: ["echo hello && sleep 3600"] # CMD를 덮어씀명령형으로 만들 때는 -- 뒤의 토큰이 위 매핑을 따릅니다.
k run cmd-demo --image=busybox:1.36 $do \
--command -- sh -c "echo hello && sleep 3600" > pod.yaml여기서 --command 플래그가 있으면 -- 뒤가 command로, 없으면 args로 들어갑니다. 이 차이가 ENTRYPOINT를 덮을지 CMD를 덮을지를 가르므로 문제 지문을 정확히 읽어야 합니다.
시험 포인트 #
- 빌드 도구는 docker가 아닐 수 있다.
podman build,buildah bud로도 같은 작업을 할 수 있도록 손에 익혀 둡니다. - 태그와 푸시 경로를 지문 그대로. 채점은 지정된 이름,태그,레지스트리 경로를 기준으로 하므로 오타 하나가 실점입니다.
- 멀티스테이지는
AS와COPY --from. 빌드 단계 이름을 붙이고 마지막 단계에서 결과물만 복사하는 패턴을 외워 둡니다. - imagePullPolicy 기본값.
:latest또는 태그 생략은Always, 구체 태그는IfNotPresent. 로컬 빌드 이미지는Never,IfNotPresent로 풀 실패를 막습니다. - 프라이빗 레지스트리는 Secret + imagePullSecrets 한 쌍.
k create secret docker-registry와 Pod 연결을 함께 기억합니다. - command,args 매핑.
command→ENTRYPOINT,args→CMD.--command플래그 유무로 무엇을 덮을지 갈립니다.
정리 #
이번 글에서 잡은 것:
- CKAD는 이미지를 직접 빌드,푸시,실행하는 한 사이클을 요구할 수 있습니다. 빌드 한 번이 아니라 Pod가 정상적으로 이미지를 당기는지까지 확인합니다.
- Dockerfile 기본.
FROM,WORKDIR,COPY,RUN,EXPOSE, 그리고CMD와ENTRYPOINT의 차이와 레이어 캐시 순서 - 멀티스테이지 빌드. 빌드 단계와 실행 단계를 분리해 distroless,alpine으로 경량화하고 공격 표면을 줄입니다.
- 빌드,태그,푸시.
podman,buildah,docker세 도구 모두build -t→push흐름은 동일합니다. - 이미지 참조. 태그 vs digest,
imagePullPolicy세 값과:latest함정, 프라이빗 레지스트리의imagePullSecrets - command,args 가
ENTRYPOINT,CMD를 덮어쓰는 매핑
다음: Workloads 1 #
이미지를 만들어 Pod로 띄우는 단위까지 왔습니다. 이제 여러 Pod를 묶어 운영하는 워크로드로 올라갑니다.
#5 Workloads 1: Deployment, ReplicaSet, rolling update와 rollback에서는 ReplicaSet으로 복제본을 유지하는 원리, Deployment로 롤링 업데이트를 굴리는 법, 문제가 생겼을 때 kubectl rollout undo로 롤백하는 절차, 그리고 시험에서 자주 나오는 “특정 리비전으로 되돌리기” 유형까지 직접 만들어 보며 정리하겠습니다.