7 분 소요

Summary

K8s 를 처음 들여다보면 YAML 파일이 정말 많이 나와요. Deployment, Service, Ingress, ConfigMap, Secret… 단어는 들어봤는데 각자 무슨 일을 하고 어떻게 물리는지 헷갈리죠. 다행히 현장에서 자주 보는 오브젝트는 사실 7~8개 안쪽 이에요. 이 글에서는 그중 7개를 골라서, 가상의 web 앱 하나에 다 붙여서 풀어볼게요. YAML 도 한 토막씩 같이 봅니다.

💡 이 글에서 다루는 것

  • K8s 오브젝트가 뭐고 왜 YAML 이 그렇게 많은지
  • Namespace, Pod, Deployment, Service, Ingress, ConfigMap, Secret 일곱 가지
  • 각 오브젝트가 어떤 문제를 푸는지 + 최소 YAML 예시
  • 일곱 개가 하나의 앱에서 어떻게 같이 굴러가는지

이 글은 Docker 와 Kubernetes 의 관계: 워커와 오케스트레이션 관점에서 의 자매편이에요. 앞 글이 “K8s 가 뭐 하는 친구인가” 였다면, 이 글은 그 K8s 에게 실제로 어떻게 시키는가 입니다. 같은 시기에 올린 Airflow on K8s 시리즈 의 보조 자료로도 같이 보면 좋아요.



1. 들어가기 전: K8s 오브젝트가 뭐길래

K8s 에게 일을 시키는 방법은 거의 항상 같은 모양이에요. “이런 상태로 있어 줘” 라고 적은 YAML 한 장을 kubectl apply -f 로 던지는 것. K8s 는 그 YAML 을 오브젝트(Object) 라는 단위로 받아두고, 실제 클러스터를 그 상태에 맞추려고 계속 노력해요.

모든 오브젝트는 같은 4개의 최상위 키를 가져요.

apiVersion: <어느 API 그룹/버전인가>
kind: <어떤 종류의 오브젝트인가 — Pod, Service, ...>
metadata:
  name: <이름>
  namespace: <어느 네임스페이스에 속하나>
spec:
  <오브젝트마다 다른 본문>

apiVersion + kind 가 이 YAML 이 뭐를 만드는지를 정하고, metadata 가 식별자, spec 이 본문이에요. 이 골격만 익혀두면 처음 보는 오브젝트도 어디를 봐야 할지 감이 와요.

✅ 헷갈리지 말 것: YAML 은 현재 상태가 아니라 원하는 상태(desired state) 를 적어요. K8s 가 그 상태를 만들기 위해 어떤 명령을 어떤 순서로 내릴지는 K8s 가 알아서 합니다.



2. Namespace — 다 담는 그릇

가장 먼저 깔리는 친구. 클러스터 안을 논리적으로 나누는 칸막이 예요. 같은 클러스터에서 팀별/환경별로 자원을 분리할 때 씁니다.

무슨 문제를 푸나 한 클러스터에서 여러 팀/환경(개발, 운영) 이 서로 자원을 침범하지 않게
자주 같이 쓰는 친구 거의 모든 다른 오브젝트가 metadata.namespace 로 소속됨
apiVersion: v1
kind: Namespace
metadata:
  name: web-demo

이 한 줄짜리 오브젝트가 깔리면, 앞으로 만들 Pod / Service / Deployment 같은 친구들이 모두 web-demo 라는 네임스페이스 안에 모이게 돼요. 다른 네임스페이스에서는 따로 적지 않으면 안 보입니다.

💡 네임스페이스를 안 적으면 자동으로 default 로 들어가요. 실험은 OK 지만 운영에서는 항상 명시적으로 박는 걸 추천.



3. Pod — 한 묶음 컨테이너

K8s 의 스케줄링 최소 단위. 컨테이너 1개~여러 개를 같은 노드, 같은 네트워크에 묶어서 돌리는 묶음이에요. 보통은 컨테이너 한 개짜리 Pod 가 대부분이에요.

무슨 문제를 푸나 “컨테이너를 어디다 어떻게 띄울까” 를 K8s 에게 묘사
자주 같이 쓰는 친구 Pod 를 직접 만드는 일은 드물고, 대부분 Deployment 가 대신 만들어줌
apiVersion: v1
kind: Pod
metadata:
  name: web
  namespace: web-demo
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx:1.27
      ports:
        - containerPort: 80

containerPort: 80 은 컨테이너가 안에서 듣는 포트 예요. 이걸 외부에 어떻게 노출할지는 Pod 의 일이 아니라 Service / Ingress 의 일이에요(뒤에서 봅니다). labels.app: web 은 다른 오브젝트가 이 Pod 를 찾아낼 때 쓰는 꼬리표예요.

⚠️ Pod 는 휘발성이라고 생각하는 게 안전해요. 죽으면 같은 이름의 Pod 가 자동으로 부활하지 않아요. 자동 부활은 다음 항목인 Deployment 가 책임집니다.



