끄적끄적 코딩
728x90

들어가며

이 글에서는 폐쇄망(Air-Gap) 환경에서 Kubespray를 사용하여 Kubernetes 클러스터를 설치하는 방법을 실습하였습니다.

일반적인 기업 환경에서는 보안을 이유로 내부망에서 외부 인터넷 접속이 차단된 경우가 많습니다. 이러한 폐쇄망 환경에서는 컨테이너 이미지, 패키지, 헬름 차트 등 필요한 모든 리소스를 사전에 준비하고 내부 저장소를 구축해야 합니다.

실습 환경은 Vagrant를 사용하여 로컬에서 재현 가능하도록 구성했으며, Admin 서버 1대와 Kubernetes 노드 2대(Control Plane 1대, Worker 1대)로 이루어져 있습니다. 실제 환경에서는 DMZ의 Bastion 서버와 내부망의 Admin 서버가 분리되지만, 이번 실습에서는 리소스 효율을 위해 단일 Admin 서버에서 모든 작업을 수행합니다.

전체 흐름 살펴보기

이번 실습은 다음과 같은 순서로 진행됩니다.

폐쇄망 환경 이해

폐쇄망에서 Kubernetes 클러스터를 운영하기 위해 필요한 구성요소를 파악합니다. NTP, DNS, Network Gateway와 같은 인프라 서비스부터 YUM Repository, Container Registry, Helm Repository, PyPI Mirror 등 애플리케이션 레이어의 저장소까지 전체 구조를 이해합니다.

실습 환경 배포

Vagrant를 사용하여 Admin 서버와 Kubernetes 노드를 배포합니다. Admin 서버는 120GB 디스크를 증설하여 오프라인 패키지를 저장할 공간을 확보합니다.

Network Gateway 설정

내부망 Kubernetes 노드가 Admin 서버를 통해 외부 인터넷에 접속할 수 있도록 NAT Gateway를 설정합니다.

Kubespray 오프라인 패키지 준비

외부 인터넷이 가능한 환경에서 Kubespray 오프라인 설치에 필요한 모든 파일을 다운로드합니다. 컨테이너 이미지, Python 패키지, 바이너리 파일 등을 수집합니다.

Private Container Registry 구성

내부망에서 사용할 컨테이너 이미지 저장소를 구축합니다. Docker Registry를 사용하여 HTTP 기반 Private Registry를 구성하고, 수집한 이미지를 업로드합니다.

Helm Repository 구성

Helm Chart를 관리할 내부 저장소를 구축합니다. OCI Registry를 활용하여 차트를 저장하고 배포합니다.

Local YUM/DNF Repository 구성

Linux 패키지를 제공할 내부 저장소를 구축합니다. reposync를 사용하여 BaseOS, AppStream, Extras 저장소를 미러링합니다.

Private PyPI Mirror 구성

Python 패키지를 제공할 내부 저장소를 구축합니다. Ansible과 Kubespray 실행에 필요한 Python 패키지를 저장합니다.

Kubespray 실행 및 클러스터 설치

준비한 오프라인 리소스를 활용하여 Kubespray를 실행하고 Kubernetes 클러스터를 설치합니다.

폐쇄망 환경 소개

폐쇄망(Air-Gap) 개념

폐쇄망(Air-Gap)은 보안이 요구되는 기업망에서 외부 인터넷과 물리적 또는 논리적으로 분리된 네트워크 환경을 의미합니다. 내부망에서는 외부 인터넷 접속이 불가능하며, 필요한 경우 방화벽 정책 승인 후 Bastion 서버(Bastion Server)를 통해서만 외부 리소스를 다운로드할 수 있습니다.

폐쇄망 환경은 일반적으로 다음과 같이 구성됩니다.

  • Internet Zone: Public Repository 및 웹 서버가 위치합니다.
  • DMZ: Bastion 서버가 위치하여 외부와 내부망 사이의 완충 역할을 수행합니다.
  • Internal Network: Admin 서버와 Kubernetes 클러스터가 위치합니다.
  • 외부/내부 방화벽: 각 구역 사이의 트래픽을 제어합니다.

폐쇄망에서 필요한 주요 구성요소

폐쇄망에서 Kubernetes를 운영하기 위해서는 다음과 같은 구성요소가 필요합니다.

  • NTP Server: 시간 동기화를 위한 서버로, 보통 벤더 어플라이언스 장비 또는 소프트웨어 솔루션으로 이중화 구성됩니다.
  • DNS Server: 도메인 이름 해석을 위한 서버입니다.
  • Network Gateway: 내부망 내 다른 네트워크와의 통신 및 DMZ/외부망과의 통신을 위한 게이트웨이입니다.
  • Local (Mirror) YUM/DNF Repository: Linux 패키지 저장소로, reposync와 createrepo를 활용하여 구축합니다.
  • Private Container (Image) Registry: 컨테이너 이미지 저장소로, Docker Registry나 Harbor를 사용합니다.
  • Helm Artifact Repository: 헬름 차트 저장소로, ChartMuseum이나 OCI Registry(zot)를 사용합니다.
  • Private PyPI Mirror: Python 패키지 저장소로, Devpi를 사용합니다.
  • Private Go Module Proxy: Go 모듈 프록시로, Athens를 사용합니다.

실습 환경 배포

실습 환경 구성

이번 실습 환경은 다음과 같이 구성됩니다.

호스트명 IP 주소 역할 사양
admin 192.168.10.10 Admin/Bastion CPU 4core, RAM 2GB, Disk 120GB
k8s-node1 192.168.10.11 Control Plane CPU 4core, RAM 2GB
k8s-node2 192.168.10.12 Worker CPU 4core, RAM 2GB

일반적으로는 DMZ의 Bastion 서버와 내부망의 Admin 서버가 분리되지만, PC 사양과 시간 단축을 위해 이번 실습에서는 1대의 Admin 서버에서 모든 작업을 수행합니다.

Vagrantfile 구성

Vagrantfile은 Rocky Linux 10.0 기반으로 3대의 가상머신을 생성합니다. Admin 서버는 오프라인 패키지를 저장하기 위해 120GB 디스크를 증설하며, Kubernetes 노드는 각각 Control Plane과 Worker 역할을 수행합니다.

# Base Image  https://portal.cloud.hashicorp.com/vagrant/discover/bento/rockylinux-10.0
BOX_IMAGE = "bento/rockylinux-10.0" # "bento/rockylinux-9"
BOX_VERSION = "202510.26.0"
N = 2 # max number of Node

Vagrant.configure("2") do |config|

