diff --git a/project.clj b/project.clj index b570636..056f52e 100644 --- a/project.clj +++ b/project.clj @@ -3,9 +3,11 @@ :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.8.0"] + :dependencies [[org.clojure/clojure "1.10.0"] [org.clojure/data.codec "0.1.1"] [nano-id "0.9.3"] + [cli-matic "0.3.8"] + [io.forward/yaml "1.0.9"] [brosenan/lambdakube "0.7.0"]] :plugins [[lein-auto "0.1.3"]] :auto {:default {:file-pattern #"\.(clj|cljs|cljx|cljc|edn)$"}} diff --git a/src/kube_deploy/core.clj b/src/kube_deploy/core.clj index df8f3cd..ce0b79d 100644 --- a/src/kube_deploy/core.clj +++ b/src/kube_deploy/core.clj @@ -1,12 +1,19 @@ (ns kube-deploy.core - (:require [lambdakube.core :as lk] - [kube-deploy.helpers :as lkh] - [lambdakube.util :as lku] + (:require [clojure.java.io :as io] [clojure.java.shell :as sh] - [clojure.java.io :as io])) + [cli-matic.core :refer [run-cmd]] + [yaml.core :as yaml] + [kube-deploy.from-yaml :as lky] + [kube-deploy.data :as data] + [kube-deploy.helpers :as lkh] + [lambdakube.core :as lk] + [lambdakube.util :as lku])) (def conf (atom {})) -(def config {:num-be-slaves 3 :num-fe-replicas 3}) +(def services (atom {})) +(reset! services (clojure.edn/read-string (slurp "config.edn"))) + +(def config {:num-be-slaves 2 :num-fe-replicas 2}) (def resources-mini {:requests @@ -22,19 +29,19 @@ (def resources-medium {:requests - {:cpu "200m" :memory "400Mi"} + {:cpu "100m" :memory "500Mi"} :limits - {:cpu "500m" :memory "1Gi"}}) + {:cpu "200m" :memory "1Gi"}}) ; will not be needed soon -(defn kube-apply-namespace +(defn kube-apply ([content file] - (kube-apply-namespace content file nil nil)) - ([content file namespace] - (kube-apply-namespace content namespace nil)) + (kube-apply content file nil nil)) + ([content file kube-namespace] + (kube-apply content kube-namespace nil)) ([content file kube-namespace kube-config] - (let [namespace-param (if (nil? namespace) nil (str "--namespace=" kube-namespace)) - config-param (if (nil? config) nil (str "--kubeconfig=" kube-config))] + (let [namespace-param (if (nil? kube-namespace) nil (str "--namespace=" kube-namespace)) + config-param (if (nil? kube-config) nil (str "--kubeconfig=" kube-config))] (when-not (and (.exists file) (= (slurp file) content)) (spit file content) @@ -47,9 +54,10 @@ (-> kube-injector (lk/rule :redis-master-rule [] (fn [] - (-> (lk/pod :redis-master {:app :redis + (-> (lk/pod :redis-master {:app :footprint + :env :production :role :master - :tier :backend}) + :tier :redis}) (lk/add-container :redis (get-in @conf [:images :redis]) (-> {} (lkh/add-to-spec :resources resources-small) @@ -59,22 +67,22 @@ (lk/expose-cluster-ip :redis-master (lk/port :redis :redis 6379 6379))))) - (lk/rule :redis-slave-rule [:redis-master-rule :num-be-slaves] - (fn [backend-master num-be-slaves] - (-> (lk/pod :redis-slave {:app :redis - :role :slave - :tier :backend}) - (lk/add-container :redis (get-in @conf [:images :redis]) - (-> {} - (lkh/add-to-spec :resources resources-small) - (lkh/add-to-spec - :args ["--slaveof" - (:hostname backend-master) - (-> backend-master :ports :redis str)]) - (lkh/add-liveness-probe-cmd ["redis-cli" "--version"]))) - (lk/deployment num-be-slaves) - (lk/expose-cluster-ip :redis-slave - (lk/port :redis :redis 6379 6379))))))) + #_(lk/rule :redis-slave-rule [:redis-master-rule :num-be-slaves] + (fn [backend-master num-be-slaves] + (-> (lk/pod :redis-slave {:app :redis + :role :slave + :tier :backend}) + (lk/add-container :redis (get-in @conf [:images :redis]) + (-> {} + (lkh/add-to-spec :resources resources-small) + (lkh/add-to-spec + :args ["--slaveof" + (:hostname backend-master) + (-> backend-master :ports :redis str)]) + (lkh/add-liveness-probe-cmd ["redis-cli" "--version"]))) + (lk/deployment num-be-slaves) + (lk/expose-cluster-ip :redis-slave + (lk/port :redis :redis 6379 6379))))))) (defn database [kube-injector] (-> kube-injector @@ -82,15 +90,19 @@ (fn [] (-> (lk/pod :postgres-master - {:app :postgres :role :master :tier :backend}) + {:app :footprint :role :master :env :production :tier :database}) (lk/add-container :postgres (get-in @conf [:images :postgres]) (-> {} (lkh/add-to-spec :resources resources-small) (lkh/add-readiness-probe-cmd ["pg_isready"]) (lkh/add-liveness-probe-cmd ["pg_isready"]) (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]))) + (lk/stateful-set 1) - (lk/deployment 1) + (lk/add-volume-claim-template :postgres-volume + {:accessModes ["ReadWriteOnce"] + :resources {:requests {:storage "1Gi"}}} + {:bar "/var/lib/postgresql/data"}) (lk/expose-cluster-ip :postgres-master (lk/port :postgres :postgres 5432 5432))))))) @@ -100,8 +112,7 @@ (fn [] (-> (lk/pod :rabbit-master - {:app :rabbit :role :master :tier :backend}) - + {:app :footprint :role :master :env :prodction :tier :rabbit}) (lk/add-container :rabbit (get-in @conf [:images :rabbitmq]) @@ -119,7 +130,7 @@ (fn [postgres redis] (-> (lk/pod :hackspace-master {:app :hackspace :role :master - :tier :backend} + :tier :web} {:imagePullSecrets [{:name "docker-registry"}]}) ; Add containers to the pod, one for frontend assets and one for backend @@ -128,26 +139,28 @@ (get-in @conf [:images :django]) (-> {} (lkh/add-to-spec :resources resources-medium) - (lkh/add-to-spec :command ["/gunicorn.sh"]) + ;(lkh/add-to-spec :command ["python" "/app/wsgi_bjoern.py"]) + ;(lkh/add-to-spec :command ["/gunicorn.sh"]) + (lkh/add-to-spec :command ["/usr/local/bin/gunicorn" "config.wsgi" "--workers" "2" "-k" "gevent" "--worker-connections" "100" "--max-requests" "300" "--keep-alive" "1000" "-b" "0.0.0.0:5000" "--chdir=/app"]) (lkh/add-to-spec :ports [{:containerPort 5000 :name "gunicorn-port"}]) (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]) - (lkh/add-readiness-probe-cmd ["wget" "127.0.0.1:5000"]) - (lkh/add-liveness-probe-cmd ["wget" "127.0.0.1:5000"]))) + #_(lkh/add-readiness-probe-cmd ["wget" "127.0.0.1:5000"]) + #_(lkh/add-liveness-probe-cmd ["wget" "127.0.0.1:5000"]))) #_(lk/add-container - :hackspace-frontend - (get-in @conf [:images :nginx]) - (-> {} - (lkh/add-to-spec :resources resources-mini) - (lkh/add-to-spec :ports [{:containerPort 80 :name "nginx-port"}]) - (lkh/add-readiness-probe-cmd ["wget" "127.0.0.1/static/images/favicon.ico"]) - (lkh/add-liveness-probe-cmd ["wget" "127.0.0.1/static/images/favicon.ico"]))) + :hackspace-frontend + (get-in @conf [:images :nginx]) + (-> {} + (lkh/add-to-spec :resources resources-mini) + (lkh/add-to-spec :ports [{:containerPort 80 :name "nginx-port"}]) + (lkh/add-readiness-probe-cmd ["wget" "127.0.0.1/static/images/favicon.ico"]) + (lkh/add-liveness-probe-cmd ["wget" "127.0.0.1/static/images/favicon.ico"]))) (lk/add-volume "static-files" {:emptyDir {}} {:hackspace-backend "/app/staticfiles"}) ; order does matter here, deployment after initcontainers - (lku/wait-for-service-port postgres :postgres) - (lku/wait-for-service-port redis :redis) + ;(lku/wait-for-service-port postgres :postgres) + ;(lku/wait-for-service-port redis :redis) ; wrap in a deployment (lk/deployment 2) @@ -159,25 +172,46 @@ (fn [] (-> (lk/pod :hackspace-queue {:app :hackspace + :env :stage :role :master - :tier :backend} + :tier :queue} {:imagePullSecrets [{:name "docker-registry"}]}) (lk/add-container :hackspace (get-in @conf [:images :django]) (-> {} (lkh/add-to-spec :resources resources-small) - (lkh/add-to-spec :command ["/app/manage.py" "huey"]) + (lkh/add-to-spec :command ["/app/manage.py" "run_huey"]) (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]))) (lk/deployment 2)))))) +(defn field-conj [m k v] + (if (contains? m k) + (update m k conj v) + ;; else + (assoc m k [v]))) + +(defn- mount-func [name mounts] + (apply comp (for [[cont path-or-opts] mounts] + #(lk/update-container % cont field-conj :volumeMounts + (if (map? path-or-opts) + (into path-or-opts {:name name}) + {:name name + :mountPath-Or-Opts path-or-opts}))))) + +(defn add-volume [pod name spec mounts] + (-> pod + (update :spec field-conj :volumes (-> {:name name} + (merge spec))) + ((mount-func name mounts)))) + (defn footprint [kube-injector] (-> kube-injector (lk/rule :footprint-backend-master [:postgres-master-rule :redis-master-rule :rabbit-master-rule] (fn [postgres redis rabbit] - (-> (lk/pod :footprint-master {:app :footprint - :role :master - :tier :backend} + (-> (lk/pod :footprint-app {:app :footprint + :env :production + :tier :web} {:imagePullSecrets [{:name "docker-registry"}]}) ; Add containers to the pod, one for frontend assets and one for backend @@ -186,8 +220,11 @@ (get-in @conf [:images :footprint]) (-> {} (lkh/add-to-spec :resources resources-medium) - (lkh/add-to-spec :command ["/gunicorn.sh"]) + ;(lkh/add-to-spec :command ["/gunicorn.sh"]) + (lkh/add-to-spec :command ["/usr/bin/gunicorn" "config.wsgi" "-b" "0.0.0.0:5000" "--chdir=/app" "--log-file=/tmp/gunicorn.log"]) + (lkh/add-to-spec :ports [{:containerPort 5000 :name "gunicorn-port"}]) + (lkh/add-to-spec :imagePullPolicy "Always") (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]) #_(lkh/add-readiness-probe-get "http://127.0.0.1" 5000) (lkh/add-liveness-probe-get "http://127.0.0.1" 5000))) @@ -197,38 +234,80 @@ (get-in @conf [:images :nginx]) (-> {} (lkh/add-to-spec :resources resources-mini) + (lkh/add-to-spec :imagePullPolicy "Always") (lkh/add-to-spec :ports [{:containerPort 80 :name "nginx-port"}]) #_(lkh/add-readiness-probe-get "127.0.0.1/static/images/favicon.ico" 80) (lkh/add-liveness-probe-get "http://127.0.0.1/static/images/favicon.ico" 80))) - (lk/add-volume "static-files" {:emptyDir {}} - {:footprint-backend "/app/staticfiles" - :footprint-frontend "/usr/share/nginx/html/static/"}) + (add-volume "static-files" {:emptyDir {}} + {:footprint-backend {:mountPath "/app/staticfiles"} + :footprint-frontend + {:mountPath "/usr/share/nginx/html/static/" + :readOnly true}}) + + (lk/add-init-container + :footprint-init-migrations + (get-in @conf [:images :footprint]) + (-> {} + (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]) + (lkh/add-to-spec :command ["./manage.py" "migrate"]))) + + (lk/add-init-container + :footprint-init-static-resources + (get-in @conf [:images :footprint]) + (-> {} + (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]) + (lkh/add-to-spec :command ["./manage.py" "collectstatic"]))) ; order does matter here, deployment after initcontainers - (lku/wait-for-service-port postgres :postgres) - (lku/wait-for-service-port redis :redis) - (lku/wait-for-service-port rabbit :rabbit) + ;(lku/wait-for-service-port postgres :postgres) + ;(lku/wait-for-service-port redis :redis) + ;(lku/wait-for-service-port rabbit :rabbit) ; wrap in a deployment (lk/deployment 2) (lk/expose-cluster-ip :footprint-backend - (lk/port :footprint-master :gunicorn-port 5000 5000)) + (lk/port :footprint-app :gunicorn-port 5000 5000)) (lk/expose-cluster-ip :footprint-frontend - (lk/port :footprint-master :nginx-port 80 80))))) + (lk/port :footprint-app :nginx-port 80 80))))) + (lk/rule :footprint-aqscheduler-master [] + (fn [] + (-> (lk/pod :footprint-scheduler + {:app :footprint + :env :production + :tier :scheduler} + {:imagePullSecrets [{:name "docker-registry"}]}) + + (lk/add-container + :scheduler + (get-in @conf [:images :scheduler]) + (-> {} + (lkh/add-to-spec :imagePullPolicy "Always") + (lkh/add-to-spec :resources resources-mini) + (lkh/add-to-spec :command ["/usr/local/bin/dramatiq_apscheduler" "/app/tasks.yml" "--debug"]) + (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]))) + (add-volume "scheduler-config" {:configMap + {:name "scheduler-tasks"}} + {:scheduler + {:name "scheduler-config" + :mountPath "/app/tasks.yml" + :subPath "tasks.yml"}}) + (lk/deployment 1)))) + (lk/rule :footprint-queue-master [] (fn [] (-> (lk/pod :footprint-queue {:app :footprint - :role :master - :tier :backend} + :env :production + :tier :queue} {:imagePullSecrets [{:name "docker-registry"}]}) (lk/add-container :footprint (get-in @conf [:images :footprint]) (-> {} - (lkh/add-to-spec :resources resources-small) + (lkh/add-to-spec :imagePullPolicy "Always") + (lkh/add-to-spec :resources resources-medium) (lkh/add-to-spec :command ["/app/manage.py" "rundramatiq" "-p" "1" "-t" "1"]) (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]))) @@ -260,12 +339,35 @@ (lkh/secrets name {} (lkh/encode-secret-map data)))))) +(defn add-config [kube-injector name config] + (lk/rule kube-injector :config-map [] + (fn [] + (lk/config-map name config)))) + +(defn make-ingress-hackspace [kube-injector] + (lk/rule kube-injector :lkube-ingress [] + (fn [] + (-> (lkh/ingress "lkube-ingress" {:app :nginx-ingress :env :production :tier :ingress}) + (lkh/add-spec :tls [{:hosts [(:ingress-host @conf)] :secretName "letsencrypt-production"}]) + (lkh/add-spec :rules [{:host (:ingress-host @conf) + :http {:paths + [{:path "/" + :backend + {:serviceName "hackspace-backend" + :servicePort 5000}} + {:path "/static/" + :backend + {:serviceName "hackspace-frontend" + :servicePort 80}}]}}]) + (lk/add-annotation :certmanager.k8s.io/cluster-issuer "letsencrypt-production") + (lk/add-annotation :kubernetes.io/ingress.class "nginx"))))) + (defn make-ingress [kube-injector] (lk/rule kube-injector :lkube-ingress [] (fn [] - (-> (lkh/ingress "lkube-ingress" {:app :nginx-ingress}) + (-> (lkh/ingress "lkube-ingress" {:app :nginx-ingress :env :production :tier :ingress}) (lkh/add-spec :tls [{:hosts [:lkube-ingress] :secretName "lkube-tls"}]) - (lkh/add-spec :rules [{:host "lkube-ingress.35.197.252.20.nip.io" + (lkh/add-spec :rules [{:host (:ingress-host @conf) :http {:paths [{:path "/" :backend @@ -279,6 +381,8 @@ (lk/add-annotation :kubernetes.io/ingress.class "nginx"))))) (defn footprint-deployment [namespace kube-config] + (println "deploying footprint") + (reset! conf (clojure.edn/read-string (slurp (str namespace "-config.edn")))) ; run deployment in default namespace to create a namespace called footprint (-> (lk/injector) @@ -286,23 +390,25 @@ lk/standard-descs (lk/get-deployable config) (lkh/to-yaml-store (str namespace "-store.yaml")) - (kube-apply-namespace (io/file (str namespace "-deploy.yaml")) nil kube-config)) + (kube-apply (io/file (str namespace "-deploy.yaml")) nil kube-config)) ; run deployment under footprint namespace (-> (lk/injector) (make-environment-secrets :environment (:vars @conf)) - (make-registry-secrets :docker-registry {:.dockerconfigjson (:registry @conf)}) + #_(make-registry-secrets :docker-registry {:.dockerconfigjson (:registry @conf)}) make-ingress rabbitmq database redis + (add-config "scheduler-tasks" (get-in @conf [:config])) footprint lk/standard-descs (lk/get-deployable config) (lkh/to-yaml-store (str namespace "-store.yaml")) - (kube-apply-namespace (io/file (str namespace "-deploy.yaml")) "footprint" kube-config))) + (kube-apply (io/file (str namespace "-deploy.yaml")) "footprint" kube-config))) (defn hackspace-deployment [namespace kube-config] + (println "deploying hackspace") (reset! conf (clojure.edn/read-string (slurp (str namespace "-config.edn")))) ; run deployment in default namespace to create a namespace called footprint (-> (lk/injector) @@ -310,22 +416,55 @@ ;lk/standard-descs (lk/get-deployable config) (lkh/to-yaml-store (str namespace "-store.yaml")) - (kube-apply-namespace (io/file (str namespace "-deploy.yaml")) nil kube-config)) + (kube-apply (io/file (str namespace "-deploy.yaml")) nil kube-config)) ; run deployment under footprint namespace (-> (lk/injector) (make-environment-secrets :environment (:vars @conf)) ;(make-registry-secrets :docker-registry {:.dockerconfigjson (:registry @conf)}) - make-ingress + make-ingress-hackspace database redis hackspace lk/standard-descs (lk/get-deployable config) (lkh/to-yaml-store (str namespace "-store.yaml")) - (kube-apply-namespace (io/file (str namespace "-deploy.yaml")) namespace kube-config))) + (kube-apply (io/file (str namespace "-deploy.yaml")) namespace kube-config))) -(defn -main [] - (hackspace-deployment "maidstone-hackspace" "/home/oly/.kube/dke.yml") - (footprint-deployment "footprint" "/home/oly/.kube/ake.yml")) +(defn deploy [{:keys [service]}] + (let [namespace (get-in data/services [(keyword service) :namespace]) + config (get-in data/services [(keyword service) :config])] + (cond + (= service "hackspace") (hackspace-deployment "maidstone-hackspace" "/home/oly/.kube/dke.yml") + (= service "footprint-production") (footprint-deployment "footprint-production" "/home/oly/.kube/ake.yml") + (= service "footprint-stage") (footprint-deployment "footprint" "/home/oly/.kube/ake.yml") + :else (println (str "Service " service " not found."))))) +(def CONFIGURATION + {:app {:command "Kubernetes deploy" + :description "Generate yaml and deploy out to cluster." + :version "0.0.1"} +; :global-opts [] + :commands [{:command "deploy" + :description "Deploy app out to cluster" + :opts [{:option "service" :as "Service to deploy" :type :string :spec string?}] + :runs deploy} + {:command "secrets" + :description "Display secrests" + :opts [{:option "service" :as "Service to deploy" :type :string :spec string?}] + :runs lky/fetch-secrets}]}) + +(defn -main [& args] + ;(deploy "hackspace") + (if args + (run-cmd args CONFIGURATION) + (do + #_(hackspace-deployment "maidstone-hackspace" "/home/oly/.kube/dke.yml") + (footprint-deployment "footprint" "/home/oly/.kube/ake.yml"))) + + #_(hackspace-deployment "maidstone-hackspace" "/home/oly/.kube/dke.yml") + #_(footprint-deployment "footprint-production" "/home/oly/.kube/ake.yml") + #_(footprint-deployment "footprint" "/home/oly/.kube/ake.yml")) + + +;/usr/local/bin/gunicorn config.wsgi --workers 2 -k gevent --worker-connections 100 --max-requests 300 --keep-alive 1000 -b 0.0.0.0:5000 --chdir=/app diff --git a/src/kube_deploy/data.clj b/src/kube_deploy/data.clj new file mode 100644 index 0000000..af29482 --- /dev/null +++ b/src/kube_deploy/data.clj @@ -0,0 +1,4 @@ +(ns kube-deploy.data) + +(def services (clojure.edn/read-string (slurp "config.edn"))) +(def fragments (clojure.edn/read-string (slurp "fragments.edn"))) diff --git a/src/kube_deploy/from_yaml.clj b/src/kube_deploy/from_yaml.clj new file mode 100644 index 0000000..ce4d11e --- /dev/null +++ b/src/kube_deploy/from_yaml.clj @@ -0,0 +1,37 @@ +(ns kube-deploy.from-yaml + (:require [yaml.core :as yaml] + [clojure.data.codec.base64 :as b64] + [kube-deploy.data :as data] + [clojure.java.shell :as sh])) + +(defn decode-secret [value] + (String. (b64/decode (.getBytes value)))) + +(defn kube-load + ([resource name] + (kube-load resource name nil nil)) + ([resource name kube-namespace] + (kube-load resource name kube-namespace nil)) + ([resource name kube-namespace kube-config] + (let [namespace-param (if (nil? kube-namespace) nil (str "--namespace=" kube-namespace)) + config-param (if (nil? kube-config) nil (str "--kubeconfig=" kube-config))] + (let [res (apply sh/sh (remove nil? ["kubectl" namespace-param config-param "get" resource name "-o=yaml"]))] + (if (= (:exit res) 0) + (:out res) + (throw (Exception. (:err res)))))))) + +(defn fetch-secrets-yaml [namespace config] + (yaml/parse-string (kube-load "secret" "environment" namespace config))) + +(defn decode-secrets [data] + (reduce-kv + (fn [m k v] (assoc m k (decode-secret v))) + {} + (:data data))) + +(defn fetch-secrets [{:keys [service]}] + (let [namespace (get-in data/services [(keyword service) :namespace]) + config (get-in data/services [(keyword service) :config])] + (-> (fetch-secrets-yaml namespace config) + decode-secrets + clojure.pprint/pprint))) diff --git a/src/kube_deploy/helpers.clj b/src/kube_deploy/helpers.clj index b654c62..308f504 100644 --- a/src/kube_deploy/helpers.clj +++ b/src/kube_deploy/helpers.clj @@ -33,6 +33,16 @@ :metadata {:name name :labels labels} :spec spec})) +(defn persistent-volume + ([obj name labels] + (ingress name labels {})) + ([obj name labels spec] + (assoc obj {:apiVersion "v1" + :kind "PersistentVolume" + :metadata {:name name :labels labels} + :spec spec}))) + + (defn add-to-spec ([container name value] (-> container @@ -107,6 +117,9 @@ (-> obj (update-in [:spec] assoc key val))) +(defn decode-secret [value] + (String. (b64/decode (.getBytes value)))) + (defn encode-secret [value] (b64/encode (.getBytes value)))