서론: 효율적인 Kubernetes 애플리케이션 배포

지금까지 Kubernetes의 다양한 리소스와 설정 방법을 배웠습니다. 하지만 실제 프로덕션 환경에서는 수십, 수백 개의 YAML 파일을 관리해야 하고, 환경별로 다른 설정을 적용해야 합니다. 이러한 복잡성을 해결하기 위해 Helm이라는 패키지 매니저가 등장했습니다.

또한 현대의 소프트웨어 개발에서는 코드 변경부터 프로덕션 배포까지의 과정을 자동화하는 CI/CD 파이프라인이 필수입니다. 이번 편에서는 Helm의 활용법과 GitOps 기반의 CI/CD 파이프라인 구축 방법을 상세히 알아보겠습니다.

1. Helm이란?

1.1 Helm 개요

Helm은 Kubernetes의 패키지 매니저입니다. Linux의 apt, yum이나 macOS의 Homebrew처럼, Helm은 Kubernetes 애플리케이션을 쉽게 설치, 업그레이드, 관리할 수 있게 해줍니다.

Helm의 핵심 개념:

  • Chart: Kubernetes 애플리케이션을 정의하는 파일들의 묶음
  • Release: 클러스터에 설치된 Chart의 인스턴스
  • Repository: Chart를 저장하고 공유하는 저장소
  • Values: Chart의 기본 설정을 커스터마이징하는 값들

1.2 Helm을 사용하는 이유

문제점 Helm의 해결책
수많은 YAML 파일 관리 단일 Chart로 패키징
환경별 설정 차이 values.yaml로 오버라이드
배포 이력 관리 릴리스 버전 관리 및 롤백
복잡한 의존성 Chart 의존성 관리
재사용성 부족 템플릿과 공유 가능한 Chart

2. Helm 설치와 기본 사용법

2.1 Helm 설치

# macOS
brew install helm

# Linux (스크립트)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Windows (Chocolatey)
choco install kubernetes-helm

# 설치 확인
helm version

2.2 Repository 관리

# 공식 Helm Chart 저장소 추가
helm repo add stable https://charts.helm.sh/stable
helm repo add bitnami https://charts.bitnami.com/bitnami

# 저장소 목록 확인
helm repo list

# 저장소 업데이트
helm repo update

# Chart 검색
helm search repo nginx
helm search repo mysql

# Hub에서 검색
helm search hub prometheus

2.3 기본 명령어

# Chart 설치
helm install my-release bitnami/nginx

# 특정 네임스페이스에 설치
helm install my-release bitnami/nginx -n my-namespace --create-namespace

# values 파일로 설치
helm install my-release bitnami/nginx -f custom-values.yaml

# 명령행에서 값 지정
helm install my-release bitnami/nginx --set replicaCount=3

# 릴리스 목록 확인
helm list
helm list -A  # 모든 네임스페이스

# 릴리스 상태 확인
helm status my-release

# 릴리스 업그레이드
helm upgrade my-release bitnami/nginx --set replicaCount=5

# 릴리스 롤백
helm rollback my-release 1  # 리비전 1로 롤백

# 릴리스 이력 확인
helm history my-release

# 릴리스 삭제
helm uninstall my-release

3. Chart 구조 이해

3.1 Chart 디렉토리 구조

my-chart/
├── Chart.yaml          # Chart 메타데이터
├── values.yaml         # 기본 설정 값
├── charts/             # 의존성 Chart
├── templates/          # Kubernetes 매니페스트 템플릿
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── _helpers.tpl    # 템플릿 헬퍼 함수
│   ├── NOTES.txt       # 설치 후 출력될 메시지
│   └── tests/          # 테스트 파일
│       └── test-connection.yaml
├── .helmignore         # 패키징 시 제외할 파일
└── README.md           # 문서

3.2 Chart.yaml

# Chart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
version: 1.0.0          # Chart 버전
appVersion: "2.0.0"     # 애플리케이션 버전

# 의존성 정의
dependencies:
  - name: mysql
    version: "9.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: mysql.enabled
  - name: redis
    version: "17.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled

# 키워드 및 메타데이터
keywords:
  - web
  - application
home: https://example.com
sources:
  - https://github.com/example/my-app
maintainers:
  - name: DevOps Team
    email: devops@example.com

3.3 values.yaml

# values.yaml
# 기본 설정 값 정의

# 애플리케이션 설정
replicaCount: 2