# Nodes 
  (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.customize ["modifyvm", :id, "--groups", "/Kubespary-offline-Lab"]
        vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
        vb.name = "k8s-node#{i}"
        vb.cpus = 4
        vb.memory = 2048
        vb.linked_clone = true
      end
      subconfig.vm.host_name = "k8s-node#{i}"
      subconfig.vm.network "private_network", ip: "192.168.10.1#{i}"
      subconfig.vm.network "forwarded_port", guest: 22, host: "6000#{i}", auto_correct: true, id: "ssh"
      subconfig.vm.synced_folder "./", "/vagrant", disabled: true
      subconfig.vm.provision "shell", path: "init_cfg.sh" , args: [ N ]
    end
  end

# Admin Node
    config.vm.define "admin" do |subconfig|
      subconfig.vm.box = BOX_IMAGE
      subconfig.vm.box_version = BOX_VERSION
      subconfig.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--groups", "/Kubespary-offline-Lab"]
        vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
        vb.name = "admin"
        vb.cpus = 4
        vb.memory = 2048
        vb.linked_clone = true
      end
      subconfig.vm.host_name = "admin"
      subconfig.vm.network "private_network", ip: "192.168.10.10"
      subconfig.vm.network "forwarded_port", guest: 22, host: "60000", auto_correct: true, id: "ssh"
      subconfig.vm.synced_folder "./", "/vagrant", disabled: true
      subconfig.vm.disk :disk, size: "120GB", primary: true # https://developer.hashicorp.com/vagrant/docs/disks/usage
      subconfig.vm.provision "shell", path: "admin.sh" , args: [ N ]
    end

end

Admin 서버 초기화 스크립트

admin.sh 스크립트는 Admin 서버의 초기 설정을 자동화합니다. 주요 작업으로는 타임존 설정, 방화벽 및 SELinux 비활성화, Local DNS 설정, IP 포워딩 활성화, 필수 패키지 설치, Helm 및 K9s 설치, 디스크 증설, SSH 키 생성 및 배포가 포함됩니다.

#!/usr/bin/env bash

echo ">>>> Initial Config Start <<<<"

echo "[TASK 1] Change Timezone and Enable NTP"
timedatectl set-local-rtc 0
timedatectl set-timezone Asia/Seoul

echo "[TASK 2] Disable firewalld and selinux"
systemctl disable --now firewalld >/dev/null 2>&1
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

echo "[TASK 3] Setting Local DNS Using Hosts file"
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/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

echo "[TASK 4] Delete default routing - enp0s9 NIC" # setenforce 0 설정 필요
nmcli connection modify enp0s9 ipv4.never-default yes
nmcli connection up enp0s9 >/dev/null 2>&1

echo "[TASK 5] Config net.ipv4.ip_forward"
cat << EOF > /etc/sysctl.d/99-ipforward.conf
net.ipv4.ip_forward = 1
EOF
sysctl --system  >/dev/null 2>&1

echo "[TASK 6] Install packages"
dnf install -y python3-pip git sshpass cloud-utils-growpart >/dev/null 2>&1

echo "[TASK 7] Install Helm"
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | DESIRED_VERSION=v3.20.0 bash >/dev/null 2>&1

echo "[TASK 8] Increase Disk Size"
growpart /dev/sda 3 >/dev/null 2>&1 # lsblk
xfs_growfs /dev/sda3 >/dev/null 2>&1 # df -hT /

echo "[TASK 9] Setting SSHD"
echo "root:qwe123" | chpasswd

cat << EOF >> /etc/ssh/sshd_config
PermitRootLogin yes
PasswordAuthentication yes
EOF
systemctl restart sshd >/dev/null 2>&1

echo "[TASK 10] Setting SSH Key"
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa >/dev/null 2>&1
sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@192.168.10.10  >/dev/null 2>&1  # cat /root/.ssh/authorized_keys
ssh -o StrictHostKeyChecking=no root@admin-lb hostname >/dev/null 2>&1
for (( i=1; i<=$1; i++  )); do sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@192.168.10.1$i >/dev/null 2>&1 ; done
for (( i=1; i<=$1; i++  )); do sshpass -p 'qwe123' ssh -o StrictHostKeyChecking=no root@k8s-node$i hostname >/dev/null 2>&1 ; done

echo "[TASK 11] Install 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  >/dev/null 2>&1
tar -xzf /tmp/k9s_linux_${CLI_ARCH}.tar.gz -C /tmp
chown root:root /tmp/k9s
mv /tmp/k9s /usr/local/bin/
chmod +x /usr/local/bin/k9s

echo "[TASK 12] ETC"
echo "sudo su -" >> /home/vagrant/.bashrc

echo ">>>> Initial Config End <<<<"

주요 작업으로는 타임존 설정, 방화벽 및 SELinux 비활성화, Local DNS 설정, IP 포워딩 활성화, 필수 패키지 설치, Helm 및 K9s 설치, 디스크 증설, SSH 키 생성 및 배포가 포함됩니다.

Kubernetes 노드 초기화 스크립트

init_cfg.sh 스크립트는 Kubernetes 노드의 초기 설정을 자동화합니다.

#!/usr/bin/env bash

echo ">>>> Initial Config Start <<<<"

echo "[TASK 1] Change Timezone and Enable NTP"
timedatectl set-local-rtc 0
timedatectl set-timezone Asia/Seoul

echo "[TASK 2] Disable firewalld and selinux"
systemctl disable --now firewalld >/dev/null 2>&1
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

echo "[TASK 3] Disable and turn off SWAP & Delete swap partitions"
swapoff -a
sed -i '/swap/d' /etc/fstab
sfdisk --delete /dev/sda 2 >/dev/null 2>&1
partprobe /dev/sda >/dev/null 2>&1

echo "[TASK 4] Config kernel & module"
cat << EOF > /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter

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 >/dev/null 2>&1

echo "[TASK 5] Setting Local DNS Using Hosts file"
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/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

echo "[TASK 6] Delete default routing - enp0s9 NIC"
nmcli connection modify enp0s9 ipv4.never-default yes
nmcli connection up enp0s9 >/dev/null 2>&1

echo "[TASK 7] Install containerd"
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y containerd.io-1.7.25 >/dev/null 2>&1
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml >/dev/null 2>&1
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
systemctl enable --now containerd >/dev/null 2>&1

echo "[TASK 8] Install kubeadm, kubelet, kubectl"
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo >/dev/null 2>&1
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
dnf install -y kubelet-1.32.1 kubeadm-1.32.1 kubectl-1.32.1 --disableexcludes=kubernetes >/dev/null 2>&1
systemctl enable --now kubelet >/dev/null 2>&1

echo "[TASK 9] Setting SSHD"
echo "root:qwe123" | chpasswd
cat << EOF >> /etc/ssh/sshd_config
PermitRootLogin yes
PasswordAuthentication yes
EOF
systemctl restart sshd >/dev/null 2>&1

echo "[TASK 10] ETC"
echo "sudo su -" >> /home/vagrant/.bashrc

echo ">>>> Initial Config End <<<<"

SWAP 비활성화 및 파티션 삭제, 커널 모듈 로드, 커널 파라미터 설정 등이 포함됩니다.

환경 배포 실행

다음 명령어로 실습 환경을 배포합니다.

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

배포가 완료되면 각 서버의 상태를 확인할 수 있습니다.

Network Gateway 설정

Kubernetes 노드 네트워크 설정

Kubernetes 노드는 기본적으로 외부 인터넷에 직접 접속할 수 없도록 설정합니다.

nmcli connection down enp0s8
nmcli connection modify enp0s8 connection.autoconnect no
nmcli connection modify enp0s9 +ipv4.routes "0.0.0.0/0 192.168.10.10 200"
nmcli connection up enp0s9
ip route

위 명령어를 통해 enp0s8 인터페이스를 비활성화하고, enp0s9를 통해 Admin 서버(192.168.10.10)를 게이트웨이로 사용하도록 설정합니다.

Admin 서버 NAT 설정

Admin 서버에서 IP 포워딩과 NAT(MASQUERADE)를 설정하여 Kubernetes 노드의 외부 통신을 중계합니다.

# 라우팅 설정 : 이미 설정 되어 있음
sysctl -w net.ipv4.ip_forward=1  # sysctl net.ipv4.ip_forward
cat <<EOF | tee /etc/sysctl.d/99-ipforward.conf
net.ipv4.ip_forward = 1
EOF
sysctl --system

# NAT 설정
iptables -t nat -A POSTROUTING -o enp0s8 -j MASQUERADE
iptables -t nat -S

iptables의 NAT 테이블에 MASQUERADE 룰을 추가하여, 내부망에서 나가는 패킷의 출발지 주소를 Admin 서버의 외부 인터페이스(enp0s8) IP로 변경합니다.

Kubespray 오프라인 패키지 준비

Kubespray 오프라인 저장소 다운로드

Admin 서버에서 Kubespray 오프라인 설치 스크립트를 다운로드합니다.

cd ~
git clone https://github.com/kubespray/kubespray-offline.git
cd kubespray-offline

오프라인 패키지 준비 스크립트 실행

prepare.sh 스크립트를 실행하여 필요한 모든 파일을 다운로드합니다.

./prepare.sh

이 스크립트는 필수 패키지 설치, Python 가상환경 생성, PyPI Mirror 생성, Kubespray 파일 다운로드, 컨테이너 이미지 다운로드, YUM/DNF Repository 생성, 타겟 스크립트 복사 등의 작업을 수행합니다.

Private Container Registry 구성

containerd 설치

setup-container.sh 스크립트를 실행하여 containerd를 설치하고 nginx와 registry 이미지를 로드합니다.

cd /root/kubespray-offline/outputs

./setup-container.sh

이 스크립트는 다음 작업을 수행합니다:

  • runc, nerdctl, containerd 바이너리 설치
  • CNI 플러그인 설치
  • containerd systemd 서비스 설정
  • nginx와 registry 컨테이너 이미지 로드

설치 완료 후 다음 명령어로 확인할 수 있습니다:

which runc && runc --version
which containerd && containerd --version
which nerdctl && nerdctl --version
tree -ug /opt/cni/bin/

systemctl status containerd.service --no-pager

nerdctl images

Nginx 웹 서버 시작

start-nginx.sh 스크립트를 실행하여 파일 서버 역할을 하는 Nginx 컨테이너를 시작합니다.

./start-nginx.sh

Nginx 컨테이너는 outputs 디렉터리를 웹 루트로 마운트하여 files, images, pypi, rpms 디렉터리를 HTTP로 제공합니다.

nerdctl ps

ss -tnlp | grep nginx

웹 브라우저로 http://192.168.10.10에 접속하여 파일 목록을 확인할 수 있습니다.

오프라인 Repository 설정

setup-offline.sh 스크립트를 실행하여 YUM repository와 PyPI mirror를 설정합니다.

./setup-offline.sh
  • 기존 repository를 비활성화하고 로컬 오프라인 repository로 변경
  • PyPI mirror 설정을 ~/.config/pip/pip.conf에 추가
tree /etc/yum.repos.d/

cat /etc/yum.repos.d/offline.repo

dnf clean all
dnf repolist

cat ~/.config/pip/pip.conf

Python 설치

setup-py.sh 스크립트를 실행하여 오프라인 repository에서 Python을 설치합니다.

./setup-py.sh
source pyver.sh
echo -e "python_version $python${PY}"

Private Container Registry 시작

start-registry.sh 스크립트를 실행하여 Docker Registry 컨테이너를 시작합니다.

./start-registry.sh

Registry는 포트 35000에서 실행되며, /var/lib/registry에 이미지를 저장합니다.

nerdctl ps

ss -tnlp | grep registry

tree /var/lib/registry/

컨테이너 이미지 업로드

load-push-all-images.sh 스크립트를 실행하여 모든 컨테이너 이미지를 로드하고 Private Registry에 푸시합니다.

./load-push-all-images.sh
nerdctl images

curl -s http://localhost:35000/v2/_catalog | jq

curl -s http://localhost:35000/v2/kube-apiserver/tags/list | jq

Helm Repository 구성 (OCI Registry)

OCI Registry 개념

OCI (Open Container Initiative) Registry는 컨테이너 이미지뿐만 아니라 Helm Chart도 저장할 수 있는 표준 방식입니다. 별도의 Helm Repository 서버 없이 기존 Container Registry를 활용할 수 있습니다.

Helm Chart 준비 예시

nginx helm chart를 작성하고 OCI Registry에 업로드하는 예시입니다.

Chart 디렉터리 구조 생성

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

Helm Chart 패키징 및 OCI Registry 업로드

helm package .

helm push /root/nginx-chart/nginx-chart-1.0.0.tgz oci://192.168.10.10:35000/helm-charts

curl -s 192.168.10.10:35000/v2/_catalog | jq | grep helm

curl -s 192.168.10.10:35000/v2/helm-charts/nginx-chart/tags/list | jq

Kubespray 설치 및 클러스터 배포

Kubespray 디렉터리 준비

cd /root/kubespray-offline/outputs/kubespray-2.30.0

source ~/.venv/3.12/bin/activate
which ansible

Kubespray 추출 및 설정

cd /root/kubespray-offline/outputs

./extract-kubespray.sh

cd /root/kubespray-offline/outputs/kubespray-2.30.0

cp ../../offline.yml .
cp -r inventory/sample inventory/mycluster
tree inventory/mycluster/

offline.yml 설정

sed -i "s/YOUR_HOST/192.168.10.10/g" offline.yml

cat offline.yml | grep 192.168.10.10

\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 -i inventory/mycluster/inventory.ini all -m ping

노드에 Offline Repository 설정

tree ../playbook/

mkdir offline-repo
cp -r ../playbook/ offline-repo/
tree offline-repo/

ansible-playbook -i inventory/mycluster/inventory.ini offline-repo/playbook/offline-repo.yml

ssh k8s-node1 tree /etc/yum.repos.d/
ssh k8s-node1 dnf repolist

ssh k8s-node1 cat /etc/yum.repos.d/offline.repo

기존 repository 제거

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 dnf repolist
ssh k8s-node2 dnf repolist

Kubespray 변수 설정

echo "kubectl_localhost: true" >> inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
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

Kubespray 실행

ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml -e kube_version="1.34.3"

kubectl 설정

cp inventory/mycluster/artifacts/kubectl /usr/local/bin/
kubectl version --client=true

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

클러스터 확인

kubectl get deploy,sts,ds -n kube-system -owide

이미지 저장소가 192.168.10.10:35000으로 설정된 것을 확인할 수 있습니다.

폐쇄망 서비스 실습

Private Container Registry 활용

nginx 샘플 애플리케이션 배포 테스트

외부 인터넷이 차단된 상태에서 nginx 애플리케이션을 배포해봅니다.

# [k8s-node] 외부 통신 확인
ping -c 1 -w 1 -W 1 8.8.8.8
ip route
crictl images

# [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
          ports:
            - containerPort: 80
EOF

배포 실패 확인

kubectl describe pod

docker.io/library/nginx:alpine 이미지를 가져올 수 없어 실패합니다.

이미지를 Private Registry에 업로드

# [admin] 로컬에 nginx:alpine 다운로드
podman pull nginx:alpine

podman images | grep nginx

# Private Registry에 태그 및 푸시
podman tag nginx:alpine 192.168.10.10:35000/library/nginx:alpine
podman images | grep nginx

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

curl -s 192.168.10.10:35000/v2/library/nginx/tags/list | jq

디플로이먼트 이미지 업데이트

kubectl get pod

kubectl get deploy -owide

kubectl set image deployment/nginx nginx=192.168.10.10:35000/library/nginx:alpine

kubectl get deploy -owide

kubectl get pod

이제 파드가 정상적으로 Running 상태로 변경됩니다.

containerd registry mirror 설정

k8s-node에서 docker.io를 자동으로 내부 registry로 변경하도록 설정합니다.

# 디플로이먼트 삭제
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
          ports:
            - containerPort: 80
EOF

[k8s-node1, k8s-node2]에서 docker.io mirror 설정

mkdir -p /etc/containerd/certs.d/docker.io

cat <<EOF > /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://docker.io"

[host."http://192.168.10.10:35000"]
  capabilities = ["pull", "resolve"]
  skip_verify = true
EOF

systemctl restart containerd

이미지 pull 확인

nerdctl pull docker.io/library/nginx:alpine

crictl images | grep nginx

파드 상태 확인

kubectl get pod

Kubespray containerd_registries_mirrors 설정

Kubespray를 통해 모든 노드에 registry mirror를 자동 설정합니다.

# [admin]
nano inventory/mycluster/group_vars/all/offline.yml
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

설정 적용

ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml -e kube_version="1.34.3" --tags containerd

ssh k8s-node2 tree /etc/containerd

ssh k8s-node2 cat /etc/containerd/certs.d/quay.io/hosts.toml

Helm Chart 활용

OCI Registry에서 Helm Chart 설치

# [admin]
helm install my-nginx oci://192.168.10.10:35000/helm-charts/nginx-chart --version 1.0.0

helm list

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

Local YUM/DNF Repository 활용

Admin 서버에 YUM Repository 구성

# [admin]
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

# Repository 동기화
dnf reposync --repoid=extras --download-metadata -p /root/kubespray-offline/outputs/rpms/rocky/10

dnf reposync --repoid=baseos --download-metadata -p /root/kubespray-offline/outputs/rpms/rocky/10

dnf reposync --repoid=appstream --download-metadata -p /root/kubespray-offline/outputs/rpms/rocky/10

웹 접속 확인

curl http://192.168.10.10/rpms/rocky/10/

k8s-node에서 내부 Repository 사용

# [k8s-node1, k8s-node2]
tree /etc/yum.repos.d
mkdir /etc/yum.repos.d/backup
mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/

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

dnf clean all
dnf repolist
dnf makecache

dnf install -y nfs-utils vim

dnf info nfs-utils | grep -i repo

Private PyPI Mirror 활용

k8s-node에서 PyPI 설정

# [k8s-node1, k8s-node2]
curl http://192.168.10.10/pypi/

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

현재 mirror에 없는 패키지 설치 시도

pip install httpx

PyPI Mirror에 패키지 추가

# [admin]
cat /root/.config/pip/pip.conf

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

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/

cd /root/kubespray-offline/

./pypi-mirror.sh

curl http://192.168.10.10/pypi/

[k8s-node]에서 재시도

pip install httpx

마무리하며

이번 실습에서는 폐쇄망(Air-Gap) 환경에서 Kubespray를 활용하여 Kubernetes 클러스터를 성공적으로 구축하는 전체 과정을 다루었습니다.

폐쇄망 환경에서는 컨테이너 이미지 저장소, 패키지 저장소, Python 패키지 미러, Helm 차트 저장소 등 다양한 인프라 구성요소를 내부망에 구축해야 합니다. kubespray-offline 프로젝트를 활용하여 prepare.sh 스크립트 하나로 약 17분 만에 필요한 오프라인 패키지를 준비하였고, 이를 기반으로 약 3분 만에 Kubernetes 클러스터를 배포할 수 있었습니다.

실무 환경에서는 Private Registry에 TLS 인증서 적용, 패키지 저장소의 정기적인 동기화, 버전 업그레이드 시 충분한 사전 테스트 등이 필요합니다. 이번 실습 환경은 Vagrant를 통해 로컬에서 재현 가능하도록 구성되어 있어, 폐쇄망 환경 구축을 처음 접하는 분들도 부담 없이 실습해볼 수 있습니다.

검색 태그