Platform

Kubernetes 애플리케이션 배포와 GitOps 운영 모델

Kubernetes 구조, Helm, Operator, ArgoCD, GitOps를 애플리케이션 배포와 운영 흐름 안에서 정리합니다.

Kubernetes 애플리케이션 배포와 GitOps 운영 모델

Kubernetes를 처음 보면 YAML을 많이 쓰는 컨테이너 배포 도구처럼 보인다. 그런데 조금 더 따라가 보면 Kubernetes의 핵심은 “컨테이너를 실행한다”보다 “애플리케이션이 어떤 상태로 운영되어야 하는지를 선언하고, 그 상태를 계속 맞춘다”에 가깝다.

하나의 백엔드 서비스를 운영한다고 생각해 보면 필요한 것은 컨테이너 실행만이 아니다. 어떤 이미지로 배포할지, 몇 개의 인스턴스를 유지할지, 트래픽은 어떤 이름으로 들어올지, 설정과 민감정보는 어디에 둘지, 준비되지 않은 파드에 요청을 보내지 않으려면 어떤 헬스체크가 필요한지, 배포가 실패했을 때 어떻게 롤백할지까지 함께 정해야 한다.

이 글은 Kubernetes, AKS, Helm, Operator, ArgoCD, GitOps를 따로 외우기보다, 애플리케이션이 컨테이너 이미지에서 클러스터의 운영 상태가 되기까지의 흐름으로 정리한 기록이다.

컨테이너에서 오케스트레이션으로 넘어가는 지점

컨테이너는 애플리케이션과 실행 환경을 이미지로 묶어 배포할 수 있게 한다. VM과 달리 각 인스턴스가 전체 OS를 포함하지 않고 호스트 OS 커널을 공유하기 때문에 시작이 빠르고 자원 사용량이 낮다. 개발 환경과 운영 환경의 차이를 줄이는 데도 유리하다.

하지만 컨테이너 하나를 잘 실행하는 것과 수십 개의 서비스를 운영하는 것은 다른 문제다. 서비스가 많아지면 다음 질문이 생긴다.

  • 컨테이너가 죽으면 누가 다시 띄우는가
  • 서버가 여러 대일 때 어느 노드에 배치할 것인가
  • 트래픽을 어떤 파드로 보낼 것인가
  • 새 버전을 어떻게 점진적으로 배포할 것인가
  • 설정과 Secret은 어떻게 분리할 것인가
  • 로그와 메트릭은 어디에서 수집할 것인가

Kubernetes는 이 문제를 다루는 컨테이너 오케스트레이션 플랫폼이다. 컨테이너 실행 자체는 containerd 같은 런타임이 담당하지만, Kubernetes는 클러스터 전체 상태를 보고 파드를 배치하고, 장애가 난 파드를 다시 만들고, 서비스 엔드포인트를 유지한다.

Control Plane과 Worker Node를 나눠서 보기

Kubernetes 클러스터는 크게 Control Plane과 Worker Node로 나뉜다. Control Plane은 클러스터의 상태를 관리하고, Worker Node는 실제 애플리케이션 파드를 실행한다.

Control Plane
- kube-apiserver: 모든 요청이 들어오는 API 진입점
- scheduler: 새 Pod를 어느 Node에 배치할지 결정
- controller-manager: 원하는 상태와 실제 상태를 맞추는 컨트롤 루프
- etcd: 클러스터 상태 저장소

Worker Node
- kubelet: Node에서 Pod 상태를 관리
- kube-proxy: Service 네트워크 라우팅 담당
- container runtime: containerd 등 실제 컨테이너 실행

AKS나 EKS 같은 관리형 Kubernetes에서는 Control Plane의 많은 부분을 클라우드 제공자가 관리한다. 사용자는 주로 Node Pool, 네트워크, 스토리지, 애플리케이션 리소스, 접근 권한, 관측성 구성을 다룬다. 관리형 서비스라고 해서 Kubernetes 운영이 사라지는 것은 아니다. API Server와 etcd 운영 부담은 줄지만, 잘못된 Deployment, 과도한 리소스 요청, 부실한 Probe, 열린 Ingress, 정리되지 않은 Secret은 여전히 서비스 문제로 이어진다.

