JUST WRITE

[Talos] 도커 하나면 쿠버네티스 설치가 가능하다고?! 본문

Infra/Kubernetes

[Talos] 도커 하나면 쿠버네티스 설치가 가능하다고?!

천재보단범재 2026. 2. 1. 18:05

Talos로 Kubernetes 설치

Talos로 Kubernetes 설치

회사에서 솔루션을 개발하게 되었습니다.

개발하기 앞서 필요한 여러 가지 서비스를 설치해야 했습니다.

MySQL, Clickhouse, Minio와 같은 스토리지와 외부 서비스를 구축해야 하였습니다.

처음에는 docker-compose로 관리했지만 서비스가 많아지면서 관리가 너무 힘들었습니다.

Kubernetes를 도입하고 싶었지만 클라우드 k8s 도입 비용이 없었고 GPU 서버 1대만 있는 상황이었습니다.

그러던 중 Talos Linux를 발견했습니다.

What is Talos?

TalosKubernetes 전용으로 설계된 Container 전용 OS입니다.

세가지 특징이 있습니다.

  1. Container 전용 OS - SSH도 없고 shell도 없습니다. 다만 talosctl를 통해 관리할 수 있습니다.
  2. 서버에 직접 파일 수정이 힘듭니다.
  3. Docker Container로 Talos를 실행할 수 있습니다.

GPU 서버가 1대밖에 없는 상황에서 Kubernetes 설치가 불가능해 보였지만

Talos를 통해 Container 형태로 Kubernetes 클러스터를 구축하였습니다.

talosctl를 통한 설치

Talos에서는 talosctl이라는 CLI Client를 제공합니다.

직접 Talos Container를 건드는 것보다는 talosctl를 통해 관리하는 것이 좋습니다.

설치 역시 talosctl로 설치가 가능합니다.

talosctl cluster create \
  --name dev \
  --config-patch @patch.yaml \
  --workers=3 \
  --kubernetes-version=v1.34.2 \
  --exposed-ports 30080:30080/tcp \
  --skip-k8s-node-readiness-check

 

위와 같은 명령어로 Talos를 통해 Kubernetes 클러스터 설치가 가능합니다.

각 플래그의 의미는 아래 표로 정리하였습니다.

Flag 설명
--name dev Kubernetes 클러스터 명칭
--config-patch @patch.yaml 추가 설정 파일 적용
--workers=3 Worker 노드 갯수
--kubernetes-version=v1.34.2 Kubernetes 버전
--exposed-ports 30080:30080/tcp Host에서 k8s 접근 포트 노출
--skip-k8s-node-readiness-check CNI 설치 전까지 NotReady가 정상이므로 대기 skip

 

위에 보면 추가 설정 파일을 적용하고 readiness-check를 skip 하였습니다.

따로 설정을 한 이유는 해당 설정 없이 설치할 경우 CoreDNS가 Pending 상태가 되었습니다.

alidating CIDR and reserving IPs
generating PKI and tokens
creating state directory in "/home/***/.talos/clusters/llmops-dev"
creating network dev
creating controlplane nodes
creating worker nodes
waiting for API
bootstrapping cluster
waiting for etcd to be healthy: OK
waiting for etcd members to be consistent across nodes: OK
waiting for etcd members to be control plane nodes: OK
waiting for apid to be ready: OK
waiting for all nodes memory sizes: OK
waiting for all nodes disk sizes: OK
waiting for no diagnostics: OK
waiting for kubelet to be healthy: OK
waiting for all nodes to finish boot sequence: OK
waiting for all k8s nodes to report: OK
waiting for all control plane static pods to be running: OK
waiting for all control plane components to be ready: OK

merging kubeconfig into "/home/***/kube/config"
renamed cluster "dev" -> "dev-1"
renamed auth info "admin@dev" -> "admin@dev-1"
renamed context "admin@dev" -> "admin@dev-1"
PROVISIONER           docker
NAME                  dev
NETWORK NAME          dev
NETWORK CIDR          10.5.0.0/24
NETWORK GATEWAY       10.5.0.1
NETWORK MTU           1500
KUBERNETES ENDPOINT   https://127.0.0.1:35531

