- Published on
k0s 폐쇄망 설치와 k0sctl 자동화 — 이미지 번들 제작부터 다중 노드 업그레이드까지
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — 바이너리 하나와 번들 하나로 끝내려면
- 반입 대상 산출 — k0s가 요구하는 파일
- 이미지 번들 제작 — list-images와 bundle-artifacts
- 수동 설치 — 단일 노드와 컨트롤러 플러스 워커
- k0sctl 자동화 — SSH로 번들과 바이너리를 밀어 넣는다
- 업그레이드 경로와 k0sctl의 한계
- 여기서 막힙니다 — k0s 폐쇄망 실패 모드와 k3s와의 비교
- 마치며 — 자동화 도구가 SSH를 쓴다는 사실이 전부입니다
- 참고 자료
들어가며 — 바이너리 하나와 번들 하나로 끝내려면
k0s의 설계 철학은 단순합니다. 단일 바이너리 하나가 컨트롤 플레인이자 워커이고, 설치 스크립트도 패키지 저장소도 필요 없습니다. 폐쇄망 관점에서 이건 큰 장점입니다. 반입 심의에 올릴 파일이 실질적으로 두 개로 줄어들기 때문입니다. k0s 바이너리 하나와 이미지 번들 하나입니다.
다만 이 단순함에는 대가가 있습니다. k3s는 릴리스에 올라온 에어갭 아카이브를 그대로 복사하면 끝나지만, k0s는 "무엇을 번들에 넣을지"를 사용자가 명시적으로 결정하는 구조에 가깝습니다. 그래서 자체 워크로드 이미지를 같은 번들에 합치기는 오히려 쉽고, 대신 번들 제작 단계에서 인터넷이 되는 장비에 k0s 바이너리를 먼저 갖다 놓아야 합니다.
검증 기준은 아래와 같습니다.
| 항목 | 값 | 확인 시점 | 확인한 출처 |
|---|---|---|---|
| k0s 최신 안정판 | v1.36.3+k0s.0 (2026-07-27) | 2026-07-31 | k0s 릴리스 |
| k0sctl 최신판 | v0.32.2 (2026-07-28) | 2026-07-31 | k0sctl 릴리스 |
| 번들 자동 임포트 | 데이터 디렉터리 하위 images | 2026-07-31 | k0s Airgap Install |
| 기본 데이터 디렉터리 | /var/lib/k0s | 2026-07-31 | k0s Airgap Install |
k0sctl v0.32.0에서 호스트 접속·원격 실행 계층이 rig v0.x에서 rig v2.0.0으로 통째로 교체되었습니다. 릴리스 노트가 접속·파일 전송·OS 감지 영역의 회귀를 주시하라고 명시했으니, 폐쇄망에 도입할 때는 스테이징에서 파일 업로드 경로를 반드시 먼저 검증하십시오.
반입 대상 산출 — k0s가 요구하는 파일
k0s 릴리스 페이지에는 폐쇄망에 필요한 자산이 이미 올라와 있습니다. v1.36.3+k0s.0 기준으로 확인한 주요 자산은 다음과 같습니다.
| 자산 | 크기(확인 시점) | 용도 |
|---|---|---|
| k0s-v1.36.3+k0s.0-amd64 | 약 250 MB | k0s 단일 바이너리 |
| k0s-airgap-bundle-v1.36.3+k0s.0-linux-amd64.tar | 약 390 MB | 기본 시스템 이미지 번들 (즉시 사용 가능) |
| k0s-airgap-bundle-v1.36.3+k0s.0-linux-arm64.tar | 약 357 MB | arm64 노드용 번들 |
| airgap-images 텍스트 파일 (amd64/arm/arm64) | 수 KB | 번들에 들어간 이미지 목록 |
| SHA256 체크섬 파일 | 수 KB | 무결성 검증 |
릴리스가 제공하는 번들을 그대로 쓰면 번들 제작 단계를 건너뛸 수 있습니다. 자체 이미지를 섞을 필요가 없는 최초 설치라면 이 경로가 가장 빠릅니다.
#!/usr/bin/env bash
# collect-k0s.sh — 인터넷이 되는 스테이징 장비
set -euo pipefail
K0S_VERSION="v1.36.3+k0s.0"
ARCH="amd64"
URLVER="${K0S_VERSION/+/%2B}"
BASE="https://github.com/k0sproject/k0s/releases/download/${URLVER}"
OUT="./k0s-airgap-${K0S_VERSION}"
mkdir -p "${OUT}" && cd "${OUT}"
curl -fL -o k0s "${BASE}/k0s-${K0S_VERSION}-${ARCH}"
chmod +x k0s
curl -fL -o "k0s-airgap-bundle-${K0S_VERSION}-linux-${ARCH}.tar" \
"${BASE}/k0s-airgap-bundle-${K0S_VERSION}-linux-${ARCH}.tar"
# 반입 매니페스트는 직접 만듭니다 — 안쪽에서 매체 손상을 판별할 유일한 근거입니다
sha256sum k0s "k0s-airgap-bundle-${K0S_VERSION}-linux-${ARCH}.tar" > MANIFEST.sha256
echo "${K0S_VERSION}" > VERSION
ls -la
이미지 번들 제작 — list-images와 bundle-artifacts
릴리스 번들을 쓰지 않고 직접 만들어야 하는 경우가 있습니다. 자체 워크로드 이미지를 같이 넣고 싶거나, 기본 CNI 대신 다른 것을 쓰거나, 사내 정책상 모든 이미지를 사내 레지스트리 경로로 태깅해서 반입해야 하는 경우입니다.
k0s는 필요한 이미지 목록을 스스로 뽑아 줍니다.
# 인터넷이 되는 장비에 k0s 바이너리를 두고 실행
./k0s airgap list-images --all > airgap-images.txt
wc -l airgap-images.txt
cat airgap-images.txt
목록이 나오면 번들로 묶습니다. 공식 문서가 제시하는 방법은 세 가지이고, 각각 전제 조건이 다릅니다.
# 방법 A — k0s 내장 도구 (도커도 실행 중인 k0s 도 필요 없음)
./k0s airgap bundle-artifacts -v -o image-bundle.tar < airgap-images.txt
# 방법 B — 이미 동작 중인 워커 노드의 containerd 저장소에서 내보내기
k0s ctr images export image-bundle.tar $(k0s airgap list-images | xargs)
# 방법 C — 도커가 있는 장비에서
k0s airgap list-images --all > airgap-images.txt
xargs -I{} docker pull {} < airgap-images.txt
docker image save -o image-bundle.tar $(xargs < airgap-images.txt)
세 방법의 실무적 차이는 이렇습니다.
| 방법 | 전제 조건 | 폐쇄망 반입 관점의 평가 |
|---|---|---|
| bundle-artifacts | k0s 바이너리와 인터넷 | 가장 깔끔. 도커 데몬 의존이 없어 심의용 스테이징 장비에 적합 |
| ctr images export | 이미 동작 중인 k0s 워커 노드 | 이미 검증된 이미지를 그대로 뜬다는 장점. 스테이징 클러스터가 필요 |
| docker save | 도커 데몬 | 익숙하지만 도커 설치가 필요. 멀티 아키텍처 처리가 가장 번거로움 |
방법 A를 기본으로 삼고, 스테이징 클러스터가 이미 있으면 방법 B로 "실제로 돌아간 이미지"를 뜨는 편이 안전합니다.
공식 문서는 k0s가 느슨한 플랫폼 매칭을 쓰기 때문에 멀티 아키텍처 번들이 서로 다른 플랫폼에서 동작한다고 명시합니다. amd64 노드와 arm64 엣지 노드가 섞인 폐쇄망에서 번들을 하나로 관리할 수 있다는 뜻이므로, 반입 단위를 줄이는 데 활용할 여지가 있습니다.
자체 워크로드 이미지를 같은 번들에 합치기
k0s 문서는 "k0s가 기본으로 쓰지 않는 컨테이너 이미지를 포함하도록 번들을 쉽게 커스터마이징할 수 있다"고 하며, 번들로 묶기 전에 이미지 목록을 검토하고 편집하라고 안내합니다. 실제로는 목록 파일에 줄을 더하기만 하면 됩니다.
# 1) k0s 시스템 이미지 목록을 뽑고
./k0s airgap list-images --all > airgap-images.txt
# 2) 사내 워크로드 이미지를 같은 목록에 덧붙입니다
cat >> airgap-images.txt <<'EOF'
registry.internal.example:5000/apps/api:1.4.2
registry.internal.example:5000/apps/worker:1.4.2
registry.internal.example:5000/infra/postgres:16.4
registry.internal.example:5000/infra/prometheus:v3.1.0
EOF
# 3) 중복 제거 후 한 번에 묶기
sort -u airgap-images.txt -o airgap-images.txt
./k0s airgap bundle-artifacts -v -o image-bundle.tar < airgap-images.txt
# 4) 매니페스트 갱신
sha256sum image-bundle.tar >> MANIFEST.sha256
이 목록 파일을 Git에 올려 두는 것을 강하게 권합니다. 폐쇄망 반입은 반복 작업이고, "이번 반입에 무엇이 들어갔는가"를 나중에 재구성할 수 있어야 합니다. 목록이 코드로 관리되면 다음 반입 때 diff만 보면 됩니다.
수동 설치 — 단일 노드와 컨트롤러 플러스 워커
반입이 끝났다면 설치 자체는 짧습니다. 번들을 데이터 디렉터리 하위 images 폴더에 두고 k0s를 기동하면, k0s가 그 폴더를 감시하다가 자동으로 임포트합니다.
#!/usr/bin/env bash
# install-k0s-single.sh — 폐쇄망 단일 노드
set -euo pipefail
STAGE=/opt/staging/k0s-airgap-v1.36.3+k0s.0
# 0) 무결성 검증부터
cd "${STAGE}" && sha256sum -c MANIFEST.sha256
# 1) 바이너리 배치
sudo install -m 0755 "${STAGE}/k0s" /usr/local/bin/k0s
k0s version
# 2) 이미지 번들 배치 — 기동 전에 넣어야 합니다
sudo mkdir -p /var/lib/k0s/images
sudo cp "${STAGE}"/k0s-airgap-bundle-*.tar /var/lib/k0s/images/image-bundle.tar
# 3) 단일 노드 설치
sudo k0s install controller --single
sudo k0s start
# 4) 상태 확인 (번들 임포트에 시간이 걸리므로 바로 Ready 가 아닐 수 있습니다)
sudo k0s status
sudo k0s kubectl get nodes -o wide
sudo k0s kubectl -n kube-system get pods
컨트롤러와 워커를 나누는 배치라면 다음과 같습니다. 이미지 번들은 워커 노드에 필요합니다. 실제로 이미지를 pull 하고 컨테이너를 띄우는 쪽이 워커이기 때문입니다. 컨트롤러가 워커를 겸하는 경우에는 컨트롤러에도 번들이 있어야 합니다.
# 컨트롤러 노드
sudo install -m 0755 /opt/staging/k0s /usr/local/bin/k0s
sudo k0s install controller --enable-worker --no-taints
sudo k0s start
sudo k0s status
# 워커 조인용 토큰 발급 (컨트롤러에서)
sudo k0s token create --role=worker > /opt/staging/worker.token
# 워커 노드
sudo install -m 0755 /opt/staging/k0s /usr/local/bin/k0s
sudo mkdir -p /var/lib/k0s/images
sudo cp /opt/staging/image-bundle.tar /var/lib/k0s/images/image-bundle.tar
sudo k0s install worker --token-file /opt/staging/worker.token
sudo k0s start
sudo k0s status
외부 pull 시도를 아예 막는다
번들이 제대로 들어갔는지 확인하는 가장 확실한 방법은, 외부에서 이미지를 가져오는 경로 자체를 차단해 보는 것입니다. k0s는 클러스터 설정으로 이를 지원합니다.
# k0s.yaml
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
spec:
images:
default_pull_policy: Never
공식 문서는 이 설정이 "Pod의 imagePullPolicy를 Never로 만들어 인터넷에서 이미지를 가져오지 않도록 보장한다"고 설명합니다. 폐쇄망에서는 이 값을 처음부터 켜 두는 편이 낫습니다. 그러면 번들에 빠진 이미지가 있을 때 몇 분 뒤 타임아웃이 아니라 즉시 실패하므로, 반입 목록의 누락을 그 자리에서 찾아냅니다.
다만 이 값을 켜면 사내 레지스트리에서 가져오는 이미지도 영향을 받는 구성이 될 수 있으니, 애플리케이션 Pod까지 일괄 적용할지는 클러스터 설정을 적용한 상태에서 실제 배포로 확인해야 합니다. 시스템 컴포넌트에만 적용되는 범위인지 워크로드 전체인지는 이번 확인 범위에서 문서 본문만으로 단정하지 못했으므로, 스테이징에서 직접 검증하십시오.
k0sctl 자동화 — SSH로 번들과 바이너리를 밀어 넣는다
노드가 세 대를 넘어가면 수동 절차는 곧 무너집니다. 누군가 한 노드에서 번들 복사를 빠뜨리고, 그 노드만 이상하게 동작합니다. k0sctl은 이 반복을 YAML 한 장으로 대체합니다.
핵심은 k0sctl이 폐쇄망 안쪽 점프 호스트에서 실행된다는 점입니다. k0sctl은 인터넷이 아니라 SSH로 대상 노드에 접속하므로, 점프 호스트에 k0s 바이너리와 이미지 번들만 있으면 나머지는 k0sctl이 각 노드로 전송합니다.
# 폐쇄망 점프 호스트에 k0sctl 바이너리 배치 (이것도 반입 대상입니다)
sudo install -m 0755 /opt/staging/k0sctl /usr/local/bin/k0sctl
k0sctl version
# 초기 설정 파일 생성
k0sctl init > k0sctl.yaml
# k0sctl.yaml — 폐쇄망 3노드 구성
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
name: onprem-airgap
spec:
k0s:
version: v1.36.3+k0s.0
config:
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
spec:
images:
default_pull_policy: Never
hosts:
- role: controller
uploadBinary: true
k0sBinaryPath: /opt/staging/k0s
ssh:
address: 10.10.20.11
user: k0sadmin
keyPath: /home/k0sadmin/.ssh/id_ed25519
- role: worker
uploadBinary: true
k0sBinaryPath: /opt/staging/k0s
ssh:
address: 10.10.20.21
user: k0sadmin
keyPath: /home/k0sadmin/.ssh/id_ed25519
files:
- src: /opt/staging/image-bundle.tar
dstDir: /var/lib/k0s/images
perm: 0755
- role: worker
uploadBinary: true
k0sBinaryPath: /opt/staging/k0s
ssh:
address: 10.10.20.22
user: k0sadmin
keyPath: /home/k0sadmin/.ssh/id_ed25519
files:
- src: /opt/staging/image-bundle.tar
dstDir: /var/lib/k0s/images
perm: 0755
여기서 폐쇄망에 결정적인 두 필드가 있습니다.
uploadBinary: true— k0sctl이 각 노드에서 k0s를 내려받는 대신, 실행 호스트의 바이너리를 업로드합니다. 이 값이 없으면 노드가 릴리스 서버에 접속을 시도하고 폐쇄망에서는 그 자리에서 실패합니다. 공식 에어갭 문서의 예시도 이 필드를 씁니다.files— 임의의 파일을 노드의 지정 경로로 전송합니다. 에어갭 문서 예시가 그대로 이미지 번들을 이 방식으로 워커에 올립니다.
k0sBinaryPath로 업로드할 로컬 바이너리 경로를 명시하는 방식은 실무에서 널리 쓰이지만, 이번 확인 시점에 공식 에어갭 문서 본문에서 이 필드명 자체를 직접 확인하지는 못했습니다. uploadBinary만으로도 동작하므로, 필드명이 불확실하면 먼저 uploadBinary: true만 두고 k0sctl apply --debug로 어떤 바이너리가 어디서 올라가는지 확인한 뒤 추가하십시오. 같은 이유로, 에어갭 문서 예시의 kind 값이 렌더링에 따라 다르게 보이는 경우가 있어 여기서는 k0sctl 설치 문서 기준인 kind: Cluster를 썼습니다.
적용과 접속은 다음과 같습니다.
# 구성 적용 — 바이너리 업로드, 번들 전송, 클러스터 구성까지 한 번에
k0sctl apply --config k0sctl.yaml
# kubeconfig 회수
k0sctl kubeconfig --config k0sctl.yaml > kubeconfig
kubectl --kubeconfig kubeconfig get nodes -o wide
kubectl --kubeconfig kubeconfig -n kube-system get pods
SSH 접근 요건은 폐쇄망에서 오히려 까다로울 수 있습니다. 점프 호스트에서 각 노드의 22번 포트가 열려 있어야 하고, 키 기반 인증이 되어야 하며, 대상 사용자가 sudo를 비밀번호 없이 쓸 수 있어야 합니다. 이 세 가지 중 하나라도 사내 보안 정책과 충돌하면 k0sctl 경로는 포기하고 수동 설치로 가야 합니다. 도입 전에 먼저 확인할 항목입니다.
업그레이드 경로와 k0sctl의 한계
k0sctl의 가장 큰 실무 가치는 설치가 아니라 업그레이드입니다. 절차가 설치와 완전히 같습니다.
# 1) 새 버전 바이너리와 번들을 점프 호스트에 반입
sha256sum -c /opt/staging/k0s-v1.36.3/MANIFEST.sha256
# 2) k0sctl.yaml 의 버전 문자열과 파일 경로만 갱신
# spec.k0s.version: v1.36.3+k0s.0
# hosts[].files[].src: /opt/staging/k0s-v1.36.3/image-bundle.tar
# 3) 같은 명령으로 적용 — k0sctl 이 노드를 순서대로 처리합니다
k0sctl apply --config k0sctl.yaml
# 4) 검증
kubectl --kubeconfig kubeconfig get nodes -o wide
kubectl --kubeconfig kubeconfig get pods -A --field-selector=status.phase!=Running
새 이미지 번들을 먼저 올려야 한다는 점이 폐쇄망 특유의 순서입니다. files 항목이 앞에 있으므로 k0sctl이 번들을 전송한 뒤 k0s를 교체하지만, 번들 파일명이 이전 것과 같으면 덮어쓰기가 되고 다르면 두 개가 쌓입니다. 디스크가 빠듯한 엣지 노드에서는 이전 번들 정리를 별도 단계로 넣어야 합니다.
한계도 분명합니다. 공식 문서는 k0sctl이 노드를 추가할 수는 있지만 기존 노드를 제거할 수는 없다고 못 박습니다. 폐쇄망에서 하드웨어를 교체하거나 노드를 줄일 때는 k0sctl 설정에서 지운다고 클러스터에서 빠지지 않으므로, 별도의 절차가 필요합니다.
# 노드 제거는 수동입니다
kubectl --kubeconfig kubeconfig drain worker-03 --ignore-daemonsets --delete-emptydir-data
kubectl --kubeconfig kubeconfig delete node worker-03
# 해당 노드에서 k0s 정리
sudo k0s stop
sudo k0s reset
여기서 막힙니다 — k0s 폐쇄망 실패 모드와 k3s와의 비교
| 증상 | 원인 | 확인 방법 |
|---|---|---|
| k0s는 떴는데 모든 Pod가 ImagePullBackOff | 번들을 기동 후에 넣었거나 경로가 틀림 | ls /var/lib/k0s/images 와 k0s ctr images ls 대조 |
| 워커만 Pod가 안 뜸 | 번들을 컨트롤러에만 배치 | 각 워커에서 images 폴더 확인 |
| k0sctl apply 가 다운로드에서 멈춤 | uploadBinary 누락 | k0sctl apply --debug 로 전송 로그 확인 |
| k0sctl apply 가 SSH 단계에서 실패 | 키 인증 또는 무비밀번호 sudo 불가 | ssh 로 직접 접속 후 sudo -n true 시도 |
| 노드를 뺐는데 클러스터에 계속 보임 | k0sctl 은 노드 제거를 하지 않음 | kubectl delete node 와 k0s reset 을 수동 수행 |
| 임포트가 오래 걸려 Ready 가 늦음 | 번들 크기가 수백 MB — 정상 동작 | journalctl -u k0scontroller 또는 k0sworker 로 임포트 로그 확인 |
| arm64 노드에서만 실패 | 아키텍처별 번들을 잘못 배포 | 번들 파일명과 uname -m 대조 |
임포트 소요 시간에 대해 한마디 덧붙입니다. 공식 문서는 k0s가 images 폴더를 감시해 자동 임포트한다고만 하고, 소요 시간은 언급하지 않습니다. 실제로는 수백 MB 번들을 풀어 containerd 저장소에 넣는 작업이므로 디스크가 느린 엣지 하드웨어에서는 수 분이 걸릴 수 있습니다. 기동 직후 노드가 NotReady라고 해서 바로 실패로 판단하지 말고, 서비스 로그에서 임포트 진행을 먼저 확인하십시오.
sudo journalctl -u k0scontroller -f # 컨트롤러
sudo journalctl -u k0sworker -f # 워커
sudo k0s ctr images ls | wc -l # 임포트된 이미지 수가 늘어나는지
k3s 흐름과의 정직한 비교
| 축 | k3s | k0s |
|---|---|---|
| 이미지 반입 | 릴리스 아카이브를 이미지 폴더에 복사 | 릴리스 번들 복사 또는 list-images로 직접 제작 |
| 자체 이미지 합치기 | 별도 tar를 추가로 배치 | 목록 파일에 줄만 추가해 한 번들로 통합 — 더 깔끔 |
| 설치 진입점 | install.sh 와 환경변수 | k0s install 서브커맨드 — 스크립트 반입이 불필요 |
| 다중 노드 자동화 | 별도 도구 필요 (Ansible 등) | k0sctl 이 공식 도구로 존재 — 더 깔끔 |
| 외부 pull 차단 | disable-default-registry-endpoint (미러 설정 대상에만) | default_pull_policy Never — 의도가 더 직관적 |
| 사설 레지스트리 미러 | registries.yaml 로 rewrite 까지 지원 | containerd 설정을 직접 다뤄야 함 — 손이 더 감 |
| 노드 제거 | 수동 | 수동 (k0sctl 이 지원하지 않음) |
| 커뮤니티 자료 | 압도적으로 많음 | 상대적으로 적어 문제 해결이 느림 — 폐쇄망에서 체감이 큼 |
정리하면, 번들 제작과 다중 노드 자동화는 k0s가 낫고, 사설 레지스트리 미러링과 문제 해결 자료는 k3s가 낫습니다. 사내 레지스트리를 이미 운영 중이고 이미지 경로 재작성이 필요한 환경이면 k3s의 registries.yaml이 주는 이점이 큽니다. 반대로 레지스트리 없이 번들만으로 여러 노드를 반복 구축·업그레이드해야 하는 환경이면 k0sctl 한 장이 주는 이점이 큽니다.
마치며 — 자동화 도구가 SSH를 쓴다는 사실이 전부입니다
폐쇄망에서 k0sctl이 유효한 이유는 기능이 화려해서가 아니라, 인터넷이 아니라 SSH를 쓰기 때문입니다. 반입 심의를 통과한 파일이 점프 호스트에 놓이는 순간부터는 클러스터 구축이 순수한 내부 작업이 됩니다. 이 성질을 가진 도구인지 아닌지가 폐쇄망 도구 선택의 첫 번째 기준입니다.
그리고 목록 파일을 Git에 넣으십시오. 번들에 무엇이 들어갔는지 기록하지 않은 반입은 반드시 다음 반입 때 같은 조사를 처음부터 다시 하게 만듭니다.