man_eb_deployer 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/release.yml +31 -0
  3. data/.github/workflows/test.yml +16 -0
  4. data/.gitignore +12 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +143 -0
  8. data/Gemfile +10 -0
  9. data/LICENSE +22 -0
  10. data/README.md +138 -0
  11. data/Rakefile +12 -0
  12. data/TODOS.md +11 -0
  13. data/bin/eb_deploy +13 -0
  14. data/eb_deployer.gemspec +22 -0
  15. data/lib/eb_deployer/application.rb +96 -0
  16. data/lib/eb_deployer/aws_driver/beanstalk.rb +158 -0
  17. data/lib/eb_deployer/aws_driver/cloud_formation_driver.rb +53 -0
  18. data/lib/eb_deployer/aws_driver/s3_driver.rb +35 -0
  19. data/lib/eb_deployer/aws_driver.rb +8 -0
  20. data/lib/eb_deployer/cf_event_source.rb +26 -0
  21. data/lib/eb_deployer/cloud_formation_provisioner.rb +120 -0
  22. data/lib/eb_deployer/component.rb +45 -0
  23. data/lib/eb_deployer/config_loader.rb +64 -0
  24. data/lib/eb_deployer/default_component.rb +32 -0
  25. data/lib/eb_deployer/default_config.rb +20 -0
  26. data/lib/eb_deployer/default_config.yml +159 -0
  27. data/lib/eb_deployer/deployment_strategy/blue_green.rb +79 -0
  28. data/lib/eb_deployer/deployment_strategy/blue_only.rb +45 -0
  29. data/lib/eb_deployer/deployment_strategy/inplace_update.rb +16 -0
  30. data/lib/eb_deployer/deployment_strategy.rb +20 -0
  31. data/lib/eb_deployer/eb_environment.rb +204 -0
  32. data/lib/eb_deployer/eb_event_source.rb +35 -0
  33. data/lib/eb_deployer/environment.rb +60 -0
  34. data/lib/eb_deployer/event_poller.rb +51 -0
  35. data/lib/eb_deployer/package.rb +39 -0
  36. data/lib/eb_deployer/resource_stacks.rb +20 -0
  37. data/lib/eb_deployer/smoke_test.rb +23 -0
  38. data/lib/eb_deployer/tasks.rb +45 -0
  39. data/lib/eb_deployer/throttling_handling.rb +17 -0
  40. data/lib/eb_deployer/utils.rb +33 -0
  41. data/lib/eb_deployer/version.rb +3 -0
  42. data/lib/eb_deployer/version_cleaner.rb +30 -0
  43. data/lib/eb_deployer.rb +339 -0
  44. data/lib/generators/eb_deployer/install/install_generator.rb +82 -0
  45. data/lib/generators/eb_deployer/install/templates/eb_deployer.rake +1 -0
  46. data/lib/generators/eb_deployer/install/templates/eb_deployer.yml.erb +181 -0
  47. data/lib/generators/eb_deployer/install/templates/ebextensions/01_postgres_packages.config +5 -0
  48. data/lib/generators/eb_deployer/install/templates/postgres_rds.json +88 -0
  49. data/test/aws_driver_stubs.rb +350 -0
  50. data/test/beanstalk_test.rb +23 -0
  51. data/test/blue_green_deploy_test.rb +114 -0
  52. data/test/blue_only_deploy_test.rb +78 -0
  53. data/test/cf_event_poller_test.rb +32 -0
  54. data/test/cloud_formation_provisioner_test.rb +47 -0
  55. data/test/config_loader_test.rb +205 -0
  56. data/test/deploy_test.rb +42 -0
  57. data/test/eb_environment_test.rb +120 -0
  58. data/test/eb_event_poller_test.rb +32 -0
  59. data/test/inplace_update_deploy_test.rb +110 -0
  60. data/test/multi_components_deploy_test.rb +164 -0
  61. data/test/rails_generators_test.rb +67 -0
  62. data/test/resources_deploy_test.rb +191 -0
  63. data/test/smoke_test_test.rb +23 -0
  64. data/test/template_deploy_test.rb +13 -0
  65. data/test/test_helper.rb +68 -0
  66. data/test/tier_setting_deploy_test.rb +24 -0
  67. data/test/versions_deploy_test.rb +120 -0
  68. metadata +176 -0
