레이블이 Kubernetes인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Kubernetes인 게시물을 표시합니다. 모든 게시물 표시

20250101

GitLab CI/CD (Docker, Kubernetes)

사내에서 GitLab CI/CD 파이프라인을 Docker + Kubernetes 조합으로 다시 짜면서 정리한 글. 이전엔 젠킨스 Freestyle job 써왔는데 팀 규모 커지면서 Git 리포에 CI 정의가 같이 들어가는 쪽이 관리가 훨씬 편하고, 리뷰도 받을 수 있어서 넘어왔다. GitLab 17 기준이다.

한 번 밑바닥부터 훑어보고 싶은 사람을 위해, 도구 설치부터 쿠버 배포까지 쭉 정리. 실제 프로덕션 파이프라인은 이거보다 stage 분리가 더 들어가 있다 (lint → unit → build → security scan → deploy). 여기선 뼈대만.

1. 사전 준비: 필수 도구 설치 및 설정

1.1 Git 설치

Ubuntu

sudo apt update
sudo apt install git -y

macOS

brew install git

Windows

Git 공식 사이트에서 인스톨러 받아서 실행.

설치 확인:

git --version

1.2 Node.js 설치

Ubuntu

sudo apt update
sudo apt install -y nodejs npm

macOS

brew install node

Windows

Node.js 공식 사이트에서 인스톨러 받음.

node --version
npm --version

1.3 Docker 설치

Docker 27 기준. 우분투에서는 공식 apt 저장소 쓰는 게 장기적으론 편한데 빠르게 돌려볼 땐 docker.io 패키지로도 충분.

Ubuntu

sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker

macOS / Windows

Docker Desktop 받아서 설치. Mac이면 OrbStack도 선택지다 (가볍고 빠름).

docker --version
sudo usermod -aG docker $USER

1.4 Kubernetes 설치 (kubectl)

Kubernetes 1.31 클러스터에 붙인다는 가정으로 kubectl을 받는다. 클라이언트 버전은 서버와 ±1 마이너 버전 안에서 맞추는 게 안전함.

curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
kubectl version --client

2. 프로젝트 초기화

2.1 Git 리포지토리 생성

mkdir my-app && cd my-app
git init

2.2 Node.js 프로젝트 초기화

npm init -y

생성되는 package.json 예시:

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {}
}

2.3 간단한 Node.js 앱

가장 미니멀한 HTTP 서버. 헬스체크 용도로도 딱 좋다.

const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello, Kubernetes!");
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

3. Docker 파일 설정

3.1 Dockerfile

팁: 실제 운영에선 node:16 같은 full 이미지 말고 node:20-alpine 쓰는 게 용량이 훨씬 작다. 여기선 호환성 우선.

# 베이스 이미지
FROM node:16

# 앱 디렉토리 생성
WORKDIR /usr/src/app

# 종속성 복사 및 설치
COPY package*.json ./
RUN npm install

# 애플리케이션 복사
COPY . .

# 애플리케이션 실행
EXPOSE 3000
CMD ["npm", "start"]

3.2 로컬 빌드 / 실행

# Docker 이미지 빌드
docker build -t my-app .

# Docker 컨테이너 실행
docker run -p 3000:3000 my-app

4. Kubernetes 배포 파일

4.1 Deployment + Service

replicas: 2 로 해두면 롤링 업데이트 시에도 최소 1개는 살아 있다. LoadBalancer 타입은 클라우드(EKS, GKE, NKS 등)에서 쓰고, 로컬 minikube 환경이면 NodePort로 바꿔야 한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: my-app:latest
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  type: LoadBalancer
  ports:
  - port: 3000
    targetPort: 3000
  selector:
    app: my-app

4.2 배포 명령

# Kubernetes 클러스터에 배포
kubectl apply -f k8s-deployment.yaml

# 상태 확인
kubectl get pods
kubectl get services

5. GitLab CI/CD 구성

5.1 .gitlab-ci.yml

아래는 뼈대. 주의할 부분은 docker build를 러너 안에서 돌리려면 러너 자체가 Docker-in-Docker (dind) 세팅이거나 호스트 소켓이 마운트돼 있어야 함. 사내에선 shell executor 러너 한 대를 빌드 전용으로 분리해서 쓴다.

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - docker build -t my-app:$CI_COMMIT_SHA .

