dump
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
31
iac/src/js/index.js
Normal 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();
|
||||
@@ -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
44
iac/src/main/core.cljs
Normal 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)))})
|
||||
))
|
||||
@@ -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!)
|
||||
145
iac/src/main/infra/init.cljs
Normal file
145
iac/src/main/infra/init.cljs
Normal 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))}))
|
||||
32
iac/src/main/k8s/csi_driver/hetzner.cljs
Normal file
32
iac/src/main/k8s/csi_driver/hetzner.cljs
Normal 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))
|
||||
74
iac/src/main/k8s/services/nextcloud/nextcloud.cljs
Normal file
74
iac/src/main/k8s/services/nextcloud/nextcloud.cljs
Normal 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 Vault‑managed 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")))}))
|
||||
34
iac/src/main/k8s/services/openbao/openbao.cljs
Normal file
34
iac/src/main/k8s/services/openbao/openbao.cljs
Normal 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}))
|
||||
Reference in New Issue
Block a user