package nl.jpoint.maven.vertx.utils; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.autoscaling.AmazonAutoScaling; import com.amazonaws.services.autoscaling.AmazonAutoScalingClientBuilder; import com.amazonaws.services.autoscaling.model.*; import com.amazonaws.services.autoscaling.model.Instance; import com.amazonaws.services.autoscaling.model.Tag; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2ClientBuilder; import com.amazonaws.services.ec2.model.DescribeInstancesRequest; import com.amazonaws.services.ec2.model.DescribeInstancesResult; import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancing; import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClientBuilder; import com.amazonaws.services.elasticloadbalancing.model.*; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersResult; import nl.jpoint.maven.vertx.mojo.DeployConfiguration; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; public class AwsAutoScalingDeployUtils { private static final String DEPLOY_STICKINESS_POLICY = "deploy-stickiness-policy"; private static final String AUTO_SCALING_GROUP = "auto-scaling-group"; private final AmazonAutoScaling awsAsClient; private final AmazonElasticLoadBalancing awsElbClient; private final AmazonEC2 awsEc2Client; private final DeployConfiguration activeConfiguration; private final Log log; public AwsAutoScalingDeployUtils(String region, DeployConfiguration activeConfiguration, Log log) { this.activeConfiguration = activeConfiguration; this.log = log; awsAsClient = AmazonAutoScalingClientBuilder.standard().withRegion(region).build(); awsElbClient = AmazonElasticLoadBalancingClientBuilder.standard().withRegion(region).build(); awsEc2Client = AmazonEC2ClientBuilder.standard().withRegion(region).build(); activeConfiguration.withAutoScalingGroup(matchAutoScalingGroupName(activeConfiguration.getAutoScalingGroupId())); } public AutoScalingGroup getAutoScalingGroup() { DescribeAutoScalingGroupsResult result = awsAsClient.describeAutoScalingGroups(new DescribeAutoScalingGroupsRequest().withAutoScalingGroupNames(activeConfiguration.getAutoScalingGroupId())); if (result.getAutoScalingGroups().isEmpty()) { log.error("No Autoscaling group found with id : " + activeConfiguration.getAutoScalingGroupId()); throw new IllegalStateException(); } return result.getAutoScalingGroups().get(0); } public void suspendScheduledActions() { awsAsClient.suspendProcesses(new SuspendProcessesRequest() .withScalingProcesses("ScheduledActions", "Terminate", "ReplaceUnhealthy", "AZRebalance", "AlarmNotification") .withAutoScalingGroupName(activeConfiguration.getAutoScalingGroupId())); log.info("Should a build fail the processes can be resumed using the AWS CLI."); log.info("aws autoscaling resume-processes --auto-scaling-group-name " + activeConfiguration.getAutoScalingGroupId() + " --scaling-processes AZRebalance ReplaceUnhealthy Terminate ScheduledActions AlarmNotification"); log.info("Suspended auto scaling processes."); } public void setMinimalCapacity(int cap) { log.info("Set minimal capacity for group to " + cap); awsAsClient.updateAutoScalingGroup(new UpdateAutoScalingGroupRequest().withAutoScalingGroupName(activeConfiguration.getAutoScalingGroupId()).withMinSize(cap)); } public void resumeScheduledActions() { awsAsClient.resumeProcesses(new ResumeProcessesRequest() .withScalingProcesses("ScheduledActions", "Terminate", "ReplaceUnhealthy", "AZRebalance", "AlarmNotification") .withAutoScalingGroupName(activeConfiguration.getAutoScalingGroupId())); log.info("Resumed auto scaling processes."); } public boolean checkInstanceInService(String instanceId) { DescribeInstancesResult instancesResult = awsEc2Client.describeInstances(new DescribeInstancesRequest().withInstanceIds(instanceId)); return instancesResult.getReservations().stream() .flatMap(r -> r.getInstances().stream()) .filter(instance -> instance.getInstanceId().equals(instanceId)) .map(this::toEc2Instance).findFirst().map(ec2Instance -> ec2Instance.isReachable(activeConfiguration.getAwsPrivateIp(), activeConfiguration.getPort(), log)).orElse(false); } public List<Ec2Instance> getInstancesForAutoScalingGroup(Log log, AutoScalingGroup autoScalingGroup) throws MojoFailureException { log.info("retrieving list of instanceId's for auto scaling group with id : " + activeConfiguration.getAutoScalingGroupId()); activeConfiguration.getHosts().clear(); log.debug("describing instances in auto scaling group"); if (autoScalingGroup.getInstances().isEmpty()) { return new ArrayList<>(); } Map<String, Instance> instanceMap = autoScalingGroup.getInstances().stream().collect(Collectors.toMap(Instance::getInstanceId, Function.identity())); try { DescribeInstancesResult instancesResult = awsEc2Client.describeInstances(new DescribeInstancesRequest().withInstanceIds(autoScalingGroup.getInstances().stream().map(Instance::getInstanceId).collect(Collectors.toList()))); List<Ec2Instance> ec2Instances = instancesResult.getReservations().stream().flatMap(r -> r.getInstances().stream()).map(this::toEc2Instance).collect(Collectors.toList()); log.debug("describing elb status"); autoScalingGroup.getLoadBalancerNames().forEach(elb -> this.updateInstancesStateOnLoadBalancer(elb, ec2Instances)); ec2Instances.forEach(i -> i.updateAsState(AwsState.map(instanceMap.get(i.getInstanceId()).getLifecycleState()))); ec2Instances.sort((o1, o2) -> { int sComp = o1.getAsState().compareTo(o2.getAsState()); if (sComp != 0) { return sComp; } else { return o1.getElbState().compareTo(o2.getElbState()); } }); if (activeConfiguration.isIgnoreInStandby()) { return ec2Instances.stream().filter(i -> i.getAsState() != AwsState.STANDBY).collect(Collectors.toList()); } return ec2Instances; } catch (AmazonClientException e) { log.error(e.getMessage(), e); throw new MojoFailureException(e.getMessage()); } } public void enableStickiness(String loadbalancerName, List<Integer> ports) { awsElbClient.createLBCookieStickinessPolicy(new CreateLBCookieStickinessPolicyRequest().withPolicyName(DEPLOY_STICKINESS_POLICY + "-" + loadbalancerName).withLoadBalancerName(loadbalancerName)); describeMatchingElbListeners(loadbalancerName, ports).forEach(l -> enableStickinessOnListener(loadbalancerName, l)); } public void disableStickiness(String loadbalancerName, List<Integer> ports) { describeMatchingElbListeners(loadbalancerName, ports).forEach(l -> disableStickinessOnListener(loadbalancerName, l)); awsElbClient.deleteLoadBalancerPolicy(new DeleteLoadBalancerPolicyRequest().withLoadBalancerName(loadbalancerName).withPolicyName(DEPLOY_STICKINESS_POLICY + "-" + loadbalancerName)); } private List<ListenerDescription> describeMatchingElbListeners(String loadbalancerName, List<Integer> ports) { DescribeLoadBalancersResult loadbalancer = awsElbClient.describeLoadBalancers(new DescribeLoadBalancersRequest().withLoadBalancerNames(loadbalancerName)); LoadBalancerDescription description = loadbalancer.getLoadBalancerDescriptions().get(0); return description.getListenerDescriptions().stream() .filter(d -> ports.contains(d.getListener().getLoadBalancerPort())) .filter(d -> d.getListener().getProtocol().startsWith("HTTP")) .collect(Collectors.toList()); } private void enableStickinessOnListener(String loadbalancerName, ListenerDescription listenerDescription) { log.info("Enable stickiness on loadbalancer " + loadbalancerName + " : " + listenerDescription.getListener().getLoadBalancerPort()); List<String> policyNames = new ArrayList<>(listenerDescription.getPolicyNames()); policyNames.add(DEPLOY_STICKINESS_POLICY + "-" + loadbalancerName); awsElbClient.setLoadBalancerPoliciesOfListener(new SetLoadBalancerPoliciesOfListenerRequest().withLoadBalancerName(loadbalancerName).withPolicyNames(policyNames).withLoadBalancerPort(listenerDescription.getListener().getLoadBalancerPort())); } private void disableStickinessOnListener(String loadbalancerName, ListenerDescription listenerDescription) { log.info("Disable stickiness on loadbalancer " + loadbalancerName + " : " + listenerDescription.getListener().getLoadBalancerPort()); List<String> policyNames = new ArrayList<>(listenerDescription.getPolicyNames()); policyNames.remove(DEPLOY_STICKINESS_POLICY + "-" + loadbalancerName); awsElbClient.setLoadBalancerPoliciesOfListener(new SetLoadBalancerPoliciesOfListenerRequest().withLoadBalancerName(loadbalancerName).withPolicyNames(policyNames).withLoadBalancerPort(listenerDescription.getListener().getLoadBalancerPort())); } public boolean shouldAddExtraInstance(AutoScalingGroup autoScalingGroup) { return autoScalingGroup.getInstances().size() < autoScalingGroup.getMaxSize() && !(activeConfiguration.getMaxCapacity() != -1 && autoScalingGroup.getInstances().size() > activeConfiguration.getMaxCapacity()); } private Ec2Instance toEc2Instance(com.amazonaws.services.ec2.model.Instance instance) { return new Ec2Instance.Builder().withInstanceId(instance.getInstanceId()).withPrivateIp(instance.getPrivateIpAddress()).withPublicIp(instance.getPublicIpAddress()).build(); } public void setDesiredCapacity(AutoScalingGroup autoScalingGroup, Integer capacity) { log.info("Setting desired capacity to : " + capacity); try { awsAsClient.setDesiredCapacity(new SetDesiredCapacityRequest() .withAutoScalingGroupName(autoScalingGroup.getAutoScalingGroupName()) .withDesiredCapacity(capacity) .withHonorCooldown(false)); } catch (AmazonClientException e) { log.error(e.getMessage(), e); } } private void updateInstancesStateOnLoadBalancer(String loadBalancerName, List<Ec2Instance> instances) { DescribeInstanceHealthResult result = awsElbClient.describeInstanceHealth(new DescribeInstanceHealthRequest(loadBalancerName)); instances.forEach(i -> result.getInstanceStates().stream().filter(s -> s.getInstanceId().equals(i.getInstanceId())).findFirst().ifPresent(s -> i.updateState(AwsState.map(s.getState())))); } public void updateInstanceState(Ec2Instance instance, List<String> loadBalancerNames) { for (String elb : loadBalancerNames) { DescribeInstanceHealthResult result = awsElbClient.describeInstanceHealth(new DescribeInstanceHealthRequest(elb)); Optional<InstanceState> state = result.getInstanceStates().stream().filter(s -> s.getInstanceId().equals(instance.getInstanceId())).findFirst(); if (!state.isPresent()) { instance.updateState(AwsState.UNKNOWN); } else { instance.updateState(AwsState.valueOf(state.get().getState().toUpperCase())); } } } public boolean checkInstanceInServiceOnAllElb(Instance newInstance, List<String> loadBalancerNames) { if (newInstance == null) { throw new IllegalStateException("Unable to check null instance"); } for (String elb : loadBalancerNames) { DescribeInstanceHealthResult result = awsElbClient.describeInstanceHealth(new DescribeInstanceHealthRequest(elb)); Optional<InstanceState> state = result.getInstanceStates().stream().filter(s -> s.getInstanceId().equals(newInstance.getInstanceId())).findFirst(); if (!state.isPresent()) { log.info("instance state for instance " + newInstance.getInstanceId() + " on elb " + elb + " is unknown"); return false; } log.info("instance state for instance " + newInstance.getInstanceId() + " on elb " + elb + " is " + state.get().getState()); if (!"InService".equals(state.get().getState())) { return false; } } return true; } public boolean checkEc2Instance(String instanceId) { boolean instanceTerminated = false; try { DescribeInstancesResult result = awsEc2Client.describeInstances(new DescribeInstancesRequest().withInstanceIds(instanceId)); List<com.amazonaws.services.ec2.model.Instance> instances = result.getReservations().stream() .flatMap(r -> r.getInstances().stream()) .filter(i -> i.getInstanceId().equals(instanceId)) .collect(Collectors.toList()); instanceTerminated = instances.isEmpty() || instances.stream() .map(com.amazonaws.services.ec2.model.Instance::getState) .anyMatch(s -> s.getCode().equals(48)); } catch (AmazonServiceException e) { log.info(e.toString(), e); if (e.getStatusCode() == 400) { instanceTerminated = true; } } if (instanceTerminated) { log.warn("Invalid instance " + instanceId + " in group " + activeConfiguration.getAutoScalingGroupId() + ". Detaching instance."); awsAsClient.detachInstances(new DetachInstancesRequest() .withAutoScalingGroupName(activeConfiguration.getAutoScalingGroupId()) .withInstanceIds(instanceId) .withShouldDecrementDesiredCapacity(false)); } return instanceTerminated; } public void setDeployMetadataTags(final String version, Properties properties) { List<Tag> tags = new ArrayList<>(); tags.add(new Tag().withPropagateAtLaunch(true) .withResourceType(AUTO_SCALING_GROUP) .withKey(activeConfiguration.getDeployType().getLatestRequestTag()).withValue(version) .withResourceId(activeConfiguration.getAutoScalingGroupId())); tags.add(new Tag().withPropagateAtLaunch(true) .withResourceType(AUTO_SCALING_GROUP) .withKey(activeConfiguration.getDeployType().getScopeTag()).withValue(Boolean.toString(activeConfiguration.isTestScope())) .withResourceId(activeConfiguration.getAutoScalingGroupId())); if (!activeConfiguration.getAutoScalingProperties().isEmpty()) { tags.add(new Tag().withPropagateAtLaunch(true) .withResourceType(AUTO_SCALING_GROUP) .withKey(activeConfiguration.getDeployType().getPropertiesTag()).withValue(activeConfiguration.getAutoScalingProperties().stream().map(key -> key + ":" + getProperty(key, properties)).collect(Collectors.joining(";"))) .withResourceId(activeConfiguration.getAutoScalingGroupId()) ); } if (!activeConfiguration.getExclusions().isEmpty()) { tags.add(new Tag().withPropagateAtLaunch(true) .withResourceType(AUTO_SCALING_GROUP) .withKey(activeConfiguration.getDeployType().getExclusionTag()).withValue(activeConfiguration.getExclusions().stream().map(e -> e.getGroupId() + ":" + e.getArtifactId()).collect(Collectors.joining(";"))) .withResourceId(activeConfiguration.getAutoScalingGroupId())); } awsAsClient.createOrUpdateTags(new CreateOrUpdateTagsRequest().withTags(tags)); } private String getProperty(String key, Properties properties) { return System.getProperty(key, properties.getProperty(key)); } private String matchAutoScalingGroupName(String regex) { DescribeAutoScalingGroupsResult result = awsAsClient.describeAutoScalingGroups(new DescribeAutoScalingGroupsRequest()); List<String> groups = toGroupNameList(result.getAutoScalingGroups()); while (result.getNextToken() != null && !result.getNextToken().isEmpty()) { result = awsAsClient.describeAutoScalingGroups(new DescribeAutoScalingGroupsRequest().withNextToken(result.getNextToken())); groups.addAll(toGroupNameList(result.getAutoScalingGroups())); } List<String> matchedGroups = groups.stream().filter(name -> name.matches(regex)).collect(Collectors.toList()); if (matchedGroups == null || matchedGroups.isEmpty() || matchedGroups.size() != 1) { int matchSize = matchedGroups == null ? -1 : matchedGroups.size(); if (matchedGroups != null && matchSize > 0) { matchedGroups.forEach(group -> log.error("Matched group : " + group)); } throw new IllegalStateException("Unable to match group regex, matched group size " + matchSize); } return matchedGroups.stream().findFirst().orElse(regex); } private List<String> toGroupNameList(List<AutoScalingGroup> groups) { if (groups == null || groups.isEmpty()) { return new ArrayList<>(); } return groups.stream().map(AutoScalingGroup::getAutoScalingGroupName).collect(Collectors.toList()); } }