test:
  stage: test
  script:
    - echo "Running tests..."
    - npm test || echo "No tests configured yet"

deploy:
  stage: deploy
  script:
    - echo "$KUBE_CONFIG" | base64 -d > kubeconfig.yaml
    - export KUBECONFIG=kubeconfig.yaml
    - kubectl apply -f k8s-deployment.yaml
  only:
    - main

5.2 환경 변수

GitLab UI의 Settings → CI/CD → VariablesKUBE_CONFIG를 base64 인코딩한 값으로 넣는다. Masked 체크박스 꼭 켤 것. 로그에 평문 노출 방지용.

6. 파이프라인 실행

git add .
git commit -m "Add CI/CD pipeline"
git push origin main

프로젝트 페이지 → CI/CD → Pipelines에서 실행 상태 확인. 실패하면 job 로그 직접 뜯어본다. "왜 안 되지" 싶을 때 95%는 러너 권한 문제거나 kubeconfig base64 인코딩 오류다.

7. 배포 확인

kubectl get pods
kubectl get services

LoadBalancer IP가 할당되면 브라우저에서 접속해서 "Hello, Kubernetes!" 뜨면 끝.

실전에서 추가로 고민하게 되는 것들

이 기본 파이프라인을 팀에 도입한 뒤에 결국 붙이게 되는 것들 목록이다.

  • 이미지 태그를 $CI_COMMIT_SHA로 고정하고 latest는 배포에 쓰지 않기 (롤백 불가 방지)
  • PR용 리뷰 environment 자동 생성 (environment: review/$CI_COMMIT_REF_NAME)
  • Trivy로 이미지 취약점 스캔 stage 추가
  • kubectl 대신 Helm으로 values 관리
  • main 머지 시 자동 배포는 staging만, 운영은 when: manual

처음부터 다 넣지는 말고, 실제로 아픈 순서대로 붙이는 게 낫다. 나는 순서대로 하나씩 아프면서 붙였음.

20210704

Kubernetes HPA custom metrics

배경

대부분 서비스는 CPU 기반 HPA로도 충분한데, 큐 컨슈머 성격의 서비스는 CPU 여유 있어도 큐 적체가 쌓일 수 있음. Redis stream 쌓인 길이 기준으로 확장하게 하려고 custom metrics 세팅.

스택

  • Kubernetes 1.20
  • prometheus-adapter로 external metrics 노출
  • Prometheus에 redis_exporter 메트릭 이미 수집 중

prometheus-adapter config

rules:
  external:
  - seriesQuery: 'redis_stream_length{job="redis-exporter"}'
    resources:
      template: "<<.Resource>>"
    name:
      matches: "^(.*)$"
      as: "stream_length"
    metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>}) by (stream)'

HPA manifest

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: order-worker
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-worker
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: External
    external:
      metric:
        name: stream_length
        selector:
          matchLabels:
            stream: "orders"
      target:
        type: AverageValue
        averageValue: "50"

스트림에 50 이상 쌓이면 파드 하나씩 늘어감. 즉시성이 너무 강하면 flap 일어나니까 behavior.scaleDown.stabilizationWindowSeconds를 300으로 걸어둠. 2beta2부터 scaling behavior 지정 가능해서 이게 큼.

배운 점

  • 초반에 metric name 매핑이 꼬여서 HPA가 "unknown metric" 띄움. kubectl get --raw로 adapter endpoint 직접 쳐보는 게 제일 빠른 디버깅.
  • averageValue 단위 설명이 애매한데, pod 개수 나눈 평균. 즉 pod당 목표 수치.
  • 큐 기반 워커는 scale-up은 빠르게, scale-down은 천천히가 정답. 일 중간에 갑자기 절반 줄면 진행 중 작업 취소 이슈 생김.

다음 단계: VPA랑 같이 돌릴 수 있는지 실험. 메모리는 VPA, replicas는 HPA 조합이 좋을지.

20200322

Kubernetes prod 장애 회고 — OOM

