Skip to content

External-DNS Webhook Provider to manage STACKIT DNS Records

License

Notifications You must be signed in to change notification settings

stackitcloud/external-dns-stackit-webhook

STACKIT Webhook - ExternalDNS

GoTemplateCIGo Report CardLicense: MITGitHub releaseLast CommitGitHub issuesGitHub pull requestsGitHub starsGitHub forks

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 a service account key, authorize the service account with DNS Admin role, 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.

# Create a Secret containing the STACKIT service-account key JSON (as a file).# This Secret will be mounted into the webhook container; AUTH_KEY_PATH will point to that mounted file. kubectl -n default create secret generic external-dns-stackit-webhook \ --from-file=sa.json=/path/to/stackit-service-account-key.json
kubectl apply -f - <<EOFapiVersion: v1kind: ServiceAccountmetadata: name: external-dns namespace: default labels: app.kubernetes.io/name: external-dns app.kubernetes.io/instance: external-dns---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: default---apiVersion: v1kind: Servicemetadata: name: external-dns namespace: default 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: default 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 - --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_KEY_PATH value: /var/run/secrets/stackit/sa.json volumeMounts: - name: stackit-sa-key mountPath: /var/run/secrets/stackit readOnly: trueEOF

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-key-path/AUTH_KEY_PATH (required): Defines the file path of the service account key for the STACKIT API. Prefer using a Kubernetes Secret mounted as a file and set AUTH_KEY_PATH to the in-container path (e.g. /var/run/secrets/stackit/sa.json).
  • --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 ``` 

Why isn't it working?

Answer: 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 ``` 

Why isn't it working?

Answer: 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 exclude test.example.stackit.rocks and only generate the record set for test.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_KEY_PATH="/absolute/path/to/stackit-service-account-key.json" make run

Lint the code:

make lint

Test the code:

make test