Last active
March 15, 2022 03:48
-
-
Save eneroth/f8c03bc5e4fcff28179aec5ea939eb86 to your computer and use it in GitHub Desktop.
Minimal DSL for CloudFormation templates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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