├── .gitignore ├── .ruby-version ├── .travis.yml ├── CHANGELOG ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── License ├── README.md ├── Rakefile ├── bin └── cfn2dsl ├── cfn2dsl.gemspec ├── lib ├── cfn2dsl.rb ├── cfn_parser.rb ├── cfndsl.erb ├── cloudformation.rb ├── condition.rb ├── error.rb ├── intrinsic_function.rb ├── mapping.rb ├── metadata.rb ├── output.rb ├── parameter.rb ├── render.rb ├── resource.rb ├── rules.rb └── version.rb └── spec ├── cfn_parser_spec.rb ├── cloudformation_spec.rb ├── condition_spec.rb ├── intrinsic_function_spec.rb ├── mapping_spec.rb ├── metadata_spec.rb ├── output_spec.rb ├── parameter_spec.rb ├── resource_spec.rb ├── samples ├── Windows_Single_Server_SharePoint_Foundation.json ├── basic-amazon-redshift-cluster.json ├── load-based-auto-scaling.json ├── s3bucket.yaml ├── s3bucket_invalid.yaml └── s3bucket_unsafe.yaml ├── spec_helper.rb └── version_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .ruby-gemset 2 | *.gem 3 | .bundle 4 | vendor 5 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.4 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4.4 4 | - ruby-head 5 | matrix: 6 | allow_failures: 7 | - rvm: ruby-head 8 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.1.3 - Added CreationPolicy attribute Support. Fixed DependsOn attribute missing in output 2 | v0.1.4 - Stable Release 3 | v0.1.5 - Fixed CreationPolicy translate issue 4 | v0.4.0 - Add YAML support (Thanks to @cmaxwellau) 5 | 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development, :test do 4 | gem 'cfndsl' 5 | gem 'rake' 6 | gem 'rspec' 7 | gem 'guard' 8 | gem 'guard-rspec' 9 | gem 'pry' 10 | gem 'ffi', '>= 1.9.24' 11 | end 12 | gem 'extlib' 13 | gem 'awesome_print' 14 | gem 'erubis' 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | awesome_print (1.8.0) 5 | cfndsl (0.17.0) 6 | coderay (1.1.2) 7 | diff-lcs (1.3) 8 | erubis (2.7.0) 9 | extlib (0.9.16) 10 | ffi (1.11.1) 11 | formatador (0.2.5) 12 | guard (2.15.0) 13 | formatador (>= 0.2.4) 14 | listen (>= 2.7, < 4.0) 15 | lumberjack (>= 1.0.12, < 2.0) 16 | nenv (~> 0.1) 17 | notiffany (~> 0.0) 18 | pry (>= 0.9.12) 19 | shellany (~> 0.0) 20 | thor (>= 0.18.1) 21 | guard-compat (1.2.1) 22 | guard-rspec (4.7.3) 23 | guard (~> 2.1) 24 | guard-compat (~> 1.1) 25 | rspec (>= 2.99.0, < 4.0) 26 | listen (3.1.5) 27 | rb-fsevent (~> 0.9, >= 0.9.4) 28 | rb-inotify (~> 0.9, >= 0.9.7) 29 | ruby_dep (~> 1.2) 30 | lumberjack (1.0.13) 31 | method_source (0.9.2) 32 | nenv (0.3.0) 33 | notiffany (0.1.3) 34 | nenv (~> 0.1) 35 | shellany (~> 0.0) 36 | pry (0.12.2) 37 | coderay (~> 1.1.0) 38 | method_source (~> 0.9.0) 39 | rake (12.3.3) 40 | rb-fsevent (0.10.3) 41 | rb-inotify (0.10.0) 42 | ffi (~> 1.0) 43 | rspec (3.8.0) 44 | rspec-core (~> 3.8.0) 45 | rspec-expectations (~> 3.8.0) 46 | rspec-mocks (~> 3.8.0) 47 | rspec-core (3.8.2) 48 | rspec-support (~> 3.8.0) 49 | rspec-expectations (3.8.4) 50 | diff-lcs (>= 1.2.0, < 2.0) 51 | rspec-support (~> 3.8.0) 52 | rspec-mocks (3.8.1) 53 | diff-lcs (>= 1.2.0, < 2.0) 54 | rspec-support (~> 3.8.0) 55 | rspec-support (3.8.2) 56 | ruby_dep (1.5.0) 57 | shellany (0.0.1) 58 | thor (0.20.3) 59 | 60 | PLATFORMS 61 | ruby 62 | 63 | DEPENDENCIES 64 | awesome_print 65 | cfndsl 66 | erubis 67 | extlib 68 | ffi (>= 1.9.24) 69 | guard 70 | guard-rspec 71 | pry 72 | rake 73 | rspec 74 | 75 | BUNDLED WITH 76 | 1.16.6 77 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: 'rspec --color --format documentation' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | end 6 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kevin Yung. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Cfn2dsl [![Build Status](https://travis-ci.org/allinwonder/cfn2dsl.svg?branch=master)](https://travis-ci.org/allinwonder/cfn2dsl) 2 | 3 | cfn2dsl is a tool to enable CloudFormation development teams to work with both JSON and YAML templates to produce Ruby DSL code written in `cfndsl` format (created by [cfndsl](https://github.com/stevenjack/cfndsl) project). 4 | 5 | The idea is to give developers options for developing CloudFormation configurations using the tool and language which they feel comfortable and efficient in. Some developers like to develop JSON templates, and others like to develop with `cfndsl`. We need a tool to synchronise development in both methods, so `cfn2dsl` is here to help. It can parse CloudFormation JSON templates and translate them into `cfndsl`'s Ruby DSL. 6 | 7 | #### what cfn2dsl can do 8 | 9 | 10 | * It parses CloudFormation JSON or YAML template and outputs `cfndsl` Ruby code. 11 | 12 | * It uses generic `cfndsl` objects in the translation, for example `Parameter()`, `Resource()`, `Output()` and `Property()`. It supports all the resources, attributes and properties officially supported by CloudFormation. 13 | 14 | * It formats Ruby hash/array objects in a more readable way using `awesome_print`. 15 | 16 | #### what cfn2dsl can't do 17 | 18 | * It is not able to keep your comments in the Ruby source, so you might need to move any important information that exists in comments out from the Ruby code into a README file. 19 | 20 | * It currently doesn't know how to translate CloudForamtion resource into the abstracted objects used in `cfndsl`, for example `EC2_Instance`. 21 | 22 | * It can't reproduce your programming logic in your `cfndsl` template if you rely on Ruby code to do interesting things, for example environment variables. (You might need to think about using CloudFormation Conditions to achieve an environment switch.) 23 | 24 | * It doesn't know how to translate special intrinsic functionality provided by `cfndsl`, for example `FnFormat()`. 25 | 26 | 27 | #### how to use this tool 28 | 29 | ``` 30 | #> gem install cfn2dsl 31 | ``` 32 | 33 | * Command line options 34 | 35 | ``` 36 | #> cfn2dsl --help 37 | 38 | Usage: cfn2dsl -t|--template file [-o|--output file] 39 | -t, --template file Template file path 40 | -o, --output [file] Output file path 41 | -h, --help show this message 42 | ``` 43 | 44 | * Translate template and send output to standard output 45 | 46 | ``` 47 | cfn2dsl -t /your/path/to/template.yaml 48 | ``` 49 | 50 | * Translate template and write output to file 51 | 52 | ``` 53 | cfn2dsl -t /your/path/to/template.yaml -o /your/path/to/template.rb 54 | ``` 55 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | namespace :cfn2dsl do 2 | task :build do 3 | `gem build cfn2dsl.gemspec` 4 | end 5 | 6 | task :install do 7 | end 8 | end 9 | 10 | require 'rspec/core/rake_task' 11 | RSpec::Core::RakeTask.new(:spec) 12 | 13 | task :default => [:spec] 14 | -------------------------------------------------------------------------------- /bin/cfn2dsl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../lib/cfn2dsl' 3 | 4 | USAGE = "Usage: #{File.basename(__FILE__)} -t|--template file [-o|--output file]" 5 | options = {} 6 | op = OptionParser.new do |opts| 7 | opts.banner = USAGE 8 | opts.on('-t', '--template file', 'Template file path') do |v| 9 | options[:template] = v 10 | end 11 | 12 | opts.on('-o', '--output [file]', 'Output file path') do |v| 13 | options[:output] = v 14 | end 15 | 16 | opts.on_tail('-h', '--help', 'show this message') do |_v| 17 | puts opts 18 | exit 19 | end 20 | 21 | opts.on_tail('-v', '--version', 'show the version') do |_v| 22 | puts Cfn2dsl::VERSION 23 | exit 24 | end 25 | end 26 | 27 | op.parse! 28 | 29 | unless options[:template] 30 | puts op 31 | exit 1 32 | else 33 | template = IO.read(options[:template]) 34 | cfndsl = CloudFormation.new(template) 35 | end 36 | 37 | if options[:output] 38 | File.open(options[:output], 'w') do |file| 39 | file.write(Render.new(cfndsl).cfn_to_cfndsl) 40 | end 41 | else 42 | puts Render.new(cfndsl).cfn_to_cfndsl 43 | end 44 | 45 | -------------------------------------------------------------------------------- /cfn2dsl.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__) + "/lib") 2 | require "version" 3 | Gem::Specification.new do |s| 4 | s.name = 'cfn2dsl' 5 | s.version = Cfn2dsl::VERSION 6 | s.date = Time.now().strftime("%Y-%m-%d") 7 | s.authors = ["Kevin Yung", "Valen Gunawan", "Cam Maxwell"] 8 | s.summary = "A tool to convert CloudFormation templates into a Ruby DSL cfndsl" 9 | s.description = s.summary 10 | s.license = 'MIT' 11 | s.files = [ 12 | "lib/cfn2dsl.rb", 13 | "lib/cfn_parser.rb", 14 | "lib/cfndsl.erb", 15 | "lib/cloudformation.rb", 16 | "lib/condition.rb", 17 | "lib/intrinsic_function.rb", 18 | "lib/mapping.rb", 19 | "lib/output.rb", 20 | "lib/parameter.rb", 21 | "lib/render.rb", 22 | "lib/resource.rb", 23 | "lib/version.rb", 24 | "lib/metadata.rb", 25 | "lib/rules.rb", 26 | "lib/error.rb", 27 | ] 28 | s.bindir = 'bin' 29 | s.executables << 'cfn2dsl' 30 | s.homepage = 'https://github.com/allinwonder/cfn2dsl.git' 31 | s.email = 'jwrong@gmail.com' 32 | s.required_ruby_version = '>= 2.4.4' 33 | s.add_dependency('extlib', '0.9.16') 34 | s.add_dependency('awesome_print', '1.8.0') 35 | s.add_dependency('erubis', '2.7.0') 36 | end 37 | -------------------------------------------------------------------------------- /lib/cfn2dsl.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | 3 | require 'json' 4 | require 'yaml' 5 | require 'optparse' 6 | require 'ap' 7 | 8 | require 'cfn_parser' 9 | require 'cloudformation' 10 | require 'condition' 11 | require 'intrinsic_function' 12 | require 'mapping' 13 | require 'output' 14 | require 'parameter' 15 | require 'resource' 16 | require 'render' 17 | require 'version' 18 | require 'rules' 19 | require 'metadata' 20 | require 'error' 21 | 22 | AwesomePrint.defaults = { 23 | :indent => -2, 24 | :index => false, 25 | :sort_keys => true, 26 | :plain => true 27 | } 28 | 29 | YAML.add_domain_type('', 'Ref') { |type, val| { 'Ref' => val } } 30 | 31 | YAML.add_domain_type('', 'GetAtt') do |type, val| 32 | if val.is_a? String 33 | val = val.split('.') 34 | end 35 | { 'Fn::GetAtt' => val } 36 | end 37 | 38 | %w(Join Base64 Sub Cidr Split Select ImportValue GetAZs FindInMap And Or If Not).each do |function_name| 39 | YAML.add_domain_type('', function_name) { |type, val| { "Fn::#{function_name}" => val } } 40 | end -------------------------------------------------------------------------------- /lib/cfn_parser.rb: -------------------------------------------------------------------------------- 1 | module CfnParser 2 | 3 | def parse_cfn(cfn_hash) 4 | send(element_type(cfn_hash).to_s.snake_case, cfn_hash) 5 | end 6 | 7 | def intrinsic_function?(obj) 8 | obj.instance_of?(Hash) && 9 | obj.keys.size == 1 && 10 | (obj.keys.first =~ /^Fn::/ or obj.keys.first == "Ref") 11 | end 12 | 13 | def array(cfn_hash) 14 | return cfn_hash.map { |a| parse_cfn(a) } 15 | end 16 | 17 | def intrinsic_function(cfn_hash) 18 | fn_name = cfn_hash.keys.first 19 | values = cfn_hash[fn_name] 20 | parameters = parse_cfn(values) 21 | return IntrinsicFunction.new(fn_name, parameters) 22 | end 23 | 24 | def hash(cfn_hash) 25 | return cfn_hash.merge(cfn_hash) { |k, v| parse_cfn(v) } 26 | end 27 | 28 | def primitive(cfn_hash) 29 | cfn_hash 30 | end 31 | 32 | private 33 | def element_type(cfn_hash) 34 | return :Array if cfn_hash.instance_of?(Array) 35 | return :IntrinsicFunction if intrinsic_function?(cfn_hash) 36 | return :Hash if cfn_hash.instance_of?(Hash) 37 | return :Primitive 38 | end 39 | end 40 | 41 | -------------------------------------------------------------------------------- /lib/cfndsl.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | CloudFormation do 3 | <% if @cfn.description %> 4 | Description '<%= @cfn.description %>' 5 | <% end %> 6 | <% if @cfn.aws_template_format_version %> 7 | AWSTemplateFormatVersion '<%= @cfn.aws_template_format_version %>' 8 | <% end %> 9 | <% # Parameters %> 10 | <% 11 | if @cfn.parameters 12 | @cfn.parameters.each do |param| 13 | %> 14 | 15 | Parameter('<%= param.name %>') do 16 | <% if param.description %> 17 | Description '<%= param.description %>' 18 | <% end %> 19 | <% if param.type %> 20 | Type '<%= param.type %>' 21 | <% end %> 22 | <% if param.default 23 | default_value = param.default 24 | if param.default.respond_to? :dump 25 | default_value = '\'' + param.default + '\'' 26 | end 27 | %> 28 | Default <%= default_value %> 29 | <% end %> 30 | <% if param.allowed_values %> 31 | <% if param.allowed_values.kind_of?(Array) %> 32 | AllowedValues <%= param.allowed_values.ai.gsub('"', '\'').gsub(' ', ' ').gsub(/^\]$/, ' ]').gsub(/^\}$/, ' }') %> 33 | <% else %> 34 | AllowedValues '<%= param.allowed_values.ai.gsub('"', '\'') %>' 35 | <% end %> 36 | <% end %> 37 | <% if param.allowed_pattern %> 38 | AllowedPattern '<%= param.allowed_pattern %>' 39 | <% end %> 40 | <% if param.no_echo %> 41 | NoEcho <%= param.no_echo %> 42 | <% end %> 43 | <% if param.max_length %> 44 | MaxLength <%= param.max_length.to_i %> 45 | <% end %> 46 | <% if param.min_length %> 47 | MinLength <%= param.min_length.to_i %> 48 | <% end %> 49 | <% if param.max_value %> 50 | MaxValue <%= param.max_value %> 51 | <% end %> 52 | <% if param.min_value %> 53 | MinValue <%= param.min_value %> 54 | <% end %> 55 | <% if param.constraint_description %> 56 | ConstraintDescription '<%= param.constraint_description %>' 57 | <% end %> 58 | end 59 | <% 60 | end 61 | end 62 | %> 63 | <% # Mappings %> 64 | <% 65 | if @cfn.mappings 66 | @cfn.mappings.each do |map| 67 | %> 68 | 69 | Mapping '<%= map.name %>' '<%= map.values.ai.gsub('"', '\'') %>' 70 | <% 71 | end 72 | end 73 | %> 74 | <% # Conditions %> 75 | <% 76 | if @cfn.conditions 77 | @cfn.conditions.each do |cond| 78 | %> 79 | 80 | Condition('<%= cond.name %>', <%= cond.evaluations.ai.gsub('"', '\'').gsub(' ', ' ').gsub(/^\]/, ' ]').gsub(/^\)/, ' )') %>) 81 | <% 82 | end 83 | end 84 | %> 85 | <% # Resources %> 86 | <% 87 | @cfn.resources.each do |res| 88 | %> 89 | <% 90 | resname = res.name 91 | restype = res.type 92 | restype = restype.split('::').delete_if{|i|i=='AWS'} 93 | restype = restype.join('::').gsub('::', '_') 94 | %> 95 | <%= restype %>('<%= resname %>') do 96 | <% 97 | if res.creation_policy 98 | res.creation_policy.each_pair do |name, value| 99 | if value.instance_of? String 100 | value = '\'' + value + '\'' 101 | else 102 | value = value.ai.gsub('"', '\'') 103 | end 104 | %> 105 | CreationPolicy('<%= name %>', <%= value %>) 106 | <% 107 | end 108 | end 109 | %> 110 | <% 111 | if res.deletion_policy 112 | %> 113 | DeletionPolicy '<%= res.deletion_policy %>)' 114 | <% 115 | end 116 | %> 117 | <% 118 | if res.condition 119 | %> 120 | Condition '<%= res.condition %>' 121 | <% 122 | end 123 | %> 124 | <% 125 | if res.update_policy 126 | res.update_policy.each_pair do |name, value| 127 | if value.instance_of? String 128 | value = "'#{value}'" 129 | else 130 | value = value.ai.gsub('"', '\'').gsub('{', '').gsub('}', '').chop 131 | end 132 | %> 133 | UpdatePolicy('<%= name %>',<%= value %>) 134 | <% 135 | end 136 | end 137 | %> 138 | <% 139 | if res.metadata 140 | res.metadata.each_pair do |name, value| 141 | if value.instance_of? String 142 | value = value 143 | else 144 | value = value.ai.gsub('"', '\'') 145 | end 146 | %> 147 | Metadata '<%= name %>' '<%= value %>' 148 | <% 149 | end 150 | end 151 | %> 152 | <% 153 | if res.depends_on 154 | if res.depends_on.instance_of? String 155 | value = '\'' + res.depends_on + '\'' 156 | else 157 | value = res.depends_on.ai.gsub('"', '\'') 158 | end 159 | %> 160 | DependsOn(<%= value %>) 161 | <% 162 | end 163 | %> 164 | <% 165 | if res.properties 166 | res.properties.each_pair do |name, value| 167 | is_value_string = true 168 | if value.instance_of? String 169 | value = '\'' + value + '\'' 170 | else 171 | is_value_string = false 172 | value = value.ai.gsub('"', '\'') 173 | .gsub(' ', ' ') 174 | .gsub(/^\]$/, ' ]') 175 | .gsub(/^\}$/, ' }') 176 | .gsub(/[ ]+=>/, ' =>') 177 | end 178 | 179 | if is_value_string 180 | %> 181 | <%= name %> <%= value %> 182 | <% else %> 183 | <%= name %>(<%= value %>) 184 | <% 185 | end 186 | end 187 | end 188 | %> 189 | end 190 | <% 191 | end 192 | %> 193 | <% # Output %> 194 | <% 195 | if @cfn.outputs 196 | @cfn.outputs.each do |out| 197 | %> 198 | 199 | Output('<%= out.name %>') do 200 | <% 201 | if out.condition 202 | %> 203 | Condition('<%= out.condition %>') 204 | <% 205 | end 206 | %> 207 | <% 208 | if out.description 209 | %> 210 | Description '<%= out.description %>' 211 | <% 212 | end 213 | %> 214 | <% 215 | if out.value 216 | if out.value.instance_of? String 217 | value = '\'' + out.value + '\'' 218 | else 219 | value = out.value.ai.gsub('"', '\'') 220 | end 221 | %> 222 | Value <%= value %> 223 | <% 224 | end 225 | %> 226 | end 227 | <% 228 | end 229 | end 230 | %> 231 | end 232 | -------------------------------------------------------------------------------- /lib/cloudformation.rb: -------------------------------------------------------------------------------- 1 | require 'extlib' 2 | require 'psych' 3 | class CloudFormation 4 | 5 | ELEMENTS = [ 6 | :parameters, 7 | :resources, 8 | :conditions, 9 | :outputs, 10 | :mappings, 11 | :rules, 12 | :metadata, 13 | :description, 14 | :aws_template_format_version 15 | ] 16 | 17 | attr_reader(*ELEMENTS) 18 | 19 | def initialize(cfn_string) 20 | cfn_hash = Psych.safe_load(cfn_string) 21 | ELEMENTS.each do |e| 22 | key = e.to_s.camel_case 23 | if key =~ /^Aws/ 24 | key = key.sub(/^Aws/, "AWS") 25 | end 26 | 27 | if cfn_hash[key] 28 | attr = parse_element(e, cfn_hash[key]) 29 | instance_variable_set("@" + e.to_s, attr) 30 | end 31 | end 32 | rescue Psych::DisallowedClass => error 33 | raise YamlValueTypeError.new "Unsupported YAML value type found: only scalar values supported. #{error.message}" 34 | rescue Psych::Exception => error 35 | raise YamlSyntaxError.new "Syntax error in template. #{error.message}" 36 | end 37 | 38 | private 39 | def parse_element(elm_name, cfn_hash) 40 | function = parser(elm_name) 41 | send(function, elm_name, cfn_hash) 42 | end 43 | 44 | def parser(name) 45 | case name 46 | when :description 47 | :simple_parser 48 | when :aws_template_format_version 49 | :simple_parser 50 | else 51 | :complex_parser 52 | end 53 | end 54 | 55 | def simple_parser(name, cfn_hash) 56 | cfn_hash 57 | end 58 | 59 | def complex_parser(name, cfn_hash) 60 | elms = [] 61 | case name 62 | when :metadata 63 | cfn_hash.each_pair { |k, v| elms << Metadata.new(k, v) } 64 | when :rules 65 | cfn_hash.each_pair { |k, v| elms << Rules.new(k, v) } 66 | when :parameters 67 | cfn_hash.each_pair { |k, v| elms << Parameter.new(k, v) } 68 | when :resources 69 | cfn_hash.each_pair { |k, v| elms << Resource.new(k, v) } 70 | when :outputs 71 | cfn_hash.each_pair { |k, v| elms << Output.new(k, v) } 72 | when :mappings 73 | cfn_hash.each_pair { |k, v| elms << Mapping.new(k, v) } 74 | when :conditions 75 | cfn_hash.each_pair { |k, v| elms << Condition.new(k, v) } 76 | end 77 | return elms 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/condition.rb: -------------------------------------------------------------------------------- 1 | class Condition 2 | include CfnParser 3 | attr_reader(:name, :evaluations) 4 | 5 | def initialize(name, eval) 6 | @name = name 7 | @evaluations = parse_cfn(eval) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/error.rb: -------------------------------------------------------------------------------- 1 | class YamlSyntaxError < StandardError 2 | def initialize(msg) 3 | super 4 | end 5 | end 6 | 7 | class YamlValueTypeError < StandardError 8 | def initialize(msg) 9 | super 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/intrinsic_function.rb: -------------------------------------------------------------------------------- 1 | class IntrinsicFunction 2 | attr_reader :name, :parameters 3 | 4 | def initialize(name, parameters) 5 | @name = name 6 | @parameters = parameters 7 | end 8 | 9 | def to_s 10 | function_name = @name.delete(':').snake_case 11 | send(function_name) 12 | end 13 | 14 | alias_method :inspect, :to_s 15 | 16 | def ==(o) 17 | o.instance_of?(IntrinsicFunction) and o.name == @name and @parameters == o.parameters 18 | end 19 | 20 | private 21 | def fn_base64 22 | if @parameters.instance_of? String 23 | value = "'#{@parameters}'" 24 | else 25 | value = @parameters.ai 26 | end 27 | return "FnBase64 '#{value}'" 28 | end 29 | 30 | def fn_select 31 | return "FnSelect(#{@parameters[0]}, #{@parameters[1..@parameters.size].ai})" 32 | end 33 | 34 | def ref 35 | return "Ref('#{@parameters.gsub('\''){'\\\''}}')" 36 | end 37 | 38 | def fn_and 39 | return "FnAnd(#{@parameters.ai})" 40 | end 41 | 42 | def fn_equals 43 | if @parameters[0].instance_of? String 44 | arg1 = '\'' + @parameters[0].gsub('\''){'\\\''} + '\'' 45 | else 46 | arg1 = @parameters[0].ai 47 | end 48 | 49 | if @parameters[1].instance_of? String 50 | arg2 = '\'' + @parameters[1].gsub('\''){'\\\''} + '\'' 51 | else 52 | arg2 = @parameters[1].ai 53 | end 54 | 55 | return "FnEquals(#{arg1}, #{arg2})" 56 | end 57 | 58 | def fn_if 59 | cond_name = '\'' + @parameters[0].gsub('\''){'\\\''} + '\'' 60 | if @parameters[1].instance_of? String 61 | true_value = '\'' + @parameters[1].gsub('\''){'\\\''} + '\'' 62 | else 63 | true_value = @parameters[1].ai 64 | end 65 | 66 | if @parameters[2].instance_of? String 67 | false_value = '\'' + @parameters[2].gsub('\''){'\\\''} + '\'' 68 | else 69 | false_value = @parameters[2].ai 70 | end 71 | return "FnIf(#{cond_name}, #{true_value}, #{false_value})" 72 | end 73 | 74 | def fn_not 75 | return "FnNot(#{@parameters.ai})" 76 | end 77 | 78 | def fn_or 79 | return "FnOr(#{@parameters.ai})" 80 | end 81 | 82 | def fn_find_in_map 83 | map_name = '\'' + @parameters[0] + '\'' 84 | 85 | if @parameters[1].instance_of? String 86 | top_level_key = '\'' + @parameters[1] + '\'' 87 | else 88 | top_level_key = @parameters[1].ai 89 | end 90 | 91 | if @parameters[2].instance_of? String 92 | sec_level_key = '\'' + @parameters[2] + '\'' 93 | else 94 | sec_level_key = @parameters[2].ai 95 | end 96 | 97 | return "FnFindInMap(#{map_name}, #{top_level_key}, #{sec_level_key})" 98 | end 99 | 100 | def fn_get_att 101 | resource_name = '\'' + @parameters[0] + '\'' 102 | attr_name = '\'' + @parameters[1] + '\'' 103 | return "FnGetAtt(#{resource_name}, #{attr_name})" 104 | end 105 | 106 | def fn_join 107 | seperator = '\'' + @parameters[0].gsub('\''){'\\\''} + '\'' 108 | values = @parameters[1].ai 109 | return "FnJoin(#{seperator}, #{values})" 110 | end 111 | 112 | def fn_sub 113 | if @parameters.instance_of? String 114 | value = '"' + @parameters.gsub('"'){'\\"'} + '"' 115 | return "FnSub(#{value})" 116 | else 117 | origin = '"' + @parameters[0].gsub('"'){'\\"'} + '"' 118 | values = @parameters[1].ai 119 | return "FnSub(#{origin}, #{values})" 120 | end 121 | end 122 | 123 | def fn_split 124 | delimiter = '"' + @parameters[0].gsub('"'){'\\"'} + '"' 125 | # value = '"' + @parameters[1] + '"' 126 | if @parameters[1].instance_of? String 127 | value = '"' + @parameters[1] + '"' 128 | else 129 | value = @parameters[1].ai 130 | end 131 | return "FnSplit(#{delimiter}, #{value})" 132 | end 133 | 134 | def fn_import_value 135 | value = '"' + @parameters[0] + '"' 136 | return "FnImportValue(#{value})" 137 | end 138 | 139 | def fn_get_a_zs 140 | if @parameters.instance_of? String 141 | value = '\'' + @parameters.gsub('\''){'\\\''} + '\'' 142 | else 143 | value = @parameters.ai 144 | end 145 | 146 | return "FnGetAZs(#{value})" 147 | end 148 | 149 | def fn_cidr 150 | return "FnCidr(#{@parameters.ai})" 151 | end 152 | 153 | end 154 | -------------------------------------------------------------------------------- /lib/mapping.rb: -------------------------------------------------------------------------------- 1 | class Mapping 2 | include CfnParser 3 | attr_reader(:name, :values) 4 | 5 | def initialize(name, values) 6 | @name = name 7 | @values = parse_cfn(values) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/metadata.rb: -------------------------------------------------------------------------------- 1 | class Metadata 2 | include CfnParser 3 | attr_reader(:name, :values) 4 | 5 | def initialize(name, values) 6 | @name = name 7 | @values = parse_cfn(values) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/output.rb: -------------------------------------------------------------------------------- 1 | class Output 2 | include CfnParser 3 | 4 | ATTRIBUTES = [ 5 | :value, 6 | :condition, 7 | :description 8 | ] 9 | 10 | attr_reader(:name, *ATTRIBUTES) 11 | 12 | def initialize(name, cfn_hash) 13 | @name = name 14 | ATTRIBUTES.each {|a| attribute(a, cfn_hash)} 15 | end 16 | 17 | private 18 | def attribute(name, cfn_hash) 19 | key = name.to_s.camel_case 20 | if cfn_hash[key] 21 | value = parse_cfn(cfn_hash[key]) 22 | instance_variable_set("@" + name.to_s, value) 23 | end 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /lib/parameter.rb: -------------------------------------------------------------------------------- 1 | require 'extlib' 2 | class Parameter 3 | ELEMENTS = [ 4 | :name, 5 | :type, 6 | :default, 7 | :description, 8 | :allowed_values, 9 | :allowed_pattern, 10 | :no_echo, 11 | :max_length, 12 | :min_length, 13 | :max_value, 14 | :min_value, 15 | :constraint_description 16 | ] 17 | 18 | attr_reader(*ELEMENTS) 19 | 20 | def initialize(name, cfn_hash) 21 | @name = name 22 | ELEMENTS.each do |e| 23 | key_name = e.to_s.camel_case 24 | instance_variable_set('@' + e.to_s, cfn_hash[key_name]) if cfn_hash[key_name] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/render.rb: -------------------------------------------------------------------------------- 1 | require 'erubis' 2 | class Render 3 | 4 | attr_reader :cfn 5 | 6 | def initialize(cfn) 7 | @cfn = cfn 8 | end 9 | 10 | def cfn_to_cfndsl 11 | input = File.read("#{File.dirname(__FILE__)}/cfndsl.erb") 12 | context = { :cfn => @cfn } 13 | Erubis::Eruby.new(input).evaluate(context).to_s 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/resource.rb: -------------------------------------------------------------------------------- 1 | class Resource 2 | include CfnParser 3 | 4 | ATTRIBUTES = [ 5 | :deletion_policy, 6 | :depends_on, 7 | :metadata, 8 | :update_policy, 9 | :condition, 10 | :type, 11 | :properties, 12 | :creation_policy 13 | ] 14 | 15 | attr_reader(:name, *ATTRIBUTES) 16 | 17 | def initialize(name, cfn_hash) 18 | @name = name 19 | ATTRIBUTES.each do |a| 20 | send(attribute_type(a), cfn_hash, a.to_s) 21 | end 22 | end 23 | 24 | private 25 | def attribute_type(name) 26 | type = '' 27 | case name 28 | when name == :properties || 29 | name == :metadata || 30 | name == :update_policy || 31 | name == :depends_on || 32 | name == :creation_policy 33 | type = "complex_attribute" 34 | else 35 | type = "basic_attribute" 36 | end 37 | return type 38 | end 39 | 40 | def complex_attribute(cfn_hash, name) 41 | if cfn_hash[name.camel_case] 42 | values = cfn_hash[name.camel_case].merge do |k, v| 43 | parse_cfn(v) 44 | end 45 | instance_variable_set('@' + name, values) 46 | end 47 | end 48 | 49 | def basic_attribute(cfn_hash, name) 50 | if cfn_hash[name.camel_case] 51 | instance_variable_set('@' + name, parse_cfn(cfn_hash[name.camel_case])) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/rules.rb: -------------------------------------------------------------------------------- 1 | class Rules 2 | include CfnParser 3 | attr_reader(:name, :values) 4 | 5 | def initialize(name, values) 6 | @name = name 7 | @values = parse_cfn(values) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | module Cfn2dsl 2 | VERSION = "0.4.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/cfn_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe CfnParser do 4 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/basic-amazon-redshift-cluster.json")} 5 | let(:cfn_hash) {YAML.load(cfn_file.read)} 6 | 7 | subject { Class.new.extend described_class } 8 | 9 | it "parses cloudformation hash" do 10 | cfn = subject.parse_cfn(cfn_hash) 11 | expect(cfn['AWSTemplateFormatVersion']).to eq "2010-09-09" 12 | expect(cfn['Conditions']['IsMultiNodeCluster'].instance_of? IntrinsicFunction).to be(true) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/cloudformation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe CloudFormation do 4 | context("loads JSON files") do 5 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/load-based-auto-scaling.json")} 6 | let(:cfn_string) {cfn_file.read} 7 | let(:cfn_hash) {YAML.load(cfn_string)} 8 | 9 | subject { described_class.new(cfn_string) } 10 | 11 | it 'creates cloudformation object from cloudformation json' do 12 | expect(subject.parameters.size).to eq 5 13 | expect(subject.resources.size).to eq 9 14 | expect(subject.outputs.size).to eq 1 15 | end 16 | 17 | it 'has parameter SSHLocation with attribute AllowedPattern' do 18 | index = subject.parameters.index {|e| e.name == "SSHLocation"} 19 | expect(index).not_to be_nil 20 | expect(subject.parameters[index].allowed_pattern).to eq cfn_hash['Parameters']['SSHLocation']['AllowedPattern'] 21 | end 22 | end 23 | 24 | 25 | context("loads YAML files") do 26 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/s3bucket.yaml")} 27 | let(:cfn_string) {cfn_file.read} 28 | let(:cfn_hash) {YAML.load(cfn_string)} 29 | 30 | subject { described_class.new(cfn_string) } 31 | 32 | it 'creates cloudformation object from cloudformation yaml' do 33 | expect(subject.parameters.size).to eq 5 34 | expect(subject.resources.size).to eq 3 35 | expect(subject.outputs.size).to eq 2 36 | end 37 | 38 | it 'has parameter VPCEndpoint with attribute AllowedPattern' do 39 | index = subject.parameters.index {|e| e.name == "VPCEndpoint"} 40 | expect(index).not_to be_nil 41 | expect(subject.parameters[index].allowed_pattern).to eq cfn_hash['Parameters']['VPCEndpoint']['AllowedPattern'] 42 | end 43 | end 44 | 45 | context("does not load invalid YAML file") do 46 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/s3bucket_invalid.yaml")} 47 | let(:cfn_string) {cfn_file.read} 48 | 49 | subject { CloudFormation } 50 | it 'raises a YamlSyntaxError exception' do 51 | allow(subject).to receive(:new).with(cfn_string).and_raise(YamlSyntaxError) 52 | end 53 | end 54 | 55 | context("does not load unsafe YAML file with unsupported value type") do 56 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/s3bucket_unsafe.yaml")} 57 | let(:cfn_string) {cfn_file.read} 58 | 59 | subject { CloudFormation } 60 | it 'raises a YamlValueTypeError exception' do 61 | allow(subject).to receive(:new).with(cfn_string).and_raise(YamlValueTypeError) 62 | end 63 | end 64 | 65 | end -------------------------------------------------------------------------------- /spec/condition_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Condition do 4 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/basic-amazon-redshift-cluster.json")} 5 | let(:cfn_hash) {YAML.load(cfn_file.read)} 6 | 7 | let(:name) {"IsMultiNodeCluster"} 8 | 9 | subject{ described_class.new(name, cfn_hash["Conditions"][name]) } 10 | 11 | it "has name IsMultiNodeCluster" do 12 | expect(subject.name).to eq "IsMultiNodeCluster" 13 | end 14 | 15 | it "has evaluation Fn::Equals" do 16 | expect(subject.evaluations).to eq IntrinsicFunction.new( 17 | "Fn::Equals", 18 | [ 19 | IntrinsicFunction.new("Ref", "ClusterType"), 20 | "multi-node" 21 | ] 22 | ) 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /spec/intrinsic_function_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe IntrinsicFunction do 4 | subject { described_class.new(name, input) } 5 | 6 | shared_examples_for 'an intrinsic function' do 7 | it "creates an intrinsic function" do 8 | expect(subject.name).to eq(name) 9 | expect(subject.parameters).to eq(input) 10 | end 11 | end 12 | 13 | context 'Fn::Cidr()' do 14 | let(:name) { 'Fn::Cidr' } 15 | let(:input) { [ "192.168.0.0/24", 6, 5 ] } 16 | it_behaves_like 'an intrinsic function' 17 | end 18 | 19 | context 'Fn::Select()' do 20 | let(:name) { 'Fn::Select' } 21 | let(:input) { [0, [ "apples", "grapes", "oranges", "mangoes" ]] } 22 | 23 | it_behaves_like 'an intrinsic function' 24 | end 25 | 26 | context 'Fn::Sub(string)' do 27 | let(:name) { 'Fn::Sub' } 28 | let(:input) {'I am a ${single} string substitution'} 29 | it "creates an intrinsic function with string" do 30 | expect(subject.name).to eq(name) 31 | expect(subject.parameters).to eq(input) 32 | expect(subject.parameters).to be_instance_of String 33 | end 34 | it_behaves_like 'an intrinsic function' 35 | end 36 | 37 | context 'Fn::Sub(hash)' do 38 | let(:name) { 'Fn::Sub' } 39 | let(:input) { ['I am a ${hashkey} substitution', {'hashkey' => 'hashvalue'}] } 40 | it "creates an intrinsic function with hash" do 41 | expect(subject.name).to eq(name) 42 | expect(subject.parameters).to eq(input) 43 | expect(subject.parameters).to be_instance_of Array 44 | expect(subject.parameters[0]).to be_instance_of String 45 | expect(subject.parameters[1]).to be_instance_of Hash 46 | end 47 | it_behaves_like 'an intrinsic function' 48 | end 49 | 50 | context 'Ref("AWS::Region")' do 51 | let(:name) { 'Ref' } 52 | let(:input) { 'AWS::Region' } 53 | 54 | it_behaves_like 'an intrinsic function' 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/mapping_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Mapping do 4 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/load-based-auto-scaling.json")} 5 | let(:cfn_hash) {YAML.load(cfn_file.read)} 6 | let(:name) {"AWSInstanceType2Arch"} 7 | 8 | subject{ described_class.new(name, cfn_hash["Mappings"][name]) } 9 | 10 | it "creates mapping AWSInstanceType2Arch has key value pair m1.xlarge => Arch => 64 " do 11 | expect(subject.name).to eq "AWSInstanceType2Arch" 12 | expect(subject.values["m1.xlarge"]["Arch"]).to eq "64" 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /spec/metadata_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe CloudFormation do 4 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/Windows_Single_Server_SharePoint_Foundation.json")} 5 | let(:cfn_string) {cfn_file.read} 6 | 7 | subject { described_class.new(cfn_string) } 8 | 9 | it 'creates resources SharePointFoundation and it has metadata with name AWS::CloudFormation::Init' do 10 | r = subject.resources.find_all do |r| 11 | r.name == "SharePointFoundation" 12 | end 13 | expect(r.size).to eq 1 14 | expect(r.first.metadata.has_key?("AWS::CloudFormation::Init")).to be true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/output_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Output do 4 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/basic-amazon-redshift-cluster.json")} 5 | let(:cfn_hash) {YAML.load(cfn_file.read)} 6 | 7 | let(:name) {"ClusterEndpoint"} 8 | subject {described_class.new(name, cfn_hash['Outputs'][name])} 9 | 10 | it "has name ClusterEndpoint" do 11 | expect(subject.name).to eq "ClusterEndpoint" 12 | end 13 | 14 | # TODO: Create compare function for IntrinsicFunction 15 | it "has value of cluster endpoint and port" do 16 | expect(subject.value.instance_of? IntrinsicFunction).to be(true) 17 | expect(subject.value.name).to eq("Fn::Join") 18 | expect(subject.value.parameters.first).to eq ":" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/parameter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Parameter do 4 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/load-based-auto-scaling.json")} 5 | let(:cfn_hash) {YAML.load(cfn_file.read)} 6 | let(:name) {"InstanceType"} 7 | 8 | subject { described_class.new(name, cfn_hash['Parameters']['InstanceType'])} 9 | 10 | context("a known element") do 11 | it "includes the :default attribute with provided value" do 12 | expect(subject.default).to eq("m1.small") 13 | end 14 | end 15 | 16 | context("empty element") do 17 | it "excludes :allowed_pattern attribute" do 18 | expect(subject.allowed_pattern).to be_nil 19 | end 20 | end 21 | 22 | context("unknown element") do 23 | it "never set unknown attribute" do 24 | expect(subject.instance_variable_get('@foo')).to be_nil 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Resource do 4 | let(:cfn_file) {File.open("#{File.dirname(__FILE__)}/samples/load-based-auto-scaling.json")} 5 | let(:cfn_hash) {YAML.load(cfn_file.read)} 6 | let(:resource_name) {"NotificationTopic"} 7 | 8 | subject { described_class.new(resource_name, cfn_hash['Resources'][resource_name]) } 9 | 10 | it "has name NotificationTopic" do 11 | expect(subject.name).to eq resource_name 12 | end 13 | 14 | it "has type 'AWS::SNS::Topic'" do 15 | expect(subject.type).to eq "AWS::SNS::Topic" 16 | end 17 | 18 | it "has property Subscription" do 19 | property = subject.properties['Subscription'] 20 | expect(property).to eq [ { 21 | "Endpoint" => IntrinsicFunction.new("Ref", "OperatorEmail"), 22 | "Protocol" => "email" } 23 | ] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/samples/Windows_Single_Server_SharePoint_Foundation.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion" : "2010-09-09", 3 | 4 | "Description" : "This template creates a single server installation of Microsoft SharePoint Foundation 2010. **WARNING** This template creates Amazon EC2 Windows instance and related resources. You will be billed for the AWS resources used if you create a stack from this template. Also, you are solely responsible for complying with the license terms for the software downloaded and installed by this template. By creating a stack from this template, you are agreeing to such terms.", 5 | 6 | "Parameters" : { 7 | "KeyName" : { 8 | "Description" : "Name of an existing EC2 KeyPair", 9 | "Type" : "AWS::EC2::KeyPair::KeyName", 10 | "ConstraintDescription" : "must be the name of an existing EC2 KeyPair." 11 | }, 12 | 13 | "InstanceType" : { 14 | "Description" : "Amazon EC2 instance type", 15 | "Type" : "String", 16 | "Default" : "m4.large", 17 | "AllowedValues" : [ "t1.micro", "t2.micro", "t2.small", "t2.medium", "m1.small", "m1.medium", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "m3.medium", "m3.large", "m3.xlarge", "m3.2xlarge", "m4.large", "m4.xlarge", "m4.2xlarge", "m4.4xlarge", "m4.10xlarge", "c1.medium", "c1.xlarge", "c3.large", "c3.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge", "c4.large", "c4.xlarge", "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", "g2.2xlarge", "r3.large", "r3.xlarge", "r3.2xlarge", "r3.4xlarge", "r3.8xlarge", "i2.xlarge", "i2.2xlarge", "i2.4xlarge", "i2.8xlarge", "d2.xlarge", "d2.2xlarge", "d2.4xlarge", "d2.8xlarge", "hi1.4xlarge", "hs1.8xlarge", "cr1.8xlarge", "cc2.8xlarge", "cg1.4xlarge"] 18 | , 19 | "ConstraintDescription" : "must be a valid EC2 instance type." 20 | }, 21 | "SourceCidrForRDP" : { 22 | "Description" : "IP Cidr from which you are likely to RDP into the instances. You can add rules later by modifying the created security groups e.g. 54.32.98.160/32", 23 | "Type" : "String", 24 | "MinLength" : "9", 25 | "MaxLength" : "18", 26 | "AllowedPattern" : "^([0-9]+\\.){3}[0-9]+\\/[0-9]+$" 27 | } 28 | }, 29 | 30 | "Mappings" : { 31 | "AWSRegion2AMI" : { 32 | "us-east-1" : {"Windows2008r2" : "ami-50903a46", "Windows2012r2" : "ami-11e84107"}, 33 | "us-west-2" : {"Windows2008r2" : "ami-59fc7439", "Windows2012r2" : "ami-09f47d69"}, 34 | "us-west-1" : {"Windows2008r2" : "ami-d59fc7b5", "Windows2012r2" : "ami-052d7565"}, 35 | "eu-west-1" : {"Windows2008r2" : "ami-fd77469b", "Windows2012r2" : "ami-d3dee9b5"}, 36 | "eu-west-2" : {"Windows2008r2" : "ami-f1a1b495", "Windows2012r2" : "ami-e5b3a681"}, 37 | "eu-central-1" : {"Windows2008r2" : "ami-1ba77174", "Windows2012r2" : "ami-d029febf"}, 38 | "ap-northeast-1" : {"Windows2008r2" : "ami-6df6aa0a", "Windows2012r2" : "ami-cb7429ac"}, 39 | "ap-northeast-2" : {"Windows2008r2" : "ami-accc1fc2", "Windows2012r2" : "ami-34d4075a"}, 40 | "ap-southeast-1" : {"Windows2008r2" : "ami-9e3c8efd", "Windows2012r2" : "ami-e5a51786"}, 41 | "ap-southeast-2" : {"Windows2008r2" : "ami-30999453", "Windows2012r2" : "ami-a63934c5"}, 42 | "ap-south-1" : {"Windows2008r2" : "ami-47e39328", "Windows2012r2" : "ami-dd8cfcb2"}, 43 | "us-east-2" : {"Windows2008r2" : "ami-912105f4", "Windows2012r2" : "ami-d85773bd"}, 44 | "ca-central-1" : {"Windows2008r2" : "ami-e13a8785", "Windows2012r2" : "ami-d242ffb6"}, 45 | "sa-east-1" : {"Windows2008r2" : "ami-6631510a", "Windows2012r2" : "ami-83f594ef"}, 46 | "cn-north-1" : {"Windows2008r2" : "ami-95b365f8", "Windows2012r2" : "ami-0e885e63"} 47 | } 48 | 49 | }, 50 | 51 | "Resources" : { 52 | "SharePointFoundationSecurityGroup" : { 53 | "Type" : "AWS::EC2::SecurityGroup", 54 | "Properties" : { 55 | "GroupDescription" : "Enable HTTP and RDP", 56 | "SecurityGroupIngress" : [ 57 | {"IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"}, 58 | {"IpProtocol" : "tcp", "FromPort" : "3389", "ToPort" : "3389", "CidrIp" : { "Ref" : "SourceCidrForRDP" }} 59 | ] 60 | } 61 | }, 62 | 63 | "SharePointFoundationEIP" : { 64 | "Type" : "AWS::EC2::EIP", 65 | "Properties" : { 66 | "InstanceId" : { "Ref" : "SharePointFoundation" } 67 | } 68 | }, 69 | 70 | "SharePointFoundation": { 71 | "Type" : "AWS::EC2::Instance", 72 | "Metadata" : { 73 | "AWS::CloudFormation::Init" : { 74 | "config" : { 75 | "files" : { 76 | "c:\\cfn\\cfn-hup.conf" : { 77 | "content" : { "Fn::Join" : ["", [ 78 | "[main]\n", 79 | "stack=", { "Ref" : "AWS::StackId" }, "\n", 80 | "region=", { "Ref" : "AWS::Region" }, "\n" 81 | ]]} 82 | }, 83 | "c:\\cfn\\hooks.d\\cfn-auto-reloader.conf" : { 84 | "content": { "Fn::Join" : ["", [ 85 | "[cfn-auto-reloader-hook]\n", 86 | "triggers=post.update\n", 87 | "path=Resources.SharePointFoundation.Metadata.AWS::CloudFormation::Init\n", 88 | "action=cfn-init.exe -v -s ", { "Ref" : "AWS::StackId" }, 89 | " -r SharePointFoundation", 90 | " --region ", { "Ref" : "AWS::Region" }, "\n" 91 | ]]} 92 | }, 93 | "C:\\SharePoint\\SharePointFoundation2010.exe" : { 94 | "source" : "http://d3adzpja92utk0.cloudfront.net/SharePointFoundation.exe" 95 | } 96 | }, 97 | "commands" : { 98 | "1-extract" : { 99 | "command" : "C:\\SharePoint\\SharePointFoundation2010.exe /extract:C:\\SharePoint\\SPF2010 /quiet /log:C:\\SharePoint\\SharePointFoundation2010-extract.log" 100 | }, 101 | "2-prereq" : { 102 | "command" : "C:\\SharePoint\\SPF2010\\PrerequisiteInstaller.exe /unattended" 103 | }, 104 | "3-install" : { 105 | "command" : "C:\\SharePoint\\SPF2010\\setup.exe /config C:\\SharePoint\\SPF2010\\Files\\SetupSilent\\config.xml" 106 | } 107 | }, 108 | 109 | "services" : { 110 | "windows" : { 111 | "cfn-hup" : { 112 | "enabled" : "true", 113 | "ensureRunning" : "true", 114 | "files" : ["c:\\cfn\\cfn-hup.conf", "c:\\cfn\\hooks.d\\cfn-auto-reloader.conf"] 115 | } 116 | } 117 | } 118 | } 119 | } 120 | }, 121 | "Properties": { 122 | "InstanceType" : { "Ref" : "InstanceType" }, 123 | "ImageId" : { "Fn::FindInMap" : [ "AWSRegion2AMI", { "Ref" : "AWS::Region" }, "Windows2008r2" ]}, 124 | "SecurityGroups" : [ {"Ref" : "SharePointFoundationSecurityGroup"} ], 125 | "KeyName" : { "Ref" : "KeyName" }, 126 | "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ 127 | "" 136 | ]]}} 137 | } 138 | }, 139 | 140 | "SharePointFoundationWaitHandle" : { 141 | "Type" : "AWS::CloudFormation::WaitConditionHandle" 142 | }, 143 | 144 | "SharePointFoundationWaitCondition" : { 145 | "Type" : "AWS::CloudFormation::WaitCondition", 146 | "DependsOn" : "SharePointFoundation", 147 | "Properties" : { 148 | "Handle" : {"Ref" : "SharePointFoundationWaitHandle"}, 149 | "Timeout" : "3600" 150 | } 151 | } 152 | }, 153 | 154 | "Outputs" : { 155 | "SharePointFoundationURL" : { 156 | "Value" : { "Fn::Join" : ["", ["http://", { "Ref" : "SharePointFoundationEIP" } ]] }, 157 | "Description" : "SharePoint Team Site URL. Please retrieve Administrator password of the instance and use it to access the URL" 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /spec/samples/basic-amazon-redshift-cluster.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "AWS CloudFormation Sample Template: Redshift cluster", 5 | 6 | "Parameters": { 7 | "DatabaseName": { 8 | "Description": "The name of the first database to be created when the redshift cluster is created", 9 | "Type": "String", 10 | "Default": "defaultdb", 11 | "AllowedPattern": "([a-z]|[0-9])+" 12 | }, 13 | "ClusterType": { 14 | "Description": "The type of the cluster", 15 | "Type": "String", 16 | "Default": "single-node", 17 | "AllowedValues": [ 18 | "single-node", 19 | "multi-node" 20 | ] 21 | }, 22 | "NumberOfNodes": { 23 | "Description": "The number of compute nodes in the redshift cluster. When cluster type is specified as: 1) single-node, the NumberOfNodes parameter should be specified as 1, 2) multi-node, the NumberOfNodes parameter should be greater than 1", 24 | "Type": "Number", 25 | "Default": "1" 26 | }, 27 | "NodeType": { 28 | "Description": "The node type to be provisioned for the redshift cluster", 29 | "Type": "String", 30 | "Default": "dw2.large" 31 | }, 32 | "MasterUsername": { 33 | "Description": "The user name associated with the master user account for the redshift cluster that is being created", 34 | "Type": "String", 35 | "Default": "defaultuser", 36 | "AllowedPattern": "([a-z])([a-z]|[0-9])*", 37 | "NoEcho": "true" 38 | }, 39 | "MasterUserPassword": { 40 | "Description": "The password associated with the master user account for the redshift cluster that is being created. ", 41 | "Type": "String", 42 | "NoEcho": "true" 43 | } 44 | }, 45 | "Conditions": { 46 | "IsMultiNodeCluster": { 47 | "Fn::Equals": [ 48 | { 49 | "Ref": "ClusterType" 50 | }, 51 | "multi-node" 52 | ] 53 | } 54 | }, 55 | "Resources": { 56 | "RedshiftCluster": { 57 | "Type": "AWS::Redshift::Cluster", 58 | "Properties": { 59 | "ClusterType": { 60 | "Ref": "ClusterType" 61 | }, 62 | "NumberOfNodes": { 63 | "Fn::If": [ 64 | "IsMultiNodeCluster", 65 | { 66 | "Ref": "NumberOfNodes" 67 | }, 68 | { 69 | "Ref": "AWS::NoValue" 70 | } 71 | ] 72 | }, 73 | "NodeType": { 74 | "Ref": "NodeType" 75 | }, 76 | "DBName": { 77 | "Ref": "DatabaseName" 78 | }, 79 | "MasterUsername": { 80 | "Ref": "MasterUsername" 81 | }, 82 | "MasterUserPassword": { 83 | "Ref": "MasterUserPassword" 84 | }, 85 | "ClusterParameterGroupName": { 86 | "Ref": "RedshiftClusterParameterGroup" 87 | } 88 | }, 89 | "DeletionPolicy": "Snapshot" 90 | }, 91 | "RedshiftClusterParameterGroup": { 92 | "Type": "AWS::Redshift::ClusterParameterGroup", 93 | "Properties": { 94 | "Description": "Cluster parameter group", 95 | "ParameterGroupFamily": "redshift-1.0", 96 | "Parameters": [ 97 | { 98 | "ParameterName": "enable_user_activity_logging", 99 | "ParameterValue": "true" 100 | } 101 | ] 102 | } 103 | } 104 | }, 105 | "Outputs": { 106 | "ClusterEndpoint": { 107 | "Value": { 108 | "Fn::Join": [ 109 | ":", 110 | [ 111 | { 112 | "Fn::GetAtt": [ 113 | "RedshiftCluster", 114 | "Endpoint.Address" 115 | ] 116 | }, 117 | { 118 | "Fn::GetAtt": [ 119 | "RedshiftCluster", 120 | "Endpoint.Port" 121 | ] 122 | } 123 | ] 124 | ] 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /spec/samples/load-based-auto-scaling.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion" : "2010-09-09", 3 | 4 | "Description" : "AWS CloudFormation Sample Template AutoScalingMultiAZWithNotifications: Create a multi-az, load balanced and Auto Scaled sample web site running on an Apache Web Serever with PHP. The application is configured to span all Availability Zones in the region and is Auto-Scaled based on the CPU utilization of the web servers. Notifications will be sent to the operator email address on scaling events. The instances are load balanced with a simple health check against the default web page. The web site is available on port 80, however, the instances can be configured to listen on any port (8888 by default). **WARNING** This template creates one or more Amazon EC2 instances and an Elastic Load Balancer. You will be billed for the AWS resources used if you create a stack from this template.", 5 | 6 | "Parameters" : { 7 | "InstanceType" : { 8 | "Description" : "WebServer EC2 instance type", 9 | "Type" : "String", 10 | "Default" : "m1.small", 11 | "AllowedValues" : [ "t1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m2.xlarge","m2.2xlarge","m2.4xlarge","m3.xlarge","m3.2xlarge","c1.medium","c1.xlarge","cc1.4xlarge","cc2.8xlarge","cg1.4xlarge"], 12 | "ConstraintDescription" : "must be a valid EC2 instance type." 13 | }, 14 | "WebServerPort" : { 15 | "Description" : "The TCP port for the Web Server", 16 | "Type" : "Number", 17 | "Default" : "8888" 18 | }, 19 | "OperatorEmail": { 20 | "Description": "Email address to notify if there are any scaling operations", 21 | "Type": "String" 22 | }, 23 | "KeyName" : { 24 | "Description" : "The EC2 Key Pair to allow SSH access to the instances", 25 | "Type" : "String" 26 | }, 27 | "SSHLocation" : { 28 | "Description" : "The IP address range that can be used to SSH to the EC2 instances", 29 | "Type": "String", 30 | "MinLength": "9", 31 | "MaxLength": "18", 32 | "Default": "0.0.0.0/0", 33 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 34 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 35 | } 36 | }, 37 | 38 | "Mappings" : { 39 | "AWSInstanceType2Arch" : { 40 | "t1.micro" : { "Arch" : "64" }, 41 | "m1.small" : { "Arch" : "64" }, 42 | "m1.medium" : { "Arch" : "64" }, 43 | "m1.large" : { "Arch" : "64" }, 44 | "m1.xlarge" : { "Arch" : "64" }, 45 | "m2.xlarge" : { "Arch" : "64" }, 46 | "m2.2xlarge" : { "Arch" : "64" }, 47 | "m2.4xlarge" : { "Arch" : "64" }, 48 | "m3.xlarge" : { "Arch" : "64" }, 49 | "m3.2xlarge" : { "Arch" : "64" }, 50 | "c1.medium" : { "Arch" : "64" }, 51 | "c1.xlarge" : { "Arch" : "64" } 52 | }, 53 | 54 | "AWSRegionArch2AMI" : { 55 | "us-east-1" : { "32" : "ami-aba768c2", "64" : "ami-81a768e8" }, 56 | "us-west-1" : { "32" : "ami-458fd300", "64" : "ami-b18ed2f4" }, 57 | "us-west-2" : { "32" : "ami-fcff72cc", "64" : "ami-feff72ce" }, 58 | "eu-west-1" : { "32" : "ami-018bb975", "64" : "ami-998bb9ed" }, 59 | "sa-east-1" : { "32" : "ami-a039e6bd", "64" : "ami-a239e6bf" }, 60 | "ap-southeast-1" : { "32" : "ami-425a2010", "64" : "ami-5e5a200c" }, 61 | "ap-southeast-2" : { "32" : "ami-b3990e89", "64" : "ami-bd990e87" }, 62 | "ap-northeast-1" : { "32" : "ami-7871c579", "64" : "ami-7671c577" } 63 | } 64 | }, 65 | 66 | "Resources" : { 67 | "NotificationTopic": { 68 | "Type": "AWS::SNS::Topic", 69 | "Properties": { 70 | "Subscription": [ { 71 | "Endpoint": { "Ref": "OperatorEmail" }, 72 | "Protocol": "email" } ] 73 | } 74 | }, 75 | 76 | "WebServerGroup" : { 77 | "Type" : "AWS::AutoScaling::AutoScalingGroup", 78 | "Properties" : { 79 | "AvailabilityZones" : { "Fn::GetAZs" : ""}, 80 | "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, 81 | "MinSize" : "1", 82 | "MaxSize" : "3", 83 | "LoadBalancerNames" : [ { "Ref" : "ElasticLoadBalancer" } ], 84 | "NotificationConfiguration" : { 85 | "TopicARN" : { "Ref" : "NotificationTopic" }, 86 | "NotificationTypes" : [ "autoscaling:EC2_INSTANCE_LAUNCH","autoscaling:EC2_INSTANCE_LAUNCH_ERROR","autoscaling:EC2_INSTANCE_TERMINATE", "autoscaling:EC2_INSTANCE_TERMINATE_ERROR"] 87 | } 88 | } 89 | }, 90 | 91 | "LaunchConfig" : { 92 | "Type" : "AWS::AutoScaling::LaunchConfiguration", 93 | "Properties" : { 94 | "KeyName" : { "Ref" : "KeyName" }, 95 | "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, 96 | { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, 97 | "Arch" ] } ] }, 98 | "UserData" : { "Fn::Base64" : { "Ref" : "WebServerPort" }}, 99 | "SecurityGroups" : [ { "Ref" : "InstanceSecurityGroup" } ], 100 | "InstanceType" : { "Ref" : "InstanceType" } 101 | } 102 | }, 103 | 104 | "WebServerScaleUpPolicy" : { 105 | "Type" : "AWS::AutoScaling::ScalingPolicy", 106 | "Properties" : { 107 | "AdjustmentType" : "ChangeInCapacity", 108 | "AutoScalingGroupName" : { "Ref" : "WebServerGroup" }, 109 | "Cooldown" : "60", 110 | "ScalingAdjustment" : "1" 111 | } 112 | }, 113 | "WebServerScaleDownPolicy" : { 114 | "Type" : "AWS::AutoScaling::ScalingPolicy", 115 | "Properties" : { 116 | "AdjustmentType" : "ChangeInCapacity", 117 | "AutoScalingGroupName" : { "Ref" : "WebServerGroup" }, 118 | "Cooldown" : "60", 119 | "ScalingAdjustment" : "-1" 120 | } 121 | }, 122 | 123 | "CPUAlarmHigh": { 124 | "Type": "AWS::CloudWatch::Alarm", 125 | "Properties": { 126 | "AlarmDescription": "Scale-up if CPU > 90% for 10 minutes", 127 | "MetricName": "CPUUtilization", 128 | "Namespace": "AWS/EC2", 129 | "Statistic": "Average", 130 | "Period": "300", 131 | "EvaluationPeriods": "2", 132 | "Threshold": "90", 133 | "AlarmActions": [ { "Ref": "WebServerScaleUpPolicy" } ], 134 | "Dimensions": [ 135 | { 136 | "Name": "AutoScalingGroupName", 137 | "Value": { "Ref": "WebServerGroup" } 138 | } 139 | ], 140 | "ComparisonOperator": "GreaterThanThreshold" 141 | } 142 | }, 143 | "CPUAlarmLow": { 144 | "Type": "AWS::CloudWatch::Alarm", 145 | "Properties": { 146 | "AlarmDescription": "Scale-down if CPU < 70% for 10 minutes", 147 | "MetricName": "CPUUtilization", 148 | "Namespace": "AWS/EC2", 149 | "Statistic": "Average", 150 | "Period": "300", 151 | "EvaluationPeriods": "2", 152 | "Threshold": "70", 153 | "AlarmActions": [ { "Ref": "WebServerScaleDownPolicy" } ], 154 | "Dimensions": [ 155 | { 156 | "Name": "AutoScalingGroupName", 157 | "Value": { "Ref": "WebServerGroup" } 158 | } 159 | ], 160 | "ComparisonOperator": "LessThanThreshold" 161 | } 162 | }, 163 | 164 | "ElasticLoadBalancer" : { 165 | "Type" : "AWS::ElasticLoadBalancing::LoadBalancer", 166 | "Properties" : { 167 | "AvailabilityZones" : { "Fn::GetAZs" : "" }, 168 | "Listeners" : [ { 169 | "LoadBalancerPort" : "80", 170 | "InstancePort" : { "Ref" : "WebServerPort" }, 171 | "Protocol" : "HTTP" 172 | } ], 173 | "HealthCheck" : { 174 | "Target" : { "Fn::Join" : [ "", ["HTTP:", { "Ref" : "WebServerPort" }, "/"]]}, 175 | "HealthyThreshold" : "3", 176 | "UnhealthyThreshold" : "5", 177 | "Interval" : "30", 178 | "Timeout" : "5" 179 | } 180 | } 181 | }, 182 | 183 | "InstanceSecurityGroup" : { 184 | "Type" : "AWS::EC2::SecurityGroup", 185 | "Properties" : { 186 | "GroupDescription" : "Enable SSH access and HTTP from the load balancer only", 187 | "SecurityGroupIngress" : [ { 188 | "IpProtocol" : "tcp", 189 | "FromPort" : "22", 190 | "ToPort" : "22", 191 | "CidrIp" : { "Ref" : "SSHLocation"} 192 | }, 193 | { 194 | "IpProtocol" : "tcp", 195 | "FromPort" : { "Ref" : "WebServerPort" }, 196 | "ToPort" : { "Ref" : "WebServerPort" }, 197 | "SourceSecurityGroupOwnerId" : {"Fn::GetAtt" : ["ElasticLoadBalancer", "SourceSecurityGroup.OwnerAlias"]}, 198 | "SourceSecurityGroupName" : {"Fn::GetAtt" : ["ElasticLoadBalancer", "SourceSecurityGroup.GroupName"]} 199 | } ] 200 | } 201 | } 202 | }, 203 | 204 | "Outputs" : { 205 | "URL" : { 206 | "Description" : "The URL of the website", 207 | "Value" : { "Fn::Join" : [ "", [ "http://", { "Fn::GetAtt" : [ "ElasticLoadBalancer", "DNSName" ]}]]} 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /spec/samples/s3bucket.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: S3 Storage integrated with KMS and IAM 3 | Parameters: 4 | ProvisioningRoleID: 5 | Type: String 6 | Default: AROAABCDEFGHIJKLMNOP 7 | Description: IAM RoleID to be allowed to administer KMS Key and access S3 8 | AllowPattern: "AROA[A-Z0-9]{17}" 9 | InstanceRoleID: 10 | Type: String 11 | Default: AROA1234567890123456 12 | Description: IAM RoleID of instance profile using the KMS Key and access S3 13 | AllowPattern: "AROA[A-Z0-9]{17}" 14 | LoggingBucket: 15 | Type: String 16 | Description: S3 Bucket where access logs from new S3 bucket will be sent 17 | VPCEndpoint: 18 | Type: String 19 | Default: vpce-1234abcd5678ef90 20 | Description: VPC Endpoint ID 21 | AllowPattern: "vpce-[a-f0-9]{8,16}" 22 | BucketName: 23 | Type: String 24 | Default: f4c8e474d09b 25 | Description: Hexadecimal string for bucket name 26 | AllowPattern: "[a-f0-9]{16}" 27 | 28 | Resources: 29 | KMSKey: 30 | Type: "AWS::KMS::Key" 31 | Properties: 32 | Description: 'KMS Key for encrypting S3 Bucket' 33 | Enabled: True 34 | EnableKeyRotation: True 35 | KeyPolicy: 36 | Version: '2012-10-17' 37 | Id: KMS Key Access 38 | Statement: 39 | - Sid: DenyDelete 40 | Effect: Deny 41 | Principal: '*' 42 | Action: 43 | - 'kms:ScheduleKeyDeletion' 44 | - 'kms:Delete*' 45 | Resource: 46 | - '*' 47 | - Sid: DenyKeyAccess 48 | Effect: Deny 49 | Principal: '*' 50 | Action: 51 | - 'kms:*' 52 | Resource: 53 | - '*' 54 | Condition: 55 | StringNotLike: 56 | 'aws:userId': 57 | - !Sub "${ProvisioningRoleID}:*" 58 | 59 | - Sid: AllowAccessForKeyAdministrator 60 | Effect: Allow 61 | Principal: '*' 62 | Action: 63 | - 'kms:CreateKey' 64 | - 'kms:CreateAlias' 65 | - 'kms:CreateGrant' 66 | - 'kms:Describe*' 67 | - 'kms:Enable*' 68 | - 'kms:List*' 69 | - 'kms:Put*' 70 | - 'kms:Update*' 71 | - 'kms:Revoke*' 72 | - 'kms:Disable*' 73 | - 'kms:Get*' 74 | - 'kms:TagResource' 75 | - 'kms:UntagResource' 76 | - 'kms:CancelKeyDeletion' 77 | - 'kms:GenerateDataKey*' 78 | Resource: 79 | - '*' 80 | Condition: 81 | StringLike: 82 | 'aws:userId': 83 | - !Sub "${ProvisioningRoleID}:*" 84 | 85 | - Sid: AllowUseOftheKey 86 | Effect: Allow 87 | Principal: '*' 88 | Action: 89 | - "kms:Encrypt" 90 | - 'kms:Decrypt' 91 | - 'kms:DescribeKey' 92 | - 'kms:GenerateDataKey*' 93 | Resource: 94 | - '*' 95 | Condition: 96 | StringLike: 97 | 'aws:userId': 98 | - !Sub "${InstanceRoleID}:*" 99 | 100 | BucketPolicy: 101 | Type: "AWS::S3::BucketPolicy" 102 | DependsOn: Bucket 103 | Properties: 104 | Bucket: !Ref Bucket 105 | PolicyDocument: 106 | Version: '2012-10-17' 107 | Statement: 108 | - Sid: DenyHTTPAccess 109 | Effect: Deny 110 | Principal: "*" 111 | Action: 112 | - 's3:*' 113 | Resource: 114 | - !GetAtt Bucket.Arn 115 | - !Sub "${Bucket.Arn}/*" 116 | Condition: 117 | Bool: 118 | aws:SecureTransport: 119 | - false 120 | 121 | - Sid: DenyIncorrectEncryptionHeader 122 | Effect: Deny 123 | Principal: "*" 124 | Action: 125 | - 's3:PutObject' 126 | Resource: 127 | - !GetAtt Bucket.Arn 128 | - !Sub "${Bucket.Arn}/*" 129 | Condition: 130 | StringNotEquals: 131 | s3:x-amz-server-side-encryption: 132 | - aws:kms 133 | 134 | - Sid: DenyUnEncryptedObjectUploads 135 | Effect: Deny 136 | Principal: "*" 137 | Action: 138 | - 's3:PutObject' 139 | Resource: 140 | - !GetAtt Bucket.Arn 141 | - !Sub "${Bucket.Arn}/*" 142 | Condition: 143 | "Null": 144 | s3:x-amz-server-side-encryption: 145 | - true 146 | 147 | - Sid: DenyAccessIfSpecificKMSKeyIsNotUsed 148 | Effect: Deny 149 | Principal: '*' 150 | Action: 151 | - 's3:PutObject' 152 | Resource: 153 | - !GetAtt Bucket.Arn 154 | - !Sub "${Bucket.Arn}/*" 155 | Condition: 156 | StringNotLikeIfExists: 157 | s3:x-amz-server-side-encryption-aws-kms-key-id: 158 | - !GetAtt KMSKey.Arn 159 | 160 | - Sid: DenyDelete 161 | Effect: Deny 162 | Principal: "*" 163 | Action: 164 | - 's3:Delete*' 165 | Resource: 166 | - !GetAtt Bucket.Arn 167 | - !Sub "${Bucket.Arn}/*" 168 | 169 | - Sid: DenyAllExceptConnectAndOthersViaVPCE 170 | Effect: Deny 171 | Principal: '*' 172 | Action: 173 | - 's3:*' 174 | Resource: 175 | - !GetAtt Bucket.Arn 176 | - !Sub "${Bucket.Arn}/*" 177 | Condition: 178 | StringNotEquals: 179 | aws:sourceVpce: !Sub "${VPCEndpoint}" 180 | 181 | - Sid: AllowObjectReadWrite 182 | Effect: Allow 183 | Principal: '*' 184 | Action: 185 | - 's3:PutObject*' 186 | - 's3:Get*' 187 | - 's3:List*' 188 | Resource: 189 | - !GetAtt Bucket.Arn 190 | - !Sub "${Bucket.Arn}/*" 191 | Condition: 192 | StringLike: 193 | 'aws:userId': 194 | - !Sub "${InstanceRoleID}:*" 195 | 196 | - Sid: AllowBucketConfiguration 197 | Effect: Allow 198 | Principal: '*' 199 | Action: 200 | - 's3:*' 201 | Resource: 202 | - !GetAtt Bucket.Arn 203 | - !Sub "${Bucket.Arn}/*" 204 | Condition: 205 | StringLike: 206 | 'aws:userId': 207 | - !Sub "${ProvisioningRoleID}:*" 208 | 209 | Bucket: 210 | Type: AWS::S3::Bucket 211 | Properties: 212 | BucketName: !Sub "${BucketName}" 213 | BucketEncryption: 214 | ServerSideEncryptionConfiguration: 215 | - ServerSideEncryptionByDefault: 216 | KMSMasterKeyID: !GetAtt KMSKey.Arn 217 | SSEAlgorithm: "aws:kms" 218 | PublicAccessBlockConfiguration: 219 | BlockPublicAcls: true 220 | BlockPublicPolicy: true 221 | IgnorePublicAcls: true 222 | RestrictPublicBuckets: true 223 | LoggingConfiguration: 224 | DestinationBucketName: !Sub "${LoggingBucket}" 225 | LogFilePrefix: !Sub "S3logs/${AWS::AccountId}/${BucketName}/" 226 | VersioningConfiguration: 227 | Status: Enabled 228 | 229 | Outputs: 230 | BucketName: 231 | Description: Bucket Name 232 | Value: !Ref Bucket 233 | 234 | BucketName: 235 | Description: Bucket Arn 236 | Value: !GetAtt Bucket.Arn 237 | 238 | KMSKey: 239 | Description: KMS Key Id 240 | Value: !Ref KMSKey 241 | 242 | -------------------------------------------------------------------------------- /spec/samples/s3bucket_invalid.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: S3 Storage integrated with KMS and IAM 3 | Parameters: 4 | ProvisioningRoleID: 5 | Type: String 6 | Default: AROAABCDEFGHIJKLMNOP 7 | Description: IAM RoleID to be allowed to administer KMS Key and access S3 8 | AllowPattern: "AROA[A-Z0-9]{17}" 9 | InstanceRoleID: 10 | Type: String 11 | Default: AROA1234567890123456 12 | Description: IAM RoleID of instance profile using the KMS Key and access S3 13 | AllowPattern: "AROA[A-Z0-9]{17}" 14 | LoggingBucket: 15 | Type: String 16 | Description: S3 Bucket where access logs from new S3 bucket will be sent 17 | VPCEndpoint: 18 | Type: String 19 | Default: vpce-1234abcd5678ef90 20 | Description: VPC Endpoint ID 21 | AllowPattern: "vpce-[a-f0-9]{8,16}" 22 | BucketName: 23 | Type: String 24 | Default: f4c8e474d09b 25 | Description: Hexadecimal string for bucket name 26 | AllowPattern: "[a-f0-9]{16}" 27 | 28 | Resources: 29 | KMSKey: 30 | Type: "AWS::KMS::Key" 31 | Properties: 32 | Description: 'KMS Key for encrypting S3 Bucket' 33 | Enabled: True 34 | EnableKeyRotation 35 | KeyPolicy: 36 | Version: '2012-10-17' 37 | Id: KMS Key Access 38 | Statement: 39 | - Sid: DenyDelete 40 | Effect: Deny 41 | Principal: '*' 42 | Action: 43 | - 'kms:ScheduleKeyDeletion' 44 | - 'kms:Delete*' 45 | Resource: 46 | - '*' 47 | - Sid: DenyKeyAccess 48 | Effect: Deny 49 | Principal: '*' 50 | Action: 51 | - 'kms:*' 52 | Resource: 53 | - '*' 54 | Condition: 55 | StringNotLike: 56 | 'aws:userId': 57 | - !Sub "${ProvisioningRoleID}:*" 58 | 59 | - Sid: AllowAccessForKeyAdministrator 60 | Effect: Allow 61 | Principal: '*' 62 | Action: 63 | - 'kms:CreateKey' 64 | - 'kms:CreateAlias' 65 | - 'kms:CreateGrant' 66 | - 'kms:Describe*' 67 | - 'kms:Enable*' 68 | - 'kms:List*' 69 | - 'kms:Put*' 70 | - 'kms:Update*' 71 | - 'kms:Revoke*' 72 | - 'kms:Disable*' 73 | - 'kms:Get*' 74 | - 'kms:TagResource' 75 | - 'kms:UntagResource' 76 | - 'kms:CancelKeyDeletion' 77 | - 'kms:GenerateDataKey*' 78 | Resource: 79 | - '*' 80 | Condition: 81 | StringLike: 82 | 'aws:userId': 83 | - !Sub "${ProvisioningRoleID}:*" 84 | 85 | - Sid: AllowUseOftheKey 86 | Effect: Allow 87 | Principal: '*' 88 | Action: 89 | - "kms:Encrypt" 90 | - 'kms:Decrypt' 91 | - 'kms:DescribeKey' 92 | - 'kms:GenerateDataKey*' 93 | Resource: 94 | - '*' 95 | Condition: 96 | StringLike: 97 | 'aws:userId': 98 | - !Sub "${InstanceRoleID}:*" 99 | 100 | BucketPolicy: 101 | Type: "AWS::S3::BucketPolicy" 102 | DependsOn: Bucket 103 | Properties: 104 | Bucket: !Ref Bucket 105 | PolicyDocument: 106 | Version: '2012-10-17' 107 | Statement: 108 | - Sid: DenyHTTPAccess 109 | Effect: Deny 110 | Principal: "*" 111 | Action: 112 | - 's3:*' 113 | Resource: 114 | - !GetAtt Bucket.Arn 115 | - !Sub "${Bucket.Arn}/*" 116 | Condition: 117 | Bool: 118 | aws:SecureTransport: 119 | - false 120 | 121 | - Sid: DenyIncorrectEncryptionHeader 122 | Effect: Deny 123 | Principal: "*" 124 | Action: 125 | - 's3:PutObject' 126 | Resource: 127 | - !GetAtt Bucket.Arn 128 | - !Sub "${Bucket.Arn}/*" 129 | Condition: 130 | StringNotEquals: 131 | s3:x-amz-server-side-encryption: 132 | - aws:kms 133 | 134 | - Sid: DenyUnEncryptedObjectUploads 135 | Effect: Deny 136 | Principal: "*" 137 | Action: 138 | - 's3:PutObject' 139 | Resource: 140 | - !GetAtt Bucket.Arn 141 | - !Sub "${Bucket.Arn}/*" 142 | Condition: 143 | "Null": 144 | s3:x-amz-server-side-encryption: 145 | - true 146 | 147 | - Sid: DenyAccessIfSpecificKMSKeyIsNotUsed 148 | Effect: Deny 149 | Principal: '*' 150 | Action: 151 | - 's3:PutObject' 152 | Resource: 153 | - !GetAtt Bucket.Arn 154 | - !Sub "${Bucket.Arn}/*" 155 | Condition: 156 | StringNotLikeIfExists: 157 | s3:x-amz-server-side-encryption-aws-kms-key-id: 158 | - !GetAtt KMSKey.Arn 159 | 160 | - Sid: DenyDelete 161 | Effect: Deny 162 | Principal: "*" 163 | Action: 164 | - 's3:Delete*' 165 | Resource: 166 | - !GetAtt Bucket.Arn 167 | - !Sub "${Bucket.Arn}/*" 168 | 169 | - Sid: DenyAllExceptConnectAndOthersViaVPCE 170 | Effect: Deny 171 | Principal: '*' 172 | Action: 173 | - 's3:*' 174 | Resource: 175 | - !GetAtt Bucket.Arn 176 | - !Sub "${Bucket.Arn}/*" 177 | Condition: 178 | StringNotEquals: 179 | aws:sourceVpce: !Sub "${VPCEndpoint}" 180 | 181 | - Sid: AllowObjectReadWrite 182 | Effect: Allow 183 | Principal: '*' 184 | Action: 185 | - 's3:PutObject*' 186 | - 's3:Get*' 187 | - 's3:List*' 188 | Resource: 189 | - !GetAtt Bucket.Arn 190 | - !Sub "${Bucket.Arn}/*" 191 | Condition: 192 | StringLike: 193 | 'aws:userId': 194 | - !Sub "${InstanceRoleID}:*" 195 | 196 | - Sid: AllowBucketConfiguration 197 | Effect: Allow 198 | Principal: '*' 199 | Action: 200 | - 's3:*' 201 | Resource: 202 | - !GetAtt Bucket.Arn 203 | - !Sub "${Bucket.Arn}/*" 204 | Condition: 205 | StringLike: 206 | 'aws:userId': 207 | - !Sub "${ProvisioningRoleID}:*" 208 | 209 | Bucket: 210 | Type: AWS::S3::Bucket 211 | Properties: 212 | BucketName: !Sub "${BucketName}" 213 | BucketEncryption: 214 | ServerSideEncryptionConfiguration: 215 | - ServerSideEncryptionByDefault: 216 | KMSMasterKeyID: !GetAtt KMSKey.Arn 217 | SSEAlgorithm: "aws:kms" 218 | PublicAccessBlockConfiguration: 219 | BlockPublicAcls: true 220 | BlockPublicPolicy: true 221 | IgnorePublicAcls: true 222 | RestrictPublicBuckets: true 223 | LoggingConfiguration: 224 | DestinationBucketName: !Sub "${LoggingBucket}" 225 | LogFilePrefix: !Sub "S3logs/${AWS::AccountId}/${BucketName}/" 226 | VersioningConfiguration: 227 | Status: Enabled 228 | 229 | Outputs: 230 | BucketName: 231 | Description: Bucket Name 232 | Value: !Ref Bucket 233 | 234 | BucketName: 235 | Description: Bucket Arn 236 | Value: !GetAtt Bucket.Arn 237 | 238 | KMSKey: 239 | Description: KMS Key Id 240 | Value: !Ref KMSKey 241 | 242 | -------------------------------------------------------------------------------- /spec/samples/s3bucket_unsafe.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: S3 Storage integrated with KMS and IAM 3 | Parameters: 4 | ProvisioningRoleID: 5 | Type: String 6 | Default: AROAABCDEFGHIJKLMNOP 7 | Description: IAM RoleID to be allowed to administer KMS Key and access S3 8 | AllowPattern: "AROA[A-Z0-9]{17}" 9 | InstanceRoleID: 10 | Type: String 11 | Default: AROA1234567890123456 12 | Description: IAM RoleID of instance profile using the KMS Key and access S3 13 | AllowPattern: "AROA[A-Z0-9]{17}" 14 | LoggingBucket: 15 | Type: String 16 | Description: S3 Bucket where access logs from new S3 bucket will be sent 17 | VPCEndpoint: 18 | Type: String 19 | Default: vpce-1234abcd5678ef90 20 | Description: VPC Endpoint ID 21 | AllowPattern: "vpce-[a-f0-9]{8,16}" 22 | BucketName: 23 | Type: String 24 | Default: f4c8e474d09b 25 | Description: Hexadecimal string for bucket name 26 | AllowPattern: "[a-f0-9]{16}" 27 | 28 | Resources: 29 | KMSKey: 30 | Type: "AWS::KMS::Key" 31 | Properties: 32 | Description: 'KMS Key for encrypting S3 Bucket' 33 | Enabled: True 34 | EnableKeyRotation: True 35 | KeyPolicy: 36 | Version: 2012-10-17 37 | Id: KMS Key Access 38 | Statement: 39 | - Sid: DenyDelete 40 | Effect: Deny 41 | Principal: '*' 42 | Action: 43 | - 'kms:ScheduleKeyDeletion' 44 | - 'kms:Delete*' 45 | Resource: 46 | - '*' 47 | - Sid: DenyKeyAccess 48 | Effect: Deny 49 | Principal: '*' 50 | Action: 51 | - 'kms:*' 52 | Resource: 53 | - '*' 54 | Condition: 55 | StringNotLike: 56 | 'aws:userId': 57 | - !Sub "${ProvisioningRoleID}:*" 58 | 59 | - Sid: AllowAccessForKeyAdministrator 60 | Effect: Allow 61 | Principal: '*' 62 | Action: 63 | - 'kms:CreateKey' 64 | - 'kms:CreateAlias' 65 | - 'kms:CreateGrant' 66 | - 'kms:Describe*' 67 | - 'kms:Enable*' 68 | - 'kms:List*' 69 | - 'kms:Put*' 70 | - 'kms:Update*' 71 | - 'kms:Revoke*' 72 | - 'kms:Disable*' 73 | - 'kms:Get*' 74 | - 'kms:TagResource' 75 | - 'kms:UntagResource' 76 | - 'kms:CancelKeyDeletion' 77 | - 'kms:GenerateDataKey*' 78 | Resource: 79 | - '*' 80 | Condition: 81 | StringLike: 82 | 'aws:userId': 83 | - !Sub "${ProvisioningRoleID}:*" 84 | 85 | - Sid: AllowUseOftheKey 86 | Effect: Allow 87 | Principal: '*' 88 | Action: 89 | - "kms:Encrypt" 90 | - 'kms:Decrypt' 91 | - 'kms:DescribeKey' 92 | - 'kms:GenerateDataKey*' 93 | Resource: 94 | - '*' 95 | Condition: 96 | StringLike: 97 | 'aws:userId': 98 | - !Sub "${InstanceRoleID}:*" 99 | 100 | BucketPolicy: 101 | Type: "AWS::S3::BucketPolicy" 102 | DependsOn: Bucket 103 | Properties: 104 | Bucket: !Ref Bucket 105 | PolicyDocument: 106 | Version: '2012-10-17' 107 | Statement: 108 | - Sid: DenyHTTPAccess 109 | Effect: Deny 110 | Principal: "*" 111 | Action: 112 | - 's3:*' 113 | Resource: 114 | - !GetAtt Bucket.Arn 115 | - !Sub "${Bucket.Arn}/*" 116 | Condition: 117 | Bool: 118 | aws:SecureTransport: 119 | - false 120 | 121 | - Sid: DenyIncorrectEncryptionHeader 122 | Effect: Deny 123 | Principal: "*" 124 | Action: 125 | - 's3:PutObject' 126 | Resource: 127 | - !GetAtt Bucket.Arn 128 | - !Sub "${Bucket.Arn}/*" 129 | Condition: 130 | StringNotEquals: 131 | s3:x-amz-server-side-encryption: 132 | - aws:kms 133 | 134 | - Sid: DenyUnEncryptedObjectUploads 135 | Effect: Deny 136 | Principal: "*" 137 | Action: 138 | - 's3:PutObject' 139 | Resource: 140 | - !GetAtt Bucket.Arn 141 | - !Sub "${Bucket.Arn}/*" 142 | Condition: 143 | "Null": 144 | s3:x-amz-server-side-encryption: 145 | - true 146 | 147 | - Sid: DenyAccessIfSpecificKMSKeyIsNotUsed 148 | Effect: Deny 149 | Principal: '*' 150 | Action: 151 | - 's3:PutObject' 152 | Resource: 153 | - !GetAtt Bucket.Arn 154 | - !Sub "${Bucket.Arn}/*" 155 | Condition: 156 | StringNotLikeIfExists: 157 | s3:x-amz-server-side-encryption-aws-kms-key-id: 158 | - !GetAtt KMSKey.Arn 159 | 160 | - Sid: DenyDelete 161 | Effect: Deny 162 | Principal: "*" 163 | Action: 164 | - 's3:Delete*' 165 | Resource: 166 | - !GetAtt Bucket.Arn 167 | - !Sub "${Bucket.Arn}/*" 168 | 169 | - Sid: DenyAllExceptConnectAndOthersViaVPCE 170 | Effect: Deny 171 | Principal: '*' 172 | Action: 173 | - 's3:*' 174 | Resource: 175 | - !GetAtt Bucket.Arn 176 | - !Sub "${Bucket.Arn}/*" 177 | Condition: 178 | StringNotEquals: 179 | aws:sourceVpce: !Sub "${VPCEndpoint}" 180 | 181 | - Sid: AllowObjectReadWrite 182 | Effect: Allow 183 | Principal: '*' 184 | Action: 185 | - 's3:PutObject*' 186 | - 's3:Get*' 187 | - 's3:List*' 188 | Resource: 189 | - !GetAtt Bucket.Arn 190 | - !Sub "${Bucket.Arn}/*" 191 | Condition: 192 | StringLike: 193 | 'aws:userId': 194 | - !Sub "${InstanceRoleID}:*" 195 | 196 | - Sid: AllowBucketConfiguration 197 | Effect: Allow 198 | Principal: '*' 199 | Action: 200 | - 's3:*' 201 | Resource: 202 | - !GetAtt Bucket.Arn 203 | - !Sub "${Bucket.Arn}/*" 204 | Condition: 205 | StringLike: 206 | 'aws:userId': 207 | - !Sub "${ProvisioningRoleID}:*" 208 | 209 | Bucket: 210 | Type: AWS::S3::Bucket 211 | Properties: 212 | BucketName: !Sub "${BucketName}" 213 | BucketEncryption: 214 | ServerSideEncryptionConfiguration: 215 | - ServerSideEncryptionByDefault: 216 | KMSMasterKeyID: !GetAtt KMSKey.Arn 217 | SSEAlgorithm: "aws:kms" 218 | PublicAccessBlockConfiguration: 219 | BlockPublicAcls: true 220 | BlockPublicPolicy: true 221 | IgnorePublicAcls: true 222 | RestrictPublicBuckets: true 223 | LoggingConfiguration: 224 | DestinationBucketName: !Sub "${LoggingBucket}" 225 | LogFilePrefix: !Sub "S3logs/${AWS::AccountId}/${BucketName}/" 226 | VersioningConfiguration: 227 | Status: Enabled 228 | 229 | Outputs: 230 | BucketName: 231 | Description: Bucket Name 232 | Value: !Ref Bucket 233 | 234 | BucketName: 235 | Description: Bucket Arn 236 | Value: !GetAtt Bucket.Arn 237 | 238 | KMSKey: 239 | Description: KMS Key Id 240 | Value: !Ref KMSKey 241 | 242 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'yaml' 3 | require 'rspec' 4 | require 'cfn2dsl' 5 | require 'pry' 6 | 7 | RSpec.configure do |config| 8 | config.filter_run :focus 9 | config.run_all_when_everything_filtered = true 10 | config.disable_monkey_patching! 11 | config.warnings = true 12 | config.order = :random 13 | end 14 | -------------------------------------------------------------------------------- /spec/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Cfn2dsl do 4 | subject { Cfn2dsl::VERSION } 5 | 6 | it 'contains version number' do 7 | expect(subject).to match(/\d+\.\d+\.\d+/) 8 | end 9 | end 10 | --------------------------------------------------------------------------------