Skip to content

Use ExternalDNS with STACKIT DNS

ExternalDNS serves as an add-on for Kubernetes designed to automate the management of Domain Name System (DNS) records for Kubernetes services by utilizing various DNS providers. While Kubernetes traditionally manages DNS records internally, ExternalDNS augments this functionality by transferring the responsibility of DNS records management to an external DNS provider such as STACKIT. Consequently, the STACKIT webhook enables the management of your STACKIT domains within your Kubernetes cluster using ExternalDNS.

  • Cluster Type - SKE: This guide provides specific instructions for deploying on an SKE cluster. Alternative cluster configurations may necessitate varying implementation steps.
  • Required Credentials: A dedicated service account and corresponding authentication service account key are necessary for the deployment process. ExternalDNS will programmatically create and delete a temporary TXT-record in the DNS zone, requiring the service account to have DNS Admin permissions.
  • STACKIT CLI: Ensure you have the STACKIT CLI installed and authenticated with your STACKIT service account.
  • Application Exposure Mechanism: This documentation presupposes the use of an SKE cluster. Consequently, the methods for exposing applications via LoadBalancer or NodePort services may vary based on your cluster configuration.

By adhering to these prerequisites, you will be better prepared for the subsequent steps outlined in this tutorial.

Create a DNS zone. This can be done either through our user interface (UI) or the REST API using STACKIT CLI. For the purpose of this guide, we will utilize the STACKIT DNS API.

Terminal window
export PROJECTID="4ec710a8-fe7c-42d3-aca4-84d9ff69a14e"
export DNS_ZONE="example.runs.onstackit.cloud"
stackit curl --location "https://dns.api.stackit.cloud/v1/projects/$PROJECTID/zones" \
--header 'Content-Type: application/json' \
--data '{
"name": "external-dns",
"dnsName": "'"$DNS_ZONE"'"
}'

Once the zone is created using the API, you’ll receive a response containing a unique identifier for the zone, referred to as zone.id. This ID is essential for making future API calls related to this zone.

Terminal window
export ZONEID="1035d922-7a90-4a20-ba89-308ed9eb0cff"

Construct a Kubernetes secret using path to the aforementioned service account key. This secret will be mounted inside the container and aids our webhook in authenticating with our API.

Terminal window
kubectl create ns external-dns && \
kubectl create secret generic external-dns-stackit-webhook \
-n external-dns \
--from-file=sa.json=/path/to/stackit-service-account-key.json

With the preparations in place, you’re set to deploy ExternalDNS using our webhook:

