· 54분 읽기
GitOps 심화 (하): 환경 설정, 시크릿 관리, Progressive Delivery
GitOps의 철학과 ArgoCD·Flux 도입을 다룬 상편에 이어, 하편에서는 실제 운영 단계에서 부딪히는 세 가지 과제를 다룹니다. 동일한 애플리케이션을 여러 환경에 배포하는 설정 관리, Git에 비밀 값을 안전하게 두는 시크릿 관리, 그리고 CI/CD 파이프라인 통합과 Progressive Delivery입니다. 각 절의 끝에는 실무 의사결정에 바로 쓸 수 있는 선택 가이드를 정리했습니다.
환경별 설정 관리: Kustomize vs Helm
실제 프로덕션에서는 동일한 애플리케이션을 여러 환경(dev, staging, prod)에 배포합니다. 환경마다 달라지는 값은 대략 다음과 같습니다.
| 항목 | dev | staging | prod |
|---|---|---|---|
| replicas | 1 | 2 | 5 |
| CPU request | 100m | 200m | 500m |
| DB host | dev-db.local | staging-db.local | prod-db.aws |
| Log level | debug | info | warn |
| Ingress domain | dev.example.com | staging.example.com | example.com |
환경마다 완전히 별개의 YAML을 관리하면 중복 코드가 대량 발생하고, 한 곳을 수정할 때마다 모든 환경에 동기화해야 하며, 실수로 누락되는 변경이 생깁니다. 해결책은 Kustomize 또는 Helm으로 공통 부분(Base)과 환경별 차이(Overlays/Values)를 분리하는 것입니다.
Kustomize: 템플릿 없는 설정 관리
Kustomize는 템플릿 없이 YAML을 패치하는 방식입니다. Kubernetes 1.14부터 kubectl에 내장되었습니다.
- Base: 공통 리소스 정의
- Overlays: 환경별 패치
- No templating: Go template 같은 문법이 없음
- Pure YAML: 결과물도 순수 YAML
myapp/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── configmap.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── patch-replicas.yaml
├── staging/
│ ├── kustomization.yaml
│ └── patch-resources.yaml
└── prod/
├── kustomization.yaml
├── patch-replicas.yaml
├── patch-resources.yaml
└── patch-ingress.yaml
Base에는 공통 리소스를 정의합니다.
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
commonLabels:
app: myapp
# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
Overlay는 base를 참조하면서 환경별 차이만 패치합니다.
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: production
commonLabels:
environment: production
images:
- name: myapp
newTag: v1.2.3
patches:
- path: patch-replicas.yaml
- path: patch-resources.yaml
# overlays/prod/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 5
# overlays/prod/patch-resources.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1024Mi
빌드와 적용은 다음과 같습니다.
# 결과 미리보기
kubectl kustomize overlays/prod
# 직접 적용
kubectl apply -k overlays/prod
# 또는 ArgoCD에서
argocd app create myapp-prod \
--repo https://github.com/myorg/myapp.git \
--path overlays/prod \
--dest-server https://kubernetes.default.svc
패치 전략은 세 가지입니다. 기본은 Strategic Merge Patch로, 배열의 특정 요소를 이름으로 매칭해 수정합니다. 더 정밀한 제어가 필요하면 JSON Patch를 씁니다.
# overlays/prod/kustomization.yaml
patches:
- target:
kind: Deployment
name: myapp
patch: |-
- op: replace
path: /spec/replicas
value: 5
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: NEW_VAR
value: "some-value"
기존 값을 완전히 대체할 때는 op: replace로 전체 필드를 덮어쓸 수 있습니다. 또한 Kustomize 3.7 이상에서는 Components로 여러 Overlay가 공유하는 패치 모음(고가용성, 모니터링, 보안 등)을 만들 수 있습니다.
# components/high-availability/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
# overlays/prod/kustomization.yaml
resources:
- ../../base
components:
- ../../components/high-availability
- ../../components/monitoring
Helm: 템플릿 기반 패키지 매니저
Helm은 템플릿 기반 패키지 매니저입니다. Chart라는 패키지 형태로 애플리케이션을 배포합니다.
- Chart: 패키지 (템플릿 + 기본값)
- Values: 설정 값
- Release: Chart의 설치된 인스턴스
- Repository: Chart 저장소
mychart/
├── Chart.yaml # 차트 메타데이터
├── values.yaml # 기본 values
├── values-dev.yaml # 환경별 values
├── values-prod.yaml
├── templates/
│ ├── _helpers.tpl # 템플릿 헬퍼
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── ingress.yaml
│ └── NOTES.txt # 설치 후 출력 메시지
└── charts/ # 의존 차트
# Chart.yaml
apiVersion: v2
name: mychart
version: 1.0.0
appVersion: "1.2.3"
description: My application chart
type: application
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
템플릿은 Go template 문법으로 작성합니다.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mychart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.env }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
환경별 차이는 values 파일로 관리합니다.
# values.yaml (기본값)
replicaCount: 1
image:
repository: myapp
tag: ""
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
ingress:
enabled: false
host: ""
env: {}
# values-prod.yaml
replicaCount: 5
image:
tag: v1.2.3
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1024Mi
ingress:
enabled: true
host: myapp.example.com
tls:
- secretName: myapp-tls
hosts:
- myapp.example.com
env:
LOG_LEVEL: warn
DB_HOST: prod-db.example.com
# 로컬 차트 설치
helm install myapp-prod ./mychart -f values-prod.yaml -n production
# 리포지토리에서 설치
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install nginx bitnami/nginx -f my-values.yaml
# 업그레이드
helm upgrade myapp-prod ./mychart -f values-prod.yaml
# 롤백
helm rollback myapp-prod 1
Helm Hooks를 쓰면 특정 시점에 작업을 실행할 수 있습니다. 예를 들어 pre-upgrade 훅으로 DB 마이그레이션 Job을 업그레이드 전에 실행하는 식입니다.
| Hook | 시점 |
|---|---|
pre-install |
설치 전 |
post-install |
설치 후 |
pre-upgrade |
업그레이드 전 |
post-upgrade |
업그레이드 후 |
pre-delete |
삭제 전 |
post-delete |
삭제 후 |
pre-rollback |
롤백 전 |
post-rollback |
롤백 후 |
하이브리드: Helm + Kustomize
ArgoCD와 Flux 모두 Helm 차트에 Kustomize 패치를 적용하는 post-rendering을 지원합니다. 써드파티 차트에 커스텀 레이블·어노테이션을 추가하거나, 차트가 지원하지 않는 설정을 패치하거나, 조직 표준 정책을 강제할 때 유용합니다.
# ArgoCD: Helm 차트 + Kustomize post-rendering
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx-prod
spec:
source:
repoURL: https://charts.bitnami.com/bitnami
chart: nginx
targetRevision: 15.0.0
helm:
values: |
replicaCount: 3
kustomize:
patches:
- target:
kind: Deployment
name: nginx
patch: |-
- op: add
path: /spec/template/metadata/annotations
value:
custom-annotation: "added-by-kustomize"
Flux에서는 HelmRelease의 postRenderers 필드로 동일한 패턴을 구성합니다.
선택 가이드
flowchart TB
Start[환경별 설정 관리 필요] --> Q1{써드파티 소프트웨어?}
Q1 -->|Yes| Helm1[Helm 사용<br/>이미 차트가 있음]
Q1 -->|No| Q2{복잡한 조건부 로직?}
Q2 -->|Yes| Helm2[Helm 사용<br/>if/range 템플릿]
Q2 -->|No| Q3{팀이 YAML에 익숙?}
Q3 -->|Yes| Kustomize1[Kustomize 사용<br/>순수 YAML 유지]
Q3 -->|No| Q4{배포 대상이 많음?}
Q4 -->|Yes| Helm3[Helm 사용<br/>재사용성 중요]
Q4 -->|No| Kustomize2[Kustomize 사용<br/>단순함 우선]
| 관점 | Kustomize | Helm |
|---|---|---|
| 학습 곡선 | 낮음 (순수 YAML) | 중간 (Go template) |
| 디버깅 | 쉬움 (YAML 그대로) | 어려움 (템플릿 렌더링 필요) |
| 조건부 로직 | 제한적 | 강력함 (if/range) |
| 재사용성 | Component로 가능 | Chart로 패키징 |
| 의존성 관리 | 없음 | Chart dependencies |
| 버전 관리 | Git으로 직접 | Chart 버전 + values |
| 에코시스템 | kubectl 내장 | ArtifactHub, 수많은 차트 |
| GitOps 친화성 | 높음 | 중간 (렌더링 필요) |
Kustomize는 내부 애플리케이션 배포, replicas·resources·env 정도의 단순한 환경별 차이, 순수 YAML 유지, 명확한 Git diff가 필요할 때 적합합니다. Helm은 nginx·postgresql·prometheus 같은 써드파티 소프트웨어 설치, 복잡한 조건부 로직, 재사용 가능한 패키지 배포, 의존성 관리가 필요할 때 적합합니다.
| 도구 | 장점 | 단점 | 사용 시기 |
|---|---|---|---|
| Kustomize | 순수 YAML, 학습 쉬움, kubectl 내장 | 조건부 로직 제한 | 내부 앱, 단순 환경 차이 |
| Helm | 강력한 템플릿, 패키지화, 에코시스템 | 디버깅 어려움 | 써드파티, 복잡한 로직 |
| 하이브리드 | 양쪽 장점 활용 | 복잡도 증가 | Helm 차트 커스터마이징 |
실무에서는 내부 앱은 Kustomize, 인프라 컴포넌트는 Helm으로 관리하는 하이브리드 접근이 일반적입니다.
시크릿 관리: Sealed Secrets, ESO, SOPS
GitOps의 핵심 원칙은 Git을 Single Source of Truth로 삼는 것입니다. 하지만 Kubernetes Secret을 그대로 Git에 저장하면 어떻게 될까요.
# 절대 이렇게 하면 안 됩니다
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # base64는 암호화가 아님
password: c3VwZXJzZWNyZXQ= # 누구나 디코딩 가능
# 즉시 평문 노출
echo "c3VwZXJzZWNyZXQ=" | base64 -d
# 출력: supersecret
Base64는 인코딩이지 암호화가 아닙니다. Git 히스토리에 한 번 들어가면 영구히 노출됩니다. 해결 방향은 두 갈래입니다. 암호화해서 Git에 저장하거나(Sealed Secrets, SOPS), Git에는 참조만 두고 실제 값은 외부 저장소에 두는 것입니다(External Secrets Operator).
flowchart TB
subgraph Problem [GitOps Secrets 딜레마]
SSOT[Git = Single Source of Truth]
Secret[Secrets도 Git에 있어야?]
Risk[평문 노출 위험]
end
SSOT --> Secret
Secret --> Risk
subgraph Solutions [해결 방법]
Encrypted[암호화해서 Git에 저장]
External[외부 저장소 참조]
end
Risk --> Encrypted
Risk --> External
Encrypted --> SealedSecrets[Sealed Secrets]
Encrypted --> SOPS[SOPS]
External --> ESO[External Secrets Operator]
External --> CSI[Secrets Store CSI Driver]
해결책 1: Sealed Secrets
Sealed Secrets는 Bitnami에서 개발한 Kubernetes 컨트롤러로, 해당 클러스터에서만 복호화할 수 있는 암호화된 Secret을 Git에 저장합니다.
sequenceDiagram
participant Dev as 개발자
participant CLI as kubeseal CLI
participant Controller as Sealed Secrets Controller
participant K8s as Kubernetes API
Note over Controller: RSA 키 쌍 보유 (private/public)
Dev->>CLI: 원본 Secret YAML 제공
CLI->>Controller: 공개키 요청
Controller-->>CLI: 공개키 반환
CLI->>CLI: 공개키로 암호화
CLI->>Dev: SealedSecret YAML 생성
Dev->>K8s: git push → GitOps → SealedSecret 적용
K8s->>Controller: SealedSecret 감지
Controller->>Controller: 비밀키로 복호화
Controller->>K8s: 일반 Secret 생성
# 컨트롤러 설치
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
-n kube-system
# CLI 설치 (macOS)
brew install kubeseal
사용 흐름은 세 단계입니다.
# 1. 원본 Secret 생성 (클러스터에 적용하지 않음)
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=supersecret \
--dry-run=client -o yaml > secret.yaml
# 2. Sealed Secret으로 암호화
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# 3. 원본 삭제, Sealed Secret만 Git에 커밋
rm secret.yaml
git add sealed-secret.yaml
git commit -m "Add encrypted database credentials"
# 생성된 SealedSecret
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: default
spec:
encryptedData:
username: AgBy8BQ...암호화된_데이터...==
password: AgCtr2I...암호화된_데이터...==
template:
metadata:
name: db-credentials
namespace: default
type: Opaque
SealedSecret은 어떤 조건에서 복호화를 허용할지 범위(scope)를 지정할 수 있습니다.
kubeseal --scope strict # 기본: 동일 namespace + name만 허용
kubeseal --scope namespace-wide # 동일 namespace 내 다른 이름 허용
kubeseal --scope cluster-wide # 모든 namespace에서 사용 가능
| Scope | 이름 변경 | 네임스페이스 변경 | 보안 수준 |
|---|---|---|---|
strict |
불가 | 불가 | 높음 |
namespace-wide |
가능 | 불가 | 중간 |
cluster-wide |
가능 | 가능 | 낮음 |
컨트롤러의 비밀키가 유출되면 모든 SealedSecret이 복호화되므로 키 백업이 필수입니다.
# 키 백업
kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml > sealed-secrets-key-backup.yaml
# 키 복원 (재해 복구 시)
kubectl apply -f sealed-secrets-key-backup.yaml
kubectl delete pod -n kube-system -l app.kubernetes.io/name=sealed-secrets
한계는 세 가지입니다. 첫째, 클러스터 종속이라 다른 클러스터에서는 복호화할 수 없습니다. 둘째, 키 로테이션이 복잡해 키 변경 시 모든 SealedSecret을 재암호화해야 합니다. 셋째, 컨트롤러 장애 시 Secret을 생성할 수 없는 단일 실패점이 됩니다.
해결책 2: External Secrets Operator (ESO)
External Secrets Operator는 AWS Secrets Manager, HashiCorp Vault 같은 외부 비밀 관리 시스템에서 값을 가져와 Kubernetes Secret으로 동기화합니다. 핵심은 Git에 **참조 정보(ExternalSecret)**만 저장하고, 실제 비밀 값은 외부 저장소에 둔다는 점입니다.
flowchart LR
subgraph External [외부 Secret 저장소]
AWS[AWS Secrets Manager]
Vault[HashiCorp Vault]
GCP[GCP Secret Manager]
Azure[Azure Key Vault]
end
subgraph Cluster [Kubernetes Cluster]
ESO[External Secrets<br/>Operator]
ExtSecret[ExternalSecret CR]
K8sSecret[Kubernetes Secret]
end
ExtSecret -->|참조| ESO
ESO -->|API 호출| AWS & Vault & GCP & Azure
ESO -->|생성/동기화| K8sSecret
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace
먼저 외부 저장소와의 연결(SecretStore 또는 클러스터 전역 ClusterSecretStore)을 설정합니다. AWS라면 IRSA(IAM Role for Service Account)로 인증합니다.
# ClusterSecretStore (클러스터 전역)
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
# IRSA용 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-secrets-sa
namespace: external-secrets
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/external-secrets-role
ExternalSecret은 외부 값과 Kubernetes Secret 키의 매핑을 정의합니다.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
# 동기화 주기
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: username # K8s Secret의 키
remoteRef:
key: prod/database # AWS Secrets Manager의 경로
property: username # JSON 내 속성
- secretKey: password
remoteRef:
key: prod/database
property: password
JSON 전체를 한 번에 가져올 수도 있습니다.
spec:
dataFrom:
- extract:
key: prod/database # JSON 전체를 가져와 각 키를 Secret 데이터로
| Provider | 설명 |
|---|---|
| AWS Secrets Manager | AWS 네이티브 |
| AWS Parameter Store | SSM 파라미터 |
| HashiCorp Vault | 온프레미스/클라우드 |
| GCP Secret Manager | GCP 네이티브 |
| Azure Key Vault | Azure 네이티브 |
| 1Password | 팀 비밀 공유 |
| Doppler | SaaS 비밀 관리 |
장점은 Git에 비밀 값이 전혀 없고, 자동 로테이션을 지원하며, 중앙 집중식으로 관리할 수 있다는 것입니다. 단점은 외부 서비스 의존성, 네트워크 지연, AWS Secrets Manager 같은 서비스의 추가 비용입니다.
해결책 3: SOPS
SOPS(Secrets OPerationS)는 Mozilla에서 개발한 파일 레벨 암호화 도구입니다. YAML, JSON, ENV 파일에서 키는 평문으로 두고 값만 선택적으로 암호화합니다. age 키, AWS/GCP KMS, Azure Key Vault, PGP를 암호화 키로 쓸 수 있습니다.
# 암호화 후에도 구조가 읽힙니다
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
stringData:
username: ENC[AES256_GCM,data:6FLiRc8=,iv:...,tag:...,type:str]
password: ENC[AES256_GCM,data:HdNqmY3WUr8=,iv:...,tag:...,type:str]
sops:
age:
- recipient: age1...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-01-15T10:00:00Z"
mac: ENC[AES256_GCM,...]
version: 3.8.1
키가 평문이므로 Git diff가 의미를 가집니다. 어떤 키가 변경되었는지 바로 알 수 있습니다. 시작은 age 키 방식이 간단합니다.
# age, SOPS 설치
brew install age sops
# 키 쌍 생성
age-keygen -o key.txt
# Public key: age1abc...
# .sops.yaml 설정 (레포지토리 루트)
cat > .sops.yaml << EOF
creation_rules:
- path_regex: .*secrets.*\.yaml$
age: age1abc... # 공개키
EOF
# 암호화
sops -e secrets.yaml > secrets.enc.yaml
# 복호화
sops -d secrets.enc.yaml > secrets.yaml
# 제자리 편집 (복호화 → 편집 → 저장 시 재암호화)
sops secrets.enc.yaml
Flux는 SOPS를 네이티브로 지원합니다.
# 복호화 키를 Secret으로 저장
apiVersion: v1
kind: Secret
metadata:
name: sops-age
namespace: flux-system
stringData:
age.agekey: |
# created: 2024-01-15
# public key: age1abc...
AGE-SECRET-KEY-1...
---
# Kustomization에서 복호화 활성화
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
namespace: flux-system
spec:
decryption:
provider: sops
secretRef:
name: sops-age
ArgoCD는 플러그인으로 지원합니다(kustomize build . | sops -d /dev/stdin 형태의 config management plugin 등록). 조직 규모가 커지면 age 대신 KMS를 붙여 환경별로 다른 키를 쓸 수 있습니다.
# .sops.yaml
creation_rules:
- path_regex: .*prod.*secrets.*\.yaml$
kms: arn:aws:kms:ap-northeast-2:123456789:key/abc-def
- path_regex: .*dev.*secrets.*\.yaml$
kms: arn:aws:kms:ap-northeast-2:123456789:key/xyz-123
보안 모범 사례
어떤 솔루션을 쓰든 다음 네 가지는 공통으로 적용합니다.
- 최소 권한 원칙: ESO ServiceAccount에는 필요한 경로의
secretsmanager:GetSecretValue정도만 부여합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": ["arn:aws:secretsmanager:*:*:secret:prod/*"]
}
]
}
- 네임스페이스 격리: 팀별 SecretStore를 네임스페이스 단위로 분리하고 전용 IAM Role을 연결합니다.
- 자동 로테이션: ESO의
refreshInterval로 주기 동기화하고, AWS Secrets Manager의 자동 로테이션(Lambda)을 활성화합니다. - 감사 로깅: CloudTrail 등에서
GetSecretValue이벤트로 Secret 접근 이력을 추적합니다.
선택 가이드
flowchart TB
Start[Secrets 관리 전략 선택] --> Q1{외부 비밀 관리 시스템<br/>이미 사용 중?}
Q1 -->|Yes| ESO[External Secrets Operator]
Q1 -->|No| Q2{멀티 클러스터?}
Q2 -->|Yes| Q3{중앙 비밀 저장소<br/>도입 가능?}
Q3 -->|Yes| ESO
Q3 -->|No| SOPS[SOPS + age/KMS]
Q2 -->|No| Q4{단순함 우선?}
Q4 -->|Yes| SealedSecrets[Sealed Secrets]
Q4 -->|No| SOPS
| 관점 | Sealed Secrets | ESO | SOPS |
|---|---|---|---|
| Git에 저장되는 것 | 암호화된 SealedSecret | 참조(ExternalSecret) | 암호화된 파일 |
| 복호화 위치 | 클러스터 내 | 외부 저장소 | 클러스터/로컬 |
| 멀티 클러스터 | 불가 (각 클러스터 키 다름) | 가능 (중앙 저장소) | 가능 (같은 키 공유) |
| 비밀 로테이션 | 수동 | 자동 (refreshInterval) | 수동 |
| 외부 의존성 | 없음 | 있음 (Vault, AWS 등) | 선택적 (age vs KMS) |
| Flux 지원 | 지원 | 지원 | 네이티브 지원 |
| ArgoCD 지원 | 지원 | 지원 | 플러그인 지원 |
| 학습 곡선 | 낮음 | 중간 | 중간 |
| 상황 | 권장 솔루션 |
|---|---|
| 단일 클러스터, 빠른 시작 | Sealed Secrets |
| AWS/GCP/Azure 사용, 중앙 관리 | External Secrets Operator |
| 멀티 클러스터, Flux 사용 | SOPS + age |
| 기존 Vault 인프라 | ESO + Vault |
| 규제 요구사항 (감사 로그 필요) | ESO + AWS Secrets Manager |
CI/CD 통합과 Progressive Delivery
전통적인 파이프라인에서는 CI와 CD가 하나의 파이프라인에서 연속으로 실행됩니다. GitOps에서는 둘이 명확히 분리됩니다. CI는 아티팩트(이미지)를 생성하고, CD는 클러스터 내부의 GitOps Agent가 담당합니다. CI가 클러스터에 직접 접근하지 않는다는 것이 핵심 원칙입니다.
flowchart TB
subgraph CI [CI Pipeline - 소스 레포]
Code[코드 변경] --> Build[빌드]
Build --> Test[테스트]
Test --> Push[이미지 Push]
Push --> UpdateManifest[매니페스트 업데이트]
end
subgraph GitOps [GitOps 레포]
Manifest[K8s Manifests]
end
subgraph CD [CD - GitOps Agent]
Agent[ArgoCD / Flux]
Cluster[Kubernetes Cluster]
end
UpdateManifest -->|PR 생성 또는 직접 커밋| Manifest
Manifest -->|Watch| Agent
Agent -->|Sync| Cluster
| 관점 | 통합 CI/CD | 분리된 CI + GitOps |
|---|---|---|
| 배포 권한 | CI가 클러스터 접근 | 클러스터 내부 Agent만 접근 |
| 감사 추적 | CI 로그에만 기록 | Git 커밋 히스토리 |
| 롤백 | 재빌드 필요 | git revert |
| 환경 일관성 | CI 파이프라인에 의존 | Git이 SSOT |
CI에서 매니페스트를 업데이트하는 세 가지 방법
이미지 빌드 후 GitOps 레포의 매니페스트를 갱신하는 방법입니다.
방법 1: CI에서 직접 커밋
# .github/workflows/build.yaml
name: Build and Update Manifest
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Build and push image
run: |
docker build -t ghcr.io/myorg/myapp:${{ github.sha }} .
docker push ghcr.io/myorg/myapp:${{ github.sha }}
- name: Update GitOps repo
env:
GITHUB_TOKEN: ${{ secrets.GITOPS_PAT }}
run: |
git clone https://$GITHUB_TOKEN@github.com/myorg/gitops-repo.git
cd gitops-repo
# Kustomize 사용 시
cd apps/myapp/overlays/prod
kustomize edit set image ghcr.io/myorg/myapp:${{ github.sha }}
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "Update myapp to ${{ github.sha }}"
git push
방법 2: PR 생성. 코드 리뷰 프로세스와 자동화된 테스트를 거쳐 승인 후 배포할 수 있어 직접 커밋보다 안전합니다.
- name: Create PR to GitOps repo
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITOPS_PAT }}
repository: myorg/gitops-repo
branch: update-myapp-${{ github.sha }}
title: "Update myapp to ${{ github.sha }}"
body: |
Automated image update
- Commit: ${{ github.sha }}
- Build: ${{ github.run_id }}
commit-message: "chore: update myapp image to ${{ github.sha }}"
방법 3: 이미지 태그 자동 업데이트. CI는 이미지만 푸시하고 GitOps 쪽 도구가 레지스트리를 감시해 매니페스트를 자동 갱신합니다. 아래에서 자세히 다룹니다.
ArgoCD Image Updater
ArgoCD Image Updater는 컨테이너 레지스트리를 주기적으로 스캔해 새 이미지 태그가 발견되면 매니페스트를 갱신하고, ArgoCD가 이를 Sync합니다.
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
annotations:
# Image Updater 활성화
argocd-image-updater.argoproj.io/image-list: myapp=ghcr.io/myorg/myapp
# 업데이트 전략: semver
argocd-image-updater.argoproj.io/myapp.update-strategy: semver
# SemVer 제약
argocd-image-updater.argoproj.io/myapp.allow-tags: regexp:^v[0-9]+\.[0-9]+\.[0-9]+$
# Git에 커밋 (write-back)
argocd-image-updater.argoproj.io/write-back-method: git
argocd-image-updater.argoproj.io/git-branch: main
spec:
source:
repoURL: https://github.com/myorg/gitops.git
path: apps/myapp
| 전략 | 설명 | 예시 |
|---|---|---|
semver |
SemVer 최신 | v1.2.3 < v1.2.4 < v1.3.0 |
latest |
최신 푸시된 태그 | 날짜/시간 기준 |
name |
알파벳순 최신 | a < b < c |
digest |
특정 태그의 digest 변경 | :latest의 실제 이미지 변경 |
write-back 방식은 git(직접 커밋)과 argocd(오버라이드)가 있습니다. argocd 방식은 Git에 기록되지 않아 GitOps 원칙에 위배되므로, 프로덕션에서는 git 방식을 권장합니다.
Flux Image Automation
Flux는 Image Automation을 핵심 기능으로 내장하고 있습니다. 세 개의 리소스로 구성합니다.
# 1. ImageRepository: 레지스트리 스캔
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: myapp
namespace: flux-system
spec:
image: ghcr.io/myorg/myapp
interval: 5m
secretRef:
name: ghcr-auth
---
# 2. ImagePolicy: 태그 선택 정책
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: myapp
namespace: flux-system
spec:
imageRepositoryRef:
name: myapp
policy:
semver:
range: ">=1.0.0"
---
# 3. ImageUpdateAutomation: Git 업데이트
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: myapp
git:
checkout:
ref:
branch: main
commit:
author:
name: Flux
email: flux@myorg.com
messageTemplate: |
Auto-update images
{{range .Changed.Changes}}
- {{.OldValue}} -> {{.NewValue}}
{{end}}
push:
branch: main
update:
path: ./deploy
strategy: Setters
업데이트 위치는 매니페스트의 마커 주석으로 지정합니다.
# deployment.yaml
spec:
template:
spec:
containers:
- name: myapp
image: ghcr.io/myorg/myapp:v1.2.3 # {"$imagepolicy": "flux-system:myapp"}
Flux가 새 버전을 발견하면 이 라인의 태그를 v1.2.4로 자동 교체해 커밋합니다.
Progressive Delivery: 배포 전략 비교
Progressive Delivery는 새 버전을 점진적으로 배포해 위험을 최소화하는 전략입니다.
| 전략 | 다운타임 | 리소스 | 롤백 속도 | 위험 노출 |
|---|---|---|---|---|
| Recreate | 있음 | 낮음 | 느림 | 높음 |
| Rolling | 없음 | 일시 증가 | 중간 | 중간 |
| Blue-Green | 없음 | 2배 | 빠름 | 낮음 |
| Canary | 없음 | 약간 증가 | 빠름 | 낮음 |
Argo Rollouts
Argo Rollouts는 Kubernetes Deployment를 대체하는 Rollout CRD로 Blue-Green, Canary 배포를 제공합니다.
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
-f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
Canary 배포는 단계별 트래픽 가중치와 대기·승인 지점을 선언합니다.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 5
revisionHistoryLimit: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: ghcr.io/myorg/myapp:v1.2.3
ports:
- containerPort: 8080
strategy:
canary:
steps:
- setWeight: 10 # 10% 트래픽
- pause: {duration: 5m} # 5분 대기
- setWeight: 30
- pause: {duration: 5m}
- setWeight: 50
- pause: {} # 수동 승인 대기
- setWeight: 100
# 분석 실행 (자동 롤백)
analysis:
templates:
- templateName: success-rate
startingStep: 1
args:
- name: service-name
value: myapp
AnalysisTemplate으로 메트릭 기반 자동 롤백을 구성합니다.
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 1m
failureLimit: 3
successCondition: result[0] >= 0.95
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{
service="{{args.service-name}}",
status=~"2.."
}[5m])) /
sum(rate(http_requests_total{
service="{{args.service-name}}"
}[5m]))
Blue-Green은 active/preview 서비스를 나누고 승격 전 분석을 붙입니다.
strategy:
blueGreen:
activeService: myapp-active
previewService: myapp-preview
autoPromotionEnabled: false # 수동 승인
prePromotionAnalysis:
templates:
- templateName: smoke-test
ArgoCD Application의 automated sync(prune, selfHeal)와 함께 쓰면 Git 변경이 Rollout 전략을 따라 점진 배포됩니다.
Flagger
Flagger는 Flux와 함께 사용되는 Progressive Delivery 도구로, Istio, Linkerd, NGINX Ingress 등과 통합됩니다. Flux의 HelmRelease로 설치하며(meshProvider: istio, metricsServer 지정), 기존 Deployment를 그대로 두고 Canary CRD가 이를 참조합니다.
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: myapp
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
# Istio VirtualService 자동 생성
service:
port: 8080
targetPort: 8080
# 분석 설정
analysis:
interval: 1m # 분석 주기
threshold: 5 # 최대 실패 횟수
maxWeight: 50 # 최대 Canary 트래픽
stepWeight: 10 # 단계별 증가량
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
# Webhook 테스트
webhooks:
- name: smoke-test
type: pre-rollout
url: http://flagger-loadtester/
timeout: 30s
metadata:
type: bash
cmd: "curl -s http://myapp-canary:8080/health | grep ok"
sequenceDiagram
participant Git as GitOps Repo
participant Flux as Flux
participant Flagger as Flagger
participant Istio as Istio
participant Prom as Prometheus
Git->>Flux: 새 이미지 감지
Flux->>Flux: Deployment 업데이트
Flagger->>Flagger: Canary Deployment 생성
Flagger->>Istio: VirtualService 업데이트 (10% canary)
loop 분석
Flagger->>Prom: 성공률 확인
alt 성공률 >= 99%
Flagger->>Istio: 트래픽 증가 (20%, 30%, ...)
else 실패
Flagger->>Istio: 롤백 (100% primary)
Flagger->>Flagger: Canary 삭제
end
end
Flagger->>Flux: Primary 업데이트
Flagger->>Istio: 100% primary
선택 가이드
| 관점 | Argo Rollouts | Flagger |
|---|---|---|
| 주 생태계 | ArgoCD | Flux |
| 적용 방식 | Deployment를 Rollout CRD로 대체 | 기존 Deployment를 targetRef로 참조 |
| 배포 전략 | Canary, Blue-Green | Canary (트래픽 점진 증가) |
| 트래픽 제어 | steps의 setWeight/pause 선언 | Istio·Linkerd·NGINX Ingress 통합 |
| 자동 롤백 근거 | AnalysisTemplate (Prometheus 쿼리) | metrics thresholdRange + webhooks |
ArgoCD 중심이면 Argo Rollouts, Flux와 서비스 메시 조합이면 Flagger가 자연스럽습니다.
프로덕션 GitOps 워크플로우
모든 요소를 종합하면 다음 흐름이 됩니다.
flowchart TB
subgraph Dev [개발]
Code[코드 작성]
PR[Pull Request]
Review[코드 리뷰]
end
subgraph CI [CI Pipeline]
Build[빌드]
Test[테스트]
Push[이미지 Push]
end
subgraph GitOps [GitOps Repo]
ImageUpdate[Image Updater<br/>또는 Flux Image Automation]
Manifest[K8s Manifests]
end
subgraph CD [CD - Progressive Delivery]
Agent[ArgoCD / Flux]
Rollout[Argo Rollouts / Flagger]
Canary[Canary 분석]
end
subgraph Prod [Production]
Cluster[Kubernetes Cluster]
Monitor[Prometheus/Grafana]
end
Code --> PR --> Review
Review -->|Merge| Build --> Test --> Push
Push -->|새 이미지 감지| ImageUpdate
ImageUpdate --> Manifest
Manifest --> Agent
Agent --> Rollout --> Canary
Canary -->|성공| Cluster
Canary <-->|메트릭 수집| Monitor
Canary -->|실패| Rollback[자동 롤백]
레포지토리 구조는 클러스터, 인프라, 앱, 이미지 자동화를 분리하는 형태로 수렴합니다.
gitops-repo/
├── clusters/
│ ├── dev/
│ │ └── kustomization.yaml
│ ├── staging/
│ │ └── kustomization.yaml
│ └── prod/
│ └── kustomization.yaml
│
├── infrastructure/
│ ├── base/
│ │ ├── ingress-nginx/
│ │ ├── cert-manager/
│ │ ├── monitoring/
│ │ └── flagger/
│ └── overlays/
│ ├── dev/
│ └── prod/
│
├── apps/
│ ├── frontend/
│ │ ├── base/
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ ├── canary.yaml # Flagger Canary
│ │ │ └── kustomization.yaml
│ │ └── overlays/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── prod/
│ └── backend/
│ └── ...
│
└── image-automation/ # Flux Image Automation
├── image-repositories.yaml
├── image-policies.yaml
└── image-update-automation.yaml
정리
하편에서 다룬 세 주제를 요약하면 다음과 같습니다.
| 주제 | 핵심 메시지 |
|---|---|
| 환경별 설정 관리 | 내부 앱은 Kustomize(순수 YAML), 써드파티·복잡 로직은 Helm(템플릿), 필요 시 post-rendering 하이브리드 |
| 시크릿 관리 | 단순 시작은 Sealed Secrets, 중앙 관리·자동 로테이션은 ESO, 멀티 클러스터·Flux는 SOPS |
| CI/CD 통합 | CI는 이미지 생성까지만, 배포는 GitOps Agent가 담당, 릴리스는 Rollouts·Flagger로 점진 배포 |
GitOps 도입은 다음 성숙도 단계를 따라 올라가는 것이 자연스럽습니다.
flowchart LR
L1[Level 1<br/>Git에 매니페스트 저장]
L2[Level 2<br/>GitOps Agent 도입]
L3[Level 3<br/>환경별 설정 분리]
L4[Level 4<br/>Secrets 자동화]
L5[Level 5<br/>Progressive Delivery]
L1 --> L2 --> L3 --> L4 --> L5
권장 순서는 ArgoCD 또는 Flux 설치, 간단한 앱으로 워크플로우 경험, Kustomize로 환경별 설정 분리, Sealed Secrets 또는 ESO 도입, Image Updater 자동화, 마지막으로 Argo Rollouts 또는 Flagger로 Progressive Delivery 구축입니다. GitOps는 단순한 도구가 아니라 Git을 중심으로 선언적 인프라를 관리하고 자동화된 Reconciliation으로 일관성을 유지하는 운영 철학입니다.