Deployment는 배포 파일이 아니라 운영 계약이다

Kubernetes에서 가장 자주 만나는 리소스는 Pod, Deployment, Service, Ingress, ConfigMap, Secret이다. 이 리소스들은 단순 YAML 파일이 아니라 “서비스가 어떤 상태로 있어야 하는가”를 표현한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: ghcr.io/example/api:1.0.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"

여기서 replicas: 3은 세 개의 파드를 유지하겠다는 선언이고, readinessProbe는 아직 준비되지 않은 파드에 트래픽을 보내지 않기 위한 장치다. livenessProbe는 애플리케이션이 복구 불가능한 상태에 빠졌을 때 재시작할 근거가 된다. resources.requests는 스케줄러가 배치 판단을 할 때 쓰고, limits는 한 컨테이너가 노드 자원을 과도하게 가져가지 않도록 제한한다.

처음에는 kubectl apply -f deployment.yaml로 배포되는지만 확인하기 쉽다. 하지만 운영 관점에서는 다음이 더 중요하다.

  • readiness와 liveness가 같은 엔드포인트를 보지 않는가
  • 애플리케이션 기동 시간이 probe 설정보다 긴가
  • CPU limit이 너무 낮아 throttling을 만들지 않는가
  • memory limit 초과로 OOMKilled가 반복되지 않는가
  • 롤링 업데이트 중 최소 가용 파드 수가 유지되는가

Kubernetes는 문제가 생겼을 때 많은 힌트를 남긴다.

kubectl get pods -o wide
kubectl describe pod api-7c9d8d9f8b-x2k7s
kubectl logs deploy/api --tail=100
kubectl rollout status deployment/api
kubectl rollout history deployment/api
kubectl rollout undo deployment/api

Service와 Ingress는 파드 교체를 숨긴다

Pod는 언제든 새로 만들어지고 사라질 수 있다. 그래서 파드 IP에 직접 의존하면 운영이 어렵다. Service는 라벨 셀렉터를 기준으로 파드 집합에 안정적인 접근 지점을 제공한다.

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: ClusterIP
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080

ClusterIP는 클러스터 내부 접근용이고, NodePort는 노드 포트를 통해 외부에 노출한다. 클라우드 환경에서는 LoadBalancer 타입을 사용하면 Azure Load Balancer나 AWS Load Balancer 같은 외부 로드밸런서와 연결된다. HTTP 계층의 라우팅과 TLS, host/path 기반 라우팅은 보통 Ingress Controller가 담당한다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api
spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80

AKS에서는 Azure CNI, Azure Load Balancer, Application Gateway Ingress Controller 같은 선택지가 있고, EKS에서는 AWS Load Balancer Controller로 ALB/NLB와 연결할 수 있다. 결국 Kubernetes 네트워크는 클러스터 내부 리소스만의 문제가 아니라 VNet/VPC, Subnet, NSG/Security Group, DNS, TLS 인증서와 이어진다.

ConfigMap과 Secret은 배포 단위를 가볍게 만든다

이미지를 만들 때 설정값을 모두 넣어버리면 환경마다 이미지를 다시 만들어야 한다. Kubernetes에서는 ConfigMap과 Secret으로 설정을 분리할 수 있다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
data:
  SPRING_PROFILES_ACTIVE: prod
  LOG_LEVEL: info
apiVersion: v1
kind: Secret
metadata:
  name: api-secret
type: Opaque
stringData:
  DATABASE_URL: postgresql://user:password@postgres:5432/app

Secret은 이름과 목적은 좋지만, 기본 Kubernetes Secret이 강력한 비밀 관리 시스템이라는 뜻은 아니다. etcd 암호화, RBAC, External Secrets, cloud secret manager 연동, Secret rotation까지 함께 봐야 운영 가능한 구성이 된다. 특히 GitOps를 적용할 때 평문 Secret을 Git에 올리지 않도록 SOPS, Sealed Secrets, External Secrets Operator 같은 방식을 검토해야 한다.

