지난 포스팅에서 kubeadm으로 k8s 클러스터를 구성해봤다. 이제 이 클러스터에 워커 노드를 추가해보자.

 

워커 노드 2개를 Join해 볼건데, 각각을 join 시키는 방법은 같기 때문에 하나만 예를 들어서 봐보자.

 

워커노드 Join 하기 전 세팅 과정은 이전 포스팅과 유사하다. 시간 설정하고, 방화벽 해제하고, Swap 비활성화 하고, OverlayFS 켜고, hosts 설정하고, containerd, kubeadm, kubelet, kubectl 등 설치하는 것.

# Time, NTP 설정
timedatectl set-local-rtc 0

# 시스템 타임존(Timezone)을 한국(KST, UTC+9) 으로 설정 : 시스템 시간은 UTC 기준 유지, 표시만 KST로 변환
timedatectl set-timezone Asia/Seoul

# SELinux 설정 : Kubernetes는 Permissive 권장
setenforce 0

# 재부팅 시에도 Permissive 적용
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

# firewalld(방화벽) 끄기
systemctl disable --now firewalld

# Swap 비활성화
swapoff -a

# 재부팅 시에도 'Swap 비활성화' 적용되도록 /etc/fstab에서 swap 라인 주석 처리
sed -i '/swap/d' /etc/fstab

# 커널 모듈 로드
modprobe overlay
modprobe br_netfilter
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# 커널 파라미터 설정 : 네트워크 설정 - 브릿지 트래픽이 iptables를 거치도록 함
cat <<EOF | tee /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

# hosts 설정
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
cat << EOF >> /etc/hosts
192.168.10.100 k8s-ctr
192.168.10.101 k8s-w1
192.168.10.102 k8s-w2
EOF
cat /etc/hosts

# ---> CRI 설치 : containerd(runc) v2.1.5

# Docker 저장소 추가
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# containerd 설치
dnf install -y containerd.io-2.1.5-1.el10

# 기본 설정 생성 및 SystemdCgroup 활성화 (매우 중요)
containerd config default | tee /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml

# systemd unit 파일 최신 상태 읽기
systemctl daemon-reload

# containerd start 와 enabled
systemctl enable --now containerd

# ---> kubeadm, kubelet 및 kubectl 설치 v1.32.11

