목적

Azure VM 위에 kubeadm 기반 바닐라 Kubernetes 클러스터를 Terraform + Cloud-init 자동화로 설치/운영하기 위한 표준 절차 정리

구축 요약

  • 컨트롤 플레인(마스터): vm-dev-mvpu-k8smaster-01 (10.25.0.15)
  • 워커: vm-dev-mvpu-k8sworker-01 (10.25.0.5), vm-dev-mvpu-k8sworker-02 (10.25.0.6)
  • 상태: 모든 노드 Ready
  • Kubernetes: v1.30.14 (패키지 버전 고정)
  • Container Runtime: containerd (SystemdCgroup=true)
  • CNI: Calico(기본, 192.168.0.0/16) / Cilium(옵션, kube-proxy 제거) / Kubenet(학습용)

특징

  • 관리형 서비스(AKS) 없이 순수 kubeadm
  • 재현성: Terraform + Cloud-init 기반 템플릿화
  • 보안/일관성: 패키지 버전 고정, 자동 업데이트 방지
  • 확장성: 워커 count 조정으로 수평 확장

최종 확인

kubectl get nodes
NAME                       STATUS   ROLES           VERSION
vm-dev-mvpu-k8smaster-01   Ready    control-plane   v1.30.14
vm-dev-mvpu-k8sworker-01   Ready    <none>          v1.30.14
vm-dev-mvpu-k8sworker-02   Ready    <none>          v1.30.14

 

실행 매뉴얼

사전 준비

  • 권한: Azure 구독 리소스 생성 권한, 대상 RG/VNet/Subnet 확보
  • 로컬 도구: Terraform, Azure CLI, OpenSSH
  • 네트워크/보안: NSG/방화벽에서 22(SSH), 6443(K8s API) 허용(사내 정책 준수)
  • 운영 표준:
    • SSH 키 기반 접속(권장), VM의 disable_password_authentication = true
    • 패키지 버전 핀(FIX): kubeadm/kubelet/kubectl 1.30.14-00
    • CNI는 Calico 또는 Cilium 중 조직 표준 선택

Terraform 배포

terraform init
terraform plan -out tf.plan
terraform apply tf.plan

VM 생성 시 Cloud-init가 자동 실행되어 마스터/워커 초기 설정이 진행됩니다.

k8s-master 작업 (Cloud-init)

자동 작업 요약

  1. 시스템 업데이트, 기본 패키지 설치
  2. swapoff, 커널 모듈/sysctl 적용
  3. containerd 설치 + SystemdCgroup=true
  4. Kubernetes repo/키링 추가
  5. kubeadm/kubelet/kubectl 버전 고정 설치
  6. kubeadm init (예: --pod-network-cidr=192.168.0.0/16 --service-cidr=10.96.0.0/12 --apiserver-advertise-address=<MASTER_IP>)
  7. CNI 설치(Calico or Cilium)
  8. kubectl 컨텍스트(root/일반 사용자)
  9. 워커 조인 명령어 저장(~/kubeadm-join-command.sh)
#!/bin/bash

# Kubernetes 마스터노드(kubeadm) 초기화 스크립트
# VM 생성 시 자동으로 실행됩니다

set -e

# 로그 파일 설정
exec > >(tee /var/log/k8s-master-init.log) 2>&1

echo "=== Kubernetes 마스터노드 초기화 시작 ==="
echo "시작 시간: $(date)"

# 시스템 업데이트
echo "시스템 패키지 업데이트 중..."
apt-get update
apt-get upgrade -y

# 필요한 패키지 설치
echo "필요한 패키지 설치 중..."
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release

# 스왑 비활성화 (쿠버네티스 요구사항)
echo "스왑 비활성화 중..."
swapoff -a
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

# 시스템 설정 최적화
echo "시스템 설정 최적화 중..."
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

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

# containerd 설치
echo "containerd 설치 중..."
apt-get install -y containerd

# containerd 설정
echo "containerd 설정 중..."
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
systemctl restart containerd
systemctl enable containerd

# Kubernetes repository 추가
echo "Kubernetes repository 추가 중..."
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list

