Back to External Dns

AWS and LocalStack

docs/tutorials/aws-localstack.md

0.21.013.3 KB
Original Source

AWS and LocalStack

Overview

This tutorial demonstrates how to configure ExternalDNS to manage DNS records in LocalStack's Route53 service using a local Kind (Kubernetes in Docker) cluster.

TL;DR

After completing this lab, you will have a Kubernetes environment running as containers in your local development machine with localstack and external-dns.

Prerequisite

Before you start, ensure you have:

Architecture Overview

In this setup:

  • Kind provides a local Kubernetes cluster
  • LocalStack simulates AWS services (specifically Route53)
  • ExternalDNS automatically creates DNS records in LocalStack based on Kubernetes resources

Bootstrap Environment

1. Create cluster

sh
kind create cluster --config=docs/snippets/tutorials/aws-localstack/kind.yaml

Creating cluster "aws-localstack" ...
 ✓ Preparing nodes 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-aws-localstack"
You can now use your cluster with:

kubectl cluster-info --context kind-aws-localstack

Verify the cluster is running:

bash
kubectl cluster-info --context kind-aws-localstack
kubectl get nodes

2. Deploy Localstack

There are multiple options to configure etcd

  1. With custom manifest.
  2. Localstack helm

In this tutorial, we'll use the second option.

sh
helm repo add localstack https://localstack.github.io/helm-charts
helm upgrade localstack localstack-charts/localstack \
  -n localstack \
  --create-namespace \
  --install \
  --atomic \
  --wait \
  -f docs/snippets/tutorials/aws-localstack/values-localstack.yml

❯❯ Release "localstack" does not exist. Installing it now.

Verify LocalStack is running

sh
kubectl get pods -n localstack
kubectl logs deploy/localstack -n localstack

3: Create a Hosted Zone in LocalStack

Test if we could reach Localstack, Route53 service is available and verify Route53 zones created in localstack.

sh
curl http://127.0.0.1:$NODE_PORT/_localstack/health | jq
docs/snippets/tutorials/aws-localstack/fetch-records.sh

Create extra hosted zones in localstack when required

sh
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_ENDPOINT_URL=http://127.0.0.1:32379

aws route53 create-hosted-zone \
  --name test.com \
  --caller-reference $(date +%s)

4. Configure ExternalDNS

Deploy with helm and minimal configuration.

Add the external-dns helm repository and check available versions

sh
helm repo add --force-update external-dns https://kubernetes-sigs.github.io/external-dns/
helm search repo external-dns --versions

Install with required configuration

sh
helm upgrade --install external-dns external-dns/external-dns \
  -n default \
  -f docs/snippets/tutorials/aws-localstack/values-extdns.yml

❯❯ Release "external-dns" does not exist. Installing it now.

Validate pod status and view logs

sh
kubectl get pods -l app.kubernetes.io/name=external-dns
kubectl logs deploy/external-dns -n default

Or run it on the host from sources

sh
# required to access localstack
export AWS_REGION=eu-west-1
export AWS_ACCESS_KEY_ID=foo
export AWS_SECRET_ACCESS_KEY=bar
export AWS_ENDPOINT_URL=http://127.0.0.1:32379

go run main.go \
    --provider=aws \
    --source=service \
    --source=ingress \
    --source=crd \
    --txt-owner-id=aws-localstack \
    --domain-filter=example.com \
    --domain-filter=local.tld \
    --log-level=info

5. Test with a Sample Service

Create a test service so that ExternalDNS to create records

yaml
[[% include 'tutorials/aws-localstack/foo-app.yml' %]]

Deploy the service

sh
kubectl apply -f docs/snippets/tutorials/aws-localstack/foo-app.yml

Validate route53 records created

sh
docs/snippets/tutorials/aws-localstack/fetch-records.sh "foo-app"

❯❯ [
    {
        "Name": "a-foo-app.example.com.",
        "Type": "TXT",
    },
    {
        "Name": "foo-app.example.com.",
        "Type": "A",
        "Value": [
            "10.244.1.18",
        ],
        "TTL": 300
    }
]

6. Using DNSEndpoint CRD (Advanced)

The DNSEndpoint Custom Resource Definition (CRD) provides direct control over DNS records, independent of Services or Ingresses. This is useful for:

  • Creating DNS records that don't correspond to Kubernetes services
  • Managing complex DNS configurations (multiple targets, weighted routing)
  • Integrating with external systems or custom controllers

