쿠버네티스 일반/Network

EKS External DNS - Terraform IRSA + Helm

Jerry_이정훈 2023. 6. 23. 16:49
728x90

TL; DR;

. IRSA(by 테라폼) + 헬름으로 External DNS 설치 경험을 공유합니다.

 

. eksctl도 가능하지만 테라폼을 이용하여 IRSA를 설치하면 코드(IaC)로 남아서 좀 더 관리가 용이합니다.

 

1. External DNS IRSA

테라폼으로 EKS 관련 모듈(ebs-csi-controller, aws load balancer controller)을 IRSA + 헬름까지 설치할 수 있습니다. 하지만 테라폼으로 헬름까지 설치하는 것은 테라폼 State가 맞지 않는 경우 디버깅을 해야 하는 위험 + 번거로움이 있어 개인적으로는 테라폼은 IRSA까지만 설치하는 것을 선호합니다.

 

AWS 관련 리소스(IAM, VPC 등)는 테라폼으로 쿠버네티스 애플리케이션은 헬름 + ArgoCD로 설치, 관리합니다.

 

테라폼으로 IRSA만 설치하는 건 검색해도 잘 나오지 않습니다. 아마도 많이 사용하지 않는 것 같아 그렇겠죠. 저만 Best Practice를 따르지 않는 것 같아 꺼림직하지만 ^^ 저(+ 우리 회사 동료 1명 더)의 정책을 고수합니다.

 

물론 검색을 좀 더 잘하면 테라폼에서 IRSA만 모아둔 깃헙을 찾을 수 있습니다. (링크) 해당 링크를 참조하여 우리가 필요한 External DNS의 IRSA를 찾습니다.

module "external_dns_irsa_role" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

  role_name                     = "external-dns"
  attach_external_dns_policy    = true
  external_dns_hosted_zone_arns = ["arn:aws:route53:::hostedzone/IClearlyMadeThisUp"]

  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:external-dns"]
    }
  }

  tags = local.tags
}

. source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
공식 깃헙 예제는 같은 디렉토리라 'source = "../../modules/iam-role-for-service-accounts-eks" ' 인데 우리는 외부에서 바로 모듈을 사용하니 깃헙 Full Path를 적어줍니다.


. external_dns_hosted_zone_arns = ["arn:aws:route53:::hostedzone/IClearlyMadeThisUp"]
'IClearlyMadeThisUp'를 각자의 hosted_zone ID로 변경합니다. 저도 처음에 헤맸는데 aws Roue 53 console에서 아래와 같이 확인할 수 있습니다.

 

Route 53 콘솔에서 확인합니다.

이제 위 external dns을 기존 EKS를 만든 테라폼 코드에 포함합니다. 필자의 예제는 여기서 확인할 수 있습니다.

 

tf init, tf plan -out planfile, tf appy planfile 순서로 테라폼 명령어를 적용하면 external dns의 IAM policy, role 등을 확인할 수 있습니다. 

 

신규 생성된 external-dns Role

위와 같이 external-dns의 Role이 정상적으로 생성됩니다. IRSA를 위한 Role, Policy가 생성되었으므로 external dns을 헬름으로 설치합니다. 

 

2. External DNS 헬름 설치

external dns 헬름 차트를 로컬에 Pull 합니다.

helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm pull external-dns/external-dns

설치에 사용할 Value 파일을 하나 더 복사하고(my-values.yaml) 아래와 같이 serviceAccount 설정을 변경합니다.

serviceAccount:
  create: true
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::XXXXXXX:role/external-dns

. eks.amazonaws.com/role-arn: arn:aws:iam::XXXXXXX:role/external-dns
위에서 생성한 Role의 ARN 지정합니다. 기본 설정에 계정 ID를 추가합니다.

 

이제 지정한 my-values.yaml 기준으로 헬름을 설치합니다.

$ (⎈ |switch-singapore-test:kube-system) helm install external-dns -f my-values.yaml .
NAME: external-dns
LAST DEPLOYED: Fri Jun 23 13:55:32 2023
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
***********************************************************************
* External DNS                                                        *
***********************************************************************
  Chart version: 1.12.2
  App version:   0.13.4
  Image tag:     registry.k8s.io/external-dns/external-dns:v0.13.4