지난주 목요일 오전 9시 30분, prod 클러스터 중 하나에서 결제 관련 파드들이 줄줄이 죽기 시작했다. OOMKilled. 30분간 결제 실패율이 치솟았고, 우리는 그 30분 동안 장님이었다.

회고 정리.

무슨 일이 있었나

한 서비스가 점점 메모리를 먹다가 limit(1Gi) 찍고 죽음. 재시작. 다시 먹음. 죽음. 이게 10분마다 반복. 처음엔 로드 스파이크 때문인 줄 알았는데 트래픽은 오히려 평소보다 낮았다.

원인

어제 배포한 PR에 있던 캐시 로직. 요청별 temp 데이터를 모듈 레벨 dict에 쌓는데, 만료 로직이 잘못돼서 삭제 안 되고 계속 쌓임. 배포 직후엔 쌓이는 속도가 느려서 테스트 환경에서 안 걸림. 12시간 쯤 지나니 1Gi 넘음.

코드 대략 이런 모양이었다.

_cache = {}

def get_or_set(key, fn, ttl=60):
    now = time.time()
    if key in _cache:
        val, exp = _cache[key]
        if exp > now:
            return val
    val = fn()
    _cache[key] = (val, now + ttl)  # 만료된 key는 영원히 안 지워짐
    return val

TTL 체크만 있고 eviction이 없다. key가 매 요청마다 유니크(request id가 섞여있었음)했기 때문에 증가만 함. 아 이건 진짜 허무한 버그.

왜 바로 못 잡았나

1) 메모리 알람 임계치가 85%인데 pod은 limit에 닿으면 그냥 kill. 알람이 울릴 시간이 없음.

2) prometheus 수집 주기 30초라 짧은 라이프사이클 pod의 메모리 그래프가 "어? 막 생겼다 사라지네" 수준으로만 보임.

3) 로그에 OOMKilled 로그 이벤트 집계 대시보드가 없었음. kubectl describe pod으로 일일이 봐야 알 수 있었다.

고친 것

  • 해당 캐시 로직을 cachetools TTLCache로 교체 (내부에 eviction 있음)
  • kube_pod_container_status_last_terminated_reason="OOMKilled" 메트릭을 alertmanager에 등록
  • 메모리 limit의 80% 닿으면 pre-alert (scrape 주기도 15초로)
  • 모든 서비스에 GOMEMLIMIT 혹은 resource 기반 인메모리 캐시 상한 명시 규칙 추가

개인적 교훈

"모듈 레벨 dict에 뭐 쌓는 순간 이게 무한 성장 가능한지 자문해라." 작년 이맘때도 비슷한 거 한 번 냈었다. 같은 실수 반복. 그래서 이번엔 린트 룰로라도 박을까 고민 중. 아니면 최소한 코드리뷰 체크리스트에 넣든가.

OOM은 보이지 않는 버그라 무섭다. 부하 테스트만으로는 못 잡고 시간이 지나야 드러난다.

20190903

Kubernetes 1.16 custom controller

1.16 알파/베타 올라오면서 CRD v1 승격 얘기가 나오는 중. 사내에 운영 반복되는 패턴(예: 환경별 secret rotation, 특정 ConfigMap 자동 동기화) 몇 개를 custom controller로 빼는 작업.

kubebuilder 2.0 써서 세팅.

kubebuilder init --domain example.com
kubebuilder create api --group ops --version v1alpha1 --kind SecretSync

Reconcile 함수 구조는 결국 이거다.

func (r *SecretSyncReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := context.Background()
    var sync opsv1.SecretSync
    if err := r.Get(ctx, req.NamespacedName, &sync); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }
    // 현재 상태 측정
    // 목표 상태와 diff
    // 적용
    return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

배운 점 두 개.

1) controller는 idempotent해야 한다. 같은 입력이 100번 와도 같은 결과. 절대 increment 같은 로직 넣지 말 것.

2) 에러 리턴하면 exponential backoff로 재시도된다. 재시도 원하는 거면 error, 아니면 RequeueAfter. 이거 헷갈리면 무한 재시도 또는 무반응.

CRD v1 정식은 1.16 GA에서. v1beta1은 deprecation 예정인데 우리처럼 지금 만들면 v1beta1로 생성해놓고 나중에 변환 웹훅으로 옮겨야 함. 귀찮.