Terminal window
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: external-dns
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
imagePullSecrets:
- name: docker-secret
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list","watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["services","endpoints"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: external-dns
---
apiVersion: v1
kind: Service
metadata:
name: external-dns
namespace: external-dns
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
ports:
- name: http
port: 7979
targetPort: http
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: external-dns
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
spec:
serviceAccountName: external-dns
securityContext:
fsGroup: 65534
volumes:
- name: stackit-sa-key
secret:
secretName: external-dns-stackit-webhook
items:
- key: sa.json
path: sa.json
containers:
- name: external-dns
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
image: registry.k8s.io/external-dns/external-dns:v0.14.0
imagePullPolicy: IfNotPresent
args:
- --log-level=info
- --log-format=text
- --interval=1m
- --source=service
- --source=ingress
- --policy=sync # set it upsert-only if you don't want it to delete records
- --txt-owner-id=my-testcluster # has to be uniq for each cluster if multiple external-dns managing one zone
- --provider=webhook
ports:
- name: http
protocol: TCP
containerPort: 7979
livenessProbe:
failureThreshold: 2
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
failureThreshold: 6
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
- name: webhook
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
image: ghcr.io/stackitcloud/external-dns-stackit-webhook:v0.2.0
imagePullPolicy: IfNotPresent
args:
- --project-id=$PROJECTID
- --domain-filter=$DNS_ZONE
ports:
- name: http
protocol: TCP
containerPort: 8888
livenessProbe:
failureThreshold: 2
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
failureThreshold: 6
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
env:
- name: AUTH_KEY_PATH
value: /var/run/secrets/stackit/sa.json
volumeMounts:
- name: stackit-sa-key
mountPath: /var/run/secrets/stackit
readOnly: true
EOF

Bear in mind, setting a domain filter isn’t mandatory. However, it can be beneficial if your cluster has multiple records associated with different zones.

To verify that ExternalDNS is operational, use the command provided below:

Terminal window
kubectl get pods -n external-dns

You will get the following output:

NAME READY STATUS RESTARTS AGE
external-dns-7fb65c6899-lqcsp 2/2 Running 0 18s

Deploy a reverse proxy, such as the Nginx Ingress Controller:

Terminal window
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
kubectl create ns ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx

As a practical example, deploy a test application onto your Kubernetes cluster and make it accessible through an ingress resource. By doing this, you’re illustrating ExternalDNS’s ability to generate records in the previously established zone.

Terminal window
kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: example-app
---
apiVersion: v1
kind: ConfigMap
metadata:
name: example-app
namespace: example-app
data:
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>ExampleApp</title>
</head>
<body>
<header>
<h1 id="Hello World">Hello World</h1>
</header>
</body>
</html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-app
namespace: example-app
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: config-volume
mountPath: /usr/share/nginx/html
volumes:
- name: config-volume
configMap:
name: example-app
---
apiVersion: v1
kind: Service
metadata:
name: example-app
namespace: example-app
spec:
selector:
app: example-app
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: example-app
annotations:
ingress.kubernetes.io/rewrite-target: /
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: "app.example.runs.onstackit.cloud"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-app
port:
number: 80
EOF

To verify the application’s functionality:

Terminal window
kubectl get pods -n example-app

You will get the following output:

NAME READY STATUS RESTARTS AGE
example-app-588974765d-r6shs 1/1 Running 0 23s

Allow a brief period to pass, then confirm that ExternalDNS has generated the records for our sample application:

Terminal window
stackit curl --location --globoff "https://dns.api.stackit.cloud/v1/projects/$PROJECTID/zones/$ZONEID/rrsets" | \
jq '.rrSets[] | {name, type, records}'
{
"name": "a-app.example.runs.onstackit.cloud.",
"type": "TXT",
"records": [
{
"content": "\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/example-app/app-ingress\"",
"id": "a316420b-4c0b-4aad-b089-5af5a361f0f9"
}
]
}
{
"name": "app.example.runs.onstackit.cloud.",
"type": "TXT",
"records": [
{
"content": "\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/example-app/app-ingress\"",
"id": "cacd591c-d62c-4a61-99e4-7a70ca642cf6"
}
]
}
{
"name": "app.example.runs.onstackit.cloud.",
"type": "A",
"records": [
{
"content": "45.129.45.243",
"id": "06a9c311-1a13-4835-a694-589897db5008"
}
]
}
{
"name": "example.runs.onstackit.cloud.",
"type": "SOA",
"records": [
{
"content": "ns1.stackit.cloud hostmaster.stackit.cloud. 2023090500 3600 600 1209600 60",
"id": "0268092d-63cd-45b2-ad39-364f026cb0a2"
}
]
}
{
"name": "example.runs.onstackit.cloud.",
"type": "NS",
"records": [
{
"content": "ns1.stackit.cloud.",
"id": "49ea254f-c8c5-4db0-952a-ee9a47bd8f04"
},
{
"content": "ns2.stackit.cloud.",
"id": "d8a5c03d-3048-49e1-a2ff-2534307be56e"
}
]
}

Test accessing your website using curl:

Terminal window
curl http://app.example.runs.onstackit.cloud
<!DOCTYPE html>
<html>
<head>
<title>ExampleApp</title>
</head>
<body>
<header>
<h1 id="Hello World">Hello World</h1>
</header>
</body>
</html>

If you’ve observed, the current setup is not fortified. For enhanced security, contemplate deploying the Cert-Manager and integrating it with STACKIT DNS. Guidance on executing this is provided in Use STACKIT DNS for DNS01 to act as a DNS01 ACME Issuer with Cert-Manager.