# Kubernetes 패키지 설치
echo "Kubernetes 패키지 설치 중..."
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

# kubelet 시작 및 활성화
systemctl enable kubelet

# kubeadm으로 클러스터 초기화
echo "kubeadm으로 클러스터 초기화 중..."
kubeadm init --pod-network-cidr=192.168.0.0/16 --apiserver-advertise-address=$(hostname -I | awk '{print $1}')

# kubectl 설정 (root 사용자용)
echo "kubectl 설정 중 (root)..."
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

# kubectl 설정 (일반 사용자용)
echo "kubectl 설정 중 (${vm_user_name})..."
mkdir -p /home/${vm_user_name}/.kube
cp -i /etc/kubernetes/admin.conf /home/${vm_user_name}/.kube/config
chown ${vm_user_name}:${vm_user_name} /home/${vm_user_name}/.kube/config

# Calico 네트워크 플러그인 설치
echo "Calico 네트워크 플러그인 설치 중..."
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml

# 클러스터 상태 확인을 위한 대기
echo "클러스터 구성 요소 시작 대기 중..."
sleep 60

# 클러스터 상태 확인
echo "클러스터 상태 확인 중..."
kubectl get nodes
kubectl get pods --all-namespaces

# join 명령어 생성 및 저장
echo "워커노드 조인 명령어 생성 중..."
kubeadm token create --print-join-command > /home/${vm_user_name}/kubeadm-join-command.sh
chmod +x /home/${vm_user_name}/kubeadm-join-command.sh
chown ${vm_user_name}:${vm_user_name} /home/${vm_user_name}/kubeadm-join-command.sh

echo ""
echo "=== Kubernetes 마스터노드 초기화 완료 ==="
echo "완료 시간: $(date)"
echo ""
echo "다음 정보를 확인하세요:"
echo "1. 클러스터 상태: kubectl get nodes"
echo "2. 파드 상태: kubectl get pods --all-namespaces"
echo "3. 워커노드 조인 명령어: cat /home/${vm_user_name}/kubeadm-join-command.sh"
echo ""
echo "워커노드 조인 명령어:"
cat /home/${vm_user_name}/kubeadm-join-command.sh
echo ""

# Helm 설치
echo "Helm 설치 중..."
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Azure CLI 설치 (선택사항)
echo "Azure CLI 설치 중..."
curl -sL https://aka.ms/InstallAzureCLIDeb | bash

# 완료 표시
touch /var/log/k8s-master-init-complete

echo "=== 마스터노드 설정 완료 ==="

 

로그 확인

sudo tail -f /var/log/k8s-master-init.log

워커 노드 초기화(Cloud-init 자동 + 수동 조인)

  • Cloud-init 자동 설정: 워커 VM 생성 시, OS 업데이트·패키지 설치·런타임(containerd) 구성·Kubernetes 패키지(kubelet/kubeadm/kubectl) 설치까지 자동화
#!/bin/bash

# Kubernetes 워커노드(kubeadm) 초기화 스크립트
# VM 생성 시 자동으로 실행됩니다

set -e

# 로그 파일 설정
exec > >(tee /var/log/k8s-worker-init.log) 2>&1

echo "=== Kubernetes 워커노드 초기화 시작 ==="
echo "시작 시간: $(date)"

# 시스템 업데이트
echo "시스템 패키지 업데이트 중..."
apt-get update
apt-get upgrade -y

# 필요한 패키지 설치
echo "필요한 패키지 설치 중..."
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release

# 스왑 비활성화 (쿠버네티스 요구사항)
echo "스왑 비활성화 중..."
swapoff -a
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

# 시스템 설정 최적화
echo "시스템 설정 최적화 중..."
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

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

# containerd 설치
echo "containerd 설치 중..."
apt-get install -y containerd

# containerd 설정
echo "containerd 설정 중..."
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
systemctl restart containerd
systemctl enable containerd

# Kubernetes repository 추가
echo "Kubernetes repository 추가 중..."
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list