Helm은 YAML 중복을 줄이지만 추상화 비용도 만든다

Kubernetes 리소스가 늘어나면 환경별 YAML 관리가 복잡해진다. Helm은 Chart라는 패키지 단위로 Kubernetes 리소스를 묶고, values.yaml로 환경별 값을 주입한다. apt, yum, pip처럼 Kubernetes 애플리케이션을 설치하고 업그레이드하는 패키지 매니저에 가깝다.

my-api-chart/
  Chart.yaml
  values.yaml
  templates/
    deployment.yaml
    service.yaml
    ingress.yaml

예를 들어 개발 환경과 운영 환경은 같은 템플릿을 쓰되 replicas, image tag, resource, ingress host만 다르게 둘 수 있다.

# values-prod.yaml
image:
  repository: ghcr.io/example/api
  tag: "1.0.0"

replicaCount: 3

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

ingress:
  enabled: true
  host: api.example.com

배포는 다음처럼 수행한다.

helm lint ./charts/api
helm template api ./charts/api -f values-prod.yaml
helm upgrade --install api ./charts/api -n production -f values-prod.yaml

Helm은 편하지만 템플릿이 지나치게 복잡해지면 오히려 읽기 어려운 배포 시스템이 된다. 좋은 Chart는 모든 것을 추상화하는 Chart가 아니라, 팀이 자주 바꾸는 값과 바꾸면 안 되는 구조를 분리해 주는 Chart다. helm template로 실제 생성되는 YAML을 확인하는 습관도 중요하다.

Kustomize는 Helm과 다른 방향이다. 템플릿 언어보다 base와 overlay를 이용해 YAML을 조합한다. Helm은 패키징과 파라미터화에 강하고, Kustomize는 기존 YAML을 비교적 그대로 유지하며 환경별 차이를 얹는 데 유리하다. 둘 중 하나가 항상 낫다기보다, 팀이 변경을 읽고 검토하기 쉬운 쪽을 선택해야 한다.

Operator는 운영 지식을 컨트롤러로 옮기는 방식이다

Operator는 Kubernetes를 확장하는 패턴이다. 단순히 Deployment와 Service를 설치하는 수준을 넘어, 특정 시스템의 운영 절차를 컨트롤러에 넣는다. 핵심은 CRD(Custom Resource Definition), CR(Custom Resource), Custom Controller다.

CRD: Prometheus라는 사용자 정의 리소스 타입을 정의
CR: 실제 Prometheus 인스턴스 요청
Controller: CR을 보고 필요한 StatefulSet, Service, Config를 생성하고 상태를 조정

Prometheus Operator를 예로 들면, 사용자는 Prometheus 서버를 구성하기 위해 복잡한 StatefulSet과 ConfigMap을 직접 모두 작성하지 않는다. Prometheus, ServiceMonitor, PodMonitor 같은 Custom Resource를 만들고, Operator가 이를 보고 필요한 하위 리소스를 구성한다.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api
spec:
  selector:
    matchLabels:
      app: api
  endpoints:
    - port: http
      path: /metrics
      interval: 30s

Operator는 편하지만 클러스터 안에서 강한 권한을 갖는 컨트롤러가 된다. 어떤 CRD를 설치하는지, 어떤 네임스페이스를 감시하는지, RBAC 권한이 과도하지 않은지 확인해야 한다. Operator Lifecycle Manager(OLM)나 Operator Hub는 설치와 생명주기 관리를 도와주지만, 운영 책임이 사라지는 것은 아니다.

GitOps는 배포 자동화보다 변경 통제에 가깝다

GitOps는 Git 저장소를 클러스터의 desired state로 두고, 실제 클러스터 상태와 비교해 동기화하는 운영 방식이다. ArgoCD는 이 모델을 Kubernetes에서 구현하는 대표적인 도구다.

일반적인 흐름은 다음과 같다.