***********************************************************************

설치가 완료되면 external dns 파드를 확인할 수 있습니다.

$ (⎈ |switch-singapore-test:kube-system) k get pod external-dns-5555f6796f-8bmxr 
NAME                            READY   STATUS    RESTARTS   AGE
external-dns-5555f6796f-8bmxr   1/1     Running   0          69m

그런데, 만약 설치에서 IRSA 설정이 안되어 있으면 파드는 'Running'이지만 아래와 같이 로그는 정상적이지 않습니다.

time="2023-06-23T04:55:38Z" level=info msg="Created Kubernetes client https://172.20.0.1:443"
time="2023-06-23T04:55:39Z" level=error msg="records retrieval failed: failed to list hosted zones: AccessDenied: User: arn:aws:sts::XXXXXXX:assumed-role/base-eks-node-group-2023052207535708960000000d/i-02473c9cf2d5895ae is not authorized to perform: route53:ListHostedZones because no identity-based policy allows the route53:ListHostedZones action\n\tstatus code: 403, request id: 88733b6b-3a54-42d3-ab09-d47e2864cdec"

'403' 로그에서 확인할 수 있듯이 권한이 제대로 설정되지 않았습니다. 다시 한 번 확인하고 정상으로 설치되면 아래의 로그와 같습니다.

$ (⎈ |switch-singapore-test:kube-system) k logs external-dns-5555f6796f-8bmxr -f
time="2023-06-23T06:09:44Z" level=info msg="config: {APIServerURL: KubeConfig: RequestTimeout:30s DefaultTargets:[] ContourLoadBalancerService:heptio-contour/contour GlooNamespace:gloo-system SkipperRouteGroupVersion:zalando.org/v1 Sources:[service ingress] Namespace: AnnotationFilter: LabelFilter: FQDNTemplate: CombineFQDNAndAnnotation:false IgnoreHostnameAnnotation:false IgnoreIngressTLSSpec:false IgnoreIngressRulesSpec:false GatewayNamespace: GatewayLabelFilter: Compatibility: PublishInternal:false PublishHostIP:false AlwaysPublishNotReadyAddresses:false ConnectorSourceServer:localhost:8080 Provider:aws GoogleProject: GoogleBatchChangeSize:1000 GoogleBatchChangeInterval:1s GoogleZoneVisibility: DomainFilter:[] ExcludeDomains:[] RegexDomainFilter: RegexDomainExclusion: ZoneNameFilter:[] ZoneIDFilter:[] TargetNetFilter:[] ExcludeTargetNets:[] AlibabaCloudConfigFile:/etc/kubernetes/alibaba-cloud.json AlibabaCloudZoneType: AWSZoneType: AWSZoneTagFilter:[] AWSAssumeRole: AWSAssumeRoleExternalID: AWSBatchChangeSize:1000 AWSBatchChangeInterval:1s AWSEvaluateTargetHealth:true AWSAPIRetries:3 AWSPreferCNAME:false AWSZoneCacheDuration:0s AWSSDServiceCleanup:false AzureConfigFile:/etc/kubernetes/azure.json AzureResourceGroup: AzureSubscriptionID: AzureUserAssignedIdentityClientID: BluecatDNSConfiguration: BluecatConfigFile:/etc/kubernetes/bluecat.json BluecatDNSView: BluecatGatewayHost: BluecatRootZone: BluecatDNSServerName: BluecatDNSDeployType:no-deploy BluecatSkipTLSVerify:false CloudflareProxied:false CloudflareDNSRecordsPerPage:100 CoreDNSPrefix:/skydns/ RcodezeroTXTEncrypt:false AkamaiServiceConsumerDomain: AkamaiClientToken: AkamaiClientSecret: AkamaiAccessToken: AkamaiEdgercPath: AkamaiEdgercSection: InfobloxGridHost: InfobloxWapiPort:443 InfobloxWapiUsername:admin InfobloxWapiPassword: InfobloxWapiVersion:2.3.1 InfobloxSSLVerify:true InfobloxView: InfobloxMaxResults:0 InfobloxFQDNRegEx: InfobloxNameRegEx: InfobloxCreatePTR:false InfobloxCacheDuration:0 DynCustomerName: DynUsername: DynPassword: DynMinTTLSeconds:0 OCIConfigFile:/etc/kubernetes/oci.yaml InMemoryZones:[] OVHEndpoint:ovh-eu OVHApiRateLimit:20 PDNSServer:http://localhost:8081 PDNSAPIKey: PDNSTLSEnabled:false TLSCA: TLSClientCert: TLSClientCertKey: Policy:upsert-only Registry:txt TXTOwnerID:default TXTPrefix: TXTSuffix: Interval:1m0s MinEventSyncInterval:5s Once:false DryRun:false UpdateEvents:false LogFormat:text MetricsAddress::7979 LogLevel:info TXTCacheInterval:0s TXTWildcardReplacement: ExoscaleEndpoint:https://api.exoscale.ch/dns ExoscaleAPIKey: ExoscaleAPISecret: CRDSourceAPIVersion:externaldns.k8s.io/v1alpha1 CRDSourceKind:DNSEndpoint ServiceTypeFilter:[] CFAPIEndpoint: CFUsername: CFPassword: RFC2136Host: RFC2136Port:0 RFC2136Zone: RFC2136Insecure:false RFC2136GSSTSIG:false RFC2136KerberosRealm: RFC2136KerberosUsername: RFC2136KerberosPassword: RFC2136TSIGKeyName: RFC2136TSIGSecret: RFC2136TSIGSecretAlg: RFC2136TAXFR:false RFC2136MinTTL:0s RFC2136BatchChangeSize:50 NS1Endpoint: NS1IgnoreSSL:false NS1MinTTLSeconds:0 TransIPAccountName: TransIPPrivateKeyFile: DigitalOceanAPIPageSize:50 ManagedDNSRecordTypes:[A CNAME] GoDaddyAPIKey: GoDaddySecretKey: GoDaddyTTL:0 GoDaddyOTE:false OCPRouterName: IBMCloudProxied:false IBMCloudConfigFile:/etc/kubernetes/ibmcloud.json TencentCloudConfigFile:/etc/kubernetes/tencent-cloud.json TencentCloudZoneType: PiholeServer: PiholePassword: PiholeTLSInsecureSkipVerify:false PluralCluster: PluralProvider:}"
time="2023-06-23T06:09:44Z" level=info msg="Instantiating new Kubernetes client"
time="2023-06-23T06:09:44Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2023-06-23T06:09:44Z" level=info msg="Created Kubernetes client https://172.20.0.1:443"
time="2023-06-23T06:09:48Z" level=info msg="Applying provider record filter for domains: [xxxx.com, ]"

