Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Experiment to setup a single step deploy process for Datomic Ions including setting up the AWS API Gateway.

Next experiment will be generating a CloudFormation template for the app.

{:app-name "arthur"
:resources [{:path-part "hello-world"
:methods [{:http-method "ANY"
:authorization-type "NONE"
:integration {:lambda-name :hello-world-web
:timeout-in-millis 29000}}]}
{:path-part "echo"
:methods [{:http-method "ANY"
:authorization-type "NONE"
:integration {:lambda-name :echo-web
:timeout-in-millis 29000}}]}]}
(ns deploy.api-release
(:require [clojure.data.json :as json]
[clojure.string :as string]
[deploy.aws-utils :refer [aws find-by submap? get-stack-context]])
(:import (java.util UUID)))
(defn sync-permission
"Setup permission based via accreate-only changes"
[region client-id rest-api-id path-part function-name FunctionArn]
(let [source-arn (str "arn:aws:execute-api:" region ":" client-id ":" rest-api-id "/*/*/" path-part)
{:keys [Policy]} (aws "lambda" "get-policy" "--function-name" function-name)
policy-data (when Policy (json/read-str Policy :key-fn keyword))]
(when-not (find-by (submap? {:Condition {:ArnLike {:AWS:SourceArn source-arn}}
:Resource FunctionArn})
(:Statement policy-data))
(aws "lambda" "add-permission"
"--function-name" function-name
"--action" "lambda:InvokeFunction"
"--principal" "apigateway.amazonaws.com"
"--statement-id" (.toString (UUID/randomUUID))
"--source-arn" source-arn))))
(defn sync-integration
"Setup integration based via accreate-only changes"
[region rest-api-id resource-id http-method function-arn timeout-in-millis]
(when-not (aws "apigateway" "get-integration"
"--rest-api-id" rest-api-id
"--resource-id" resource-id
"--http-method" http-method)
(let [service_api (str "2015-03-31/functions/" function-arn "/invocations") ; TODO: Okay to hardcode this date?
uri (str "arn:aws:apigateway:" region ":lambda:path/" service_api)]
(aws "apigateway" "put-integration"
"--rest-api-id" rest-api-id
"--resource-id" resource-id
"--http-method" http-method
"--type" "AWS_PROXY"
"--integration-http-method" "POST"
"--timeout-in-millis" (str timeout-in-millis)
"--uri" uri))))
(defn sync-method
"Setup method based via accreate-only changes"
[rest-api-id resource-id http-method authorization-type]
(if-let [method (aws "apigateway" "get-method"
"--rest-api-id" rest-api-id
"--resource-id" resource-id
"--http-method" http-method)]
(let [kvs {:httpMethod http-method :authorizationType authorization-type}]
(when-not (submap? kvs method)
(throw (ex-info "method incompatible with config" {:kvs kvs :method method}))))
(aws "apigateway" "put-method"
"--rest-api-id" rest-api-id
"--resource-id" resource-id
"--http-method" http-method
"--authorization-type" authorization-type)))
(defn sync-resource
"Setup resource based via accreate-only changes"
[client-id region group rest-api-id api-resources parent-id path-part methods]
(let [api-resource (or (find-by (submap? {:parentId parent-id :pathPart path-part}) api-resources)
(aws "apigateway" "create-resource"
"--rest-api-id" rest-api-id
"--parent-id" parent-id
"--path-part" path-part))
resource-id (:id api-resource)]
(doseq [{:keys [http-method authorization-type integration]} methods]
(let [{:keys [lambda-name timeout-in-millis]} integration
function-name (str group "-" (name lambda-name))
function (aws "lambda" "get-function" "--function-name" function-name)
function-arn (get-in function [:Configuration :FunctionArn])]
(sync-method rest-api-id resource-id http-method authorization-type)
(sync-integration region rest-api-id resource-id http-method function-arn timeout-in-millis)
(sync-permission region client-id rest-api-id path-part function-name function-arn)))))
(defn get-rest-api-id
[app-name]
(let [ids (aws "apigateway" "get-rest-apis" "--query" (str "items[?name==`" app-name "`].id"))]
(when (next ids)
(throw (ex-info "Multiple matching rest-apis" {:name app-name :rest-api-ids ids})))
(first ids)))
(defn sync-rest-api
"Setup REST API based via accreate-only changes"
[api-config]
(let [{:keys [app-name resources]} api-config
{:keys [client-id region group]} (get-stack-context app-name)
rest-api-id (or (get-rest-api-id app-name)
(:id (aws "apigateway" "create-rest-api"
"--name" app-name
"--binary-media-types" "*/*"
"--endpoint-configuration" "{\"types\" : [\"REGIONAL\"]}")))
api-resources (:items (aws "apigateway" "get-resources" "--rest-api-id" rest-api-id))
parent-id (:id (find-by (submap? {:path "/"}) api-resources))]
(doseq [{:keys [path-part methods]} resources]
(sync-resource client-id region group rest-api-id api-resources parent-id path-part methods))))
(defn deploy-uri
[app-name stage-name]
(let [stacks (aws "cloudformation" "describe-stacks")
app-stack (find-by (submap? {:StackName app-name}) (:Stacks stacks))
[_ _ _ region & _] (string/split (:StackId app-stack) #":")
rest-api-id (get-rest-api-id app-name)]
(str "https://" rest-api-id ".execute-api." region ".amazonaws.com/" stage-name "/")))
(defn deploy-api
[app-name stage-name]
(let [rest-api-id (get-rest-api-id app-name)]
(aws "apigateway" "create-deployment" "--rest-api-id" rest-api-id "--stage-name" stage-name)))
(ns deploy.aws-utils
(:require [clojure.data.json :as json]
[clojure.string :as string]
[clojure.java.shell :as shell]))
(defn submap?
([a] (partial submap? a))
([a b] (= a (select-keys b (keys a)))))
(defn find-by [pred ms] (first (filter pred ms)))
(defn aws [& args]
(apply println "aws" args)
(let [ret (apply shell/sh "aws" args)]
(when (= 0 (:exit ret))
(json/read-str (:out ret) :key-fn keyword :eof-error? false))))
(defn get-stack-context
[app-name]
(let [app-stack (aws "cloudformation" "describe-stacks" "--query" (str "Stacks[?StackName==`" app-name "`] | [0]"))
{:keys [StackId Outputs]} app-stack
[_ _ _ region client-id & _] (string/split StackId #":")
group (:OutputValue (find-by (submap? {:OutputKey "CodeDeployDeploymentGroup"}) Outputs))]
{:client-id client-id
:region region
:group group}))
(ns deploy.ion-release
(:require [datomic.ion.dev :as ion-dev]
[deploy.aws-utils :as aws-utils]))
(defn release
"Do push and deploy of app. Supports stable and unstable releases. Returns when deploy finishes running."
[ion-config args]
(let [group (:group (aws-utils/get-stack-context (:app-name ion-config)))
push-data (ion-dev/push args)
deploy-args (merge (select-keys args [:creds-profile :region :uname :group])
(select-keys push-data [:rev])
{:group group})]
(let [deploy-data (ion-dev/deploy deploy-args)
deploy-status-args (merge (select-keys args [:creds-profile :region])
(select-keys deploy-data [:execution-arn]))]
(loop []
(let [status-data (ion-dev/deploy-status deploy-status-args)]
(case (:code-deploy-status status-data)
"RUNNING" (do (Thread/sleep 5000) (recur))
"SUCCEEDED" status-data
(throw (ex-info "ion-release did not succeed" status-data))))))))
(ns user
(:require [clojure.edn :as edn]
[deploy.api-release :as api-release]
[deploy.ion-release :as ion-release]))
(def ion-config (edn/read-string (slurp "resources/datomic/ion-config.edn")))
(def api-config (edn/read-string (slurp "resources/datomic/api-config.edn")))
(defn deploy []
(ion-release/release ion-config {})
(api-release/sync-rest-api api-config)
(api-release/deploy-api (:app-name api-config) "dev")
(println "Deployed to" (api-release/deploy-uri (:app-name api-config) "dev")))
(comment (deploy))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment