Skip to content
Published on

Ingress를 코드로 관리하기 — Helm, Kustomize, GitOps

Authors

들어가며

Ingress 설정은 한 번 만들면 끝나는 정적인 대상이 아닙니다. TLS 인증서를 갱신하고, 타임아웃과 바디 크기 한도를 조정하고, 레이트 리밋 어노테이션을 추가하고, 새 서비스가 생길 때마다 경로를 늘립니다. 그리고 이 모든 변경은 dev, staging, production이라는 여러 환경에서 미묘하게 다른 값으로 적용돼야 합니다. 누군가 kubectl edit ingress로 production을 직접 고치는 순간, 그 변경은 아무 기록도 없이 사라질 운명에 놓입니다.

그래서 Ingress야말로 코드로 관리되어야 하는 대표적인 대상입니다. 컨트롤러 자체의 배포(어떤 버전을, 어떤 리소스로, 어떤 ConfigMap으로)와 개별 Ingress 매니페스트(호스트, 경로, 어노테이션, TLS) 양쪽 모두를 Git에 두고, 선언적으로 클러스터에 적용해야 합니다. 이것이 GitOps의 핵심 가치입니다. Git이 단일 진실 공급원(single source of truth)이 되고, 클러스터 상태는 항상 Git을 향해 수렴합니다.

이 글에서는 Ingress 형상관리를 네 단계로 나눠 다룹니다. 첫째, Helm values로 컨트롤러를 환경별로 배포하기. 둘째, Kustomize로 Ingress 매니페스트를 템플릿화하고 환경별 patch를 얹기. 셋째, Argo CD와 Flux로 배포와 드리프트 감지를 자동화하기. 넷째, Kyverno 같은 정책 엔진으로 필수 어노테이션과 IngressClass를 강제하는 게이트를 세우기. 2026년 현재 Ingress API는 동결 상태이고 Gateway API가 후계 표준이지만, 여기서 다루는 형상관리 패턴은 두 API 모두에 동일하게 적용된다는 점도 짚어 둡니다.

형상관리가 필요한 것들

Ingress 영역에서 Git에 담아야 할 대상은 크게 두 층입니다.

대상변경 빈도관리 도구
플랫폼컨트롤러 배포, ConfigMap, IngressClass낮음(분기별)Helm values
애플리케이션Ingress 매니페스트(호스트/경로/TLS/어노테이션)높음(상시)Kustomize/Helm

이 둘을 분리하는 것이 중요합니다. 컨트롤러는 플랫폼 팀이 신중하게 버전과 보안 패치를 관리하고, Ingress 매니페스트는 각 애플리케이션 팀이 자기 서비스에 맞춰 자주 바꿉니다. 두 층을 하나의 거대한 차트에 섞으면 변경 영향 범위가 커지고 리뷰가 어려워집니다.

  Git 저장소
  ├── platform/ingress-controller/   (Helm values, 환경별)
  │     ├── values-base.yaml
  │     ├── values-dev.yaml
  │     └── values-prod.yaml
  └── apps/<service>/ingress/        (Kustomize base + overlays)
        ├── base/
        └── overlays/{dev,staging,prod}/

Helm으로 컨트롤러 배포

ingress-nginx, Traefik 등 주요 컨트롤러는 모두 공식 Helm 차트를 제공합니다. 핵심은 환경별로 다른 values를 두되, 공통 부분은 base values에 모으는 것입니다.

base values 예시입니다.

controller:
  replicaCount: 2
  ingressClassResource:
    name: nginx
    default: false
  config:
    use-forwarded-headers: "true"
    proxy-body-size: "16m"
  resources:
    requests:
      cpu: 100m
      memory: 128Mi

production 오버레이에서는 레플리카와 리소스를 키우고, 외부 노출 방식을 명시합니다.

controller:
  replicaCount: 4
  service:
    type: LoadBalancer
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
  resources:
    requests:
      cpu: 500m
      memory: 512Mi
  config:
    proxy-body-size: "64m"

배포는 두 values 파일을 겹쳐 적용합니다.

helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace \
  -f values-base.yaml -f values-prod.yaml

이렇게 하면 production과 dev의 차이가 작은 오버레이 파일 하나로 명확히 드러나, "왜 이 환경만 바디 크기 한도가 다르지?" 같은 질문에 Git diff로 즉시 답할 수 있습니다.

Kustomize로 Ingress 매니페스트 템플릿화

개별 Ingress 매니페스트는 Kustomize가 잘 맞습니다. 공통 구조를 base에 두고, 환경별로 호스트나 TLS, 어노테이션만 patch로 바꾸는 방식입니다.

base의 Ingress입니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
  ingressClassName: nginx
  rules:
    - host: web.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

base의 kustomization입니다.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ingress.yaml

production 오버레이에서는 호스트와 TLS를 patch로 바꿉니다.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
patches:
  - target:
      kind: Ingress
      name: web
    patch: |-
      - op: replace
        path: /spec/rules/0/host
        value: web.prod.example.com
      - op: add
        path: /spec/tls
        value:
          - hosts:
              - web.prod.example.com
            secretName: web-prod-tls

공통 어노테이션을 모든 Ingress에 일괄 추가하고 싶다면 commonAnnotations를 쓸 수 있습니다.

commonAnnotations:
  team: platform
  managed-by: kustomize

이 패턴의 장점은 base가 단 하나의 진실이라는 것입니다. 모든 환경이 base를 공유하므로, 공통 로직을 한 곳에서 고치면 전 환경에 일관되게 반영됩니다.

Argo CD로 배포와 드리프트 감지

이제 Git에 정리된 매니페스트를 클러스터에 자동 적용할 차례입니다. Argo CD는 Application 리소스로 "이 Git 경로를 이 클러스터/네임스페이스에 동기화하라"를 선언합니다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-ingress-prod
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/infra.git
    targetRevision: main
    path: apps/web/ingress/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: web
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

핵심은 selfHeal: trueprune: true입니다. 누군가 kubectl edit로 클러스터의 Ingress를 직접 바꾸면, Argo CD는 이를 드리프트로 감지하고 Git 상태로 되돌립니다. Git에서 리소스를 지우면 클러스터에서도 제거합니다. 이로써 "클러스터 = Git" 불변식이 유지됩니다.

Flux를 쓴다면 Kustomization 리소스로 같은 일을 합니다.

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: web-ingress
  namespace: flux-system
spec:
  interval: 5m
  path: ./apps/web/ingress/overlays/prod
  prune: true
  sourceRef:
    kind: GitRepository
    name: infra

interval: 5m마다 Git과 클러스터를 비교하고, 차이가 있으면 조정합니다. 드리프트 감지의 본질은 같습니다.

정책 게이트 — Kyverno로 강제

GitOps만으로는 "잘못된 Ingress가 Git에 머지되는 것" 자체를 막지는 못합니다. 그래서 클러스터 진입 직전에 정책 게이트를 둡니다. Kyverno는 어드미션 웹훅으로 동작해, 규칙을 어기는 리소스를 거부하거나 자동 변형합니다.

첫째, 모든 Ingress에 IngressClass를 강제하는 정책입니다. 클래스가 없으면 어느 컨트롤러가 처리할지 모호해지므로 이를 막습니다.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-ingress-class
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-ingress-class
      match:
        any:
          - resources:
              kinds:
                - Ingress
      validate:
        message: "모든 Ingress는 spec.ingressClassName을 지정해야 합니다."
        pattern:
          spec:
            ingressClassName: "?*"

둘째, 보안상 필수 어노테이션(예: SSL 리다이렉트 강제)을 요구하는 정책입니다.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-ssl-redirect
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-ssl-redirect
      match:
        any:
          - resources:
              kinds:
                - Ingress
      validate:
        message: "Ingress는 force-ssl-redirect 어노테이션이 필요합니다."
        pattern:
          metadata:
            annotations:
              nginx.ingress.kubernetes.io/force-ssl-redirect: "true"