로그가 정상으로 확인되면 준비가 되었습니다. 새로운 Ingress를 만들어서 Ingress에 등록한 DNS가 Route 53에 정상으로 등록되는지 확인합니다.

 

테스트에 사용한 Ingress 예제는 aws load balancer controller를 따릅니다. (필자 깃헙)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: echoserver
  namespace: echoserver
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/group.name: switch-singpore-test-alb-group
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    external-dns.alpha.kubernetes.io/hostname: test-01.ap-southeast-1.testweb.com
spec:
  ingressClassName: alb
  rules:
  - host: test-01.ap-southeast-1.testweb.com
    http:
      paths:
      - path: /echo-server
        pathType: Prefix
        backend:
          service:
            name: echoserver
            port:
              number: 80

. annotations.external-dns.alpha.kubernetes.io/hostname: test-01.ap-southeast-1.testweb.com
Ingress 매니페스트의 annotations으로 external dns 설정을 추가합니다. external dns controller가 이벤트를 감지하고 있다가 external dns 설정이 포함된 ingress가 등록되면 이를 외부의 AWS Route53의 dns로 등록합니다. 다른 IRSA와 마찬가지로 쿠버네티스 리소스가 외부 AWS 리소스를 제어하므로 별도의 IRSA가 필요합니다.

 

위 설명을 좀 더 자세하게 챗GPT 통해서 알아보겠습니다.

 

