Skip to content

Instantly share code, notes, and snippets.

@eneroth
Last active March 15, 2022 03:48
Show Gist options
  • Save eneroth/f8c03bc5e4fcff28179aec5ea939eb86 to your computer and use it in GitHub Desktop.
Save eneroth/f8c03bc5e4fcff28179aec5ea939eb86 to your computer and use it in GitHub Desktop.
Minimal DSL for CloudFormation templates
;; DSL
;; ############################
(ns example.lib.cloudformation
(:require [camel-snake-kebab.core :refer [->PascalCase]]))
(defmacro defresource
"Defines a resource. The CF name of the resource will be the same
as the name given to it, except Pascal-cased, since that seems
to be the style favoured by CF. Optionally, a literal name
in the form of a keyword can be given as second argument to
override this."
([resource-name body]
(let [aws-name (-> resource-name name ->PascalCase keyword)]
`(defresource ~resource-name ~aws-name ~body)))
([resource-name aws-name body]
`(def ~resource-name
{~aws-name ~body})))
(defmacro defparam
"Same as defresource, but for parameters."
[param-name & args]
`(defresource ~param-name ~@args))
(defmacro defoutput
"Same as defresource, but for outputs."
[output-name & args]
`(defresource ~output-name ~@args))
(defmacro defcondition
"Same as defresource, but for conditions."
[condition-name & args]
`(defresource ~condition-name ~@args))
(defn export
"Takes a bunch of defresources and merges theme together,
making them ready for export to JSON or YML."
[& exports]
(apply merge exports))
;; Clojure->AWS converters
;; ############################
(defn logical-name
"Get the logical name of a defresource or defparam."
[resource]
(ffirst resource))
(defn ref
"Reference a previously defined defresource or defparam.
Expands to a CF reference to the logical name."
[resource]
{:Ref (logical-name resource)})
(defn deps
"Given some defresources, expands to a CF-approved
dependency declaration."
[dep & more]
(if more
(vec (map logical-name (conj more dep)))
(logical-name dep)))
(defn join
"Creates a Fn::Join"
[& values]
{"Fn::Join" ["" (vec values)]})
(defn get-att
"Creates a Fn::GetAtt, given a defresource and an attribute."
[res att]
{"Fn::GetAtt" [(logical-name res) att]})
(defn sub
"Creates a Fn::Sub given a text, or a text and a map of some
replacements."
([text]
(sub text nil))
([text opts]
(if opts
{"Fn::Sub" [text opts]}
{"Fn::Sub" text})))
(defn tag
"Create a tag entry."
[k v & opts]
(merge {:Key k
:Value v}
(first opts)))
(defn base-64
"Create a Fn::Base64."
[data]
{"Fn::Base64" data})
(defn cond-equals [v1 v2]
{"Fn::Equals" [v1 v2]})
(defn cond-and [& conds]
{"Fn::And" (vec conds)})
(defn cond-or [& conds]
{"Fn::Or" (vec conds)})
(defn cond-not [v]
{"Fn::Not" [v]})
(defn cond-if [condition value-true value-false]
{"Fn::If" [condition value-true value-false]})
(def aws-no-value {"AWS::NoValue" nil})
;; example/elb.clj
;; ############################
(ns example.elb
(:require [example.lib.cloudformation :refer [defresource defoutput] :as cf]
[example.resources.vpc :as vpc]))
(defresource network-load-balancer
{:Type "AWS::ElasticLoadBalancingV2::LoadBalancer"
:Properties
{:Tags [(cf/tag "Name" (cf/sub "${AWS::StackName}"))]
:Scheme :internal ;:internet-facing
:Type :network
:Subnets [(cf/ref vpc/subnet-1)
(cf/ref vpc/subnet-2)]}})
(defresource target-5789
{:Type "AWS::ElasticLoadBalancingV2::TargetGroup"
:Properties
{:Tags [(cf/tag "Name" (cf/sub "${AWS::StackName}"))]
:Port 5789
:Protocol "TCP"
:VpcId (cf/ref vpc/vpc)
:HealthCheckIntervalSeconds 10
:HealthCheckPath "/"
:HealthCheckPort 5789
:HealthCheckProtocol "HTTP"
:HealthCheckTimeoutSeconds 6
:HealthyThresholdCount 3
:UnhealthyThresholdCount 3
:TargetGroupAttributes [{:Key "deregistration_delay.timeout_seconds"
:Value 30}]}})
(defresource listener-5789
{:Type "AWS::ElasticLoadBalancingV2::Listener"
:Properties
{:DefaultActions [{:Type :forward
:TargetGroupArn (cf/ref target-5789)}]
:LoadBalancerArn (cf/ref network-load-balancer)
:Port 5789
:Protocol "TCP"}})
;; Exports and outputs
(def exports
(cf/export
network-load-balancer
target-5789
listener-5789))
(defoutput nlb-name :NLBName
{:Description "The name of the NLB"
:Value (cf/get-att network-load-balancer :LoadBalancerName)})
(defoutput nlb-full-name :NLBFullName
{:Description "The full name of the NLB"
:Value (cf/get-att network-load-balancer :LoadBalancerFullName)})
(defoutput nlb-arn :NLBArn
{:Description "The ARN of the NLB"
:Value (cf/ref network-load-balancer)})
(def outputs
(cf/export
nlb-name
nlb-full-name
nlb-arn))
;; example/core.clj
;; ############################
(def cloud-formation-template
{:AWSTemplateFormatVersion "2010-09-09"
:Description "Example template"
:Parameters parameters/exports
:Conditions conditions/exports
:Metadata {"AWS::CloudFormation::Interface" parameters/parameter-groups}
:Resources (cf/export
alerts/exports
asg/exports
codedeploy/exports
elb/exports
endpoint-service/exports
iam/exports
launch-config/exports
routes/exports
s3/exports
security-group/exports
vpc/exports)
:Outputs (cf/export
asg/outputs
security-group/outputs
codedeploy/outputs
elb/outputs)})
(defn generate-templates []
(spit "cluster.json" (generate-string cloud-formation-template {:pretty true})) ;; cheshire
(spit "cluster.yaml" (yaml/generate-string cloud-formation-template))) ;; clj-commons/clj-yaml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment