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.
Prerequisites
Section titled “Prerequisites”- 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.
Implementation
Section titled “Implementation”Creating a Zone
Section titled “Creating a Zone”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.
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.
export ZONEID="1035d922-7a90-4a20-ba89-308ed9eb0cff"Setting Up Kubernetes Secret
Section titled “Setting Up Kubernetes Secret”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.
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.jsonDeploying ExternalDNS with Webhook
Section titled “Deploying ExternalDNS with Webhook”With the preparations in place, you’re set to deploy ExternalDNS using our webhook:
kubectl apply -f - <<EOFapiVersion: v1kind: ServiceAccountmetadata: name: external-dns namespace: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dnsimagePullSecrets: - name: docker-secret---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dnsrules: - 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/v1kind: ClusterRoleBindingmetadata: name: external-dns-viewer labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dnsroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dnssubjects: - kind: ServiceAccount name: external-dns namespace: external-dns---apiVersion: v1kind: Servicemetadata: name: external-dns namespace: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dnsspec: 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/v1kind: Deploymentmetadata: name: external-dns namespace: external-dns labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dnsspec: 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: trueEOFBear 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:
kubectl get pods -n external-dnsYou will get the following output:
NAME READY STATUS RESTARTS AGEexternal-dns-7fb65c6899-lqcsp 2/2 Running 0 18sDeploying a Sample Application
Section titled “Deploying a Sample Application”Deploy a reverse proxy, such as the Nginx Ingress Controller:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo updatekubectl create ns ingress-nginxhelm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginxAs 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.
kubectl apply -f - <<EOFapiVersion: v1kind: Namespacemetadata: name: example-app---apiVersion: v1kind: ConfigMapmetadata: name: example-app namespace: example-appdata: index.html: | <!DOCTYPE html> <html> <head> <title>ExampleApp</title> </head> <body> <header> <h1 id="Hello World">Hello World</h1> </header> </body> </html>---apiVersion: apps/v1kind: Deploymentmetadata: name: example-app namespace: example-appspec: 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: v1kind: Servicemetadata: name: example-app namespace: example-appspec: selector: app: example-app ports: - protocol: TCP port: 80 targetPort: 80---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: 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: 80EOFTo verify the application’s functionality:
kubectl get pods -n example-appYou will get the following output:
NAME READY STATUS RESTARTS AGEexample-app-588974765d-r6shs 1/1 Running 0 23sAllow a brief period to pass, then confirm that ExternalDNS has generated the records for our sample application:
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:
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.