1. 애플리케이션 코드 변경
2. CI에서 테스트와 이미지 빌드
3. 이미지가 Registrypush
4. 배포 저장소의 Helm values 또는 manifest image tag 변경
5. Pull Request 리뷰
6. mergeArgoCD가 변경 감지
7. 클러스터 상태가 Gitdesired state로 동기화

이 구조에서 중요한 점은 클러스터를 사람이 직접 고치는 일이 줄어든다는 것이다. 운영자가 kubectl edit deployment로 임의 수정하면 ArgoCD는 Git과 실제 상태의 차이를 drift로 감지한다. Sync Policy에 따라 자동으로 되돌리거나, 수동 승인 후 동기화할 수 있다.

ArgoCD 설치는 비교적 단순하다.

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

처음에는 외부 접근을 위해 argocd-server 서비스를 LoadBalancer로 바꿀 수 있다. 실습이나 로컬 확인에서는 port-forward가 더 안전하고 간단하다.

kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'

kubectl port-forward svc/argocd-server -n argocd 8080:443

초기 비밀번호는 Secret에서 확인하거나 CLI로 조회한다.

kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d

argocd admin initial-password -n argocd
argocd login <ARGOCD_SERVER>
argocd account update-password

Guestbook 예제를 배포하면 ArgoCD의 기본 동작을 확인할 수 있다.

kubectl config set-context --current --namespace=argocd

argocd app create guestbook \
  --repo https://github.com/argoproj/argocd-example-apps.git \
  --path guestbook \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace default

argocd app get guestbook
argocd app sync guestbook

운영 환경에서는 예제보다 저장소 구조가 더 중요하다. 애플리케이션 소스 저장소, 배포 설정 저장소, 플랫폼 애드온 저장소를 분리할지 결정해야 한다.

app-source-repo
- application code
- Dockerfile
- tests

app-deploy-repo
- Helm chart or manifests
- values-dev.yaml
- values-staging.yaml
- values-prod.yaml

platform-addons-repo
- ingress controller
- cert-manager
- external-secrets
- prometheus stack
- logging stack

저장소를 나누면 변경 책임을 분리하기 쉽지만, 흐름이 많아진다. 한 저장소에 모두 두면 단순하지만 권한과 리뷰 경계가 흐려질 수 있다. GitOps에서 중요한 것은 도구 이름보다 변경이 Pull Request로 남고, 승인된 설정만 클러스터에 반영되며, 롤백이 Git 히스토리와 연결되는 구조다.

ArgoCD Application도 이 구조에 맞춰 선언할 수 있다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-prod
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/deployments.git
    targetRevision: main
    path: apps/api/chart
    helm:
      valueFiles:
        - values-prod.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

prune: true는 Git에서 사라진 리소스를 클러스터에서도 제거하겠다는 의미다. 편리하지만 위험한 설정이기도 하다. 리소스 삭제가 필요한 구조라면 PR 리뷰에서 “새로 추가된 리소스”뿐 아니라 “사라지는 리소스”를 반드시 확인해야 한다. selfHeal: true는 클러스터에서 수동으로 바뀐 설정을 Git 기준으로 되돌린다. 운영자가 급하게 kubectl edit로 고친 내용도 다시 원복될 수 있으므로, 긴급 변경도 결국 Git에 반영하는 절차가 있어야 한다.

Terraform과 Ansible은 클러스터 이전 단계를 관리한다

Kubernetes 리소스는 GitOps로 관리할 수 있지만, 그 Kubernetes 클러스터 자체는 별도의 인프라 수명주기를 가진다. AKS나 EKS를 만들고, 노드 풀을 구성하고, 네트워크와 권한을 연결하고, kubeconfig를 발급하는 작업은 Terraform이 맡기 좋다.

resource "azurerm_resource_group" "platform" {
  name     = "rg-platform-prod"
  location = "koreacentral"
}

resource "azurerm_kubernetes_cluster" "main" {
  name                = "aks-platform-prod"
  location            = azurerm_resource_group.platform.location
  resource_group_name = azurerm_resource_group.platform.name
  dns_prefix          = "platform-prod"

  default_node_pool {
    name       = "system"
    node_count = 3
    vm_size    = "Standard_D2s_v5"
  }

  identity {
    type = "SystemAssigned"
  }
}

