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,71 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # VPC NAT Gateway plugin
4
+ module NATGateway
5
+ # Allocate new elastic IP in given VPC template
6
+ #
7
+ # @param [String] name eip resource name
8
+ # @param [Array<String>] depends_on list of resource names this resource depends on
9
+ # @return [Hash] result of Fn::GetAtt function
10
+ def allocate_new_eip(name, depends_on: [])
11
+ fail('Dependency on the VPC-gateway attachment must be provided') if depends_on.empty?
12
+ eip_resource_name = name
13
+ resource eip_resource_name,
14
+ DependsOn: depends_on,
15
+ Type: 'AWS::EC2::EIP',
16
+ Properties: {
17
+ Domain: 'vpc'
18
+ }
19
+
20
+ output eip_resource_name,
21
+ Description: 'Elastic IP address for NAT Gateway',
22
+ Value: ref(eip_resource_name)
23
+
24
+ get_att(eip_resource_name, 'AllocationId')
25
+ end
26
+
27
+ # Create new route rule
28
+ #
29
+ # @param [String] name route rule name
30
+ # @param [Array<String>] depends_on list of resource names this resource depends on
31
+ def add_route_rule(name, route_table_name, nat_gateway_name, dest_cidr_block, depends_on: [])
32
+ options = {
33
+ Type: 'AWS::EC2::Route'
34
+ }
35
+ options[:DependsOn] = depends_on unless depends_on.blank?
36
+ resource name,
37
+ options.merge(
38
+ Properties: {
39
+ RouteTableId: ref(route_table_name),
40
+ NatGatewayId: ref(nat_gateway_name),
41
+ DestinationCidrBlock: dest_cidr_block
42
+ })
43
+ end
44
+
45
+ # Create new NAT gateway
46
+ def nat_gateway_init(name, subnet_name, route_table_name, dest_cidr_block: '0.0.0.0/0', depends_on: [])
47
+ nat_gateway_eip_name = "#{name}EIP"
48
+ nat_gateway_eip = allocate_new_eip(nat_gateway_eip_name, depends_on: depends_on)
49
+ nat_gateway_name = name
50
+ nat_gateway_options = {
51
+ Type: 'AWS::EC2::NatGateway'
52
+ }
53
+ nat_gateway_options[:DependsOn] = depends_on unless depends_on.blank?
54
+ resource nat_gateway_name,
55
+ nat_gateway_options.merge(
56
+ Properties: {
57
+ AllocationId: nat_gateway_eip,
58
+ SubnetId: ref(subnet_name)
59
+ })
60
+ nat_route_rule_name = "#{name}Route"
61
+ add_route_rule(nat_route_rule_name, route_table_name, nat_gateway_name, dest_cidr_block, depends_on: depends_on)
62
+
63
+ output nat_gateway_name,
64
+ Description: 'NAT Gateway',
65
+ Value: ref(nat_gateway_name)
66
+
67
+ nat_gateway_name
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,141 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Amazon RDS instance
4
+ module RDS
5
+ # Create new Amazon RDS instance
6
+ #
7
+ # @param [String] db_name database name
8
+ # @param [Boolean] use_snapshot use snapshot or not
9
+ # @param [Integer] allocated_storage size of instance primary storage
10
+ # @param [String] storage_type instance storage type
11
+ # @param [String] multizone deploy as multizone or use only single availability zone
12
+ # @param [String] parameter_group RDS instance parameter group
13
+ # @param [String] instance_type instance type
14
+ # @param [Hash] properties additional properties
15
+ def rds_init(db_name,
16
+ use_snapshot: false,
17
+ allocated_storage: 5,
18
+ storage_type: 'gp2',
19
+ multizone: 'false',
20
+ engine: 'MySQL',
21
+ engine_version: '5.6',
22
+ parameter_group: 'default.mysql5.6',
23
+ instance_type: 'db.t2.small',
24
+ properties: {})
25
+
26
+ parameter_name "RDS#{db_name}"
27
+
28
+ parameter_rds_instance_type "RDS#{db_name}", type: instance_type
29
+
30
+ parameter_allocated_storage "RDS#{db_name}",
31
+ default: allocated_storage,
32
+ min: 5,
33
+ max: 1024
34
+
35
+ parameter "RDS#{db_name}Engine",
36
+ Default: engine,
37
+ Description: 'DB engine type of the DB instance',
38
+ Type: 'String'
39
+
40
+ parameter "RDS#{db_name}EngineVersion",
41
+ Default: engine_version,
42
+ Description: 'DB engine version of the DB instance',
43
+ Type: 'String'
44
+
45
+ parameter "RDS#{db_name}StorageType",
46
+ Default: storage_type,
47
+ Description: 'Storage type to be associated with the DB instance',
48
+ Type: 'String',
49
+ AllowedValues: %w( gp2 standard io1 )
50
+
51
+ parameter "RDS#{db_name}Multizone",
52
+ Default: multizone,
53
+ Description: 'Multizone deployment',
54
+ Type: 'String'
55
+
56
+ parameter "RDS#{db_name}ParameterGroup",
57
+ Default: parameter_group,
58
+ Description: 'Custom parameter group for an RDS database family',
59
+ Type: 'String'
60
+
61
+ parameter_username "RDS#{db_name}"
62
+
63
+ parameter_password "RDS#{db_name}"
64
+
65
+ resource "RDS#{db_name}SubnetGroup",
66
+ Type: 'AWS::RDS::DBSubnetGroup',
67
+ Properties: {
68
+ DBSubnetGroupDescription: 'Subnet group within VPC',
69
+ SubnetIds: ref_resource_subnets,
70
+ Tags: [
71
+ {
72
+ Key: 'Name',
73
+ Value: "RDS#{db_name}SubnetGroup"
74
+ }
75
+ ]
76
+ }
77
+
78
+ # DBName and DBSnapshotIdentifier are mutually exclusive, thus
79
+ # when snapshot_id is given DBName won't be included to resource parameters
80
+ props = properties.deep_dup
81
+ if use_snapshot
82
+ parameter "RDS#{db_name}SnapshotId",
83
+ Description: 'Identifier for the DB snapshot to restore from',
84
+ Type: 'String',
85
+ MinLength: '1',
86
+ MaxLength: '64'
87
+ props[:DBSnapshotIdentifier] = ref("RDS#{db_name}SnapshotId")
88
+ else
89
+ props[:DBName] = ref("RDS#{db_name}Name")
90
+ end
91
+
92
+ rds_instance_tags = [
93
+ {
94
+ Key: 'Name',
95
+ Value: "RDS#{db_name}Instance"
96
+ }
97
+ ]
98
+
99
+ # Set instance tags
100
+ if props.key?(:Tags) && !props[:Tags].empty?
101
+ props[:Tags].concat(rds_instance_tags)
102
+ else
103
+ props[:Tags] = rds_instance_tags
104
+ end
105
+
106
+ rds_props = {
107
+ PubliclyAccessible: 'false',
108
+ MultiAZ: ref("RDS#{db_name}Multizone"),
109
+ Engine: ref("RDS#{db_name}Engine"),
110
+ EngineVersion: ref("RDS#{db_name}EngineVersion"),
111
+ MasterUsername: ref("RDS#{db_name}Username"),
112
+ MasterUserPassword: ref("RDS#{db_name}Password"),
113
+ DBInstanceClass: ref("RDS#{db_name}InstanceType"),
114
+ VPCSecurityGroups: [ref_resource_security_group, ref_private_security_group],
115
+ DBSubnetGroupName: ref("RDS#{db_name}SubnetGroup"),
116
+ DBParameterGroupName: ref("RDS#{db_name}ParameterGroup"),
117
+ AllocatedStorage: ref("RDS#{db_name}AllocatedStorage"),
118
+ StorageType: ref("RDS#{db_name}StorageType")
119
+ }
120
+
121
+ rds_instance_resource_name = "RDS#{db_name}Instance"
122
+ resource rds_instance_resource_name,
123
+ Type: 'AWS::RDS::DBInstance',
124
+ Properties: rds_props.merge(props)
125
+
126
+ output "RDS#{db_name}EndpointAddress",
127
+ Description: "#{db_name} Endpoint Address",
128
+ Value: get_att("RDS#{db_name}Instance", 'Endpoint.Address')
129
+
130
+ rds_instance_resource_name
131
+ end
132
+
133
+ # Ensure that plugin using this template is a subclass of EnAppTemplateDSL
134
+ def self.included(klass)
135
+ if klass.superclass != Enscalator::EnAppTemplateDSL
136
+ fail("Plugin #{name.to_s.demodulize} requires template to be subclass of #{EnAppTemplateDSL}")
137
+ end
138
+ end
139
+ end # RDS
140
+ end # Plugins
141
+ end # Enscalator
@@ -0,0 +1,38 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Redis on EC2 instance
4
+ module Redis
5
+ include Enscalator::Plugins::Ubuntu
6
+
7
+ # Create new Redis instance
8
+ #
9
+ # @param [String] instance_name instance name
10
+ # @param [String] key_name instance key
11
+ # @param [String] instance_type instance type
12
+ def redis_init(instance_name,
13
+ key_name:,
14
+ instance_type: 't2.medium')
15
+
16
+ parameter "Ubuntu#{instance_name}KeyName",
17
+ Default: key_name,
18
+ Description: 'Keypair name',
19
+ Type: 'String'
20
+
21
+ ubuntu_init instance_name, instance_type: instance_type, properties: { 'UserData' => redis_user_data }
22
+ end
23
+
24
+ # Install and run Redis on EC2 instance
25
+ # @return [String] user-data
26
+ def redis_user_data
27
+ Base64.encode64(%(
28
+ #!/usr/bin/env bash
29
+ apt-get update
30
+ apt-get upgrade -y
31
+ apt-get install -y redis-server
32
+ sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' /etc/redis/redis.conf
33
+ service redis-server restart
34
+ ).gsub(/^\s+/, ''))
35
+ end
36
+ end # Redis
37
+ end # Plugins
38
+ end # Enscalator
@@ -0,0 +1,21 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # RethinkDB appliance
4
+ module RethinkDB
5
+ # Mapping for Rethinkdb x64 images
6
+ def self.mapping
7
+ {
8
+ 'eu-central-1': { paravirtual: 'ami-1249740f' },
9
+ 'eu-west-1': { paravirtual: 'ami-7a40f00d' },
10
+ 'ap-northeast-1': { paravirtual: 'ami-90c3c491' },
11
+ 'us-east-1': { paravirtual: 'ami-6ea9fb06' },
12
+ 'us-west-1': { paravirtual: 'ami-7f6d7d3a' },
13
+ 'us-west-2': { paravirtual: 'ami-cf0d2cff' },
14
+ 'ap-southeast-1': { paravirtual: 'ami-aa5b6cf8' },
15
+ 'ap-southeast-2': { paravirtual: 'ami-b9325b83' },
16
+ 'sa-east-1': { paravirtual: 'ami-b38b3aae' }
17
+ }.with_indifferent_access
18
+ end
19
+ end # RethinkDB
20
+ end # Plugins
21
+ end # Enscalator
@@ -0,0 +1,143 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Create Route53 resources
4
+ module Route53
5
+ # Valid types for Route53 healthcheck
6
+ HEALTH_CHECK_TYPE = %w(HTTP HTTPS HTTP_STR_MATCH HTTPS_STR_MATCH TCP)
7
+
8
+ # Valid types for dns records
9
+ RECORD_TYPE = %w(A AAAA CNAME MX NS PTR SOA SPF SRV TXT)
10
+
11
+ # Create Route53 healthcheck for given fqdn/ip address
12
+ #
13
+ # @param [String] app_name application name
14
+ # @param [String] stack_name stack name
15
+ # @param [String] fqdn fully qualified domain name (FQDN)
16
+ # @param [String] ip_address ip address
17
+ # @param [Integer] port number
18
+ # @param [String] type healthcheck type
19
+ # @param [String] resource_path uri path healthcheck backend would query
20
+ # @param [Integer] request_interval query intervals for healthcheck backend
21
+ # @param [Integer] failure_threshold number of accumulated failures to consider endpoint not healthy
22
+ # @param [Array] tags additional tags
23
+ def create_healthcheck(app_name,
24
+ stack_name,
25
+ fqdn: nil,
26
+ ip_address: nil,
27
+ port: 80,
28
+ type: 'HTTP',
29
+ resource_path: '/',
30
+ request_interval: 30,
31
+ failure_threshold: 3,
32
+ tags: [])
33
+ unless HEALTH_CHECK_TYPE.include?(type)
34
+ fail("Route53 healthcheck type can only be one of the following: #{HEALTH_CHECK_TYPE.join(',')}")
35
+ end
36
+ fail('Route53 healthcheck requires either fqdn or ip address') unless fqdn || ip_address
37
+
38
+ properties = {
39
+ HealthCheckConfig: {
40
+ IPAddress: ip_address,
41
+ FullyQualifiedDomainName: fqdn,
42
+ Port: port,
43
+ Type: type,
44
+ ResourcePath: resource_path,
45
+ RequestInterval: request_interval,
46
+ FailureThreshold: failure_threshold
47
+ }
48
+ }
49
+
50
+ properties[:HealthCheckTags] = [
51
+ {
52
+ Key: 'Application',
53
+ Value: app_name
54
+ },
55
+ {
56
+ Key: 'Stack',
57
+ Value: stack_name
58
+ }
59
+ ]
60
+
61
+ properties[:HealthCheckTags].concat(tags) unless tags.blank?
62
+
63
+ resource "#{app_name}Healthcheck",
64
+ Type: 'AWS::Route53::HealthCheck',
65
+ Properties: properties
66
+ end
67
+
68
+ # [RESERVED] Create new hosted zone
69
+ def create_hosted_zone
70
+ fail('method "create_hosted_zone" is not implemented yet')
71
+ end
72
+
73
+ # TODO: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html
74
+
75
+ # Create new single record set for given hosted zone
76
+ #
77
+ # @param [String] app_name application name
78
+ # @param [String] stack_name stack name
79
+ # @param [String] zone_name hosted zone name
80
+ # @param [String] record_name dns record name
81
+ # @param [Integer] ttl time to live
82
+ # @param [String] type dns record type
83
+ # @param [Hash] healthcheck reference to the healthcheck resource
84
+ # @param [Hash] alias_target alias target
85
+ # @param [Array] resource_records resources associated with record_name
86
+ def create_single_dns_record(app_name,
87
+ stack_name,
88
+ zone_name,
89
+ record_name,
90
+ ttl: 300,
91
+ type: 'A',
92
+ healthcheck: nil,
93
+ zone_id: nil,
94
+ alias_target: {},
95
+ resource_records: [])
96
+ if type && !RECORD_TYPE.include?(type)
97
+ fail("Route53 record type can only be one of the following: #{RECORD_TYPE.join(',')}")
98
+ end
99
+ if healthcheck && (!healthcheck.is_a?(Hash) || !healthcheck.include?(:Ref))
100
+ fail('healthcheck must be a valid cloudformation Ref function')
101
+ end
102
+ if alias_target && (!alias_target.is_a?(Hash))
103
+ fail('AliasTarget must be a Hash')
104
+ end
105
+
106
+ name = app_name || stack_name.titleize.remove(/\s/)
107
+ properties = {
108
+ Name: record_name,
109
+ Comment: "#{type} record for #{[app_name, 'in '].join(' ') if app_name}#{stack_name} stack",
110
+ Type: type
111
+ }
112
+
113
+ # HostedZoneId and HostedZoneName options are mutually exclusive
114
+ if zone_id && !zone_id.nil?
115
+ properties[:HostedZoneId] = zone_id
116
+ else
117
+ properties[:HostedZoneName] = zone_name
118
+ end
119
+
120
+ if !alias_target.blank?
121
+ fail('AliasTarget can be created only for A or AAAA type records') unless %w(A AAAA).include?(type)
122
+ unless alias_target.key?(:HostedZoneId) && alias_target.key?(:DNSName)
123
+ fail('AliasTarget must have HostedZoneId and DNSName properties')
124
+ end
125
+ properties[:AliasTarget] = alias_target
126
+ else
127
+ properties[:TTL] = ttl
128
+ properties[:HealthCheckId] = healthcheck if healthcheck
129
+ properties[:ResourceRecords] = resource_records.empty? ? ref("#{app_name}PublicIpAddress") : resource_records
130
+ end
131
+
132
+ resource "#{name}Hostname",
133
+ Type: 'AWS::Route53::RecordSet',
134
+ Properties: properties
135
+ end
136
+
137
+ # [RESERVED] Create multiple record sets for given hosted zone
138
+ def create_multiple_dns_records
139
+ fail('method "create_multiple_dns_records" is not implemented')
140
+ end
141
+ end # module Route53
142
+ end # module Plugins
143
+ end # module Enscalator
@@ -0,0 +1,85 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Ubuntu appliance
4
+ module Ubuntu
5
+ class << self
6
+ # Supported storage types in AWS
7
+ STORAGE = [:ebs, :'ebs-io1', :'ebs-ssd', :'instance-store']
8
+
9
+ # Supported Ubuntu image architectures
10
+ ARCH = [:amd64, :i386]
11
+
12
+ # Supported Ubuntu releases
13
+ RELEASE = {
14
+ vivid: '15.04',
15
+ utopic: '14.10',
16
+ trusty: '14.04',
17
+ saucy: '13.10',
18
+ raring: '13.04',
19
+ quantal: '12.10',
20
+ precise: '12.04'
21
+ }
22
+
23
+ # Structure to hold parsed record
24
+ Struct.new('Ubuntu', :name, :edition, :state, :timestamp, :root_storage, :arch, :region, :ami, :virtualization)
25
+
26
+ # Get mapping for Ubuntu images
27
+ #
28
+ # @param [Symbol, String] release a codename or version number
29
+ # @param [Symbol] storage storage kind
30
+ # @param [Symbol] arch architecture
31
+ # @raise [ArgumentError] if release is nil, empty or not one of supported values
32
+ # @raise [ArgumentError] if storage is nil, empty or not one of supported values
33
+ # @raise [ArgumentError] if arch is nil, empty or not one of supported values
34
+ # @return [Hash] mapping for Ubuntu amis
35
+ def get_mapping(release: :trusty, storage: :ebs, arch: :amd64)
36
+ fail ArgumentError, 'release can be either codename or version' unless RELEASE.to_a.flatten.include? release
37
+ fail ArgumentError, "storage can only be one of #{STORAGE}" unless STORAGE.include? storage
38
+ fail ArgumentError, "arch can only be one of #{ARCH}" unless ARCH.include? arch
39
+ begin
40
+ version = RELEASE.keys.include?(release) ? release : RELEASE.key(release)
41
+ body = open("https://cloud-images.ubuntu.com/query/#{version}/server/released.current.txt") { |f| f.read }
42
+ body.split("\n").map { |m| m.squeeze("\t").split("\t").reject { |r| r.include? 'aki' } }
43
+ .map { |l| Struct::Ubuntu.new(*l) }
44
+ .select { |r| r.root_storage == storage.to_s && r.arch == arch.to_s }
45
+ .group_by(&:region)
46
+ .map { |k, v| [k, v.map { |i| [i.virtualization, i.ami] }.to_h] }
47
+ .to_h
48
+ .with_indifferent_access
49
+ end
50
+ end
51
+ end # class << self
52
+
53
+ # Create new Ubuntu instance
54
+ #
55
+ # @param [String] instance_name instance name
56
+ # @param [String] storage storage kind (ebs or ephemeral)
57
+ # @param [String] arch architecture (amd64 or i386)
58
+ # @param [String] instance_type instance type
59
+ def ubuntu_init(instance_name,
60
+ storage: :ebs,
61
+ arch: :amd64,
62
+ instance_type: 't2.medium', properties: {})
63
+
64
+ mapping 'AWSUbuntuAMI', Ubuntu.get_mapping(storage: storage, arch: arch)
65
+
66
+ parameter_allocated_storage "Ubuntu#{instance_name}",
67
+ default: 5,
68
+ min: 5,
69
+ max: 1024
70
+
71
+ parameter_ec2_instance_type "Ubuntu#{instance_name}", type: instance_type
72
+
73
+ instance_vpc "Ubuntu#{instance_name}",
74
+ find_in_map('AWSUbuntuAMI', ref('AWS::Region'), 'hvm'),
75
+ ref_application_subnets.first,
76
+ [ref_private_security_group, ref_application_security_group],
77
+ depends_on: [],
78
+ properties: {
79
+ KeyName: ref("Ubuntu#{instance_name}KeyName"),
80
+ InstanceType: ref("Ubuntu#{instance_name}InstanceType")
81
+ }.merge(properties)
82
+ end
83
+ end # Ubuntu
84
+ end # Plugins
85
+ end # Enscalator