4. Deployment — “같은 Pod 가 N개 떠 있어 줘” 선언

Pod 를 개수와 상태로 관리 하는 오브젝트. “이 모양의 Pod 가 항상 3개 있어 줘” 라고 적어두면 K8s 가 그 상태를 유지해줘요. 노드가 죽거나 Pod 가 죽으면 다른 곳에 다시 띄워요.

무슨 문제를 푸나 Pod 자가복구, 스케일, 무중단 롤링 업데이트
자주 같이 쓰는 친구 거의 항상 Service 와 짝. Deployment 가 만든 Pod 들 앞에 Service 가 안정 주소
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: web-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80

핵심 세 가지만 보면 돼요.

  • replicas: 3 — 같은 모양 Pod 를 3개 유지해줘
  • selector.matchLabels — “내가 관리하는 Pod 는 이 라벨을 단 애들이야”
  • template — 그 Pod 가 어떻게 생겼는지 (= 위 항목의 Pod spec 과 같은 구조)

이미지 태그(nginx:1.27) 만 바꿔서 kubectl apply 하면 K8s 가 알아서 롤링 업데이트 를 굴려요. Pod 를 한 번에 다 바꾸지 않고, 새 버전을 하나씩 띄우고 옛 버전을 하나씩 내려요.

✅ 운영에서 Pod 를 직접 만드는 경우는 거의 없어요. 거의 모든 워크로드는 Deployment(또는 StatefulSet / DaemonSet) 으로 감싸서 굴립니다.



5. Service — Pod 들 앞의 안정적인 주소

Pod 는 죽고 살고를 반복하니까 IP 가 계속 바뀌어요. 그래서 이름과 IP 가 안 바뀌는 안정적인 입구 가 필요해요. 그게 Service 예요.

무슨 문제를 푸나 끊임없이 바뀌는 Pod 들 앞에 한 자리에 박힌 주소를 둠
자주 같이 쓰는 친구 Deployment (뒤에 깔리는 Pod 묶음), Ingress (Service 를 외부에 노출)
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: web-demo
spec:
  selector:
    app: web
  ports:
    - port: 80          # 서비스가 받는 포트
      targetPort: 80    # 뒤에 있는 Pod 의 containerPort

selector.app: webDeployment 가 단 라벨과 같아서 자동으로 그 Pod 들을 묶어줘요. 클러스터 안에서 다른 Pod 가 http://web.web-demo.svc.cluster.local 로 부르면 이 Service 가 받아서 뒤의 Pod 들에 부하 분산해요.

💡 Service 의 종류(type) 가 몇 가지 있어요(ClusterIP 기본 / NodePort / LoadBalancer). 입문 단계에선 클러스터 내부용인 ClusterIP 만 알아도 충분. 외부 노출은 다음 친구인 Ingress 가 맡아요.



6. Ingress — 클러스터 밖에서 안으로 들어오는 입구

Service 는 클러스터 내부 주소예요. 클러스터 에서 사용자가 브라우저로 접속하려면 한 단계 더 필요해요. 그 입구가 Ingress.

무슨 문제를 푸나 HTTP/HTTPS 트래픽을 호스트/경로 기준으로 클러스터 안 Service 로 라우팅
자주 같이 쓰는 친구 Service (뒤에서 받는 쪽), Ingress Controller(NGINX, Traefik 등 실제로 트래픽을 처리하는 데몬)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  namespace: web-demo
spec:
  rules:
    - host: web.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

web.example.com 으로 들어오는 모든 요청을 web 이라는 Service 의 80 포트로 보내요. 같은 클러스터에서 여러 호스트/경로를 한 곳에서 묶어서 관리할 수 있어요.

🚨 Ingress 오브젝트만 만든다고 트래픽이 들어오지 않아요. 실제로 트래픽을 받아주는 Ingress Controller(예: ingress-nginx) 가 클러스터에 깔려 있어야 해요. 보통은 운영팀이 미리 깔아둠.



7. ConfigMap — 설정값을 코드/이미지와 분리

같은 컨테이너 이미지를 개발/스테이징/운영에서 똑같이 쓰되, 설정값만 환경에 맞게 바꾸고 싶어요. 그때 쓰는 게 ConfigMap.

무슨 문제를 푸나 환경설정/플래그/연결 정보 같은 민감하지 않은 키-값 을 코드 밖으로 분리
자주 같이 쓰는 친구 Deployment (env 또는 볼륨으로 주입), Secret (비밀값은 Secret 으로 짝)
apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
  namespace: web-demo
data:
  GREETING: "Hello from K8s"
  LOG_LEVEL: "info"

이 ConfigMap 을 만들고 나면, 컨테이너에 환경변수로 꽂아 넣을 수 있어요. Deployment 의 containers[0] 안에 다음 한 토막만 추가하면 돼요.

envFrom:
  - configMapRef:
      name: web-config