Terraform을 쓸 때 중요한 것은 “클러스터가 만들어졌다”가 아니라 같은 정의로 다시 만들 수 있는가다. 리소스 그룹, VNet/Subnet, AKS, ACR, Key Vault, Log Analytics Workspace, DNS, 권한 할당을 코드로 묶어두면 환경 재현성이 좋아진다. 반대로 Portal에서 수동으로 바꾼 설정이 늘어나면 Terraform state와 실제 리소스가 어긋나기 시작한다.

Ansible은 조금 다른 위치에 있다. 관리형 Kubernetes에서는 Ansible의 역할이 줄어들지만, VM 기반 클러스터나 Kubespray 같은 구성에서는 노드 준비 작업에 유용하다. 예를 들어 swap 비활성화, 커널 파라미터, 패키지 설치, SSH 접근, container runtime 설정은 선언형 인프라와 운영 스크립트 사이에 있다.

- name: Prepare Kubernetes nodes
  hosts: kube_nodes
  become: true
  tasks:
    - name: Disable swap
      command: swapoff -a

    - name: Enable bridge netfilter
      modprobe:
        name: br_netfilter
        state: present

    - name: Configure sysctl for Kubernetes networking
      copy:
        dest: /etc/sysctl.d/k8s.conf
        content: |
          net.bridge.bridge-nf-call-iptables = 1
          net.ipv4.ip_forward = 1

운영 관점에서는 Terraform과 Ansible, ArgoCD의 책임을 섞지 않는 것이 중요하다.

Terraform
- cloud resources
- Kubernetes cluster
- network, IAM, registry, logging workspace

Ansible
- VM node bootstrap
- OS package and kernel settings
- self-managed cluster preparation

ArgoCD / GitOps
- Kubernetes workloads
- Helm values and manifests
- platform add-ons

이 경계를 정해두면 장애가 났을 때도 질문이 명확해진다. 노드가 없거나 네트워크가 잘못되었다면 Terraform 영역이고, 노드 OS 설정이 맞지 않으면 Ansible 영역이며, Deployment나 Ingress가 Git과 다르면 ArgoCD 영역이다.

AKS 프로비저닝은 클러스터 생성보다 운영 입력값을 남기는 일이다

Terraform으로 AKS를 만들 때는 azurerm_kubernetes_cluster 리소스 하나만 보는 것으로 부족하다. 실제 운영 입력값은 네트워크, 노드 풀, ACR 연결, 로그 워크스페이스, Key Vault 접근, 업그레이드 정책, 권한 모델까지 함께 결정된다. 클러스터가 생성되는 것보다 중요한 것은 다음 환경에서도 같은 판단을 반복할 수 있게 코드와 변수로 남기는 것이다.

variable "environment" {
  type    = string
  default = "prod"
}

variable "location" {
  type    = string
  default = "koreacentral"
}

