DigitalOcean에서 GitOps 구축하기 (2) - ExternalDNS로 DNS 자동화

2025년 12월 04일

devops

# ExternalDNS# Kubernetes# DigitalOcean# GitOps# DNS

시리즈 소개

시리즈 구성

  1. 왜 GitOps인가?
  2. Kubernetes 클러스터와 Traefik
  3. ExternalDNS - 자동 DNS 레코드 관리 (현재 글)
  4. SOPS + age - 시크릿 암호화
  5. ArgoCD - GitOps 배포 자동화

ExternalDNS란?

ExternalDNS는 Kubernetes 리소스(Service, Ingress 등)를 기반으로 DNS 레코드를 자동으로 생성/관리하는 컨트롤러입니다.

왜 ExternalDNS가 필요한가?

ExternalDNS 없이:

DNS ProviderKubernetes개발자DNS ProviderKubernetes개발자매번 수동 작업 필요Ingress 생성 (app.example.com)DNS 콘솔 접속A 레코드 수동 추가

ExternalDNS 사용 시:

DNS ProviderExternalDNSKubernetes개발자DNS ProviderExternalDNSKubernetes개발자자동화!Ingress 생성 (app.example.com)Ingress 감지A 레코드 자동 생성

지원하는 DNS Provider

ExternalDNS는 다양한 DNS 프로바이더를 지원합니다:

  • DigitalOcean DNS (이 글에서 사용)
  • Cloudflare
  • AWS Route53
  • Google Cloud DNS
  • Azure DNS
  • 그 외 다수

ExternalDNS 설치

Helmfile 설정

# helmfile.yaml
repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami

releases:
  - name: external-dns
    namespace: external-dns
    createNamespace: true
    chart: bitnami/external-dns
    version: 8.8.2
    values:
      - ./values/external-dns/common.yaml
      - ./values/external-dns/{{ .Environment.Name }}.yaml

values 파일

# values/external-dns/common.yaml
provider: digitalocean

# 어떤 소스에서 DNS 정보를 가져올지
sources:
  - service
  - ingress
  - traefik-proxy  # Traefik IngressRoute 지원

# traefik-proxy source 사용 시 필요
extraArgs:
  traefik-disable-legacy:

# 관리할 도메인 필터
domainFilters:
  - "example.com"

# 동기화 주기
interval: "1m"

# 동기화 정책: sync (삭제도 동기화) vs upsert-only (추가만)
policy: sync

# TXT 레코드로 소유권 관리
registry: txt
txtOwnerId: "external-dns"

logLevel: info

# DigitalOcean API 토큰이 저장된 Secret
digitalocean:
  secretName: digitalocean-dns-secret

rbac:
  create: true

serviceAccount:
  create: true
  name: external-dns

DigitalOcean API Token Secret 생성

ExternalDNS가 DigitalOcean DNS API를 호출하려면 API 토큰이 필요합니다.

# DigitalOcean Console에서 API Token 생성
# https://cloud.digitalocean.com/account/api/tokens

# Secret 생성
kubectl create secret generic digitalocean-dns-secret \
  --namespace external-dns \
  --from-literal=digitalocean_token=<YOUR_DO_API_TOKEN>

이 Secret은 3편 SOPS + age에서 암호화하여 Git에 저장하는 방법을 다룹니다.

설치 실행

helmfile -e dev sync

설치 확인

# Pod 상태 확인
kubectl get pods -n external-dns

# 로그 확인
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns -f

동작 방식

DNS 레코드 생성 흐름

있음

없음

Ingress 생성

ExternalDNS 감지

hostname 어노테이션?

DNS API 호출

host 필드 확인

A 레코드 생성

TXT 레코드 생성

TXT 레코드의 역할

ExternalDNS는 각 DNS 레코드에 대응하는 TXT 레코드를 함께 생성합니다.

# 실제 생성되는 레코드 예시
app.example.com.           A     143.xxx.xxx.xxx
app.example.com.           TXT   "heritage=external-dns,external-dns/owner=external-dns"

TXT 레코드가 필요한 이유:

  1. 소유권 표시: ExternalDNS가 관리하는 레코드임을 표시
  2. 충돌 방지: 다른 ExternalDNS 인스턴스나 수동 레코드와 구분
  3. 안전한 삭제: policy: sync 사용 시 자신이 생성한 레코드만 삭제

Ingress와 연동

어노테이션 사용

Ingress에 external-dns.alpha.kubernetes.io/hostname 어노테이션을 추가하면 해당 도메인으로 DNS 레코드가 생성됩니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    # ExternalDNS가 이 어노테이션을 읽어 DNS 레코드 생성
    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    # Traefik TLS 설정
    traefik.ingress.kubernetes.io/router.tls: "true"
    traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
spec:
  ingressClassName: traefik
  rules:
    - host: my-app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  name: http

실제 사용 예시 (JVM 앱)

저희 팀에서 사용하는 실제 values 파일입니다:

# values/kohi-core-api/values.yaml
nameOverride: kohi-core-api
fullnameOverride: kohi-core-api

replicaCount: 2

ingress:
  annotations:
    traefik.ingress.kubernetes.io/router.tls: "true"
    traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
    external-dns.alpha.kubernetes.io/hostname: kohi-api.dev.example.com
  hosts:
    - host: kohi-api.dev.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - hosts:
        - kohi-api.dev.example.com

app:
  resources:
    requests:
      cpu: 300m
      memory: 768Mi

service:
  type: ClusterIP
  ports:
    - name: http
      port: 8080
      targetPort: 8080
      protocol: TCP

배포 시 자동으로 일어나는 일:

  1. ArgoCD가 Helm Chart를 배포
  2. Ingress 리소스 생성
  3. ExternalDNS가 Ingress 감지
  4. kohi-api.dev.example.com A 레코드 자동 생성
  5. Traefik이 Let’s Encrypt 인증서 자동 발급
  6. HTTPS로 서비스 접근 가능

환경별 도메인 구조

개발/운영 환경을 서브도메인으로 구분하면 관리가 편합니다.

example.com
├── dev.example.com      # 개발 환경
│   ├── api.dev.example.com
│   └── app.dev.example.com
├── staging.example.com   # 스테이징 환경
└── example.com          # 운영 환경
    ├── api.example.com
    └── app.example.com

환경별 values 파일

# values/external-dns/dev.yaml
domainFilters:
  - "dev.example.com"

# values/external-dns/prod.yaml
domainFilters:
  - "example.com"

트러블슈팅

DNS 레코드가 생성되지 않는 경우

# 1. ExternalDNS 로그 확인
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns

# 2. Ingress 어노테이션 확인
kubectl get ingress my-app -o yaml | grep external-dns

# 3. domainFilters 확인 (필터에 해당 도메인이 포함되어 있는지)

# 4. API 토큰 권한 확인 (DigitalOcean)
# DNS write 권한이 있는지 확인

일반적인 오류와 해결

증상 원인 해결
level=error msg="failed to list... API 토큰 문제 Secret 확인, 토큰 권한 확인
레코드 생성 안됨 domainFilters 미포함 도메인 필터 설정 확인
레코드 중복 txtOwnerId 충돌 각 환경별 고유 ID 사용

다음 글 예고

다음 글에서는 SOPS + age를 사용하여 Kubernetes Secret을 안전하게 Git에 저장하는 방법을 다룹니다. API 토큰, 데이터베이스 비밀번호 등 민감한 정보도 GitOps로 관리할 수 있습니다.


참고 자료

© 2025, 미나리와 함께 만들었음