폐쇄망에 같은 구성을 여러 번 올리다 보면 스크립트가 길어진다. 한 파일에 몰아 넣으면 중간에 실패했을 때 처음부터 다시 돌려야 하고, 어디까지 됐는지 알기 어렵다. 단계별 파일로 나누고 공통 함수를 모아 두면 실패 지점부터 이어서 돌릴 수 있다.
bootstrap/
.env 공통 환경변수
files/
kubeadm-config.tmpl.yaml
harbor.yml.tmpl
registries.yaml.tmpl
scripts/
util.sh 공통 함수
01_prereqs.sh 커널 모듈 · sysctl · swap · 방화벽
02_runtime.sh containerd 설치와 설정
03_kube_binaries.sh kubeadm · kubelet · kubectl
04_kubeadm_init.sh 컨트롤 플레인
05_join_worker.sh 워커 조인
06_cni.sh CNI 배포
07_ingress.sh ingress-nginx
08_registry.sh Harbor
99_uninstall.sh 정리
Makefile
각 스크립트는 단독 실행과 Make 타깃 양쪽에서 돌아가게 둔다.
source 와 bash 실행은 성격이 다르다. 헷갈리면 변수를 못 읽거나 exit 가 상위 스크립트까지 끝내 버린다.
| 방식 | 프로세스 | 변수·함수 | exit 영향 |
|---|---|---|---|
source utils.sh |
현재 셸 | 공유된다 | 호출한 스크립트까지 종료 |
bash step.sh |
새 프로세스 | 공유되지 않는다 | 호출한 스크립트는 계속 |
설정과 함수는 source, 단계 실행은 bash 로 나눈다.
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${ROOT}/scripts/util.sh"
source "${ROOT}/.env"
bash "${ROOT}/scripts/02_runtime.sh"
$(dirname "$0") 는 호출 방식에 따라 값이 달라진다. ${BASH_SOURCE[0]} 를 쓰고 cd ... && pwd 로 절대경로를 만들어 두면 어디서 실행해도 같은 경로를 본다.
.env 는 기본값이고, 실행할 때 환경변수로 덮어쓸 수 있어야 한다.
: "${K8S_VERSION:=1.31.4}"
: "${POD_CIDR:=10.244.0.0/16}"
HARBOR_DIR="${1:-$(pwd)}"
${1:-$(pwd)} 는 첫 번째 인자가 없으면 현재 디렉터리를 쓴다는 뜻이다. 경로를 인자로 받는 스크립트에서 기본값을 주는 관용적인 형태다.
저장소를 로컬로 돌리거나 RPM 을 미리 모아 둔다. 스크립트 안에서 인터넷 저장소를 직접 붙이는 줄은 전부 걷어 낸다.
필요한 이미지를 미리 받아 tar 로 옮긴 뒤, 로컬 레지스트리에 밀어 넣거나 각 노드에 적재한다.
kubeadm config images list --kubernetes-version "v${K8S_VERSION}"
이 목록을 기준으로 미러링한다. skopeo copy 나 docker save · ctr images import 중 환경에 맞는 것을 쓴다.
containerd 가 사내 레지스트리를 보게 하려면 미러 설정을 둔다.
# /etc/containerd/certs.d/<registry-host>/hosts.toml
server = "https://<registry-host>"
[host."https://<registry-host>"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/<registry-host>/ca.crt"
오프라인 설치 관리자는 tarball 안에 이미지까지 들어 있다. 별도로 받은 이미지를 섞으면 버전이 어긋난다.
tar tvf harbor-offline-installer-v<version>.tgz | head
tar xzf harbor-offline-installer-v<version>.tgz -C /opt
cd /opt/harbor
cp harbor.yml.tmpl harbor.yml
./prepare
./install.sh
install.sh 가 어떤 compose 를 부르는지 먼저 확인한다. 하이픈 형태(docker-compose)를 부르는데 서버에 플러그인 형태만 있으면 설치가 중간에 멈춘다.
이미지를 따로 적재해야 하는 구성이면 경로를 절대경로로 다룬다.
while read -r f; do
docker load -i "${HARBOR_DIR}/images/${f}"
done < "${HARBOR_DIR}/images/list.txt"
설치가 반쯤 되다 만 상태에서 다시 돌리면 이전 잔재 때문에 실패한다. 처음부터 다시 갈 수 있는 정리 스크립트를 같이 둔다.
kubeadm reset -f
systemctl stop kubelet containerd
rm -rf /etc/kubernetes /var/lib/etcd /var/lib/kubelet /etc/cni/net.d "$HOME/.kube"
ip link delete cni0 2>/dev/null || true
ip link delete flannel.1 2>/dev/null || true
iptables -F && iptables -t nat -F && iptables -t mangle -F
CNI 가상 인터페이스와 iptables 규칙이 남아 있으면 다시 설치한 클러스터에서 파드 네트워크가 이상하게 동작한다.
파드 샌드박스 생성이 실패하면 CNI 쪽을 먼저 본다.
Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox
CNI 데몬셋을 재시작하고, 노드에 /etc/cni/net.d 설정 파일이 실제로 생겼는지 확인한다.
${VAR:=default} 형태의 동작.