20180726

Kubernetes Helm chart 작성 팁

Helm 2.9 + K8s 1.10 클러스터 기준. 팀에서 차트 3개 관리하다 보니 패턴이 보여서 공유 메모. tiller 이슈까지 겸해서 정리.

1. values 기본값은 values.yaml에 주석으로. 주석 없으면 그 값이 뭘 의미하는지 아무도 모른다. README 써도 읽지 않는다. 사용자가 가장 먼저 여는 파일인 values.yaml 자체를 문서로 본다는 관점으로.

# -- 이미지 레지스트리 정보
image:
  repository: registry.local/app
  # -- 빈 문자열이면 Chart.AppVersion 사용. 배포 파이프라인에서 override 권장
  tag: ""
  pullPolicy: IfNotPresent

# -- 레플리카 수. HPA 쓰면 initial로만 의미
replicaCount: 2

# -- JVM/파이썬 등 런타임 힙 설정은 resources.limits.memory 기준으로 80% 잡기
resources:
  limits: { cpu: 500m, memory: 512Mi }
  requests: { cpu: 100m, memory: 256Mi }

2. _helpers.tpl에 라벨 정의. helm create가 뱉는 기본 템플릿을 그대로 쓰지 말고 직접 유지. 중요한 건 selector 라벨과 metadata 라벨의 분리. selector는 변경하면 기존 Deployment의 pod selector가 mismatch 나서 업그레이드가 깨진다. chart, version 같은 자주 변하는 값은 meta 라벨에만.

