폐쇄망(Air-Gap) 환경에서 kubespray 설치(offline)시 사전에 필요한 작업들을 설명
폐쇄망(Air-Gap) 환경은 보안을 위해 외부 인터넷과 물리적으로 분리된 네트워크다.
보안 요구사항:
격리 이유:

구성 요소:
역할:
역할:
통제 항목:
구성 요소:
역할:
동작 흐름:
1. 외부에서 파일 다운로드
2. 바이러스/악성코드 스캔
3. 승인 대기
4. 내부 방화벽 정책 추가
5. 내부망으로 전달
역할:
구성 요소:
특징:
실습은 VirtualBox와 Vagrant를 이용해 로컬 환경에서 폐쇄망을 시뮬레이션한다. 일반적으로는 Bastion → Admin → K8s Node 순서로 구성하지만, PC 리소스와 시간을 고려해 Admin 서버가 외부 통신도 가능하도록 단순화했다.
실습 환경 구성
Admin 서버 스펙
K8s Node 서버 스펙
Base Box: bento/rockylinux-10.0
VM 구성
admin 서버 (디스크 120GB 증설)k8s-node1 (Control Plane)k8s-node2 (Worker)Private Network: 192.168.10.0/24
SSH root 비밀번호: qwe123

| Node | IP | 역할 | CPU | Memory | Disk |
|---|---|---|---|---|---|
| admin | 192.168.10.10 | Kubespray 실행 / 관리 | 4 | 2GB | 120GB |
| k8s-node1 | 192.168.10.11 | Control Plane | 4 | 2GB | 기본 |
| k8s-node2 | 192.168.10.12 | Worker | 4 | 2GB | 기본 |
Vagrantfile은 3대의 VM을 생성한다. Rocky Linux 10.0 기반으로 K8s Node 2대와 Admin 서버 1대를 구성한다.
BOX_IMAGE = "bento/rockylinux-10.0"
BOX_VERSION = "202510.26.0"
N = 2 # k8s-node 개수
Vagrant.configure("2") do |config|
# k8s-node1, k8s-node2
(1..N).each do |i|
config.vm.define "k8s-node#{i}" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "virtualbox" do |vb|
vb.name = "k8s-node#{i}"
vb.cpus = 4
vb.memory = 2048
end
subconfig.vm.hostname = "k8s-node#{i}"
subconfig.vm.network "private_network", ip: "192.168.10.1#{i}"
subconfig.vm.provision "shell", path: "init_cfg.sh", args: [N]
end
end
# Admin Server
config.vm.define "admin" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "virtualbox" do |vb|
vb.name = "admin"
vb.cpus = 4
vb.memory = 2048
end
subconfig.vm.hostname = "admin"
subconfig.vm.network "private_network", ip: "192.168.10.10"
subconfig.vm.disk :disk, size: "120GB", primary: true # 디스크 증설
subconfig.vm.provision "shell", path: "admin.sh", args: [N]
end
end
주요 설정
| 항목 | Admin | K8s Node1 | K8s Node2 |
|---|---|---|---|
| Hostname | admin | k8s-node1 | k8s-node2 |
| CPU | 4 Core | 4 Core | 4 Core |
| Memory | 2GB | 2GB | 2GB |
| Disk | 120GB | 기본 | 기본 |
| IP (enp0s9) | 192.168.10.10 | 192.168.10.11 | 192.168.10.12 |
| IP (enp0s8) | DHCP | DHCP | DHCP |
| Provisioning | admin.sh | init_cfg.sh | init_cfg.sh |
네트워크 구성
#!/usr/bin/env bash
echo ">>>> Initial Config Start <<<<"
# 1. Timezone 및 NTP 설정
timedatectl set-timezone Asia/Seoul
# 2. 방화벽 및 SELinux 비활성화
systemctl disable --now firewalld
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
# 3. Hosts 파일 설정
echo "192.168.10.10 admin" >> /etc/hosts
for (( i=1; i<=$1; i++ )); do
echo "192.168.10.1$i k8s-node$i" >> /etc/hosts
done
# 4. enp0s9 기본 라우트 삭제
nmcli connection modify enp0s9 ipv4.never-default yes
nmcli connection up enp0s9
# 5. IP forwarding 활성화
cat << EOF > /etc/sysctl.d/99-ipforward.conf
net.ipv4.ip_forward = 1
EOF
sysctl --system
# 6. 패키지 설치
dnf install -y python3-pip git sshpass cloud-utils-growpart
# 7. Helm 설치
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | \
DESIRED_VERSION=v3.20.0 bash
# 8. 디스크 증설
growpart /dev/sda 3
xfs_growfs /dev/sda3
# 9. SSH 설정
echo "root:qwe123" | chpasswd
cat << EOF >> /etc/ssh/sshd_config
PermitRootLogin yes
PasswordAuthentication yes
EOF
systemctl restart sshd
# 10. SSH Key 생성 및 배포
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa
sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@192.168.10.10
for (( i=1; i<=$1; i++ )); do
sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@192.168.10.1$i
done
# 11. K9s 설치
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
wget -P /tmp https://github.com/derailed/k9s/releases/latest/download/k9s_linux_${CLI_ARCH}.tar.gz
tar -xzf /tmp/k9s_linux_${CLI_ARCH}.tar.gz -C /tmp
mv /tmp/k9s /usr/local/bin/
chmod +x /usr/local/bin/k9s
echo ">>>> Initial Config End <<<<"
Admin 서버 초기화 스크립트다. NAT Gateway 역할을 수행하고 Kubespray 실행을 위한 환경을 구성한다.
| TASK | 설명 | 주요 명령 |
|---|---|---|
| 1. Timezone & NTP | 시간대를 Asia/Seoul로 설정 | timedatectl set-timezone Asia/Seoul |
| 2. 방화벽/SELinux | 방화벽 비활성화, SELinux Permissive 모드 | systemctl disable firewalldsetenforce 0 |
| 3. Local DNS | /etc/hosts에 admin과 k8s-node 정보 추가 | echo "192.168.10.10 admin" >> /etc/hosts |
| 4. 라우팅 설정 | enp0s9는 디폴트 라우트 생성 안함 | nmcli connection modify enp0s9 ipv4.never-default yes |
| 5. IP Forward | 커널 파라미터로 IP 포워딩 활성화 | net.ipv4.ip_forward = 1 |
| 6. 패키지 설치 | Python, Git, sshpass 설치 | dnf install -y python3-pip git sshpass |
| 7. Helm 설치 | Helm v3.20.0 설치 | curl -fsSL ... \| bash |
| 8. 디스크 확장 | Admin 서버 디스크를 120GB로 확장 | growpart /dev/sda 3xfs_growfs /dev/sda3 |
| 9. SSHD 설정 | Root 로그인 허용, 비밀번호 인증 활성화 | PermitRootLogin yesPasswordAuthentication yes |
| 10. SSH Key | SSH Key 생성 및 배포 | ssh-keygen -t rsassh-copy-id |
| 11. K9s 설치 | Kubernetes CLI 도구 K9s 설치 | 아키텍처 자동 감지 후 설치 |
주요 포인트
#!/usr/bin/env bash
echo ">>>> Initial Config Start <<<<"
# 1. Timezone 설정
timedatectl set-timezone Asia/Seoul
# 2. 방화벽 및 SELinux 비활성화
systemctl disable --now firewalld
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
# 3. SWAP 비활성화
swapoff -a
sed -i '/swap/d' /etc/fstab
sfdisk --delete /dev/sda 2
partprobe /dev/sda
# 4. 커널 모듈 로드
cat << EOF > /etc/modules-load.d/k8s.conf
overlay
br_netfilter
vxlan
EOF
modprobe overlay
modprobe br_netfilter
# 5. sysctl 설정
cat << EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
# 6. Hosts 파일 설정
echo "192.168.10.10 admin" >> /etc/hosts
for (( i=1; i<=$1; i++ )); do
echo "192.168.10.1$i k8s-node$i" >> /etc/hosts
done
# 7. enp0s9 기본 라우트 삭제
nmcli connection modify enp0s9 ipv4.never-default yes
nmcli connection up enp0s9
# 8. SSH 설정
echo "root:qwe123" | chpasswd
cat << EOF >> /etc/ssh/sshd_config
PermitRootLogin yes
PasswordAuthentication yes
EOF
systemctl restart sshd
# 9. 패키지 설치
dnf install -y python3-pip git
echo ">>>> Initial Config End <<<<"
K8s Node 초기화 스크립트다. Kubernetes 운영을 위한 기본 환경을 설정한다.
| TASK | 설명 | 주요 명령 |
|---|---|---|
| 1. Timezone & NTP | 시간대를 Asia/Seoul로 설정 | timedatectl set-timezone Asia/Seoul |
| 2. 방화벽/SELinux | 방화벽 비활성화, SELinux Permissive 모드 | systemctl disable firewalldsetenforce 0 |
| 3. SWAP 비활성화 | Kubernetes 요구사항에 따라 SWAP 완전 제거 | swapoff -ased -i '/swap/d' /etc/fstabsfdisk --delete /dev/sda 2 |
| 4. 커널 모듈 | 컨테이너 네트워킹에 필요한 모듈 로드 | overlaybr_netfiltervxlan |
| 5. 커널 파라미터 | 브리지 트래픽과 IP 포워딩 설정 | net.bridge.bridge-nf-call-iptables = 1net.ipv4.ip_forward = 1 |
| 6. Local DNS | /etc/hosts에 admin과 k8s-node 정보 추가 | echo "192.168.10.10 admin" >> /etc/hosts |
| 7. 라우팅 설정 | enp0s9는 디폴트 라우트 생성 안함 | nmcli connection modify enp0s9 ipv4.never-default yes |
| 8. SSHD 설정 | Root 로그인 허용, 비밀번호 인증 활성화 | PermitRootLogin yesPasswordAuthentication yes |
| 9. 패키지 설치 | Python, Git 설치 | dnf install -y python3-pip git |
주요 포인트
BOX_IMAGE = "bento/rockylinux-10.0"
BOX_VERSION = "202510.26.0"
N = 2
(1..N).each do |i|
k8s-node1, k8s-node2 자동 생성subconfig.vm.disk :disk, size: "120GB", primary: true
👉 Vagrant Disk 기능 사용
👉 루트 파티션은 admin.sh에서 growpart + xfs_growfs로 확장
# 작업 디렉터리 생성 및 이동
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
# VM 생성
vagrant up
# 상태 확인
vagrant status
# 서버 접속
sshpass -p 'qwe123' ssh root@192.168.10.10 # admin
sshpass -p 'qwe123' ssh root@192.168.10.11 # k8s-node1
sshpass -p 'qwe123' ssh root@192.168.10.12 # k8s-node2
폐쇄망에서 Kubernetes를 운영하려면 외부 의존성을 모두 내부화해야 한다.
필요성:
구축 방법:
# Chrony 설치
dnf install -y chrony
# 설정 파일 편집
cat << EOF > /etc/chrony.conf
server time.google.com iburst
allow 192.168.0.0/16
EOF
# 서비스 시작
systemctl enable --now chronyd
클라이언트 설정:
# /etc/chrony.conf
server 192.168.10.10 iburst
필요성:
구축 방법 (bind9):
# BIND 설치
dnf install -y bind bind-utils
# 설정
cat << EOF > /etc/named.conf
options {
listen-on port 53 { any; };
allow-query { any; };
recursion yes;
};
zone "cluster.local" IN {
type master;
file "/var/named/cluster.local.zone";
};
EOF
# 재시작
systemctl enable --now named
대안: CoreDNS를 내부 DNS로 사용
필요성:
구축 방법:
# reposync 설치
dnf install -y dnf-plugins-core createrepo
# Rocky Linux 10 패키지 동기화
reposync --repo baseos --download-metadata --repoid=baseos -p /srv/repos
reposync --repo appstream --download-metadata --repoid=appstream -p /srv/repos
reposync --repo extras --download-metadata --repoid=extras -p /srv/repos
createrepo /srv/repos/baseos
createrepo /srv/repos/appstream
createrepo /srv/repos/extras
# nginx 설치
dnf install -y nginx
# 설정
cat << EOF > /etc/nginx/conf.d/repos.conf
server {
listen 80;
server_name repos.internal;
root /srv/repos;
autoindex on;
}
EOF
systemctl enable --now nginx
cat << EOF > /etc/yum.repos.d/local.repo
[local-baseos]
name=Local Rocky Linux BaseOS
baseurl=http://192.168.10.10/baseos
enabled=1
gpgcheck=0
[local-appstream]
name=Local Rocky Linux AppStream
baseurl=http://192.168.10.10/appstream
enabled=1
gpgcheck=0
EOF
# 캐시 정리 및 확인
dnf clean all
dnf repolist
필요성:
옵션:
# Registry 실행
docker run -d -p 5000:5000 \
--restart=always \
--name registry \
-v /mnt/registry:/var/lib/registry \
registry:2
# 이미지 푸시
docker tag myapp:latest 192.168.10.10:5000/myapp:latest
docker push 192.168.10.10:5000/myapp:latest
클라이언트 설정 (insecure registry):
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.10.10:5000"]
endpoint = ["http://192.168.10.10:5000"]
주의: Harbor는 현재 arm64 지원이 제한적이다 (GitHub Issue).
# docker-compose로 설치
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgz
tar xzvf harbor-offline-installer-v2.10.0.tgz
cd harbor
# 설정
cp harbor.yml.tmpl harbor.yml
vim harbor.yml # hostname, harbor_admin_password 수정
# 설치
./install.sh
# 접속
open http://192.168.10.10
# admin / Harbor12345
필요성:
옵션:
# ChartMuseum 설치
docker run -d -p 8080:8080 \
--restart=always \
--name chartmuseum \
-v /mnt/charts:/charts \
-e STORAGE=local \
-e STORAGE_LOCAL_ROOTDIR=/charts \
chartmuseum/chartmuseum:latest
# Chart 업로드
curl --data-binary "@mychart-0.1.0.tgz" http://192.168.10.10:8080/api/charts
# Helm 클라이언트 설정
helm repo add internal http://192.168.10.10:8080
helm repo update
# Helm Chart를 OCI 형식으로 푸시
helm package mychart
helm push mychart-0.1.0.tgz oci://192.168.10.10:5000/charts
# 설치
helm install myrelease oci://192.168.10.10:5000/charts/mychart --version 0.1.0
장점:
C. zot (OCI Artifact Registry)
경량 OCI 레지스트리로 컨테이너 이미지와 Helm Chart를 모두 저장할 수 있다.
# zot 설치
wget https://github.com/project-zot/zot/releases/download/v2.0.0/zot-linux-amd64
chmod +x zot-linux-amd64
mv zot-linux-amd64 /usr/local/bin/zot
# 설정
cat << EOF > zot-config.json
{
"storage": {
"rootDirectory": "/var/lib/zot"
},
"http": {
"address": "0.0.0.0",
"port": "5000"
}
}
EOF
# 실행
zot serve zot-config.json
필요성:
구축 방법 (Devpi):
# Devpi 설치
pip install devpi-server devpi-web
# 초기화
devpi-init
# 서버 시작
devpi-server --host 0.0.0.0 --port 3141
# 클라이언트 설정
pip config set global.index-url http://192.168.10.10:3141/root/pypi/+simple/
미러링:
# PyPI 패키지 미러링
devpi use http://192.168.10.10:3141
devpi login root --password ''
devpi index -c mirror bases=root/pypi
devpi use mirror
devpi upload *.whl
필요성:
구축 방법:
# IP forwarding 활성화
echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/99-ipforward.conf
sysctl -p /etc/sysctl.d/99-ipforward.conf
# iptables NAT 설정
iptables -t nat -A POSTROUTING -o enp0s8 -j MASQUERADE
iptables -A FORWARD -i enp0s9 -o enp0s8 -j ACCEPT
iptables -A FORWARD -i enp0s8 -o enp0s9 -m state --state RELATED,ESTABLISHED -j ACCEPT
# 영구 저장
iptables-save > /etc/sysconfig/iptables
# 디렉터리 생성
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
# VM 생성
vagrant up
# 상태 확인
vagrant status
출력:
Current machine states:
k8s-node1 running (virtualbox)
k8s-node2 running (virtualbox)
admin running (virtualbox)
# admin 서버
sshpass -p 'qwe123' ssh root@192.168.10.10
# k8s-node1
sshpass -p 'qwe123' ssh root@192.168.10.11
# k8s-node2
sshpass -p 'qwe123' ssh root@192.168.10.12
Rocky Linux 10은 NetworkManager를 사용하여 네트워크를 관리한다.
NetworkManager (systemd)
↓
Connection Profiles (/etc/NetworkManager/system-connections/)
↓
Network Interfaces (enp0s8, enp0s9)