Verify the CRD is installed

sh
kubectl get crd dnsendpoints.externaldns.k8s.io

Example 1: Multiple Records

Create a simple A record pointing to a specific IP

yaml
[[% include 'tutorials/aws-localstack/dnsendpoint-multi.yml' %]]

Apply and verify

sh
kubectl apply -f docs/snippets/tutorials/aws-localstack/dnsendpoint-multi.yml
# Check the DNSEndpoint status
kubectl get dnsendpoint simple-example -o yaml
# validate
docs/snippets/tutorials/aws-localstack/fetch-records.sh "dnsendpoint-a"
docs/snippets/tutorials/aws-localstack/fetch-records.sh "dnsendpoint-aaaa"

❯❯ [
    {
        "Name": "a-dnsendpoint-a.example.com.",
        "Type": "TXT",
        "Value": [
            "heritage=external-dns,external-dns/owner=aws-localstack"
        ],
        "TTL": 300
    },
    {
        "Name": "dnsendpoint-a.example.com.",
        "Type": "A",
        "Value": [
            "192.168.1.100"
        ],
        "TTL": 300
    },
    {
        "Name": "dnsendpoint-aaaa.example.com.",
        "Type": "AAAA",
        "Value": [
            "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
        ],
        "TTL": 600
    },
]

Example 2: CNAME Record

Create a CNAME record pointing to another domain:

yaml
[[% include 'tutorials/aws-localstack/dnsendpoint-cname.yml' %]]

Apply and verify

sh
kubectl apply -f docs/snippets/tutorials/aws-localstack/dnsendpoint-cname.yml
# Check the DNSEndpoint status
kubectl get dnsendpoint cname-example -o yaml
# validate
docs/snippets/tutorials/aws-localstack/fetch-records.sh "www.example"

❯❯ [
    {
        "Name": "a-www.example.com.",
        "Type": "TXT",
        "Value": [
            "\"heritage=external-dns,external-dns/owner=aws-localstack,external-dns/resource=crd/default/cname-example\""
        ],
        "TTL": 300
    },
    {
        "Name": "www.example.com.",
        "Type": "A",
        "Value": [
            "example.com"
        ],
        "TTL": 600
    }
]

Example 4: TXT Records

Create TXT records (useful for domain verification, SPF, DKIM, etc.)

yaml
[[% include 'tutorials/aws-localstack/dnsendpoint-txt.yml' %]]

Apply and verify

sh
kubectl apply -f docs/snippets/tutorials/aws-localstack/dnsendpoint-txt.yml
# Check the DNSEndpoint status
kubectl get dnsendpoint txt-example -o yaml
# validate
docs/snippets/tutorials/aws-localstack/fetch-records.sh

7. Test with Service LoadBalancer (Advanced)

With Kind, LoadBalancer services won't get external IPs automatically. You can:

  • Use MetalLB for LoadBalancer support in Kind
  • Install and run Cloud Provider KInd
  • Patch services, to manually assign an Ingress IPs. It just makes the Service appear like a real LoadBalancer for tools/tests.
yaml
[[% include 'tutorials/aws-localstack/service-lb.yml' %]]

Apply, patch and verify

sh
kubectl apply -f docs/snippets/tutorials/aws-localstack/service-lb.yml
# patch
kubectl patch svc loadbalancer-service --type=merge \
 -p '{"status":{"loadBalancer":{"ingress":[{"ip":"172.18.0.2"}]}}}' --subresource=status
❯❯ service/loadbalancer-service
# validate
docs/snippets/tutorials/aws-localstack/fetch-records.sh "my-loadbalancer"

Cleanup

Remove all resources:

sh
kind delete cluster --name aws-localstack

Diagrams

System Architecture

Description: This diagram illustrates the complete setup where ExternalDNS runs inside the Kind cluster, watches Kubernetes Service and Ingress resources, and automatically creates corresponding DNS records in LocalStack's Route53 service. Both the Kind cluster and LocalStack container run on the same Docker network, enabling communication between them.