ExternalDNS는 Kubernetes 클러스터에서 실행되는 컨트롤러로서, 주기적으로 Kubernetes API 서버를 폴링하여 변경된 서비스 및 인그레스 리소스를 감지하고 외부 DNS 서비스에 해당하는 도메인의 DNS 레코드를 자동으로 생성하거나 갱신합니다. 이를 통해 클러스터 내부 리소스에 대한 외부 도메인 이름을 유지하고 관리합니다.

다음은 ExternalDNS의 동작 원리를 단계별로 설명합니다:

1. ExternalDNS 구성: ExternalDNS는 클러스터 내에서 실행되는 컨트롤러로서, Kubernetes API 서버와 통신하여 리소스 변경을 감지하고 DNS 레코드를 관리합니다. ExternalDNS를 배포하기 위해 Kubernetes Deployment 또는 DaemonSet을 생성하고 필요한 권한 및 설정을 제공합니다.

2. 리소스 감지: ExternalDNS는 일정한 간격으로 Kubernetes API 서버를 폴링하여 변경된 서비스 및 인그레스 리소스를 감지합니다. 이를 위해 Kubernetes API 서버와의 통신에는 Kubernetes 클라이언트 라이브러리를 사용합니다.

3. 외부 도메인 이름 확인: ExternalDNS는 변경된 리소스에서 외부 도메인 이름을 확인합니다. 이를 위해 Kubernetes 리소스의 애너테이션 또는 레이블을 사용하거나, 특정 네임스페이스 또는 리소스 유형에 대한 규칙을 설정할 수 있습니다.

4. 외부 DNS 서비스와 통합: ExternalDNS는 외부 DNS 서비스와의 통합을 위해 해당 서비스의 API를 사용합니다. 예를 들어, AWS Route 53과 통합할 경우 AWS API를 호출하여 도메인의 DNS 레코드를 생성하거나 갱신합니다. ExternalDNS는 각 외부 DNS 서비스에 대한 인증 정보 및 구성 설정을 제공하여 해당 서비스에 접근합니다.

5. DNS 레코드 생성 및 갱신: ExternalDNS는 감지된 변경 사항에 따라 외부 DNS 서비스에 대응하는 도메인의 DNS 레코드를 생성하거나 갱신합니다. 변경된 리소스의 외부 도메인 이름과 해당하는 IP 주소 또는 CNAME 값을 사용하여 DNS 레코드를 생성하거나 갱신합니다.

6. 주기적인 동기화: ExternalDNS는 일정한 주기로 리소스를 감지하고 DNS 레코드를 동기화합니다. 이를 통해 클러스터 내의 서비스에 대한 외부 도메인 이름을 최신 상태로 유지합니다.

ExternalDNS는 이러한 과정을 반복하여 Kubernetes 클러스터 내의 서비스

 및 인그레스 리소스에 대한 외부 도메인 이름을 자동으로 할당하고 관리합니다. 이를 통해 클러스터 내의 리소스에 접근하기 위해 의미 있는 도메인 이름을 사용할 수 있으며, 변경된 리소스에 따라 DNS 레코드가 자동으로 업데이트됩니다.

 

위 Ingress 매니페스트를 적용(kubectl apply)하고 external dns 로그를 확인하면 정상적인 로그를 확인할 수 있습니다.

time="2023-06-23T06:13:50Z" level=info msg="Desired change: CREATE xxxxx.xxxx.xxx TXT [Id: /hostedzone/Z067635620DEBWCAUD00G]"
time="2023-06-23T06:13:50Z" level=info msg="Desired change: CREATE xxxxx.xxxx.xxx A [Id: /hostedzone/Z067635620DEBWCAUD00G]"
time="2023-06-23T06:13:50Z" level=info msg="Desired change: CREATE xxxxx.xxxx.xxx TXT [Id: /hostedzone/Z067635620DEBWCAUD00G]"

물론 AWS 콘솔에서도 추가된 도메인을 확인할 수 있습니다.

 

Ingress에 등록한 도메인이 Route53에 3개의 Record name으로 등록됩니다.

잘됩니다. 이제 인그레스 생성하고 Route53에서 수동으로 등록하지 않아도 자동으로 도메인과 ALB가 연결됩니다.

 

이상 테라폼 IRSA, 헬름을 이용하여 external dns 설치를 공유합니다.

반응형