블로그 불러오는 중...
ㅗ## kubespray offline 설치
graph LR
%% 서비스 영역 정의
subgraph Internet_Zone [🌐 Internet]
Public_Repo[Public Repo/Web Server]
end
subgraph DMZ_Zone [DMZ]
Bastion[Bastion Server]
end
subgraph Internal_Zone [내부망 Internal Network]
Admin[Admin Server]
subgraph K8s_Cluster [☸️ K8s Cluster]
K8s_Nodes[K8s Worker Nodes]
end
end
%% 방화벽 및 연결 설정
FW_Ext((🛡️ 외부 방화벽))
FW_Int((🛡️ 내부 방화벽))
%% 흐름 정의
Public_Repo <--> FW_Ext
FW_Ext <--> Bastion
Bastion <--> FW_Int
FW_Int <--> Admin
Admin <--> K8s_Cluster
%% 스타일링
style FW_Ext fill:#f96,stroke:#333,stroke-width:2px
style FW_Int fill:#f96,stroke:#333,stroke-width:2px
style Bastion fill:#fff4dd,stroke:#d4a017,stroke-width:2px
style Admin fill:#e1f5fe,stroke:#01579b,stroke-width:2px
style K8s_Nodes fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
이런 형태로 보통 기업에서 outbound 인터넷이 되지 않는 환경이 있다. 우리 실습에서는 admin server 가 bastion 서버 역할을 하며 해당 server는 네트워크 통신이 된다.
실습에서는 실제 인터넷이 되는 환경이 아닌 설치에 필요한 것만 따로 받을 수 있게 끔 설치한다.
mkdir k8s-offline
cd k8s-offline
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-kubespary-offline/Vagrantfile
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-kubespary-offline/admin.sh
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-kubespary-offline/init_cfg.sh
vagrant up
vagrant status
# admin, k8s-node1, k8s-node2 각각 접속 : 호스트 OS에 sshpass가 없을 경우 ssh로 root로 접속 후 암호 qwe123 입력
sshpass -p 'qwe123' ssh root@192.168.10.10 # ssh root@192.168.10.10
sshpass -p 'qwe123' ssh root@192.168.10.11 # ssh root@192.168.10.11
sshpass -p 'qwe123' ssh root@192.168.10.12 # ssh root@192.168.10.12실습을 위한 환경을 구축한다.
각 노드에서 실행한다 k8s-node1, k8s-node2
# enp0s8 연결 내리기 : 실행 직후 부터 외부 통신 X
nmcli connection down enp0s8
# enp0s8 확인 : 할당된 IP가 제거되고, 외부 통신 라우팅 정보도 삭제됨
cat /etc/NetworkManager/system-connections/enp0s8.nmconnection
ip addr show enp0s8
2: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 08:00:27:90:ea:eb brd ff:ff:ff:ff:ff:ff
altname enx08002790eaeb
ip route
192.168.10.0/24 dev enp0s9 proto kernel scope link src 192.168.10.11 metric 101
# 재부팅 이후에도 자동 연결 내리기 설정 시
nmcli connection modify enp0s8 connection.autoconnect no
cat /etc/NetworkManager/system-connections/enp0s8.nmconnection
[connection]
id=enp0s8
uuid=7f94e839-e070-4bfe-9330-07090381d89f
type=ethernet
autoconnect=false
...
# 외부 통신을 위해 enp0s9 에 디폴트 라우팅 추가 : 우선순위 200 설정
nmcli connection modify enp0s9 +ipv4.routes "0.0.0.0/0 192.168.10.10 200"
cat /etc/NetworkManager/system-connections/enp0s9.nmconnection
...
[ipv4]
address1=192.168.10.11/24
method=manual
never-default=true # nmcli connection modify enp0s9 ipv4.never-default yes
route1=0.0.0.0/0,192.168.10.10
...
# 설정 적용하기
nmcli connection up enp0s9
ip route
# 외부 통신 확인
ping -w 1 -W 1 8.8.8.8
curl www.google.com
curl: (6) Could not resolve host: www.google.com
# DNS Nameserver 정보 확인
cat /etc/resolv.conf
cat << EOF > /etc/resolv.conf
nameserver 168.126.63.1
nameserver 8.8.8.8
EOF
curl www.google.com인터넷이 차단 된 것을 확인 할 수 있다.
admin 에서 네트워크가 되게 해 본다
iptables -t nat -A POSTROUTING -o enp0s8 -j MASQUERADE
iptables -t nat -S인터넷이 되는 것을 확인하고 다시 제거한다.
iptables -t nat -D POSTROUTING -o enp0s8 -j MASQUERADE
위에서 설명한 offline 시 필요한 repo 들을 직접 구축하는 것은 넘어가고, kubespray 에서 지원하는 것을 사용해 본다. 그런데, kubespray 에서 지원하는 것을 더 편하게 만든 Repo 가 존재한다.
그 전에, 시간과 DNS 셋팅을 진행 해 준다.
admin 노드에 적용한다.
# 현재 ntp 서버와 타임 동기화 설정 및 상태 확인
systemctl status chronyd.service --no-pager
grep "^[^#]" /etc/chrony.conf
pool 2.rocky.pool.ntp.org iburst # 시간 정보를 받아올 외부 서버 주소입니다. 서비스 시작 직후 짧은 간격으로 4~8개의 패킷을 보내 서버와 빠르게 연결(초기 동기화)되도록 돕는 옵션입니다.
sourcedir /run/chrony-dhcp # DHCP 서버로부터 받은 NTP 서버 정보를 저장하는 디렉토리입니다. 네트워크 환경에 따라 유동적으로 서버를 추가할 수 있게 해줍니다.
driftfile /var/lib/chrony/drift # 컴퓨터 내부 시계(수정 진동자)가 실제 시간과 얼마나 오차가 발생하는지(시간 편차) 기록하는 파일입니다. 나중에 네트워크가 끊겨도 이 기록을 바탕으로 오차를 보정합니다.
makestep 1.0 3 # 시스템 부팅 시 시간 차이가 너무 크면 점진적 보정 대신 '강제 점프(Step)'를 수행하라는 설정입니다. 1.0 3은 "시간 차이가 1초 이상일 경우, 초기 3번의 업데이트 내에서 즉시 시간을 맞추라"는 뜻입니다.
rtcsync # 시스템 시계(OS 시간)의 동기화 결과를 하드웨어 시계(메인보드의 RTC)에 주기적으로 복사합니다. 서버를 재부팅해도 시간이 잘 맞게 유지됩니다.
ntsdumpdir /var/lib/chrony # NTS(Network Time Security) 키 정보를 저장하는 위치입니다. (보안 연결용)
logdir /var/log/chrony
# chrony가 어떤 NTP 서버들을 알고 있고, 그중 어떤 서버를 기준으로 시간을 맞추는지를 보여줍니다.
chronyc sources -v
dig +short 2.rocky.pool.ntp.org
# chrony 설정
cp /etc/chrony.conf /etc/chrony.bak
cat << EOF > /etc/chrony.conf
# 외부 한국 공용 NTP 서버 설정
server pool.ntp.org iburst
server kr.pool.ntp.org iburst
# 내부망(192.168.10.0/24)에서 이 서버에 접속하여 시간 동기화 허용
allow 192.168.10.0/24
# 외부망이 끊겼을 때도 로컬 시계를 기준으로 내부망에 시간 제공 (선택 사항)
local stratum 10
# 로그
logdir /var/log/chrony
EOF
systemctl restart chronyd.service
systemctl status chronyd.service --no-pager
# 상태 확인
timedatectl status
chronyc sources -vk8s-node1, k8s-node2 에 각각 적용한다
아래 코드는 시간을 admin 에서 받아오기 위한 설정이다.
# 상태 확인
timedatectl status
chronyc sources -v
# chrony 설정
cp /etc/chrony.conf /etc/chrony.bak
cat << EOF > /etc/chrony.conf
server 192.168.10.10 iburst
logdir /var/log/chrony
EOF
systemctl restart chronyd.service
systemctl status chronyd.service --no-pager
# 상태 확인
timedatectl status
chronyc sources -v
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^* admin 3 6 37 42 +1886ns[ +894us] +/- 4486us
# [admin-lb] 자신의 NTP Server 를 사용하는 클라이언트 확인
chronyc clients
Hostname NTP Drop Int IntL Last Cmd Drop Int Last
===============================================================================
k8s-node1 42 0 10 - 1 0 0 - -
k8s-node2 39 0 11 - 44 0 0 - -
다음과 같이 두가지 노드에 시간서버가 admin 으로 적용 된 것을 볼 수 있다.
admin 에 적용한다
# bind 설치
dnf install -y bind bind-utils
# named.conf 설정
cp /etc/named.conf /etc/named.bak
cat <<EOF > /etc/named.conf
options {
listen-on port 53 { any; };
listen-on-v6 port 53 { ::1; };
directory "/var/named";
dump-file "/var/named/data/cache_dump.db";
statistics-file "/var/named/data/named_stats.txt";
memstatistics-file "/var/named/data/named_mem_stats.txt";
secroots-file "/var/named/data/named.secroots";
recursing-file "/var/named/data/named.recursing";
allow-query { 127.0.0.1; 192.168.10.0/24; };
allow-recursion { 127.0.0.1; 192.168.10.0/24; };
forwarders {
168.126.63.1;
8.8.8.8;
};
recursion yes;
dnssec-validation yes;
managed-keys-directory "/var/named/dynamic";
geoip-directory "/usr/share/GeoIP";
pid-file "/run/named/named.pid";
session-keyfile "/run/named/session.key";
include "/etc/crypto-policies/back-ends/bind.config";
};
logging {
channel default_debug {
file "data/named.run";
severity dynamic;
};
};
zone "." IN {
type hint;
file "named.ca";
};
include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";
EOF
# 문법 오류 확인 (아무 메시지 없으면 정상)
named-checkconf /etc/named.conf
# 서비스 활성화 및 시작
systemctl enable --now named
# DMZ 서버 자체 DNS 설정 (자기 자신 사용)
cat /etc/resolv.conf
echo "nameserver 192.168.10.10" > /etc/resolv.conf
# 확인
dig +short google.com @192.168.10.10
dig +short google.com
k8s-node1, k8s-node2 에 적용한다.
# NetworkManager에서 DNS 관리 끄기
cat /etc/NetworkManager/conf.d/99-dns-none.conf
cat << EOF > /etc/NetworkManager/conf.d/99-dns-none.conf
[main]
dns=none
EOF
systemctl restart NetworkManager
# DNS 서버 정보 설정
nmcli connection modify enp0s8 ipv4.dns "192.168.10.10"
nmcli connection up enp0s8
echo "nameserver 192.168.10.10" > /etc/resolv.conf
# 확인
dig +short google.com @192.168.10.10
dig +short google.com
이 설정 두개 진행하고 나머지는 아래 소스코드 저장소 기반으로 실습하게 된다.
kubespray-offline support scripts - Github 해당 내용으로 실습을 진행해 볼 예정이다.
해당 저장소를 clone 하고, 설치 준비를 한다. 이후 download-all.sh 를 실행시킨다.
# 기본 60G -> 120G 증설 확인
lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 120G 0 disk
├─sda1 8:1 0 600M 0 part /boot/efi
├─sda2 8:2 0 3.8G 0 part [SWAP]
└─sda3 8:3 0 115.6G 0 part /
df -hT /
Filesystem Type Size Used Avail Use% Mounted on
/dev/sda3 xfs 116G 5.5G 111G 5% /
# git clone
git clone https://github.com/kubespray-offline/kubespray-offline
tree kubespray-offline/
cd kubespray-offline/
# 변수 정보 확인 : nginx 와 registry 는 각각 1.29.4 와 3.0.0 변수 선언 확인 <- 버전 변경 시에는 이 단계에서 수정 필요!
source ./config.sh
echo -e "kubespary $KUBESPRAY_VERSION"
echo -e "runc $RUNC_VERSION"
echo -e "containerd $CONTAINERD_VERSION"
echo -e "nercdtl $NERDCTL_VERSION"
echo -e "cni $CNI_VERSION"
echo -e "nginx $NGINX_VERSION"
echo -e "registry $REGISTRY_VERSION"
echo -e "registry_port: $REGISTRY_PORT"
echo -e "Additional container registry hosts: $ADDITIONAL_CONTAINER_REGISTRY_LIST"
echo -e "cpu arch: $IMAGE_ARCH"
# 17분 소요
cat download-all.sh
./download-all.sh
...
(230/230): container-selinux-2.240.0-1.el10.noarch.rpm 4.6 kB/s | 56 kB 00:12
/bin/rm: cannot remove 'outputs/rpms/local/*.i686.rpm': No such file or directory # 32비트 RPM 파일이 애초에 없어서 지울 게 없음. 오류 아니여서 무시해도됨.
==> createrepo
Directory walk started
Directory walk done - 230 packages
Temporary output repo path: outputs/rpms/local/.repodata/
Pool started (with 5 workers)
Pool finished
create-repo done.
=> Running: ./copy-target-scripts.sh
==> Copy target scripts
Done.
# venv 디렉터리 확인
du -sh ~/.venv
490M /root/.venv
tree ~/.venv | more
/root/.venv
└── 3.12
├── bin
│ ├── activate
│ ├── activate.csh
│ ├── activate.fish
│ ├── Activate.ps1
│ ├── ansible
...
# /root/.cache 디렉터리 확인
tree ~/.cache | more
du -sh ~/.cache
814M /root/.cache
# 다운로드 될 파일과 이미지 생성 스크립트는 kubespary repo 에 offline 참고 확인
tree /root/kubespray-offline/cache/kubespray-2.30.0/contrib/offline/
cache/kubespray-2.30.0/contrib/offline/
├── docker-daemon.json
├── generate_list.sh
├── generate_list.yml
├── manage-offline-container-images.sh
├── manage-offline-files.sh
├── nginx.conf
├── README.md
├── registries.conf
├── temp
│ ├── files.list*
│ ├── files.list.template
│ ├── images.list*
│ └── images.list.template
└── upload2artifactory.py
# 용량 확인
du -sh /root/kubespray-offline/outputs/
3.3G outputs/
# 디렉터리/파일 구조 확인
tree /root/kubespray-offline/outputs/ | more
tree /root/kubespray-offline/outputs/ -L 1
outputs/
├── config.sh
├── config.toml
├── containerd.service
├── extract-kubespray.sh
├── files # kubectl/kubelet/kubeadm, containerd 등 바이너리 파일들
├── images # 컨테이너 이미지를 .tar.gz 압축파일
├── install-containerd.sh
├── load-push-all-images.sh
├── nginx-default.conf
├── patches
├── playbook # 노드들에 offline repo 설정을 위한 playbook/role
├── pypi # python 패키지 파일들 - index.html, *.whl, *.tar.gz
├── pyver.sh
├── rpms # rpm 패키지 파일들
├── setup-all.sh
├── setup-container.sh
├── setup-offline.sh
├── setup-py.sh
├── start-nginx.sh
├── start-registry.sh
└── venv.sh
7 directories, 15 filescd /root/kubespray-offline/outputs
ls -l *.sh
./setup-container.sh
# 설치된 바이너리 파일 및 버전 확인
which runc && runc --version
which containerd && containerd --version
which nerdctl && nerdctl --version
tree -ug /opt/cni/bin/
# containerd systemd unit 확인
cat /etc/containerd/config.toml
cat /etc/systemd/system/containerd.service
systemctl status containerd.service --no-pager
# 다운받은 이미지를 압축해제 후 로컬에 load 확인 : CPU Arch = PLATFORM 확인!
nerdctl images
이미지 두개가 업로드 된 것을 볼 수 있다.
그냥 실행해도 되는데, 우리는 그 의존성 들의 파일의 실행 목록을 tree 형태로 보여주는 옵션을 주고 설치 한다.
# (옵션) nginx conf 파일 수정 : 디렉터리 목록 표시되게
cp nginx-default.conf nginx-default.bak
cat << EOF > nginx-default.conf
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
# index index.html index.htm;
autoindex on; # 디렉터리 목록 표시
autoindex_exact_size off; # 파일 크기 KB/MB/GB 단위로 보기 좋게
autoindex_localtime on; # 서버 로컬 타임으로 표시
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# Force sendfile to off
sendfile off;
}
EOF
# Start nginx container.
./start-nginx.sh
# (옵션) 실행 명령 참고
echo "===> Start nginx"
$NERDCTL container run -d \
--network host \
--restart always \
--name nginx \
-v ${BASEDIR}:/usr/share/nginx/html \
-v ${BASEDIR}/nginx-default.conf:/etc/nginx/conf.d/default.conf \
${NGINX_IMAGE} || exit 1
# nginx 컨테이너 확인
nerdctl ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0b71e6989724 docker.io/library/nginx:1.29.4 "/docker-entrypoint.…" 18 seconds ago Up nginx
ss -tnlp | grep nginx
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=19771,fd=6),("nginx",pid=19770,fd=6),("nginx",pid=19769,fd=6),("nginx",pid=19768,fd=6),("nginx",pid=19735,fd=6))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=19771,fd=7),("nginx",pid=19770,fd=7),("nginx",pid=19769,fd=7),("nginx",pid=19768,fd=7),("nginx",pid=19735,fd=7))
# nginx 웹 접속
open http://192.168.10.10설치가 완료되면, 아까 설정한 목록을 트리처럼 보여주는 옵션을 켰기 때문에 다음과 같이 내부 구성요소들을 확인할 수 있다.