mermaid
graph TB
    subgraph "Host Machine"
        kubectl[kubectl CLI]
        awscli[AWS CLI]
    end

    subgraph "Docker Network: kind"
        subgraph "Kind Cluster"
            subgraph "Control Plane"
                api[API Server]
            end

            subgraph "Namespace: external-dns"
                ed[ExternalDNS Pod]
            end

            subgraph "Namespace: default"
                nginx[Nginx Pod]
                svc[Service
nginx.example.com]
                ing[Ingress
nginx-ingress.example.com]
            end
        end

        ls[LocalStack Container
Route53 Mock]
    end

    kubectl -->|manages| api
    awscli -->|configures DNS| ls
    ed -->|watches| svc
    ed -->|watches| ing
    ed -->|creates/updates
DNS records| ls
    api -->|provides resources| ed
    svc -->|routes to| nginx
    ing -->|routes to| svc

    style ed fill:#326ce5,color:#fff
    style ls fill:#ff9900,color:#fff
    style kubectl fill:#326ce5,color:#fff
    style awscli fill:#ff9900,color:#fff

DNS Record Creation Flow

Description: This sequence diagram demonstrates the automated DNS lifecycle management. When you create a Service with an ExternalDNS annotation, ExternalDNS detects the new resource, extracts the hostname, and creates corresponding DNS records in LocalStack. It also creates TXT records for ownership tracking. When the Service is deleted, ExternalDNS automatically cleans up the DNS records.

mermaid
sequenceDiagram
    participant User
    participant K8s as Kubernetes API
    participant ED as ExternalDNS
    participant LS as LocalStack Route53

    User->>K8s: kubectl apply -f service.yaml
    K8s->>K8s: Service created

    Note over ED: Watches for Service changes
    K8s->>ED: Service event detected
    ED->>ED: Parse annotation:
nginx.example.com
    ED->>LS: Check existing records
    LS-->>ED: No record exists
    ED->>LS: Create A record
nginx.example.com → LoadBalancer IP
    LS->>LS: Record created
    LS-->>ED: Success
    ED->>LS: Create TXT record
"heritage=external-dns,..."
    LS-->>ED: Success

    Note over ED: Continues watching for changes

    User->>K8s: kubectl delete service nginx
    K8s->>ED: Service deletion event
    ED->>LS: Delete A record
    ED->>LS: Delete TXT record
    LS-->>ED: Records deleted

ExternalDNS Decision Flow

Description: This flowchart illustrates ExternalDNS's decision-making process. It checks for DNS annotations, validates the domain filter, ensures IP addresses are available, and uses TXT records to track ownership. This prevents conflicts when multiple DNS controllers or manual DNS entries exist. The ownership mechanism ensures ExternalDNS only modifies records it created.

mermaid
flowchart TD
    Start([ExternalDNS detects
Kubernetes resource])

    Start --> CheckAnnotation{Has external-dns
annotation?}
    CheckAnnotation -->|No| Skip[Skip - No DNS needed]
    CheckAnnotation -->|Yes| ExtractHost[Extract hostname from
annotation]

    ExtractHost --> CheckDomain{Hostname matches
domain-filter?}
    CheckDomain -->|No| Skip2[Skip - Outside managed domain]
    CheckDomain -->|Yes| GetIP[Get LoadBalancer IP or
Ingress address]

    GetIP --> CheckIP{IP/Address
available?}
    CheckIP -->|No| Wait[Wait for IP assignment]
    CheckIP -->|Yes| QueryRoute53[Query LocalStack Route53
for existing record]

    QueryRoute53 --> CheckExists{Record
exists?}
    CheckExists -->|No| Create[Create new A record
+ TXT ownership record]
    CheckExists -->|Yes| CheckOwner{Check TXT record
owner ID}

    CheckOwner -->|Not owned by us| Skip3[Skip - Managed by
another controller]
    CheckOwner -->|Owned by us| CheckIP2{IP changed?}

    CheckIP2 -->|No| NoAction[No action needed]
    CheckIP2 -->|Yes| Update[Update A record
with new IP]

    Create --> Success([DNS record created])
    Update --> Success
    NoAction --> Success
    Skip --> End([End])
    Skip2 --> End
    Skip3 --> End
    Wait --> End
    Success --> End

    style Start fill:#90EE90,color:#000
    style Success fill:#90EE90,color:#000
    style Create fill:#ADD8E6,color:#000
    style Update fill:#ADD8E6,color:#000
    style Skip fill:#FFB6C1,color:#000
    style Skip2 fill:#FFB6C1,color:#000
    style Skip3 fill:#FFB6C1,color:#000

Additional Resources