NODES:

NAME                  TYPE           IP         CPU    RAM      DISK
/dev-controlplane-1   controlplane   10.5.0.2   2.00   2.1 GB   -
/dev-worker-1         worker         10.5.0.3   2.00   2.1 GB   -
/dev-worker-2         worker         10.5.0.4   2.00   2.1 GB   -
/dev-worker-3         worker         10.5.0.5   2.00   2.1 GB   -


$ kubectl get nodes
NAME                 STATUS     ROLES           AGE     VERSION
dev-controlplane-1   NotReady   control-plane   7m57s   v1.34.2
dev-worker-1         NotReady   <none>          7m57s   v1.34.2
dev-worker-2         NotReady   <none>          7m56s   v1.34.2
dev-worker-3         NotReady   <none>          7m56s   v1.34.2

$ kubectl get all -n kube-system
NAME                                                    READY   STATUS    RESTARTS        AGE
pod/coredns-6bb5667469-zmqmz                            0/1     Pending   0               7m26s
pod/coredns-6bb5667469-zrqjb                            0/1     Pending   0               7m26s
pod/kube-apiserver-llmops-dev-controlplane-1            1/1     Running   0               7m8s
pod/kube-controller-manager-llmops-dev-controlplane-1   1/1     Running   3 (7m56s ago)   7m8s
pod/kube-scheduler-llmops-dev-controlplane-1            1/1     Running   4 (7m42s ago)   7m8s

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
service/kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   7m33s

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   0/2     2            0           7m33s

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-6bb5667469   2         2         0       7m26

 

이유는 기본 설정에서 CNI 제대로 설치가 되지 않았기 때문입니다.

해당 이슈는 아래 Github 이슈에서도 확인할 수 있습니다.

 

[1.8.0] coredns does not start with talosctl cluster create · Issue #9419 · siderolabs/talos

Bug Report Description When creating a cluster with talosctl and docker, coredns does not start talosctl cluster create View logs talosctl cluster create validating CIDR and reserving IPs generatin...

github.com

별도로 CNI를 세팅하였습니다.

Cilium 설치

CNI는 Kubernetes내 Pod에 네트워크 인터페이스를 부여하는 것입니다.

Cilium을 helm으로 설치하였습니다.

helm repo add cilium https://helm.cilium.io/
helm pull cilium/cilium
tar zxvf cilium-1.18.4.tgz

 

values-overrides.yaml을 따로 작성해서 만들었습니다.

공식 talos 문서에도 Cilium CNI 설치에 관한 안내가 있습니다.

해당 문서를 참고해서 설치하였습니다.

 

Deploy Cilium CNI - Sidero Documentation

In this guide you will learn how to set up Cilium CNI on Talos.

docs.siderolabs.com

ipam:
  mode: "kubernetes"

kubeProxyReplacement: "true"

securityContext:
    capabilities:
      ciliumAgent: ["CHOWN","KILL","NET_ADMIN","NET_RAW","IPC_LOCK","SYS_ADMIN","SYS_RESOURCE","DAC_OVERRIDE","FOWNER","SETGID","SETUID"]
      cleanCiliumState: ["NET_ADMIN","SYS_ADMIN","SYS_RESOURCE"]

cgroup:
  autoMount:
    enabled: false
  hostRoot: /sys/fs/cgroup

k8sServiceHost: "localhost"
k8sServicePort: 7445

 

이제 helm install를 통해 Cilium을 설치합니다.

$ helm install cilium -n kube-system -f values-override.yaml .
NAME: cilium
LAST DEPLOYED: Tue Nov 25 10:40:20 2025
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
You have successfully installed Cilium with Hubble.

Your release version is 1.18.4.

For any further help, visit https://docs.cilium.io/en/v1.18/gettinghelp