@@ -0,0 +1,158 @@
1
+ module EbDeployer
2
+ module AWSDriver
3
+ class Beanstalk
4
+ include Utils
5
+ attr_reader :client
6
+
7
+ def initialize(client=Aws::ElasticBeanstalk::Client.new)
8
+ @client = client
9
+ end
10
+
11
+ def create_application(app)
12
+ @client.create_application(:application_name => app)
13
+ end
14
+
15
+ def delete_application(app)
16
+ @client.delete_application(:application_name => app)
17
+ end
18
+
19
+ def application_exists?(app)
20
+ @client.describe_applications(:application_names => [app])[:applications].any?
21
+ end
22
+
23
+ def update_environment_settings(app, env, settings)
24
+ env_id = convert_env_name_to_id(app, [env]).first
25
+ @client.update_environment(:environment_id => env_id, :option_settings => settings)
26
+ end
27
+
28
+ def update_environment(app_name, env_name, version, tier, settings, template_name)
29
+ env_id = convert_env_name_to_id(app_name, [env_name]).first
30
+ request = reject_nil({
31
+ :environment_id => env_id,
32
+ :version_label => version,
33
+ :option_settings => settings,
34
+ :tier => environment_tier(tier),
35
+ :template_name => template_name
36
+ })
37
+ @client.update_environment(request)
38
+ end
39
+
40
+ def environment_exists?(app_name, env_name)
41
+ alive_envs(app_name, [env_name]).any?
42
+ end
43
+
44
+ def environment_names_for_application(app_name)
45
+ alive_envs(app_name).collect { |env| env[:environment_name] }
46
+ end
47
+
48
+ def create_environment(app_name, env_name, stack_name, cname_prefix, version, tier, tags, settings, template_name)
49
+ request = reject_nil({
50
+ :application_name => app_name,
51
+ :environment_name => env_name,
52
+ :solution_stack_name => stack_name,
53
+ :version_label => version,
54
+ :option_settings => settings,
55
+ :tier => environment_tier(tier),
56
+ :cname_prefix => cname_prefix,
57
+ :tags => tags,
58
+ :template_name => template_name
59
+ })
60
+ @client.create_environment(request)
61
+ end
62
+
63
+ def delete_environment(app_name, env_name)
64
+ @client.terminate_environment(:environment_name => env_name)
65
+ end
66
+
67
+ def delete_application_version(app_name, version, delete_source_bundle)
68
+ request = {
69
+ :application_name => app_name,
70
+ :version_label => version,
71
+ :delete_source_bundle => delete_source_bundle
72
+ }
73
+ @client.delete_application_version(request)
74
+ end
75
+
76
+ def create_application_version(app_name, version_label, source_bundle)
77
+ @client.create_application_version(:application_name => app_name,
78
+ :source_bundle => source_bundle,
79
+ :version_label => version_label)
80
+ end
81
+
82
+ def application_version_labels(app_name)
83
+ application_versions(app_name).map { |apv| apv[:version_label] }
84
+ end
85
+
86
+ def application_versions(app_name)
87
+ request = { :application_name => app_name }
88
+ @client.describe_application_versions(request)[:application_versions]
89
+ end
90
+
91
+ def fetch_events(app_name, env_name, params, &block)
92
+ response = @client.describe_events(params.merge(:application_name => app_name,
93
+ :environment_name => env_name))
94
+ return [response[:events], response[:next_token]]
95
+ end
96
+
97
+ def environment_cname_prefix(app_name, env_name)
98
+ cname = environment_cname(app_name, env_name)
99
+ if cname =~ /^([^\.]+)(?:\.(?:[a-z0-9\-]+))?\.elasticbeanstalk\.com/i
100
+ $1
101
+ end
102
+ end
103
+
104
+ def environment_cname(app_name, env_name)
105
+ get_environment_attribute(app_name, env_name, :cname)
106
+ end
107
+
108
+ def environment_health_state(app_name, env_name)
109
+ get_environment_attribute(app_name, env_name, :health)
110
+ end
111
+
112
+ def environment_status(app_name, env_name)
113
+ get_environment_attribute(app_name, env_name, :status)
114
+ end
115
+
116
+ def environment_verion_label(app_name, env_name)
117
+ get_environment_attribute(app_name, env_name, :version_label)
118
+ end
119
+
120
+ def environment_swap_cname(app_name, env1, env2)
121
+ env1_id, env2_id = convert_env_name_to_id(app_name, [env1, env2])
122
+ @client.swap_environment_cnames(:source_environment_id => env1_id,
123
+ :destination_environment_id => env2_id)
124
+ end
125
+
126
+ def list_solution_stack_names
127
+ @client.list_available_solution_stacks[:solution_stacks]
128
+ end
129
+
130
+ private
131
+
132
+ TIERS = [
133
+ {:name=>"Worker", :type=>"SQS/HTTP"},
134
+ {:name=>"WebServer", :type=>"Standard"}
135
+ ]
136
+
137
+ def get_environment_attribute(app_name, env_name, attribute)
138
+ env = alive_envs(app_name, [env_name]).first
139
+ env && env[attribute]
140
+ end
141
+
142
+ def environment_tier(name)
143
+ TIERS.find {|t| t[:name].downcase == name.downcase} || raise("No tier found with name #{name.inspect}")
144
+ end
145
+
146
+ def convert_env_name_to_id(app_name, env_names)
147
+ envs = alive_envs(app_name, env_names)
148
+ envs.map { |env| env[:environment_id] }
149
+ end
150
+
151
+ def alive_envs(app_name, env_names=[])
152
+ envs = @client.describe_environments(:application_name => app_name, :environment_names => env_names)[:environments]
153
+
154
+ envs.select {|e| e[:status] != 'Terminated' }
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,53 @@
1
+ module EbDeployer
2
+ module AWSDriver
3
+ class CloudFormationDriver
4
+
5
+ def initialize
6
+ @client = Aws::CloudFormation::Client.new
7
+ end
8
+
9
+ def stack_exists?(name)
10
+ describe_stack(name)
11
+ true
12
+ rescue Aws::CloudFormation::Errors::ValidationError
13
+ false
14
+ end
15
+
16
+ def create_stack(name, template, opts)
17
+ @client.create_stack(opts.merge(:stack_name => name,
18
+ :template_body => template,
19
+ :parameters => convert_parameters(opts[:parameters])))
20
+ end
21
+
22
+ def update_stack(name, template, opts)
23
+ @client.update_stack(opts.merge(:stack_name => name,
24
+ :template_body => template,
25
+ :parameters => convert_parameters(opts[:parameters])))
26
+ end
27
+
28
+ def query_output(name, key)
29
+ output = describe_stack(name)[:outputs].find { |o| o[:output_key] == key }
30
+ output && output[:output_value]
31
+ end
32
+
33
+ def fetch_events(name, options={})
34
+ response = @client.describe_stack_events(options.merge(:stack_name => name))
35
+ return response.stack_events, response.next_token
36
+ end
37
+
38
+ private
39
+
40
+ def describe_stack(name)
41
+ @client.describe_stacks(:stack_name => name)[:stacks].first
42
+ end
43
+
44
+ def log(msg)
45
+ puts "[#{Time.now.utc}][cloud_formation_driver] #{msg}"
46
+ end
47
+
48
+ def convert_parameters(params)
49
+ params.map { |k, v| {:parameter_key => k, :parameter_value => v}}
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ module EbDeployer
2
+ module AWSDriver
3
+ class S3Driver
4
+ def create_bucket(bucket_name)
5
+ s3.create_bucket(:bucket => bucket_name)
6
+ end
7
+
8
+ def bucket_exists?(bucket_name)
9
+ s3.bucket(bucket_name).exists?
10
+ end
11
+
12
+ def object_length(bucket_name, obj_name)
13
+ obj(bucket_name, obj_name).content_length rescue nil
14
+ end
15
+
16
+ def upload_file(bucket_name, obj_name, file)
17
+ o = obj(bucket_name, obj_name)
18
+ o.upload_file(file)
19
+ end
20
+
21
+ private
22
+ def s3
23
+ Aws::S3::Resource.new(client: Aws::S3::Client.new)
24
+ end
25
+
26
+ def obj(bucket_name, obj_name)
27
+ s3.bucket(bucket_name).object(obj_name)
28
+ end
29
+
30
+ def buckets
31
+ s3.buckets
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ require 'eb_deployer/aws_driver/s3_driver'
2
+ require 'eb_deployer/aws_driver/beanstalk'
3
+ require 'eb_deployer/aws_driver/cloud_formation_driver'
4
+
5
+ module EbDeployer
6
+ module AWSDriver
7
+ end
8
+ end
@@ -0,0 +1,26 @@
1
+ module EbDeployer
2
+ class CfEventSource
3
+ def initialize(stack_name, cf_driver)
4
+ @stack_name = stack_name
5
+ @cf_driver = cf_driver
6
+ end
7
+
8
+ def get_anchor
9
+ events, _ = @cf_driver.fetch_events(@stack_name)
10
+ events.first
11
+ end
12
+
13
+ def fetch_events(from_anchor, &block)
14
+ events, next_token = @cf_driver.fetch_events(@stack_name)
15
+ should_continue = yield(events)
16
+ fetch_next(next_token, &block) if next_token && should_continue
17
+ end
18
+
19
+ private
20
+ def fetch_next(next_token, &block)
21
+ events, next_token = @cf_driver.fetch_events(@stack_name, :next_token => next_token)
22
+ should_continue = yield(events)
23
+ fetch_next(next_token, &block) if next_token && should_continue
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,120 @@
1
+ module EbDeployer
2
+ class ResourceNotInReadyState < StandardError
3
+ end
4
+
5
+ class CloudFormationProvisioner
6
+ SUCCESS_STATS = ["CREATE_COMPLETE", "UPDATE_COMPLETE"]
7
+ FAILED_STATS = ["CREATE_FAILED", "UPDATE_FAILED", "UPDATE_ROLLBACK_COMPLETE"]
8
+
9
+ def initialize(stack_name, cf_driver)
10
+ @stack_name = stack_name
11
+ @cf_driver = cf_driver
12
+ @poller = EventPoller.new(CfEventSource.new(@stack_name, @cf_driver))
13
+ end
14
+
15
+ def provision(resources, tags)
16
+ resources = symbolize_keys(resources)
17
+ template = File.read(resources[:template])
18
+ capabilities = resources[:capabilities] || []
19
+ params = resources[:inputs] || resources[:parameters] || {}
20
+ policy = File.read(resources[:policy]) if resources[:policy]
21
+ override_policy = resources[:override_policy] || false
22
+ anchor = nil
23
+ begin
24
+ if stack_exists?
25
+ anchor = @poller.get_anchor
26
+ update_stack(template, params, capabilities, policy, override_policy, tags)
27
+ else
28
+ create_stack(template, params, capabilities, policy, tags)
29
+ end
30
+ rescue Aws::CloudFormation::Errors::ValidationError => e
31
+ if e.message =~ /No updates are to be performed/
32
+ log(e.message)
33
+ return
34
+ else
35
+ raise
36
+ end
37
+ end
38
+ wait_for_stack_op_terminate(anchor)
39
+ log("Resource stack provisioned successfully")
40
+ end
41
+
42
+ def transform_outputs(resources)
43
+ resources = symbolize_keys(resources)
44
+ outputs = resources[:outputs] || {}
45
+ transforms = resources[:transforms] || {}
46
+ transform_output_to_settings(convert_to_transforms(outputs).merge(transforms))
47
+ end
48
+
49
+ def output(key)
50
+ @cf_driver.query_output(@stack_name, key)
51
+ rescue Aws::CloudFormation::Errors::ValidationError
52
+ raise ResourceNotInReadyState.new("Resource stack not in ready state yet, perhaps you should provision it first?")
53
+ end
54
+
55
+ private
56
+
57
+ #todo: remove duplication
58
+ def symbolize_keys(hash)
59
+ hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
60
+ end
61
+
62
+
63
+ def convert_to_transforms(outputs)
64
+ outputs.inject({}) do |memo, (key, value)|
65
+ memo[key] = lambda { |output_value| value.merge('value' => output_value) }
66
+ memo
67
+ end
68
+ end
69
+
70
+ def update_stack(template, params, capabilities, policy, override_policy, tags)
71
+ opts = {:capabilities => capabilities, :parameters => params, :tags => tags}
72
+ if (policy)
73
+ opts[:stack_policy_during_update_body] = policy if override_policy
74
+ log("Using temporary stack policy to apply resource stack updates") if override_policy
75
+ opts[:stack_policy_body] = policy unless override_policy
76
+ log("Applying new stack policy to existing resource stack") unless override_policy
77
+ end
78
+ @cf_driver.update_stack(@stack_name, template, opts)
79
+ end
80
+
81
+ def stack_exists?
82
+ @cf_driver.stack_exists?(@stack_name)
83
+ end
84
+
85
+ def create_stack(template, params, capabilities, policy, tags)
86
+ opts = {:disable_rollback => true, :capabilities => capabilities, :parameters => params, :tags => tags}
87
+ opts[:stack_policy_body] = policy if policy
88
+ log("Applying stack policy to new resource stack") if policy
89
+ @cf_driver.create_stack(@stack_name, template, opts)
90
+ end
91
+
92
+ def transform_output_to_settings(transforms)
93
+ (transforms || []).inject([]) do |settings, pair|
94
+ key, transform = pair
95
+ settings << transform.call(output(key))
96
+ settings
97
+ end.flatten
98
+ end
99
+
100
+ def wait_for_stack_op_terminate(anchor)
101
+ @poller.poll(anchor) do |event|
102
+ log_event(event)
103
+ if FAILED_STATS.include?(event.resource_status)
104
+ raise "Resource stack update failed!"
105
+ end
106
+
107
+ break if event.logical_resource_id == @stack_name && SUCCESS_STATS.include?(event.resource_status)
108
+ end
109
+ end
110
+
111
+ def log_event(event)
112
+ puts "[#{event.timestamp}][cloud_formation_provisioner] #{event.resource_type}(#{event.logical_resource_id}) #{event.resource_status} \"#{event.resource_status_reason}\""
113
+ end
114
+
115
+
116
+ def log(msg)
117
+ puts "[#{Time.now.utc}][cloud_formation_provisioner] #{msg}"
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,45 @@
1
+ module EbDeployer
2
+ class Component
3
+ attr_reader :name
4
+
5
+ def initialize(name, env, options, eb_driver)
6
+ @name = name
7
+ @env = env
8
+ @eb_driver = eb_driver
9
+ @options = options.dup
10
+ @component_eb_settings = @options.delete(:option_settings) || []
11
+ @component_inactive_settings = @options.delete(:inactive_settings) || []
12
+ strategy_name = @options[:strategy] || @env.strategy_name
13
+ @strategy = DeploymentStrategy.create(self, strategy_name)
14
+ end
15
+
16
+ def cname_prefix
17
+ @options[:cname_prefix] || default_cname_prefix
18
+ end
19
+
20
+ def deploy(version_label, eb_settings, inactive_settings=[])
21
+ @strategy.test_compatibility(create_options)
22
+ @strategy.deploy(version_label,
23
+ eb_settings + @component_eb_settings,
24
+ inactive_settings + @component_inactive_settings)
25
+ end
26
+
27
+ def new_eb_env(suffix=nil, cname_prefix_overriding=nil)
28
+ EbEnvironment.new(@env.app_name,
29
+ [@env.name, @name, suffix].compact.join('-'),
30
+ @eb_driver,
31
+ create_options.merge(:cname_prefix => cname_prefix_overriding || cname_prefix))
32
+
33
+ end
34
+
35
+ private
36
+
37
+ def create_options
38
+ @env.creation_opts.merge(@options)
39
+ end
40
+
41
+ def default_cname_prefix
42
+ [@env.app_name, @env.name, @name].join('-')
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,64 @@
1
+ require 'securerandom'
2
+ require 'digest'
3
+ require 'yaml'
4
+
5
+ module EbDeployer
6
+ class ConfigLoader
7
+ include Utils
8
+
9
+ class EvalBinding
10
+ attr_reader :environment, :package_digest
11
+ def initialize(package_digest, env)
12
+ @package_digest = package_digest
13
+ @environment = env
14
+ end
15
+
16
+ def random_hash
17
+ SecureRandom.hex[0..9]
18
+ end
19
+ end
20
+
21
+ def load(options)
22
+ options = options.dup
23
+ package_digest = package_digest(options[:package])
24
+ config_file = options.delete(:config_file)
25
+
26
+ env = options[:environment]
27
+ config_settings = load_config_settings(config_file, package_digest, env)
28
+
29
+ app_name = config_settings[:application]
30
+
31
+ common_settings = symbolize_keys(config_settings[:common])
32
+ common_settings[:version_label] ||= package_digest
33
+
34
+ envs = config_settings[:environments]
35
+ raise "Environment #{env} is not defined in #{config_file}" unless envs.has_key?(env)
36
+ env_settings = symbolize_keys(envs[env] || {})
37
+ env_option_settings = env_settings.delete(:option_settings) || []
38
+
39
+ ret = options.merge(common_settings).merge(env_settings)
40
+ ret[:application] = app_name
41
+ ret[:option_settings] ||= []
42
+ ret[:option_settings] += env_option_settings
43
+ ret
44
+ end
45
+
46
+ private
47
+
48
+ def load_config_settings(config_file, package_digest, env)
49
+ yaml = ERB.new(File.read(config_file)).
50
+ result(eval_binding(package_digest, env))
51
+ symbolize_keys(YAML.load(yaml))
52
+ end
53
+
54
+ def eval_binding(package_digest, env)
55
+ EvalBinding.new(package_digest, env).instance_eval { binding }
56
+ end
57
+
58
+ def package_digest(package)
59
+ return nil unless package
60
+ return package unless File.exist?(package)
61
+ Digest::MD5.file(package).hexdigest
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,32 @@
1
+ module EbDeployer
2
+ class DefaultComponent
3
+ def initialize(env, creation_opts, strategy_name, eb_driver)
4
+ @env = env
5
+ @eb_driver = eb_driver
6
+ @creation_opts = creation_opts
7
+ @strategy = DeploymentStrategy.create(self, strategy_name)
8
+ end
9
+
10
+ def cname_prefix
11
+ @creation_opts[:cname_prefix] || default_cname_prefix
12
+ end
13
+
14
+ def deploy(version_label, eb_settings, inactive_settings=[])
15
+ @strategy.test_compatibility(@creation_opts)
16
+ @strategy.deploy(version_label, eb_settings, inactive_settings)
17
+ end
18
+
19
+ def new_eb_env(suffix=nil, cname_prefix_overriding=nil)
20
+ EbEnvironment.new(@env.app_name,
21
+ [@env.name, suffix].compact.join('-'),
22
+ @eb_driver,
23
+ @creation_opts.merge(:cname_prefix => cname_prefix_overriding || cname_prefix))
24
+ end
25
+
26
+ private
27
+
28
+ def default_cname_prefix
29
+ [@env.app_name, @env.name].join('-')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ module EbDeployer
2
+ class DefaultConfig
3
+ attr_reader :app_name
4
+
5
+ def initialize(app_name)
6
+ @app_name = app_name.gsub('_', '-')
7
+ end
8
+
9
+ def write_to(path)
10
+ FileUtils.mkdir_p(File.dirname(path))
11
+ File.open(path, 'w') { |f| f << ERB.new(File.read(config_template)).result(binding) }
12
+ end
13
+
14
+ private
15
+
16
+ def config_template
17
+ File.expand_path("../default_config.yml", __FILE__)
18
+ end
19
+ end
20
+ end