{{/* Selector — 절대 바꾸면 안 됨 */}}
{{- define "app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/* Metadata — 정보용. 바뀌어도 롤아웃 안 깨짐 */}}
{{- define "app.labels" -}}
{{ include "app.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- end }}

Deployment의 spec.selector.matchLabels에는 selectorLabels만. metadata.labels와 template.metadata.labels엔 labels 전체. 이 두 줄 차이를 무시하고 합쳐 썼다가 다음 마이너 bump에서 "selector is immutable" 에러 만나 배포 막히는 케이스 흔함.

3. ConfigMap 변경 시 Pod 자동 재시작. 알아두기 전까진 "왜 설정 바꿨는데 반영이 안 되지" 하면서 pod delete 수동으로 하다가 사고 치기 쉽다. 해시를 파드 템플릿 어노테이션에 박아 두면 ConfigMap 변경 → 템플릿 해시 변경 → Deployment rolling update 자동 트리거.

# Deployment spec.template.metadata.annotations
annotations:
  checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
  checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}

원리: Pod 템플릿의 PodTemplateSpec 해시가 바뀌면 controller-manager의 deployment_controller가 신규 ReplicaSet을 생성하고 기존 것을 점진 축소(rolling update). configmap/secret 자체는 mount된 pod에 eventually 반영되지만 프로세스가 재시작되지 않으면 환경변수나 초기 로드만 쓰는 앱은 감지 못함. 이 해시 트릭이 rolling update를 강제하는 관용구.

4. 의존성 차트 관리. requirements.yaml로 redis/postgres subchart를 묶어 쓰는 경우, alias와 condition 적극 활용.

# requirements.yaml
dependencies:
  - name: postgresql
    version: 3.10.0
    repository: https://kubernetes-charts.storage.googleapis.com
    condition: postgresql.enabled
    alias: db

condition으로 외부 RDS를 쓸지 in-cluster PG를 쓸지 스위치. alias로 여러 벌 띄우기도 가능. 스테이징만 in-cluster, 프로덕션은 condition=false.

5. hook 순서 이해. pre-install, post-install, pre-upgrade, post-upgrade 훅과 helm.sh/hook-weight 조합. DB 마이그레이션 Job은 pre-upgrade로 두고, 차트가 실패하면 helm.sh/hook-delete-policy: before-hook-creation로 재시도 가능하게. 레시피가 몇 개 있다.

  • DB 마이그 Job: pre-upgrade, weight -5
  • 인덱스 생성 Job: post-upgrade, weight 0
  • 스모크 테스트 Job: post-upgrade, weight 10 (hook-succeeded면 삭제)

6. --force는 죄악. helm upgrade --force가 자주 필요하다면 차트 설계 오류다. immutable selector를 바꾸는 게 가장 흔한 원인. --force는 배포된 리소스를 kubectl replace 하는 거라 PVC 바인딩이나 외부 LB 엔드포인트가 뜯어질 수 있다.

7. 값 검증. Helm 2에는 values 스키마 기능이 아직 없다. 대신 required "x is required" .Values.foo나 필요하면 _helpers.tpl에 fail 헬퍼 써서 차트 렌더 단계에서 실패시킨다. Helm 3에는 JSON Schema 기반 validation이 들어온다는 루머라 기대 중.

tiller 고민

Helm 2의 가장 큰 통증. tiller가 클러스터 전역에 cluster-admin 권한으로 떠서 RBAC 정책이 엉성하면 보안상 문제. namespace 단위 tiller로 제한하거나 helm template | kubectl apply로 tiller를 우회하는 운영이 늘고 있다. Helm 3에서 tiller가 빠진다는 로드맵이 나와 있어서 2.10/2.11까지만 2.x 유지하고 3.0 RC 시점에 전환 계획.

운영 팁. helm history, helm rollback 자주 쓰게 되는데 기본적으로 deployment revision과 helm revision이 다름을 기억. kubectl rollout undo는 deployment 레벨 이전 ReplicaSet으로 돌리는 거고, helm rollback은 차트의 이전 릴리스로 돌리는 거. 둘이 엇갈리면 헷갈린다. 쉬운 규칙: "helm으로 올렸으면 helm으로 되돌리기".

RBAC 매번 뜯어고치는 게 지겨워서 chart-testing(ct) + 사내용 기본 RBAC 템플릿을 _helpers.tpl로 만들어 두는 중. 신규 차트 만들 때 이 기반에서 파생.

20170821

Kubernetes Ingress 도입기

지난번 k8s 이관 이후 쌓여있던 숙제. ingress를 제대로 도입함. 그동안은 서비스마다 type: LoadBalancer 로 띄워서 비용이 새고 있었음.

선택은 nginx-ingress-controller. traefik도 고려했는데 사내 nginx 경험 많아서 디버깅 편한 쪽으로.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-body-size: 20m
spec:
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /v1
        backend:
          serviceName: api-v1
          servicePort: 80
      - path: /v2
        backend:
          serviceName: api-v2
          servicePort: 80

인증서는 cert-manager 도입. Let's Encrypt로 자동 발급/갱신. ingress에 annotation 하나 달고 Issuer 만들면 끝. 수동 인증서 발급하던거랑 비교하면 정말 편함.

metadata:
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"

뻘짓한 포인트:

1. proxy-body-size 기본 1m. 업로드 API에서 413 뜸. annotation으로 늘려야 함. 이거 찾는데 한 시간.

2. rewrite-target. path: /v1 로 매칭했을때 백엔드에는 / 만 전달하려면 rewrite 필요. 아니면 백엔드가 /v1/* 를 받게 되어 라우팅 꼬임.

3. health check. ingress-controller 자체가 죽지 않게 PDB (PodDisruptionBudget) 걸어둠. 노드 drain 때 전체 다운 안 되도록.

4. sticky session. 세션 고정이 필요한 서비스는 annotation:

nginx.ingress.kubernetes.io/affinity: cookie
nginx.ingress.kubernetes.io/session-cookie-name: sticky

이관 후 LoadBalancer 인스턴스 개수 7개 → 1개 (ingress용). 비용 2/3 절감. 배포 유연성도 올라감. 도메인/경로 기반 라우팅을 Ingress 리소스로 선언적으로 관리하니 리뷰/기록이 깔끔.

다음 숙제는 external-dns로 Route53 레코드까지 자동화. 점점 GitOps 방향으로 가는 중.

20170408

Docker Swarm 운영 vs K8s 이관

6개월 전 docker swarm mode (1.12 부터 통합된 그거)로 올려놨던 서비스 몇개. 이제 k8s로 이전하는 중. 왜 바꿨는지 정리.

swarm 좋았던 점:

  • docker compose 익숙하면 러닝커브 제로 수준. docker stack deploy -c docker-compose.yml mystack
  • 기본 세팅이 빠름. docker swarm init 한 줄
  • built-in routing mesh — 어느 노드로 요청 오든 해당 서비스 컨테이너로 라우팅

swarm 한계:

  • ingress controller / 복잡한 라우팅 규칙 제한적
  • configmap은 있는데 secret rotation 같은 운영 기능이 덜 무르익음
  • helm 같은 패키지 매니저 생태계 없음. 직접 stack 파일 관리
  • 커뮤니티 확장이 k8s 대비 훨씬 적음. 모니터링/로깅 도구 연동이 번거로움
  • 대규모 클러스터 (수백 노드) 운영 사례가 많지 않음

k8s 쪽에서 지금 쓰는 것들:

  • deployment / service / ingress
  • configmap / secret
  • helm chart (prometheus, grafana 등 기본 스택)
  • nginx-ingress-controller
  • cert-manager (let's encrypt 자동 발급)

마이그 진행방식: 서비스별로 helm chart 만들고, stage에서 동시에 swarm/k8s에 띄워놓고 비교 → k8s 쪽으로 DNS 스위치 → swarm 측 철수. 한번에 넘기지 않고 점진적으로.

빡셌던 건 ingress 쪽. swarm의 routing mesh처럼 암묵적 분배가 되는게 아니라 명시적 ingress 리소스 만들고 rule 정의 해야됨. 처음엔 귀찮은데 익숙해지면 강력함.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: web
          servicePort: 80

결론: 소규모 / 단순 케이스에서는 swarm이 여전히 간편하고 충분. 근데 조금만 요구사항 커져도 k8s로 가는게 장기적으로 덜 피곤. 커뮤니티 모멘텀이 k8s로 완전히 쏠린게 체감됨. 회사 표준도 k8s로 맞출 계획.

20160822

Kubernetes 1.3 실사용 감상

Kubernetes 1.3 올라왔고 회사에서 파일럿 클러스터 구축 지시. 몇 주 써본 소감.

환경: 자체 서버 3노드 (kubeadm 아직 공식 아님 → 1.4 쯤 추가 예정이라는 얘기. 우리는 kube-up.sh 대신 수동 설치). ubuntu 14.04 위에 etcd, kube-apiserver 등 수동 배치. 설치가 정말 빡셈.

처음 부딪힌 개념들:

  • Pod — 컨테이너의 단위라기보다 "같은 lifecycle을 공유하는 컨테이너 그룹"
  • ReplicationController → 1.2부터 ReplicaSet + Deployment로 바뀜. 이제 Deployment 쓰면 됨
  • Service — Pod의 IP가 바뀌어도 안정적 접근점 제공. ClusterIP / NodePort / LoadBalancer
  • ConfigMap / Secret — 1.2부터 공식 stable. 환경변수/설정파일 주입

간단한 deploy + service yaml:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  template:
    metadata:
      labels: { app: web }
    spec:
      containers:
      - name: web
        image: registry.internal/web:1.2.3
        ports: [{ containerPort: 8080 }]
---
apiVersion: v1
kind: Service
metadata: { name: web }
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: 8080 }]
  type: ClusterIP

kubectl apply -f deploy.yaml 로 반영.

좋은 점: 롤링 업데이트가 진짜 쉬움. 이미지 태그만 바꿔 kubectl set image 하면 자동으로 순차 교체. health check 기반이라 새 pod 준비된 다음에 예전 pod 삭제.

빡센 점:

  • 네트워킹. 오버레이 네트워크 필수 (우리는 flannel). CNI 세팅 안 맞으면 Pod끼리 통신 안 됨
  • 영속 볼륨. PV/PVC 개념이 복잡. NFS로 대충 붙여놨는데 production은 더 고민해야 함
  • Ingress는 1.3 기준 아직 베타. nginx ingress controller 따로 띄워서 검토중
  • 모니터링/로그. 기본 툴 부족. Heapster + Grafana 세팅 권장

학습 곡선이 진짜 가파른데, 갖춰놓고 나면 배포는 너무 편함. 3개월쯤 운영해보고 본서비스 이관 여부 판단 예정. 지금은 Docker Compose + swarm 조합이랑 병행.