{"id":502,"date":"2025-09-01T13:06:34","date_gmt":"2025-09-01T13:06:34","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=502"},"modified":"2025-09-01T13:06:34","modified_gmt":"2025-09-01T13:06:34","slug":"setup-externaldns-on-eks","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=502","title":{"rendered":"How to Set Up ExternalDNS on EKS? (Step by Step Guide)"},"content":{"rendered":"<p>In this blog, you will learn how to install and configure ExternalDNS on Amazon EKS and automate DNS records in Route53 for Kubernetes Services and Ingress resources.<\/p>\n<p>By the end of this blog, you will learn,<\/p>\n<ul>\n<li>How to set up ExternalDNS on EKS with Route53<\/li>\n<li>How to configure IAM for DNS automation<\/li>\n<li>How to test with Services and Ingress with SSL.<\/li>\n<\/ul>\n<h2 id=\"what-is-externaldns-and-why-use-it\">What is ExternalDNS and Why Use It?<\/h2>\n<p>When you work on EKS projects, you will have to expose services using Route 53 DNS (private or public). Normally, this is done manually by the DevOps or network team.<\/p>\n<p><strong>ExternalDNS<\/strong> automates this process. It manages DNS records for you and keeps them in sync with your Kubernetes resources.<\/p>\n<p>Here is how it works.<\/p>\n<ul>\n<li>ExternalDNS runs inside the Kubernetes cluster as a <a href=\"https:\/\/devopscube.com\/kubernetes-deployment-tutorial\/\" rel=\"noreferrer\">Deployment<\/a>.<\/li>\n<li>It continuously watches Kubernetes resources like Services and Ingress<\/li>\n<li>When it detects Service or Ingress resources with eternalDNS specific annotations with a DNS hostname, it creates corresponding DNS records.<\/li>\n<li>It does this by talking to your DNS providers API (for example, Route 53).<\/li>\n<li>When the resources are deleted, it automatically cleans up the associated DNS records (if enabled)<\/li>\n<\/ul>\n<p>In short, it removes manual DNS management when deploying applications on EKS. Also, it gives you a GitOps-friendly workflow for DNS management.<\/p>\n<h2 id=\"externaldns-workflow-in-awk-eks\">ExternalDNS Workflow in AWK EKS<\/h2>\n<p>Now, let&#8217;s understand how ExternalDNS works with EKS.<\/p>\n<p>The following workflow explains how the ExternalDNS works on the EKS cluster<\/p>\n<figure class=\"kg-card kg-image-card kg-card-hascaption\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-8.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"1999\" height=\"1926\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/09\/image-8.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/09\/image-8.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/09\/image-8.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-8.png 1999w\" sizes=\"auto, (min-width: 720px) 720px\"><figcaption><span style=\"white-space: pre-wrap;\">ExternalDNS Workflow in AWK EKS<\/span><\/figcaption><\/figure>\n<p>Here is how the ExternalDNS works in the EKS cluster.<\/p>\n<ol>\n<li>The <strong>ExternalDNS<\/strong> on the <a href=\"https:\/\/devopscube.com\/create-aws-eks-cluster-eksctl\/\" rel=\"noreferrer\">EKS cluster<\/a> keeps watching the <strong>Ingress<\/strong> and <strong>Service<\/strong> objects.<\/li>\n<li>If any of these objects are created, including the <strong>ExternalDNS annotation<\/strong> with a <strong>domain name<\/strong>, ExternalDNS collects details of the domain name, node <a href=\"https:\/\/devopscube.com\/ip-address-tutorial\/\" rel=\"noreferrer\">IP<\/a>, or Load Balancer DNS. For example, <br \/><code>external-dns.alpha.kubernetes.io\/hostname: app.devopscube.com<\/code><\/li>\n<li>Using this information, it creates or updates the DNS records in <strong>Route 53<\/strong>.<\/li>\n<li>To allow ExternalDNS to make changes in Route 53, the <strong>Pod Identity Agent Plugin<\/strong> provides the necessary permissions from the assigned <a href=\"https:\/\/devopscube.com\/aws-iam-role-instance-profile\/\" rel=\"noreferrer\">IAM Role.<\/a><\/li>\n<\/ol>\n<p>Now, we can start the setup of the ExternalDNS on the EKS cluster.<\/p>\n<h2 id=\"setup-prerequisites\"><strong>Setup Prerequisites <\/strong><\/h2>\n<p>To set up the ExternalDNS, we need the following requirements.<\/p>\n<ol>\n<li>EKS Cluster v1.30+<\/li>\n<li>Pod Identity Agent &#8211; (Must be available on EKS cluster)<\/li>\n<li>Valid Domain Name and DNS Server (e.g., Route53, Cloudflare, etc.)<\/li>\n<li>AWS Load Balancer Controller in the EKS cluster<\/li>\n<li><a href=\"https:\/\/devopscube.com\/use-aws-cli-create-ec2-instance\/\" rel=\"noreferrer\">AWS CLI<\/a> [Local Workstation]<\/li>\n<li><a href=\"https:\/\/devopscube.com\/create-helm-chart\/\" rel=\"noreferrer\">Helm<\/a> [Local Workstation]<\/li>\n<\/ol>\n<p>Once the prerequisites are ready, we can start the installation.<\/p>\n<h2 id=\"step-by-step-setup-guide\">Step-by-Step Setup Guide<\/h2>\n<p>Before we install the ExternalDNS, we need to create an IAM Role for the External DNS to access Route53.<\/p>\n<h3 id=\"step-1-create-an-iam-policy-for-external-dns\">Step 1: Create an IAM Policy for External DNS<\/h3>\n<p>First, we need to give ExternalDNS permissions to manage Route53 records. We will do this by creating an IAM Policy using the following JSON.<\/p>\n<pre><code class=\"language-json\">cat &lt;&lt; EOF &gt;  eks_route53_policy.json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:ChangeResourceRecordSets\"\n      ],\n      \"Resource\": [\n        \"arn:aws:route53:::hostedzone\/*\"\n      ]\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:ListHostedZones\",\n        \"route53:ListResourceRecordSets\",\n        \"route53:ListTagsForResource\"\n      ],\n      \"Resource\": [\n        \"*\"\n      ]\n    }\n  ]\n}\nEOF<\/code><\/pre>\n<p>Now, we can create the IAM Policy using the following command.<\/p>\n<pre><code class=\"language-bash\">aws iam create-policy \\\n    --policy-name AllowExternalDNSUpdates \\\n    --policy-document file:\/\/eks_route53_policy.json<\/code><\/pre>\n<p>Export Policy <a href=\"https:\/\/devopscube.com\/aws-arn-guide\/\" rel=\"noreferrer\">ARN<\/a> for the upcoming configuration<\/p>\n<pre><code class=\"language-bash\">export POLICY_NAME=\"AllowExternalDNSUpdates\"<\/code><\/pre>\n<pre><code class=\"language-bash\">export POLICY_ARN=$(aws iam list-policies --query \"Policies[?PolicyName=='${POLICY_NAME}'].Arn\" --output text)\n<\/code><\/pre>\n<p>At this point, you have created an IAM Policy that allows ExternalDNS to manage Route53 records (create, update, list).<\/p>\n<h3 id=\"step-2-create-an-iam-role-for-externaldns\">Step 2: Create an IAM Role for ExternalDNS<\/h3>\n<p>Before creating the role, we need a <strong>Trust Policy<\/strong> that is suitable for the <strong>Pod Identity Agent<\/strong> to identify the role.<\/p>\n<pre><code class=\"language-json\">cat &lt;&lt;EOF &gt; trust-policy.json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Service\": \"pods.eks.amazonaws.com\"\n      },\n      \"Action\": [\n        \"sts:AssumeRole\",\n        \"sts:TagSession\"\n      ]\n    }\n  ]\n}\nEOF<\/code><\/pre>\n<p>Next, we need an IAM Role that the ExternalDNS pods can assume. This role will be linked with our Service Account later.<\/p>\n<pre><code class=\"language-bash\">aws iam create-role \\\n  --role-name externalDNSRole \\\n  --assume-role-policy-document file:\/\/\"trust-policy.json\"<\/code><\/pre>\n<p>Store the Role name and ARN as environment variables for the upcoming configurations.<\/p>\n<pre><code class=\"language-bash\">export ROLE_NAME=externalDNSRole<\/code><\/pre>\n<pre><code class=\"language-bash\">export ROLE_ARN=$(aws iam get-role --role-name $ROLE_NAME --query \"Role.Arn\" --output text)\n<\/code><\/pre>\n<p>Now, we have our IAM Role and Policy, so we need to attach the IAM Policy to the Role.<\/p>\n<pre><code class=\"language-bash\">aws iam attach-role-policy \\\n  --policy-arn ${POLICY_ARN} \\\n  --role-name ${ROLE_NAME}\n<\/code><\/pre>\n<p>Now, the permission for the ExternalDNS is ready, so we need to attach this permission to the ExternalDNS Service Account.<\/p>\n<h3 id=\"step-3-create-a-service-account-for-the-external-dns\">Step 3: Create a Service Account for the External DNS<\/h3>\n<p>We are deploying the ExternalDNS on a dedicated namespace on <a href=\"https:\/\/devopscube.com\/kubernetes-tutorials-beginners\/\" rel=\"noreferrer\">Kubernetes<\/a>, so we need to create a namespace.<\/p>\n<pre><code class=\"language-bash\">export NAMESPACE=external-dns<\/code><\/pre>\n<pre><code class=\"language-bash\">kubectl create ns ${NAMESPACE}<\/code><\/pre>\n<p>Since ExternalDNS will run inside Kubernetes, we need a Service Account. It will later be associated with our IAM Role so the pods can use AWS permissions.<\/p>\n<pre><code>cat &lt;&lt; EOF &gt; externaldns-sa.yaml\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: external-dns\n  namespace: external-dns\n  labels:\n    app.kubernetes.io\/name: external-dns\nEOF<\/code><\/pre>\n<p>To apply the manifest, use the following command.<\/p>\n<pre><code>kubectl apply -f externaldns-sa.yaml<\/code><\/pre>\n<p>The Service Account in the <code>external-dns<\/code> namespace is now ready. This has to be mapped to the IAM Role through Pod Identity.<\/p>\n<h3 id=\"step-4-pod-identity-association\">Step 4: Pod Identity Association<\/h3>\n<blockquote><p>Note: Assuming you already have the Pod Identity Agent Plugin on your EKS cluster.<\/p><\/blockquote>\n<p>Create the necessary environment variables for the association.<\/p>\n<pre><code>export CLUSTER_NAME=eks-spot-cluster\nexport SERVICE_ACCOUNT=external-dns<\/code><\/pre>\n<p>Now, we can use the following command to perform the Pod Identity Association.<\/p>\n<pre><code>eksctl create podidentityassociation \\\n    --cluster $CLUSTER_NAME \\\n    --namespace $NAMESPACE \\\n    --service-account-name $SERVICE_ACCOUNT \\\n    --role-arn $ROLE_ARN<\/code><\/pre>\n<p>If you want to ensure that the Pod Identity Association is properly done, you can use the following command.<\/p>\n<pre><code>eksctl get podidentityassociations --cluster $CLUSTER_NAME<\/code><\/pre>\n<p>At this point, your Service Account and IAM Role are connected through Pod Identity. ExternalDNS pods running with this Service Account will have permissions to update Route53.<\/p>\n<h3 id=\"step-5-install-externaldns\">Step 5: Install ExternalDNS <\/h3>\n<p>Now, we need to create a Deployment manifest to deploy the ExternalDNS.<\/p>\n<p>To the major changes that you need to make to the following configuration.<\/p>\n<ul>\n<li><code>--domain-filter<\/code> should be your <a href=\"https:\/\/devopscube.com\/route53-private-hosted-zone\/\" rel=\"noreferrer\">Route53 hosted zone <\/a>name.<\/li>\n<li><code>env.value<\/code> should be the region where your EKS cluster is.<\/li>\n<\/ul>\n<pre><code class=\"language-yaml\">cat &lt;&lt; EOF &gt; external-dns.yaml\napiVersion: rbac.authorization.k8s.io\/v1\nkind: ClusterRole\nmetadata:\n  name: external-dns\n  labels:\n    app.kubernetes.io\/name: external-dns\nrules:\n- apiGroups: [\"\"]\n  resources: [\"services\",\"endpoints\",\"nodes\",\"pods\"]\n  verbs: [\"get\",\"list\",\"watch\"]\n- apiGroups: [\"discovery.k8s.io\"]\n  resources: [\"endpointslices\"]\n  verbs: [\"get\",\"list\",\"watch\"]\n- apiGroups: [\"networking.k8s.io\"]\n  resources: [\"ingresses\",\"ingressclasses\"]\n  verbs: [\"get\",\"list\",\"watch\"]\n- apiGroups: [\"gateway.networking.k8s.io\"]\n  resources: [\"gateways\",\"httproutes\",\"grpcroutes\",\"tcproutes\",\"udproutes\",\"tlsroutes\"]\n  verbs: [\"get\",\"list\",\"watch\"]\n---\napiVersion: rbac.authorization.k8s.io\/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: external-dns-viewer\n  labels:\n    app.kubernetes.io\/name: external-dns\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: external-dns\nsubjects:\n  - kind: ServiceAccount\n    name: external-dns\n    namespace: external-dns \n---\n\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: external-dns\n  namespace: external-dns\n  labels:\n    app.kubernetes.io\/name: external-dns\nspec:\n  strategy:\n    type: Recreate\n  selector:\n    matchLabels:\n      app.kubernetes.io\/name: external-dns\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io\/name: external-dns\n    spec:\n      serviceAccountName: external-dns\n      containers:\n        - name: external-dns\n          image: registry.k8s.io\/external-dns\/external-dns:v0.18.0\n          args:\n            - --source=service\n            - --source=ingress\n            - --domain-filter=devopsproject.dev \n            - --provider=aws\n            - --policy=upsert-only \n            - --aws-zone-type=public \n            - --registry=txt\n            - --txt-owner-id=external-dns\n          env:\n            - name: AWS_DEFAULT_REGION\n              value: us-west-2\nEOF<\/code><\/pre>\n<p>Here,<\/p>\n<ul>\n<li> <code>--source<\/code> &#8211;  Defines what Kubernetes objects should watch.<\/li>\n<li><code>--domain-fiter<\/code> &#8211; Defines which hosted zone should use.<\/li>\n<li><code>--provider<\/code> &#8211; Which DNS provider should choose (e.g., Route53, CloudFlare, CoreDNS, etc). To view all available providers, refer to this <a href=\"https:\/\/github.com\/kubernetes-sigs\/external-dns?ref=devopscube.com\" rel=\"noreferrer\">official documentation<\/a>.<\/li>\n<li><code>--aws-zone-type<\/code> &#8211; Option to choose private or public hosted zone.<\/li>\n<li><code>--registry<\/code> &#8211; This will create a record with the metadata of the DNS record creation, like who created this. <\/li>\n<li><code>--txt-owner-id<\/code> &#8211; Defines the owner name of the DNS records.\n<ul>\n<li>We can give any name to this. <\/li>\n<li>If you are running multiple clusters, assign a different Owner ID to each to avoid conflicts between clusters.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\"><code spellcheck=\"false\" style=\"white-space: pre-wrap;\">--policy<\/code> parameters is an important one. It fefines whether the created records should be deleted or not. <\/p>\n<p><code spellcheck=\"false\" style=\"white-space: pre-wrap;\">upsert-only<\/code> will create\/update but will not delete. <\/p>\n<p>The <code spellcheck=\"false\" style=\"white-space: pre-wrap;\">sync<\/code> policy allows full synchronization. That means ExternalDNS will create, update, <i><em class=\"italic\" style=\"white-space: pre-wrap;\">and <\/em><\/i>delete DNS records. It ensures the DNS zone matches the current cluster resource state.<\/div>\n<\/div>\n<p>To deploy the manifest, use the following command.<\/p>\n<pre><code>kubectl apply -f external-dns.yaml<\/code><\/pre>\n<p>Once the deployment is completed, use the following command to list them to ensure that the ExternalDNS is running without any issues.<\/p>\n<pre><code>kubectl -n external-dns get deploy<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-113.png\" class=\"kg-image\" alt=\"the output of the external dns installation\" loading=\"lazy\" width=\"1318\" height=\"270\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-113.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-113.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-113.png 1318w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>The output ensures that the deployment is running without any issues. It is now watching Services and Ingress resources, ready to sync DNS records in Route53 automatically.<\/p>\n<p>We can not test whether the External DNS automatically manages the DNS records on Route53.<\/p>\n<h2 id=\"testing-with-services-nodeport-loadbalancer\">Testing with Services (NodePort &amp; LoadBalancer)<\/h2>\n<p>ExternalDNS will create records for both the <a href=\"https:\/\/devopscube.com\/kubernetes-api-access-service-account\/\" rel=\"noreferrer\">Kubernetes <strong>Service<\/strong><\/a> object and the <strong>Ingress<\/strong> object.<\/p>\n<p>We can test both, but for that, we need a demo deployment.<\/p>\n<pre><code class=\"language-yaml\">cat &lt;&lt;EOF &gt; test-deployment.yaml\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: nginx\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - image: nginx\n        name: nginx\n        ports:\n        - containerPort: 80\n          name: http\n        - containerPort: 443\n          name: https\nEOF<\/code><\/pre>\n<p>To deploy this application, use the following command.<\/p>\n<pre><code class=\"language-yaml\">kubectl apply -f test-deployment.yaml<\/code><\/pre>\n<p>To create a DNS record for a service, we need to add an annotation (<code>metadata.annotations.external-dns.alpha.kubernetes.io\/hostname<\/code>) with the domain name on the service configuration.<\/p>\n<p>Now, we need to create a service for this application with the mentioned annotation.<\/p>\n<h3 id=\"service-with-type-nodeport\">Service with Type NodePort<\/h3>\n<p>The External DNS works with both service types, such as <strong>NodePort<\/strong> and <strong>LoadBalancer<\/strong>.<\/p>\n<p>We will start with a NodePort Service and add a DNS annotation. ExternalDNS should automatically create the record in Route53.<\/p>\n<p>Here is the service YAML.<\/p>\n<pre><code class=\"language-yaml\">cat &lt;&lt;EOF &gt; np-svc.yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx\n  annotations:\n    external-dns.alpha.kubernetes.io\/hostname: nginx.devopsproject.dev\nspec:\n  type: NodePort\n  selector:\n    app: nginx\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n      nodePort: 32000\n    - name: https\n      port: 443\n      targetPort: 443\n      nodePort: 32443\nEOF<\/code><\/pre>\n<p>To deploy this, use the following command.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f np-svc.yaml<\/code><\/pre>\n<p>Once your service is created, ExternalDNS will create &#8220;<strong>A<\/strong>&#8221; and &#8220;<strong>TXT<\/strong>&#8221; DNS records for your service.<\/p>\n<ul>\n<li><strong>A Record<\/strong> &#8211; Map the IP address with the host name.<\/li>\n<li><strong>TXT Record <\/strong>&#8211; Stores the information about the DNS records, such as owner name, TTL, etc.<\/li>\n<\/ul>\n<p>In this NodePort method, the ExternalDNS will map the <strong>IP address<\/strong> of the EKS worker nodes to the hostname, which you can check from the Route53 console.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-114.png\" class=\"kg-image\" alt=\"the created dns records on route53 by the externaldns\" loading=\"lazy\" width=\"1316\" height=\"571\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-114.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-114.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-114.png 1316w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>The output confirms that the DNS records were automatically created in Route53.<\/p>\n<p>Now, we can try to access our deployed application using this domain name.<\/p>\n<p>For that, <\/p>\n<p>Open the terminal on your local machine and run the <code>curl<\/code> command with the hostname.<\/p>\n<p>Since this is a <strong>NodePort<\/strong> service, we need to add the NodePort number <code>32000<\/code> to the URL.<\/p>\n<pre><code class=\"language-bash\">curl nginx.devopsproject.dev:32000<\/code><\/pre>\n<p>Now, we get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-116.png\" class=\"kg-image\" alt=\"the output of the nginx web server\" loading=\"lazy\" width=\"1896\" height=\"1530\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-116.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-116.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/08\/image-116.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-116.png 1896w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>This ensures that the mapping is working correctly, so we can access the web page.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">We don&#8217;t have to worry about whether the node gets restarted or destroyed, because if any changes happen in the cluster, ExternalDNS automatically updates the configurations in Route53 and ensures that the domain names always reach the correct <a href=\"https:\/\/devopscube.com\/kubernetes-resoruces\/\" rel=\"noreferrer\">Kubernetes resource.<\/a><\/div>\n<\/div>\n<p>Next, we will test with the other service type, so delete this service using the following command.<\/p>\n<pre><code class=\"language-bash\">kubectl delete -f np-svc.yaml<\/code><\/pre>\n<p>Also, remove the DNS records from Route53.<\/p>\n<blockquote><p>Note: Since we use the policy as <code>--policy=upsert-only<\/code>, the created DNS records won&#8217;t be deleted automatically so we need to clean them up manually.<\/p><\/blockquote>\n<h3 id=\"service-with-type-load-balancer\">Service with Type Load Balancer<\/h3>\n<p>To create a LoadBalancer service for the same deployment, use the following contents.<\/p>\n<pre><code class=\"language-yaml\">cat &lt;&lt;EOF &gt; lb-svc.yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx\n  annotations:\n    external-dns.alpha.kubernetes.io\/hostname: nginx.devopsproject.dev\nspec:\n  type: LoadBalancer\n  selector:\n    app: nginx\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\nEOF<\/code><\/pre>\n<p>To apply this service, use the following command.<\/p>\n<pre><code class=\"language-yaml\">kubectl apply -f lb-svc.yaml<\/code><\/pre>\n<p>In this service, an <strong>AWS Load Balancer<\/strong> is created to route the external traffic to the Kubernetes workload.<\/p>\n<p>So the mapping will be done with the Load Balancer&#8217;s DNS name instead of the worker nodes IP. <\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-117.png\" class=\"kg-image\" alt=\"the output of the updated dns records by the external dns\" loading=\"lazy\" width=\"1083\" height=\"555\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-117.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-117.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-117.png 1083w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Here, you can see one more record along with the &#8220;A&#8221; record, which is the &#8220;AAAA&#8221; record.<\/p>\n<p>&#8220;AAAA&#8221; record is nothing but mapping the IPv6 address with the domain name. But you can see that both values are the same for both of the records.<\/p>\n<p>Now, we can access the application using the domain name, and this time we do not have to use the nodeport number because it is a Load balancer service.<\/p>\n<pre><code class=\"language-yaml\">curl nginx.devopsproject.dev<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-118.png\" class=\"kg-image\" alt=\"the output of web server\" loading=\"lazy\" width=\"960\" height=\"570\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-118.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-118.png 960w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>The output ensures that we can access the web page.<\/p>\n<p>Next, we will try ExternalDNS with the Ingress object.<\/p>\n<p>To delete this service, use the following command.<\/p>\n<pre><code class=\"language-yaml\">kubectl delete -f lb-svc.yaml<\/code><\/pre>\n<p>Delete the related Route53 records as well from the console.<\/p>\n<h2 id=\"testing-with-ingress-tls-acm-integration\">Testing with Ingress &amp; TLS (ACM Integration)<\/h2>\n<p>External DNS not only works with services but also with the Ingress. You can use any method for the ingress (<a href=\"https:\/\/devopscube.com\/nginx-ingress-with-cert-manager\/\" rel=\"noreferrer\">Nginx ingress<\/a> or AWS Load Balancer controller)<\/p>\n<p>Here, I am showing an example with the AWS Load Balancer controller. <\/p>\n<p>Assuming, you already have the AWS Load Balancer controller on the EKS cluster, if you don&#8217;t use the blog &#8211;&gt; <a href=\"https:\/\/devopscube.com\/aws-load-balancer-controller-on-eks\/\" rel=\"noreferrer\">AWS Load Balancer Controller on EKS<\/a><\/p>\n<p>Before creating an Ingress object, we need a service with type ClusterIP.<\/p>\n<pre><code class=\"language-yaml\">cat &lt;&lt;EOF &gt; svc.yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx\nspec:\n  type: ClusterIP\n  selector:\n    app: nginx\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n    - name: https\n      port: 443\n      targetPort: 443\nEOF<\/code><\/pre>\n<p>To apply this service.<\/p>\n<pre><code class=\"language-yaml\">kubectl apply -f svc.yaml<\/code><\/pre>\n<p>Now, we can create an Ingress object for the deployment.<\/p>\n<pre><code class=\"language-yaml\">cat &lt;&lt; EOF &gt; ingress.yaml\napiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: nginx-ingress\n  namespace: default\n  annotations:\n    alb.ingress.kubernetes.io\/scheme: internet-facing\n    alb.ingress.kubernetes.io\/healthcheck-path: \/\n    alb.ingress.kubernetes.io\/target-type: 'ip'\n    external-dns.alpha.kubernetes.io\/hostname: nginx.devopsproject.dev\nspec:\n  ingressClassName: alb\n  rules:\n  - host: nginx.devopsproject.dev\n    http:\n      paths:\n      - path: \/\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx\n            port:\n              number: 80\nEOF<\/code><\/pre>\n<p>To apply this, use the following command.<\/p>\n<pre><code>kubectl apply -f ingress.yaml<\/code><\/pre>\n<p>This is also the same as the above one, but the difference is that here, the ALB controller provisions the Load Balancer in AWS.<\/p>\n<p>Since the <a href=\"https:\/\/devopscube.com\/terraform-autoscaling-group\/\" rel=\"noreferrer\">ALB<\/a> controller creates an Application Load Balancer for the Ingress object, we can use the AWS ACM TLS certificates with configuration.<\/p>\n<h3 id=\"aws-certificate-manager-integration-optional\">AWS Certificate Manager integration (Optional)<\/h3>\n<p>If you want to safeguard your application by encrypting it using the <a href=\"https:\/\/devopscube.com\/configure-ingress-tls-kubernetes\/\" rel=\"noreferrer\">TLS certificate<\/a>, we can use the certificates from the ACM service.<\/p>\n<p>Assuming, you already have a TLS created on the <a href=\"https:\/\/devopscube.com\/setup-ssl-tls-aws-certificate-manager\/\" rel=\"noreferrer\">AWS Certificate Manager<\/a>. If not, please refer to this blog &#8211;&gt; <a href=\"https:\/\/devopscube.com\/setup-ssl-tls-aws-certificate-manager\/\" rel=\"noreferrer\">Setup SSL\/TLS With AWS Certificate Manager<\/a><\/p>\n<p>Now, we can configure this existing certificate on the Ingress object.<\/p>\n<p>Here, you need to change the ARN of your ACM certificate.<\/p>\n<pre><code class=\"language-yaml\">cat &lt;&lt; EOF &gt; ingress.yaml\napiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: nginx-ingress\n  namespace: default\n  annotations:\n    alb.ingress.kubernetes.io\/scheme: internet-facing\n    alb.ingress.kubernetes.io\/certificate-arn: arn:aws:acm:us-west-2:&lt;AWS ACCOUNT ID&gt;:certificate\/b7a364de-72e2-4a36-bb98-258e8d11a224\n    alb.ingress.kubernetes.io\/ssl-policy: ELBSecurityPolicy-2016-08\n    alb.ingress.kubernetes.io\/backend-protocol: HTTPS\n    alb.ingress.kubernetes.io\/healthcheck-path: \/\n    alb.ingress.kubernetes.io\/target-type: 'ip'\n    alb.ingress.kubernetes.io\/listen-ports: '[{\"HTTP\":80,\"HTTPS\":  443}]'\n    alb.ingress.kubernetes.io\/actions.ssl-redirect: '{\"Type\": \"redirect\", \"RedirectConfig\": { \"Protocol\": \"HTTPS\", \"Port\": \"443\", \"StatusCode\": \"HTTP_301\"}}'\n    external-dns.alpha.kubernetes.io\/hostname: nginx.devopsproject.dev\nspec:\n  ingressClassName: alb\n  rules:\n  - host: nginx.devopsproject.dev\n    http:\n      paths:\n      - path: \/\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx\n            port:\n              number: 443\nEOF<\/code><\/pre>\n<p>To replace the existing ingress, use the following command.<\/p>\n<pre><code class=\"language-yaml\">kubectl replace -f ingress.yaml --force<\/code><\/pre>\n<p>Now, if you check over a browser, you can see that the validation of the certificate, which ensures that our connection is now secured.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-120.png\" class=\"kg-image\" alt=\"the output of the attached certificates on the load balancer\" loading=\"lazy\" width=\"810\" height=\"457\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-120.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-120.png 810w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>This is how we secure our connection over the internet to the application in Kubernetes.<\/p>\n<p>We can even automate the certificate creation using the Certificate Manager, for that you can refer to this <a href=\"https:\/\/devopscube.com\/nginx-ingress-with-cert-manager\/\" rel=\"noreferrer\">blog<\/a>.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">If you want to use the ExternalDNS with the <a href=\"https:\/\/devopscube.com\/kubernetes-gateway-api\/\" rel=\"noreferrer\">Kubernetes Gateway API<\/a>, you can refer to the <a href=\"https:\/\/kubernetes-sigs.github.io\/external-dns\/v0.13.1\/tutorials\/gateway-api\/?ref=devopscube.com#manifest-with-rbac\" rel=\"noreferrer\">official documentation<\/a>.<\/div>\n<\/div>\n<h2 id=\"cleanup\">Cleanup<\/h2>\n<p>Let&#8217;s clean up the resources we created so you don&#8217;t leave extra DNS records or workloads running.<\/p>\n<p>First, delete the ingress.<\/p>\n<pre><code class=\"language-bash\">kubectl delete -f ingress.yaml<\/code><\/pre>\n<p>Now, we can delete the deployment and the service.<\/p>\n<pre><code class=\"language-bash\">kubectl delete -f svc.yaml \nkubectl delete -f test-deploy.yaml<\/code><\/pre>\n<p>Before we endup the blog, we can have a look at how to actually use ExternalDNS in a hybrid DNS method.<\/p>\n<h2 id=\"best-practices\">Best Practices<\/h2>\n<p>Here are some of the best practices of the ExternalDNS.<\/p>\n<ol>\n<li>Use different hosted zones if you are using multiple environments.<\/li>\n<li>Enable TXT registry and give different names for each ExternalDNS deployment if you are using multiple clusters.<\/li>\n<li>In our setup, use the Pod Identity Agent if you are using this on EKS instead of the IRSA to avoid the complexity of managing permissions.<\/li>\n<li>Use Prometheus to collect the metrics of the ExternalDNS to monitor.<\/li>\n<li>Add <code>--log-level=<\/code> to generate logs so that we can identify if any issues occurred.<\/li>\n<\/ol>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>This is the high-level overview of the External DNS, and it really simplifies the DNS Record creation and management of the Kubernetes resources.<\/p>\n<p>Explore all the deployment fields so that you can choose the required parameters and values as per your requirements.<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/setup-externaldns-on-eks\/\" target=\"_blank\" rel=\"noopener noreferrer\">How to Set Up ExternalDNS on EKS? (Step by Step Guide) \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/setup-externaldns-on-eks\/<\/p>\n","protected":false},"author":1,"featured_media":503,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-502","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/502","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=502"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/502\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/503"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=502"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=502"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=502"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}