This commit is contained in:
2025-08-26 17:32:31 -05:00
parent ad65e6f674
commit f1c4accf52
11 changed files with 365 additions and 96 deletions

View File

@@ -9,6 +9,8 @@ I'll try to include any pertinent documentation here in the tooling I use or the
#### Upcoming
Initially we'll try to migrate our services from a docker compose and into a reproducible and controlled deployment scheme here. I'll also likely break this into its own repo and instead reference it as a submodule in our dotfiles (because it makes far more sense that way).
Since hcloud keeps (seriously, several times) making me wait for verification I've opted to go ahead and rewrite it into Clojurescript.
#### Goals
The long term goal is for this to be a mostly uninteractive, to completion set up of my cloud services. Since it'll be IaC should I ever choose down the road to migrate certain ones to local nodes I run then that effort should also be more or less feasible.

View File

@@ -2,6 +2,7 @@
"name": "vultr-k8s",
"main": "index.js",
"scripts": {
"test": "npx shadow-cljs watch app",
"build": "shadow-cljs release app",
"pulumi": "pulumi up",
"deploy": "npm run build && npm run pulumi"

View File

@@ -1,7 +1,6 @@
;; shadow-cljs.edn
{:source-paths ["src/main"]
:dependencies []
:builds
{:app {:target :node-script
:output-to "index.js"
:main infra.core/main!}}}
:main core/main!}}}

31
iac/src/js/index.js Normal file
View File

@@ -0,0 +1,31 @@
const k8s = require("@pulumi/kubernetes");
const core = require("./core");
const vault = require("./k8/openbao/openbao");
const nextcloud = require("./k8/nextcloud/nextcloud");
const hetznercsi = require('./k8/csi-drivers/hetzner');
async function main() {
const cluster = core.createCluster();
const appOutputs = cluster.kubeconfig.apply(async (kc) => {
const provider = new k8s.Provider("k8s-dynamic-provider", {
kubeconfig: kc,
});
hetznercsi.deployCsiDriver(provider);
vault.deployVault(provider);
const app = await nextcloud.deployNextcloudApp(kc, provider);
return {
nextcloudUrl: app.nextcloudUrl,
};
});
return {
masterIp: cluster.masterIp,
kubeconfig: cluster.kubeconfig,
nextcloudUrl: appOutputs.nextcloudUrl,
};
}
module.exports = main();

View File