resource "azurerm_log_analytics_workspace" "platform" {
  name                = "log-platform-${var.environment}"
  location            = var.location
  resource_group_name = azurerm_resource_group.platform.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

resource "azurerm_kubernetes_cluster" "main" {
  name                = "aks-platform-${var.environment}"
  location            = var.location
  resource_group_name = azurerm_resource_group.platform.name
  dns_prefix          = "platform-${var.environment}"

  default_node_pool {
    name                = "system"
    vm_size             = "Standard_D2s_v5"
    node_count          = 3
    vnet_subnet_id      = azurerm_subnet.aks.id
    type                = "VirtualMachineScaleSets"
    orchestrator_version = null
  }

  identity {
    type = "SystemAssigned"
  }

  oms_agent {
    log_analytics_workspace_id = azurerm_log_analytics_workspace.platform.id
  }

  network_profile {
    network_plugin    = "azure"
    load_balancer_sku = "standard"
    outbound_type     = "loadBalancer"
  }
}

이 예시에서 핵심은 AKS 자체보다 주변 의존성이다. Log Analytics가 빠지면 장애 분석이 어려워지고, subnet을 명확히 지정하지 않으면 네트워크 정책과 IP 계획이 나중에 흔들린다. ACR, Key Vault, DNS Zone을 붙이는 순간부터 클러스터는 독립 리소스가 아니라 플랫폼의 일부가 된다.

Terraform 실행 흐름도 운영 절차로 남겨야 한다.

terraform init -upgrade
terraform fmt -recursive
terraform validate
terraform plan -out main.tfplan
terraform apply main.tfplan

az aks get-credentials \
  --resource-group rg-platform-prod \
  --name aks-platform-prod \
  --overwrite-existing

kubectl get nodes -o wide
kubectl get pods -A

terraform plan 결과에서 봐야 할 것은 생성 개수만이 아니다. 노드 풀 재생성, subnet 변경, identity 교체, role assignment 삭제처럼 장애로 이어질 수 있는 변경을 찾아야 한다. 특히 AKS는 노드 풀과 네트워크 설정이 바뀌면 단순 수정이 아니라 교체로 이어질 수 있다. 그래서 PR에서는 Terraform diff를 코드 리뷰의 일부로 다루는 편이 좋다.

state 관리도 빠뜨릴 수 없다. 로컬 state는 개인 실습에는 편하지만 협업 환경에서는 충돌과 유실 위험이 있다.

terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate-prod"
    storage_account_name = "sttfstateprod001"
    container_name       = "tfstate"
    key                  = "platform/aks-prod.tfstate"
  }
}

remote state를 사용하면 누가 언제 어떤 인프라 변경을 적용했는지 추적하기 쉬워진다. 다만 state 파일에는 리소스 식별자와 일부 민감한 값이 들어갈 수 있으므로 Storage Account 접근 권한도 최소화해야 한다.

Kubespray는 관리형 Kubernetes와 다른 질문을 던진다

AKS/EKS 같은 관리형 Kubernetes에서는 control plane 운영 부담을 클라우드가 가져간다. 반면 Kubespray로 VM 위에 클러스터를 구성하면 control plane, etcd, worker node, container runtime, 인증서, 업그레이드 절차를 직접 더 많이 이해해야 한다. 이 경험은 관리형 서비스를 쓰더라도 도움이 된다. 장애가 났을 때 “Kubernetes가 안 된다”가 아니라 어느 계층의 문제인지 더 빨리 좁힐 수 있기 때문이다.

self-managed cluster preparation

1. prepare Linux nodes and SSH access
2. disable swap and configure kernel modules
3. configure container runtime
4. define inventory for control plane and workers
5. run Kubespray playbook
6. verify kubeconfig and cluster components
7. apply CNI, storage class, ingress, monitoring

Kubespray의 장점은 반복 가능한 클러스터 설치를 Ansible inventory와 playbook으로 남길 수 있다는 점이다. 하지만 운영 책임도 함께 커진다. etcd 백업, 인증서 만료, control plane 업그레이드, 노드 OS 패치, CNI 호환성까지 직접 점검해야 한다.

[kube_control_plane]
cp-01 ansible_host=10.10.1.10
cp-02 ansible_host=10.10.1.11
cp-03 ansible_host=10.10.1.12

[kube_node]
worker-01 ansible_host=10.10.2.10
worker-02 ansible_host=10.10.2.11

[etcd:children]
kube_control_plane

[k8s_cluster:children]
kube_control_plane
kube_node

관리형 Kubernetes를 선택할지, 자체 구축을 선택할지는 기술 취향이 아니라 운영 모델의 문제다. 작은 팀이 빠르게 제품을 운영해야 한다면 AKS/EKS가 현실적이고, 네트워크와 OS 계층까지 강하게 통제해야 하는 환경이라면 자체 구축 또는 프라이빗 PaaS가 검토 대상이 된다. 어느 쪽이든 GitOps가 담당하는 범위와 인프라 프로비저닝이 담당하는 범위는 분리해두는 편이 안전하다.

배포 전략은 YAML 옵션이 아니라 장애 범위를 정하는 일이다

