Skip to content

Commit a3ebb38

Browse files
authored
gitops in backstage templates - Cdk8s crossplane keycloak (#34)
wrote about cdk8s for the keycloak crossplane provider to set oidc client redirect urls. framed around backstage templates which include gitops for iac.
1 parent 73a8830 commit a3ebb38

19 files changed

+257
-8
lines changed

docs/_posts/2023-04-14-the-joy-of-kubernetes-2---let-us-encrypt.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ tags: joy-of-kubernetes kubernetes cert-manager letsencrypt google cloud dns cer
77

88
# Welcome to the Joy of Kubernetes
99

10-
If you are new to the series, check out [the previous post](../the-joy-of-kubernetes-1-argocd-with-private-git-repo) about Argo CD if you like, we will leverage Argo CD a bit to deploy what we are doing today. You can just as well replace Argo CD with applying the manifests or helm charts manually.
10+
If you are new to the series, check out [the previous post](./the-joy-of-kubernetes-1-argocd-with-private-git-repo) about Argo CD if you like, we will leverage Argo CD a bit to deploy what we are doing today. You can just as well replace Argo CD with applying the manifests or helm charts manually.
1111

1212
In this second entry in The Joy of Kubernetes we will take a closer look at the Cert-Manager and it's ClusterIssuer resource. I am interested in requesting and issuing TLS certificates as secrets in kubernetes by just asking nicely, particulary for a real external DNS.
1313

@@ -30,7 +30,7 @@ In this second entry in The Joy of Kubernetes we will take a closer look at the
3030
## Prerequisites 🎨
3131

3232
- ~~A canvas, some brushes, and some paint~~ A kubernetes cluster and kubectl.
33-
- Optional, Argo CD with the application set creation from [the previous post](../the-joy-of-kubernetes-1-argocd-with-private-git-repo).
33+
- Optional, Argo CD with the application set creation from [the previous post](./the-joy-of-kubernetes-1-argocd-with-private-git-repo).
3434
- A domain that you control
3535
- A [supported DNS provider](https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers)
3636

docs/_posts/2023-04-20-the-joy-of-kubernetes-3---private-docker-registry-on-nfs-storage.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The goal is to set up a private container registry on our kubernetes cluster usi
3333
## Prerequisites 🎨
3434

3535
- ~~A canvas, some brushes, and some paint~~ A kubernetes cluster and kubectl.
36-
- Optional, Argo CD with the application set creation from [the first post in the series](../the-joy-of-kubernetes-1-argocd-with-private-git-repo).
36+
- Optional, Argo CD with the application set creation from [the first post in the series](./the-joy-of-kubernetes-1-argocd-with-private-git-repo).
3737
- The ability to set up NFS storage, or some other storage class
3838

3939
### Argo CD
@@ -193,7 +193,7 @@ images:
193193
```
194194
195195
## TLS and Ingress: Finishing touches
196-
I am so happy we have the cert-manager available to us from [the last episode](../2023-04-14-the-joy-of-kubernetes-2---let-us-encrypt.md).
196+
I am so happy we have the cert-manager available to us from [the last episode](./the-joy-of-kubernetes-2-let-us-encrypt).
197197
198198
This allows us to add highlights to our image container registry with TLS. Rather than ingress annotations we will perform the steps a bit differently today.
199199

docs/_posts/2025-01-12-certs-for-your-home-lab.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ In this post I will show you how to use cronjobs in kubernetes to automate the u
2727

2828
### Tell me, where do certificates come from?
2929

30-
[In a previous post](../the-joy-of-kubernetes-2-let-us-encrypt) we looked at how to use cert-manager to automate the creation of certificates from Let's Encrypt. This is increadibly useful of course to slap on your ingress so that any traffic coming in is encrypted and that the user can trust that they have come to the right place.
30+
[In a previous post](./the-joy-of-kubernetes-2-let-us-encrypt) we looked at how to use cert-manager to automate the creation of certificates from Let's Encrypt. This is increadibly useful of course to slap on your ingress so that any traffic coming in is encrypted and that the user can trust that they have come to the right place.
3131

3232
But what if you want to use certificates outside of kubernetes, like for servers in your homelab or other equipment in your office?
3333

@@ -89,7 +89,7 @@ Pi-hole struck a balance between how simple it was and how poor the existing doc
8989

9090
### The certificate request
9191

92-
Similar to in [the previous post](../the-joy-of-kubernetes-2-let-us-encrypt), in this cluster I have set up a ClusterIssuer called letsencrypt-prod. Each certificate has this form:
92+
Similar to in [the previous post](./the-joy-of-kubernetes-2-let-us-encrypt), in this cluster I have set up a ClusterIssuer called letsencrypt-prod. Each certificate has this form:
9393

9494
``` yaml
9595
apiVersion: cert-manager.io/v1

docs/_posts/2025-02-17-utilizing-nvidia-hardware-in-kubernetes-on-virtual-machines.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ And lastly I added some configuration analogous to ingress, with istio being wha
504504

505505
Istio will handle tls termination at the gateway level, if I were to run a service mesh it would do mTLS from there to the pod, but I have no need.
506506

507-
I am reusing a certificate that matches *.mgmt.dsoderlund.consulting which is the hostname that maps to the IP of the istio ingress gateway available on my office network. Check out previous posts on [how I got DNS to work in the office](../2025-01-17-external-dns-with-pi-hole), and [how the istio gateway certificates are generated](../the-joy-of-kubernetes-2-let-us-encrypt).
507+
I am reusing a certificate that matches *.mgmt.dsoderlund.consulting which is the hostname that maps to the IP of the istio ingress gateway available on my office network. Check out previous posts on [how I got DNS to work in the office](./external-dns-with-pi-hole), and [how the istio gateway certificates are generated](./the-joy-of-kubernetes-2-let-us-encrypt).
508508

509509
{% highlight yaml %}
510510
apiVersion: networking.istio.io/v1beta1
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
title: GitOps in Backstage templates
3+
published: true
4+
excerpt_separator: <!--more-->
5+
tags: keycloak authz cdk8s argocd backstage crossplane
6+
---
7+
8+
Complicating things is fun, and sometimes also useful!
9+
10+
All I wanted was to make an elegant way for requesting an external hostname in my api gateway as part of my ingress abstraction.
11+
12+
<!--more-->
13+
14+
![overview of the abomination](../assets/2025-06-05_21-46-00-abomination.png)
15+
16+
Once I actually got this to work I took a step back and wondered how did I get here?
17+
18+
![rant from bluesky](../assets/2025-07-07_21-27-55-blueskypost.png)
19+
20+
[Picture is from this post](https://bsky.app/profile/dsoderlund.consulting/post/3lqv3nf7esc2i)
21+
22+
## The problem I want to solve
23+
24+
I want to be able to create a new file with one or more URLs. All the files in the folder will together be an array of valid redirect URLs for my oauth2proxy client in keycloak such that external traffic for new apps on those hostnames will be valid for authorization without allowing everything and anything. I want this configuration to be in git to allow gitops work, add new configuration with templates without changing files, and to be able to remove or change a file to update the configuration in keycloak.
25+
26+
This picture illustrates the flow of information when using templates for a new repo with a gitops component in an existing gitops-repo.
27+
28+
![flow of information](../assets/2025-07-08_10-54-26.png)
29+
30+
For a summary of where this "callback url" configuration item fits in my api-gateway. Check out [this old post](./istio-api-gateway-with-keycloak-as-idp)
31+
32+
Strap yourselves in, this is a journey!
33+
34+
## Background
35+
36+
Backstage is a platform for building internal developer portals, and I have worked a lot with creating and maintaining templates in backstage. In my interest in being able to let applications and their dependant resources have a shared lifecyle, I have often chosen to use crossplane over terraform for my infrastructure-as-code so that it all fits together nicely when prototyping and demonstrating ideas.
37+
38+
### Home lab setup
39+
40+
In my homelab I have this setup.
41+
42+
![](../assets/2025-07-07_20-24-30-homelab-setup.png)
43+
44+
Basically traffic can reach my cluster in a couple of different ways. I have two different ingress gateways, one bound to hostnames which resolvable and routable externally with public DNS, and one bound to *.mgmt.dsoderlund.consulting which is resolvable and routable locally or with VPN.
45+
46+
For the public addresses some are via a cloudflare vpn tunnel, and some are "old school not so secure" port-forwarded via the static public IP from my ISP. Usually this is turned off except when holding an interactive demonstration with others.
47+
48+
For kubernetes the nodes are talos vms virtualized on proxmox.
49+
50+
Everything relevant runs in kubernetes with the exception of my dns (pi-hole), the router (openwrt), and storage (synology-nas).
51+
52+
### Lifecycle of a fullstack app
53+
54+
In a recent webinar I wanted to demonstrate the expections for a developer of their platform, and as part of that lifecycle management of a fullstack app generated with a template. The template should work such that you would input information about your new app and you would get a new repo, a new deployment, and a registration in the catalog which would then link to everything providing a single pane of glass.
55+
56+
![](../assets/2025-07-07_20-30-27-twelvefactorapp.png)
57+
58+
So when using this "fullstackapp" template what happens is of course that the four different resources get created (frontend, backend, ingress/virtualservice, database).
59+
60+
The end result from an operations perspective would look like this from the argocd ui.
61+
62+
![argocd view of the four resources](../assets/2025-07-08_10-13-00.png)
63+
64+
[Check out the full webinar here if you want more details](https://www.youtube.com/watch?v=0-5HOpMCTiw)
65+
66+
### Gitops
67+
68+
What I expect when it comes to lifecycle management then, is that I can then make changes or remove those resources from the gitops-repo and the desired state of my infrastructure will be reconciled accordingly.
69+
70+
Lastly I want the structure of my gitops-repo to be add only if possible, that is **I don't want to write code for my templates that make changes to existing files** because then I have leaky abstractions.
71+
72+
[The public repo I am using for the new fullstack app demonstration is here](https://github.com/dsoderlund-consulting/demo-gitops)
73+
74+
### My backstage template
75+
76+
The template summary looks like this when run for a fullstack app called "developersbay".
77+
78+
![backstage template review](../assets/2025-07-08_09-45-08.png)
79+
80+
81+
Things to highlight:
82+
- The hostname will be `<componentname>.example.com` and an ingress will be created if we tick the ingress box.
83+
- The database connection details will be injected as environment variables if we tick the database box.
84+
- Frontend bundle and backend server will both run on the same hostname but backend will use /api prefix.
85+
86+
Once the template starts to run you can see logs of what is being done and upon completion you get links to the new component in backstage and the new git repo.
87+
88+
![template results](../assets/2025-07-08_09-50-15.png)
89+
90+
And the resulting component view in backstage would look like this. Very nice, it has links to builds, and to argocd deployments.
91+
92+
![single pane of glass](../assets/2025-07-08_10-36-48.png)
93+
94+
## Hostname in backstage template to valid callback URL
95+
96+
Back to implementation details.
97+
98+
Ok, so we know from our form that the user can request which hostname to use via the component name.
99+
100+
Given our initial requirement of a bunch of files with one or more URLs, why not just create a new file for each component in a folder and have those be put together as the configuration for keycloak?
101+
102+
[Here is the actual commit from when this template was run in the webinar.](https://github.com/dsoderlund-consulting/demo-gitops/commit/96cc61db2e3a6a46d423875e829b9b2d8da07570)
103+
104+
This new file demonstrates the way configuration should work.
105+
106+
![the new url file](../assets/2025-07-08_10-41-00.png)
107+
108+
### Enter crossplane
109+
110+
The first issue of course is how do we configure keycloak with gitops? Should we call an API directly with our CD pipeline? Should we use terraform? [No we are using crossplane here and this excellent provider for keycloak.](https://github.com/crossplane-contrib/provider-keycloak)
111+
112+
Next problem, how do we create a nice abstraction of an oidc-client which doesn't require changes to any files?
113+
114+
### Content from any number of files should populate array of a kubernetes resource
115+
116+
My first idea was to use kustomize and splice things together with patches, but it turns out that kustomize doesn't support globbing or any way I could figure out to go from multiple files whose names are not known without changing the kustomization files into one field in a resource.
117+
118+
Again, the point here is that the backstage template will create a new file with the hostname we want, it shouldn't have to change any existing files or know any other structure.
119+
120+
One could conceivably construct a helm chart but at that point you are making things complicated without the fun part which is what this post is all about!
121+
122+
It then dawned on me that I already have the ~~perfect~~ solution from [my post about cdk8s in argocd](./cdks8s-through-argocd).
123+
124+
### cdk8s
125+
126+
With cdk8s we can render the resource defintion (yaml or json) that we want argocd to deploy, or in my case I can check it in as is and let my argocd plugin figure it out (less recommended, more fun).
127+
128+
Here is my general idea of something to put into gitops to create the oauth2proxy client configuration I want.
129+
130+
1. Import the crossplane keycloak CRD to the new cdk8s folder
131+
2. Write some typescript to read the files in the folder and create an array of strings.
132+
3. Validate each URL with regex
133+
4. Construct the openid client resource.
134+
135+
Again the code is publically available on github if you want to check it out further.
136+
137+
138+
``` sh
139+
kubectl get crds openidclient.keycloak.crossplane.io -o json | cdk8s import /dev/stdin
140+
```
141+
142+
``` typescript
143+
import { Construct } from "constructs";
144+
import { App, Chart, ChartProps } from "cdk8s";
145+
import {
146+
Client,
147+
ClientSpecDeletionPolicy,
148+
ClientSpecManagementPolicies,
149+
} from "./imports/openidclient.keycloak.crossplane.io";
150+
import * as path from "path";
151+
import { getValidRedirectUrisFromDnsNames } from "./getValidRedirectUrisFromDnsNames";
152+
153+
export class MyChart extends Chart {
154+
constructor(
155+
scope: Construct,
156+
id: string,
157+
props: ChartProps = { disableResourceNameHashes: true }
158+
) {
159+
super(scope, id, props);
160+
const allowedDnsNamesFolder = path.join(__dirname, "allowedDnsNames");
161+
const dynamicallyGeneratedRedirectUris = getValidRedirectUrisFromDnsNames(
162+
allowedDnsNamesFolder
163+
);
164+
new Client(this, "client", {
165+
metadata: {
166+
name: "oauth2proxy",
167+
annotations: {
168+
"dsoderlund.consulting/rendered-by": "cdk8s",
169+
"dsoderlund.consulting/managed-by": "crossplane",
170+
},
171+
},
172+
spec: {
173+
providerConfigRef: {
174+
name: "keycloak-config",
175+
},
176+
deletionPolicy: ClientSpecDeletionPolicy.ORPHAN,
177+
managementPolicies: [ClientSpecManagementPolicies.VALUE_ASTERISK],
178+
forProvider: {
179+
import: true,
180+
accessType: "CONFIDENTIAL",
181+
clientId: "oauth2proxy",
182+
description:
183+
"Generated through cdk8s and applied with crossplane (you can't make changes to this in the keycloak UI, they will be overwritten)",
184+
realmId: "master",
185+
validRedirectUris:
186+
dynamicallyGeneratedRedirectUris.length > 0
187+
? dynamicallyGeneratedRedirectUris
188+
: undefined,
189+
},
190+
},
191+
});
192+
}
193+
}
194+
195+
const app = new App();
196+
new MyChart(app, "oauth2proxy-shared-config");
197+
app.synth();
198+
```
199+
200+
### The abstraction in action
201+
202+
To summarize, the new file with the new hostname was added to the gitops-repo. Once synched with argocd, the cdk8s plugin synthesizes the cdk8s app which resolves the array of URLs for the oauth2proxy client.
203+
204+
205+
``` yaml
206+
apiVersion: openidclient.keycloak.crossplane.io/v1alpha1
207+
kind: Client
208+
metadata:
209+
annotations:
210+
argocd.argoproj.io/tracking-id: >-
211+
oauth2proxy-shared-config:openidclient.keycloak.crossplane.io/Client:oauth2proxy/oauth2proxy
212+
dsoderlund.consulting/managed-by: crossplane
213+
dsoderlund.consulting/rendered-by: cdk8s
214+
name: oauth2proxy
215+
spec:
216+
deletionPolicy: Orphan
217+
forProvider:
218+
accessType: CONFIDENTIAL
219+
clientId: oauth2proxy
220+
description: >-
221+
Generated through cdk8s and applied with crossplane (you can't make
222+
changes to this in the keycloak UI, they will be overwritten)
223+
import: true
224+
realmId: master
225+
validRedirectUris:
226+
- https://demo.dsoderlund.consulting/oauth2/callback
227+
- https://demo2.sam.dsoderlund.consulting/oauth2/callback
228+
- https://developersbay.sam.dsoderlund.consulting/oauth2/callback
229+
managementPolicies:
230+
- '*'
231+
providerConfigRef:
232+
name: keycloak-config
233+
```
234+
235+
And once that information makes its way into kubernetes, crossplane will sync it in keycloak and the callback URL for the new application gets registered allowing ingress traffic to be handled by the api gateway.
236+
237+
![client in keycloak](../assets/2025-07-08_10-50-51.png)
238+
239+
## Wrap up
240+
241+
Great, so now I can have my backstage template clone the gitops repo, add the file with the hostname I my application should have, once synched we can surf to that address and be served a working application. Once I grow tired of this app we just remove the file together with the rest of the application configuration and the valid callback urls for the client in keycloak get fewer.
723 KB
Loading
638 KB
Loading
52.9 KB
Loading
162 KB
Loading
111 KB
Loading

0 commit comments

Comments
 (0)