# Kubernetes 패키지 설치 (워커노드는 kubectl 불필요)
echo "Kubernetes 패키지 설치 중..."
apt-get update
apt-get install -y kubelet kubeadm
apt-mark hold kubelet kubeadm

# kubelet 시작 및 활성화
systemctl enable kubelet

echo ""
echo "=== Kubernetes 워커노드 초기화 완료 ==="
echo "완료 시간: $(date)"
echo ""
echo "다음 단계:"
echo "1. 마스터노드에서 join 명령어 확인:"
echo "   kubectl get nodes  # 마스터노드에서 실행"
echo "   kubeadm token create --print-join-command  # 마스터노드에서 실행"
echo ""
echo "2. 이 워커노드를 클러스터에 조인:"
echo "   sudo kubeadm join <MASTER_IP>:6443 --token <TOKEN> --discovery-token-ca-cert-hash sha256:<HASH>"
echo ""
echo "3. 조인 후 마스터노드에서 확인:"
echo "   kubectl get nodes"
echo ""

# 조인을 위한 헬퍼 스크립트 생성
cat <<EOF > /home/${vm_user_name}/join-cluster.sh
#!/bin/bash
# 이 스크립트를 사용하여 클러스터에 조인하세요
# 사용법: ./join-cluster.sh <MASTER_IP> <TOKEN> <DISCOVERY_TOKEN_CA_CERT_HASH>

if [ \$# -ne 3 ]; then
    echo "사용법: \$0 <MASTER_IP> <TOKEN> <DISCOVERY_TOKEN_CA_CERT_HASH>"
    echo "예시: \$0 10.0.1.4 abc123.def456ghi789 sha256:123abc..."
    exit 1
fi

MASTER_IP=\$1
TOKEN=\$2
HASH=\$3

echo "클러스터 조인 중..."
echo "마스터 IP: \$MASTER_IP"
echo "토큰: \$TOKEN"
echo "해시: \$HASH"

sudo kubeadm join \$MASTER_IP:6443 --token \$TOKEN --discovery-token-ca-cert-hash \$HASH

if [ \$? -eq 0 ]; then
    echo "클러스터 조인 성공!"
    echo "마스터노드에서 'kubectl get nodes'로 확인하세요."
else
    echo "클러스터 조인 실패!"
    echo "로그 확인: journalctl -u kubelet"
fi
EOF

chmod +x /home/${vm_user_name}/join-cluster.sh
chown ${vm_user_name}:${vm_user_name} /home/${vm_user_name}/join-cluster.sh

echo "조인 헬퍼 스크립트가 /home/${vm_user_name}/join-cluster.sh에 생성되었습니다."

# 완료 표시
touch /var/log/k8s-worker-init-complete

echo "=== 워커노드 설정 완료 ==="
  • 수동 조인: 마스터에서 발급한 kubeadm join 명령어를 워커 노드에서 실행해 클러스터에 수동 조인
# 마스터
cat /home/<USER>/kubeadm-join-command.sh

# 워커에서 (예)
sudo kubeadm join 10.25.0.15:6443 --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH>

Master Join 확인

부록 – 스크립트/TF Code

1) Calico

#!/bin/bash
set -Eeuo pipefail
export DEBIAN_FRONTEND=noninteractive
exec > >(tee /var/log/k8s-master-init.log) 2>&1

log() { echo "[$(date +'%F %T')] $*"; }
log "=== master init start ==="

mkdir -p /etc/apt/keyrings && chmod 755 /etc/apt/keyrings
apt-get update && apt-get -y -o Dpkg::Options::="--force-confnew" upgrade
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release

swapoff -a && sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
cat <https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \
  | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
cat >/etc/apt/sources.list.d/kubernetes.list <<'EOF'
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /
EOF
apt-get update
apt-get install -y kubelet=1.30.14-00 kubeadm=1.30.14-00 kubectl=1.30.14-00
apt-mark hold kubelet kubeadm kubectl
systemctl enable kubelet

MASTER_IF="eth0"
MASTER_IP=$(ip -4 addr show "$MASTER_IF" | awk '/inet/ {print $2}' | cut -d/ -f1)
POD_CIDR="192.168.0.0/16"
SERVICE_CIDR="10.96.0.0/12"

