개요
쿠버네티스 클러스터가 커져감에 따라 새로 생겨나는 Worker Node에 Docker Image가 Pull되어 있지 않은 상태에서 Pod을 띄울때 상당히 오랜시간이 걸리는 것을 보고 개선 방안을 생각해보았다. sandbox와 prod 클러스터에 노드가 200대 씩 되고나니 자동화나 개선의 필요성이 느껴졌기 때문이다.
개선요약
- k8s에는
kind
중에 daemonset 이 존재하는데 동시에 모든 Worker Node에 명령을 내릴수 있음 - 기본적으로 daemonset은 무한반복하는데 이를 한번만 실행하게 yaml을 만든 뒤 shell script로 daemonset을 실행 후 종료 시킴
daemonset.yaml 정의
initContainer로 docker pull을 수행한 뒤 실제로는 puase 컨테이너가 동작하는 Trick으로 daemonset이 단한번만 동작하게 만든다. 예제에서는 hello-world
docker image를 pull받는다.
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
name: kwangsiklee-prepull
spec:
selector:
matchLabels:
name: kwangsiklee-prepull
template:
metadata:
labels:
name: kwangsiklee-prepull
spec:
initContainers:
- name: kwangsiklee-prepull
image: docker
command: ["docker", "pull", "hello-world"]
volumeMounts:
- name: docker
mountPath: /var/run
volumes:
- name: docker
hostPath:
path: /var/run
containers:
- name: pause
image: gcr.io/google_containers/pause
run-once-daemoneset.sh
스크립트를 통해 daemonset을 생성하면 initContainers
단계에서 docker 이미지를 pull 받은 뒤 연달아 pause
컨테이너를 실행한 상태에서 Pod이 Running 상태로 바뀐다.
이후 모든 Pod이 다 running으로 바뀌길 기다린 후 daemonset을 삭제한 뒤 종료한다.
#!/bin/bash
set -euo pipefail
function main() {
kubectl apply -f daemonset.yaml
wait_for_pods kwangsiklee-prepull
kubectl delete -f daemonset.yaml
}
function wait_for_pods() {
echo -n "waiting for $1 pods to run"
sleep 5
PODS=$(kubectl get pods | grep $1 | awk '{print $1}')
for POD in ${PODS}; do
while [[ $(kubectl get pod ${POD} -o go-template --template "{{.status.phase}}") != "Running" ]]; do
sleep 1
echo -n "."
done
done
echo
}
main
검증하기
쉘을 실행해보면 아래와 같이 정상 실행됨을 알 수 있다.
daemonset.yaml만 따로 실행해보면 아래와 같이 각 Pod이 모두 Running되는것을 알 수 있다.
daemonset도 조회해보면 Worker Node 200개에 정상적으로 적용된것을 확인 할 수 있다.
응용하기 #1
필자의 조직에서는 젠킨스를 통해 Docker Build & Push를 관리한다. 이제 젠킨스를 통해 Docker Image Build & Push Job 이후로 연달아 k8s Worker Node에 daemonset이 실행되게 downstream job을 걸어두면 Docker Build 후 자동으로 전체 Worker Node에 Docker Pull을 받아두게 된다.
예를들어 아래와 같이 Docker Build Job이 있으면 이 후 prepull하는 job을 downstream 걸어둔다.
앞으로 docker build 후 연달아 prepull job이 실행되어 전체 worker node에 docker image가 pull된다.
응용하기 #2
조금 더 응용하면 docker image 및 deploy phase(개발 phase인 sandbox 인지 상용 phase인 prod인지)등을 고려하여 동적 스크립트화 할 수 있다.
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
name: {{IMAGE_NAME}}-prepull-{{PHASE}}
spec:
selector:
matchLabels:
name: {{IMAGE_NAME}}-prepull-{{PHASE}}
template:
metadata:
labels:
name: {{IMAGE_NAME}}-prepull-{{PHASE}}
spec:
initContainers:
- name: {{IMAGE_NAME}}-prepull-{{PHASE}}
image: docker
command: ["docker", "pull", "{{IMAGE_NAME}}:{{TAG}}"]
volumeMounts:
- name: docker
mountPath: /var/run
volumes:
- name: docker
hostPath:
path: /var/run
containers:
- name: pause
image: gcr.io/google_containers/pause
#!/bin/bash
if [ "$#" -eq 0 ]; then
echo "Usage: $0 image_name tag (prod|sandbox)" >&2
exit 1
fi
if [ $3 == 'prod' ]; then
prod 클러스터로 change
else
sandbox 클러스터로 change
fi
set -euo pipefail
function wait_for_pods() {
echo "waiting for $1 pods to run"
sleep 15
while [[ $(kubectl get daemonset | sed -n 2p | awk '{print $2}') -gt $(kubectl get daemonset | sed -n 2p | awk '{print $4}') ]]; do
sleep 3
echo "progress: $(kubectl get daemonset | sed -n 2p | awk '{print $4}')/$(kubectl get daemonset | sed -n 2p | awk '{print $2}')"
done
echo "completed"
}
cat daemonset.yaml | sed "s/{{PHASE}}/$3/g" | sed "s/{{IMAGE_NAME}}/$1/g" | sed "s/{{TAG}}/$2/g" | kubectl apply -f -
wait_for_pods $1-prepull
cat daemonset.yaml | sed "s/{{PHASE}}/$3/g" | sed "s/{{IMAGE_NAME}}/$1/g" | sed "s/{{TAG}}/$2/g" | kubectl delete -f -
Reference
- https://codefresh.io/kubernetes-tutorial/single-use-daemonset-pattern-pre-pulling-images-kubernetes/
- https://etoews.github.io/blog/2017/05/13/run-once-daemonset-on-kubernetes/