image:
  repository: myregistry/my-app
  tag: "latest"
  pullPolicy: IfNotPresent

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

# 서비스 설정
service:
  type: ClusterIP
  port: 80
  targetPort: 8080

# Ingress 설정
ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: myapp-tls
      hosts:
        - myapp.example.com

# 리소스 제한
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 128Mi

# 오토스케일링
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

# 환경 변수
env:
  - name: APP_ENV
    value: production
  - name: LOG_LEVEL
    value: info

# 의존성 활성화
mysql:
  enabled: true
  auth:
    database: myapp
    username: appuser

redis:
  enabled: false

3.4 템플릿 작성

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          {{- if .Values.env }}
          env:
            {{- toYaml .Values.env | nindent 12 }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5

templates/_helpers.tpl

{{/*
애플리케이션 이름 생성
*/}}
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
전체 이름 생성 (릴리스 이름 포함)
*/}}
{{- define "my-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
공통 라벨
*/}}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ include "my-app.chart" . }}
{{ include "my-app.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
셀렉터 라벨
*/}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Chart 이름과 버전
*/}}
{{- define "my-app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

4. values.yaml 커스터마이징

4.1 환경별 values 파일

# 환경별 values 파일 구성
values/
├── values.yaml           # 기본 값
├── values-dev.yaml       # 개발 환경
├── values-staging.yaml   # 스테이징 환경
└── values-prod.yaml      # 프로덕션 환경

values-prod.yaml

# values-prod.yaml - 프로덕션 환경 설정
replicaCount: 5

image:
  tag: "1.2.3"  # 특정 버전 고정

resources:
  limits:
    cpu: 1000m
    memory: 1Gi
  requests:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70

ingress:
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: api-prod-tls
      hosts:
        - api.example.com

env:
  - name: APP_ENV
    value: production
  - name: LOG_LEVEL
    value: warn
  - name: DB_HOST
    valueFrom:
      secretKeyRef:
        name: db-credentials
        key: host
# 여러 values 파일 조합
helm install my-app ./my-chart \
  -f values.yaml \
  -f values-prod.yaml \
  --set image.tag=1.2.4

4.2 템플릿 렌더링 확인

# 렌더링된 템플릿 확인 (실제 배포 없이)
helm template my-release ./my-chart -f values-prod.yaml

# 특정 템플릿만 확인
helm template my-release ./my-chart -s templates/deployment.yaml

# 디버그 모드
helm install my-release ./my-chart --dry-run --debug

5. 유용한 Helm Charts

5.1 Prometheus 스택 설치

# prometheus-community 저장소 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# kube-prometheus-stack 설치 (Prometheus + Grafana + AlertManager)
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.adminPassword=admin123

커스텀 values 파일

# prometheus-values.yaml
prometheus:
  prometheusSpec:
    retention: 15d
    resources:
      requests:
        memory: 2Gi
        cpu: 500m
      limits:
        memory: 4Gi
        cpu: 1000m
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: standard
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 50Gi

grafana:
  adminPassword: "secure-password"
  persistence:
    enabled: true
    size: 10Gi
  ingress:
    enabled: true
    hosts:
      - grafana.example.com
    tls:
      - secretName: grafana-tls
        hosts:
          - grafana.example.com

alertmanager:
  alertmanagerSpec:
    storage:
      volumeClaimTemplate:
        spec:
          storageClassName: standard
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 10Gi

5.2 Grafana 단독 설치

# Grafana 저장소 추가
helm repo add grafana https://grafana.github.io/helm-charts

# Grafana 설치
helm install grafana grafana/grafana \
  --namespace monitoring \
  --set persistence.enabled=true \
  --set adminPassword='admin123'

5.3 기타 유용한 Charts

# Nginx Ingress Controller
helm install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace

# cert-manager (TLS 인증서 자동화)
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set installCRDs=true

# Redis
helm install redis bitnami/redis \
  --set auth.password=mypassword

# PostgreSQL
helm install postgresql bitnami/postgresql \
  --set auth.postgresPassword=mypassword

# Elasticsearch
helm install elasticsearch elastic/elasticsearch \
  --set replicas=3

6. CI/CD 개념 복습

6.1 CI/CD란?

  • CI (Continuous Integration): 코드 변경을 자주 통합하고, 자동화된 빌드와 테스트를 수행
  • CD (Continuous Delivery): 프로덕션에 배포할 준비가 된 상태를 항상 유지
  • CD (Continuous Deployment): 모든 변경이 자동으로 프로덕션에 배포

6.2 전통적인 CI/CD vs GitOps

특성 전통적인 CI/CD GitOps
배포 트리거 CI 파이프라인이 push Git 변경을 pull
진실의 원천 CI 서버 상태 Git 저장소
롤백 재배포 필요 Git revert
감사 추적 CI 로그 Git 히스토리
보안 CI에 클러스터 접근 권한 필요 클러스터 내부에서만 동작

7. GitOps 소개

7.1 GitOps 원칙

  1. 선언적 설정: 모든 인프라와 애플리케이션 상태를 선언적으로 정의
  2. 버전 관리: Git을 유일한 진실의 원천으로 사용
  3. 자동 동기화: 승인된 변경은 자동으로 클러스터에 적용
  4. 지속적 조정: 실제 상태와 원하는 상태의 차이를 지속적으로 감지하고 수정

7.2 ArgoCD

ArgoCD는 가장 널리 사용되는 GitOps 도구입니다.

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

# ArgoCD CLI 설치 (macOS)
brew install argocd

# 초기 admin 비밀번호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# 포트 포워딩으로 접속
kubectl port-forward svc/argocd-server -n argocd 8080:443

ArgoCD Application 정의

# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/my-app-config.git
    targetRevision: main
    path: kubernetes/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true        # 삭제된 리소스 자동 정리
      selfHeal: true     # 수동 변경 자동 복구
    syncOptions:
      - CreateNamespace=true

7.3 Flux CD

# Flux CLI 설치
curl -s https://fluxcd.io/install.sh | sudo bash

# Flux 부트스트랩 (GitHub 사용)
flux bootstrap github \
  --owner=myorg \
  --repository=fleet-infra \
  --branch=main \
  --path=./clusters/production \
  --personal

Flux GitRepository와 Kustomization

# flux-source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: my-app
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/myorg/my-app-config
  ref:
    branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: my-app
  namespace: flux-system
spec:
  interval: 10m
  targetNamespace: production
  sourceRef:
    kind: GitRepository
    name: my-app
  path: ./kubernetes/overlays/production
  prune: true
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: my-app
      namespace: production

8. GitHub Actions로 K8s 배포 자동화

8.1 GitHub Actions 워크플로우 기본 구조

# .github/workflows/ci-cd.yaml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # 1. 테스트 및 빌드
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Run linting
        run: npm run lint

  # 2. Docker 이미지 빌드 및 푸시
  build:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # 3. Kubernetes 배포
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v3
        with:
          version: 'latest'

      - name: Configure kubeconfig
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config
          chmod 600 $HOME/.kube/config

      - name: Set up Helm
        uses: azure/setup-helm@v3
        with:
          version: 'latest'

      - name: Deploy with Helm
        run: |
          helm upgrade --install my-app ./charts/my-app \
            --namespace production \
            --create-namespace \
            --set image.tag=${{ github.sha }} \
            --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
            --wait \
            --timeout 5m

      - name: Verify deployment
        run: |
          kubectl rollout status deployment/my-app -n production
          kubectl get pods -n production -l app.kubernetes.io/name=my-app

8.2 GitOps 방식의 GitHub Actions

# .github/workflows/gitops-ci.yaml
name: GitOps CI Pipeline

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'Dockerfile'

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  CONFIG_REPO: myorg/k8s-config

jobs:
  build-and-update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      # GitOps: 설정 저장소 업데이트
      - name: Checkout config repo
        uses: actions/checkout@v4
        with:
          repository: ${{ env.CONFIG_REPO }}
          token: ${{ secrets.CONFIG_REPO_TOKEN }}
          path: config-repo

      - name: Update image tag
        run: |
          cd config-repo
          # kustomize를 사용하는 경우
          cd kubernetes/overlays/production
          kustomize edit set image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      - name: Commit and push
        run: |
          cd config-repo
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add .
          git commit -m "Update image to ${{ github.sha }}"
          git push

9. 실전 파이프라인 구축 예제

9.1 전체 프로젝트 구조

my-project/
├── .github/
│   └── workflows/
│       ├── ci.yaml           # CI 파이프라인
│       ├── cd-staging.yaml   # 스테이징 배포
│       └── cd-production.yaml # 프로덕션 배포
├── src/                      # 애플리케이션 소스
├── tests/                    # 테스트 코드
├── charts/
│   └── my-app/              # Helm Chart
│       ├── Chart.yaml
│       ├── values.yaml
│       ├── values-staging.yaml
│       ├── values-production.yaml
│       └── templates/
├── Dockerfile
└── package.json

9.2 완전한 CI/CD 파이프라인

.github/workflows/ci.yaml

name: Continuous Integration

on:
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/testdb

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'CRITICAL,HIGH'

  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Build Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          tags: test-image:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

.github/workflows/cd-staging.yaml

name: Deploy to Staging

on:
  push:
    branches: [develop]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging

      - name: Configure kubectl
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.STAGING_KUBECONFIG }}" | base64 -d > $HOME/.kube/config

      - name: Deploy to Staging
        run: |
          helm upgrade --install my-app-staging ./charts/my-app \
            --namespace staging \
            --create-namespace \
            -f ./charts/my-app/values-staging.yaml \
            --set image.tag=${{ github.sha }} \
            --wait

      - name: Run smoke tests
        run: |
          STAGING_URL=$(kubectl get ingress my-app-staging -n staging -o jsonpath='{.spec.rules[0].host}')
          curl -f https://$STAGING_URL/health || exit 1

      - name: Notify Slack
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,message,commit,author
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