cat /etc/NetworkManager/system-connections/enp0s8.nmconnection
내용:
[connection]
id=enp0s8
type=ethernet
interface-name=enp0s8
[ipv4]
method=auto # DHCP로 IP 자동 할당
[ipv6]
method=auto
역할:
cat /etc/NetworkManager/system-connections/enp0s9.nmconnection
내용:
[connection]
id=enp0s9
type=ethernet
interface-name=enp0s9
autoconnect-priority=-100
autoconnect-retries=1
[ipv4]
address1=192.168.10.10/24
method=manual
never-default=true # 절대 기본 라우트 생성 안 함
[ethernet]
mac-address=08:00:27:86:00:C2
역할:
never-default=true)# Connection 목록
nmcli connection show
# IP 주소 확인
ip addr show
# 라우팅 테이블
ip route
출력:
default via 10.0.2.2 dev enp0s8 proto dhcp metric 100
10.0.2.0/24 dev enp0s8 proto kernel scope link src 10.0.2.15 metric 100
192.168.10.0/24 dev enp0s9 proto kernel scope link src 192.168.10.10 metric 101
의미:
핵심 데몬이다.
systemctl status NetworkManager.service
역할:
부팅 시 네트워크 연결을 기다린다.
systemctl status NetworkManager-wait-online.service
역할:
인터페이스 이벤트 시 스크립트를 실행한다.
systemctl status NetworkManager-dispatcher.service
역할:
/etc/NetworkManager/dispatcher.d/ 스크립트 실행사용 예시:
# VPN 연결 시 DNS 변경
cat << 'EOF' > /etc/NetworkManager/dispatcher.d/10-vpn-dns
#!/bin/bash
if [ "$2" = "vpn-up" ]; then
echo "nameserver 10.0.0.1" > /etc/resolv.conf
fi
EOF
chmod +x /etc/NetworkManager/dispatcher.d/10-vpn-dns