enscalator 0.4.0.pre.alpha.pre.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +9 -0
  4. data/.rubocop_todo.yml +59 -0
  5. data/.travis.yml +22 -0
  6. data/CODE_OF_CONDUCT.md +13 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +148 -0
  10. data/Rakefile +43 -0
  11. data/bin/console +11 -0
  12. data/bin/setup +7 -0
  13. data/enscalator.gemspec +57 -0
  14. data/exe/enscalator +13 -0
  15. data/lib/enscalator/core/cf_parameters.rb +146 -0
  16. data/lib/enscalator/core/cf_resources.rb +225 -0
  17. data/lib/enscalator/core/instance_type.rb +205 -0
  18. data/lib/enscalator/core/network_config.rb +21 -0
  19. data/lib/enscalator/core.rb +10 -0
  20. data/lib/enscalator/enapp.rb +248 -0
  21. data/lib/enscalator/helpers/dns.rb +62 -0
  22. data/lib/enscalator/helpers/stack.rb +107 -0
  23. data/lib/enscalator/helpers/sub_process.rb +72 -0
  24. data/lib/enscalator/helpers/wrappers.rb +55 -0
  25. data/lib/enscalator/helpers.rb +127 -0
  26. data/lib/enscalator/plugins/amazon_linux.rb +93 -0
  27. data/lib/enscalator/plugins/auto_scale.rb +80 -0
  28. data/lib/enscalator/plugins/core_os.rb +88 -0
  29. data/lib/enscalator/plugins/couchbase.rb +98 -0
  30. data/lib/enscalator/plugins/debian.rb +71 -0
  31. data/lib/enscalator/plugins/elastic_beanstalk.rb +74 -0
  32. data/lib/enscalator/plugins/elasticache.rb +168 -0
  33. data/lib/enscalator/plugins/elasticsearch_amazon.rb +75 -0
  34. data/lib/enscalator/plugins/elasticsearch_bitnami.rb +198 -0
  35. data/lib/enscalator/plugins/elasticsearch_opsworks.rb +225 -0
  36. data/lib/enscalator/plugins/elb.rb +139 -0
  37. data/lib/enscalator/plugins/nat_gateway.rb +71 -0
  38. data/lib/enscalator/plugins/rds.rb +141 -0
  39. data/lib/enscalator/plugins/redis.rb +38 -0
  40. data/lib/enscalator/plugins/rethink_db.rb +21 -0
  41. data/lib/enscalator/plugins/route53.rb +143 -0
  42. data/lib/enscalator/plugins/ubuntu.rb +85 -0
  43. data/lib/enscalator/plugins/user-data/elasticsearch +367 -0
  44. data/lib/enscalator/plugins/vpc_peering_connection.rb +48 -0
  45. data/lib/enscalator/plugins.rb +30 -0
  46. data/lib/enscalator/rich_template_dsl.rb +209 -0
  47. data/lib/enscalator/templates/vpc_peering.rb +112 -0
  48. data/lib/enscalator/templates.rb +20 -0
  49. data/lib/enscalator/version.rb +5 -0
  50. data/lib/enscalator/vpc.rb +11 -0
  51. data/lib/enscalator/vpc_with_nat_gateway.rb +311 -0
  52. data/lib/enscalator/vpc_with_nat_instance.rb +402 -0
  53. data/lib/enscalator.rb +103 -0
  54. metadata +427 -0
@@ -0,0 +1,93 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Amazon Linux appliance
4
+ module AmazonLinux
5
+ class << self
6
+ # Supported storage types in AWS
7
+ STORAGE = [:ebs, :'instance-store']
8
+
9
+ # Supported EBS volume types
10
+ EBS_VOLUME_TYPES = [:standard, :gp2]
11
+
12
+ # Supported Debian image architectures
13
+ ARCH = [:x86_64, :i386]
14
+
15
+ # @param [Symbol, String] region name
16
+ # @param [Symbol, String] release a codename or version number
17
+ # @param [Symbol] storage storage kind
18
+ # @param [Symbol] arch architecture
19
+ # @param [Symbol] ebs_type (:standard or :gp2)
20
+ # @param [Hash] filters search filters
21
+ # @raise [ArgumentError] if storage is nil, empty or not one of supported values
22
+ # @raise [ArgumentError] if arch is nil, empty or not one of supported values
23
+ # @return [String] first ami-id found for the query
24
+ def get_ami(region:, release: '2015.09.1', storage: :ebs, arch: :x86_64, ebs_type: :gp2, filters: {})
25
+ fail ArgumentError, "storage can only be one of #{STORAGE}" unless STORAGE.include? storage
26
+ fail ArgumentError, "arch can only be one of #{ARCH}" unless ARCH.include? arch
27
+ fail ArgumentError, "ebs_type can only be one of #{EBS_VOLUME_TYPES}" unless EBS_VOLUME_TYPES.include? ebs_type
28
+
29
+ client = Aws::EC2::Client.new(region: region)
30
+
31
+ resp = client.describe_images(
32
+ owners: ['amazon'],
33
+ filters: [
34
+ {
35
+ name: 'name',
36
+ values: %W(amzn-ami-hvm-#{release}* amzn-ami-pv-#{release}*)
37
+ },
38
+ {
39
+ name: 'root-device-type',
40
+ values: [storage.to_s]
41
+ },
42
+ {
43
+ name: 'architecture',
44
+ values: [arch.to_s]
45
+ }
46
+ ] + filters_map_to_array(filters)
47
+ )
48
+
49
+ err_msg = format('Could not find any Linux Amazon Ami that fits the criteria: %s, %s, %s, %s, %s, %s',
50
+ region, release, storage, arch, ebs_type, filters)
51
+ fail StandardError, err_msg unless resp.images
52
+
53
+ images = resp.images.sort_by(&:creation_date).reverse
54
+ images = images.select { |i| i.block_device_mappings.first.ebs.volume_type == ebs_type.to_s } if storage == :ebs
55
+ images.first.image_id
56
+ end
57
+
58
+ private
59
+
60
+ def filters_map_to_array(filters)
61
+ filters.map do |k, v|
62
+ {
63
+ name: k.to_s,
64
+ values: v.is_a?(Array) ? v : [v.to_s]
65
+ }
66
+ end
67
+ end
68
+ end
69
+
70
+ # Create AMI id parameter for an Amazon linux instance
71
+ #
72
+ # @param [Symbol, String] region name
73
+ # @param [Symbol, String] release a codename or version number
74
+ # @param [Symbol] storage storage kind (ebs or instance_store)
75
+ # @param [String] arch architecture (x86_64 or i386)
76
+ # @param [Symbol] ebs_type (:standard or :gp2)
77
+ # @param [Hash] filters filters for the search
78
+ def amazon_linux_init(region: self.region,
79
+ release: '2015.09.1',
80
+ storage: :ebs,
81
+ arch: :x86_64,
82
+ ebs_type: :gp2,
83
+ filters: {})
84
+ parameter_ami 'AmazonLinux', AmazonLinux.get_ami(region: region,
85
+ release: release,
86
+ storage: storage,
87
+ arch: arch,
88
+ ebs_type: ebs_type,
89
+ filters: filters)
90
+ end
91
+ end # class AmazonLinux
92
+ end # module Plugins
93
+ end # module Enscalator
@@ -0,0 +1,80 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Auto scaling group plugin
4
+ module AutoScale
5
+ # Create new auto scaling group
6
+ #
7
+ # @param [String] image_id image id that will be used to launch instance
8
+ # @param [String] auto_scale_name auto scaling group name (default: stack name)
9
+ # @param [Hash] launch_config_props dictionary that is used to overwrite default launch configuration settings
10
+ # Reference: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html
11
+ # @param [Hash] auto_scale_props dictionary that is used to overwrite default auto scaling group settings
12
+ # Reference: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html
13
+ # @param [Array] auto_scale_tags list of tags that is added to auto scaling group tags
14
+ # @return [String] auto scaling group resource name
15
+ def auto_scale_init(image_id,
16
+ auto_scale_name: nil,
17
+ launch_config_props: {},
18
+ auto_scale_props: {},
19
+ auto_scale_tags: [])
20
+
21
+ name = auto_scale_name || app_template_name
22
+ auto_scale_name = "#{name}AutoScale"
23
+ launch_config_resource_name = "#{name}LaunchConfig"
24
+ auto_scale_resource_name = auto_scale_name
25
+ auto_scale_key_name = gen_ssh_key_name auto_scale_name.underscore, region, stack_name
26
+
27
+ pre_run do
28
+ create_ssh_key auto_scale_key_name,
29
+ region,
30
+ force_create: false
31
+ end
32
+
33
+ resource launch_config_resource_name,
34
+ Type: 'AWS::AutoScaling::LaunchConfiguration',
35
+ Properties: {
36
+ ImageId: image_id,
37
+ InstanceType: 'm3.medium',
38
+ KeyName: auto_scale_key_name,
39
+ AssociatePublicIpAddress: false,
40
+ SecurityGroups: [ref_private_security_group, ref_application_security_group]
41
+ }.merge(launch_config_props)
42
+
43
+ if auto_scale_props.key?(:Tags)
44
+ warn('Do not use auto_scale_props to set Tags, auto_scale_tags is available for that purpose')
45
+ auto_scale_props.delete_if { |k, _| k == :Tags }
46
+ end
47
+
48
+ auto_scale_current_tags = [
49
+ {
50
+ Key: 'Name',
51
+ Value: auto_scale_name,
52
+ PropagateAtLaunch: true
53
+ }
54
+ ].concat(auto_scale_tags)
55
+
56
+ auto_scale_current_properties = {
57
+ AvailabilityZones: availability_zones.values,
58
+ VPCZoneIdentifier: ref_application_subnets,
59
+ LaunchConfigurationName: ref(launch_config_resource_name),
60
+ MinSize: 0,
61
+ MaxSize: 1,
62
+ DesiredCapacity: 1,
63
+ Tags: auto_scale_current_tags
64
+ }.merge(auto_scale_props)
65
+
66
+ resource auto_scale_resource_name,
67
+ Type: 'AWS::AutoScaling::AutoScalingGroup',
68
+ Properties: auto_scale_current_properties
69
+
70
+ # return resource name
71
+ auto_scale_resource_name
72
+ end
73
+
74
+ # Callback to get name of the class which included this module
75
+ def self.included(klass)
76
+ send(:define_method, :app_template_name) { "#{klass.name.demodulize}" }
77
+ end
78
+ end # module AutoScale
79
+ end # module Plugins
80
+ end # module Enscalator
@@ -0,0 +1,88 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # CoreOS appliance
4
+ module CoreOS
5
+ class << self
6
+ # CoreOS Release channels
7
+ # @see https://coreos.com/releases
8
+ CHANNELS = [:alpha, :beta, :stable].freeze
9
+
10
+ # Get CoreOS mapping for specific version from specific channel (stable, beta or alpha)
11
+ #
12
+ # @param [Symbol] channel channel identifier
13
+ # @param [String] tag specific version release tag
14
+ # @return [Hash] CoreOS mapping for specific version and channel
15
+ # (if version tag is not given, returns the most latest version number)
16
+ def get_channel_version(channel: :stable, tag: nil)
17
+ fail ArgumentError, "channel can only be one of #{CHANNELS}" unless CHANNELS.include? channel
18
+ base_url = "http://#{channel}.release.core-os.net/amd64-usr"
19
+ fetch_mapping(base_url, tag)
20
+ end
21
+
22
+ # Get CoreOS mapping for specific version regardless of its release channel
23
+ #
24
+ # @param [String] tag version tag
25
+ # @return [Hash] CoreOS mapping for specific version
26
+ # (if version tag is not given, returns the most latest version number)
27
+ def get_specific_version(tag: nil)
28
+ urls = CHANNELS.map { |c| "http://#{c}.release.core-os.net/amd64-usr" }
29
+ mapping = nil
30
+ urls.each do |u|
31
+ mapping ||= fetch_mapping(u, tag)
32
+ end
33
+ mapping
34
+ end
35
+
36
+ private
37
+
38
+ # Fetch CoreOS region/virtualization/ami mapping
39
+ #
40
+ # @param [String] base_url url excluding version number and mapping file
41
+ # @param [String] tag specific version release tag
42
+ # @return [Hash] CoreOS mapping
43
+ def fetch_mapping(base_url, tag)
44
+ fail ArgumentError, 'url cannot be empty or nil' if base_url.blank?
45
+ versions = fetch_versions(base_url.ends_with?('/') ? base_url : base_url + '/')
46
+ version = if tag && !tag.empty?
47
+ versions.find { |v| v == Semantic::Version.new(tag) }.to_s
48
+ else
49
+ versions.sort.last.to_s
50
+ end
51
+
52
+ images = open([base_url, version, 'coreos_production_ami_all.json'].join('/')) { |f| f.read }
53
+
54
+ json = JSON.parse(images) if images
55
+ parse_raw_mapping(json)
56
+ end
57
+
58
+ # Make request to CoreOS release pages, parse response and make a list of versions
59
+ #
60
+ # @param [String] url of web page with CoreOS versions
61
+ # @return [Array] list of Semantic::Version
62
+ def fetch_versions(url)
63
+ html = Nokogiri::HTML(open(url))
64
+ raw_versions = html.xpath('/html/body/a').map { |a| a.children.first.text.chomp('/') }
65
+ raw_versions.select { |rw| rw =~ /^[0-9]/ }.map { |rw| Semantic::Version.new(rw) }
66
+ end
67
+
68
+ # Parse and reformat CoreOS default mapping
69
+ #
70
+ # @param [Array] coreos_mapping list of region to virtualization kind mappings
71
+ # @return [Hash] mapping, that can be referred to with find_in_map
72
+ def parse_raw_mapping(coreos_mapping)
73
+ return {} unless coreos_mapping
74
+ amis = coreos_mapping.empty? ? [] : coreos_mapping['amis']
75
+ Hash[
76
+ amis.map { |a| [a['name'], { pv: a['pv'], hvm: a['hvm'] }] }
77
+ ].with_indifferent_access
78
+ end
79
+ end # class << self
80
+
81
+ # Initialize CoreOS related configurations
82
+ #
83
+ def core_os_init
84
+ mapping 'AWSCoreOSAMI', CoreOS.get_channel_version(channel: :stable)
85
+ end
86
+ end # module CoreOS
87
+ end # module Plugins
88
+ end # module Enscalator
@@ -0,0 +1,98 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Plugin for Couchbase
4
+ module Couchbase
5
+ # Couchbase instance
6
+ #
7
+ # @param [String] db_name database name
8
+ # @param [String] bucket bucket
9
+ # @param [Integer] allocated_storage size of instance primary storage
10
+ # @param [String] instance_type instance type
11
+ def couchbase_init(db_name,
12
+ bucket: nil,
13
+ allocated_storage: 5,
14
+ instance_type: 't2.medium')
15
+ @couchbase_mapping ||=
16
+ mapping 'AWSCouchbaseAMI', couchbase_ami_mapping
17
+
18
+ fail 'You need to provide a bucket for couchbase' if bucket.nil?
19
+
20
+ parameter_key_name "Couchbase#{db_name}"
21
+
22
+ parameter_allocated_storage "Couchbase#{db_name}",
23
+ default: allocated_storage,
24
+ min: 5,
25
+ max: 1024
26
+
27
+ parameter_ec2_instance_type "Couchbase#{db_name}", type: instance_type
28
+
29
+ instance_vpc("Couchbase#{db_name}",
30
+ find_in_map('AWSCouchbaseAMI', ref('AWS::Region'), 'amd64'),
31
+ ref_resource_subnets.first,
32
+ [ref_private_security_group, ref_resource_security_group],
33
+ depends_on: [],
34
+ properties: {
35
+ KeyName: ref("Couchbase#{db_name}KeyName"),
36
+ InstanceType: ref("Couchbase#{db_name}InstanceType"),
37
+ UserData: Base64.encode64(
38
+ couchbase_user_data(bucket, 'Administrator', '3fA76JWtzYbm')
39
+ )
40
+ })
41
+ end
42
+
43
+ # Couchbase ami mapping for x64 images
44
+ # @return [Hash] aws mapping
45
+ def couchbase_ami_mapping
46
+ {
47
+ 'us-west-2': { paravirtual: 'ami-c398c6f3' },
48
+ 'us-west-1': { paravirtual: 'ami-1a554c5f' },
49
+ 'us-east-1': { paravirtual: 'ami-403b4328' },
50
+ 'sa-east-1': { paravirtual: 'ami-59229f44' },
51
+ 'eu-west-1': { paravirtual: 'ami-8129aaf6' },
52
+ 'ap-southeast-1': { paravirtual: 'ami-88745fda' },
53
+ 'ap-northeast-1': { paravirtual: 'ami-6a7b676b' }
54
+ }.with_indifferent_access
55
+ end
56
+
57
+ # Couchbase user data
58
+ #
59
+ # @param [String] bucket couchbase bucket
60
+ # @return [String] user data script
61
+ def couchbase_user_data(bucket, user, password)
62
+ data = <<-EOG
63
+ #!/usr/bin/env bash
64
+ while [[ ! -e /opt/couchbase/var/lib/couchbase/couchbase-server.pid ]]; do
65
+ sleep 20
66
+ echo "wait for couchbase" >> /tmp/userdatalog
67
+ service couchbase-server status >> /tmp/userdatalog
68
+ done
69
+
70
+ # Wait for everything to be initialized
71
+ sleep 1m
72
+ RAMSIZE=`cat /proc/meminfo | grep MemTotal | awk {'print $2'}`
73
+ RAMSIZE=`echo "($RAMSIZE/1000*(75/100.0))" | bc -l | xargs printf %0.f`
74
+ INSTANCE=`curl http://169.254.169.254/latest/meta-data/instance-id`
75
+ /opt/couchbase/bin/couchbase-cli cluster-init -c 127.0.0.1:8091 \
76
+ -u #{user} \
77
+ -p $INSTANCE \
78
+ --cluster-init-password=#{password} \
79
+ --cluster-init-ramsize=$RAMSIZE 2>&1 >> /tmp/userdatalog
80
+ /opt/couchbase/bin/couchbase-cli node-init -c 127.0.0.1:8091 \
81
+ -u #{user} \
82
+ -p #{password} 2>&1 >> /tmp/userdatalog
83
+ /opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 \
84
+ --bucket=#{bucket} \
85
+ --bucket-type=couchbase \
86
+ --bucket-password=#{password} \
87
+ --bucket-port=11211 \
88
+ --bucket-ramsize=$RAMSIZE \
89
+ --bucket-replica=1 \
90
+ --bucket-priority=low \
91
+ --wait \
92
+ -u #{user} -p #{password} 2>&1 >> /tmp/userdatalog
93
+ EOG
94
+ data
95
+ end
96
+ end # module Couchbase
97
+ end # module Plugins
98
+ end # module Enscalator
@@ -0,0 +1,71 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Debian appliance
4
+ module Debian
5
+ class << self
6
+ # Supported storage types in AWS
7
+ STORAGE = [:ebs, :'instance-store']
8
+
9
+ # Supported Debian image architectures
10
+ ARCH = [:x86_64, :i386]
11
+
12
+ # Supported Debian releases
13
+ RELEASE = { jessie: '8', wheezy: '7' }
14
+
15
+ # Structure to hold parsed record
16
+ Struct.new('Debian', :region, :ami, :virtualization, :arch, :root_storage)
17
+
18
+ # @param [Symbol, String] release a codename or version number
19
+ # @param [Symbol] storage storage kind
20
+ # @param [Symbol] arch architecture
21
+ # @raise [ArgumentError] if release is nil, empty or not one of supported values
22
+ # @raise [ArgumentError] if storage is nil, empty or not one of supported values
23
+ # @raise [ArgumentError] if arch is nil, empty or not one of supported values
24
+ # @return [Hash] mapping for Debian amis
25
+ def get_mapping(release: :jessie, storage: :ebs, arch: :x86_64)
26
+ fail ArgumentError, 'release can be either codename or version' unless RELEASE.to_a.flatten.include? release
27
+ fail ArgumentError, "storage can only be one of #{STORAGE}" unless STORAGE.include? storage
28
+ fail ArgumentError, "arch can only be one of #{ARCH}" unless ARCH.include? arch
29
+ version = RELEASE.keys.include?(release) ? release : RELEASE.invert[release]
30
+ url = "https://wiki.debian.org/Cloud/AmazonEC2Image/#{version.capitalize}?action=raw"
31
+ body = open(url) { |f| f.read }
32
+ parse_raw_entries(body.split("\r\n").select { |b| b.starts_with?('||') })
33
+ .select { |i| i.ami =~ /ami[-][a-z0-9]{8}/ }
34
+ .select { |r| r.root_storage == storage.to_s && r.arch == arch.to_s }
35
+ .group_by(&:region)
36
+ .map { |k, v| [k, v.map { |i| [i.virtualization, i.ami] }.to_h] }
37
+ .to_h
38
+ .with_indifferent_access
39
+ end
40
+
41
+ private
42
+
43
+ # Parse raw entries and convert them to meaningful structs
44
+ #
45
+ # @param [Array] items in its raw form
46
+ def parse_raw_entries(items)
47
+ header, *entries = items.map do |item|
48
+ if (item.include?('Region'))..(item.include?('paravirtual'))
49
+ item.downcase.split('||').map(&:strip).map { |i| i.delete("'") }.reject(&:empty?)
50
+ end
51
+ end.compact
52
+ amis = entries.select { |e| e.first =~ /[a-z]{2}-/ }.flat_map do |entry|
53
+ region, *images = entry
54
+ images.map.with_index(1).map do |ami, i|
55
+ [region, ami, header[i].nil? ? '' : header[i].split].flatten
56
+ end
57
+ end
58
+
59
+ amis.map { |a| Struct::Debian.new(*a) }
60
+ end
61
+ end
62
+ # Create new Debian instance
63
+ #
64
+ # @param [String] storage storage kind (usually ebs or ephemeral)
65
+ # @param [String] arch architecture (amd64 or i386)
66
+ def debian_init(storage: :ebs, arch: :x86_64)
67
+ mapping 'AWSDebianAMI', Debian.get_mapping(storage: storage, arch: arch)
68
+ end
69
+ end # class Debian
70
+ end # module Plugins
71
+ end # module Enscalator
@@ -0,0 +1,74 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Collection of methods to work with Elastic Beanstalk
4
+ module ElasticBeanstalk
5
+ include Enscalator::Helpers
6
+
7
+ # Create new ElasticBeanstalk instance
8
+ #
9
+ # @param [String] app_name application name
10
+ # @param [String] ssh_key name of ssh key to configure instance with
11
+ # @param [String] solution_stack_name stack name of Elastic Beanstalk solution
12
+ # @param [String] instance_type default instance type
13
+ def elastic_beanstalk_app(app_name,
14
+ stack_name,
15
+ ssh_key: app_name,
16
+ solution_stack_name: '64bit Amazon Linux 2015.09 v2.0.4 running Ruby 2.2 (Passenger Standalone)',
17
+ instance_type: 't2.small')
18
+
19
+ properties = {
20
+ ApplicationName: app_name,
21
+ Description: "#{app_name} in #{stack_name} stack",
22
+ ConfigurationTemplates:
23
+ [
24
+ {
25
+ TemplateName: 'DefaultConfiguration',
26
+ Description: 'Default Configuration Version 1.0 - with SSH access',
27
+ SolutionStackName: solution_stack_name,
28
+ OptionSettings: [
29
+ {
30
+ 'Namespace': 'aws:autoscaling:launchconfiguration',
31
+ 'OptionName': 'EC2KeyName',
32
+ 'Value': ssh_key
33
+ },
34
+ {
35
+ 'Namespace': 'aws:ec2:vpc',
36
+ 'OptionName': 'VPCId',
37
+ 'Value': vpc.id
38
+ },
39
+ {
40
+ 'Namespace': 'aws:ec2:vpc',
41
+ 'OptionName': 'Subnets',
42
+ 'Value': { 'Fn::Join': [',', ref_application_subnets] }
43
+ },
44
+ {
45
+ 'Namespace': 'aws:ec2:vpc',
46
+ 'OptionName': 'ELBSubnets',
47
+ 'Value': { 'Fn::Join': [',', public_subnets] }
48
+ },
49
+ {
50
+ 'Namespace': 'aws:autoscaling:launchconfiguration',
51
+ 'OptionName': 'SecurityGroups',
52
+ 'Value': { 'Fn::Join': [',', [ref_application_security_group, ref_private_security_group]] }
53
+ },
54
+ {
55
+ 'Namespace': 'aws:autoscaling:launchconfiguration',
56
+ 'OptionName': 'InstanceType',
57
+ 'Value': instance_type
58
+ }
59
+ ]
60
+ }
61
+ ]
62
+ }
63
+
64
+ elastic_beanstalk_resource_name = "#{app_name}BeanstalkApp"
65
+
66
+ resource elastic_beanstalk_resource_name,
67
+ Type: 'AWS::ElasticBeanstalk::Application',
68
+ Properties: properties
69
+
70
+ elastic_beanstalk_resource_name
71
+ end
72
+ end # module ElasticBeanstalk
73
+ end # module Plugins
74
+ end # module Enscalator
@@ -0,0 +1,168 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Collection of methods to work with ElastiCache (Redis)
4
+ module Elasticache
5
+ include Enscalator::Helpers
6
+
7
+ # Generates magic number (sha256 hex digest) from given Hash
8
+ # @param [Object] input used for digest generation
9
+ # @return [String]
10
+ def magic_number(input)
11
+ str = if input.is_a?(String)
12
+ input
13
+ elsif input.is_a?(Array)
14
+ input.flatten.map(&:to_s).sort.join('&')
15
+ elsif input.is_a?(Hash)
16
+ flatten_hash(input).map { |k, v| "#{k}=#{v}" }.sort.join('&')
17
+ else
18
+ fail("Not supported input format: #{input.class}")
19
+ end
20
+ Digest::SHA256.hexdigest(str)
21
+ end
22
+
23
+ # Initialize resources common for all ElastiCache instances
24
+ # @param [Object] app_name application stack name
25
+ # @param [Object] cache_node_type node type
26
+ def init_cluster_resources(app_name, cache_node_type, param_group_seed: nil, **properties)
27
+ subnet_group_name = "#{app_name}ElasticacheSubnetGroup"
28
+ resource subnet_group_name,
29
+ Type: 'AWS::ElastiCache::SubnetGroup',
30
+ Properties: {
31
+ Description: 'SubnetGroup for elasticache',
32
+ SubnetIds: ref_resource_subnets
33
+ }
34
+
35
+ security_group_name =
36
+ security_group_vpc "#{app_name}RedisSecurityGroup",
37
+ "Redis Security Group for #{app_name}",
38
+ ref_vpc_id,
39
+ security_group_ingress: [
40
+ {
41
+ IpProtocol: 'tcp',
42
+ FromPort: '6379',
43
+ ToPort: '6389',
44
+ SourceSecurityGroupId: ref_application_security_group
45
+ }
46
+ ],
47
+ tags: {
48
+ Name: join('-', aws_stack_name, 'res', 'sg'),
49
+ Application: aws_stack_name
50
+ }
51
+
52
+ parameter_group_required_props = {
53
+ 'reserved-memory': Core::InstanceType.elasticache_instance_type.max_memory(cache_node_type) / 2
54
+ }
55
+
56
+ parameter_group_props = (
57
+ if !properties.nil? && properties.key?(:parameter_group_properties)
58
+ properties[:parameter_group_properties]
59
+ else
60
+ {}
61
+ end).merge(parameter_group_required_props)
62
+
63
+ # TODO: remove this workaround when related template gets fixed
64
+ parameter_group_name = if param_group_seed
65
+ "#{app_name}RedisParameterGroup#{param_group_seed}"
66
+ else
67
+ "#{app_name}RedisParameterGroup#{magic_number(parameter_group_props)}"
68
+ end
69
+
70
+ resource parameter_group_name,
71
+ Type: 'AWS::ElastiCache::ParameterGroup',
72
+ Properties: {
73
+ Description: "#{app_name} redis parameter group",
74
+ CacheParameterGroupFamily: 'redis3.2',
75
+ Properties: parameter_group_props
76
+ }
77
+
78
+ {
79
+ subnet_group: subnet_group_name,
80
+ security_group: security_group_name,
81
+ parameter_group: parameter_group_name
82
+ }
83
+ end
84
+
85
+ # Create ElastiCache cluster
86
+ # @param [String] app_name application name
87
+ # @param [String] cache_node_type instance node type
88
+ # @param [Integer] num_cache_nodes number of nodes to create
89
+ def elasticache_cluster_init(app_name, cache_node_type: 'cache.m1.small', num_cache_nodes: 1)
90
+ cluster_resources = init_cluster_resources(app_name, cache_node_type)
91
+
92
+ resource_name = "#{app_name}RedisCluster"
93
+ resource resource_name,
94
+ Type: 'AWS::ElastiCache::CacheCluster',
95
+ Properties: {
96
+ Engine: 'redis',
97
+ NumCacheNodes: "#{num_cache_nodes}",
98
+ CacheNodeType: cache_node_type,
99
+ CacheSubnetGroupName: ref("#{app_name}ElasticacheSubnetGroup"),
100
+ CacheParameterGroupName: ref(cluster_resources[:parameter_group]),
101
+ VpcSecurityGroupIds: [get_att("#{app_name}RedisSecurityGroup", 'GroupId')]
102
+ }
103
+ resource_name
104
+
105
+ # Unable to get created resource endpoint and port with Fn::GetAtt for engine == redis
106
+ # For more details see here:
107
+ # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticache-cache-cluster.html
108
+ end
109
+
110
+ # Create ElastiCache replication group
111
+ # @param [String] app_name application name
112
+ # @param [String] cache_node_type instance node type
113
+ def elasticache_repl_group_init(app_name, cache_node_type: 'cache.t2.small',
114
+ num_cache_clusters: 2, seed: nil, **properties)
115
+ if %w(t1).map { |t| cache_node_type.include?(t) }.include?(true)
116
+ fail "T1 instance types are not supported, got '#{cache_node_type}'"
117
+ end
118
+
119
+ cluster_properties =
120
+ !properties.nil? && properties.key?(:cluster_properties) ? properties[:cluster_properties] : {}
121
+
122
+ cluster_resources = init_cluster_resources(app_name,
123
+ cache_node_type,
124
+ param_group_seed: seed,
125
+ parameter_group_properties: cluster_properties)
126
+
127
+ resource_name = "#{app_name}RedisReplicationGroup"
128
+ resource resource_name,
129
+ Type: 'AWS::ElastiCache::ReplicationGroup',
130
+ Properties: {
131
+ Engine: 'redis',
132
+ ReplicationGroupDescription: "Redis Replication group for #{app_name}",
133
+ AutomaticFailoverEnabled: num_cache_clusters > 1 ? 'true' : 'false',
134
+ NumCacheClusters: num_cache_clusters,
135
+ CacheNodeType: cache_node_type,
136
+ CacheSubnetGroupName: ref("#{app_name}ElasticacheSubnetGroup"),
137
+ CacheParameterGroupName: ref(cluster_resources[:parameter_group]),
138
+ SecurityGroupIds: [
139
+ get_att("#{app_name}RedisSecurityGroup", 'GroupId'),
140
+ ref_private_security_group
141
+ ]
142
+ }.merge(properties.reject { |k, _v| k == :cluster_properties })
143
+
144
+ output "#{app_name}RedisReplicationGroup",
145
+ Description: "Redis ReplicationGroup #{app_name}",
146
+ Value: ref("#{app_name}RedisReplicationGroup")
147
+
148
+ output "#{app_name}RedisPrimaryEndpointAddress",
149
+ Description: "Redis Primary Endpoint Address #{app_name}",
150
+ Value: get_att(resource_name, 'PrimaryEndPoint.Address')
151
+
152
+ output "#{app_name}RedisPrimaryEndpointPort",
153
+ Description: "Redis Primary Endpoint Port #{app_name}",
154
+ Value: get_att(resource_name, 'PrimaryEndPoint.Port')
155
+
156
+ output "#{app_name}RedisReadOnlyEndpointAddresses",
157
+ Description: "Redis ReadOnly Endpoint Addresses #{app_name}",
158
+ Value: get_att(resource_name, 'ReadEndPoint.Addresses')
159
+
160
+ output "#{app_name}RedisReadOnlyEndpointPorts",
161
+ Description: "Redis ReadOnly Endpoint Ports #{app_name}",
162
+ Value: get_att(resource_name, 'ReadEndPoint.Ports')
163
+
164
+ resource_name
165
+ end
166
+ end # module ElasticCache
167
+ end # module Plugins
168
+ end # module Enscalator