$ kubectl get nodes
NAME                 STATUS   ROLES           AGE   VERSION
dev-controlplane-1   Ready    control-plane   12m   v1.34.2
dev-worker-1         Ready    <none>          12m   v1.34.2
dev-worker-2         Ready    <none>          12m   v1.34.2
dev-worker-3         Ready    <none>          12m   v1.34.2

Cilium 설치 후 각 Kubernetes 각 Node가 정상적인 상태인 것을 확인할 수 있습니다.

그리고 CoreDNS도 Pending이 아닌 Running 상태인 것을 확인할 수 있습니다.

$ kubetctl get all -n kube-system
NAME                                                    READY   STATUS    RESTARTS      AGE
pod/cilium-25cfl                                        1/1     Running   0             82s
pod/cilium-2ktjs                                        1/1     Running   0             82s
pod/cilium-dvjlr                                        1/1     Running   0             82s
pod/cilium-envoy-6wmh5                                  1/1     Running   0             82s
pod/cilium-envoy-lg6lq                                  1/1     Running   0             82s
pod/cilium-envoy-p7gcz                                  1/1     Running   0             82s
pod/cilium-envoy-t6h9t                                  1/1     Running   0             82s
pod/cilium-operator-6b565f556c-5xdrr                    1/1     Running   0             82s
pod/cilium-operator-6b565f556c-7fkcz                    1/1     Running   0             82s
pod/cilium-xtxzw                                        1/1     Running   0             82s
pod/coredns-6bb5667469-zmqmz                            1/1     Running   0             12m
pod/coredns-6bb5667469-zrqjb                            1/1     Running   0             12m
pod/kube-apiserver-llmops-dev-controlplane-1            1/1     Running   0             11m
pod/kube-controller-manager-llmops-dev-controlplane-1   1/1     Running   3 (12m ago)   11m
pod/kube-scheduler-llmops-dev-controlplane-1            1/1     Running   4 (12m ago)   11m

NAME                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
service/cilium-envoy   ClusterIP   None             <none>        9964/TCP                 82s
service/hubble-peer    ClusterIP   10.105.165.190   <none>        443/TCP                  82s
service/kube-dns       ClusterIP   10.96.0.10       <none>        53/UDP,53/TCP,9153/TCP   12m

NAME                          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/cilium         4         4         4       4            4           kubernetes.io/os=linux   82s
daemonset.apps/cilium-envoy   4         4         4       4            4           kubernetes.io/os=linux   82s

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/cilium-operator   2/2     2            2           82s
deployment.apps/coredns           2/2     2            2           12m

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/cilium-operator-6b565f556c   2         2         2       82s
replicaset.apps/coredns-6bb5667469           2         2         2       12m

kubeconfig 설정

위에서 보면 kubectl로 Talos로 설치한 Kuberenetes에 접근이 가능합니다.

kubectl 사용을 위해 kubeconfig를 가져와야 합니다.

talosctl 명령어로 가져올 수 있습니다.

# kubeconfig 파일을 현재 디렉토리로 추출
$ talosctl kubeconfig --nodes 127.0.0.1 dev

# 추출된 파일 확인
$ ls -la
-rw------- 1 user user 2281 11월 25 10:31 dev

# 내용 확인 (실제로는 certificate-authority-data 등 민감정보 포함)
$ cat llmops-dev
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ***
    server: https://127.0.0.1:35531
  name: dev
...

기본적으로 talosctl cluster create 실행 시 ~/. kubeconfig/config에 자동으로 merge 됩니다.

context 이름은 자동으로 admin@{cluster_name}으로 생성됩니다.

Storageclass 설치

이제 Talos로 Kubernetes 설치를 완료하였습니다.

Talos는 기본적으로 Storageclass를 제공하지 않으므로 직접 설치해야 합니다.

이번 포스팅에서는 rancher의 local-strorage-provisioner로 설치하였습니다.

$ wget https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.32/deploy/local-path-storage.yaml