kubeadm init \
  --pod-network-cidr="$POD_CIDR" \
  --service-cidr="$SERVICE_CIDR" \
  --apiserver-advertise-address="$MASTER_IP"

mkdir -p $HOME/.kube && cp -i /etc/kubernetes/admin.conf $HOME/.kube/config && chown $(id -u):$(id -g) $HOME/.kube/config
mkdir -p /home/${vm_user_name}/.kube && cp -i /etc/kubernetes/admin.conf /home/${vm_user_name}/.kube/config && chown ${vm_user_name}:${vm_user_name} /home/${vm_user_name}/.kube/config

# Calico (기본)
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
kubectl -n kube-system rollout status ds/calico-node --timeout=180s || true

kubeadm token create --print-join-command > /home/${vm_user_name}/kubeadm-join-command.sh
chmod +x /home/${vm_user_name}/kubeadm-join-command.sh && chown ${vm_user_name}:${vm_user_name} /home/${vm_user_name}/kubeadm-join-command.sh

touch /var/log/k8s-master-init-complete
log "=== master init done ==="

2) Cilium

# kube-proxy 미설치(또는 스킵) + Cilium kube-proxy replacement
kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --apiserver-advertise-address="$MASTER_IP" \
  --skip-phases=addon/kube-proxy

# Cilium 설치(버전 고정 권장)
helm repo add cilium https://helm.cilium.io
helm repo update
helm install cilium cilium/cilium \
  --version 1.15.0 \
  --namespace kube-system \
  --set kubeProxyReplacement=strict \
  --set k8sServiceHost="$MASTER_IP" \
  --set k8sServicePort=6443 \
  --set ipam.mode=cluster-pool \
  --set ipam.operator.clusterPoolIPv4PodCIDRList="{10.244.0.0/16}" \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true

3) Kubenet

# Kubenet은 네트워크 정책 미지원, 멀티노드/라우팅 제약
# 운영보다는 교육/테스트 목적 권장
kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --apiserver-advertise-address="$MASTER_IP"

4) Terraform

providers.tf

terraform {
  required_version = ">= 1.6.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.115"
    }
  }
}

provider "azurerm" {
  features {}
}

variables.tf

variable "vm_user_name" { default = "azureadmin" }
variable "vm_size_name" { default = "Standard_D4s_v3" }   # master 예시
variable "os_disk_info" {
  type = map(string)
  default = {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
    disk_size_gb         = "50"
  }
}
variable "golden_image_info" {
  type = map(string)
  default = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts"
    version   = "latest"
  }
}

main.tf (master VM)

resource "azurerm_linux_virtual_machine" "k8s_master" {
  name                            = "vm-${var.infix_env}-${var.infix_service_name}-k8smaster-${var.suffix_seq}"
  resource_group_name             = var.target_resource_group_name
  location                        = var.resource_group_location
  size                            = var.vm_size_name
  disable_password_authentication = true  # SSH 키 기반 권장
  admin_username                  = var.vm_user_name
  admin_ssh_key {
    username   = var.vm_user_name
    public_key = file(var.ssh_public_key_path)
  }

  network_interface_ids = [azurerm_network_interface.k8s_master_nic.id]

  custom_data = base64encode(templatefile("${path.module}/scripts/init-k8s-master.tmpl", {
    vm_user_name = var.vm_user_name
  }))

  os_disk {
    caching              = var.os_disk_info.caching
    storage_account_type = var.os_disk_info.storage_account_type
    disk_size_gb         = tonumber(var.os_disk_info.disk_size_gb)
  }

  source_image_reference {
    publisher = var.golden_image_info.publisher
    offer     = var.golden_image_info.offer
    sku       = var.golden_image_info.sku
    version   = var.golden_image_info.version
  }

  tags = {
    NodeType    = "k8s-master"
    ClusterName = "vanilla-k8s-cluster"
  }
}

outputs.tf 

output "master_ip" {
  value = azurerm_network_interface.k8s_master_nic.private_ip_address
}

참고 문서