# Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved # # Licensed under the BSD-3 license (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License in the root of the project or at # # http://egt-labs.com/mu/LICENSE.html # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. module MU class Cloud class AWS # A server pool as configured in {MU::Config::BasketofKittens::server_pools} class ServerPool < MU::Cloud::ServerPool # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us. # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat def initialize(**args) super @mu_name ||= @deploy.getResourceName(@config['name']) end # Called automatically by {MU::Deploy#createResources} def create MU.setVar("curRegion", @region) if !@region.nil? createUpdateLaunchConfig asg_options = buildOptionsHash MU.log "Creating AutoScale group #{@mu_name}", details: asg_options zones_to_try = @config["zones"] begin asg = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_auto_scaling_group(asg_options) rescue Aws::AutoScaling::Errors::ValidationError => e if zones_to_try != nil and zones_to_try.size > 0 MU.log "#{e.message}, retrying with individual AZs", MU::WARN asg_options[:availability_zones] = [zones_to_try.pop] retry else MU.log e.message, MU::ERR, details: asg_options raise MuError, "#{e.message} creating AutoScale group #{@mu_name}" end end if zones_to_try != nil and zones_to_try.size < @config["zones"].size zones_to_try.each { |zone| begin MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group( auto_scaling_group_name: @mu_name, availability_zones: [zone] ) rescue Aws::AutoScaling::Errors::ValidationError => e MU.log "Couldn't enable Availability Zone #{zone} for AutoScale Group #{@mu_name} (#{e.message})", MU::WARN end } end @cloud_id = @mu_name # Wait and see if we successfully bring up some instances attempts = 0 begin sleep 5 desc = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_auto_scaling_groups(auto_scaling_group_names: [@mu_name]).auto_scaling_groups.first MU.log "Looking for #{desc.min_size} instances in #{@mu_name}, found #{desc.instances.size}", MU::DEBUG attempts = attempts + 1 if attempts > 25 and desc.instances.size == 0 MU.log "No instances spun up after #{5*attempts} seconds, something's wrong with Autoscale group #{@mu_name}", MU::ERR, details: MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_scaling_activities(auto_scaling_group_name: @mu_name).activities raise MuError, "No instances spun up after #{5*attempts} seconds, something's wrong with Autoscale group #{@mu_name}" end end while desc.instances.size < desc.min_size MU.log "#{desc.instances.size} instances spinning up in #{@mu_name}" # If we're holding to bootstrap some nodes, do so, then set our min/max # sizes to their real values. if @config["wait_for_nodes"] > 0 MU.log "Waiting for #{@config["wait_for_nodes"]} nodes to fully bootstrap before proceeding" parent_thread_id = Thread.current.object_id groomthreads = Array.new desc.instances.each { |member| begin groomthreads << Thread.new { MU.dupGlobals(parent_thread_id) MU.log "Initializing #{member.instance_id} in ServerPool #{@mu_name}" MU::MommaCat.lock(member.instance_id+"-mommagroom") begin kitten = MU::Cloud::Server.new(mommacat: @deploy, kitten_cfg: @config, cloud_id: member.instance_id) rescue RuntimeError => e if e.message.match(/can't add a new key into hash during iteration/) MU.log e.message+", retrying", MU::WARN sleep 3 retry else raise e end end MU::MommaCat.lock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies") MU::MommaCat.unlock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies") if !kitten.postBoot(member.instance_id) raise MU::Groomer::RunError, "Failure grooming #{member.instance_id}" end kitten.groom MU::MommaCat.unlockAll } rescue MU::Groomer::RunError => e MU.log "Proceeding after failed initial Groomer run, but #{member.instance_id} may not behave as expected!", MU::WARN, details: e.inspect rescue StandardError => e if !member.nil? and !done MU.log "Aborted before I could finish setting up #{@config['name']}, cleaning it up. Stack trace will print once cleanup is complete.", MU::WARN if !@deploy.nocleanup MU::MommaCat.unlockAll if !@deploy.nocleanup Thread.new { MU.dupGlobals(parent_thread_id) MU::Cloud.resourceClass("AWS", "Server").terminateInstance(id: member.instance_id) } end end raise MuError, e.inspect end } groomthreads.each { |t| t.join } MU.log "Setting min_size to #{@config['min_size']} and max_size to #{@config['max_size']}" MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group( auto_scaling_group_name: @mu_name, min_size: @config['min_size'], max_size: @config['max_size'] ) end if @config['scale_in_protection'] need_instances = @config['scale_in_protection'].match(/^\d+$/) ? @config['scale_in_protection'].to_i : @config['min_size'] setScaleInProtection(need_instances) end return asg end # Make sure we have a set of instances with scale-in protection set which jives with our config # @param need_instances [Integer]: The number of instanceswhich must have scale-in protection set def setScaleInProtection(need_instances = @config['min_size']) live_instances = [] begin desc = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_auto_scaling_groups(auto_scaling_group_names: [@mu_name]).auto_scaling_groups.first live_instances = desc.instances.map { |i| i.instance_id } already_set = 0 desc.instances.each { |i| already_set += 1 if i.protected_from_scale_in } if live_instances.size < need_instances sleep 5 elsif already_set > need_instances unset_me = live_instances.sample(already_set - need_instances) MU.log "Disabling scale-in protection for #{unset_me.size.to_s} instances in #{@mu_name}", MU::NOTICE, details: unset_me MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).set_instance_protection( auto_scaling_group_name: @mu_name, instance_ids: unset_me, protected_from_scale_in: false ) elsif already_set < need_instances live_instances = live_instances.sample(need_instances) MU.log "Enabling scale-in protection for #{@config['scale_in_protection']} instances in #{@mu_name}", details: live_instances begin MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).set_instance_protection( auto_scaling_group_name: @mu_name, instance_ids: live_instances, protected_from_scale_in: true ) rescue Aws::AutoScaling::Errors::ValidationError => e if e.message.match(/not in InService/i) sleep 5 retry else raise e end end end end while live_instances.size < need_instances end # List out the nodes that are members of this pool # @return [Array] def listNodes nodes = [] me = MU::Cloud::AWS::ServerPool.find(cloud_id: cloud_id).values.first if me and me.instances me.instances.each { |instance| found = MU::MommaCat.findStray("AWS", "server", cloud_id: instance.instance_id, region: @region, dummy_ok: true) nodes.concat(found) } end nodes end # Called automatically by {MU::Deploy#createResources} def groom if @config['notifications'] and @config['notifications']['topic'] # XXX expand to a full reference block for a Notification resource arn = if @config['notifications']['topic'].match(/^arn:/) @config['notifications']['topic'] else "arn:#{MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws"}:sns:#{@region}:#{MU::Cloud::AWS.credToAcct(@credentials)}:#{@config['notifications']['topic']}" end eventmap = { "launch" => "autoscaling:EC2_INSTANCE_LAUNCH", "failed_launch" => "autoscaling:EC2_INSTANCE_LAUNCH_ERROR", "terminate" => "autoscaling:EC2_INSTANCE_TERMINATE", "failed_terminate" => "autoscaling:EC2_INSTANCE_TERMINATE_ERROR" } MU.log "Sending simple notifications (#{@config['notifications']['events'].join(", ")}) to #{arn}" MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).put_notification_configuration( auto_scaling_group_name: @mu_name, topic_arn: arn, notification_types: @config['notifications']['events'].map { |e| eventmap[e] } ) end if @config['schedule'] ext_actions = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_scheduled_actions( auto_scaling_group_name: @mu_name ).scheduled_update_group_actions @config['schedule'].each { |s| sched_config = { :auto_scaling_group_name => @mu_name, :scheduled_action_name => s['action_name'] } ['max_size', 'min_size', 'desired_capacity', 'recurrence'].each { |flag| sched_config[flag.to_sym] = s[flag] if s[flag] } ['start_time', 'end_time'].each { |flag| sched_config[flag.to_sym] = Time.parse(s[flag]) if s[flag] } action_already_correct = false ext_actions.each { |ext| if s['action_name'] == ext.scheduled_action_name if !MU.hashCmp(MU.structToHash(ext), sched_config, missing_is_default: true) MU.log "Removing scheduled action #{s['action_name']} from AutoScale group #{@mu_name}" MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_scheduled_action( auto_scaling_group_name: @mu_name, scheduled_action_name: s['action_name'] ) else action_already_correct = true end break end } if !action_already_correct MU.log "Adding scheduled action to AutoScale group #{@mu_name}", MU::NOTICE, details: sched_config MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).put_scheduled_update_group_action( sched_config ) end } end createUpdateLaunchConfig current = cloud_desc asg_options = buildOptionsHash need_tag_update = false oldtags = current.tags.map { |t| t.key+" "+t.value+" "+t.propagate_at_launch.to_s } tag_conf = { :tags => asg_options[:tags] } tag_conf[:tags].each { |t| if !oldtags.include?(t[:key]+" "+t[:value]+" "+t[:propagate_at_launch].to_s) need_tag_update = true end t[:resource_id] = @mu_name t[:resource_type] = "auto-scaling-group" } if need_tag_update MU.log "Updating ServerPool #{@mu_name} with new tags", MU::NOTICE, details: tag_conf[:tags] MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_or_update_tags(tag_conf) current.instances.each { |instance| tag_conf[:tags].each { |t| MU::Cloud::AWS.createTag(instance.instance_id, t[:key], t[:value], region: @region, credentials: @credentials) } } end # XXX actually compare for changes instead of just blindly updating asg_options.delete(:tags) asg_options[:min_size] = @config["min_size"] asg_options[:max_size] = @config["max_size"] asg_options[:new_instances_protected_from_scale_in] = (@config['scale_in_protection'] == "all") if asg_options[:target_group_arns] MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).attach_load_balancer_target_groups( auto_scaling_group_name: @mu_name, target_group_arns: asg_options[:target_group_arns] ) asg_options.delete(:target_group_arns) end MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group(asg_options) if @config['scale_in_protection'] if @config['scale_in_protection'] == "all" setScaleInProtection(listNodes.size) elsif @config['scale_in_protection'] == "initial" setScaleInProtection(@config['min_size']) elsif @config['scale_in_protection'].match(/^\d+$/) setScaleInProtection(@config['scale_in_protection'].to_i) end else setScaleInProtection(0) end ext_pols = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_policies( auto_scaling_group_name: @mu_name ).scaling_policies if @config["scaling_policies"] and @config["scaling_policies"].size > 0 legit_policies = [] @config["scaling_policies"].each { |policy| legit_policies << @deploy.getResourceName("#{@config['name']}-#{policy['name']}") } # Delete any scaling policies we're not configured for ext_pols.each { |ext| if !legit_policies.include?(ext.policy_name) MU.log "Scaling policy #{ext.policy_name} is not named in scaling_policies, removing from #{@mu_name}", MU::NOTICE, details: ext MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_policy( auto_scaling_group_name: @mu_name, policy_name: ext.policy_name ) end } @config["scaling_policies"].each { |policy| policy_name = @deploy.getResourceName("#{@config['name']}-#{policy['name']}") policy_params = { :auto_scaling_group_name => @mu_name, :policy_name => policy_name, :policy_type => policy['policy_type'] } if policy["policy_type"] == "SimpleScaling" policy_params[:cooldown] = policy['cooldown'] policy_params[:scaling_adjustment] = policy['adjustment'] policy_params[:adjustment_type] = policy['type'] elsif policy["policy_type"] == "TargetTrackingScaling" policy_params[:target_tracking_configuration] = MU.strToSym(policy['target_tracking_configuration']) policy_params[:target_tracking_configuration].delete(:preferred_target_group) if policy_params[:target_tracking_configuration][:predefined_metric_specification] and policy_params[:target_tracking_configuration][:predefined_metric_specification][:predefined_metric_type] == "ALBRequestCountPerTarget" lb = @deploy.deployment["loadbalancers"].values.first if @deploy.deployment["loadbalancers"].size > 1 MU.log "Multiple load balancers attached to Autoscale group #{@mu_name}, guessing wildly which one to use for TargetTrackingScaling policy", MU::WARN end lb_path = if lb["targetgroups"].size > 1 if policy['target_tracking_configuration']["preferred_target_group"] and lb["targetgroups"][policy['target_tracking_configuration']["preferred_target_group"]] lb["arn"].split(/:/)[5].sub(/^loadbalancer\//, "")+"/"+lb["targetgroups"][policy['target_tracking_configuration']["preferred_target_group"]].split(/:/)[5] else if policy['target_tracking_configuration']["preferred_target_group"] MU.log "preferred_target_group was set to '#{policy["preferred_target_group"]}' but I don't see a target group by that name", MU::WARN end MU.log "Multiple target groups attached to Autoscale group #{@mu_name}, guessing wildly which one to use for TargetTrackingScaling policy", MU::WARN, details: lb["targetgroups"].keys lb["arn"].split(/:/)[5].sub(/^loadbalancer\//, "")+"/"+lb["targetgroups"].values.first.split(/:/)[5] end end policy_params[:target_tracking_configuration][:predefined_metric_specification][:resource_label] = lb_path end policy_params[:estimated_instance_warmup] = policy['estimated_instance_warmup'] elsif policy["policy_type"] == "StepScaling" step_adjustments = [] policy['step_adjustments'].each{|step| step_adjustments << {:metric_interval_lower_bound => step["lower_bound"], :metric_interval_upper_bound => step["upper_bound"], :scaling_adjustment => step["adjustment"]} } policy_params[:metric_aggregation_type] = policy['metric_aggregation_type'] policy_params[:step_adjustments] = step_adjustments policy_params[:estimated_instance_warmup] = policy['estimated_instance_warmup'] policy_params[:adjustment_type] = policy['type'] end policy_params[:min_adjustment_magnitude] = policy['min_adjustment_magnitude'] if !policy['min_adjustment_magnitude'].nil? policy_already_correct = false ext_pols.each { |ext| if ext.policy_name == policy_name if !MU.hashCmp(MU.structToHash(ext), policy_params, missing_is_default: true) MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_policy( auto_scaling_group_name: @mu_name, policy_name: policy_name ) else policy_already_correct = true end break end } if !policy_already_correct MU.log "Putting scaling policy #{policy_name} for #{@mu_name}", MU::NOTICE, details: policy_params MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).put_scaling_policy(policy_params) end } end end @cloud_desc_cache = nil # Retrieve the AWS descriptor for this Autoscale group # @return [OpenStruct] def cloud_desc(use_cache: true) return @cloud_desc_cache if @cloud_desc_cache and use_cache return nil if !@cloud_id @cloud_desc_cache = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_auto_scaling_groups( auto_scaling_group_names: [@mu_name] ).auto_scaling_groups.first @cloud_desc_cache end # Canonical Amazon Resource Number for this resource # @return [String] def arn cloud_desc.auto_scaling_group_arn end # Retrieve deployment metadata for this Autoscale group # @return [Hash] def notify return MU.structToHash(cloud_desc) end # Locate an existing ServerPool or ServerPools and return an array containing matching AWS resource descriptors for those that match. # @return [Hash]: The cloud provider's complete descriptions of matching ServerPools def self.find(**args) found = {} if args[:cloud_id] resp = MU::Cloud::AWS.autoscale(region: args[:region], credentials: args[:credentials]).describe_auto_scaling_groups({ auto_scaling_group_names: [ args[:cloud_id] ], }) resp.auto_scaling_groups.each { |asg| found[asg.auto_scaling_group_name] = asg } elsif args[:instance_id] # try to reverse map from an instance id to an autoscale group resp = MU::Cloud::AWS.autoscale(region: args[:region], credentials: args[:credentials]).describe_auto_scaling_instances(instance_ids: [args[:instance_id]]) if resp and resp.auto_scaling_instances asg_names = resp.auto_scaling_instances.map { |g| g.auto_scaling_group_name }.uniq asg_names.each { |asg_name| found.merge!(find(cloud_id: asg_name, credentials: args[:credentials], region: args[:region])) } end else next_token = nil begin resp = MU::Cloud::AWS.autoscale(region: args[:region], credentials: args[:credentials]).describe_auto_scaling_groups next_token = resp.next_token resp.auto_scaling_groups.each { |asg| found[asg.auto_scaling_group_name] = asg } end while next_token end # TODO implement the tag-based search return found end # Reverse-map our cloud description into a runnable config hash. # We assume that any values we have in +@config+ are placeholders, and # calculate our own accordingly based on what's live in the cloud. def toKitten(**_args) bok = { "cloud" => "AWS", "credentials" => @credentials, "cloud_id" => @cloud_id, "region" => @region } if !cloud_desc MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config return nil end if cloud_desc.tags and !cloud_desc.tags.empty? cloud_desc.tags.each { |tag| bok['tags'] ||= [] bok['tags'] << { "key" => tag.key, "value" => tag.value } } realname = MU::Adoption.tagsToName(bok['tags'], basename: @cloud_id) if realname bok['name'] = realname bok['name'].gsub!(/[^a-zA-Z0-9_\-]/, "_") end end bok['name'] ||= @cloud_id bok['min_size'] = cloud_desc.min_size bok['max_size'] = cloud_desc.max_size if cloud_desc.launch_configuration_name launch = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_launch_configurations( launch_configuration_names: [cloud_desc.launch_configuration_name] ).launch_configurations.first bok['basis'] = { "launch_config" => { "image_id" => launch.image_id, "name" => bok['name'], "size" => launch.instance_type } } end if cloud_desc.vpc_zone_identifier and !cloud_desc.vpc_zone_identifier.empty? nets = cloud_desc.vpc_zone_identifier.split(/,/) begin resp = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_subnets(subnet_ids: nets).subnets.first bok['vpc'] = MU::Config::Ref.get( id: resp.vpc_id, cloud: "AWS", credentials: @credentials, type: "vpcs", subnets: nets.map { |s| { "subnet_id" => s } } ) rescue Aws::EC2::Errors::InvalidSubnetIDNotFound => e if e.message.match(/The subnet ID '(subnet-[a-f0-9]+)' does not exist/) nets.delete(Regexp.last_match[1]) if nets.empty? MU.log "Autoscale Group #{@cloud_id} was configured for a VPC, but the configuration held no valid subnets", MU::WARN, details: cloud_desc.vpc_zone_identifier.split(/,/) end else raise e end end end # MU.log @cloud_id, MU::NOTICE, details: cloud_desc bok end # Cloud-specific configuration properties. # @param _config [MU::Config]: The calling MU::Config object # @return [Array]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource def self.schema(_config) toplevel_required = [] term_policies = MU::Cloud::AWS.credConfig ? MU::Cloud::AWS.autoscale.describe_termination_policy_types.termination_policy_types : ["AllocationStrategy", "ClosestToNextInstanceHour", "Default", "NewestInstance", "OldestInstance", "OldestLaunchConfiguration", "OldestLaunchTemplate"] schema = { "role_strip_path" => { "type" => "boolean", "default" => false, "description" => "Normally we namespace IAM roles with a +path+ set to match our +deploy_id+; this disables that behavior. Temporary workaround for a bug in EKS/IAM integration." }, "notifications" => { "type" => "object", "description" => "Send notifications to an SNS topic for basic AutoScaling events", "properties" => { "topic" => { "type" => "string", "description" => "The short name or ARN of an SNS topic which should receive notifications for basic Autoscaling events" }, "events" => { "type" => "array", "description" => "The AutoScaling events which should generate a notification", "items" => { "type" => "string", "description" => "The AutoScaling events which should generate a notification", "enum" => ["launch", "failed_launch", "terminate", "failed_terminate"] }, "default" => ["launch", "failed_launch", "terminate", "failed_terminate"] } } }, "generate_iam_role" => { "type" => "boolean", "default" => true, "description" => "Generate a unique IAM profile for this Server or ServerPool.", }, "iam_role" => { "type" => "string", "description" => "An Amazon IAM instance profile, from which to harvest role policies to merge into this node's own instance profile. If generate_iam_role is false, will simple use this profile.", }, "iam_policies" => { "type" => "array", "items" => { "description" => "Amazon-compatible role policies which will be merged into this node's own instance profile. Not valid with generate_iam_role set to false. Our parser expects the role policy document to me embedded under a named container, e.g. { 'name_of_policy':'{ } }", "type" => "object" } }, "canned_iam_policies" => { "type" => "array", "items" => { "description" => "IAM policies to attach, pre-defined by Amazon (e.g. AmazonEKSWorkerNodePolicy)", "type" => "string" } }, "schedule" => { "type" => "array", "items" => { "type" => "object", "required" => ["action_name"], "description" => "Tell AutoScale to alter min/max/desired for this group at a scheduled time, optionally repeating.", "properties" => { "action_name" => { "type" => "string", "description" => "A name for this scheduled action, e.g. 'scale-down-over-night'" }, "start_time" => { "type" => "string", "description" => "When should this one-off scheduled behavior take effect? Times are UTC. Must be a valid Ruby Time.parse() string, e.g. '20:00' or '2014-05-12T08:00:00Z'. If declared along with 'recurrence,' AutoScaling performs the action at this time, and then performs the action based on the specified recurrence." }, "end_time" => { "type" => "string", "description" => "When should this scheduled behavior end? Times are UTC. Must be a valid Ruby Time.parse() string, e.g. '20:00' or '2014-05-12T08:00:00Z'" }, "recurrence" => { "type" => "string", "description" => "A recurring schedule for this action, in Unix cron syntax format (e.g. '0 20 * * *'). Times are UTC." }, "min_size" => {"type" => "integer"}, "max_size" => {"type" => "integer"}, "desired_capacity" => { "type" => "integer", "description" => "The number of Amazon EC2 instances that should be running in the group. Should be between min_size and max_size." }, } } }, "scale_in_protection" => { "type" => "string", "description" => "Protect instances from scale-in termination. Can be 'all', 'initial' (essentially 'min_size'), or an number; note the number needs to be a string, so put it in quotes", "pattern" => "^(all|initial|\\d+)$" }, "scale_with_alb_traffic" => { "type" => "float", "description" => "Shorthand for creating a target_tracking_configuration to scale on ALBRequestCountPerTarget with some reasonable defaults" }, "scale_with_cpu" => { "type" => "float", "description" => "Shorthand for creating a target_tracking_configuration to scale on ASGAverageCPUUtilization with some reasonable defaults" }, "scale_with_network_in" => { "type" => "float", "description" => "Shorthand for creating a target_tracking_configuration to scale on ASGAverageNetworkIn with some reasonable defaults" }, "scale_with_network_out" => { "type" => "float", "description" => "Shorthand for creating a target_tracking_configuration to scale on ASGAverageNetworkOut with some reasonable defaults" }, "termination_policies" => { "type" => "array", "minItems" => 1, "items" => { "type" => "String", "default" => "Default", "enum" => term_policies } }, "scaling_policies" => { "type" => "array", "minItems" => 1, "items" => { "type" => "object", "required" => ["name"], "additionalProperties" => false, "description" => "A custom AWS Autoscale scaling policy for this pool.", "properties" => { "name" => { "type" => "string" }, "alarms" => MU::Config::Alarm.inline, "type" => { "type" => "string", "enum" => ["ChangeInCapacity", "ExactCapacity", "PercentChangeInCapacity"], "description" => "Specifies whether 'adjustment' is an absolute number or a percentage of the current capacity for SimpleScaling and StepScaling. Valid values are ChangeInCapacity, ExactCapacity, and PercentChangeInCapacity." }, "adjustment" => { "type" => "integer", "description" => "The number of instances by which to scale. 'type' determines the interpretation of this number (e.g., as an absolute number or as a percentage of the existing Auto Scaling group size). A positive increment adds to the current capacity and a negative value removes from the current capacity. Used only when policy_type is set to 'SimpleScaling'" }, "cooldown" => { "type" => "integer", "default" => 1, "description" => "The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start." }, "min_adjustment_magnitude" => { "type" => "integer", "description" => "Used when 'type' is set to 'PercentChangeInCapacity', the scaling policy changes the DesiredCapacity of the Auto Scaling group by at least the number of instances specified in the value." }, "policy_type" => { "type" => "string", "enum" => ["SimpleScaling", "StepScaling", "TargetTrackingScaling"], "description" => "'StepScaling' will add capacity based on the magnitude of the alarm breach, 'SimpleScaling' will add capacity based on the 'adjustment' value provided. Defaults to 'SimpleScaling'.", "default" => "SimpleScaling" }, "metric_aggregation_type" => { "type" => "string", "enum" => ["Minimum", "Maximum", "Average"], "description" => "Defaults to 'Average' if not specified. Required when policy_type is set to 'StepScaling'", "default" => "Average" }, "step_adjustments" => { "type" => "array", "minItems" => 1, "items" => { "type" => "object", "title" => "admin", "description" => "Requires policy_type 'StepScaling'", "required" => ["adjustment"], "additionalProperties" => false, "properties" => { "adjustment" => { "type" => "integer", "description" => "The number of instances by which to scale at this specific step. Postive value when adding capacity, negative value when removing capacity" }, "lower_bound" => { "type" => "integer", "description" => "The lower bound value in percentage points above/below the alarm threshold at which to add/remove capacity for this step. Positive value when adding capacity and negative when removing capacity. If this is the first step and capacity is being added this value will most likely be 0" }, "upper_bound" => { "type" => "integer", "description" => "The upper bound value in percentage points above/below the alarm threshold at which to add/remove capacity for this step. Positive value when adding capacity and negative when removing capacity. If this is the first step and capacity is being removed this value will most likely be 0" } } } }, "estimated_instance_warmup" => { "type" => "integer", "description" => "Required when policy_type is set to 'StepScaling'" }, "target_tracking_configuration" => { "type" => "object", "description" => "Required when policy_type is set to 'TargetTrackingScaling' https://docs.aws.amazon.com/sdkforruby/api/Aws/AutoScaling/Types/TargetTrackingConfiguration.html", "required" => ["target_value"], "additionalProperties" => false, "properties" => { "target_value" => { "type" => "float", "description" => "The target value for the metric." }, "preferred_target_group" => { "type" => "string", "description" => "If our load balancer has multiple target groups, prefer the one with this name instead of choosing one arbitrarily" }, "disable_scale_in" => { "type" => "boolean", "description" => "If set to true, new instances created by this policy will not be subject to termination by scaling in.", "default" => false }, "predefined_metric_specification" => { "description" => "A predefined metric. You can specify either a predefined metric or a customized metric. https://docs.aws.amazon.com/sdkforruby/api/Aws/AutoScaling/Types/PredefinedMetricSpecification.html", "type" => "string", "enum" => ["ASGAverageCPUUtilization", "ASGAverageNetworkIn", "ASGAverageNetworkOut", "ALBRequestCountPerTarget"], "default" => "ASGAverageCPUUtilization" }, "customized_metric_specification" => { "type" => "object", "description" => "A customized metric. You can specify either a predefined metric or a customized metric. https://docs.aws.amazon.com/sdkforruby/api/Aws/AutoScaling/Types/TargetTrackingConfiguration.html#customized_metric_specification-instance_method", "additionalProperties" => false, "required" => ["metric_name", "namespace", "statistic"], "properties" => { "metric_name" => { "type" => "string", "description" => "The name of the attribute to monitor eg. CPUUtilization." }, "namespace" => { "type" => "string", "description" => "The name of container 'metric_name' belongs to eg. 'AWS/ApplicationELB'" }, "statistic" => { "type" => "string", "enum" => ["Average", "Minimum", "Maximum", "SampleCount", "Sum"] }, "unit" => { "type" => "string", "description" => "Associated with the 'metric', usually something like Megabits or Seconds" }, "dimensions" => { "type" => "array", "description" => "What resource to monitor with the alarm we are implicitly declaring", "items" => { "type" => "object", "additionalProperties" => false, "required" => ["name", "value"], "description" => "What resource to monitor with the alarm we are implicitly declaring", "properties" => { "name" => { "type" => "string", "description" => "The type of resource we're monitoring, e.g. InstanceId or AutoScalingGroupName" }, "value" => { "type" => "string", "description" => "The name or cloud identifier of the resource we're monitoring" } } } } } } } } } } }, "ingress_rules" => MU::Cloud.resourceClass("AWS", "FirewallRule").ingressRuleAddtlSchema } [toplevel_required, schema] end # Cloud-specific pre-processing of {MU::Config::BasketofKittens::server_pools}, bare and unvalidated. # @param pool [Hash]: The resource to process and validate # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member # @return [Boolean]: True if validation succeeded, False otherwise def self.validateConfig(pool, configurator) ok = true if pool["termination_policy"] valid_policies = MU::Cloud::AWS.autoscale(region: pool['region']).describe_termination_policy_types.termination_policy_types if !valid_policies.include?(pool["termination_policy"]) ok = false MU.log "Termination policy #{pool["termination_policy"]} is not valid in region #{pool['region']}", MU::ERR, details: valid_policies end end if !pool["schedule"].nil? pool["schedule"].each { |s| if !s['min_size'] and !s['max_size'] and !s['desired_capacity'] MU.log "Scheduled action for AutoScale group #{pool['name']} must declare at least one of min_size, max_size, or desired_capacity", MU::ERR ok = false end if !s['start_time'] and !s['recurrence'] MU.log "Scheduled action for AutoScale group #{pool['name']} must declare at least one of start_time or recurrence", MU::ERR ok = false end ['start_time', 'end_time'].each { |time| next if !s[time] begin Time.parse(s[time]) rescue StandardError => e MU.log "Failed to parse #{time} '#{s[time]}' in scheduled action for AutoScale group #{pool['name']}: #{e.message}", MU::ERR ok = false end } if s['recurrence'] and !s['recurrence'].match(/^\s*[\d\-\*]+\s+[\d\-\*]+\s[\d\-\*]+\s[\d\-\*]+\s[\d\-\*]\s*$/) MU.log "Failed to parse recurrence '#{s['recurrence']}' in scheduled action for AutoScale group #{pool['name']}: does not appear to be a valid cron string", MU::ERR ok = false end } end scale_aliases = { "scale_with_alb_traffic" => "ALBRequestCountPerTarget", "scale_with_cpu" => "ASGAverageCPUUtilization", "scale_with_network_in" => "ASGAverageNetworkIn", "scale_with_network_out" => "ASGAverageNetworkOut" } scale_aliases.keys.each { |sp| if pool[sp] pool['scaling_policies'] ||= [] pool['scaling_policies'] << { 'name' => scale_aliases[sp], 'adjustment' => 1, 'policy_type' => "TargetTrackingScaling", 'estimated_instance_warmup' => 60, 'target_tracking_configuration' => { 'target_value' => pool[sp], 'predefined_metric_specification' => scale_aliases[sp] } } end } if !pool["basis"]["launch_config"].nil? launch = pool["basis"]["launch_config"] launch['iam_policies'] ||= pool['iam_policies'] launch['size'] = MU::Cloud.resourceClass("AWS", "Server").validateInstanceType(launch["size"], pool["region"]) ok = false if launch['size'].nil? if !launch['generate_iam_role'] if !launch['iam_role'] and pool['cloud'] != "CloudFormation" MU.log "Must set iam_role if generate_iam_role set to false", MU::ERR ok = false end if !launch['iam_policies'].nil? and launch['iam_policies'].size > 0 MU.log "Cannot mix iam_policies with generate_iam_role set to false", MU::ERR ok = false end end ["generate_iam_role", "iam_role", "canned_iam_policies", "iam_policies"].each { |key| pool[key] = launch[key] if !launch[key].nil? } MU::Cloud.resourceClass("AWS", "Server").generateStandardRole(pool, configurator) launch["ami_id"] ||= launch["image_id"] if launch["server"].nil? and launch["instance_id"].nil? and launch["ami_id"].nil? img_id = MU::Cloud.getStockImage("AWS", platform: pool['platform'], region: pool['region']) if img_id launch['ami_id'] = configurator.getTail("pool"+pool['name']+"AMI", value: img_id, prettyname: "pool"+pool['name']+"AMI", cloudtype: "AWS::EC2::Image::Id") else ok = false MU.log "One of the following MUST be specified for launch_config: server, ami_id, instance_id.", MU::ERR end end if launch["server"] != nil MU::Config.addDependency(pool, launch["server"], "server", their_phase: "groom") # XXX I dunno, maybe toss an error if this isn't done already # servers.each { |server| # if server["name"] == launch["server"] # server["create_ami"] = true # end # } end end if !pool["scaling_policies"].nil? pool["scaling_policies"].each { |policy| if policy['type'] != "PercentChangeInCapacity" and !policy['min_adjustment_magnitude'].nil? MU.log "Cannot specify scaling policy min_adjustment_magnitude if type is not PercentChangeInCapacity", MU::ERR ok = false end if policy["policy_type"] == "SimpleScaling" unless policy["cooldown"] && policy["adjustment"] MU.log "You must specify 'cooldown' and 'adjustment' when 'policy_type' is set to 'SimpleScaling'", MU::ERR ok = false end unless policy['type'] MU.log "You must specify a 'type' when 'policy_type' is set to 'SimpleScaling'", MU::ERR ok = false end elsif policy["policy_type"] == "TargetTrackingScaling" unless policy["target_tracking_configuration"] MU.log "You must specify 'target_tracking_configuration' when 'policy_type' is set to 'TargetTrackingScaling'", MU::ERR ok = false end unless policy["target_tracking_configuration"]["customized_metric_specification"] or policy["target_tracking_configuration"]["predefined_metric_specification"] MU.log "Your target_tracking_configuration must specify one of customized_metric_specification or predefined_metric_specification when 'policy_type' is set to 'TargetTrackingScaling'", MU::ERR ok = false end # we gloss over an annoying layer of indirection in the API here if policy["target_tracking_configuration"]["predefined_metric_specification"] policy["target_tracking_configuration"]["predefined_metric_specification"] = { "predefined_metric_type" => policy["target_tracking_configuration"]["predefined_metric_specification"] } end elsif policy["policy_type"] == "StepScaling" if policy["step_adjustments"].nil? || policy["step_adjustments"].empty? MU.log "You must specify 'step_adjustments' when 'policy_type' is set to 'StepScaling'", MU::ERR ok = false end unless policy['type'] MU.log "You must specify a 'type' when 'policy_type' is set to 'StepScaling'", MU::ERR ok = false end policy["step_adjustments"].each{ |step| if step["adjustment"].nil? MU.log "You must specify 'adjustment' for 'step_adjustments' when 'policy_type' is set to 'StepScaling'", MU::ERR ok = false end if step["adjustment"] >= 1 && policy["estimated_instance_warmup"].nil? MU.log "You must specify 'estimated_instance_warmup' when 'policy_type' is set to 'StepScaling' and adding capacity", MU::ERR ok = false end if step["lower_bound"].nil? && step["upper_bound"].nil? MU.log "You must specify 'lower_bound' and/or upper_bound for 'step_adjustments' when 'policy_type' is set to 'StepScaling'", MU::ERR ok = false end } end if policy["alarms"] && !policy["alarms"].empty? policy["alarms"].each { |alarm| alarm["name"] = "scaling-policy-#{pool["name"]}-#{alarm["name"]}" alarm["cloud"] = "AWS", alarm['dimensions'] = [] if !alarm['dimensions'] alarm['dimensions'] << { "name" => pool["name"], "cloud_class" => "AutoScalingGroupName" } alarm["namespace"] = "AWS/EC2" if alarm["namespace"].nil? alarm['cloud'] = pool['cloud'] alarm['credentials'] = pool['credentials'] alarm['region'] = pool['region'] ok = false if !configurator.insertKitten(alarm, "alarms") } end } end ok end # Does this resource type exist as a global (cloud-wide) artifact, or # is it localized to a region/zone? # @return [Boolean] def self.isGlobal? false end # Denote whether this resource implementation is experiment, ready for # testing, or ready for production use. def self.quality MU::Cloud::RELEASE end # Remove all autoscale groups associated with the currently loaded deployment. # @param noop [Boolean]: If true, will only print what would be done # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server # @param region [String]: The cloud provider region # @return [void] def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {}) MU.log "AWS::ServerPool.cleanup: need to support flags['known']", MU::DEBUG, details: flags filters = [{name: "key", values: ["MU-ID"]}] if !ignoremaster filters << {name: "key", values: ["MU-MASTER-IP"]} end resp = MU::Cloud::AWS.autoscale(credentials: credentials, region: region).describe_tags( filters: filters, max_records: 100 ) return nil if resp.tags.nil? or resp.tags.size == 0 maybe_purge = [] no_purge = [] resp.data.tags.each { |asg| if asg.resource_type != "auto-scaling-group" no_purge << asg.resource_id end if asg.key == "MU-MASTER-IP" and asg.value != MU.mu_public_ip and !ignoremaster no_purge << asg.resource_id end if asg.key == "MU-ID" and asg.value == deploy_id maybe_purge << asg.resource_id end } maybe_purge.each { |resource_id| next if no_purge.include?(resource_id) MU.log "Removing AutoScale group #{resource_id}" next if noop retries = 0 begin MU::Cloud::AWS.autoscale(credentials: credentials, region: region).delete_auto_scaling_group( auto_scaling_group_name: resource_id, # XXX this should obey @force force_delete: true ) rescue Aws::AutoScaling::Errors::InternalFailure => e if retries < 5 MU.log "Got #{e.inspect} while removing AutoScale group #{resource_id}.", MU::WARN sleep 10 retry else MU.log "Failed to delete AutoScale group #{resource_id}", MU::ERR end end # MU::Cloud.resourceClass("AWS", "Server").removeIAMProfile(resource_id) # Generally there should be a launch_configuration of the same name # XXX search for these independently, too? retries = 0 begin MU.log "Removing AutoScale Launch Configuration #{resource_id}" MU::Cloud::AWS.autoscale(credentials: credentials, region: region).delete_launch_configuration( launch_configuration_name: resource_id ) rescue Aws::AutoScaling::Errors::ValidationError => e MU.log "No such Launch Configuration #{resource_id}" rescue Aws::AutoScaling::Errors::InternalFailure => e if retries < 5 MU.log "Got #{e.inspect} while removing Launch Configuration #{resource_id}.", MU::WARN sleep 10 retry else MU.log "Failed to delete Launch Configuration #{resource_id}", MU::ERR end end } return nil end private def createUpdateLaunchConfig return if !@config['basis'] or !@config['basis']["launch_config"] instance_secret = Password.random(50) @deploy.saveNodeSecret("default", instance_secret, "instance_secret") if !@config['basis']['launch_config']["server"].nil? #XXX this isn't how we find these; use findStray or something if @deploy.deployment["images"].nil? or @deploy.deployment["images"][@config['basis']['launch_config']["server"]].nil? raise MuError, "#{@mu_name} needs an AMI from server #{@config['basis']['launch_config']["server"]}, but I don't see one anywhere" end @config['basis']['launch_config']["ami_id"] = @deploy.deployment["images"][@config['basis']['launch_config']["server"]]["image_id"] MU.log "Using AMI '#{@config['basis']['launch_config']["ami_id"]}' from sibling server #{@config['basis']['launch_config']["server"]} in ServerPool #{@mu_name}" elsif !@config['basis']['launch_config']["instance_id"].nil? @config['basis']['launch_config']["ami_id"] = MU::Cloud.resourceClass("AWS", "Server").createImage( name: @mu_name, instance_id: @config['basis']['launch_config']["instance_id"], credentials: @credentials, region: @region )[@region] end MU::Cloud.resourceClass("AWS", "Server").waitForAMI(@config['basis']['launch_config']["ami_id"].to_s, credentials: @credentials) oldlaunch = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_launch_configurations( launch_configuration_names: [@mu_name] ).launch_configurations.first userdata = MU::Cloud.fetchUserdata( platform: @config["platform"], cloud: "AWS", credentials: @credentials, template_variables: { "deployKey" => Base64.urlsafe_encode64(@deploy.public_key), "deploySSHKey" => @deploy.ssh_public_key, "muID" => @deploy.deploy_id, "muUser" => MU.chef_user, "publicIP" => MU.mu_public_ip, "mommaCatPort" => MU.mommaCatPort, "chefVersion" => MU.chefVersion, "adminBucketName" => MU::Cloud::AWS.adminBucketName(@credentials), "windowsAdminName" => @config['windows_admin_username'], "skipApplyUpdates" => @config['skipinitialupdates'], "resourceName" => @config["name"], "resourceType" => "server_pool", "platform" => @config["platform"] }, custom_append: @config['userdata_script'] ) # Figure out which devices are embedded in the AMI already. image = MU::Cloud::AWS.ec2.describe_images(image_ids: [@config["basis"]["launch_config"]["ami_id"]]).images.first if image.nil? raise "#{@config["basis"]["launch_config"]["ami_id"]} does not exist, cannot update/create launch config #{@mu_name}" end ext_disks = {} if !image.block_device_mappings.nil? image.block_device_mappings.each { |disk| if !disk.device_name.nil? and !disk.device_name.empty? and !disk.ebs.nil? and !disk.ebs.empty? ext_disks[disk.device_name] = MU.structToHash(disk.ebs) if ext_disks[disk.device_name].has_key?(:snapshot_id) ext_disks[disk.device_name].delete(:encrypted) end end } end storage = [] if !@config["basis"]["launch_config"]["storage"].nil? @config["basis"]["launch_config"]["storage"].each { |vol| if ext_disks.has_key?(vol["device"]) if ext_disks[vol["device"]].has_key?(:snapshot_id) vol.delete("encrypted") end end mapping, _cfm_mapping = MU::Cloud.resourceClass("AWS", "Server").convertBlockDeviceMapping(vol) storage << mapping } end storage.concat(MU::Cloud.resourceClass("AWS", "Server").ephemeral_mappings) if !oldlaunch.nil? olduserdata = Base64.decode64(oldlaunch.user_data) if userdata == olduserdata and oldlaunch.image_id == @config["basis"]["launch_config"]["ami_id"] and oldlaunch.ebs_optimized == @config["basis"]["launch_config"]["ebs_optimized"] and oldlaunch.instance_type == @config["basis"]["launch_config"]["size"] and oldlaunch.instance_monitoring.enabled == @config["basis"]["launch_config"]["monitoring"] # XXX check more things # launch.block_device_mappings != storage # XXX block device comparison isn't this simple return end # Put our Autoscale group onto a temporary launch config begin MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_launch_configuration( launch_configuration_name: @mu_name+"-TMP", user_data: Base64.encode64(olduserdata), image_id: oldlaunch.image_id, key_name: oldlaunch.key_name, security_groups: oldlaunch.security_groups, instance_type: oldlaunch.instance_type, block_device_mappings: storage, instance_monitoring: oldlaunch.instance_monitoring, iam_instance_profile: oldlaunch.iam_instance_profile, ebs_optimized: oldlaunch.ebs_optimized, associate_public_ip_address: oldlaunch.associate_public_ip_address ) rescue ::Aws::AutoScaling::Errors::ValidationError => e if e.message.match(/Member must have length less than or equal to (\d+)/) MU.log "Userdata script too long updating #{@mu_name} Launch Config (#{Base64.encode64(userdata).size.to_s}/#{Regexp.last_match[1]} bytes)", MU::ERR else MU.log "Error updating #{@mu_name} Launch Config", MU::ERR, details: e.message end raise e.message end MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group( auto_scaling_group_name: @mu_name, launch_configuration_name: @mu_name+"-TMP" ) # ...now back to an identical one with the "real" name MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_launch_configuration( launch_configuration_name: @mu_name ) end # Now to build the new one sgs = [] if @dependencies.has_key?("firewall_rule") @dependencies['firewall_rule'].values.each { |sg| sgs << sg.cloud_id } end launch_options = { :launch_configuration_name => @mu_name, :user_data => Base64.encode64(userdata), :image_id => @config["basis"]["launch_config"]["ami_id"], :key_name => @deploy.ssh_key_name, :security_groups => sgs, :instance_type => @config["basis"]["launch_config"]["size"], :block_device_mappings => storage, :instance_monitoring => {:enabled => @config["basis"]["launch_config"]["monitoring"]}, :ebs_optimized => @config["basis"]["launch_config"]["ebs_optimized"] } if @config["vpc"] or @config["vpc_zone_identifier"] launch_options[:associate_public_ip_address] = @config["associate_public_ip"] end ["kernel_id", "ramdisk_id", "spot_price"].each { |arg| if @config['basis']['launch_config'][arg] launch_options[arg.to_sym] = @config['basis']['launch_config'][arg] end } rolename = nil ['generate_iam_role', 'iam_policies', 'canned_iam_policies', 'iam_role'].each { |field| if !@config['basis']['launch_config'].nil? @config[field] = @config['basis']['launch_config'][field] else @config['basis']['launch_config'][field] = @config[field] end } @config['iam_role'] = @config['basis']['launch_config']['iam_role'] = launch_options[:iam_instance_profile] = MU::Cloud.resourceClass("AWS", "Server").getIAMProfile( @config['name'], @deploy, generated: @config['basis']['launch_config']['generate_iam_role'], role_name: @config['basis']['launch_config']['iam_role'], region: @region, credentials: @credentials ).values.first lc_attempts = 0 begin MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_launch_configuration(launch_options) rescue Aws::AutoScaling::Errors::ValidationError => e if lc_attempts > 3 MU.log "Got error while creating #{@mu_name} Launch Config#{@credentials ? " with credentials #{@credentials}" : ""}: #{e.message}, retrying in 10s", MU::WARN, details: launch_options.reject { |k,_v | k == :user_data } end sleep 5 lc_attempts += 1 retry end if !oldlaunch.nil? # Tell the ASG to use the new one, and nuke the old one MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group( auto_scaling_group_name: @mu_name, launch_configuration_name: @mu_name ) MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_launch_configuration( launch_configuration_name: @mu_name+"-TMP" ) MU.log "Launch Configuration #{@mu_name} replaced" else MU.log "Launch Configuration #{@mu_name} created" end end def buildOptionsHash asg_options = { :auto_scaling_group_name => @mu_name, :launch_configuration_name => @mu_name, :default_cooldown => @config["default_cooldown"], :health_check_type => @config["health_check_type"], :health_check_grace_period => @config["health_check_grace_period"], :tags => [] } MU::MommaCat.listStandardTags.each_pair { |name, value| asg_options[:tags] << {key: name, value: value, propagate_at_launch: true} } if @config['optional_tags'] MU::MommaCat.listOptionalTags.each_pair { |name, value| asg_options[:tags] << {key: name, value: value, propagate_at_launch: true} } end if @config['tags'] @config['tags'].each { |tag| asg_options[:tags] << {key: tag['key'], value: tag['value'], propagate_at_launch: true} } end if @dependencies.has_key?("container_cluster") @dependencies['container_cluster'].values.each { |cc| if cc.config['flavor'] == "EKS" asg_options[:tags] << { key: "kubernetes.io/cluster/#{cc.mu_name}", value: "owned", propagate_at_launch: true } end } end if @config["wait_for_nodes"] > 0 asg_options[:min_size] = @config["wait_for_nodes"] asg_options[:max_size] = @config["wait_for_nodes"] else asg_options[:min_size] = @config["min_size"] asg_options[:max_size] = @config["max_size"] end if @config["loadbalancers"] lbs = [] tg_arns = [] # XXX refactor this into the LoadBalancer resource @config["loadbalancers"].each { |lb| if lb["existing_load_balancer"] lbs << lb["existing_load_balancer"] @deploy.deployment["loadbalancers"] = Array.new if !@deploy.deployment["loadbalancers"] @deploy.deployment["loadbalancers"] << { "name" => lb["existing_load_balancer"], "awsname" => lb["existing_load_balancer"] # XXX probably have to query API to get the DNS name of this one } elsif lb["concurrent_load_balancer"] lb = @deploy.findLitterMate(name: lb['concurrent_load_balancer'], type: "loadbalancers") raise MuError, "No loadbalancers exist! I need one named #{lb['concurrent_load_balancer']}" if !lb lbs << lb.mu_name if lb.targetgroups tg_arns = lb.targetgroups.values.map { |tg| tg.target_group_arn } end end } if tg_arns.size > 0 asg_options[:target_group_arns] = tg_arns end if lbs.size > 0 # asg_options[:load_balancer_names] = lbs end end asg_options[:termination_policies] = @config["termination_policies"] if @config["termination_policies"] asg_options[:desired_capacity] = @config["desired_capacity"] if @config["desired_capacity"] if @config["vpc_zone_identifier"] asg_options[:vpc_zone_identifier] = @config["vpc_zone_identifier"] elsif @config["vpc"] if !@vpc and @config['vpc'].is_a?(MU::Config::Ref) @vpc = @config['vpc'].kitten end subnet_ids = [] if !@vpc raise MuError, "Failed to load vpc for Autoscale Group #{@mu_name}" end if !@config["vpc"]["subnets"].nil? and @config["vpc"]["subnets"].size > 0 @config["vpc"]["subnets"].each { |subnet| subnet_obj = @vpc.getSubnet(cloud_id: subnet["subnet_id"], name: subnet["subnet_name"]) next if !subnet_obj subnet_ids << subnet_obj.cloud_id } else @vpc.subnets.each { |subnet_obj| next if subnet_obj.private? and ["all_public", "public"].include?(@config["vpc"]["subnet_pref"]) next if !subnet_obj.private? and ["all_private", "private"].include?(@config["vpc"]["subnet_pref"]) subnet_ids << subnet_obj.cloud_id } end if subnet_ids.size == 0 raise MuError, "No valid subnets found for #{@mu_name} from #{@config["vpc"]}" end asg_options[:vpc_zone_identifier] = subnet_ids.join(",") end if @config['basis']["server"] srv_name = @config['basis']["server"] # XXX cloudformation bits if @deploy.deployment['servers'] != nil and @deploy.deployment['servers'][srv_name] != nil asg_options[:instance_id] = @deploy.deployment['servers'][srv_name]["instance_id"] end elsif @config['basis']["instance_id"] # TODO should go fetch the name tag or something # XXX cloudformation bits asg_options[:instance_id] = @config['basis']["instance_id"] end if !asg_options[:vpc_zone_identifier].nil? and asg_options[:vpc_zone_identifier].empty? asg_options.delete(:vpc_zone_identifier) end # Do the dance of specifying individual zones if we haven't asked to # use particular VPC subnets. if @config['zones'].nil? and asg_options[:vpc_zone_identifier].nil? @config["zones"] = MU::Cloud::AWS.listAZs(region: @region) MU.log "Using zones from #{@region}", MU::DEBUG, details: @config['zones'] end asg_options[:availability_zones] = @config["zones"] if @config["zones"] != nil asg_options end end end end end