Class: Jets::Commands::Deploy

Inherits:
Object
  • Object
show all
Extended by:
Memoist
Includes:
StackInfo
Defined in:
lib/jets/commands/deploy.rb

Instance Method Summary collapse

Methods included from StackInfo

#first_run?, #parent_stack_name, #s3_bucket, #stack_type

Methods included from AwsServices

#apigateway, #aws_lambda, #cfn, #dynamodb, #logs, #s3, #s3_resource, #sns, #sqs, #sts

Methods included from AwsServices::StackStatus

#lookup, #stack_exists?, #stack_in_progress?

Methods included from AwsServices::GlobalMemoist

included

Constructor Details

#initialize(options) ⇒ Deploy

Returns a new instance of Deploy.



7
8
9
# File 'lib/jets/commands/deploy.rb', line 7

def initialize(options)
  @options = options
end

Instance Method Details

#aws_config_update!Object

Override the AWS retry settings during a deploy.

The aws-sdk-core has expondential backup with this formula:

2 ** c.retries * c.config.retry_base_delay

So the max delay will be 2 ** 7 * 0.6 = 76.8s

Only scoping this to deploy because dont want to affect people’s application that use the aws sdk.

There is also additional rate backoff logic elsewhere, since this is only scoped to deploys.

Useful links:

https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-core/lib/aws-sdk-core/plugins/retry_errors.rb
https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html


56
57
58
59
60
61
# File 'lib/jets/commands/deploy.rb', line 56

def aws_config_update!
  Aws.config.update(
    retry_limit: 7, # default: 3
    retry_base_delay: 0.6, # default: 0.3
  )
end

#build_codeObject



84
85
86
# File 'lib/jets/commands/deploy.rb', line 84

def build_code
  Jets::Commands::Build.new(@options).build_code
end

#check_dev_modeObject



77
78
79
80
81
82
# File 'lib/jets/commands/deploy.rb', line 77

def check_dev_mode
  if File.exist?("#{Jets.root}/dev.mode")
    puts "The dev.mode file exists. Please removed it and run bundle update before you deploy.".color(:red)
    exit 1
  end
end

#create_s3_event_bucketsObject



63
64
65
66
67
68
# File 'lib/jets/commands/deploy.rb', line 63

def create_s3_event_buckets
  buckets = Jets::Job::Base.s3_events.keys
  buckets.each do |bucket|
    Jets::AwsServices::S3Bucket.ensure_exists(bucket)
  end
end

#delete_minimal_stackObject



70
71
72
73
74
75
# File 'lib/jets/commands/deploy.rb', line 70

def delete_minimal_stack
  puts "Existing stack is in ROLLBACK_COMPLETE state from a previous failed minimal deploy. Deleting stack and continuing."
  cfn.delete_stack(stack_name: stack_name)
  status.wait
  status.reset
end

#exit_unless_updateable!Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/jets/commands/deploy.rb', line 154

def exit_unless_updateable!
  return if ENV['JETS_FORCE_UPDATEABLE'] # useful for debugging if stack stack updating

  stack_name = Jets::Naming.parent_stack_name
  exists = stack_exists?(stack_name)
  return unless exists # continue because stack could be updating

  stack = cfn.describe_stacks(stack_name: stack_name).stacks.first
  status = stack["stack_status"]
  if status =~ /^ROLLBACK_/ ||
     status =~ /_IN_PROGRESS$/
    region = `aws configure get region`.strip rescue "us-east-1"
    url = "https://console.aws.amazon.com/cloudformation/home?region=#{region}#/stacks"
    puts "The parent stack of the #{Jets.config.project_name.color(:green)} project is not in an updateable state."
    puts "Stack name #{stack_name.color(:yellow)} status #{stack["stack_status"].color(:yellow)}"
    puts "Here's the CloudFormation url to check for more details #{url}"
    exit 1
  end
end

#find_stack(stack_name) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/jets/commands/deploy.rb', line 128

def find_stack(stack_name)
  retries = 0
  resp = cfn.describe_stacks(stack_name: stack_name)
  resp.stacks.first
rescue Aws::CloudFormation::Errors::ValidationError => e
  # example: Stack with id demo-dev does not exist
  if e.message =~ /Stack with/ && e.message =~ /does not exist/
    nil
  else
    raise
  end
rescue Aws::CloudFormation::Errors::Throttling => e
  retries += 1
  seconds = 2 ** retries

  puts "WARN: find_stack #{e.class} #{e.message}".color(:yellow)
  puts "Backing off and will retry in #{seconds} seconds."
  sleep(seconds)
  if seconds > 90 # 2 ** 6 is 64 so will give up after 6 retries
    puts "Giving up after #{retries} retries"
  else
    retry
  end
end

#minimal_rollback_complete?Boolean

Checks for a few things before deciding to delete the parent stack

* Parent stack status status is ROLLBACK_COMPLETE
* Parent resources are in the DELETE_COMPLETE state

Returns:

  • (Boolean)


116
117
118
119
120
121
122
123
124
125
126
# File 'lib/jets/commands/deploy.rb', line 116

def minimal_rollback_complete?
  stack = find_stack(stack_name)
  return false unless stack

  return false unless stack.stack_status == 'ROLLBACK_COMPLETE'

  # Finally check if all the minimal resources in the parent template have been deleted
  resp = cfn.describe_stack_resources(stack_name: stack_name)
  resource_statuses = resp.stack_resources.map(&:resource_status).uniq
  resource_statuses == ['DELETE_COMPLETE']
end

#runObject



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/jets/commands/deploy.rb', line 11

def run
  aws_config_update!
  deployment_env = Jets.config.project_namespace.color(:green)
  puts "Deploying to Lambda #{deployment_env} environment..."
  return if @options[:noop]

  check_dev_mode
  validate_routes!

  # deploy full nested stack when stack already exists
  # Delete existing rollback stack from previous bad minimal deploy
  delete_minimal_stack if minimal_rollback_complete?
  exit_unless_updateable! # Stack could be in a weird rollback state or in progress state

  if first_run?
    ship(stack_type: :minimal)
    Jets.application.reload_configs!
  end

  # Build code after the minimal stack because need s3 bucket for assets
  # on_aws? and s3_base_url logic
  # TODO: possible deploy hook point: before_build
  build_code

  # TODO: possible deploy hook point: before_ship
  create_s3_event_buckets
  ship(stack_type: :full, s3_bucket: s3_bucket)
end

#ship(stack_options) ⇒ Object



96
97
98
99
100
# File 'lib/jets/commands/deploy.rb', line 96

def ship(stack_options)
  options = @options.merge(stack_options) # includes stack_type and s3_bucket
  Jets::Commands::Build.new(options).build_templates
  Jets::Cfn::Ship.new(options).run
end

#stack_nameObject



107
108
109
# File 'lib/jets/commands/deploy.rb', line 107

def stack_name
  Jets::Naming.parent_stack_name
end

#statusObject



102
103
104
# File 'lib/jets/commands/deploy.rb', line 102

def status
  Jets::Cfn::Status.new(stack_name)
end

#validate_routes!Object



88
89
90
91
92
93
94
# File 'lib/jets/commands/deploy.rb', line 88

def validate_routes!
  valid = Jets::Router.validate_routes!
  unless valid
    puts "Deploy fail: The jets application contain invalid routes.".color(:red)
    exit 1
  end
end