Skip to content

Commit a31be63

Browse files
authored
Pihole external dns (#31)
* wrote about external dns with pihole * Made a thing to import screenshot files faster * fixed markdown of powershell syntax in some older posts * added support for code block line highlighting --------- Signed-off-by: David Söderlund <[email protected]>
1 parent b4c8083 commit a31be63

9 files changed

+201
-12
lines changed

docs/_plugins/highlight.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
module Jekyll
2+
module Tags
3+
class HighlightBlock
4+
def render_rouge(code)
5+
require "rouge"
6+
formatter = Rouge::Formatters::HTML.new
7+
formatter = line_highlighter_formatter(formatter) if @highlight_options[:mark_lines]
8+
formatter = table_formatter(formatter) if @highlight_options[:linenos]
9+
lexer = Rouge::Lexer.find_fancy(@lang, code) || Rouge::Lexers::PlainText
10+
formatter.format(lexer.lex(code))
11+
end
12+
13+
def line_highlighter_formatter(formatter)
14+
Rouge::Formatters::HTMLLineHighlighter.new(
15+
formatter,
16+
:highlight_lines => mark_lines
17+
)
18+
end
19+
20+
def mark_lines
21+
value = @highlight_options[:mark_lines]
22+
return value.map(&:to_i) if value.is_a?(Array)
23+
raise SyntaxError, "Syntax Error for mark_lines declaration. Expected a double-quoted list of integers."
24+
end
25+
26+
def table_formatter(formatter)
27+
Rouge::Formatters::HTMLTable.new(
28+
formatter,
29+
:css_class => "highlight",
30+
:gutter_class => "gutter",
31+
:code_class => "code"
32+
)
33+
end
34+
end
35+
end
36+
end
37+

docs/_posts/2025-01-15-testing-out-flexibility-in-microservices-deployment-with-dapr.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Then we went ahead and replaced one of the services with our own implementation
4848

4949
First we create the new app folder and initialize the dependencies.
5050

51-
``` PowerShell
51+
``` powershell
5252
# Creates a new typescript backend app with dapr to send tweets (eventually, the posting to social media is left as an excercise for the reader).
5353
mkdir tweet-js # our replacement of the tweet server from the Viktor's silly demo
5454
cd tweet-js
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: External DNS with pihole
3+
published: true
4+
excerpt_separator: <!--more-->
5+
---
6+
7+
The [external-dns operator for kubernetes](https://kubernetes-sigs.github.io/external-dns/) is simple enough that running it at home is as easy as **π**. In this post I will show how to set up external-dns with pihole as the DNS provider, and cover some of the quirks when you use [Istio](https://istio.io/) instead of a traditional ingress.
8+
9+
<!--more-->
10+
11+
## Background
12+
13+
In order to route traffic to an ingress you will need DNS records. In my home lab I run Istio ingress gateways, and previously I had to manually create DNS records for each hostname in my [DNS server which is pihole](https://pi-hole.net/). I have used external-dns in the past when I've helped run kubernetes on AKS or have routed traffic through my firewall to a k3s cluster where I have then used public DNS with Azure for AKS and Cloudflare for k3s respecively. When I found out there was a configuration for pihole I had to try it out with my [Talos Linux](https://www.talos.dev/) cluster.
14+
15+
## Pihole settings
16+
17+
In order to be able to automate the DNS configuration of pihole you need only the hostname and password for pihole.
18+
19+
I run with sealed secrets so my creation of the the password needed to go through the sealed secrets process before ending up in git to be then deployed by [ArgoCD](https://argo-cd.readthedocs.io).
20+
21+
``` powershell
22+
$somepassword = Read-Host -Prompt "Enter password" -MaskInput
23+
kubectl create secret generic -n external-dns pihole-password `
24+
--from-literal EXTERNAL_DNS_PIHOLE_PASSWORD=$somepassword -o yaml --dry-run=client `
25+
| kubeseal -o yaml > gitops\apps\rh-appset\mgmt\default\external-dns\external-dns-pihole\dns-pihole-ss.yaml
26+
Remove-Variable somepassword
27+
```
28+
29+
This renders the new sealed secrets in the folder for my external-dns-pihole application which is destined for the external-dns namespace.
30+
31+
The rest of the configuration can be found in the [documentation for external-dns](https://kubernetes-sigs.github.io/external-dns/latest/docs/tutorials/pihole/).
32+
33+
I went ahead and just put each resource in a separate file, and bundled it all with kustomize. I replaced some of the names to include my `-pihole` suffix in case I want to deploy another external-dns-cloudflare app for public access to this cluster in the future.
34+
35+
``` yaml
36+
# kustomize.yaml
37+
apiVersion: kustomize.config.k8s.io/v1beta1
38+
kind: Kustomization
39+
resources:
40+
- dns-pihole-crb.yaml
41+
- dns-pihole-dp.yaml
42+
- dns-pihole-sa.yaml
43+
- dns-pihole-cr.yaml
44+
- dns-pihole-ss.yaml
45+
```
46+
47+
Lastly I made sure that I included istio-gateways for discovering the hostnames I wanted to register.
48+
49+
{% highlight yaml mark_lines="14" %}
50+
# dns-pihole-dp.yaml
51+
apiVersion: apps/v1
52+
kind: Deployment
53+
metadata:
54+
name: external-dns-pihole
55+
spec:
56+
# Removed for brevity
57+
spec:
58+
serviceAccountName: external-dns-pihole
59+
containers:
60+
- name: external-dns-pihole
61+
image: registry.k8s.io/external-dns/external-dns:v0.15.1
62+
args:
63+
- --source=istio-gateway # ingress is also possible
64+
- --source=service # for additional loadbalancer services in the future
65+
- --domain-filter=mgmt.dsoderlund.consulting # home office domain for talos mgmt cluster
66+
- --provider=pihole
67+
- --policy=upsert-only # to avoid nuking existing records
68+
- --pihole-server=https://pihole.office.dsoderlund.consulting # hostname of pihole
69+
- --registry=noop
70+
# Removed for brevity
71+
{% endhighlight %}
72+
73+
Upon sync through argocd the app appears with the five different resources that I specified.
74+
75+
![External DNS with pihole](../assets/2025-01-16-21-14-12-external-dns-pihole-argocd.png)
76+
77+
## Issues I ran into
78+
79+
### External-dns cluster role doesn't cover istio resources
80+
81+
I got some fun errors like
82+
83+
> istio external-dns pihole "failed to sync *v1.Service: context deadline exceeded"
84+
85+
This is simply due to the pod not being able to query kubernetes for all of the resources it wants. Inspecting the default cr (ClusterRole) it became clear that I needed to add the istio resources.
86+
87+
{% highlight yaml mark_lines="16 17 18" %}
88+
# dns-pihole-cr.yaml
89+
apiVersion: rbac.authorization.k8s.io/v1
90+
kind: ClusterRole
91+
metadata:
92+
name: external-dns-pihole
93+
rules:
94+
- apiGroups: [""]
95+
resources: ["services","endpoints","pods"]
96+
verbs: ["get","watch","list"]
97+
- apiGroups: ["extensions","networking.k8s.io"]
98+
resources: ["ingresses"]
99+
verbs: ["get","watch","list"]
100+
- apiGroups: [""]
101+
resources: ["nodes"]
102+
verbs: ["list", "watch"]
103+
- apiGroups: ["networking.istio.io"]
104+
resources: ["gateways", "virtualservices"]
105+
verbs: ["get","watch","list"]
106+
{% endhighlight %}
107+
108+
### Wildcard CNAMEs are not supported
109+
110+
I had previously set up my gateway to pick up traffic from any matching DNS name on my home office network. Pihole doesn't support wildcard DNS records which is made very clear but the next error message I got.
111+
112+
> time="2025-01-16T19:53:40Z" level=info msg="add *.mgmt.dsoderlund.consulting IN A -> 192.168.0.32"
113+
114+
> time="2025-01-16T19:53:40Z" level=error msg="Failed to do run once: soft error\nUNSUPPORTED: Pihole DNS names cannot return wildcard"
115+
116+
The IP address *192.168.0.32* is the one [metallb](https://metallb.io/) assigned to my istio ingress service, which is how external-dns knows what it is supposed to tell pihole that the DNS record is for.
117+
118+
I simply updated my hostname list in the gateway with the names I needed, and after syncing with argocd things started happening in the pihole.
119+
120+
![Highlighting the changes to hostnames in the istio-gateway](../assets/2025-01-16-22-16-15-wildcard-hostname-replaced.png)
121+
122+
One that was in sync, hostnames started to appear in the pihole UI and DNS records started to work against the cluster again.
123+
124+
![pod logs of the DNS records being updated](../assets/2025-01-16-21-59-59-pod-logs-of-dns-records.png)
125+
126+
![pihole UI showing the new hostnames](../assets/2025-01-16-22-30-54-pihole-ui-showing-the-new-hostnames.png)
127+
128+
## Conclusion
129+
130+
My cluster is now equiped to automatically make sure dns names that I use on my istio gateways get mapped to the istio ingress service. This can be further used if I create new services that I don't want Istio for.

docs/_sass/base.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ pre {
162162
overflow: auto;
163163
overflow-y: hidden;
164164
}
165-
165+
.hll {
166+
background-color: #ffff1f5e;
167+
}
166168
code.highlighter-rouge {
167169
background: rgba(0,0,0,0.9);
168170
border: 1px solid rgba(255, 255, 255, 0.15);
52.3 KB
Loading
60.8 KB
Loading
100 KB
Loading
67.7 KB
Loading

jekyll.build.ps1

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ param(
33
[parameter()]$jekyllversion = '4',
44
[parameter()]$servecontainername = 'jekyll-serve',
55
[parameter()][string]$postname = '',
6+
[parameter()][int]$cutoffMinutes = 5,
67
[switch]$wait
78
)
89
$rootdir = git rev-parse --show-toplevel
@@ -17,25 +18,24 @@ task proofread {
1718
}
1819
}
1920
task new {
20-
mkdir docs -ea 0 | out-null
21+
New-Item -ItemType Directory -ErrorAction SilentlyContinue -Path docs | Out-Null
2122
push-location $rootdir/docs
22-
2323
#new
2424
docker run --rm -it --volume="$($PWD):/srv/jekyll" --env JEKYLL_ENV=production jekyll/jekyll:4 jekyll new . --force
2525

2626
Pop-Location
2727
}
2828

2929
task build {
30-
if (test-path .\docs) {
31-
Push-Location $rootdir/docs
32-
#build
33-
docker run --rm -it --volume="$($PWD):/srv/jekyll" --volume="$PWD/vendor/bundle:/usr/local/bundle" --env JEKYLL_ENV=production jekyll/jekyll:$jekyllversion /bin/sh -c "bundle install && jekyll build"
34-
Pop-Location
35-
}
36-
else {
37-
throw "You cannot build what does not exist, run invoke-build new first!"
30+
if (-not (test-path .\docs)) {
31+
throw "You cannot build what does not exist, run invokebuild new first!"
3832
}
33+
Push-Location $rootdir/docs
34+
#build
35+
docker run --rm -it --volume="$($PWD):/srv/jekyll" --volume="$PWD/vendor/bundle:/usr/local/bundle" --env JEKYLL_ENV=production jekyll/jekyll:$jekyllversion /bin/sh -c "bundle install && jekyll build"
36+
Pop-Location
37+
38+
3939
}
4040

4141
task remove stop, {
@@ -108,4 +108,24 @@ Content here
108108
code $postfile
109109
}
110110

111+
task importImages {
112+
if($IsLinux -eq $true) {
113+
$screenshotdir = Get-Item ~/Pictures
114+
$prefixToRemove = 'Screenshot from '
115+
}
116+
elseif($IsWindows -eq $true) {
117+
$screenshotdir = Get-Item ~/OneDrive/Pictures/Screenshots
118+
$prefixToRemove = 'Skärmbild '
119+
}
120+
if($screenshotdir -ne $null) {
121+
Get-ChildItem $screenshotdir | Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-$cutoffMinutes) } | ForEach-Object {
122+
$newname = $_.Name.Replace($prefixToRemove,'').Replace(' ','-').ToLower()
123+
Write-Host "Copying $($_.FullName) to $rootdir/docs/assets/$newname"
124+
Copy-Item $_.FullName "$rootdir/docs/assets/$newname"
125+
}
126+
}
127+
else {
128+
Write-Host "Couldn't import any screenshots, check the path [$screenshotdir]"
129+
}
130+
}
111131
task . proofread, serve, surf

0 commit comments

Comments
 (0)