dnf 저장소 레포를 변경하고, python 의존성 경로도 변경해 준다.
# 스크립트 실행 전 기본 정보 확인
dnf repolist
repo id repo name
appstream Rocky Linux 10 - AppStream
baseos Rocky Linux 10 - BaseOS
extras Rocky Linux 10 - Extras
...
cat /etc/redhat-release
Rocky Linux release 10.0 (Red Quartz)
# 스크립트 실행 : Setup yum/deb repo config and PyPI mirror config to use local nginx server.
./setup-offline.sh
===> Disable all yumrepositories
===> Setup local yum repository
===> Setup PyPI mirror
# 기존 repo 이름이 .original로 변경되고, offline.repo 추가 확인
tree /etc/yum.repos.d/
/etc/yum.repos.d/
├── offline.repo
├── rocky-addons.repo.original
├── rocky-devel.repo.original
├── rocky-extras.repo.original
└── rocky.repo.original
# offline.repo 파일 확인
cat /etc/yum.repos.d/offline.repo
[offline-repo]
name=Offline repo
baseurl=http://localhost/rpms/local/
enabled=1
gpgcheck=0
# offline repo 확인
dnf clean all
dnf repolist
repo id repo name
offline-repo Offline repo
# pip 전역 설정 : pypi mirror 설정 확인
cat ~/.config/pip/pip.conf
[global]
index = http://localhost/pypi/
index-url = http://localhost/pypi/
trusted-host = localhost./setup-py.sh 실행한다.
# Install python3 and venv from local repo.
## sudo dnf install -y --disablerepo=* --enablerepo=offline-repo python${PY}
./setup-py.sh
===> Install python, venv, etc
Last metadata expiration check: 0:06:40 ago on Wed 04 Feb 2026 12:23:11 AM KST.
Package python3-3.12.12-3.el10_1.aarch64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
# 변수 확인
source pyver.sh
echo -e "python_version $python${PY}"
python_version 3.12
# offline-repo 에 패키지 파일 확인
dnf info python3
tree rpms/local/ | grep -i python
├── libcap-ng-python3-0.8.4-6.el10.aarch64.rpm
├── python3-3.12.12-3.el10_1.aarch64.rpm
...# Start docker private registry container.
./start-registry.sh
===> Start registry
# (옵션) 실행 명령 참고
echo "===> Start registry"
sudo /usr/local/bin/nerdctl run -d \
--network host \
-e REGISTRY_HTTP_ADDR=0.0.0.0:${REGISTRY_PORT} \
--restart always \
--name registry \
-v $REGISTRY_DIR:/var/lib/registry \
$REGISTRY_IMAGE || exit 1
# 관련 변수 확인
source config.sh
echo -e "registry_port: $REGISTRY_PORT"
registry_port: 35000
# 확인
nerdctl ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
778cd497fcaf docker.io/library/registry:3.0.0 "/entrypoint.sh /etc…" About a minute ago Up registry
0b71e6989724 docker.io/library/nginx:1.29.4 "/docker-entrypoint.…" 30 minutes ago Up nginx
ss -tnlp | grep registry
LISTEN 0 4096 *:35000 *:* users:(("registry",pid=20021,fd=7))
LISTEN 0 4096 *:5001 *:* users:(("registry",pid=20021,fd=3))
# 현재는 registry 서버 내부에 저장된 (컨테이너) 이미지 없는 상태 : (참고) REGISTRY_DIR=${REGISTRY_DIR:-/var/lib/registry}
tree /var/lib/registry/
/var/lib/registry/
# tcp 5001 port : debug, metrics
curl 192.168.10.10:5001/metrics
curl 192.168.10.10:5001/debug/pprof/
# 스크립트 실행 전 관련 변수 확인
echo -e "cpu arch: $IMAGE_ARCH"
cpu arch: arm64
## (옵션) 'registry.k8s.io k8s.gcr.io gcr.io ghcr.io docker.io quay.io' 이외에 추가로 필요한 저장소가 있다면 설정
echo -e "Additional container registry hosts: $ADDITIONAL_CONTAINER_REGISTRY_LIST"
Additional container registry hosts: myregistry.io
# nerdctl load 할 .tar.gz 파일들
ls -l images/*.tar.gz
-rw-r--r--. 1 root root 10068768 Feb 3 19:42 images/docker.io_amazon_aws-alb-ingress-controller-v1.1.9.tar.gz
-rw-r--r--. 1 root root 175407403 Feb 3 19:44 images/docker.io_amazon_aws-ebs-csi-driver-v0.5.0.tar.gz
-rw-r--r--. 1 root root 95707259 Feb 3 19:40 images/docker.io_cloudnativelabs_kube-router-v2.1.1.tar.gz
...
# Load all container images to containerd. Tag and push them to the private registry.
./load-push-all-images.sh
# FATA[0003] image might be filtered out (Hint: set `--platform=PLATFORM` or `--all-platforms`)
# aarch64) PLATFORM="linux/arm64" vs x86_64) PLATFORM="linux/amd64"
# (TS) mac사용자: 아래 내용 추가 후 스크립트 다시 실행
vi load-push-all-images.sh
# 아래 추가 -----------------------
...
load_images() {
for image in $BASEDIR/images/*.tar.gz; do
echo "===> Loading $image"
sudo $NERDCTL load --all-platforms -i $image || exit 1
done
...
push_images() {
...
echo "===> Push ${newImage}"
sudo $NERDCTL push --platform=linux/arm64 ${newImage} || exit 1 # 왼쪽 추가 안해도 됨
done
# -------------------------------
# 다시 실행 후 성공! : 2분 소요
./load-push-all-images.sh
# 로컬 이미지 load 확인
nerdctl images
REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
localhost:35000/kube-proxy v1.34.3 fa5ed2c96dd3 46 seconds ago linux/arm64 78.05MB 75.94MB
localhost:35000/kube-scheduler v1.34.3 985575f183de 46 seconds ago linux/arm64 53.34MB 51.59MB
localhost:35000/kube-controller-manager v1.34.3 354700b61969 47 seconds ago linux/arm64 74.38MB 72.62MB
localhost:35000/kube-apiserver v1.34.3 dece5cf2dd3b 47 seconds ago linux/arm64 86.56MB 84.81MB
...
localhost:35000/flannel/flannel-cni-plugin v1.7.1-flannel1 332db17b4c4a About a minute ago linux/arm64 11.39MB 11.37MB
localhost:35000/flannel/flannel v0.27.3 3b36a8d4db19 About a minute ago linux/arm64 102.6MB 101.5MB
...
registry.k8s.io/pause 3.10.1 3f85f9d8a6bc About a minute ago linux/arm64 516.1kB 516.9kB
registry.k8s.io/metrics-server/metrics-server v0.8.0 87ccea7af925 About a minute ago linux/arm64 82.58MB 80.84MB
registry.k8s.io/kube-scheduler v1.34.3 985575f183de About a minute ago linux/arm64 53.34MB 51.59MB
registry.k8s.io/kube-proxy v1.34.3 fa5ed2c96dd3 About a minute ago linux/arm64 78.05MB 75.94MB
registry.k8s.io/kube-controller-manager v1.34.3 354700b61969 About a minute ago linux/arm64 74.38MB 72.62MB
registry.k8s.io/kube-apiserver v1.34.3 dece5cf2dd3b About a minute ago linux/arm64 86.56MB 84.81MB
registry.k8s.io/ingress-nginx/controller v1.13.3 68a587e5104f About a minute ago linux/arm64 336.3MB 334.2MB
registry.k8s.io/dns/k8s-dns-node-cache 1.25.0 7071feee8b70 About a minute ago linux/arm64 90.54MB 88.43MB
registry.k8s.io/cpa/cluster-proportional-autoscaler v1.8.8 4146047e636f About a minute ago linux/arm64 39.98MB 37.86MB
registry.k8s.io/coredns/coredns v1.12.1 e674cf21adf3 About a minute ago linux/arm64 74.94MB 73.19MB
...
quay.io/metallb/speaker v0.13.9 51f18d4f5d4d About a minute ago linux/arm64 111.1MB 111.1MB
quay.io/metallb/controller v0.13.9 b724b69a4c9b About a minute ago linux/arm64 63.12MB 63.11MB
quay.io/jetstack/cert-manager-webhook v1.15.3 2d91656807bb About a minute ago linux/arm64 58.15MB 56.39MB
quay.io/jetstack/cert-manager-controller v1.15.3 5114bfbeac23 About a minute ago linux/arm64 67.13MB 65.37MB
quay.io/jetstack/cert-manager-cainjector v1.15.3 a13418dc926e About a minute ago linux/arm64 44.65MB 42.89MB
quay.io/coreos/etcd v3.5.26 4b003fe9069c About a minute ago linux/arm64 66.06MB 63.34MB
...
rancher/local-path-provisioner v0.0.32 4a3d51575c84 2 minutes ago linux/arm64 61.37MB 61.35MB
mirantis/k8s-netchecker-server v1.2.2 8e0ef348cf54 2 minutes ago linux/amd64 125.8MB 123.7MB
mirantis/k8s-netchecker-agent v1.2.2 e07c83f8f083 2 minutes ago linux/amd64 5.681MB 5.856MB
flannel/flannel v0.27.3 3b36a8d4db19 2 minutes ago linux/arm64 102.6MB 101.5MB
flannel/flannel-cni-plugin v1.7.1-flannel1 332db17b4c4a 2 minutes ago linux/arm64 11.39MB 11.37MB
...
# (참고) kube-proxy 컨테이너가 localhost:35000 과 registry.k8s.io 로 push 된 상태 확인
nerdctl images | grep -i kube-proxy
localhost:35000/kube-proxy v1.34.3 fa5ed2c96dd3 24 minutes ago linux/arm64 78.05MB 75.94MB
registry.k8s.io/kube-proxy v1.34.3 fa5ed2c96dd3 24 minutes ago linux/arm64 78.05MB 75.94MB
# localhost 있는 이미지가 각각 registry, quay, rancher, flannel 등으로 push 된것을 알 수 있다
nerdctl images | grep localhost
nerdctl images | grep localhost | wc -l
55
nerdctl images | grep -v localhost
nerdctl images | grep -v localhost | wc -l
56
# 이미지 저장소 카탈로그 확인
curl -s http://localhost:35000/v2/_catalog | jq
{
"repositories": [
"amazon/aws-alb-ingress-controller",
"amazon/aws-ebs-csi-driver",
...
# kube-apiserver 정보 확인
curl -s http://localhost:35000/v2/kube-apiserver/tags
curl -s http://localhost:35000/v2/kube-apiserver/tags/list | jq
{
"name": "kube-apiserver",
"tags": [
"v1.34.3"
]
}
## Image Manifest
curl -s http://localhost:35000/v2/kube-apiserver/manifests/v1.34.3 | jq
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:cf65ae6c8f700cc27f57b7305c6e2b71276a7eed943c559a0091e1e667169896",
"size": 2906
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar",
"digest": "sha256:378b3db0974f7a5a8767b6329ad310983bc712d0e400ff5faa294f95f869cc8c",
"size": 327680
},
...(생략)
# 이미지 저장소의 저장 디렉터리 확인
tree /var/lib/registry/ -L 5
/var/lib/registry/
└── docker
└── registry
└── v2
├── blobs
│ └── sha256
└── repositories
├── amazon
├── calico
├── cilium
├── cloudnativelabs
├── coredns
├── coreos
├── cpa
├── dns
├── flannel
├── ingress-nginx
├── jetstack
├── k8snetworkplumbingwg
├── kube-apiserver
├── kube-controller-manager
├── kubeovn
├── kube-proxy
├── kubernetesui
├── kube-scheduler
├── kube-vip
├── library
├── metallb
├── metrics-server
├── mirantis
├── pause
├── provider-os
├── rancher
└── sig-storage# 스크립트 실행 전 관련 파일 확인 : kubespary repo 압축된 파일
ls -lh files/kubespray-*
-rw-r--r--. 1 root root 2.5M Feb 3 19:30 files/kubespray-2.30.0.tar.gz
# patches 파일 : kubespary-2.18.0 버전에서 patch 되는 내용으로, kubespary-2.30.0과 관계없음
tree patches/
└── 2.18.0
├── 0001-nerdctl-insecure-registry-config-8339.patch
├── 0002-Update-config.toml.j2-8340.patch
└── 0003-generate-list-8537.patch
# Extract kubespray tarball and apply all patches.
./extract-kubespray.sh
# kubespary 저장소 압축해제된 파일들 확인
tree kubespray-2.30.0/ -L 1
kubespray-2.30.0/
├── ansible.cfg
├── CHANGELOG.md
├── cluster.yml
├── CNAME
├── code-of-conduct.md
├── _config.yml
├── contrib
├── CONTRIBUTING.md
├── Dockerfile
├── docs
├── extra_playbooks
├── galaxy.yml
├── index.html
├── inventory
├── library
├── LICENSE
├── logo
├── meta
├── OWNERS
├── OWNERS_ALIASES
├── pipeline.Dockerfile
├── playbooks
├── plugins
├── README.md
├── recover-control-plane.yml
├── RELEASE.md
├── remove-node.yml
├── remove_node.yml
├── requirements.txt
├── reset.yml
├── roles
├── scale.yml
├── scripts
├── SECURITY_CONTACTS
├── test-infra
├── tests
├── upgrade-cluster.yml
├── upgrade_cluster.yml
└── Vagrantfile
14 directories, 26 files위의 과정이 다 끝나면 이제 실제로 노드에 kubespray 로 설치를 한다
중간에 진행하다 보면 mac 기반의 pc 에서는 에러가 발생한다.
sed -i 's/amd64/arm64/g' inventory/mycluster/group_vars/all/offline.yml
명령어로 변경해 준다. 아래 해당 명령어 내용과 확인하는 내용도 첨부 되어 있으나 별첨으로 남긴다.
# check python version
python --version
Python 3.12.12
# venv 실행
python3.12 -m venv ~/.venv/3.12
source ~/.venv/3.12/bin/activate
which ansible
tree ~/.venv/3.12/ -L 4
/root/.venv/3.12/
├── bin
│ ├── activate
│ ├── activate.csh
│ ├── activate.fish
│ ├── Activate.ps1
│ ├── ansible
...
# kubespary 디렉터리 이동
cd /root/kubespray-offline/outputs/kubespray-2.30.0
# Install ansible : 이미 설치 완료된 상태
pip install -U pip # update pip
pip install -r requirements.txt # Install ansible
# offline.yml 파일 복사 후 inventory 복사
cp ../../offline.yml .
cp -r inventory/sample inventory/mycluster
tree inventory/mycluster/
# 웹서버와 이미지 저장소 정보 수정 : http_server, registry_host
cat offline.yml
sed -i "s/YOUR_HOST/192.168.10.10/g" offline.yml
cat offline.yml | grep 192.168.10.10
http_server: "http://192.168.10.10"
registry_host: "192.168.10.10:35000"
# 수정 반영된 offline.yml 파일을 inventory 디렉터리 내부로 복사
\cp -f offline.yml inventory/mycluster/group_vars/all/offline.yml
cat inventory/mycluster/group_vars/all/offline.yml
# inventory 파일 작성
cat <<EOF > inventory/mycluster/inventory.ini
[kube_control_plane]
k8s-node1 ansible_host=192.168.10.11 ip=192.168.10.11 etcd_member_name=etcd1
[etcd:children]
kube_control_plane
[kube_node]
k8s-node2 ansible_host=192.168.10.12 ip=192.168.10.12
EOF
cat inventory/mycluster/inventory.ini
# ansible 연결 확인
ansible -i inventory/mycluster/inventory.ini all -m ping
# 각 노드에 offline repo 설정
tree ../playbook/
├── offline-repo.yml
└── roles
└── offline-repo
├── defaults
│ └── main.yml
├── files
│ └── 99offline
└── tasks
├── Debian.yml
├── main.yml
└── RedHat.yml
mkdir offline-repo
cp -r ../playbook/ offline-repo/
tree offline-repo/
ansible-playbook -i inventory/mycluster/inventory.ini offline-repo/playbook/offline-repo.yml
# k8s-node 확인
ssh k8s-node1 tree /etc/yum.repos.d/
ssh k8s-node1 dnf repolist
repo id repo name
appstream Rocky Linux 10 - AppStream
baseos Rocky Linux 10 - BaseOS
extras Rocky Linux 10 - Extras
offline-repo Offline repo for kubespray
ssh k8s-node1 cat /etc/yum.repos.d/offline.repo
[offline-repo]
baseurl = http://192.168.10.10/rpms/local
enabled = 1
gpgcheck = 0
name = Offline repo for kubespray
## 추가로 설치를 위해 기존 repo 제거 : 미실행할 경우, kubespary 실행 시 fail됨
for i in rocky-addons rocky-devel rocky-extras rocky; do
ssh k8s-node1 "mv /etc/yum.repos.d/$i.repo /etc/yum.repos.d/$i.repo.original"
ssh k8s-node2 "mv /etc/yum.repos.d/$i.repo /etc/yum.repos.d/$i.repo.original"
done
ssh k8s-node1 tree /etc/yum.repos.d/
ssh k8s-node1 dnf repolist
ssh k8s-node2 tree /etc/yum.repos.d/
ssh k8s-node2 dnf repolist
# admin-lb 에 kubectl 없는 것 확인
which kubectl
# group vars 실습 환경에 맞게 설정
echo "kubectl_localhost: true" >> inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml # 배포를 수행하는 로컬 머신의 bin 디렉토리에도 kubectl 바이너리를 다운로드
sed -i 's|kube_owner: kube|kube_owner: root|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
sed -i 's|kube_network_plugin: calico|kube_network_plugin: flannel|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
sed -i 's|kube_proxy_mode: ipvs|kube_proxy_mode: iptables|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
sed -i 's|enable_nodelocaldns: true|enable_nodelocaldns: false|g' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
grep -iE 'kube_owner|kube_network_plugin:|kube_proxy_mode|enable_nodelocaldns:' inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
echo "enable_dns_autoscaler: false" >> inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
echo "flannel_interface: enp0s9" >> inventory/mycluster/group_vars/k8s_cluster/k8s-net-flannel.yml
grep "^[^#]" inventory/mycluster/group_vars/k8s_cluster/k8s-net-flannel.yml
sed -i 's|helm_enabled: false|helm_enabled: true|g' inventory/mycluster/group_vars/k8s_cluster/addons.yml
sed -i 's|metrics_server_enabled: false|metrics_server_enabled: true|g' inventory/mycluster/group_vars/k8s_cluster/addons.yml
grep -iE 'metrics_server_enabled:' inventory/mycluster/group_vars/k8s_cluster/addons.yml
echo "metrics_server_requests_cpu: 25m" >> inventory/mycluster/group_vars/k8s_cluster/addons.yml
echo "metrics_server_requests_memory: 16Mi" >> inventory/mycluster/group_vars/k8s_cluster/addons.yml
# 지원 버전 정보 확인
cat roles/kubespray_defaults/vars/main/checksums.yml | grep -i kube -A40
# [macOS 사용자] (TS) 이슈 해결
# -----------------------------------------------
TASK [download : Download_file | Download item] **************************************************************************
fatal: [k8s-node1]: FAILED! => {"attempts": 4, "changed": false, "dest": "/tmp/releases/etcd-3.5.26-linux-arm64.tar.gz", "elapsed": 0, "msg": "Request failed", "response": "HTTP Error 404: Not Found", "status_code": 404, "url": "http://192.168.10.10/files/kubernetes/etcd/etcd-v3.5.26-linux-amd64.tar.gz"}
http://192.168.10.10/files/kubernetes/etcd/etcd-v3.5.26-linux-amd64.tar.gz
# vi roles/download/tasks/download_file.yml >> no_log: false # (참고) 실패 task 상세 로그 출력 설정하여 원인 파악
cat inventory/mycluster/group_vars/all/offline.yml | grep amd64
etcd_download_url: "{{ files_repo }}/kubernetes/etcd/etcd-v{{ etcd_version }}-linux-amd64.tar.gz"
sed -i 's/amd64/arm64/g' inventory/mycluster/group_vars/all/offline.yml
# -----------------------------------------------
# 배포 : 설치 완료까지 3분!
ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml -e kube_version="1.34.3"
# 설치 후 NetworkManger 에 dns 설정 파일 추가 확인
ssh k8s-node2 cat /etc/NetworkManager/conf.d/dns.conf
[global-dns-domain-*]
servers = 10.233.0.3,192.168.10.10
[global-dns]
searches = default.svc.cluster.local,svc.cluster.local
options = ndots:2,timeout:2,attempts:2
# 하지만 '/etc/NetworkManager/conf.d/99-dns-none.conf' 파일로 인해, 위 설정이 resolv.conf 에 반영되지 않음, 즉 노드에서는 service명으로 도메인 질의는 불가능.
ssh k8s-node2 cat /etc/resolv.conf
nameserver 192.168.10.10
# 설치 후 NetworkManger 에서 특정 NIC은 관리하지 않게 설정 추가 확인
ssh k8s-node2 cat /etc/NetworkManager/conf.d/k8s.conf
[keyfile]
unmanaged-devices+=interface-name:kube-ipvs0;interface-name:nodelocaldns
# kubectl 바이너리 파일을 ansible-playbook 실행한 서버에 다운로드 확인
file inventory/mycluster/artifacts/kubectl
ls -l inventory/mycluster/artifacts/kubectl
tree inventory/mycluster/
inventory/mycluster/
├── artifacts
│ └── kubectl
├── credentials
│ └── kubeadm_certificate_key.creds
├── group_vars
...
cp inventory/mycluster/artifacts/kubectl /usr/local/bin/
kubectl version --client=true
Client Version: v1.34.3
Kustomize Version: v5.7.1
# k8s admin 자격증명 확인
mkdir /root/.kube
scp k8s-node1:/root/.kube/config /root/.kube/
sed -i 's/127.0.0.1/192.168.10.11/g' /root/.kube/config
k9s
# 자동완성 및 단축키 설정
source <(kubectl completion bash)
alias k=kubectl
complete -F __start_kubectl k
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'alias k=kubectl' >> /etc/profile
echo 'complete -F __start_kubectl k' >> /etc/profile
# 이미지 저장소가 192.168.10.10:35000 임을 확인
kubectl get deploy,sts,ds -n kube-system -owide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/coredns 2/2 2 2 4m9s coredns 192.168.10.10:35000/coredns/coredns:v1.12.1 k8s-app=kube-dns
deployment.apps/metrics-server 1/1 1 1 4m5s metrics-server 192.168.10.10:35000/metrics-server/metrics-server:v0.8.0 app.kubernetes.io/name=metrics-server,version=0.8.0
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR
daemonset.apps/kube-flannel 0 0 0 0 0 <none> 4m25s kube-flannel 192.168.10.10:35000/flannel/flannel:v0.27.3 app=flannel
...
daemonset.apps/kube-proxy 1 1 1 1 1 kubernetes.io/os=linux 4m43s kube-proxy 192.168.10.10:35000/kube-proxy:v1.34.3 k8s-app=kube-proxy[k8s-node1 , k8s-node2] 에서 확인 해 본다. 이미지 주소가 192.168.10.10 으로 되어 있는 것 정도 확인해 본다.
# 로컬 이미지 확인
crictl images
IMAGE TAG IMAGE ID SIZE
192.168.10.10:35000/coredns/coredns v1.12.1 138784d87c9c5 73.2MB
192.168.10.10:35000/flannel/flannel-cni-plugin v1.7.1-flannel1 e5bf9679ea8c3 11.4MB
192.168.10.10:35000/flannel/flannel v0.27.3 cadcae92e6360 102MB
192.168.10.10:35000/kube-apiserver v1.34.3 cf65ae6c8f700 84.8MB
192.168.10.10:35000/kube-controller-manager v1.34.3 7ada8ff13e54b 72.6MB
192.168.10.10:35000/kube-proxy v1.34.3 4461daf6b6af8 75.9MB
192.168.10.10:35000/kube-scheduler v1.34.3 2f2aa21d34d2d 51.6MB
192.168.10.10:35000/library/nginx 1.28.0-alpine 5a91d90f47ddf 51.2MB
192.168.10.10:35000/metrics-server/metrics-server v0.8.0 bc6c1e09a843d 80.8MB
192.168.10.10:35000/pause 3.10.1 d7b100cd9a77b 517kB
# containerd 정보 확인
tree /etc/containerd/
/etc/containerd/
├── certs.d
│ └── 192.168.10.10:35000
│ └── hosts.toml
├── config.toml
└── cri-base.json
cat /etc/containerd/config.toml
...
[plugins."io.containerd.cri.v1.images".pinned_images]
sandbox = "192.168.10.10:35000/pause:3.10.1"
[plugins."io.containerd.cri.v1.images".registry]
config_path = "/etc/containerd/certs.d"
....
cat /etc/containerd/certs.d/192.168.10.10\:35000/hosts.toml
server = "https://192.168.10.10:35000"
[host."http://192.168.10.10:35000"]
capabilities = ["pull","resolve"]
skip_verify = true
override_path = falsekubespray 에서 offline 폴더를 확인한다. 이 스크립트는 맨 처음에 download-all.sh 에서 실행될 때 해당 부분들이 실행이 된다.
#
cd /root/kubespray-offline/outputs/kubespray-2.30.0/contrib/offline
tree
├── docker-daemon.json
├── generate_list.sh
├── generate_list.yml
├── manage-offline-container-images.sh
├── manage-offline-files.sh
├── nginx.conf
├── README.md
├── registries.conf
└── upload2artifactory.py
# 다운받을 파일과 (컨테이너)이미지 목록 파일로 작성 스크립트(ansible generate_list.yml 실행 포함) 실행
# https://github.com/kubernetes-sigs/kubespray/blob/master/roles/kubespray_defaults/defaults/main/download.yml
cat ../../roles/kubespray_defaults/defaults/main/download.yml # 다운로드될 파일 목록과 컨테이너 이미지 목록 참고 파일
./generate_list.sh
PLAY [Collect container images for offline deployment] ****************************************************************************
TASK [Collect container images for offline deployment] ****************************************************************************
...
# temp 디렉터리 확인
tree temp
├── files.list
├── files.list.template
├── images.list
└── images.list.template
# 파일 목록 확인
cat temp/files.list.template
{{ dl_k8s_io_url }}/release/v{{ kube_version }}/bin/linux/{{ image_arch }}/kubelet
{{ dl_k8s_io_url }}/release/v{{ kube_version }}/bin/linux/{{ image_arch }}/kubectl
{{ dl_k8s_io_url }}/release/v{{ kube_version }}/bin/linux/{{ image_arch }}/kubeadm
...
cat temp/files.list | tee 1-files.list
https://dl.k8s.io/release/v1.34.3/bin/linux/arm64/kubelet
https://dl.k8s.io/release/v1.34.3/bin/linux/arm64/kubectl
https://dl.k8s.io/release/v1.34.3/bin/linux/arm64/kubeadm
...
# (컨테이너)이미지 목록 확인
cat temp/images.list | tee 1-images.list
tail temp/images.list -n 4 | tee 1-images.list
registry.k8s.io/kube-apiserver:v1.34.3
registry.k8s.io/kube-controller-manager:v1.34.3
registry.k8s.io/kube-scheduler:v1.34.3
registry.k8s.io/kube-proxy:v1.34.3
cat temp/images.list.template
tail temp/images.list.template -n 4
{{ kube_image_repo }}/kube-apiserver:v{{ kube_version }}
{{ kube_image_repo }}/kube-controller-manager:v{{ kube_version }}
{{ kube_image_repo }}/kube-scheduler:v{{ kube_version }}
{{ kube_image_repo }}/kube-proxy:v{{ kube_version }}
# 목록 정보 수정 필요 시: 필요한 변수명 확인 후 설정하여 반영
## cat temp/files.list.template
## cat temp/images.list.template
# cat ../../roles/kubespray_defaults/vars/main/checksums.yml | grep -i kube -A40
# arm64
# 다시 목록 생성 : ansible-playbook 실행이 되니, 스크립트 실행 시 -e 변수 지정 가능
./generate_list.sh -e kube_version=1.33.7 -e image_arch=amd64 # Windows 사용자분들은 arm64 사용할 것!
cat temp/files.list | tee 2-files.list
cat temp/images.list | tee 2-images.list
# 변경 비교
diff 1-files.list 2-files.list
vi -d 1-files.list 2-files.list
diff 1-images.list 2-images.list
vi -d 1-images.list 2-images.list
rm -rf *.list해당 코드로 다운 받을 버전들을 매핑이 된다.
폐쇄망 내부에서 필요한 구성 요소들을 한번 더 알아본다.
예시로 nginx 를 배포하는 실습인데, 폐쇄망이기 때문에 container registry 에 접속하지 못하고 pod 가 pull 을 받지 못한다.
# [k8s-node] 외부 통신은 안되는 상태
ping -c 1 -w 1 -W 1 8.8.8.8
ip route
crictl images
tree /etc/containerd/certs.d/
/etc/containerd/certs.d/
└── 192.168.10.10:35000
└── hosts.toml
# [admin] nginx 디플로이먼트 배포 시도
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine # docker.io/library/nginx:alpine
ports:
- containerPort: 80
EOF
# 확인 : docker.io/library/nginx:alpine 이미지 가져오기 실패!
kubectl describe pod
...
Warning Failed 8s kubelet Failed to pull image "nginx:alpine": failed to pull and unpack image "docker.io/library/nginx:alpine": failed to resolve image: failed to do request: Head "https://registry-1.docker.io/v2/library/nginx/manifests/alpine": dial tcp [2600:1f18:2148:bc01:c6a5:489e:7233:242c]:443: connect: no route to host
...# [admin] (컨테이너) 이미지 저장소에 이미지 push
podman images
# 로컬에 nginx:alpine 다운로드
podman pull nginx:alpine
docker.io/library/nginx:alpine 선택
podman images | grep nginx
# (컨테이너) 이미지 저장소에 이미지 push
podman tag nginx:alpine 192.168.10.10:35000/library/nginx:alpine
podman images | grep nginx
# 기본적으로 컨테이너 엔진들은 HTTPS를 요구합니다. 내부망에서 HTTP로 테스트하려면 Registry 주소를 '안전하지 않은 저장소'로 등록해야 합니다.
# (참고) registries.conf 는 containers-common 설정이라서, 'podman, skopeo, buildah' 등 전부 동일하게 적용됨.
cat <<EOF >> /etc/containers/registries.conf
[[registry]]
location = "192.168.10.10:35000"
insecure = true
EOF
grep "^[^#]" /etc/containers/registries.conf
# 프라이빗 레지스트리에 업로드 : 성공!
podman push 192.168.10.10:35000/library/nginx:alpine
# 업로드된 이미지와 태그 조회
curl -s 192.168.10.10:35000/v2/_catalog | jq
{
"repositories": [
...
"library/nginx",
curl -s 192.168.10.10:35000/v2/library/nginx/tags/list | jq
{
"name": "library/nginx",
"tags": [
"1.28.0-alpine",
"1.29.4",
"alpine"
]
}디플로이 이름을 변경했기 때문에(내부망에 있는 이미지)로 잘 가져와 지는 걸 확인 할 수 있다.
그러나 이제 docker 이미지 그 자체를 가지고 오게끔 처리 해 본다(base url) 변경한다
kubectl delete deployments.apps nginx
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine # docker.io/library/nginx:alpine
ports:
- containerPort: 80
EOF
# 현재 파드 상태
kubectl get pod일단은 이렇게 했을 때는 당연히 안 올라온다.
node1, node2 에 각각 설정 변경한다
# docker.io 대신 내부 이미지 레지스트리 주소 설정
mkdir -p /etc/containerd/certs.d/docker.io
cat <<EOF > /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://docker.io" # [HTTPS] 원본 registry 주소 , docker.io 를 대상으로 할 때 이 설정을 참조한다는 의미
[host."http://192.168.10.10:35000"] # [HTTP] 내부 레지스트리를 미러로 지정 , docker.io 대신 실제 이미지를 가져올 내부 레지스트리 주소
capabilities = ["pull", "resolve"] # "pull": 이미지 다운로드 허용 , "resolve": 태그 → 다이제스트 해석
skip_verify = true # HTTPS 인증서 검증 스킵 (HTTP라서 사실상 의미 없는지 테스트 해보자)
EOF
systemctl restart containerd
# 이미지 가져오기 실행 후 확인 : k8s-nodes는 현재 외부 통신 불능 상태인데, 아래 처럼 docker.io 미러 설정되어서 가져오는 것을 확인!
nerdctl pull docker.io/library/nginx:alpine
crictl images | grep nginx
192.168.10.10:35000/library/nginx alpine aea88c29b151e 25.7MB
docker.io/library/nginx alpine aea88c29b151e 25.7MB
# 현재 파드 상태
kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-54fc99c8d-8jpqt 1/1 Running 0 12m정상적으로 동작한다. docker.io 의 주소를 192.168.10.10:35000 으로 변경해 잘 돌아가게 된다.
이걸 ansible 에서도 설정할 수 있다.
[admin]
# containerd registry 정보 확인
cat /root/kubespray-offline/outputs/kubespray-2.30.0/inventory/mycluster/group_vars/all/offline.yml | head -n 15
...
http_server: "http://192.168.10.10"
registry_host: "192.168.10.10:35000"
# Insecure registries for containerd
containerd_registries_mirrors:
- prefix: "{{ registry_host }}"
mirrors:
- host: "http://{{ registry_host }}"
capabilities: ["pull", "resolve"]
skip_verify: true
# 수정
nano inventory/mycluster/group_vars/all/offline.yml
-------------------------------------------------
# Insecure registries for containerd
containerd_registries_mirrors:
- prefix: "{{ registry_host }}"
mirrors:
- host: "http://{{ registry_host }}"
capabilities: ["pull", "resolve"]
skip_verify: true
- prefix: "docker.io"
mirrors:
- host: "http://192.168.10.10:35000"
capabilities: ["pull", "resolve"]
skip_verify: false
- prefix: "registry-1.docker.io"
mirrors:
- host: "http://192.168.10.10:35000"
capabilities: ["pull", "resolve"]
skip_verify: false
- prefix: "quay.io"
mirrors:
- host: "http://192.168.10.10:35000"
capabilities: ["pull", "resolve"]
skip_verify: false
-------------------------------------------------
cat inventory/mycluster/group_vars/all/offline.yml | head -n 30
# 설정 업데이트
ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml -e kube_version="1.34.3" --tags containerd
# 확인
ssh k8s-node2 tree /etc/containerd
/etc/containerd
├── certs.d
│ ├── 192.168.10.10:35000
│ │ └── hosts.toml
│ ├── docker.io
│ │ └── hosts.toml
│ ├── quay.io
│ │ └── hosts.toml
│ └── registry-1.docker.io
│ └── hosts.toml
├── config.toml
└── cri-base.json
ssh k8s-node2 cat /etc/containerd/certs.d/quay.io/hosts.toml
server = "https://quay.io"
[host."http://192.168.10.10:35000"]
capabilities = ["pull","resolve"]
skip_verify = false
override_path = false헬름 차트 저장소도 바꿔 본다 헬름 차트의 경우 OCI 방식이 있다고 한다.
| 구분 | 헬름 레포 (Classic) | OCI 방식 (Modern) |
|---|---|---|
| 저장소 | 웹 서버, GitHub Pages, S3 등 | 도커 레지스트리 (ECR, GAR, Harbor 등) |
| 색인 파일 | index.yaml 필수 | 없음 (레지스트리 자체 관리) |
| 프로토콜 | http://, https:// | oci:// |
| 연결 방식 | helm repo add 필요 | helm registry login 필요 (등록 불필요) |
| 검색 | helm search repo 지원 | 지원하지 않음 (UI 사용) |
| 속도 | 차트가 많아지면 느려짐 | 항상 빠름 |
다음과 같은 차이점이 있다고 한다.
일단 admin 에서 이미지를 저장 후 push 한다
podman images
# 로컬에 nginx:alpine 다운로드
podman pull nginx:1.28.0-alpine
podman images | grep nginx
# (컨테이너) 이미지 저장소에 이미지 push
podman tag nginx:1.28.0-alpine 192.168.10.10:35000/library/nginx:1.28.0-alpine
podman images | grep nginx헬름차트를 작성해 본다
# 디렉터리 생성
cd
mkdir nginx-chart
cd nginx-chart
mkdir templates
cat > templates/configmap.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}
data:
index.html: |
{{ .Values.indexHtml | indent 4 }}
EOF
cat > templates/deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: nginx
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
ports:
- containerPort: 80
volumeMounts:
- name: index-html
mountPath: /usr/share/nginx/html/index.html
subPath: index.html
volumes:
- name: index-html
configMap:
name: {{ .Release.Name }}
EOF
cat > templates/service.yaml <<EOF
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
spec:
selector:
app: {{ .Release.Name }}
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30000
type: NodePort
EOF
cat > values.yaml <<EOF
indexHtml: |
<!DOCTYPE html>
<html>
<head>
<title>Welcome to Nginx!</title>
</head>
<body>
<h1>Hello, Kubernetes!</h1>
<p>Nginx version 1.28.0 - alpine</p>
</body>
</html>
image:
repository: nginx
tag: 1.28.0-alpine
replicaCount: 1
EOF
cat > Chart.yaml <<EOF
apiVersion: v2
name: nginx-chart
description: A Helm chart for deploying Nginx with custom index.html
type: application
version: 1.0.0
appVersion: "1.28.0-alpine"
EOF
tree
├── Chart.yaml
├── templates
│ ├── configmap.yaml
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml패키징 해본다
# 적용 전 렌더링 확인 Render chart templates locally and display the output.
helm template dev-nginx . -f values.yaml
# 직접 배포 해보기
helm install dev-nginx . -f values.yaml
helm list
kubectl get deploy,svc,ep,cm dev-nginx -owide
# 호출 확인
curl http://192.168.10.11:30000
curl -s http://192.168.10.11:30000 | grep version
open http://192.168.10.11:30000
# 차트를 .tgz 파일로 패키징
helm package .
Successfully packaged chart and saved it to: /root/nginx-chart/nginx-chart-1.0.0.tgz
tar -tzf nginx-chart-1.0.0.tgz
nginx-chart/Chart.yaml
nginx-chart/values.yaml
nginx-chart/templates/configmap.yaml
nginx-chart/templates/deployment.yaml
nginx-chart/templates/service.yaml
zcat nginx-chart-1.0.0.tgz | tar -xOf - nginx-chart/Chart.yaml
zcat nginx-chart-1.0.0.tgz | tar -xOf - nginx-chart/values.yaml
# 다음 실습을 위해 삭제
helm uninstall dev-nginx
helm list
kubectl get deploy,svc,ep,cm dev-nginx -owide# 사전 준비 : 로컬에 nginx:alpine 다운로드 후 로컬 저장소에 push
podman pull docker.io/bitnami/nginx:latest
podman tag bitnami/nginx:latest 192.168.10.10:35000/bitnami/nginx:latest
podman push 192.168.10.10:35000/bitnami/nginx:latest
# (참고) helm repo 확인 : 이번 실습에서는 해당 방식 미사용
helm repo list
# Bitnami nginx chart OCI Registry 주소 oci://registry-1.docker.io/bitnamicharts/nginx
# (참고) helm show 명령
helm show readme oci://registry-1.docker.io/bitnamicharts/nginx
helm show values oci://registry-1.docker.io/bitnamicharts/nginx
helm show chart oci://registry-1.docker.io/bitnamicharts/nginx
# helm chart 가져오기 : OCI Registry 사용
cd
mkdir nginx-oci-reg && cd nginx-oci-reg
helm pull oci://registry-1.docker.io/bitnamicharts/nginx --version 22.4.7
# 파일 목록 확인
tar -tzf nginx-22.4.7.tgz
nginx/Chart.yaml
nginx/README.md
nginx/values.schema.json
nginx/values.yaml
...
zcat nginx-22.4.7.tgz| tar -xOf - nginx/Chart.yaml
zcat nginx-22.4.7.tgz| tar -xOf - nginx/values.yaml
zcat nginx-22.4.7.tgz| tar -xOf - nginx/values.schema.json
# helm chart 설치
helm install my-nginx ./nginx-22.4.7.tgz --set service.type=NodePort
helm repo list # 미사용으로 없음!
# helm 확인
helm list
helm get metadata my-nginx
# 디플로이먼트 확인 : IMAGES tags 확인
kubectl get deploy -owide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
my-nginx 1/1 1 1 10m nginx registry-1.docker.io/bitnami/nginx:latest app.kubernetes.io/instance=my-nginx,app.kubernetes.io/name=nginx
helm get manifest my-nginx | grep 'image:'
image: registry-1.docker.io/bitnami/nginx:latest
# 삭제
helm uninstall my-nginx
# ChartMuseum 컨테이너 실행 (Podman)
## 저장 디렉터리 준비
mkdir -p /data/chartmuseum/charts
chmod 777 /data/chartmuseum/charts # (테스트용) 컨테이너 전용 디렉터리 권한 조정
## ChartMuseum 컨테이너 실행
podman run -d \
--name chartmuseum \
-p 8080:8080 \
-v /data/chartmuseum/charts:/charts \
-e STORAGE=local \
-e STORAGE_LOCAL_ROOTDIR=/charts \
-e DEBUG=true \
ghcr.io/helm/chartmuseum:v0.16.4
podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
94261cce61d0 ghcr.io/helm/chartmuseum:v0.16.4 10 seconds ago Up 9 seconds 0.0.0.0:8080->8080/tcp chartmuseum
## 정상 동작 확인
curl -s http://192.168.10.10:8080/health | jq
curl -s http://192.168.10.10:8080/api/charts | jq
helm push 플러그인을 이용해서 push 한다.
# Helm 클라이언트에 Repo 등록 : 이름 internal
helm repo add internal http://192.168.10.10:8080
helm repo update
helm repo list
NAME URL
internal http://192.168.10.10:8080
# 차트 업로드 (Push)
## 방법 A. helm-push 플러그인 vs 방법 B. curl api엔드포인트 직접 요청
helm plugin install https://github.com/chartmuseum/helm-push.git
helm plugin list
NAME VERSION DESCRIPTION
cm-push 0.10.4 Push chart package to ChartMuseum
ls -l /root/nginx-chart/*.tgz
-rw-r--r--. 1 root root 855 Feb 8 01:25 /root/nginx-chart/nginx-chart-1.0.0.tgz
## 내부 차트 저장소 ChartMuseum에 업로드 : 성공!
helm cm-push /root/nginx-chart/nginx-chart-1.0.0.tgz internal
Pushing nginx-chart-1.0.0.tgz to internal...
Done.
# 확인
helm repo update
curl -s http://192.168.10.10:8080/api/charts | jq
{
"nginx-chart": [
{
"name": "nginx-chart",
"version": "1.0.0",
"description": "A Helm chart for deploying Nginx with custom index.html",
"apiVersion": "v2",
"appVersion": "1.28.0-alpine",
"type": "application",
"urls": [
"charts/nginx-chart-1.0.0.tgz"
],
"created": "2026-02-04T14:47:30.575320363Z",
"digest": "85f06deaf9b76cb203fa64ab9bf5ac927bf22814be6f76fcf1c50c7d60506db0"
}
]
}
# 사내 OCI Registry로 Helm Chart Push
helm push /root/nginx-chart/nginx-chart-1.0.0.tgz oci://192.168.10.10:35000/helm-charts
Pushed: 192.168.10.10:35000/helm-charts/nginx-chart:1.0.0
Digest: sha256:3bf4f6d919299aefbdea64e689bf0219d6e3416ed660fc8f00b243be0516f2b2
# 사내 OCI Registry 에서 확인
curl -s 192.168.10.10:35000/v2/_catalog | jq | grep helm
"helm-charts/nginx-chart",
curl -s 192.168.10.10:35000/v2/helm-charts/nginx-chart/tags/list | jq
{
"name": "helm-charts/nginx-chart",
"tags": [
"1.0.0"
]
}
# 사내 OCI Registry 에서 바로 설치
helm install my-nginx oci://192.168.10.10:35000/helm-charts/nginx-chart --version 1.0.0
# 확인
helm list
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
my-nginx default 1 2026-02-06 01:33:43.428898781 +0900 KST deployed nginx-chart-1.0.0 1.28.0-alpine
kubectl get deploy,svc,ep,cm my-nginx -owide
curl http://192.168.10.11:30000
curl -s http://192.168.10.11:30000 | grep version
# 다음 실습을 위해 삭제
helm uninstall my-nginx
helm list실습에서 기존에 offline 을 붙여서 전부 비활성화 했던 것을 다시 활성화하고 레포를 동기화 한다
[admin]
# 패키지(저장소) 동기화 (reposync) : 외부 저장소(BaseOS, AppStream 등)의 패키지를 로컬 디렉토리로 가져옵니다.
## 미러 저장 디렉터리 생성
mkdir -p /root/kubespray-offline/outputs/rpms/rocky/10
cd /root/kubespray-offline/outputs/rpms/rocky/10
# 기본 repo 다시 활성화
tree /etc/yum.repos.d/
for i in rocky-addons rocky-devel rocky-extras rocky; do
mv /etc/yum.repos.d/$i.repo.original /etc/yum.repos.d/$i.repo
done
dnf repolist
# 특정 레포 동기화 (예: baseos, extras, appstream)
# --download-metadata 옵션을 쓰면 원본 메타데이터까지 가져옵니다 : 메타데이터 생성 - 다운로드한 패키지들을 dnf가 인식할 수 있도록 인덱싱 작업을 합니다.
## extras : 금방 끝남, 67M
dnf reposync --repoid=extras --download-metadata -p /root/kubespray-offline/outputs/rpms/rocky/10
## baseos : 3분 소요, 4.8G
dnf reposync --repoid=baseos --download-metadata -p /root/kubespray-offline/outputs/rpms/rocky/10
## appstream : 9분 소요, 13G
dnf reposync --repoid=appstream --download-metadata -p /root/kubespray-offline/outputs/rpms/rocky/10
# admin-lb 가상머신에 잔여 Disk 용량 확인
df -hT /
free -h
# 접속 테스트
curl http://192.168.10.10/rpms/rocky/10/
open http://192.168.10.10/rpms/rocky/10/baseos/
[k8s-node1, k8s-node2]
# 로컬 레포 파일 생성: 서버 IP는 Repo 서버의 IP로 수정
tree /etc/yum.repos.d
cat <<EOF > /etc/yum.repos.d/internal-rocky.repo
[internal-baseos]
name=Internal Rocky 10 BaseOS
baseurl=http://192.168.10.10/rpms/rocky/10/baseos
enabled=1
gpgcheck=0
[internal-appstream]
name=Internal Rocky 10 AppStream
baseurl=http://192.168.10.10/rpms/rocky/10/appstream
enabled=1
gpgcheck=0
[internal-extras]
name=Internal Rocky 10 Extras
baseurl=http://192.168.10.10/rpms/rocky/10/extras
enabled=1
gpgcheck=0
EOF
tree /etc/yum.repos.d
# 내부 서버 repo 정상 동작 확인 : 클라이언트에서 캐시를 비우고 목록을 불러옵니다.
dnf clean all
dnf repolist
dnf makecache
# 패키지 인스톨 정상 실행 확인
dnf install -y nfs-utils vim
## 패키지 정보에 repo 확인
dnf info nfs-utils | grep -i repo
Repository : @System
From repo : internal-baseosinternal 에서 가져오는 것을 확인이 가능하다.
k8s-node1, k8s-node2 에 각각 진행한다.
curl http://192.168.10.10/pypi/
<title>Simple index</title>
</head>
<body>
<a href="ansible/index.html">ansible</a>
<a href="ansible-core/index.html">ansible-core</a>
<a href="cffi/index.html">cffi</a>
...
# pip 설정 확인
cat /etc/pip.conf
cat <<EOF > /etc/pip.conf
[global]
index-url = http://192.168.10.10/pypi
trusted-host = 192.168.10.10
timeout = 60
EOF
pip list | grep -i netaddr
pip install netaddr
pip list | grep -i netaddr
netaddr 1.3.0
# 현재 admin pypi mirror에 없는 패키지 설치 시도 : 실패!
pip install httpx
admin 에 설정한다
cat /root/.config/pip/pip.conf
[global]
index = http://localhost/pypi/
index-url = http://localhost/pypi/
trusted-host = localhost
#
mv /root/.config/pip/pip.conf /root/.config/pip/pip.bak
pip install httpx
pip list | grep httpx
#
find / -name *.whl | tee whl.list
cat whl.list | grep -i http
/root/.cache/pip/wheels/c6/69/46/5e87f24c4c35735a0015d9b6c234048dd71c273d789dffa96f/httpx-0.28.1-py3-none-any.whl
#
tree /root/kubespray-offline/outputs/pypi/files/
cp /root/.cache/pip/wheels/c6/69/46/5e87f24c4c35735a0015d9b6c234048dd71c273d789dffa96f/httpx-0.28.1-py3-none-any.whl /root/kubespray-offline/outputs/pypi/files/
tree /root/kubespray-offline/outputs/pypi/files/
# pypi index registry 에 httpx 추가
cd /root/kubespray-offline/
./pypi-mirror.sh
#
curl http://192.168.10.10/pypi/
<a href="httpx/index.html">httpx</a>
설정 이후에 노드에서 아까 실행이 안 되었던 pip install httpx 를 다시 시도해 본다.
Repo 를 실제로 구현하는 건 정말 어려운 일이라는 것을 느꼈다. 실제로 실행해 볼때는 많은 테스트가 필요할 것 같다는 생각이 든다.
많은 경험을 실습을 하면서 느낀다. 회사에서는 사실 대부분의 업무는 내 지식의 범위를 넘어가지 않았던 것 같은데 매일매일 새롭다.
사실 쿠버네티스의 경우에도 실무로는 사용해 본 적이 없다. 혼자 실습을 하면서 실력을 키워 봐야겠다.