Kubernetes Deployment는 기본적으로 RollingUpdate를 지원한다. 새 ReplicaSet을 만들고, 새 파드가 준비되면 기존 파드를 줄인다.

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

maxUnavailable: 0은 배포 중에도 기존 가용 파드를 줄이지 않겠다는 의미이고, maxSurge: 1은 일시적으로 하나 더 띄울 수 있다는 뜻이다. 이 설정은 리소스 여유가 필요하지만, 트래픽이 있는 서비스에서는 안정적이다.

Blue/Green이나 Canary가 필요하면 Argo Rollouts 같은 도구를 검토할 수 있다. Canary는 새 버전에 일부 트래픽만 보내고 메트릭을 보며 점진적으로 늘리는 방식이다. 이때 Prometheus와 연계해 에러율이나 latency를 기준으로 자동 중단할 수도 있다. 중요한 것은 “Canary를 쓴다”가 아니라, 어떤 지표가 나빠지면 배포를 멈출지 정하는 것이다.

Canary decision signals
- 5xx error rate
- p95 / p99 latency
- pod restart count
- CPU throttling
- business transaction failure

관측성은 배포 이후가 아니라 리소스 설계에 들어가야 한다

Kubernetes에서 문제를 볼 때는 Pod 상태만으로 부족하다. Deployment rollout, Service endpoint, Ingress routing, Node resource, 애플리케이션 로그, 메트릭, 트레이스를 함께 봐야 한다.

kubectl get deploy,rs,pod,svc,ingress
kubectl get endpoints api
kubectl top pod
kubectl top node
kubectl describe ingress api
kubectl logs deploy/api -f

Prometheus/Grafana는 메트릭을 보고, Loki나 OpenSearch는 로그 검색을 돕고, Tempo나 Jaeger는 분산 추적을 제공한다. OpenTelemetry를 애플리케이션에 붙이면 HTTP 요청, DB 쿼리, 외부 API 호출을 하나의 trace로 따라갈 수 있다.

서비스 메시인 Istio는 mTLS, 트래픽 정책, retry, timeout, circuit breaking, telemetry를 제공한다. 다만 처음부터 메시를 넣으면 운영 복잡도가 커진다. 먼저 Service, Ingress, Probe, Resource, 로그/메트릭이 안정적으로 정리된 뒤 트래픽 제어 요구가 분명해졌을 때 검토하는 편이 낫다.

정리

Kubernetes는 컨테이너를 띄우는 도구에서 출발하지만, 실제로는 애플리케이션 운영 상태를 선언하고 조정하는 플랫폼에 가깝다. Deployment는 롤아웃과 복구 단위를 정의하고, Service와 Ingress는 파드 교체를 숨기며, ConfigMap과 Secret은 설정을 이미지에서 분리한다. Helm은 환경별 차이를 관리하고, Operator는 반복되는 운영 절차를 컨트롤러로 옮긴다.

GitOps와 ArgoCD는 배포 버튼을 하나 더 만드는 방식이 아니라, 변경 이력과 클러스터 상태를 Git 기준으로 맞추는 운영 모델이다. 이 모델이 제대로 동작하려면 YAML을 많이 쓰는 것보다 더 기본적인 질문을 계속 해야 한다. 어떤 상태를 desired state로 둘 것인가, 누가 변경을 승인할 것인가, drift가 생겼을 때 되돌릴 것인가, 어떤 지표가 나빠지면 배포를 멈출 것인가.

결국 Kubernetes 운영은 “배포 성공”에서 끝나지 않는다. 요청이 들어오고, 파드가 준비 상태가 되고, 서비스가 트래픽을 전달하고, 배포가 점진적으로 진행되고, 문제가 생겼을 때 로그와 메트릭으로 원인을 좁힐 수 있어야 한다. 그 전체 흐름을 Git과 선언적 설정으로 관리할 때 Kubernetes는 단순 실행 환경을 넘어 플랫폼이 된다.

이 글의 무단 복제, 재배포, 자동 수집을 허용하지 않습니다. 인용이 필요한 경우 출처와 원문 링크를 함께 남겨주세요.