Awesome
STACKIT Webhook - ExternalDNS
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.
For utilizing ExternalDNS with STACKIT, it is mandatory to establish a STACKIT project, a service account within the project, generate an authentication token for the service account, authorize the service account to create and read dns zones, and finally, establish a STACKIT zone.
Kubernetes Deployment
The STACKIT webhook is presented as a standard Open Container Initiative (OCI) image released in the GitHub container registry. The deployment is compatible with all Kubernetes-supported methods. The subsequent example demonstrates the deployment as a sidecar container within the ExternalDNS pod.
# We create a Secret from an auth token. Alternatively, you can also
# use keys to authenticate the webhook - see "Authentication" below.
kubectl create secret generic external-dns-stackit-webhook --from-literal=auth-token='<Your-Token>'
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: default
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
---
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: default
---
apiVersion: v1
kind: Service
metadata:
name: external-dns
namespace: default
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: default
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
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
- --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=c158c736-0300-4044-95c4-b7d404279b35 # your project id
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_TOKEN
valueFrom:
secretKeyRef:
name: external-dns-stackit-webhook
key: auth-token
EOF
Configuration
The configuration of the STACKIT webhook can be accomplished through command line arguments and environment variables. Below are the options that are available.
--project-id
/PROJECT_ID
(required): Specifies the project id of the STACKIT project.--auth-token
/AUTH_TOKEN
(required ifauth-key-path
is not set): Defines the authentication token for the STACKIT API. Mutually exclusive with 'auth-key-path'.--auth-key-path
/AUTH_KEY_PATH
(required ifauth-token
is not set): Defines the file path of the service account key for the STACKIT API. Mutually exclusive with 'auth-token'.--worker
/WORKER
(optional): Specifies the number of workers to employ for querying the API. Given that we need to iterate over all zones and records, it can be parallelized. However, it is important to avoid setting this number excessively high to prevent receiving 429 rate limiting from the API (default 10).--base-url
/BASE_URL
(optional): Identifies the Base URL for utilizing the API ( default "https://dns.api.stackit.cloud").--api-port
/API_PORT
(optional): Specifies the port to listen on (default 8888).--domain-filter
/DOMAIN_FILER
(optional): Establishes a filter for DNS zone names (default []).--dry-run
/DRY_RUN
(optional): Specifies whether to perform a dry run (default false).--log-level
/LOG_LEVEL
(optional): Defines the log level (default "info"). Possible values are: debug, info, warn, error.
FAQ
1. Issue with Creating Service using External DNS Annotation
If your zone is example.runs.onstackit.cloud
and you're trying to create a service with the following external DNS
annotation:
```yaml
apiVersion: v1
kind: Service
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: example.runs.onstackit.cloud
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/component: controller
name: nginx-ingress-controller
namespace: nginx-ingress-controller
spec:
type: LoadBalancer
externalTrafficPolicy: Local
ipFamilyPolicy: SingleStack
ipFamilies:
- IPv4
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: nginx
app.kubernetes.io/name: ingress-nginx
```
<b>Why isn't it working?</b>
<b>Answer</b>: The External DNS will try to create a TXT record named a-example.runs.onstackit.cloud
, which will fail
because you can't establish a record outside the zone. The solution is to use a name that's within the zone, such as
nginx.example.runs.onstackit.cloud
.
2. Issues with Creating Ingresses not in the Zone
For a project containing the zone example.runs.onstackit.cloud
, suppose you've created these two ingress:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/rewrite-target: /
kubernetes.io/ingress.class: nginx
name: example-ingress-external-dns
namespace: default
spec:
rules:
- host: test.example.runs.onstackit.cloud
http:
paths:
- backend:
service:
name: example
port:
number: 80
path: /
pathType: Prefix
- host: test.example.stackit.rocks
http:
paths:
- backend:
service:
name: example
port:
number: 80
path: /
pathType: Prefix
```
<b>Why isn't it working?</b>
<b>Answer</b>: External DNS will attempt to establish a record set for test.example.stackit.rocks
. As the zone
example.stackit.rocks
isn't within the project, it'll fail. There are two potential fixes:
- Incorporate the zone
example.stackit.rocks
into the project. - Adjust the domain filter to
example.runs.onstackit.cloud
by setting the domain filter flag--domain-filter="example.runs.onstackit.cloud"
. This will excludetest.example.stackit.rocks
and only generate the record set fortest.example.runs.onstackit.cloud
.
Development
Run the app:
export BASE_URL="https://dns.api.stackit.cloud"
export PROJECT_ID="c158c736-0300-4044-95c4-b7d404279b35"
export AUTH_TOKEN="your-auth-token"
make run
Lint the code:
make lint
Test the code:
make test