# version -> v0.0.32 (25/11/25 기준)
apiVersion: v1
kind: Namespace
metadata:
  name: local-path-storage
  labels:
    pod-security.kubernetes.io/enforce: privileged #추가
    pod-security.kubernetes.io/audit: privileged # 추가
    pod-security.kubernetes.io/warn: privileged # 추가
...
...
...
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
  annotations:
    storageclass.kubernetes.io/is-default-class: "true" # 추가
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
...
...
...
kind: ConfigMap
apiVersion: v1
metadata:
  name: local-path-config
  namespace: local-path-storage
data:
  config.json: |-
    {
            "nodePathMap":[
            {
                    "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
                    "paths":["/var/lib/local-path-provisioner"] # 수정
            }
            ]
    }
  setup: |-
    #!/bin/sh
    set -eu
    mkdir -m 0777 -p "$VOL_DIR"
  teardown: |-
    #!/bin/sh
    set -eu
    rm -rf "$VOL_DIR"
  helperPod.yaml: |-
    apiVersion: v1
    kind: Pod
    metadata:
      name: helper-pod
    spec:
      priorityClassName: system-node-critical
      tolerations:
        - key: node.kubernetes.io/disk-pressure
          operator: Exists
          effect: NoSchedule
      containers:
      - name: helper-pod
        image: busybox
        imagePullPolicy: IfNotPresent


$ kubectl apply -f local-path-storage.yaml
namespace/local-path-storage created
serviceaccount/local-path-provisioner-service-account created
role.rbac.authorization.k8s.io/local-path-provisioner-role created
clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
rolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created
configmap/local-path-config created


$ kubectl get stroageclass
NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  48s

설치는 간단합니다. 

버전을 참고해서 local-path-provisioner의 yaml 파일을 다운로드하여 설치하면 됩니다.

근데 Namespace에 Pod-security labels을 추가해줘야 합니다.

Label 역할
pod-security.kubernetes.io/enforce: privileged 이 수준을 벗어나는 Pod은 생성 자체가 거부됨.
pod-security.kubernetes.io/audit: privileged 위반 시 감사 로그에 기록됨
pod-security.kubernetes.io/warn: privileged 위반시 경고메시지를 표시함

 

local-path-provisioner는 내부적으로 호스트 파일 시스템에 직접 접근해서 볼륨을 생성합니다.

이를 위해 hostPath 볼륨과 특권 권한이 필요하는데, 기본 보안 정책 하에서는 이것이 막힙니다.

세 가지 label을 모두 privileged로 설정하면, 해당 Namespace에서 특권 Pod을 자유롭게 실행할 수 있습니다.

Talos 공식문서에서도 local-path-provisioner 설치 내용을 확인할 수 있습니다.

 

Local Storage - Sidero Documentation

Using local storage for Kubernetes workloads.

docs.siderolabs.com

정리

Talos로 docker container형태로 Kubernetes를 설치하였습니다.

docker ps 명령어를 통해 Kubernetes Node가 Container형태로 구성된 것을 확인할 수 있습니다.

$ docker ps
CONTAINER ID   IMAGE                                  COMMAND   CREATED        STATUS         NAMES
a1b2c3d4e5f6   ghcr.io/siderolabs/talos:v1.7.6       "/init"   2 min ago      Up 2 min       dev-controlplane-1
b2c3d4e5f6a7   ghcr.io/siderolabs/talos:v1.7.6       "/init"   2 min ago      Up 2 min       dev-worker-1
c3d4e5f6a7b8   ghcr.io/siderolabs/talos:v1.7.6       "/init"   2 min ago      Up 2 min       dev-worker-2
d4e5f6a7b8c9   ghcr.io/siderolabs/talos:v1.7.6       "/init"   2 min ago      Up 2 min       dev-worker-3

서비스가 너무 많고 서버가 1대라면 docker-compse 말고 Talos를 고려해 보면 좋을 거 같습니다.

Talos 덕분에 솔루션 개발을 위한 서비스 관리나 배포가 쉬울 거 같습니다.

[참고자료]

 

728x90
반응형
Comments