Published: May 9, 2024 by Isaac Johnson
I love Rundeck. It’s a great utility system for providing webhooks for invoking commands. I was thinking on it recently for addressing a business need at work when I saw an email hit my inbox announcing version 5.2 was just released.
Today, let’s see if we can setup Rundeck in Kubernetes (I’ve always had to fall back to Docker). We’ll explore the latest features and focus primarily on the free/community offering.
Installation
Up till now, the way I’ve run Rundeck is to just expose a Docker version via Kubernetes
This is essentially done via an endpoint
object in Kubernetes (one could use endpoint or endpointslice for this)
builder@DESKTOP-QADGF36:~$ kubectl get ingress rundeckingress
NAME CLASS HOSTS ADDRESS PORTS AGE
rundeckingress <none> rundeck.freshbrewed.science 80, 443 55d
builder@DESKTOP-QADGF36:~$ kubectl get service rundeck-external-ip
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
rundeck-external-ip ClusterIP None <none> 80/TCP 55d
builder@DESKTOP-QADGF36:~$ kubectl get service rundeck-external-ip
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
rundeck-external-ip ClusterIP None <none> 80/TCP 55d
builder@DESKTOP-QADGF36:~$ kubectl get endpoints rundeck-external-ip
NAME ENDPOINTS AGE
rundeck-external-ip 192.168.1.100:5440 55d
I won’t really dig into the specifics of that but you are welcome to review my last writeup that covers upgrading it in Docker.
I don’t want “Enterprise”, but I can use their docs to get an idea how we might run standard using the same constructs.
For instance, this compose shows running Rundeck with PostgreSQL
version: '3'
services:
rundeck:
image: ${RUNDECK_IMAGE:-rundeck/rundeck:SNAPSHOT}
links:
- postgres
environment:
RUNDECK_DATABASE_DRIVER: org.postgresql.Driver
RUNDECK_DATABASE_USERNAME: rundeck
RUNDECK_DATABASE_PASSWORD: rundeck
RUNDECK_DATABASE_URL: jdbc:postgresql://postgres/rundeck?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
RUNDECK_GRAILS_URL: http://localhost:4440
volumes:
- ${RUNDECK_LICENSE_FILE:-/dev/null}:/home/rundeck/etc/rundeckpro-license.key
ports:
- 4440:4440
postgres:
image: postgres
expose:
- 5432
environment:
- POSTGRES_DB=rundeck
- POSTGRES_USER=rundeck
- POSTGRES_PASSWORD=rundeck
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
Converting that to a Kubernetes manifest with the adjusted image and removing the license key:
apiVersion: apps/v1
kind: Deployment
metadata:
name: rundeck
spec:
replicas: 1
selector:
matchLabels:
app: rundeck
template:
metadata:
labels:
app: rundeck
spec:
containers:
- name: rundeck
image: rundeck/rundeck:5.2.0
env:
- name: RUNDECK_DATABASE_DRIVER
value: org.postgresql.Driver
- name: RUNDECK_DATABASE_USERNAME
value: rundeck
- name: RUNDECK_DATABASE_PASSWORD
value: rundeck
- name: RUNDECK_DATABASE_URL
value: jdbc:postgresql://postgres/rundeck?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
- name: RUNDECK_GRAILS_URL
value: http://localhost:4440
ports:
- containerPort: 4440
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: rundeck-service
spec:
selector:
app: rundeck
ports:
- protocol: TCP
port: 4440
targetPort: 4440
type: ClusterIP # Adjust the type as needed (e.g. ClusterIP)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres
env:
- name: POSTGRES_DB
value: rundeck
- name: POSTGRES_USER
value: rundeck
- name: POSTGRES_PASSWORD
value: rundeck
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi # Adjust storage size as needed
storageClassName: managed-nfs-storage # Replace with your storage class
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
I can now create a namespace and apply it
$ kubectl create ns rundeck
namespace/rundeck created
$ kubectl apply -n rundeck -f ./manifest.yaml
deployment.apps/rundeck created
service/rundeck-service created
deployment.apps/postgres created
persistentvolumeclaim/postgres-pvc created
service/postgres created
I found the NFS provisioner wasn’t jiving with PSQL
Warning BackOff 1s (x4 over 31s) kubelet Back-off restarting failed container postgres in pod postgres-6b984b548d-6vb9w_rundeck(93e755ea-cf66-4f23-95a9-82ce4d898378)
builder@DESKTOP-QADGF36:~/Workspaces/rundeck$ kubectl logs postgres-6b984b548d-6vb9w -n rundeck
chown: changing ownership of '/var/lib/postgresql/data': Operation not permitted
I waited a bit to see if it might clear up
$ kubectl get pods -n rundeck
NAME READY STATUS RESTARTS AGE
postgres-6b984b548d-6vb9w 0/1 CrashLoopBackOff 13 (3m54s ago) 46m
rundeck-6df9fcbbd-vlvw5 0/1 CrashLoopBackOff 12 (75s ago) 46m
I switched to local-path which worked:
$ kubectl get pods -n rundeck
NAME READY STATUS RESTARTS AGE
rundeck-6df9fcbbd-vmfg2 1/1 Running 0 66s
postgres-6b984b548d-9rb85 1/1 Running 0 66s
The first step we should be to change the admin password (which has the default password of “admin”)
I’ll port-forward to the service
$ kubectl port-forward svc/rundeck-service -n rundeck 4440:4440
Forwarding from 127.0.0.1:4440 -> 4440
Forwarding from [::1]:4440 -> 4440
Handling connection for 4440
Handling connection for 4440
Handling connection for 4440
Then login
In looking, I found that the user/pass is hardcoded in a file, unfortunately.
They do support SSO and LDAP auth via Env vars (defined here)
But looking, we can see it’s ‘admin:admin’ and ‘user:user’:
builder@DESKTOP-QADGF36:~/Workspaces/rundeck$ kubectl exec -it rundeck-6df9fcbbd-vmfg2 -n rundeck -- /bin/bash
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
rundeck@rundeck-6df9fcbbd-vmfg2:~$ cat /home/rundeck/server/config/realm.properties
#
# This file defines users passwords and roles for a HashUserRealm
#
# The format is
# <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed. The class
# org.mortbay.util.Password should be used to generate obfuscated
# passwords or password checksums
#
# This sets the temporary user accounts for the Rundeck app
#
admin:admin,user,admin
user:user,user
#
# example users matching the example aclpolicy template roles
#
#project-admin:admin,user,project_admin
#job-runner:admin,user,job_runner
#job-writer:admin,user,job_writer
#job-reader:admin,user,job_reader
#job-viewer:admin,user,job_viewer
Let’s enter some new territory and solve this using Kuberntes objects.
I’ll first create a configmap
apiVersion: v1
kind: ConfigMap
metadata:
name: realm-properties-configmap
data:
realm.properties: |
#
# This file defines users passwords and roles for a HashUserRealm
#
# The format is
# <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed. The class
# org.mortbay.util.Password should be used to generate obfuscated
# passwords or password checksums
#
# This sets the temporary user accounts for the Rundeck app
#
admin:admin,user,admin
user:user,user
#
# example users matching the example aclpolicy template roles
#
#project-admin:admin,user,project_admin
#job-runner:admin,user,job_runner
#job-writer:admin,user,job_writer
#job-reader:admin,user,job_reader
#job-viewer:admin,user,job_viewer
I editted the manifest
apiVersion: v1
kind: ConfigMap
metadata:
name: realm-properties-configmap
data:
realm.properties: |
#
# This file defines users passwords and roles for a HashUserRealm
#
# The format is
# <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed. The class
# org.mortbay.util.Password should be used to generate obfuscated
# passwords or password checksums
#
# This sets the temporary user accounts for the Rundeck app
#
admin:newpassword,user,admin
user:newpassword,user
#
# example users matching the example aclpolicy template roles
#
#project-admin:admin,user,project_admin
#job-runner:admin,user,job_runner
#job-writer:admin,user,job_writer
#job-reader:admin,user,job_reader
#job-viewer:admin,user,job_viewer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rundeck
spec:
replicas: 1
selector:
matchLabels:
app: rundeck
template:
metadata:
labels:
app: rundeck
spec:
containers:
- name: rundeck
image: rundeck/rundeck:5.2.0
volumeMounts:
- name: config-volume
mountPath: /home/rundeck/server/config/
subPath: realm.properties
env:
- name: RUNDECK_DATABASE_DRIVER
value: org.postgresql.Driver
- name: RUNDECK_DATABASE_USERNAME
value: rundeck
- name: RUNDECK_DATABASE_PASSWORD
value: rundeck
- name: RUNDECK_DATABASE_URL
value: jdbc:postgresql://postgres/rundeck?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
- name: RUNDECK_GRAILS_URL
value: http://localhost:4440
ports:
- containerPort: 4440
protocol: TCP
volumes:
- name: config-volume
configMap:
name: realm-properties-configmap
---
apiVersion: v1
kind: Service
metadata:
name: rundeck-service
spec:
selector:
app: rundeck
ports:
- protocol: TCP
port: 4440
targetPort: 4440
type: ClusterIP # Adjust the type as needed (e.g. ClusterIP)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres
env:
- name: POSTGRES_DB
value: rundeck
- name: POSTGRES_USER
value: rundeck
- name: POSTGRES_PASSWORD
value: rundeck
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi # Adjust storage size as needed
storageClassName: local-path
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
Which I applied
$ kubectl apply -n rundeck -f ./manifest.yaml
configmap/realm-properties-configmap created
deployment.apps/rundeck configured
service/rundeck-service unchanged
deployment.apps/postgres unchanged
persistentvolumeclaim/postgres-pvc unchanged
service/postgres unchanged
Unfortuantely it didn’t work
$ kubectl get pods -n rundeck
NAME READY STATUS RESTARTS AGE
rundeck-6df9fcbbd-vmfg2 1/1 Running 0 33m
postgres-6b984b548d-9rb85 1/1 Running 0 33m
rundeck-7c6c8995cc-wp82k 0/1 CrashLoopBackOff 4 (36s ago) 2m4s
This failed because it could not overwrite the file
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m24s default-scheduler Successfully assigned rundeck/rundeck-7c6c8995cc-wp82k to builder-hp-elitebook-745-g5
Normal Pulled 57s (x5 over 2m24s) kubelet Container image "rundeck/rundeck:5.2.0" already present on machine
Normal Created 57s (x5 over 2m24s) kubelet Created container rundeck
Warning Failed 57s (x5 over 2m24s) kubelet Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/var/lib/kubelet/pods/02c9840e-9880-4f72-aa1b-c7fe99ec1195/volume-subpaths/config-volume/rundeck/0" to rootfs at "/home/rundeck/server/config/": mount /var/lib/kubelet/pods/02c9840e-9880-4f72-aa1b-c7fe99ec1195/volume-subpaths/config-volume/rundeck/0:/home/rundeck/server/config/ (via /proc/self/fd/6), flags: 0x5001: not a directory: unknown
Warning BackOff 31s (x10 over 2m22s) kubelet Back-off restarting failed container rundeck in pod rundeck-7c6c8995cc-wp82k_rundeck(02c9840e-9880-4f72-aa1b-c7fe99ec1195)
Let’s switch to copying it over first
apiVersion: v1
kind: ConfigMap
metadata:
name: realm-properties-configmap
data:
realm.properties: |
#
# This file defines users passwords and roles for a HashUserRealm
#
# The format is
# <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed. The class
# org.mortbay.util.Password should be used to generate obfuscated
# passwords or password checksums
#
# This sets the temporary user accounts for the Rundeck app
#
admin:newpassword,user,admin
user:newpassword,user
#
# example users matching the example aclpolicy template roles
#
#project-admin:admin,user,project_admin
#job-runner:admin,user,job_runner
#job-writer:admin,user,job_writer
#job-reader:admin,user,job_reader
#job-viewer:admin,user,job_viewer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rundeck
spec:
replicas: 1
selector:
matchLabels:
app: rundeck
template:
metadata:
labels:
app: rundeck
spec:
containers:
- name: rundeck
image: rundeck/rundeck:5.2.0
volumeMounts:
- name: config-volume
mountPath: /mnt/
subPath: realm.properties
command:
- "/bin/sh"
- "-c"
- "cp -f /mnt/realm.properties /home/rundeck/server/config/realm.properties && java -XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -Dlog4j.configurationFile=/home/rundeck/server/config/log4j2.properties -Dlogging.config=file:/home/rundeck/server/config/log4j2.properties -Dloginmodule.conf.name=jaas-loginmodule.conf -Dloginmodule.name=rundeck -Drundeck.jaaslogin=true -Drundeck.jetty.connector.forwarded=false -jar rundeck.war"
env:
- name: RUNDECK_DATABASE_DRIVER
value: org.postgresql.Driver
- name: RUNDECK_DATABASE_USERNAME
value: rundeck
- name: RUNDECK_DATABASE_PASSWORD
value: rundeck
- name: RUNDECK_DATABASE_URL
value: jdbc:postgresql://postgres/rundeck?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
- name: RUNDECK_GRAILS_URL
value: http://localhost:4440
ports:
- containerPort: 4440
protocol: TCP
volumes:
- name: config-volume
configMap:
name: realm-properties-configmap
---
apiVersion: v1
kind: Service
metadata:
name: rundeck-service
spec:
selector:
app: rundeck
ports:
- protocol: TCP
port: 4440
targetPort: 4440
type: ClusterIP # Adjust the type as needed (e.g. ClusterIP)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres
env:
- name: POSTGRES_DB
value: rundeck
- name: POSTGRES_USER
value: rundeck
- name: POSTGRES_PASSWORD
value: rundeck
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi # Adjust storage size as needed
storageClassName: local-path
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
Let’s test that
$ kubectl apply -n rundeck -f ./manifest.yaml
configmap/realm-properties-configmap unchanged
deployment.apps/rundeck configured
service/rundeck-service unchanged
deployment.apps/postgres unchanged
persistentvolumeclaim/postgres-pvc unchanged
service/postgres unchanged
That too vommitted
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 34s default-scheduler Successfully assigned rundeck/rundeck-87cbcb44c-4ps2b to builder-hp-elitebook-745-g5
Normal Pulled 19s (x3 over 34s) kubelet Container image "rundeck/rundeck:5.2.0" already present on machine
Normal Created 19s (x3 over 34s) kubelet Created container rundeck
Warning Failed 19s (x3 over 34s) kubelet Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/var/lib/kubelet/pods/9b0b0143-4593-4feb-851c-fa3ba24e69eb/volume-subpaths/config-volume/rundeck/0" to rootfs at "/mnt/": mount /var/lib/kubelet/pods/9b0b0143-4593-4feb-851c-fa3ba24e69eb/volume-subpaths/config-volume/rundeck/0:/mnt/ (via /proc/self/fd/6), flags: 0x5001: not a directory: unknown
Warning BackOff 8s (x4 over 33s) kubelet Back-off restarting failed container rundeck in pod rundeck-87cbcb44c-4ps2b_rundeck(9b0b0143-4593-4feb-851c-fa3ba24e69eb)
However, this last form seemed to jive with my cluster (using subpaths and item keys):
$ cat manifest.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: realm-properties-configmap
data:
realm.properties: |
#
# This file defines users passwords and roles for a HashUserRealm
#
# The format is
# <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed. The class
# org.mortbay.util.Password should be used to generate obfuscated
# passwords or password checksums
#
# This sets the temporary user accounts for the Rundeck app
#
admin:newpassword,user,admin
user:newpassword,user
#
# example users matching the example aclpolicy template roles
#
#project-admin:admin,user,project_admin
#job-runner:admin,user,job_runner
#job-writer:admin,user,job_writer
#job-reader:admin,user,job_reader
#job-viewer:admin,user,job_viewer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rundeck
spec:
replicas: 1
selector:
matchLabels:
app: rundeck
template:
metadata:
labels:
app: rundeck
spec:
containers:
- name: rundeck
image: rundeck/rundeck:5.2.0
volumeMounts:
- name: config-volume
mountPath: /mnt/realm.properties
subPath: realm.properties
command:
- "/bin/sh"
- "-c"
- "cp -f /mnt/realm.properties /home/rundeck/server/config/realm.properties && java -XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -Dlog4j.configurationFile=/home/rundeck/server/config/log4j2.properties -Dlogging.config=file:/home/rundeck/server/config/log4j2.properties -Dloginmodule.conf.name=jaas-loginmodule.conf -Dloginmodule.name=rundeck -Drundeck.jaaslogin=true -Drundeck.jetty.connector.forwarded=false -jar rundeck.war"
env:
- name: RUNDECK_DATABASE_DRIVER
value: org.postgresql.Driver
- name: RUNDECK_DATABASE_USERNAME
value: rundeck
- name: RUNDECK_DATABASE_PASSWORD
value: rundeck
- name: RUNDECK_DATABASE_URL
value: jdbc:postgresql://postgres/rundeck?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
- name: RUNDECK_GRAILS_URL
value: http://localhost:4440
ports:
- containerPort: 4440
protocol: TCP
volumes:
- name: config-volume
configMap:
name: realm-properties-configmap
items:
- key: realm.properties
path: realm.properties
---
apiVersion: v1
kind: Service
metadata:
name: rundeck-service
spec:
selector:
app: rundeck
ports:
- protocol: TCP
port: 4440
targetPort: 4440
type: ClusterIP # Adjust the type as needed (e.g. ClusterIP)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres
env:
- name: POSTGRES_DB
value: rundeck
- name: POSTGRES_USER
value: rundeck
- name: POSTGRES_PASSWORD
value: rundeck
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi # Adjust storage size as needed
storageClassName: local-path
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
On issue that is bothering me is that without a readiness probe or health check we show a “live” container well before it’s actually started.
Once the logs show
Grails application running at http://0.0.0.0:4440/ in environment: production
Then I know its good. So let’s fix that too. I also pivotted back to just a direct mount of the properties file
$ cat manifest.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: realm-properties-configmap
data:
realm.properties: |
#
# This file defines users passwords and roles for a HashUserRealm
#
# The format is
# <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed. The class
# org.mortbay.util.Password should be used to generate obfuscated
# passwords or password checksums
#
# This sets the temporary user accounts for the Rundeck app
#
admin:newpassword,user,admin
user:newpassword,user
#
# example users matching the example aclpolicy template roles
#
#project-admin:admin,user,project_admin
#job-runner:admin,user,job_runner
#job-writer:admin,user,job_writer
#job-reader:admin,user,job_reader
#job-viewer:admin,user,job_viewer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rundeck
spec:
replicas: 1
selector:
matchLabels:
app: rundeck
template:
metadata:
labels:
app: rundeck
spec:
containers:
- name: rundeck
image: rundeck/rundeck:5.2.0
volumeMounts:
- name: config-volume
mountPath: /home/rundeck/server/config/realm.properties
subPath: realm.properties
env:
- name: RUNDECK_DATABASE_DRIVER
value: org.postgresql.Driver
- name: RUNDECK_DATABASE_USERNAME
value: rundeck
- name: RUNDECK_DATABASE_PASSWORD
value: rundeck
- name: RUNDECK_DATABASE_URL
value: jdbc:postgresql://postgres/rundeck?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
- name: RUNDECK_GRAILS_URL
value: http://localhost:4440
ports:
- containerPort: 4440
protocol: TCP
readinessProbe:
httpGet:
path: /health
port: 4440
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 4440
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 5
volumes:
- name: config-volume
configMap:
name: realm-properties-configmap
items:
- key: realm.properties
path: realm.properties
---
apiVersion: v1
kind: Service
metadata:
name: rundeck-service
spec:
selector:
app: rundeck
ports:
- protocol: TCP
port: 4440
targetPort: 4440
type: ClusterIP # Adjust the type as needed (e.g. ClusterIP)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres
env:
- name: POSTGRES_DB
value: rundeck
- name: POSTGRES_USER
value: rundeck
- name: POSTGRES_PASSWORD
value: rundeck
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi # Adjust storage size as needed
storageClassName: local-path
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
applied
$ kubectl apply -n rundeck -f ./manifest.yaml
configmap/realm-properties-configmap unchanged
deployment.apps/rundeck configured
service/rundeck-service unchanged
deployment.apps/postgres unchanged
persistentvolumeclaim/postgres-pvc unchanged
service/postgres unchanged
Ingress
Since I already used ‘rundeck’ with my AWS/Route53 address so I’ll use Azure for this instance.
That means I first need an A Record
$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n rundeck
{
"ARecords": [
{
"ipv4Address": "75.73.224.240"
}
],
"TTL": 3600,
"etag": "96b46f0a-7b1c-4380-95df-5668b851ae61",
"fqdn": "rundeck.tpk.pw.",
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/rundeck",
"name": "rundeck",
"provisioningState": "Succeeded",
"resourceGroup": "idjdnsrg",
"targetResource": {},
"type": "Microsoft.Network/dnszones/A"
}
Next I can create and apply an Ingress YAML to use it
$ cat rundeck.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: azuredns-tpkpw
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.org/websocket-services: rundeck-service
name: rundeck-ingress
spec:
rules:
- host: rundeck.tpk.pw
http:
paths:
- backend:
service:
name: rundeck-service
port:
number: 4440
path: /
pathType: Prefix
tls:
- hosts:
- rundeck.tpk.pw
secretName: rundeck-tls
$ kubectl apply -f ./rundeck.ingress.yaml -n rundeck
ingress.networking.k8s.io/rundeck-ingress created
When I saw the cert was good
$ kubectl get cert -n rundeck
NAME READY SECRET AGE
rundeck-tls True rundeck-tls 3m33s
While it works, I still seem to a get a bit of delay redirecting after login to https://rundeck.tpk.pw/menu/home.
Usage
My first step is to create a project
Next I’ll create a job
I’ll give it a name
In the Workflow section, I’ll do a basic test pwd
command
Once saved, I can see it in the All Jobs list
I can click “Run Job Now”
Which I see it succeeded
I can see it ran on the main Node
I find it still seems to hang on output in the page, but I can view it with the raw output
Next, I’ll create a webhook
I decided to give it the admin role
For some reason, yet again the UI doesn’t work for the Web URL, but does with a port-forward
I’ll pick a test job
And I can see URL after I save
I can then test with curl
$ curl -X POST https://rundeck.tpk.pw/api/47/webhook/pee2vJEp2I2OPY22wzAZy0is0SaRKWwO#testhook
{"jobId":"ce88f1bc-77cf-4bf7-aabe-576f4eaa5555","executionId":"17"}
I realized i had left the default URL in the deployment section of Rundeck
spec:
containers:
- name: rundeck
image: rundeck/rundeck:5.2.0
volumeMounts:
- name: config-volume
mountPath: /home/rundeck/server/config/realm.properties
subPath: realm.properties
env:
- name: RUNDECK_DATABASE_DRIVER
value: org.postgresql.Driver
- name: RUNDECK_DATABASE_USERNAME
value: rundeck
- name: RUNDECK_DATABASE_PASSWORD
value: rundeck
- name: RUNDECK_DATABASE_URL
value: jdbc:postgresql://postgres/rundeck?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
- name: RUNDECK_GRAILS_URL
value: http://localhost:4440
I changed that last one to
- name: RUNDECK_GRAILS_URL
value: https://rundeck.tpk.pw
However, a consequence of rotating pods is the output logs get lost
However, the webhooks and URLs persist
$ curl -X POST https://rundeck.tpk.pw/api/47/webhook/pee2vJEp2I2OPY22wzAZy0is0SaRKWwO#testhook
{"jobId":"ce88f1bc-77cf-4bf7-aabe-576f4eaa5555","executionId":"21"}
This time I had no troubles getting output
Nodes
Let’s add a node. First, we need to add an SSH key that will be used to connect to the Node.
We can do this in the Project Settings under Key storage
Click “+ Add or Upload a Key”
I can enter the Private Key and set the name
We can see it now saved
Next, we’ll choose to Edit Nodes
Where we can then add a Node source
I’ll now add a file node
I’ll chose YAML format and to include localhost
I then need to save it
Which is confirmed
Now, even though I did chose to create it, I found it failed to do so.
So I touched a file on the pod and gave write permissions
builder@DESKTOP-QADGF36:~/Workspaces/rundeck$ kubectl get pods -n rundeck
NAME READY STATUS RESTARTS AGE
postgres-6b984b548d-9rb85 1/1 Running 0 7h21m
rundeck-7cb68459bf-vssv8 1/1 Running 0 117m
builder@DESKTOP-QADGF36:~/Workspaces/rundeck$ kubectl exec -it rundeck-7cb68459bf-vssv8 -n rundeck -- /bin/bash
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
rundeck@rundeck-7cb68459bf-vssv8:~$ pwd
/home/rundeck
rundeck@rundeck-7cb68459bf-vssv8:~$ touch resourceyaml
rundeck@rundeck-7cb68459bf-vssv8:~$ chmod 777 ./resourceyaml
rundeck@rundeck-7cb68459bf-vssv8:~$
Now when I clicked Edit under Nodes, I can see some basic details
I clicked save. If I cat it on the pods, I can see its contents now
rundeck@rundeck-7cb68459bf-vssv8:~$ cat resourceyaml
rundeck-7cb68459bf-vssv8:
nodename: rundeck-7cb68459bf-vssv8
hostname: rundeck-7cb68459bf-vssv8
osVersion: 5.15.0-97-generic
osFamily: unix
osArch: amd64
description: Rundeck server node
osName: Linux
username: rundeck
tags: ''
Next, I’ll add a node to the list I can use
To test, I’ll create a new Job and use ‘mac.*’ in my filter. We can see ‘mac81’ was matched
The problem is I’ll really need to fix auth before we use it.
Let’s first get the path to that private key we created
We can now edit the resourceyaml and add the line
ssh-key-storage-path: keys/project/Freshbrewed/DESKTOP-QADGF36
Which we can see below at lien 10
On this new test job, I’ll use the command ‘whoami’ to show we should be “builder” on that host
I saved and ran and we can see it ran the job on mac81
Expanding the log output shows, indeed, we are running as “builder” on that host
Summary
In this first part we setup Rundeck in Kubernetes. Since there is no official chart (of which I’m aware), we built out our own solving some configmaps and user/password issues. Once running, we sorted out TLS and Ingress before diving in to setup. In our first test, we ran a test job against the local executor.
Our goal today was getting Rundeck to use external nodes as we’ll need that for some ansible work coming next. We figured out SSH passwords and then how to use the “File” approach to add nodes. Lastly, we proved it work by running a “whoami” on a destination host.
In our next post, we’ll address how to use Commands to run arbitrary steps on hosts, how the SCM connectivity works (leveraging GIT with Forgejo). Imports of the XML files stored in GIT (using an alternate host) and Ansible two ways.