Kubernetes에서 Canary Deployment를 구현할 때 가장 중요한 질문은 다음과 같다.
"Canary Pod를 만들었는데 실제 사용자 트래픽을 어떻게 10%만 보내는가?"
단순히 Kubernetes Service를 두 개 만드는 것만으로는 충분하지 않다.
예를 들어 다음과 같이 만들었다고 하자.
fastapi-stable
fastapi-canary
Service가 두 개 존재한다고 해서 자동으로 다음과 같은 트래픽이 만들어지는 것은 아니다.
Stable 90%
Canary 10%
실제 HTTP 요청을 다음과 같이 제어하려면 Traffic Routing이 필요하다.
Nginx Ingress
|
+-----------+-----------+
| |
v v
Stable Canary
90% 10%
그리고 Canary의 상태를 Prometheus로 검사한다.
Canary
|
+--> HTTP 5xx
|
+--> P95 Latency
|
+--> Request Rate
|
v
Prometheus
|
v
Argo Rollouts
정상이면:
10%
↓
25%
↓
50%
↓
100%
문제가 발생하면:
Canary
|
v
Prometheus Analysis
|
v
FAILED
|
v
Argo Rollouts Abort
|
v
Stable 100%
이번 글에서는 이 과정을 실제 Kubernetes 환경에서 구현할 수 있도록 구성한다.
이번 실습의 전체 구조는 다음과 같다.
Developer
|
git push
|
v
GitHub Application
|
v
GitHub Actions
|
+---------+---------+
| |
v v
Test/Build Docker Image
|
v
Container Registry
|
v
GitOps Repository
|
v
Argo CD
|
v
Kubernetes Cluster
|
v
Argo Rollouts
|
Traffic Routing
|
v
Nginx Ingress
|
+--------------+--------------+
| |
v v
Stable Canary
v1.0 v1.1
| |
+--------------+--------------+
|
v
FastAPI
|
+-------------+-------------+
| |
v v
PostgreSQL Redis
Observability
FastAPI
|
+---- Metrics ----> Prometheus ----> Grafana
|
+---- Logs -------> Loki ----------> Grafana
|
+---- Traces -----> Tempo ---------> Grafana
Canary Verification
Argo Rollouts
|
v
Prometheus Analysis
|
+---- 5xx
|
+---- P95
|
v
PASS / FAIL
|
+---- PASS ----> Next Weight
|
+---- FAIL ----> Abort
이번에는 단순히 Canary를 배포하는 것이 목적이 아니다.
다음 6가지를 모두 연결한다.
10%
25%
50%
100%
Canary HTTP Request
Canary HTTP 5xx
Canary P95
5xx < 1%
P95 < 500ms
Stable
Canary
5xx
P95
Traffic
Metrics
|
+--> Logs
|
+--> Traces
Code Push
|
v
GitHub Actions
|
v
Docker Image
|
v
GitOps values.yaml
|
v
Argo CD
일반적인 Kubernetes Service는 기본적으로 Pod 집합에 트래픽을 분산한다.
예를 들어:
Service
|
+-- Pod 1
+-- Pod 2
+-- Pod 3
+-- Pod 4
하지만 Canary에서는 다음과 같은 의도가 필요하다.
Stable
|
+-- 90%
Canary
|
+-- 10%
이것은 단순한 Replica 수와 다른 개념이다.
예를 들어 Stable Pod 9개와 Canary Pod 1개를 만들었다고 해서 정확히 HTTP 요청의 90%와 10%가 보장되는 것은 아니다.
따라서 HTTP Layer에서 Traffic Weight를 제어해야 한다.
각 구성요소의 역할을 구분하면 이해하기 쉽다.
Argo Rollouts
"몇 %를 Canary로 보낼 것인가?"
Nginx Ingress:
"실제 HTTP 요청을 어느 Service로 보낼 것인가?"
Prometheus:
"Canary가 정상인가?"
Grafana:
"운영자가 현재 상태를 어떻게 볼 것인가?"
Loki:
"무슨 오류가 발생했는가?"
Tempo:
"어느 구간에서 시간이 오래 걸렸는가?"
Argo CD:
"Git에 정의된 상태를 Kubernetes에 반영한다."
GitHub Actions:
"코드를 테스트하고 Image를 만든다."
예제에서는 다음 Namespace를 사용한다.
kubectl create namespace app
kubectl create namespace monitoring
kubectl create namespace argocd
확인:
kubectl get namespace
Nginx Ingress Controller가 Kubernetes Cluster에 설치되어 있어야 한다.
확인:
kubectl get pods -A | grep ingress
예:
ingress-nginx-controller-xxxxx
Service 확인:
kubectl get svc -n ingress-nginx
외부 IP 또는 NodePort를 통해 HTTP 요청이 Ingress Controller까지 도착해야 한다.
간단한 FastAPI Application을 사용한다.
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator
app = FastAPI()
Instrumentator().instrument(app).expose(app)
@app.get("/")
def root():
return {
"version": "v1.0",
"message": "Hello Kubernetes"
}
@app.get("/health")
def health():
return {
"status": "ok"
}
Canary 버전에서는 응답을 다음처럼 변경한다.
@app.get("/")
def root():
return {
"version": "v1.1",
"message": "Hello Canary"
}
이렇게 하면 실제 Traffic이 어느 버전으로 들어가는지 쉽게 확인할 수 있다.
여기서 매우 중요한 부분이 있다.
단순히 다음과 같이 Metrics를 수집하면:
http_requests_total
Stable과 Canary의 Metrics가 섞일 수 있다.
그러면 Prometheus가 다음을 계산할 때 문제가 발생한다.
전체 Application 5xx
우리가 원하는 것은:
Canary만의 5xx
따라서 Pod에 명확한 Label을 추가한다.
Stable:
metadata:
labels:
app: fastapi
rollouts-pod-template-hash: stable
version: stable
Canary:
metadata:
labels:
app: fastapi
version: canary
실제 Argo Rollouts에서는 Pod Template Hash 등의 동적 label을 활용하는 방식이 더 적절하다.
핵심은 다음과 같다.
Stable Metrics
|
+---- version="stable"
Canary Metrics
|
+---- version="canary"
FastAPI에서 다음 Metrics를 노출한다고 하자.
http_requests_total
http_request_duration_seconds_bucket
Prometheus에서 다음과 같이 확인할 수 있다.
http_requests_total
Canary만 확인:
http_requests_total{
app="fastapi",
version="canary"
}
Stable:
http_requests_total{
app="fastapi",
version="stable"
}
apiVersion: v1
kind: Service
metadata:
name: fastapi-stable
namespace: app
spec:
selector:
app: fastapi
ports:
- name: http
port: 8000
targetPort: 8000
하지만 이 Service는 모든 FastAPI Pod를 선택할 수 있다.
따라서 실제 Rollouts 환경에서는 Argo Rollouts가 Service selector를 관리하도록 구성하는 것이 중요하다.
apiVersion: v1
kind: Service
metadata:
name: fastapi-canary
namespace: app
spec:
selector:
app: fastapi
ports:
- name: http
port: 8000
targetPort: 8000
Argo Rollouts가 Stable/Canary ReplicaSet을 구분할 수 있도록 Rollout과 Service를 함께 구성한다.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: fastapi
namespace: app
spec:
replicas: 6
revisionHistoryLimit: 3
selector:
matchLabels:
app: fastapi
template:
metadata:
labels:
app: fastapi
spec:
containers:
- name: fastapi
image: ghcr.io/example/fastapi:v1.0
ports:
- name: http
containerPort: 8000
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10
Argo Rollouts에서 Nginx Ingress Traffic Routing을 사용하도록 지정한다.
strategy:
canary:
canaryService: fastapi-canary
stableService: fastapi-stable
trafficRouting:
nginx:
stableIngress: fastapi-ingress
steps:
- setWeight: 10
- pause:
duration: 5m
- setWeight: 25
- pause:
duration: 5m
- setWeight: 50
- pause:
duration: 10m
- setWeight: 100
여기서 중요한 부분은:
trafficRouting:
nginx:
stableIngress: fastapi-ingress
이다.
이 설정을 통해 Argo Rollouts가 Nginx Ingress의 Canary 설정을 조정할 수 있다.
Ingress는 Stable Service를 기본 Backend로 사용한다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fastapi-ingress
namespace: app
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: fastapi-stable
port:
number: 8000
처음에는 모든 트래픽이 Stable로 간다.
100% Stable
새로운 버전이 배포된다.
Argo Rollouts:
- setWeight: 10
그러면 Nginx Ingress가 Canary Weight를 반영하도록 변경된다.
개념적으로:
Nginx Ingress
|
+--------+--------+
| |
v v
Stable Canary
90% 10%
| |
v v
v1.0 v1.1
이제 실제 사용자 요청의 일부가 Canary로 전달된다.
예를 들어 다음 명령을 반복한다.
for i in {1..100}; do
curl -s http://api.example.com/
echo
done
결과를 보면:
v1.0
v1.0
v1.1
v1.0
v1.0
v1.1
...
대략 Canary 요청이 10% 수준으로 나타날 수 있다.
단, 짧은 테스트에서 정확히 10개가 나온다는 의미는 아니다.
Traffic Weight는 장기적인 요청 분포를 기준으로 이해해야 한다.
Prometheus 검증 결과가 정상이라면:
10%
|
v
25%
Argo Rollouts:
- setWeight: 25
Traffic:
Stable 75%
Canary 25%
구조:
Nginx Ingress
|
+--------+--------+
| |
v v
Stable Canary
75% 25%
다음 단계:
- setWeight: 50
결과:
Stable 50%
Canary 50%
이 단계에서는 Canary에 상당히 많은 실제 요청이 전달되므로 문제가 있다면 더욱 빠르게 감지할 수 있다.
최종 단계:
- setWeight: 100
결과:
Stable 0%
Canary 100%
Canary가 최종 Production 버전이 된다.
v1.0
|
| 0%
|
v
v1.1
|
| 100%
|
v
Production
이제 중요한 부분을 추가한다.
각 단계에서 Prometheus를 통해 Canary 상태를 확인한다.
검증 기준:
HTTP 5xx < 1%
P95 < 500ms
예를 들어:
Canary 10%
5xx = 0.2%
P95 = 180ms
PASS
그러면:
10%
↓
25%
Canary만 계산하도록 한다.
sum(
rate(
http_requests_total{
app="fastapi",
version="canary",
status=~"5.."
}[5m]
)
)
/
sum(
rate(
http_requests_total{
app="fastapi",
version="canary"
}[5m]
)
)
결과:
0.002
이면:
0.2%
이다.
Histogram Metric을 사용한다.
histogram_quantile(
0.95,
sum(
rate(
http_request_duration_seconds_bucket{
app="fastapi",
version="canary"
}[5m]
)
) by (le)
)
결과:
0.18
이면:
180ms
이다.
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: fastapi-canary-analysis
namespace: app
spec:
metrics:
- name: canary-error-rate
interval: 1m
count: 3
failureLimit: 1
successCondition: result[0] < 0.01
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(
rate(
http_requests_total{
app="fastapi",
version="canary",
status=~"5.."
}[5m]
)
)
/
sum(
rate(
http_requests_total{
app="fastapi",
version="canary"
}[5m]
)
)
- name: canary-p95
interval: 1m
count: 3
failureLimit: 1
successCondition: result[0] < 0.5
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
histogram_quantile(
0.95,
sum(
rate(
http_request_duration_seconds_bucket{
app="fastapi",
version="canary"
}[5m]
)
) by (le)
)
strategy:
canary:
canaryService: fastapi-canary
stableService: fastapi-stable
trafficRouting:
nginx:
stableIngress: fastapi-ingress
steps:
- setWeight: 10
- pause:
duration: 5m
- analysis:
templates:
- templateName: fastapi-canary-analysis
- setWeight: 25
- pause:
duration: 5m
- analysis:
templates:
- templateName: fastapi-canary-analysis
- setWeight: 50
- pause:
duration: 10m
- analysis:
templates:
- templateName: fastapi-canary-analysis
- setWeight: 100
이제 배포 흐름은 다음과 같다.
10%
|
+--> Prometheus
| |
| +--> PASS
|
v
25%
|
+--> Prometheus
| |
| +--> PASS
|
v
50%
|
+--> Prometheus
| |
| +--> PASS
|
v
100%
예를 들어 Canary 25%에서:
5xx = 4.2%
P95 = 1.8s
기준:
5xx < 1%
P95 < 500ms
따라서:
Analysis
|
v
FAILED
Argo Rollouts:
Abort
Traffic:
Canary 25%
|
X
|
v
Stable 100%
Abort 이후에는 단순히 Pod를 삭제하는 것보다 Traffic을 Stable로 복귀시키는 것이 중요하다.
Before
Stable 75%
Canary 25%
Abort:
Stable 100%
Canary 0%
사용자는 다시 Stable 버전을 사용한다.
Grafana에서는 최소한 다음 Panel을 구성하는 것을 권장한다.
+---------------------------------------------------+
| FastAPI Canary Dashboard |
+---------------------------------------------------+
| |
| Canary Traffic 25% |
| |
+---------------------------------------------------+
| |
| Stable RPS 820 req/s |
| Canary RPS 270 req/s |
| |
+---------------------------------------------------+
| |
| Canary 5xx 0.42 % |
| |
+---------------------------------------------------+
| |
| Canary P95 210 ms |
| |
+---------------------------------------------------+
| |
| Stable P95 180 ms |
| |
+---------------------------------------------------+
| |
| Canary Pods 2 |
| Stable Pods 4 |
| |
+---------------------------------------------------+
Grafana에서 다음 PromQL을 사용할 수 있다.
sum(
rate(
http_requests_total{
app="fastapi",
version="canary"
}[5m]
)
)
Stable:
sum(
rate(
http_requests_total{
app="fastapi",
version="stable"
}[5m]
)
)
두 값을 하나의 Graph에서 비교하면 Canary Traffic 변화를 볼 수 있다.
100 *
sum(
rate(
http_requests_total{
app="fastapi",
version="canary",
status=~"5.."
}[5m]
)
)
/
sum(
rate(
http_requests_total{
app="fastapi",
version="canary"
}[5m]
)
)
단위는 Percentage로 설정한다.
예:
0.25%
histogram_quantile(
0.95,
sum(
rate(
http_request_duration_seconds_bucket{
app="fastapi",
version="canary"
}[5m]
)
) by (le)
)
Grafana Unit:
seconds (s)
또는 적절한 시간 단위로 표시한다.
Canary 장애가 발생하면 Metrics만 보는 것으로 끝내면 안 된다.
Grafana에서 Loki를 조회한다.
예:
{namespace="app", app="fastapi", version="canary"}
ERROR:
{namespace="app", app="fastapi", version="canary"} |= "ERROR"
HTTP 500:
{namespace="app", app="fastapi", version="canary"} |= "500"
Stable과 Canary 로그를 분리하면 매우 유용하다.
Stable
INFO request completed
INFO database query completed
Canary:
ERROR database timeout
ERROR redis connection timeout
ERROR HTTP 500
따라서:
Canary Metric
|
v
5xx 증가
|
v
Loki
|
v
Canary ERROR Log
형태로 빠르게 장애를 추적할 수 있다.
FastAPI에 OpenTelemetry를 적용하면 Request Trace를 Tempo로 보낼 수 있다.
개념:
Client
|
v
Ingress
|
v
FastAPI
|
+------ Redis
|
+------ PostgreSQL
Trace:
Trace ID
|
+-- FastAPI 1.2s
|
+-- Redis 20ms
|
+-- PostgreSQL 1.1s
이런 식으로 어느 구간에서 지연이 발생하는지 확인할 수 있다.
FastAPI
|
| OpenTelemetry SDK
v
OTel Collector
|
v
Tempo
운영 환경에서는 애플리케이션에서 Tempo로 직접 보내기보다 OpenTelemetry Collector를 중간에 두는 구조가 관리하기 좋다.
FastAPI
|
v
OpenTelemetry Collector
|
v
Tempo
가장 중요한 운영 패턴이다.
예를 들어 Grafana에서:
Canary 5xx = 5%
를 발견한다.
그 다음:
5xx
|
v
Loki
|
v
ERROR Log
|
v
Trace ID
|
v
Tempo
|
v
PostgreSQL
이렇게 연결한다.
즉:
Prometheus
|
| "문제가 발생했다"
v
Loki
|
| "무슨 오류인가?"
v
Tempo
|
| "어디에서 발생했는가?"
v
Root Cause
이제 CI/CD를 연결한다.
Developer
|
v
git push
|
v
GitHub
|
v
GitHub Actions
|
+-- Test
|
+-- Build
|
+-- Security Scan
|
+-- Push Image
|
+-- Update GitOps
이미지는 Git Commit SHA를 사용하는 것을 권장한다.
예:
ghcr.io/example/fastapi:
8f4d92c
또는:
ghcr.io/example/fastapi:
sha-8f4d92c
이렇게 하면 어떤 Commit에서 만들어진 이미지인지 추적하기 쉽다.
name: Build Application
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Image
run: |
docker build \
-t ghcr.io/example/fastapi:${{ github.sha }} .
- name: Push Image
run: |
docker push \
ghcr.io/example/fastapi:${{ github.sha }}
이제 중요한 단계다.
Build가 끝나면 GitOps Repository의:
values.yaml
을 변경한다.
기존:
image:
tag: "8f4d92c"
새 버전:
image:
tag: "9ab1234"
GitHub Actions가 이 파일을 변경하고 Commit한다.
GitHub Actions에서는 GitOps Repository를 별도로 Checkout할 수 있다.
- name: Checkout GitOps
uses: actions/checkout@v4
with:
repository: example/k8s-gitops
token: ${{ secrets.GITOPS_TOKEN }}
path: gitops
여기서:
GITOPS_TOKEN
은 GitOps Repository를 수정할 수 있는 권한이 필요하다.
실제 운영에서는 장기 PAT보다 GitHub App 또는 적절한 단기 인증 방식을 검토하는 것이 좋다.
예:
- name: Update Image Tag
run: |
sed -i \
"s/^ tag:.*/ tag: \"${GITHUB_SHA}\"/" \
gitops/environments/prod/values.yaml
확인:
cat gitops/environments/prod/values.yaml
결과:
image:
repository: ghcr.io/example/fastapi
tag: "9ab1234..."
- name: Commit GitOps
run: |
cd gitops
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add environments/prod/values.yaml
git commit \
-m "Deploy fastapi ${GITHUB_SHA}"
git push
이제 전체 흐름이 완성된다.
Application Git
|
v
GitHub Actions
|
v
Docker Image
|
v
GitOps Repository
|
v
Argo CD
예:
Before
image:
tag: v1.0
GitHub Actions 실행 후:
After
image:
tag: 9ab1234
Git Commit:
Deploy fastapi 9ab1234
Argo CD가 변경을 감지한다.
Git
|
| changed
v
Argo CD
|
v
OutOfSync
|
v
Sync
Argo CD:
GitOps
|
v
Helm
|
v
Kubernetes Manifest
|
v
Argo Rollout
새로운 Image:
v1.1
이 적용된다.
최종적으로 다음과 같은 흐름이 만들어진다.
Developer
|
| git push
v
GitHub Repository
|
v
GitHub Actions
|
+---------+---------+
| |
v v
Test Docker Build
|
v
Registry
|
v
GitOps Repo
|
v
Argo CD
|
v
Argo Rollouts
|
v
Nginx Ingress
|
+-------------+-------------+
| |
v v
Stable Canary
90% 10%
| |
+-------------+-------------+
|
v
Users
Argo Rollouts
|
v
setWeight: 10
|
v
Nginx Ingress
|
+---+---+
| |
90% 10%
| |
Stable Canary
Prometheus:
5xx = 0.2%
P95 = 180ms
결과:
PASS
Stable 75%
Canary 25%
Prometheus:
5xx = 0.3%
P95 = 210ms
결과:
PASS
Stable 50%
Canary 50%
Prometheus:
5xx = 0.4%
P95 = 230ms
결과:
PASS
Stable 0%
Canary 100%
Deployment 완료.
v1.1
|
v
Production
이번에는 v1.2에 문제가 있다고 가정한다.
GitHub:
v1.1
|
v
v1.2
GitHub Actions:
PASS
GitOps:
v1.2
Argo CD:
Sync
Canary:
10%
그런데 Canary에서 DB Query가 느려진다.
Prometheus:
P95 = 1.9s
기준:
P95 < 500ms
Analysis:
FAILED
Argo Rollouts:
ABORT
Traffic:
Stable 100%
Canary 0%
Grafana에서:
Canary P95
|
v
1.9s
Loki:
{namespace="app", app="fastapi", version="canary"} |= "timeout"
결과:
ERROR PostgreSQL query timeout
Tempo:
FastAPI
|
+-- PostgreSQL
|
+-- 1.7s
최종적으로 PostgreSQL Query 문제임을 확인할 수 있다.
여기서 매우 중요한 운영 개념이 있다.
GitOps:
Desired State
Kubernetes:
Actual State
Argo Rollouts:
Runtime Progressive Delivery
따라서:
Git
|
| Desired State
v
Argo CD
|
v
Kubernetes
|
v
Argo Rollouts
|
v
Runtime Traffic
이 구조를 이해해야 한다.
예를 들어 GitOps에는:
image:
tag: v1.2
가 들어 있다.
Canary가 실패해서 Abort되었다고 하자.
Runtime:
Stable v1.1
Canary v1.2
|
X
하지만 Git은 여전히:
v1.2
를 가리키고 있을 수 있다.
따라서 운영 정책을 별도로 정해야 한다.
Runtime Abort만 하고 Git은 유지.
Git = v1.2
Runtime = v1.1
GitOps Repository도 자동으로 이전 버전으로 Revert.
Git v1.2
|
v
Git v1.1
Production 환경에서는 이 둘의 의미를 명확하게 구분해야 한다.
GitHub Actions가 Kubernetes에 직접:
kubectl rollout undo
를 실행하도록 만들 수도 있다.
하지만 GitOps 구조에서는 권장되지 않는다.
왜냐하면:
Git
|
+-- v1.2
Kubernetes
|
+-- v1.1
처럼 상태가 달라질 수 있기 때문이다.
GitOps에서는 가능한 한:
Git
|
v
Argo CD
|
v
Kubernetes
흐름을 유지하는 것이 중요하다.
Production에서는 다음처럼 역할을 분리하는 것이 좋다.
GitHub Actions
CI 담당
- Test
- Build
- Scan
- Push Image
- GitOps 변경
Argo CD
CD 담당
- Sync
- Drift Detection
- Self Heal
Argo Rollouts
Progressive Delivery 담당
- Canary
- Traffic Weight
- Analysis
- Abort
Prometheus
자동 상태 판단
Grafana
운영자 Dashboard
Loki
Log 분석
Tempo
Trace 분석
이렇게 하면 각 시스템의 책임이 명확하다.
실제 운영 Dashboard에서는 다음을 한 화면에 보여주는 것이 좋다.
+------------------------------------------------------+
| CANARY DEPLOYMENT |
+------------------------------------------------------+
| |
| Rollout Status Progressing |
| Current Weight 25% |
| |
+------------------------------------------------------+
| Stable Traffic 75% |
| Canary Traffic 25% |
| |
+------------------------------------------------------+
| Canary 5xx 0.31% |
| Threshold 1.00% |
| |
+------------------------------------------------------+
| Canary P95 220ms |
| Threshold 500ms |
| |
+------------------------------------------------------+
| Canary Pods 2 |
| Stable Pods 4 |
| |
+------------------------------------------------------+
이 Dashboard만 봐도 현재 배포 상태를 빠르게 파악할 수 있다.
실제 장애가 발생하면 다음 순서가 유용하다.
Grafana 확인
5xx 증가?
P95 증가?
Traffic 이상?
Prometheus 확인
Canary만 문제인가?
Stable도 문제인가?
Loki 확인
ERROR
Exception
Timeout
Database
Redis
Tempo 확인
어느 Span이 느린가?
Kubernetes 확인
kubectl get pods -n app
kubectl get rollout -n app
kubectl describe rollout fastapi -n app
Argo Rollouts 확인
kubectl argo rollouts get rollout fastapi -n app
필요하면 Abort
kubectl argo rollouts abort fastapi -n app
User
|
v
Ingress
|
v
Canary
|
v
FastAPI
|
+----------+----------+
| | |
v v v
Metrics Logs Traces
| | |
v v v
Prometheus Loki Tempo
| | |
+----------+----------+
|
v
Grafana
|
v
Operator
|
v
Argo Rollouts
|
v
Abort
|
v
Stable
최종적으로 GitHub Actions는 다음 구조가 된다.
git push
|
v
Checkout
|
v
Unit Test
|
v
Docker Build
|
v
Security Scan
|
v
Push Registry
|
v
Checkout GitOps
|
v
Update values.yaml
|
v
Git Commit
|
v
Git Push
Argo CD는:
GitOps Change
|
v
Argo CD
|
v
Sync
Argo Rollouts는:
New Version
|
v
10%
|
v
Analysis
|
v
25%
|
v
Analysis
|
v
50%
|
v
Analysis
|
v
100%
모든 내용을 하나로 합치면 다음과 같다.
Developer
|
| git push
v
GitHub Application
|
v
GitHub Actions
|
+--------------+--------------+
| |
v v
Test Docker Build
|
v
Container Registry
|
v
GitOps Repo
|
v
Argo CD
|
v
Argo Rollouts
|
+---------+---------+
| |
v v
Stable Canary
90% 10%
| |
+---------+---------+
|
v
Nginx Ingress
|
v
Users
Observability
FastAPI
|
+---- Metrics ----> Prometheus
| |
| v
| Analysis
| |
| v
| PASS / FAIL
|
+---- Logs -------> Loki
| |
| v
| Grafana
|
+---- Traces -----> OTel Collector
|
v
Tempo
|
v
Grafana
이번 실습에서 반드시 기억해야 할 것은 다음이다.
첫 번째.
Service 두 개
!=
Traffic Split
실제 Canary Traffic을 제어하려면 Traffic Routing이 필요하다.
두 번째.
Canary Metric
!=
전체 Metric
Canary 전용 label 또는 다른 안정적인 식별 기준을 사용해 Canary 상태만 측정해야 한다.
세 번째.
Prometheus
는 단순 모니터링 도구가 아니라 Argo Rollouts의 자동 배포 검증에도 사용할 수 있다.
네 번째.
Grafana
는 사람이 전체 상태를 확인하는 Dashboard 역할을 한다.
다섯 번째.
Loki
는 Metric만으로 알 수 없는 실제 오류 내용을 확인한다.
여섯 번째.
Tempo
는 하나의 Request가 어느 서비스 또는 Database 구간에서 느려졌는지 추적한다.
일곱 번째.
GitHub Actions
는 Kubernetes에 직접 배포하기보다는 Application Build와 GitOps Repository 변경을 담당하게 만들 수 있다.
여덟 번째.
Argo CD
는 Git을 Kubernetes의 Desired State로 연결한다.
아홉 번째.
Argo Rollouts
는 실제 Runtime에서 Canary Traffic을 단계적으로 증가시키고 Prometheus Analysis 결과에 따라 다음 단계로 진행하거나 Abort할 수 있다.
결국 우리가 만드는 시스템은 다음 한 줄로 표현할 수 있다.
Code
→ GitHub Actions
→ Docker Image
→ GitOps
→ Argo CD
→ Argo Rollouts
→ Nginx Ingress
→ 10%
→ Prometheus
→ 25%
→ Prometheus
→ 50%
→ Prometheus
→ 100%
→ Production
장애가 발생하면:
Canary
|
v
Prometheus
|
+---- 5xx 증가
|
+---- P95 증가
|
v
Analysis Failed
|
v
Argo Rollouts Abort
|
v
Stable 100%
|
+---- Loki → 오류 로그
|
+---- Tempo → 장애 Trace
|
v
Grafana → 운영자 분석
이 구조가 완성되면 단순한 Kubernetes 배포를 넘어 Progressive Delivery + GitOps + Observability 기반의 실제 운영형 DevOps/SRE 플랫폼을 직접 구축한 것이 된다.
이제 이론적으로 필요한 구성은 거의 모두 연결되었다.
다음 실전 단계에서는 위 YAML을 단순 예제로 보는 것이 아니라 실제로 다음 순서대로 구축하면 된다.
1. Kubernetes Cluster
↓
2. Nginx Ingress Controller
↓
3. Argo CD
↓
4. Argo Rollouts
↓
5. Prometheus + Grafana
↓
6. Loki
↓
7. OpenTelemetry Collector + Tempo
↓
8. FastAPI + PostgreSQL + Redis
↓
9. Helm Chart
↓
10. Nginx Ingress Traffic Routing
↓
11. Canary 10 → 25 → 50 → 100
↓
12. Prometheus Analysis
↓
13. Grafana Dashboard
↓
14. Loki / Tempo 장애 분석
↓
15. GitHub Actions
↓
16. GitOps 자동 변경
↓
17. Argo CD 자동 Sync
↓
18. Canary 자동 Abort 테스트
최종 목표는 정상 배포 테스트와 고의적인 장애를 발생시키는 실패 배포 테스트를 모두 수행하는 것이다.
예를 들어 Canary v2에서 의도적으로 HTTP 500을 발생시키면:
GitHub Push
↓
GitHub Actions
↓
Docker Image
↓
GitOps
↓
Argo CD
↓
Argo Rollouts
↓
Canary 10%
↓
HTTP 5xx 증가
↓
Prometheus Analysis FAILED
↓
Argo Rollouts ABORT
↓
Stable 100%
↓
Loki에서 ERROR 확인
↓
Tempo에서 문제 Trace 확인
↓
Grafana에서 전체 상황 확인
여기까지 직접 성공하면 CI/CD → GitOps → Canary → Observability → 자동 장애 대응이라는 하나의 완성된 Kubernetes 운영 플랫폼을 구축한 것으로 볼 수 있다.