Class: Ufo::Ship
Instance Method Summary collapse
-
#add_load_balancer!(container, options, target_group) ⇒ Object
Only support Application Load Balancer Think there is an AWS bug that complains about not having the LB name but you cannot pass both a LB Name and a Target Group.
- #cluster_arn ⇒ Object
-
#container_info(task_definition) ⇒ Object
assume only 1 container_definition assume only 1 port mapping in that container_defintion.
-
#create_service ⇒ Object
$ aws ecs create-service –generate-cli-skeleton { “cluster”: “”, “serviceName”: “”, “taskDefinition”: “”, “desiredCount”: 0, “loadBalancers”: [ { “targetGroupArn”: “”, “containerName”: “”, “containerPort”: 0 } ], “role”: “”, “clientToken”: “”, “deploymentConfiguration”: { “maximumPercent”: 0, “minimumHealthyPercent”: 0 } }.
- #deploy ⇒ Object
-
#deployment_complete(deployed_service) ⇒ Object
aws ecs describe-services –services hi-web-prod –cluster prod-hi Passing in the service because we need to capture the deployed task_definition that was actually deployed.
- #ecs_clusters ⇒ Object
- #ensure_cluster_exist ⇒ Object
- #ensure_log_group_exist ⇒ Object
-
#find_all_ecs_services ⇒ Object
find all services on a cluster yields Ufo::ECS::Service object.
- #find_ecs_service ⇒ Object
-
#find_updated_service(service) ⇒ Object
used for polling must pass in a service and cannot use @service for the case of multi_services mode.
-
#initialize(service, task_definition, options = {}) ⇒ Ship
constructor
A new instance of Ship.
- #old_task?(deployed_task_definition_arn, task_definition_arn) ⇒ Boolean
- #process_deployment ⇒ Object
- #service_arns ⇒ Object
- #service_tasks(cluster, service) ⇒ Object
-
#stop_old_task(deployed_service) ⇒ Object
aws ecs list-tasks –cluster prod-hi –service-name gr-web-prod aws ecs describe-tasks –tasks arn:aws:ecs:us-east-1:467446852200:task/09038fd2-f989-4903-a8c6-1bc41761f93f –cluster prod-hi.
- #stop_old_tasks(services) ⇒ Object
-
#target_group_prompt(container) ⇒ Object
Returns the target_group.
- #task_name(task_definition) ⇒ Object
- #task_version(task_definition) ⇒ Object
-
#update_service(ecs_service) ⇒ Object
$ aws ecs update-service –generate-cli-skeleton { “cluster”: “”, “service”: “”, “taskDefinition”: “”, “desiredCount”: 0, “deploymentConfiguration”: { “maximumPercent”: 0, “minimumHealthyPercent”: 0 } } Only thing we want to change is the task-definition.
- #validate_target_group(arn) ⇒ Object
- #wait_for_all_deployments(deployed_services) ⇒ Object
-
#wait_for_deployment(deployed_service, quiet = false) ⇒ Object
service is the returned object from aws-sdk not the @service which is just a String.
Methods included from Util
#default_cluster, #default_params, #display_params, #execute, #pretty_time, #settings
Methods included from AwsService
#cloudwatchlogs, #ecr, #ecs, #elb
Constructor Details
#initialize(service, task_definition, options = {}) ⇒ Ship
Returns a new instance of Ship.
11 12 13 14 15 16 17 18 19 |
# File 'lib/ufo/ship.rb', line 11 def initialize(service, task_definition, ={}) @service = service @task_definition = task_definition @options = @target_group_prompt = @options[:target_group_prompt].nil? ? true : @options[:target_group_prompt] @cluster = @options[:cluster] || default_cluster @wait_for_deployment = @options[:wait].nil? ? true : @options[:wait] @stop_old_tasks = @options[:stop_old_tasks].nil? ? false : @options[:stop_old_tasks] end |
Instance Method Details
#add_load_balancer!(container, options, target_group) ⇒ Object
Only support Application Load Balancer Think there is an AWS bug that complains about not having the LB name but you cannot pass both a LB Name and a Target Group.
261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/ufo/ship.rb', line 261 def add_load_balancer!(container, , target_group) .merge!( load_balancers: [ { container_name: container[:name], container_port: container[:port], target_group_arn: target_group, } ] ) end |
#cluster_arn ⇒ Object
354 355 356 |
# File 'lib/ufo/ship.rb', line 354 def cluster_arn @cluster_arn ||= ecs_clusters.first.cluster_arn end |
#container_info(task_definition) ⇒ Object
assume only 1 container_definition assume only 1 port mapping in that container_defintion
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/ufo/ship.rb', line 312 def container_info(task_definition) Ufo.check_task_definition!(task_definition) task_definition_path = ".ufo/output/#{task_definition}.json" task_definition = JSON.load(IO.read(task_definition_path)) container_def = task_definition["containerDefinitions"].first mappings = container_def["portMappings"] if mappings map = mappings.first port = map["containerPort"] end { name: container_def["name"], port: port } end |
#create_service ⇒ Object
$ aws ecs create-service –generate-cli-skeleton {
"cluster": "",
"serviceName": "",
"taskDefinition": "",
"desiredCount": 0,
"loadBalancers": [
{
"targetGroupArn": "",
"containerName": "",
"containerPort": 0
}
],
"role": "",
"clientToken": "",
"deploymentConfiguration": {
"maximumPercent": 0,
"minimumHealthyPercent": 0
}
}
If the service needs to be created it will get created with some default settings. When does a normal deploy where an update happens only the only thing that ufo will update is the task_definition. The other settings should normally be updated with the ECS console. ‘ufo scale` will allow you to updated the desired_count from the CLI though.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/ufo/ship.rb', line 199 def create_service puts "This service #{@service.colorize(:green)} does not yet exist in the #{@cluster.colorize(:green)} cluster. This deploy will create it." container = container_info(@task_definition) target_group = target_group_prompt(container) = "#{@service} service created on #{@cluster} cluster" if @options[:noop] = "NOOP #{}" else = { cluster: @cluster, service_name: @service, task_definition: @task_definition } = .merge(default_params[:create_service]) unless target_group.nil? || target_group.empty? add_load_balancer!(container, , target_group) end puts "Creating ECS service with params:" display_params() response = ecs.create_service() service = response.service # must set service here since this might never be called if @wait_for_deployment is false end puts unless @options[:mute] service end |
#deploy ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/ufo/ship.rb', line 21 def deploy = "Shipping #{@service}..." unless @options[:mute] if @options[:noop] puts "NOOP: #{}" return else puts .green end end ensure_log_group_exist ensure_cluster_exist process_deployment puts "Software shipped!" unless @options[:mute] end |
#deployment_complete(deployed_service) ⇒ Object
aws ecs describe-services –services hi-web-prod –cluster prod-hi Passing in the service because we need to capture the deployed task_definition that was actually deployed. We use it to pull the describe_services until all the paramters we expect upon a completed deployment are updated.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/ufo/ship.rb', line 158 def deployment_complete(deployed_service) deployed_task_definition = deployed_service.task_definition # want the stale task_definition out of the wa service = find_updated_service(deployed_service) # polling deployment = service.deployments.first # Edge case when another deploy superseds this deploy in this case break out of this loop deployed_task_version = task_version(deployed_task_definition) current_task_version = task_version(service.task_definition) if current_task_version > deployed_task_version raise ShipmentOverridden.new("deployed_task_version was #{deployed_task_version} but task_version is now #{current_task_version}") end (deployment.task_definition == deployed_task_definition && deployment.desired_count == deployment.running_count) end |
#ecs_clusters ⇒ Object
373 374 375 |
# File 'lib/ufo/ship.rb', line 373 def ecs_clusters ecs.describe_clusters(clusters: [@cluster]).clusters end |
#ensure_cluster_exist ⇒ Object
358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/ufo/ship.rb', line 358 def ensure_cluster_exist cluster = ecs_clusters.first unless cluster && cluster.status == "ACTIVE" = "#{@cluster} cluster created." if @options[:noop] = "NOOP #{}" else ecs.create_cluster(cluster_name: @cluster) # TODO: Aad Waiter logic, sometimes the cluster does not exist by the time # we create the service end puts unless @options[:mute] end end |
#ensure_log_group_exist ⇒ Object
39 40 41 |
# File 'lib/ufo/ship.rb', line 39 def ensure_log_group_exist LogGroup.new(@task_definition, @options).create end |
#find_all_ecs_services ⇒ Object
find all services on a cluster yields Ufo::ECS::Service object
334 335 336 337 338 339 340 341 342 |
# File 'lib/ufo/ship.rb', line 334 def find_all_ecs_services ecs_services = [] service_arns.each do |service_arn| ecs_service = Ufo::ECS::Service.new(cluster_arn, service_arn) yield(ecs_service) if block_given? ecs_services << ecs_service end ecs_services end |
#find_ecs_service ⇒ Object
328 329 330 |
# File 'lib/ufo/ship.rb', line 328 def find_ecs_service find_all_ecs_services.find { |ecs_service| ecs_service.service_name == @service } end |
#find_updated_service(service) ⇒ Object
used for polling must pass in a service and cannot use @service for the case of multi_services mode
149 150 151 |
# File 'lib/ufo/ship.rb', line 149 def find_updated_service(service) ecs.describe_services(services: [service.service_name], cluster: @cluster).services.first end |
#old_task?(deployed_task_definition_arn, task_definition_arn) ⇒ Boolean
63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/ufo/ship.rb', line 63 def old_task?(deployed_task_definition_arn, task_definition_arn) puts "deployed_task_definition_arn: #{deployed_task_definition_arn.inspect}" puts "task_definition_arn: #{task_definition_arn.inspect}" deployed_version = deployed_task_definition_arn.split(':').last.to_i version = task_definition_arn.split(':').last.to_i puts "deployed_version #{deployed_version.inspect}" puts "version #{version.inspect}" is_old = version < deployed_version puts "is_old #{is_old.inspect}" is_old end |
#process_deployment ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/ufo/ship.rb', line 43 def process_deployment ecs_service = find_ecs_service deployed_service = if ecs_service # update all existing service update_service(ecs_service) else # create service on the first cluster create_service end wait_for_deployment(deployed_service) if @wait_for_deployment && !@options[:noop] stop_old_task(deployed_service) if @stop_old_tasks end |
#service_arns ⇒ Object
344 345 346 347 348 349 350 351 352 |
# File 'lib/ufo/ship.rb', line 344 def service_arns services = ecs.list_services(cluster: @cluster) list_service_arns = services.service_arns while services.next_token != nil services = ecs.list_services(cluster: @cluster, next_token: services.next_token) list_service_arns += services.service_arns end list_service_arns end |
#service_tasks(cluster, service) ⇒ Object
57 58 59 60 61 |
# File 'lib/ufo/ship.rb', line 57 def service_tasks(cluster, service) all_task_arns = ecs.list_tasks(cluster: cluster, service_name: service).task_arns return [] if all_task_arns.empty? ecs.describe_tasks(cluster: cluster, tasks: all_task_arns).tasks end |
#stop_old_task(deployed_service) ⇒ Object
aws ecs list-tasks –cluster prod-hi –service-name gr-web-prod aws ecs describe-tasks –tasks arn:aws:ecs:us-east-1:467446852200:task/09038fd2-f989-4903-a8c6-1bc41761f93f –cluster prod-hi
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/ufo/ship.rb', line 83 def stop_old_task(deployed_service) deployed_task_definition_arn = deployed_service.task_definition puts "deployed_task_definition_arn #{deployed_task_definition_arn.inspect}" # cannot use @serivce because of multiple mode all_tasks = service_tasks(@cluster, deployed_service.service_name) old_tasks = all_tasks.select do |task| old_task?(deployed_task_definition_arn, task.task_definition_arn) end reason = "Ufo #{Ufo::VERSION} has deployed new code and waited until the newer code is running." puts reason # Stopping old tasks after we have confirmed that the new task definition has the same # number of desired_count and running_count speeds up clean up and ensure that we # dont have any stale code being served. It seems to take a long time for the # ELB to drain the register container otherwise. This might cut off some requests but # providing this as an option that can be turned of beause I've seen deploys go way too # slow. old_tasks.each do |task| puts "stopping task.task_definition_arn #{task.task_definition_arn.inspect}" ecs.stop_task(cluster: @cluster, task: task.task_arn, reason: reason) end if @options[:stop_old_tasks] end |
#stop_old_tasks(services) ⇒ Object
75 76 77 78 79 |
# File 'lib/ufo/ship.rb', line 75 def stop_old_tasks(services) services.each do |service| stop_old_task(service) end end |
#target_group_prompt(container) ⇒ Object
Returns the target_group. Will only allow an target_group and the service to use a load balancer if the container name is “web”.
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/ufo/ship.rb', line 276 def target_group_prompt(container) return if @options[:noop] # If a target_group is provided at the CLI return it right away. return @options[:target_group] if @options[:target_group] # Allows skipping the target group prompt. return unless @target_group_prompt # If the container name is web then it is assume that this is a web service that # needs a target group/elb. return unless container[:name] == 'web' puts "Would you like this service to be associated with an Application Load Balancer?" puts "If yes, please provide the Application Load Balancer Target Group ARN." puts "If no, simply press enter." print "Target Group ARN: " arn = $stdin.gets.strip until arn == '' or validate_target_group(arn) puts "You have provided an invalid Application Load Balancer Target Group ARN: #{arn}." puts "It should be in the form: arn:aws:elasticloadbalancing:us-east-1:123456789:targetgroup/target-name/2378947392743" puts "Please try again or skip adding a Target Group by just pressing enter." print "Target Group ARN: " arn = $stdin.gets.strip end arn end |
#task_name(task_definition) ⇒ Object
377 378 379 380 381 382 |
# File 'lib/ufo/ship.rb', line 377 def task_name(task_definition) # "arn:aws:ecs:us-east-1:123456789:task-definition/hi-web-prod:72" # -> # "task-definition/hi-web-prod:72" task_definition.split('/').last end |
#task_version(task_definition) ⇒ Object
384 385 386 387 |
# File 'lib/ufo/ship.rb', line 384 def task_version(task_definition) # "task-definition/hi-web-prod:72" -> 72 task_name(task_definition).split(':').last.to_i end |
#update_service(ecs_service) ⇒ Object
$ aws ecs update-service –generate-cli-skeleton {
"cluster": "",
"service": "",
"taskDefinition": "",
"desiredCount": 0,
"deploymentConfiguration": {
"maximumPercent": 0,
"minimumHealthyPercent": 0
}
} Only thing we want to change is the task-definition
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/ufo/ship.rb', line 238 def update_service(ecs_service) = "#{ecs_service.service_name} service updated on #{ecs_service.cluster_name} cluster with task #{@task_definition}" if @options[:noop] = "NOOP #{}" else params = { cluster: ecs_service.cluster_arn, # can use the cluster name also since it is unique service: ecs_service.service_arn, # can use the service name also since it is unique task_definition: @task_definition } params = params.merge(default_params[:update_service] || {}) puts "Updating ECS service with params:" display_params(params) response = ecs.update_service(params) service = response.service # must set service here since this might never be called if @wait_for_deployment is false end puts unless @options[:mute] service end |
#validate_target_group(arn) ⇒ Object
303 304 305 306 307 308 |
# File 'lib/ufo/ship.rb', line 303 def validate_target_group(arn) elb.describe_target_groups(target_group_arns: [arn]) true rescue Aws::ElasticLoadBalancingV2::Errors::ValidationError false end |
#wait_for_all_deployments(deployed_services) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/ufo/ship.rb', line 128 def wait_for_all_deployments(deployed_services) start_time = Time.now threads = deployed_services.map do |deployed_service| Thread.new do # http://stackoverflow.com/questions/1383390/how-can-i-return-a-value-from-a-thread-in-ruby Thread.current[:output] = wait_for_deployment(deployed_service, quiet=true) end end threads.each { |t| t.join } total_took = Time.now - start_time puts "" puts "Shipments for all #{deployed_service.size} services took a total of #{pretty_time(total_took).green}." puts "Each deployment took:" threads.each do |t| service_name, took = t[:output] puts " #{service_name}: #{pretty_time(took)}" end end |
#wait_for_deployment(deployed_service, quiet = false) ⇒ Object
service is the returned object from aws-sdk not the @service which is just a String. Returns [service_name, time_took]
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/ufo/ship.rb', line 109 def wait_for_deployment(deployed_service, quiet=false) start_time = Time.now deployed_task_name = task_name(deployed_service.task_definition) puts "Waiting for deployment of task definition #{deployed_task_name.green} to complete" unless quiet begin until deployment_complete(deployed_service) print '.' sleep 5 end rescue ShipmentOverridden => e puts "This deployed was overridden by another deploy" puts e. end puts '' unless quiet took = Time.now - start_time puts "Time waiting for ECS deployment: #{pretty_time(took).green}." unless quiet [deployed_service.service_name, took] end |