man_eb_deployer 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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