Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 144 additions & 131 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,19 @@
[![GitHub stars](https://img.shields.io/github/stars/stackitcloud/external-dns-stackit-webhook.svg?style=social&label=Star&maxAge=2592000)](https://github.com/stackitcloud/external-dns-stackit-webhook/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/stackitcloud/external-dns-stackit-webhook.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/stackitcloud/external-dns-stackit-webhook/network)

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](https://github.com/kubernetes-sigs/external-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.

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.
Consequently, the STACKIT webhook enables the management of your STACKIT domains within your Kubernetes cluster using [ExternalDNS](https://github.com/kubernetes-sigs/external-dns).

For utilizing ExternalDNS with STACKIT, it is mandatory to establish a STACKIT project, create credentials (either a Service Account Key or configure Workload Identity Federation), authorize the service account with the DNS Admin role, and 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](https://github.com/stackitcloud/external-dns-stackit-webhook/pkgs/container/external-dns-stackit-webhook).
The deployment is compatible with all Kubernetes-supported methods. The subsequent example
demonstrates the deployment as a
[sidecar container](https://kubernetes.io/docs/concepts/workloads/pods/#workload-resources-for-managing-pods)
within the ExternalDNS pod.
The STACKIT webhook is provided as a standard Open Container Initiative (OCI) image available in the [GitHub container registry](https://github.com/stackitcloud/external-dns-stackit-webhook/pkgs/container/external-dns-stackit-webhook). It is deployed as a [sidecar container](https://kubernetes.io/docs/concepts/workloads/pods/#workload-resources-for-managing-pods) within the ExternalDNS pod.

### Option A: Authenticating via Service Account Key

This method uses a standard STACKIT Service Account Key mounted into the container as a file.

```shell
# Create a Secret containing the STACKIT service-account key JSON (as a file).
Expand All @@ -38,8 +32,7 @@ kubectl -n default create secret generic external-dns-stackit-webhook \
--from-file=sa.json=/path/to/stackit-service-account-key.json
```

```shell
kubectl apply -f - <<EOF
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
Expand Down Expand Up @@ -154,7 +147,7 @@ spec:
- --interval=1m
- --source=service
- --source=ingress
- --policy=sync # set it upsert-only if you don't want it to delete records
- --policy=sync # set to upsert-only if you don't want it to delete records
- --provider=webhook
ports:
- name: http
Expand Down Expand Up @@ -186,10 +179,11 @@ spec:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
image: ghcr.io/stackitcloud/external-dns-stackit-webhook:v0.2.0
image: ghcr.io/stackitcloud/external-dns-stackit-webhook:v1.0.0
imagePullPolicy: IfNotPresent
args:
- --project-id=c158c736-0300-4044-95c4-b7d404279b35 # your project id
- --auth-key-path=/var/run/secrets/stackit/sa.json
ports:
- name: http
protocol: TCP
Expand All @@ -212,136 +206,157 @@ spec:
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
```

### Option B: Authenticating via Workload Identity Federation (WIF)

If your cluster supports Workload Identity Federation, you can avoid managing long-lived Service Account keys entirely by projecting a short-lived token into the webhook container.

For prerequisites and cluster setup, refer to the [Use Workload Identity STACKIT documentation](https://docs.stackit.cloud/de/products/runtime/kubernetes-engine/how-tos/workload-identity/).

If you are using STACKIT Kubernetes Engine (SKE) or have the `stackit-pod-identity-webhook` installed, you do not need to manually mount the projected token volumes. You simply annotate the ServiceAccount, and the identity webhook will automatically inject the token and `STACKIT_FEDERATED_TOKEN_FILE` environment variable into the pod.

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: default
annotations:
# Specify the STACKIT Service Account email to assume the identity of
workload-identity.stackit.cloud/service-account-email: "your-service-account@sa.stackit.cloud"
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
```

In your deployment, simply pass the `--auth-wif` flag to explicitly enforce the federated flow:

```yaml
- name: webhook
image: ghcr.io/stackitcloud/external-dns-stackit-webhook:v1.0.0
args:
- --project-id=c158c736-0300-4044-95c4-b7d404279b35
- --auth-wif
```

## 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`).
- `--token-url`/`TOKEN_URL` (optional): Specifies alternative URL for authentication with service account key (default "https://service-account.api.stackit.cloud/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.
The configuration of the STACKIT webhook is accomplished through command-line arguments or environment variables. The webhook utilizes explicit flags to enforce authentication intent, but it will fall back to automatic SDK discovery if no explicit auth flag is provided.

### Authentication Flags (Choose ONE)
- `--auth-key-path`/`AUTH_KEY_PATH`: Defines the file path of the Service Account key JSON.
- `--auth-wif`/`AUTH_WIF` (boolean): Explicitly enables Workload Identity Federation (WIF) authentication.
- `--auth-wif-token-path`/`AUTH_WIF_TOKEN_PATH` (optional): Defines a custom file path for the federated JWT token for WIF authentication. This is generally only needed if you are overriding standard Kubernetes volume projections manually.

*Note: If no explicit `--auth-*` flags are provided, the webhook delegates authentication to the STACKIT SDK, which will automatically search the environment for standard SDK variables (e.g., `STACKIT_FEDERATED_TOKEN_FILE`, `STACKIT_SERVICE_ACCOUNT_KEY_PATH`) or a local `~/.stackit/credentials.json` file.*

### General Configuration
- `--project-id`/`PROJECT_ID` (required): Specifies the project ID of the STACKIT project.
- `--token-url`/`TOKEN_URL` (optional): Specifies an alternative OAuth2 endpoint for trading the Service Account key for an access token (default: "https://service-account.api.stackit.cloud/token").
- `--worker`/`WORKER` (optional): Specifies the number of concurrent workers to employ for querying the API. Given that we iterate over all zones and records, this is parallelized. Avoid setting this excessively high to prevent `429 Too Many Requests` responses from the API (default: 10).
- `--base-url`/`BASE_URL` (optional): Identifies the Base URL for utilizing the STACKIT DNS API (default: "https://dns.api.stackit.cloud").
- `--api-port`/`API_PORT` (optional): Specifies the port the webhook listens on (default: 8888).
- `--domain-filter`/`DOMAIN_FILTER` (optional): Establishes a filter for DNS zone names (default: []).
- `--dry-run`/`DRY_RUN` (optional): Specifies whether to perform a dry run without applying actual DNS changes (default: false).
- `--log-level`/`LOG_LEVEL` (optional): Defines the logging level. Possible values are: `debug`, `info`, `warn`, `error` (default: "info").

## 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:
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`.
```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**: ExternalDNS will try to create a TXT record named `a-example.runs.onstackit.cloud`, which will fail because you cannot establish a record outside the boundary of the zone. The solution is to use a name that resolves *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:
For a project containing the zone `example.runs.onstackit.cloud`, suppose you've created these two ingresses:

```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 exclude `test.example.stackit.rocks` and only
generate
the record set for `test.example.runs.onstackit.cloud`.
```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
```

## Development
**Why isn't it working?**

**Answer**: ExternalDNS will attempt to establish a record set for `test.example.stackit.rocks`. Because the zone `example.stackit.rocks` does not exist within the project, the operation will fail.

Run the app:
There are two potential fixes:
- Incorporate the zone `example.stackit.rocks` into the STACKIT project.
- Restrict ExternalDNS scoping by applying a domain filter flag `--domain-filter="example.runs.onstackit.cloud"`. This forces the webhook to ignore `test.example.stackit.rocks` and only synchronize records for `test.example.runs.onstackit.cloud`.

## Development

Run the app locally:
```bash
export BASE_URL="https://dns.api.stackit.cloud"
export PROJECT_ID="c158c736-0300-4044-95c4-b7d404279b35"
Expand All @@ -351,13 +366,11 @@ make run
```

Lint the code:

```bash
make lint
```

Test the code:

```bash
make test
```
Expand All @@ -373,4 +386,4 @@ make test-e2e-local \
PROJECT_ID="your-project-id" \
ZONE_NAME="your.test.zone.cloud" \
AUTH_KEY_PATH="/absolute/path/to/your/sa.json"
```
```
Loading