@@ -10,7 +10,7 @@ const yaml = require("js-yaml");
* @param {string} kubeconfig - The kubeconfig content for the cluster.
* @param {k8s.Provider} provider - The Kubernetes provider to deploy resources with.
*/
exports.deployNextcloudApp = async function(kubeconfig, provider) {
exports.deployNextcloudApp = async function(provider) {
const vaultConfig = new pulumi.Config("vault");
const vaultAddress = vaultConfig.require("address");

44
iac/src/main/core.cljs Normal file
View File

@@ -0,0 +1,44 @@
(ns core
(:require
["@pulumi/kubernetes" :as k8s]
[clojure.core.async :refer [go <!]]
[clojure.core.async.interop :refer [<p!]]
[infra.init :as init]
[k8s.csi-driver.hetzner :as hetznercsi]
[k8s.services.openbao.openbao :as vault]
[k8s.services.nextcloud.nextcloud :as nextcloud]
))
(defn app-deployments [provider]
(let [
nextcloud-result (nextcloud/deploy-nextcloud-app provider)
vault-result (vault/deploy-vault provider)
]
{
:nextcloud nextcloud-result
:vault vault-result
}
))
(defn main! []
(let [cluster (init/create-cluster)
app-outputs (.apply (get cluster :kubeconfig)
(fn [kc]
(js/Promise.
(fn [resolve _reject]
(let [provider (k8s/Provider. "k8s-dynamic-provider" #js {:kubeconfig kc})]
(hetznercsi/deploy-csi-driver provider)
(resolve (app-deployments provider)))))))]
(set! (.-exports js/module)
#js {
:kubeconfig (get cluster :kubeconfig)
:masterIp (get cluster :masterIp)
:nextcloudUrl (.apply app-outputs #(get app-outputs :nextcloudUrl))})
#_(set! (.-exports js/module)
#js {:nextcloudUrl (.apply app-outputs (fn [outputs] (.-nextcloudUrl outputs)))})
))

View File

@@ -1,93 +0,0 @@
(ns infra.core
(:require ["@pulumi/pulumi" :as pulumi]
["@pulumi/hcloud" :as hcloud]
["@pulumi/command/remote" :as command]
["@pulumi/kubernetes" :as k8s]))
(def config (pulumi/Config.))
(def ssh-key-name (.require config "sshKeyName"))
(def private-key (.requireSecret config "privateKeySsh"))
(defn install-master-script [public-ip]
(str "if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\"--flannel-backend=wireguard-native --node-external-ip="
public-ip
"\" sh -\n"
"fi"))
(defn install-worker-script [master-ip token]
(pulumi/interpolate
(str "if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | K3S_URL=https://"
master-ip
":6443 K3S_TOKEN=\"" token "\" sh -\n"
"fi")))
(defn hcloud-server [name server-type location ssh-key & {:keys [user-data]}]
(hcloud/Server. name
#js {:serverType server-type
:image "ubuntu-22.04"
:location location
:sshKeys #js [ssh-key]
:userData user-data}))
(defn ssh-connection [ip]
#js {:host ip
:user "root"
:privateKey private-key})
(defn main! []
(let [
master (hcloud-server "k3s-master-de" "cx22" "fsn1" ssh-key-name)
master-conn (.apply (.-ipv4Address master) ssh-connection)
install-master (command/Command. "install-master"
#js {:connection master-conn
:create (.apply (.-ipv4Address master)
install-master-script)})
token-cmd (-> (.-stdout install-master)
(.apply (fn [_]
(command/Command. "get-token"
#js {:connection master-conn
:create "cat /var/lib/rancher/k3s/server/node-token"}))))
token-stdout (-> token-cmd (.apply (fn [cmd] (.-stdout cmd))))
worker-script (install-worker-script (.-ipv4Address master) token-stdout)
worker-de (hcloud-server "k3s-worker-de" "cx22" "fsn1" ssh-key-name :user-data worker-script)
worker-us (hcloud-server "k3s-worker-us" "cpx11" "ash" ssh-key-name :user-data worker-script)
kubeconfig-cmd (-> (pulumi/all #js [(.-stdout install-master) (.-ipv4Address master)])
(.apply (fn [[_ master-ip]]
(command/Command. "get-kubeconfig"
#js {:connection master-conn
:create (str "sleep 10 &&" "sed 's/127.0.0.1/" master-ip "/' /etc/rancher/k3s/k3s.yaml")}))))
kubeconfig-stdout (-> kubeconfig-cmd (.apply (fn [cmd] (.-stdout cmd))))
all-workers-ready (pulumi/all #js [(.-urn worker-de) (.-urn worker-us)])
ready-kubeconfig (pulumi/all #js [kubeconfig-stdout all-workers-ready]
(fn [[kc _]] kc))
k8s-provider (k8s/Provider. "k8s-provider"
#js {:kubeconfig ready-kubeconfig})]
(-> (pulumi/all #js [(.-ipv4Address master)
(.-ipv4Address worker-de)
(.-ipv4Address worker-us)
kubeconfig-stdout])
(.apply (fn [[master-ip worker-de-ip worker-us-ip kc]]
(js-obj
"masterIp" master-ip
"workerDeIp" worker-de-ip
"workerUsIp" worker-us-ip
"kubeconfig" (pulumi/secret kc)))))))
(set! (.-main js/module) main!)

View File

@@ -0,0 +1,145 @@
(ns infra.init
(:require ["@pulumi/pulumi" :as pulumi]
["@pulumi/hcloud" :as hcloud]
["@pulumi/command/remote" :as remote]
["@pulumi/command/local" :as local]
["@pulumi/kubernetes" :as k8s]
["fs" :as fs]))
(defn- install-master-script [public-ip]
(str "# Create manifests dir\n"
"mkdir -p /var/lib/rancher/k3s/server/manifests\n\n"
"# Traefik NodePort config\n"
"cat <<EOF > /var/lib/rancher/k3s/server/manifests/traefik-config.yaml\n"
"apiVersion: helm.cattle.io/v1\n"
"kind: HelmChartConfig\n"
"metadata:\n"
" name: traefik\n"
" namespace: kube-system\n"
"spec:\n"
" valuesContent: |-\n"
" service:\n"
" spec:\n"
" type: NodePort\n"
" ports:\n"
" web:\n"
" nodePort: 30080\n"
" websecure:\n"
" nodePort: 30443\n"
"EOF\n\n"
"# Install k3s if not present\n"
"if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\"--flannel-backend=wireguard-native --node-external-ip=" public-ip "\" sh -\n"
"fi\n\n"
"# Wait for node readiness\n"
"until sudo k3s kubectl get node >/dev/null 2>&1; do\n"
" echo 'Waiting for master node...'\n"
" sleep 5\n"
"done\n"))
(defn- install-worker-script [master-ip token]
(str "#!/bin/bash\n"
"exec > /root/k3s-install.log 2>&1\n"
"set -x\n"
"echo '--- Starting worker install ---'\n\n"
"until ping -c1 " master-ip "; do\n"
" echo 'Waiting for master...'\n"
" sleep 2\n"
"done\n\n"
"WORKER_PUBLIC_IP=$(curl -s https://ifconfig.me/ip)\n"
"echo \"Public IP: $WORKER_PUBLIC_IP\"\n\n"
"if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | "
"K3S_URL=https://" master-ip ":6443 "
"K3S_TOKEN=\"" token "\" "
"INSTALL_K3S_EXEC=\"--node-external-ip=$WORKER_PUBLIC_IP\" sh -\n"
"fi\n\n"
"echo '--- Finished worker install ---'\n"))
(defn create-cluster []
(let [cfg (pulumi/Config.)
ssh-key (.require cfg "sshKeyName")
priv-key (.requireSecret cfg "privateKeySsh")
firewall (hcloud/Firewall.
"k3s-firewall"
(clj->js {:rules [{:direction "in" :protocol "tcp" :port "22" :sourceIps ["0.0.0.0/0" "::/0"]}
{:direction "in" :protocol "tcp" :port "6443" :sourceIps ["0.0.0.0/0" "::/0"]}
{:direction "in" :protocol "udp" :port "51820" :sourceIps ["0.0.0.0/0" "::/0"]}
{:direction "in" :protocol "icmp" :sourceIps ["0.0.0.0/0" "::/0"]}]}))
master (hcloud/Server.
"k3s-master-de"
(clj->js {:serverType "cx22"
:image "ubuntu-22.04"
:location "fsn1"
:sshKeys [ssh-key]
:firewallIds [(.-id firewall)]}))
master-ip (.-ipv4Address master)
master-conn (clj->js {:host master-ip
:user "root"
:privateKey priv-key})
install-master
(remote/Command.
"install-master"
(clj->js {:connection master-conn
:create (.apply master-ip install-master-script)})
(clj->js {:dependsOn [master]}))
token-cmd
(remote/Command.
"get-token"
(clj->js {:connection master-conn
:create "sudo cat /var/lib/rancher/k3s/server/node-token"})
(clj->js {:dependsOn [install-master]}))
worker-script
(.apply (pulumi/all [master-ip (.-stdout token-cmd)])
(fn [[ip token]] (install-worker-script ip (.trim token))))
worker-de (hcloud/Server.
"k3s-worker-de"
(clj->js {:serverType "cx22"
:image "ubuntu-22.04"
:location "fsn1"
:sshKeys [ssh-key]
:userData worker-script
:firewallIds [(.-id firewall)]}))
worker-us (hcloud/Server.
"k3s-worker-us"
(clj->js {:serverType "cpx11"
:image "ubuntu-22.04"
:location "ash"
:sshKeys [ssh-key]
:userData worker-script
:firewallIds [(.-id firewall)]}))
kubeconfig-cmd
(remote/Command.
"get-kubeconfig"
(clj->js {:connection master-conn
:create (.apply master-ip
(fn [ip]
(str "sudo sed 's/127.0.0.1/" ip "/' /etc/rancher/k3s/k3s.yaml")))})
(clj->js {:dependsOn [install-master]}))
label-node
(local/Command.
"label-german-node"
(clj->js {:create (.apply (pulumi/all [(.-stdout kubeconfig-cmd) (.-name worker-de)])
(fn [[kubeconfig worker-name]]
(let [path "./kubeconfig.yaml"]
(.writeFileSync fs path kubeconfig)
(str "kubectl --kubeconfig=" path
" label node " worker-name
" location=de --overwrite"))))})
(clj->js {:dependsOn [kubeconfig-cmd]}))]
{:masterIp master-ip
:workerDeIp (.-ipv4Address worker-de)
:workerUsIp (.-ipv4Address worker-us)
:kubeconfig (pulumi/secret (.-stdout kubeconfig-cmd))}))

View File

@@ -0,0 +1,32 @@
(ns k8s.csi-driver.hetzner
(:require ["@pulumi/pulumi" :as pulumi]
["@pulumi/kubernetes" :as k8s]))
(defn deploy-csi-driver [provider]
(let [hcloud-config (pulumi/Config. "hcloud")
hcloud-token (.requireSecret hcloud-config "token")
core-v1 (.. k8s -core -v1)
helm-v3 (.. k8s -helm -v3)
csi-secret (core-v1.Secret.
"hcloud-csi-secret"
(clj->js {:metadata {:name "hcloud"
:namespace "kube-system"}
:stringData {:token hcloud-token}})
#js {:provider provider})
secret-name (-> csi-secret .-metadata .-name)
csi-chart (helm-v3.Chart.
"hcloud-csi"
(clj->js {:chart "hcloud-csi"
:fetchOpts {:repo "https://charts.hetzner.cloud"}
:namespace "kube-system"
:values {:controller
{:secret {:enabled false}
:existingSecret {:name secret-name}}
:node
{:existingSecret {:name secret-name}}}})
(clj->js {:provider provider
:dependsOn [csi-secret]}))]
csi-chart))

View File

@@ -0,0 +1,74 @@
(ns k8s.services.nextcloud.nextcloud
(:require
["@pulumi/kubernetes" :as k8s]
["@pulumi/pulumi" :as pulumi]
["@pulumi/vault" :as vault]
["fs" :as fs]
["js-yaml" :as yaml]
["path" :as path]
[clojure.core.async :refer [go]]))
(defn- get-secret-val
"Extract a specific key from a Vault secret Output/Promise."
[secret-promise key]
(.then secret-promise #(aget (.-data %) key)))
(defn deploy-nextcloud-app
"Deploy Nextcloud using Vaultmanaged secrets and a Helm chart."
[provider]
(let [core-v1 (.. k8s -core -v1)
helm-v3 (.. k8s -helm -v3)
vault-cfg (pulumi/Config. "vault")
vault-provider (vault/Provider.
"vault-provider"
(clj->js {:address (.require vault-cfg "address")
:token (.requireSecret vault-cfg "token")}))
nextcloud-secrets (.getSecret (.-generic vault)
(clj->js {:path "secret/nextcloud"})
(clj->js {:provider vault-provider}))
ns ((.. core-v1 -Namespace)
"nextcloud-ns"
(clj->js {:metadata {:name "nextcloud"}})
(clj->js {:provider provider}))
admin-secret ((.. core-v1 -Secret)
"nextcloud-admin-secret"
(clj->js {:metadata {:name "nextcloud-admin-secret"
:namespace (.. ns -metadata -name)}
:stringData {:password (get-secret-val nextcloud-secrets "adminPassword")}})
(clj->js {:provider provider}))
db-secret ((.. core-v1 -Secret)
"nextcloud-db-secret"
(clj->js {:metadata {:name "nextcloud-db-secret"
:namespace (.. ns -metadata -name)}
:stringData {"mariadb-root-password" (get-secret-val nextcloud-secrets "dbPassword")
"mariadb-password" (get-secret-val nextcloud-secrets "dbPassword")}})
(clj->js {:provider provider}))
values-path (.join path js/__dirname "values.yaml")
helm-values (-> values-path
(fs/readFileSync "utf8")
(yaml/load))
_ (aset (aget (aget (aget helm-values "ingress") "hosts") 0)
"host"
(get-secret-val nextcloud-secrets "host"))
chart ((.. helm-v3 -Chart)
"my-nextcloud"
(clj->js {:chart "nextcloud"
:fetchOpts {:repo "https://nextcloud.github.io/helm/"}
:namespace (.. ns -metadata -name)
:values helm-values})
(clj->js {:provider provider
:dependsOn [admin-secret db-secret]}))]
{:namespace ns
:admin-secret admin-secret
:db-secret db-secret
:chart chart
:nextcloud-url (.then nextcloud-secrets
#(str "https://" (aget (.-data %) "host")))}))

View File

@@ -0,0 +1,34 @@
(ns k8s.services.openbao.openbao
(:require
["@pulumi/kubernetes" :as k8s]
["fs" :as fs]
["js-yaml" :as yaml]
["path" :as path]
[clojure.core.async :refer [go]]))
(defn deploy-vault
"Deploy OpenBao via Helm chart on the given Kubernetes provider."
[provider]
(let [core-v1 (.. k8s -core -v1)
helm-v3 (.. k8s -helm -v3)
vault-ns ((.. core-v1 -Namespace)
"vault-ns"
(clj->js {:metadata {:name "vault"}})
(clj->js {:provider provider}))
values-path (.join path js/__dirname "values.yaml")
helm-values (-> values-path
(fs/readFileSync "utf8")
(yaml/load))
chart ((.. helm-v3 -Chart)
"openbao"
(clj->js {:chart "openbao"
:fetchOpts {:repo "https://openbao.github.io/openbao-helm"}
:namespace (.. vault-ns -metadata -name)
:values helm-values})
(clj->js {:provider provider
:dependsOn [vault-ns]}))]
{:namespace vault-ns
:chart chart}))