이러면 컨테이너 안에서 $GREETING / $LOG_LEVEL 환경변수로 바로 읽혀요. 이미지 다시 안 굽고도 설정만 갈아끼울 수 있는 게 핵심이에요.

💡 ConfigMap 은 암호화되지 않아요. 평문으로 클러스터에 박힘. 비밀번호/토큰은 절대 ConfigMap 에 넣지 말고 다음 친구로.



8. Secret — 비밀값 따로

ConfigMap 의 비밀값 전용 짝꿍. 비밀번호, API 토큰, TLS 인증서 같은 노출되면 안 되는 값 을 따로 모아둬요.

무슨 문제를 푸나 비밀값을 일반 환경설정과 분리, 권한과 감사를 따로 관리
자주 같이 쓰는 친구 Deployment (env/볼륨으로 주입), 외부 시크릿 매니저(Vault, AWS Secrets Manager 등)
apiVersion: v1
kind: Secret
metadata:
  name: web-secret
  namespace: web-demo
type: Opaque
stringData:
  API_TOKEN: "<API_TOKEN>"

stringData 는 평문으로 적어두면 K8s 가 알아서 base64 인코딩해서 저장해줘요. 컨테이너에서 꺼내 쓸 때는 ConfigMap 과 똑같이 envFrom 으로 끼우면 돼요.

envFrom:
  - secretRef:
      name: web-secret

이러면 $API_TOKEN 으로 잡혀요.

🚨 Secret 은 기본 설정에서는 base64 인코딩일 뿐 암호화는 아니에요. etcd 가 평문에 가깝게 보관해요. 운영에서는 etcd 암호화 활성화 + Sealed Secrets / External Secrets Operator 같이 묶어 쓰는 게 표준. 다만 ConfigMap 보다는 한 단계 더 격리되니까 비밀값은 반드시 Secret 으로.



9. 같이 굴려보면 어떻게 묶이나

지금까지 본 7개를 web-demo 네임스페이스에 한꺼번에 깔면 이런 모양이에요.

[ 클라이언트 ]
       │  https://web.example.com
       ▼
[ Ingress (networking.k8s.io/v1) ]
       │  host=web.example.com → service:web
       ▼
[ Service web (ClusterIP, port 80) ]
       │  selector: app=web
       ▼  Pod 들 사이에서 부하 분산
┌───────────┬───────────┬───────────┐
│  Pod 1    │  Pod 2    │  Pod 3    │   ← Deployment(replicas: 3)
│  nginx    │  nginx    │  nginx    │     template.metadata.labels.app=web
│  :80      │  :80      │  :80      │     env:
└───────────┴───────────┴───────────┘       envFrom: [ConfigMap, Secret]
                                              ↑
                                    web-config (GREETING / LOG_LEVEL)
                                    web-secret (API_TOKEN)

[ 모두 Namespace: web-demo 안 ]

흐름을 말로 풀면:

  1. 사용자가 web.example.com 으로 들어옴
  2. Ingress 컨트롤러가 받아서 라우팅 규칙대로 Service web 으로 넘김
  3. Service 가 selector 로 묶인 Pod 들 중 하나에 트래픽 분배
  4. Pod 안의 nginx 가 응답. 환경변수 GREETING, LOG_LEVEL, API_TOKEN 은 ConfigMap/Secret 에서 받아둔 값
  5. Pod 하나가 죽으면 Deployment 가 자동으로 새로 띄워서 다시 3개를 채움

이 다섯 단계가 거의 모든 K8s 위 웹 서비스의 기본 골격이에요. 같이 깔 때는 보통 한 YAML 안에 --- 로 구분해서 모아두거나, 디렉토리 한 폴더 안에 파일별로 두고 kubectl apply -f ./manifests/ 로 한꺼번에 적용해요.



10. 마무리

이 7개만 익히면 K8s YAML 의 80% 정도 는 읽힙니다. 나머지는 이런 친구들이에요.

다음 단계 친구 언제
StatefulSet 상태를 가진 워크로드(DB, 카프카) — 각 Pod 가 고유 이름/디스크
DaemonSet 모든 노드에 1개씩 — 로그수집, 노드 에이전트
Job / CronJob 끝나면 사라지는 일회성/주기성 잡
PersistentVolumeClaim 영속 디스크가 필요할 때 (StatefulSet/Deployment 의 짝)
ServiceAccount / Role / RoleBinding 권한(RBAC) — 누가 무엇을 할 수 있는지

이 친구들도 결국 위에서 본 4개의 최상위 키(apiVersion/kind/metadata/spec) 골격을 그대로 따라요. 한 번 길이 보이면 나머지는 같은 패턴이에요.

같이 보면 좋은 글.

일단 오늘은 여기까지…..
다음 글에서는 위 표의 다음 단계 친구들(StatefulSet, Job, PersistentVolumeClaim) 을 같은 결로 풀어볼게요.