.github/workflows/cd-production.yaml

name: Deploy to Production

on:
  release:
    types: [published]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract version
        id: version
        run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

      - name: Configure kubectl
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.PRODUCTION_KUBECONFIG }}" | base64 -d > $HOME/.kube/config

      - name: Deploy to Production
        run: |
          helm upgrade --install my-app ./charts/my-app \
            --namespace production \
            --create-namespace \
            -f ./charts/my-app/values-production.yaml \
            --set image.tag=${{ steps.version.outputs.VERSION }} \
            --wait \
            --timeout 10m

      - name: Verify deployment
        run: |
          kubectl rollout status deployment/my-app -n production
          kubectl get pods -n production

      - name: Run integration tests
        run: |
          PROD_URL=$(kubectl get ingress my-app -n production -o jsonpath='{.spec.rules[0].host}')
          npm run test:integration -- --url=https://$PROD_URL

      - name: Create deployment record
        run: |
          echo "Deployed version ${{ steps.version.outputs.VERSION }} at $(date)" >> DEPLOYMENTS.md
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add DEPLOYMENTS.md
          git commit -m "Record deployment ${{ steps.version.outputs.VERSION }}" || true
          git push || true

10. 결론 및 시리즈 마무리

이번 편에서는 Kubernetes 애플리케이션 배포를 효율화하는 Helm과 자동화된 CI/CD 파이프라인에 대해 알아보았습니다:

  • Helm: Kubernetes 패키지 매니저로서 복잡한 애플리케이션 배포를 단순화
  • Chart 구조: 템플릿, values, 헬퍼 함수를 활용한 재사용 가능한 패키지
  • GitOps: Git을 진실의 원천으로 하는 현대적인 배포 방식
  • ArgoCD/Flux: 대표적인 GitOps 도구
  • GitHub Actions: 완전한 CI/CD 파이프라인 구축

Docker & Kubernetes 완전 정복 시리즈 요약

지금까지 10편에 걸쳐 Docker와 Kubernetes의 핵심 개념과 실전 활용법을 다루었습니다:

  1. 1편: Docker 기초와 컨테이너 개념
  2. 2편: Docker 이미지와 Dockerfile
  3. 3편: Docker Compose로 멀티 컨테이너 관리
  4. 4편: Kubernetes 기초와 아키텍처
  5. 5편: Pod, Deployment, Service
  6. 6편: Volume과 영속성
  7. 7편: 리소스 관리와 스케일링
  8. 8편: 네트워킹과 Service Mesh
  9. 9편: ConfigMap, Secret, Ingress
  10. 10편: Helm과 CI/CD 파이프라인

이 시리즈를 통해 컨테이너 기술의 기초부터 프로덕션 레벨의 Kubernetes 운영까지 전반적인 지식을 습득하셨기를 바랍니다. 컨테이너와 오케스트레이션 기술은 계속 발전하고 있으니, 공식 문서와 커뮤니티를 통해 최신 동향을 계속 파악하시기 바랍니다.

실습과 경험이 가장 좋은 선생님입니다. 배운 내용을 직접 프로젝트에 적용해 보세요!