# repo 추가
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[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 kubeadm kubectl --disableexcludes=kubernetes

# kubelet 활성화 (실제 기동은 kubeadm init 후에 시작됨)
systemctl enable --now kubelet

# /etc/crictl.yaml 파일 작성
cat << EOF > /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
EOF

 

 

워커노드 Join 하기

위 과정까지 진행했으면, 실제로 Join하는 과정은 yaml 파일 하나 만들어서 kubeadm 한번 실행시키면 된다.

NODEIP=$(ip -4 addr show enp0s9 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
echo $NODEIP
cat << EOF > kubeadm-join.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: JoinConfiguration
discovery:
  bootstrapToken:
    token: "123456.1234567890123456"
    apiServerEndpoint: "192.168.10.100:6443"
    unsafeSkipCAVerification: true
nodeRegistration:
  criSocket: "unix:///run/containerd/containerd.sock"
  kubeletExtraArgs:
    - name: node-ip
      value: "$NODEIP"
EOF
cat kubeadm-join.yaml

# join
kubeadm join --config="kubeadm-join.yaml"

위와 같이 진행 후 다시 컨트롤 플레인으로 접근해서 노드 상태를 살펴보면 워커노드가 제대로 추가됐음을 알 수 있다.

# at 컨트롤 플레인
$ kubectl get node -owide
NAME      STATUS   ROLES           AGE     VERSION    INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                        KERNEL-VERSION                  CONTAINER-RUNTIME
k8s-ctr   Ready    control-plane   6h58m   v1.32.11   192.168.10.100   <none>        Rocky Linux 10.0 (Red Quartz)   6.12.0-55.39.1.el10_0.aarch64   containerd://2.1.5
k8s-w1    Ready    <none>          2m29s   v1.32.11   192.168.10.101   <none>        Rocky Linux 10.0 (Red Quartz)   6.12.0-55.39.1.el10_0.aarch64   containerd://2.1.5
k8s-w2    Ready    <none>          2m29s   v1.32.11   192.168.10.102   <none>        Rocky Linux 10.0 (Red Quartz)   6.12.0-55.39.1.el10_0.aarch64   containerd://2.1.5

 

프로메테우스 설치

자 이제는 컨트롤 플레인에 프로메테우스 설치해서 리소스 모니터링을 할 수 있도록 해보자.

프로메테우스 설치는 딱히 별거 없다. 그냥 명령어 적절히 해서 설치하면 바로 된다.

helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm upgrade --install metrics-server metrics-server/metrics-server --set 'args[0]=--kubelet-insecure-tls' -n kube-system

# 확인
kubectl top node
kubectl top pod -A --sort-by='cpu'
kubectl top pod -A --sort-by='memory'

# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

# 파라미터 파일 생성
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    scrapeInterval: "20s"
    evaluationInterval: "20s"
    externalLabels:
      cluster: "myk8s-cluster"
  service:
    type: NodePort
    nodePort: 30001

grafana:
  defaultDashboardsTimezone: Asia/Seoul
  adminPassword: prom-operator
  service:
    type: NodePort
    nodePort: 30002

alertmanager:
  enabled: true
defaultRules:
  create: true

kubeProxy:
  enabled: false
prometheus-windows-exporter:
  prometheus:
    monitor:
      enabled: false
EOT
cat monitor-values.yaml

# 배포
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 80.13.3 -f monitor-values.yaml --create-namespace --namespace monitoring

# 확인
helm list -n monitoring
kubectl get pod,svc,ingress,pvc -n monitoring
kubectl get prometheus,servicemonitors,alertmanagers -n monitoring
kubectl get crd | grep monitoring

helm 통해서 설치하면 된다.

프로메테우스 실행
그라파나 실행

 

앱 배포하기

앱 배포하는 것도 간단하다. kubectl apply 사용해서 하면 된다.

cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webpod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webpod
  template:
    metadata:
      labels:
        app: webpod
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - sample-app
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: webpod
        image: traefik/whoami
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: webpod
  labels:
    app: webpod
spec:
  selector:
    app: webpod
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
EOF

이렇게 하면 아래와 같이 pod가 생성됨을 확인할 수 있다

$ kubectl get pod
NAME                      READY   STATUS    RESTARTS   AGE
webpod-697b545f57-4cgrg   1/1     Running   0          87s
webpod-697b545f57-nr5xs   1/1     Running   0          87s

# --> 다른 NAMESPACE에 있는거 조회하지 않도록 주의
$ kubectl get pod -n kube-system
NAME                                       READY   STATUS    RESTARTS        AGE
calico-kube-controllers-7498b9bb4c-dctts   1/1     Running   1 (2m13s ago)   26m
calico-node-5lxhq                          1/1     Running   0               16m
calico-node-h8nzl                          1/1     Running   0               19m
calico-node-jg5fc                          1/1     Running   0               26m
coredns-668d6bf9bc-hkfnb                   1/1     Running   0               33m
coredns-668d6bf9bc-zbnjl                   1/1     Running   0               33m
etcd-k8s-ctr                               1/1     Running   0               2m7s
kube-apiserver-k8s-ctr                     1/1     Running   0               34m
kube-controller-manager-k8s-ctr            1/1     Running   0               2m7s
kube-proxy-kstr5                           1/1     Running   0               19m
kube-proxy-w9xld                           1/1     Running   0               16m
kube-proxy-zxlxc                           1/1     Running   0               33m
kube-scheduler-k8s-ctr                     1/1     Running   0               2m7s
metrics-server-5dd7b49d79-2bf5n            1/1     Running   0               14m

 

이제 됐다. 지난번 포스팅에 이어서 kubeadm으로 k8s 구성 후 노드 join 시키고 앱 배포하는 것까지 해봤다. 

'Kubernetes' 카테고리의 다른 글

kubeadm으로 k8s 구성하기  (0) 2026.01.25
Ansible: 사용해보기  (0) 2026.01.18
Ansible: 에이전트 없이 완성하는 자동화  (0) 2026.01.18
On-Premise K8s Hands-on Study 1주차  (1) 2026.01.11
Posted by 빛나유
,

k8s 구성을 kubeadm을 통해서 해보자. 지지난 주에는 k8s 구성을 좀 빡센 방법으로 했다면. 이번에는 조금 순한 맛이랄까.

 

언제나 그렇듯. 컨트롤 플레인 구성하는 것이 가장 중요하다. 컨트롤 플레인 구성하는 부분부터 살펴보자.

 

컨트롤 플레인 구성전 세팅 점검 사항

본격적인 컨트롤 플레인 구성에 앞서 몇 가지 세팅을 진행하려고 한다. 

1. 타임 세팅

# timedatectl 정보 확인
timedatectl status # RTC in local TZ: yes -> Warning:...
timedatectl set-local-rtc 0
timedatectl status
timedatectl set-timezone Asia/Seoul

# systemd가 시간 동기화 서비스(chronyd) 를 관리하도록 설정되어 있음 : ntpd 대신 chrony 사용 (Rocky 9/10 기본)
timedatectl status
timedatectl set-ntp true # System clock synchronized: yes -> NTP service: active

chronyc sources -v
chronyc tracking

 

2. SELinux 설정, firewalld(방화벽) 끄기

k8s에서는 SELinux는 Permissive로 세팅하는 것을 권장한다.

setenforce 0
getenforce
sestatus      # Current mode:                   permissive

# 재부팅 시에도 Permissive 적용
cat /etc/selinux/config | grep ^SELINUX
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
cat /etc/selinux/config | grep ^SELINUX

# firewalld(방화벽) 끄기
systemctl status firewalld
systemctl disable --now firewalld
systemctl status firewalld

 

3. (중요) Swap 비활성화

이게 좀 특이할 수도 있다. k8s 구성 시 swap 비활성화는 k8s의 사상에 따라 매우 중요하다. 

k8s에서 노드를 통째로 관리하고자 한다. swap 영역이 활성화되어 있으면 여러 개의 노드가 실행되고 있을 때 swap을 쓰면서 계속 버티고 버텨서 결국에는 전체의 노드의 성능을 낮추게 된다는 것이다. k8s 입장에서 리소스 관리 차원에서 메모리가 부족하면 Pod를 종료하고 다른 노드에서 시작하면 그만이다. 그런데 swap 영역이 활성화되어 있으면 메모리 부족 시에 그 판단을 하지 못하고 느린 상태로 계속 실행되고 있게 된다.

# Swap 비활성화
swapoff -a

# 재부팅 시에도 'Swap 비활성화' 적용되도록 /etc/fstab에서 swap 라인 삭제
cat /etc/fstab | grep swap
sed -i '/swap/d' /etc/fstab
cat /etc/fstab | grep swap

 

4. OverlayFS 모듈 활성화

OverlayFS 모듈 활성화도 해야 한다. OverlayFS란 도커 이미지의 사이즈를 절약하기 위해 사용한다. 보통 Dockerfile 작성할 때 한 줄 한 줄이 모두 레이어 단위로 생성되는데, OverlayFS는 이것을 하나로 묶어준다. 가령 아래와 같이 우분투 이미지를 하나 사용하는 컨테이너1, 2, 3이 있다고 해보자. 

# 같은 Base Image 사용하는 컨테이너 100개
ubuntu:20.04 (100MB) ← 이건 한번만 저장
  ↓
Container 1 (변경사항만 1MB)
Container 2 (변경사항만 2MB)
Container 3 (변경사항만 1MB)
...

 OverlayFS가 없으면 100MB짜리 이미지가 3개 사용되지만, OverlayFS가 있으면 100MB에 각 컨테이너 별 변동 사항(1+2+1MB) 만 사용되면 된다. 

# 커널 모듈 로드
modprobe overlay
modprobe br_netfilter
lsmod | grep -iE 'overlay|br_netfilter'

# 
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
tree /etc/modules-load.d/

# 커널 파라미터 설정 : 네트워크 설정 - 브릿지 트래픽이 iptables를 거치도록 함
cat <<EOF | tee /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
tree /etc/sysctl.d/

# 설정 적용
sysctl --system

# 적용 확인
sysctl net.bridge.bridge-nf-call-iptables
sysctl net.ipv4.ip_forward

 

5. CRI 설치하기

파드가 노드에서 실행될 수 있도록 클러스터의 각 노드에 컨테이너 런타임을 설치해야 한다. 이 때 설치해야 하는 것이 containerd이다. 

dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf repolist
dnf makecache

# 설치 가능한 모든 containerd.io 버전 확인
dnf list --showduplicates containerd.io

# containerd 설치
dnf install -y containerd.io-2.1.5-1.el10

# 설치된 파일 확인
which runc && runc --version
which containerd && containerd --version
which containerd-shim-runc-v2 && containerd-shim-runc-v2 -v
which ctr && ctr --version
cat /etc/containerd/config.toml
tree /usr/lib/systemd/system | grep containerd
cat /usr/lib/systemd/system/containerd.service

# 기본 설정 생성 및 SystemdCgroup 활성화 (매우 중요)
containerd config default | tee /etc/containerd/config.toml

cat /etc/containerd/config.toml | grep -i systemdcgroup
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
cat /etc/containerd/config.toml | grep -i systemdcgroup

# systemd unit 파일 최신 상태 읽기
systemctl daemon-reload

# containerd start 와 enabled
systemctl enable --now containerd

# 
systemctl status containerd --no-pager
journalctl -u containerd.service --no-pager
pstree -alnp
systemd-cgls --no-pager

# containerd의 유닉스 도메인 소켓 확인 : kubelet에서 사용 , containerd client 3종(ctr, nerdctr, crictl)도 사용
containerd config dump | grep -n containerd.sock
ls -l /run/containerd/containerd.sock
ss -xl | grep containerd
ss -xnp | grep containerd

# 플러그인 확인
ctr --address /run/containerd/containerd.sock version

 

6. kubeadm, kubelet 및 kubectl 설치

이제 kubeadm, kubelet, kubectl 삼총사 설치다. 

dnf repolist
tree /etc/yum.repos.d/
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[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 makecache

## 버전 정보 미지정 시, 제공 가능 최신 버전 설치됨.
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes


# kubelet 활성화 (실제 기동은 kubeadm init 후에 시작됨)
systemctl enable --now kubelet
ps -ef |grep kubelet

# 설치 파일들 확인
which kubeadm && kubeadm version -o yaml
which kubectl && kubectl version --client=true
which kubelet && kubelet --version


# cri-tools
which crictl && crictl version

# /etc/crictl.yaml 파일 작성
cat << EOF > /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
EOF

#
systemctl is-active kubelet
systemctl status kubelet --no-pager
journalctl -u kubelet --no-pager
tree /usr/lib/systemd/system | grep kubelet -A1

 

컨트롤 플레인 구성하기

이제 컨트롤 플레인을 본격적으로 구성해보자. 시작은 kubeadm init 를 통해서 할 수 있다. init을 통해 마스터 노드가 설치되고(컨트롤 플레인), API 서버, 스케줄러, 컨트롤러 매니저 등 핵심 구성 요소가 컨테이너로 실행된다. 그리고 결과적으로 워커 노드들이 join할 수 있는 상태를 만들어 준다. 

cat << EOF > kubeadm-init.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
bootstrapTokens:
- token: "123456.1234567890123456"
  ttl: "0s"
  usages:
  - signing
  - authentication
nodeRegistration:
  kubeletExtraArgs:
    - name: node-ip
      value: "192.168.10.100"  # 미설정 시 10.0.2.15 맵핑
  criSocket: "unix:///run/containerd/containerd.sock"
localAPIEndpoint:
  advertiseAddress: "192.168.10.100"
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: "1.32.11"
networking:
  podSubnet: "10.244.0.0/16"
  serviceSubnet: "10.96.0.0/16"
EOF
cat kubeadm-init.yaml

# (옵션) 컨테이너 이미지 미리 다운로드 : 특히 업그레이드 작업 시, 작업 시간 단축을 위해서 수행할 것
kubeadm config images pull

# k8s controlplane 초기화 설정 수행
kubeadm init --config="kubeadm-init.yaml"

kubeadm init을 통해서 아래와 같이 컨테이너 이미지 들이 받아지고, 실행된다.

$ crictl images
IMAGE                                     TAG                 IMAGE ID            SIZE
registry.k8s.io/coredns/coredns           v1.11.3             c69fa2e9cbf5f       18.6MB
registry.k8s.io/etcd                      3.5.24-0            8cb12dd0c3e42       23.7MB
registry.k8s.io/kube-apiserver            v1.32.11            7757c58248a29       29.1MB
registry.k8s.io/kube-controller-manager   v1.32.11            0175d0a8243db       26.7MB
registry.k8s.io/kube-proxy                v1.32.11            4d8fb2dc57519       31.2MB
registry.k8s.io/kube-scheduler            v1.32.11            23d6a1fb92fda       21.1MB
registry.k8s.io/pause                     3.10                873ed75102791       320kB

$ crictl ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD                               NAMESPACE
292892b19d9f9       4d8fb2dc57519       21 seconds ago      Running             kube-proxy                0                   89e451c3bf121       kube-proxy-zb866                  kube-system
f50ef12c64408       0175d0a8243db       43 seconds ago      Running             kube-controller-manager   0                   caf30030971b9       kube-controller-manager-k8s-ctr   kube-system
72740e8e7377e       8cb12dd0c3e42       43 seconds ago      Running             etcd                      0                   8d389a0ae1ea1       etcd-k8s-ctr                      kube-system
d3856ede49213       23d6a1fb92fda       43 seconds ago      Running             kube-scheduler            0                   f276d7991ed95       kube-scheduler-k8s-ctr            kube-system
02519515fe4e8       7757c58248a29       43 seconds ago      Running             kube-apiserver            0                   2351d78edf708       kube-apiserver-k8s-ctr            kube-system

이제 클러스터의 상태와 노드의 상태를 확인해보면 아래와 같다.

$ kubectl cluster-info
Kubernetes control plane is running at https://192.168.10.100:6443
CoreDNS is running at https://192.168.10.100:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

$ kubectl get node -owide
NAME      STATUS     ROLES           AGE    VERSION    INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                        KERNEL-VERSION                 CONTAINER-RUNTIME
k8s-ctr   NotReady   control-plane   2m7s   v1.32.11   192.168.10.100   <none>        Rocky Linux 10.0 (Red Quartz)   6.12.0-55.39.1.el10_0.x86_64   containerd://2.1.5

 

이제 작업의 편리성을 위한 alias 설정이나 kubecolor, kubectx, kubens, kube-ps, helm, k9s 등을 설치해보자.

echo "sudo su -" >> /home/vagrant/.bashrc

# Source the completion
source <(kubectl completion bash)
source <(kubeadm completion bash)
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'source <(kubeadm completion bash)' >> /etc/profile
# kubectl get <tab 2번>

# Alias kubectl to k
alias k=kubectl
complete -o default -F __start_kubectl k
echo 'alias k=kubectl' >> /etc/profile
echo 'complete -o default -F __start_kubectl k' >> /etc/profile
k get node

# kubecolor 설치 : https://kubecolor.github.io/setup/install/
dnf install -y 'dnf-command(config-manager)'
dnf config-manager --add-repo https://kubecolor.github.io/packages/rpm/kubecolor.repo
dnf repolist
dnf install -y kubecolor
kubecolor get node

alias kc=kubecolor
echo 'alias kc=kubecolor' >> /etc/profile
kc get node
kc describe node

# Install Kubectx & Kubens"
dnf install -y git
git clone https://github.com/ahmetb/kubectx /opt/kubectx
ln -s /opt/kubectx/kubens /usr/local/bin/kubens
ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx


# Install Kubeps & Setting PS1
git clone https://github.com/jonmosco/kube-ps1.git /root/kube-ps1
cat << "EOT" >> /root/.bash_profile
source /root/kube-ps1/kube-ps1.sh
KUBE_PS1_SYMBOL_ENABLE=true
function get_cluster_short() {
  echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT

kubectl config rename-context "kubernetes-admin@kubernetes" "HomeLab"
kubens default

# helm 3 설치 : https://helm.sh/docs/intro/install
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | DESIRED_VERSION=v3.18.6 bash
helm version

# k9s 설치 : https://github.com/derailed/k9s
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
wget https://github.com/derailed/k9s/releases/latest/download/k9s_linux_${CLI_ARCH}.tar.gz
tar -xzf k9s_linux_*.tar.gz
ls -al k9s
chown root:root k9s
mv k9s /usr/local/bin/
chmod +x /usr/local/bin/k9s
# k9s



# ---> [k8s-ctr] Flannel CNI 설치 v0.27.3
# 현재 k8s 클러스터에 파드 전체 CIDR 확인
kc describe pod -n kube-system kube-controller-manager-k8s-ctr


# 노드별 파드 CIDR 확인 
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'



# Deploying Flannel with Helm
# https://github.com/flannel-io/flannel/blob/master/Documentation/configuration.md
helm repo add flannel https://flannel-io.github.io/flannel
helm repo update

kubectl create namespace kube-flannel
cat << EOF > flannel.yaml
podCidr: "10.244.0.0/16"
flannel:
  cniBinDir: "/opt/cni/bin"
  cniConfDir: "/etc/cni/net.d"
  args:
  - "--ip-masq"
  - "--kube-subnet-mgr"
  - "--iface=enp0s9"  
  backend: "vxlan"
EOF

helm install flannel flannel/flannel --namespace kube-flannel --version 0.27.3 -f flannel.yaml

# 확인
helm list -A
helm get values -n kube-flannel flannel
kubectl get ds,pod,cm -n kube-flannel -owide
kc describe cm -n kube-flannel kube-flannel-cfg
kc describe ds -n kube-flannel

# flannel cni 바이너리 설치 확인
ls -l /opt/cni/bin/

# cni 설정 정보 확인
tree /etc/cni/net.d/
cat /etc/cni/net.d/10-flannel.conflist | jq

# cni 설치 후 아래 상태(conditions) 정상 확인
crictl info | jq

여기까지 실행하면 coredns 가 정상적으로 실행된다.

$ crictl ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD                                        NAMESPACE
2f0ab103a3d29       5e785d005ccc1       20 minutes ago      Running             calico-kube-controllers   0                   fddf38adda54d       calico-kube-controllers-7498b9bb4c-4wrgb   kube-system
c8f835582e200       c69fa2e9cbf5f       20 minutes ago      Running             coredns                   0                   bc064c8bf7f8a       coredns-668d6bf9bc-bv5pj                   kube-system
e50e390260ee4       c69fa2e9cbf5f       21 minutes ago      Running             coredns                   0                   4581742a31a86       coredns-668d6bf9bc-mhxr5                   kube-system
6e9bfdc3bef00       08616d26b8e74       21 minutes ago      Running             calico-node               0                   5253a657ddf86       calico-node-6kjzd                          kube-system
5b2f7851119ca       4d8fb2dc57519       33 minutes ago      Running             kube-proxy                0                   9fa051aa68bb9       kube-proxy-krvgr                           kube-system
b9d4fb053a535       23d6a1fb92fda       33 minutes ago      Running             kube-scheduler            0                   a1d3adc9deccb       kube-scheduler-k8s-ctr                     kube-system
e7ec4646e8643       8cb12dd0c3e42       33 minutes ago      Running             etcd                      0                   5344400bd942d       etcd-k8s-ctr                               kube-system
14c967a56d607       0175d0a8243db       33 minutes ago      Running             kube-controller-manager   0                   7983ea6661deb       kube-controller-manager-k8s-ctr            kube-system
aafaa8dbd170b       7757c58248a29       33 minutes ago      Running             kube-apiserver            0                   750ff351df496       kube-apiserver-k8s-ctr                     kube-system

 

사실, 바로 실행되진 않았고, coredns가 containercreating 상태로 뜨면서 계속 Running 상태가 안됐었다. 

$ kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

 

위와 같이 실행하니 Running 상태로 떴다. coredns는 반드시 네트워크 플러그인이 적용되어야 실행되는데 Flannel이 잘 설정이 안됐는지 에러가 났다. Calico를 통해서 네트워크 플러그인 적용하니 해결돼었다. (Flannel은 다음에 좀 더 보는걸로...)

 

이제 워커 노드를 Join할 준비가 됐다.이것부터는 다음 포스팅에서 작성해보련다.

'Kubernetes' 카테고리의 다른 글

워커 노드 Join 하기  (0) 2026.01.25
Ansible: 사용해보기  (0) 2026.01.18
Ansible: 에이전트 없이 완성하는 자동화  (0) 2026.01.18
On-Premise K8s Hands-on Study 1주차  (1) 2026.01.11
Posted by 빛나유
,
** 서종호(가시다)님의 On-Premise K8s Hands-on Study 2주차 학습 내용을 기반으로 합니다. ** 

 

어제 포스팅에서 Ansible 설치를 해보고 Ansible의 기본적인 구성 요소들에 대한 설명을 했었다. 설치는 pip3를 통해 간단하게 진행할 수 있으니, 설치는 했다고 가정하고 진행해보려고 한다.

 

인벤토리 생성

인벤토리는 컨트롤 노드가 관리할 노드를 나열해둔 파일로 INI, YAML 모두 지원한다. 여기에서는 INI로 작성해보려고 한다. 

원래는 그냥 나열만 해도 된다.

$ cat inventory
10.10.1.1
10.10.1.2
10.10.1.3

그런데 위와 같이 하면 뭐가 어떤 노드인지 모른다. 그래서 이름을 /etc/hosts 기반으로 바꿔도 된다.

$ cat inventory
tnode1
tnode2
tnode3

이렇게 노드 이름을 붙일 수 있다. 그런데 관리할 노드가 엄청 많아지면 어떤 호스트가 무슨 역할의 노드인지 모를 수 있다. 그래서 그룹을 지어줄 수 있다.

$ cat inventory
[web]
tnode1 ansible_python_interpreter=/usr/bin/python3
tnode2 ansible_python_interpreter=/usr/bin/python3

[db]
tnode3 ansible_python_interpreter=/usr/bin/python3

[all:children]
web
db

$ ansible-inventory -i ./inventory --list | more
{
    "_meta": {
        "hostvars": {
            "tnode1": {
                "ansible_all_ipv4_addresses": [
                    {
                        "__ansible_unsafe": "10.10.1.11"
                    },
                    {
                        "__ansible_unsafe": "10.0.2.15"
                    }
                ],
                "ansible_all_ipv6_addresses": [
                    {
                        "__ansible_unsafe": "fe80::a00:27ff:fe83:a1fc"
                    },
                    {
                        "__ansible_unsafe": "fd17:625c:f037:2:a00:27ff:fef8:c2eb"
                    },
                    {
                        "__ansible_unsafe": "fe80::a00:27ff:fef8:c2eb"
                    }
                ],
                "ansible_apparmor": {
                    "status": {
                        "__ansible_unsafe": "enabled"
                    }
                },
                "ansible_architecture": {
                    "__ansible_unsafe": "x86_64"
                },
                "ansible_bios_date": {
                    "__ansible_unsafe": "12/01/2006"
                },
                "ansible_bios_vendor": {
                    "__ansible_unsafe": "innotek GmbH"
                },
                "ansible_bios_version": {
                    "__ansible_unsafe": "VirtualBox"
                },
                "ansible_board_asset_tag": {
                    "__ansible_unsafe": "NA"
                },
                "ansible_board_name": {
                    "__ansible_unsafe": "VirtualBox"
                },
                "ansible_board_serial": {
                    "__ansible_unsafe": "0"
                },
                "ansible_board_vendor": {
                    "__ansible_unsafe": "Oracle Corporation"
                },
                "ansible_board_version": {
                    "__ansible_unsafe": "1.2"
                },
                "ansible_chassis_asset_tag": {
                    "__ansible_unsafe": "NA"
                },
                "ansible_chassis_serial": {
                    "__ansible_unsafe": "NA"
                ... < 중략 > ...

 

이렇게 하면 tnode1, tnode2는 web 그룹이고, tnode3이 db 노드이구나~ 하고 알 수 있다. 그리고 Ansible에서 inventory 작성할 때 다행히도 정규 표현식 같은 표현을 지원한다.

가령, 100개의 web 서버 이름을 넣으려고 하면

[webservers]
web[1:100].example.com

[db-servers]
db[01:09].example.com

## a.dns.example.com, b.dns.example.com, c.dns.example.com을 의미함.
[dns]
[a:c].dns.example.com

이런식으로 작성할 수도 있다고 한다. 와 편해.. 

인벤토리는 아래와 같이 간단하게 그래프 형태로도 프린트할 수 있다.

$ ansible-inventory -i ./inventory --graph
@all:
  |--@ungrouped:
  |--@web:
  |  |--tnode1
  |  |--tnode2
  |--@db:
  |  |--tnode3

이렇게 만든 inventory 파일을 실제로 적용하려고 하면 ansible.cfg 파일에 적용해주면 된다.

$ cat ansible.cfg
[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false

"./inventory" 로 적은 부분으로 인해 inventory 파일을 적용할 수 있다. 

 

플레이북

이제 인벤토리를 작성했으니, 플레이북을 작성해보고 실행해보자. 그런데 그 전에 ad-hoc 커맨드에 대해서 먼저 알아보자.

 

ad-hoc 커맨드는 플레이북을 실행하지 않고 각각의 관리 노드에 간단하게 ansible을 통해 명령어를 실행해보는 것이다. 간단하게 ping 테스트를 해볼 수 있다.

$ ansible -m ping web
tnode2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
tnode1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

web은 인벤토리에서 설정한데로 tnode1, tnode2이다. 두 노드가 정상적으로 작동중이기 때문에 tnode1 | SUCCESS 와 같이 출력된다. CLI 명령어를 직접 실행하고 싶으면 아래와 같이 "-m shell"을 옵션으로 넣어 실행시키면 된다.

# db 그룹에만 실행
$ ansible -m shell -a uptime db
tnode3 | CHANGED | rc=0 >>
 13:01:48 up 16:28,  1 user,  load average: 0.00, 0.00, 0.00
 
# web 그룹에만 실행
$ ansible -m shell -a "free -h" web
tnode1 | CHANGED | rc=0 >>
               total        used        free      shared  buff/cache   available
Mem:           1.4Gi       347Mi       585Mi       1.0Mi       685Mi       1.1Gi
Swap:          2.9Gi          0B       2.9Gi
tnode2 | CHANGED | rc=0 >>
               total        used        free      shared  buff/cache   available
Mem:           1.4Gi       349Mi       590Mi       1.0Mi       678Mi       1.1Gi
Swap:          2.9Gi          0B       2.9Gi

# 모든 노드에 실행
$ ansible -m shell -a "tail -n 3 /etc/passwd" all
tnode3 | CHANGED | rc=0 >>
tcpdump:x:72:72::/:/sbin/nologin
vagrant:x:1000:1000::/home/vagrant:/bin/bash
vboxadd:x:991:1::/var/run/vboxadd:/bin/false
tnode1 | CHANGED | rc=0 >>
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash
vboxadd:x:999:1::/var/run/vboxadd:/bin/false
tnode2 | CHANGED | rc=0 >>
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash
vboxadd:x:999:1::/var/run/vboxadd:/bin/false

 

자 본격적으로 플레이북을 작성해보자.

$ cat first-playbook.yml
---
- hosts: all
  tasks:
    - name: Print message
      debug:
        msg: Hello CloudNet@ Ansible Study

$ ansible-playbook first-playbook.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Print message] ************************************************************************************************************************************************************************************************************************
ok: [tnode2] => {
    "msg": "Hello CloudNet@ Ansible Study"
}
ok: [tnode1] => {
    "msg": "Hello CloudNet@ Ansible Study"
}
ok: [tnode3] => {
    "msg": "Hello CloudNet@ Ansible Study"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode2                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode3                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

참고로 작성한 플레이북의 yml 파일의 문법을 체크해볼 수도 있다.

# 정상적일 경우
$ ansible-playbook --syntax-check first-playbook.yml

playbook: first-playbook.yml


# 에러를 포함할 경우
$ ansible-playbook --syntax-check first-playbook-with-error.yml
[ERROR]: conflicting action statements: debug, msg
Origin: /root/my-ansible/first-playbook-with-error.yml:4:7

2 - hosts: all
3   tasks:
4     - name: Print message
        ^ column 7

이제 ssh 서비스를 재시작하는 플레이북을 만들어보자.

---
- hosts: all
  tasks:
    - name: Restart sshd service
      ansible.builtin.service:
        name: ssh # sshd
        state: restarted

위 플레이북을 실행하면 에러가 발생한다.

$ ansible-playbook restart-service-error.yml
PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Restart sshd service] *****************************************************************************************************************************************************************************************************************
[ERROR]: Task failed: Module failed: Could not find the requested service ssh: host
Origin: /root/my-ansible/restart-service-error.yml:4:7

2 - hosts: all
3   tasks:
4     - name: Restart sshd service
        ^ column 7

fatal: [tnode3]: FAILED! => {"changed": false, "msg": "Could not find the requested service ssh: host"}
changed: [tnode1]
changed: [tnode2]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode2                     : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode3                     : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

tnode3는 RedHat 계열의 노드이다. RedHat 계열에서는 ssh가 아니라 sshd이다. Ansible은 플레이북을 작성할 때 when이라는 키워드를 통해서 조건 값을 넣어줄 수 있다.

$ cat restart-service.yml
- hosts: all
  tasks:
    - name: Restart SSH on Debian
      ansible.builtin.service:
        name: ssh
        state: restarted
      when: ansible_facts['os_family'] == 'Debian'

    - name: Restart SSH on RedHat
      ansible.builtin.service:
        name: sshd
        state: restarted
      when: ansible_facts['os_family'] == 'RedHat'

$ ansible-playbook restart-service.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Restart SSH on Debian] ****************************************************************************************************************************************************************************************************************
skipping: [tnode3]
changed: [tnode1]
changed: [tnode2]

TASK [Restart SSH on RedHat] ****************************************************************************************************************************************************************************************************************
skipping: [tnode1]
skipping: [tnode2]
changed: [tnode3]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
tnode2                     : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
tnode3                     : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

다음 절에서 설명할 것이지만, 미리 좀 말하면 팩트라는 값을 통해서 os 종류를 변수 형태로 가지고 있을 수 있다. ansible_facts['os_family']라고 하면 각 관리 노드가 RedHat 계열인지 Debian 계열인지 등을 알 수 있다.

 

팩트

팩트를 설명하기 전에 플레이북이 실행되는 과정을 먼저 봐보자.

https://juwon8891.github.io/2026/01/17/k8s-deploy-week2-ansible-basics/

 

[K8s-Deploy] Week 2 - Ansible 기초

주차 소개 Week 2에서는 Ansible의 기초를 학습합니다. Ansible은 에이전트리스(Agentless) 아키텍처를 기반으로 SSH를 통해 서버를 관리하는 자동화 도구입니다. 선언적 문법(YAML)과 멱등성(Idempotency)을

juwon8891.github.io

위 그림을 보면 SSH Connect 시도 후 Gather facts를 실행하는데, 컨트롤 노드가 관리 노드에 SSH를 붙은 후에 여러 정보들을 수집하는 과정임을 알 수 있다. 수집된 정보들은 ansible_facts를 통해 컨트롤 노드로 수집된다.  facts가 어떤 값을 가지고 있는지 알아보면 아래와 같다.

$ cat facts.yml
---

- hosts: db

  tasks:
  - name: Print all facts
    ansible.builtin.debug:
      var: ansible_facts

$ ansible-playbook facts.yml

PLAY [db] ***********************************************************************************************************************************************************************************************************************************

TASK [Print all facts] **********************************************************************************************************************************************************************************************************************
ok: [tnode3] => {
    "ansible_facts": {
        "all_ipv4_addresses": [
            "10.0.2.15",
            "10.10.1.13"
        ],
        "all_ipv6_addresses": [
            "fd17:625c:f037:2:a00:27ff:fe6f:16b4",
            "fe80::a00:27ff:fe6f:16b4",
            "fe80::a00:27ff:fec1:3efc"
        ],
        "ansible_local": {},
        "apparmor": {
            "status": "disabled"
        },
        "architecture": "x86_64",
        "bios_date": "12/01/2006",
        "bios_vendor": "innotek GmbH",
        "bios_version": "VirtualBox",
        "board_asset_tag": "NA",
        "board_name": "VirtualBox",
        "board_serial": "0",
        "board_vendor": "Oracle Corporation",
        "board_version": "1.2",
        "chassis_asset_tag": "NA",
        "chassis_serial": "NA",
        "chassis_vendor": "Oracle Corporation",
        "chassis_version": "NA",
        "cmdline": {
            "BOOT_IMAGE": "(hd0,msdos2)/boot/vmlinuz-5.14.0-570.52.1.el9_6.x86_64",
            "console": "ttyS0,115200n8",
            "no_timer_check": true,
            "resume": "UUID=2635f2a2-b00d-4ea8-b7ae-83a0dbab13b1",
            "ro": true,
            "root": "UUID=857c800e-4079-4023-b9be-38c4dc944806"
        },
        "date_time": {
            "date": "2026-01-18",
            "day": "18",
            "epoch": "1768671545",
            "epoch_int": "1768671545",
            "hour": "02",
            "iso8601": "2026-01-17T17:39:05Z",
            "iso8601_basic": "20260118T023905564860",
            "iso8601_basic_short": "20260118T023905",
            "iso8601_micro": "2026-01-17T17:39:05.564860Z",
            "minute": "39",
            "month": "01",
            "second": "05",
            "time": "02:39:05",
            "tz": "KST",
            "tz_dst": "KST",
            "tz_offset": "+0900",
            "weekday": "Sunday",
            "weekday_number": "0",
            "weeknumber": "02",
            "year": "2026"
        },
        "default_ipv4": {
            "address": "10.0.2.15",
            "alias": "enp0s3",
            "broadcast": "10.0.2.255",
            "gateway": "10.0.2.2",
            "interface": "enp0s3",
            "macaddress": "08:00:27:6f:16:b4",
            "mtu": 1500,
            "netmask": "255.255.255.0",
            "network": "10.0.2.0",
            "prefix": "24",
            "type": "ether"
        },
        .... <중략> ....
        "processor_cores": 2,
        "processor_count": 1,
        "processor_nproc": 2,
        "processor_threads_per_core": 1,
        "processor_vcpus": 2,
        "product_name": "VirtualBox",
        "product_serial": "VirtualBox-1741655f-ab00-4384-9aba-2fa3bd5cec6a",
        "product_uuid": "5f654117-00ab-8443-9aba-2fa3bd5cec6a",
        "product_version": "1.2",
        "python": {
            "executable": "/usr/bin/python3",
            "has_sslcontext": true,
            "type": "cpython",
            "version": {
                "major": 3,
                "micro": 21,
                "minor": 9,
                "releaselevel": "final",
                "serial": 0
            },
            "version_info": [
                3,
                9,
                21,
                "final",
                0
            ]
        },
        "python_version": "3.9.21",
        "real_group_id": 0,
        "real_user_id": 0,
        "selinux": {
            "config_mode": "permissive",
            "mode": "permissive",
            "policyvers": 33,
            "status": "enabled",
            "type": "targeted"
        },
        "selinux_python_present": true,
        "service_mgr": "systemd",
        "ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGPshAekxOswu/JODuLJjSH6xr8XTV/HPnnNhle8uCRLOKIBQFDd0AtdFYpjSVO7U2APoNuwzWy0419qz6ftNC0=",
        "ssh_host_key_ecdsa_public_keytype": "ecdsa-sha2-nistp256",
        "ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAIEWg0xPy7I/ESDGi6VpaQ1sTPpjxN0I2w/50a5rHwDm9",
        "ssh_host_key_ed25519_public_keytype": "ssh-ed25519",
        "ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABgQCUlVAW8dXJHOVUIPotot1kKw5N3t9OOkn8z+UWWtCb9BJz8bshxz590cSxmxynbx+sNzPpF+h8JIirC7/AqKc4+gSWLm+SfyYV+zGZqmKf25rCic2bQ62hdtIk6liXOgfVuMvVWPeXM8L71jhKqQ27Zg9Mm4PwgIMVpMhc/jJkYbeZe5DGli4GoXmO33PTqA3MWY/hWKbhnCSIrkfCMOvQZX+4Gt+Xq+O1XZsvCWJ3Kspt1pxjpopg/vRaQi4Ke1CCbz46KRT1Oc37fUIpwkSEO5B4Ha+wkPJRc1CQwj7sJ4vQ4BCJbq7XUY2/4Q0Vei7TvT8RVK/+pmFgWcTrbNG2z1saKLAEFoADD2JhWPqTiXyQgKrZIyB0AnLfbW+Y2wM12d2ov3mWdV7NsCQpnlfvAGax01RkO0YrGv6kAJaafuxNqzDdcpCX4l5iSZp/mwMnYyBzrFbNJqizwKB3UBZLc+S8c4XllABAJIeBlZHeY0OSah5jlSXvOraLLUdLPZc=",
        "ssh_host_key_rsa_public_keytype": "ssh-rsa",
        "swapfree_mb": 3096,
        "swaptotal_mb": 3096,
        "system": "Linux",
        "system_capabilities": [],
        "system_capabilities_enforced": "False",
        "system_vendor": "innotek GmbH",
        "systemd": {
            "features": "+PAM +AUDIT +SELINUX -APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN -IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK +XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified",
            "version": 252
        },
        "uptime_seconds": 48618,
        "user_dir": "/root",
        "user_gecos": "root",
        "user_gid": 0,
        "user_id": "root",
        "user_shell": "/bin/bash",
        "user_uid": 0,
        "userspace_architecture": "x86_64",
        "userspace_bits": "64",
        "virtualization_role": "guest",
        "virtualization_tech_guest": [
            "virtualbox"
        ],
        "virtualization_tech_host": [],
        "virtualization_type": "virtualbox"
    }
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode3                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Ansible 팩트의 값을 활용하고 싶으면 아래와 같이 하면 된다. 예를 들어 IPv4 주소를 출력하고 싶다고 하면...

$ cat facts-ipv4.yml
---

- hosts: db

  tasks:
  - name: Print all facts
    ansible.builtin.debug:
      msg: >
        The default IPv4 address of {{ ansible_facts.hostname }}
        is {{ ansible_facts.default_ipv4.address }}
        
$ ansible-playbook facts-ipv4.yml

PLAY [db] ***********************************************************************************************************************************************************************************************************************************

TASK [Print all facts] **********************************************************************************************************************************************************************************************************************
ok: [tnode3] => {
    "msg": "The node's host name is tnode3 and the ip is 10.0.2.15\n"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode3                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

위와 같이 출력이 된다. ansible_facts.default_ipv4.address 를 통해서 접근할 수 있게 된다.

 

루프

루프는 플레이북 yml 파일을 작성할 때 귀찮은 작업을 줄여주기 위해서 변수 등을 통해 특정 작업을 반복하도록 하는 것이다. 예를 들어보자. 

$ cat loop.yaml
---
- hosts: all
  vars:
    services:
      - vboxadd-service  # ssh
      - rsyslog

  tasks:
  - name: Check sshd and rsyslog state
    ansible.builtin.service:
      name: "{{ item }}"
      state: started
    loop: "{{ services }}"

위 yml 파일의 경우 services라는 변수를 vboxadd-service, rsyslog로 vars에 저장해두었다. 그러면 name: "{{ item }}" 의 item 부분에 차례로 vboxadd-service, rsyslog가 대입되어 실행된다. 

 

조건문

when절을 위에서도 잠깐 소개했었다. 조건문을 변수를 통해서도 아래와 같이 실행해볼 수 있다.

$ cat when.yml
---
- hosts: localhost
  vars:
    run_my_task: true

  tasks:
  - name: echo message
    ansible.builtin.shell: "echo test"
    when: run_my_task
    register: result

  - name: Show result
    ansible.builtin.debug:
      var: result

위 플레이북 yml을 해석해보면 run_my_task가 true일 경우에만 shell "echo test"를 실행하는 것이다. 그리고 결과를 result에 저장 하여 결과를 출력한다.

PLAY [localhost] ****************************************************************************************************************************************************************************************************************************

TASK [Print all facts] **********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "ansible_local": {
        "my-custom": {
            "packages": {
                "db_package": "mariadb-server",
                "web_package": "httpd"
            },
            "users": {
                "user1": "ansible",
                "user2": "gasida"
            }
        }
    }
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

root@server:~/my-ansible# vi when.yml
$ ansible-playbook when.yml

PLAY [localhost] ****************************************************************************************************************************************************************************************************************************

TASK [echo message] *************************************************************************************************************************************************************************************************************************
changed: [localhost]

TASK [Show result] **************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "result": {
        "changed": true,
        "cmd": "echo test",
        "delta": "0:00:00.015129",
        "end": "2026-01-18 13:49:13.705687",
        "failed": false,
        "msg": "",
        "rc": 0,
        "start": "2026-01-18 13:49:13.690558",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "test",
        "stdout_lines": [
            "test"
        ]
    }
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

result 변수를 확인할 수 있다. 물론 run_my_task를 false로 수정 수 실행하면 echo가 실행이 안된다.

$ cat when.yml
---
- hosts: localhost
  vars:
    run_my_task: false

  tasks:
  - name: echo message
    ansible.builtin.shell: "echo test"
    when: run_my_task
    register: result

  - name: Show result
    ansible.builtin.debug:
      var: result


$ ansible-playbook when.yml
PLAY [localhost] ****************************************************************************************************************************************************************************************************************************

TASK [echo message] *************************************************************************************************************************************************************************************************************************
skipping: [localhost]

TASK [Show result] **************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "result": {
        "changed": false,
        "false_condition": "run_my_task",
        "skip_reason": "Conditional result was False",
        "skipped": true
    }
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

 

참고로 when 조건절에 아래와 같이 조건 연산자를 통해서 조건을 부여할 수도 있다.

ansible_facts[’machine’] == “x86_64” ansible_facts[’machine’] 값이 x86_64와 같으면 true
max_memory == 512 max_memory 값이 512와 같다면 true
min_memory < 128 min_memory 값이 128보다 작으면 true
min_memory > 256 min_memory 값이 256보다 크면 true
min_memory <= 256 min_memory 값이 256보다 작거나 같으면 true
min_memory >= 512 min_memory 값이 512보다 크거나 같으면 true
min_memory != 512 min_memory 값이 512와 같지 않으면 true
min_memory is defined min_memory 라는 변수가 있으면 true
min_memory is not defined min_memory 라는 변수가 없으면 true
memory_available memory 값이 true이며 true, 이때 해당 값이 1이거나 True 또는 yes면 true
not memory_available memory 값이 false이며 true, 이때 해당 값이 0이거나 False 또는 no면 true
ansible_facts[’distribution’] in supported_distros ansible_facts[’distribution’]의 값이 supported_distros 라는 변수에 있으면 true

 

아래는 "in"에 대한 예시이다.

$ cat check-os.yml
---
- hosts: all
  vars:
    supported_distros:
      - Ubuntu
      - CentOS

  tasks:
    - name: Print supported os
      ansible.builtin.debug:
        msg: "This {{ ansible_facts['distribution'] }} need to use apt"
      when: ansible_facts['distribution'] in supported_distros

$ ansible-playbook check-os.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Print supported os] *******************************************************************************************************************************************************************************************************************
ok: [tnode1] => {
    "msg": "This Ubuntu need to use apt"
}
skipping: [tnode3]
ok: [tnode2] => {
    "msg": "This Ubuntu need to use apt"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode2                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode3                     : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

 

 

'Kubernetes' 카테고리의 다른 글

워커 노드 Join 하기  (0) 2026.01.25
kubeadm으로 k8s 구성하기  (0) 2026.01.25
Ansible: 에이전트 없이 완성하는 자동화  (0) 2026.01.18
On-Premise K8s Hands-on Study 1주차  (1) 2026.01.11
Posted by 빛나유
,