이런 정책을 두면, 실수로 평문 HTTP만 노출하거나 클래스 없는 Ingress를 만드는 것을 클러스터 수준에서 차단할 수 있습니다. GitOps(원하는 상태 보장)와 정책 엔진(허용 가능한 상태 강제)은 상호 보완적입니다.

멀티 환경·멀티 클러스터

규모가 커지면 환경이 여럿이고 클러스터도 여럿입니다. Argo CD는 ApplicationSet으로 동일한 패턴을 여러 대상에 펼칩니다. 예를 들어 클러스터 목록을 generator로 두고, 각 클러스터에 같은 Ingress 오버레이를 자동 배포합니다. 이때도 base는 공유하고 클러스터별 차이만 오버레이로 표현하는 원칙은 동일합니다.

시크릿(TLS) 관리

TLS 인증서는 형상관리의 까다로운 부분입니다. 평문 Secret을 Git에 그대로 넣을 수는 없기 때문입니다. 실무에서는 두 가지 접근이 표준입니다.

  • cert-manager로 자동 발급: Ingress 어노테이션에 issuer를 지정하면 cert-manager가 ACME(Let's Encrypt 등)로 인증서를 발급·갱신합니다. 인증서 자체는 Git에 없고, 선언만 Git에 둡니다.
  • 암호화 시크릿: 외부 시크릿 매니저나 봉인된(Sealed) 시크릿으로 암호화한 형태만 Git에 두고, 클러스터에서 복호화합니다.

cert-manager 방식의 Ingress 어노테이션 예시입니다.

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

이렇게 하면 인증서 갱신까지 선언적으로 관리되어, 만료 임박 알림에 수동 대응하던 작업이 사라집니다.

리뷰 프로세스

GitOps의 진짜 가치는 변경이 Pull Request를 거친다는 데서 나옵니다. Ingress 변경에 대한 권장 리뷰 흐름입니다.

1. 브랜치에서 오버레이 patch 수정
2. CI에서 kustomize build + kubeconform으로 스키마 검증
3. CI에서 Kyverno CLI로 정책 사전 검사
4. Argo CD diff를 PR 코멘트로 자동 게시(실제 적용 전 변경 미리보기)
5. 동료 리뷰 후 머지 -> Argo CD가 자동 동기화

핵심은 "적용 전에 본다"입니다. Argo CD diff를 PR에 붙이면, 머지 결과가 클러스터에 어떤 변화를 일으킬지 사람이 눈으로 확인한 뒤 승인할 수 있습니다.

체크리스트

[ ] 컨트롤러(플랫폼)와 Ingress(앱) 형상관리를 분리했는가
[ ] Helm base values + 환경 오버레이로 컨트롤러를 배포하는가
[ ] Kustomize base 하나를 모든 환경이 공유하는가
[ ] Argo CD/Flux에서 selfHeal과 prune을 켰는가
[ ] Kyverno로 IngressClass와 필수 어노테이션을 강제하는가
[ ] TLS는 cert-manager 또는 암호화 시크릿으로 관리하는가
[ ] CI에서 build/스키마/정책 검증을 거치는가
[ ] PR에 Argo CD diff를 게시해 적용 전 검토하는가

마치며

Ingress를 코드로 관리한다는 것은 단순히 YAML을 Git에 넣는 일이 아닙니다. 컨트롤러와 매니페스트를 두 층으로 분리하고, Helm과 Kustomize로 환경 차이를 명시적으로 표현하고, Argo CD나 Flux로 클러스터를 Git에 수렴시키고, Kyverno로 안전 가드레일을 세우는, 일관된 시스템을 만드는 일입니다.

이 시스템이 자리 잡으면 "production Ingress를 누가, 언제, 왜 바꿨는가"라는 질문에 Git 히스토리로 답할 수 있고, 잘못된 변경은 정책 게이트와 드리프트 감지가 막아 줍니다. 그리고 이 패턴은 Ingress API든 후계 표준인 Gateway API든 동일하게 적용되므로, 향후 Gateway API로 이행하더라도 형상관리 체계는 그대로 가져갈 수 있습니다.

참고 자료