/* * Copyright 2008-2010 Xebia and the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * 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. */ package fr.xebia.cloud.amazon.aws.tools; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.ec2.model.*; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalk; import com.amazonaws.services.elasticbeanstalk.model.*; import com.amazonaws.services.route53.AmazonRoute53; import com.amazonaws.services.route53.model.*; import com.google.common.collect.*; import fr.xebia.workshop.monitoring.InfrastructureCreationStep; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.PropertiesCredentials; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.rds.AmazonRDS; import com.amazonaws.services.rds.model.DBInstance; import com.amazonaws.services.rds.model.DBInstanceNotFoundException; import com.amazonaws.services.rds.model.DescribeDBInstancesRequest; import com.amazonaws.services.rds.model.DescribeDBInstancesResult; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; public class AmazonAwsUtils { /** * private constructor for utils class. */ private AmazonAwsUtils() { } /** * <p> * Create EC2 instances and ensure these instances are successfully started. * </p> * <p> * Successfully started means they reached the * {@link InstanceStateName#Running} state. * </p> * <p> * If the startup of an instance failed (e.g. * "Server.InternalError: Internal error on launch"), the instance is * terminated and another one is launched. * </p> * <p> * Max retry count: 3. * </p> * * @param runInstancesRequest * @param ec2 * @return list of "Running" created instances. List size is greater or * equals to given {@link RunInstancesRequest#getMinCount()} */ @Nonnull public static List<Instance> reliableEc2RunInstances(@Nonnull RunInstancesRequest runInstancesRequest, @Nonnull AmazonEC2 ec2) { int initialInstanceMinCount = runInstancesRequest.getMinCount(); int initialInstanceMaxCount = runInstancesRequest.getMaxCount(); try { int tryCount = 1; List<Instance> result = ec2.runInstances(runInstancesRequest).getReservation().getInstances(); result = AmazonAwsUtils.awaitForEc2Instances(result, ec2); //Check for instances state while (result.size() < initialInstanceMinCount && tryCount < 3) { runInstancesRequest.setMinCount(initialInstanceMinCount - result.size()); runInstancesRequest.setMaxCount(initialInstanceMinCount - result.size()); List<Instance> instances = ec2.runInstances(runInstancesRequest).getReservation().getInstances(); instances = AmazonAwsUtils.awaitForEc2Instances(instances, ec2); result.addAll(instances); tryCount++; } //Check for SSH availability for (Iterator<Instance> itInstance = result.iterator(); itInstance.hasNext(); ) { Instance instance = itInstance.next(); try { if (instance.getImageId().equals(InfrastructureCreationStep.GRAPHITE_IMAGE_ID)) { awaitForSshAvailability(instance, "root"); } else { awaitForSshAvailability(instance, "ec2-user"); } } catch (IllegalStateException e) { //Not available => terminate instance ec2.terminateInstances(new TerminateInstancesRequest(Lists.newArrayList(instance.getInstanceId()))); itInstance.remove(); } } if (result.size() < initialInstanceMinCount) { throw new IllegalStateException("Failure to create " + initialInstanceMinCount + " instances, only " + result.size() + " instances (" + Joiner.on(",").join(Collections2.transform(result, AmazonAwsFunctions.EC2_INSTANCE_TO_INSTANCE_ID)) + ") were started on request " + runInstancesRequest); } return result; } finally { // restore runInstancesRequest state runInstancesRequest.setMinCount(initialInstanceMinCount); runInstancesRequest.setMaxCount(initialInstanceMaxCount); } } public final static Predicate<Instance> PREDICATE_RUNNING_OR_PENDING_INSTANCE = new Predicate<Instance>() { @Override public boolean apply(Instance instance) { if (InstanceStateName.Running.toString().equals(instance.getState().getName()) || InstanceStateName.Pending.toString().equals(instance.getState().getName())) { return true; } else { return false; } } }; public final static String AMI_AMZN_LINUX_EU_WEST = "ami-47cefa33"; public final static String AMI_AMZN_LINUX_EU_WEST_2012_03 = "ami-f9231b8d"; public static final String AMI_AMZN_LINUX_EU_WEST_2012_09 = "ami-c37474b7"; /** * Load {@link AWSCredentials} from '<code>AwsCredentials.properties</code> * '. */ @Nonnull public static AWSCredentials loadAwsCredentials() { String credentialsFilePath = "AwsCredentials.properties"; InputStream credentialsAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(credentialsFilePath); Preconditions.checkState(credentialsAsStream != null, "File '" + credentialsFilePath + "' NOT found in the classpath"); try { return new PropertiesCredentials(credentialsAsStream); } catch (IOException e) { throw new IllegalStateException("Exception loading '" + credentialsFilePath + "'", e); } } /** * <p> * Wait for the ec2 instances startup and returns a list of {@link Instance} * with up to date values. * </p> * <p> * Note: some information are missing of the {@link Instance} returned by * {@link AmazonEC2#describeInstances(DescribeInstancesRequest)} as long as * the instance is not "running" (e.g. {@link Instance#getPublicDnsName()}). * </p> * * @param instances * @return up to date instances */ @Nonnull public static List<Instance> awaitForEc2Instances(@Nonnull Iterable<Instance> instances, @Nonnull AmazonEC2 ec2) { List<Instance> result = Lists.newArrayList(); for (Instance instance : instances) { instance = awaitForEc2Instance(instance, ec2); if (instance != null) { result.add(instance); } } return result; } /** * <p> * Wait for the ec2 instance startup and returns it up to date * </p> * <p> * Note: some information are missing of the {@link Instance} returned by * {@link AmazonEC2#describeInstances(DescribeInstancesRequest)} as long as * the instance is not "running" (e.g. {@link Instance#getPublicDnsName()}). * </p> * * @param instance * @return up to date instances or <code>null</code> if the instance crashed * at startup. */ @Nullable public static Instance awaitForEc2Instance(@Nonnull Instance instance, @Nonnull AmazonEC2 ec2) { logger.trace("Wait for startup of {}: {}", instance.getInstanceId(), instance); try { // initially wait for 3 secs to prevent "InvalidInstanceID.NotFound, AWS Error Message: The instance ID 'i-2f79c967' does not exist" Thread.sleep(3 * 1000); } catch (InterruptedException e) { throw Throwables.propagate(e); } int counter = 0; while (InstanceStateName.Pending.equals(instance.getState()) || (instance.getPublicIpAddress() == null) || (instance.getPublicDnsName() == null)) { logger.trace("Wait for startup of {}: {}", instance.getInstanceId(), instance); try { // 3s because ec2 instance creation < 10 seconds Thread.sleep(3 * 1000); } catch (InterruptedException e) { throw Throwables.propagate(e); } DescribeInstancesRequest describeInstancesRequest = new DescribeInstancesRequest().withInstanceIds(instance.getInstanceId()); DescribeInstancesResult describeInstances = ec2.describeInstances(describeInstancesRequest); instance = Iterables.getOnlyElement(toEc2Instances(describeInstances.getReservations())); counter++; if (counter >= 20) { logger.warn("Timeout waiting for startup of {}: {}", instance); return instance; } } if (InstanceStateName.ShuttingDown.equals(instance.getState()) || InstanceStateName.Terminated.equals(instance.getState())) { // typically a "Server.InternalError: Internal error on launch" logger.warn("Terminate and skip dying instance {} (stateReason={}, stateTransitionReason={}): {}", new Object[]{instance.getInstanceId(), instance.getStateReason(), instance.getStateTransitionReason(), instance}); try { ec2.terminateInstances(new TerminateInstancesRequest(Lists.newArrayList(instance.getInstanceId()))); } catch (Exception e) { logger.warn("Silently ignore exception terminating dying instance {}: {}", new Object[]{instance.getInstanceId(), instance, e}); } return null; } logger.debug("Instance {} is started: {}", instance.getInstanceId(), instance); return instance; } /** * Awaits for the given healthcheck url to return 200/OK. If the url did not * return 200/OK within 240 secs, an {@link IllegalStateException} is * raised. * * @param healthCheckUrl * @throws IllegalStateException if the healthCheckUrl did not return 200/OK within 240 * seconds. */ public static void awaitForHttpAvailability(@Nonnull String healthCheckUrl) throws IllegalStateException { RuntimeException cause = null; for (int i = 0; i < 6 * 60; i++) { try { HttpURLConnection healthCheckHttpURLConnection = (HttpURLConnection) new URL(healthCheckUrl).openConnection(); healthCheckHttpURLConnection.setReadTimeout(1000); healthCheckHttpURLConnection.setConnectTimeout(1000); int responseCode = healthCheckHttpURLConnection.getResponseCode(); if (HttpURLConnection.HTTP_OK == responseCode) { logger.info("URL {} is available", healthCheckUrl); return; } else { logger.trace("URL {} is not yet available, responseCode={}", healthCheckUrl, responseCode); cause = new IllegalStateException("'" + healthCheckUrl + "' returned response code " + responseCode); } } catch (IOException e) { cause = new IllegalStateException("Exception invoking '" + healthCheckUrl + "'", e); logger.trace("Exception invoking healthcheck URL {}", healthCheckUrl, e); } try { Thread.sleep(1 * 1000); } catch (InterruptedException e) { throw Throwables.propagate(e); } } throw cause; } /** * Terminate all instances matching the given "Role" tag. * * @param role searched value of the "Role" tag * @param ec2 */ public static void terminateInstancesByRole(@Nonnull String role, @Nonnull AmazonEC2 ec2) { terminateInstancesByTagValue("Role", role, ec2); } /** * Terminate all instances matching the given "Workshop" tag. * * @param workshop searched value of the "Workshop" tag * @param ec2 */ public static void terminateInstancesByWorkshop(@Nonnull String workshop, @Nonnull AmazonEC2 ec2) { terminateInstancesByTagValue("Workshop", workshop, ec2); } /** * Terminate all instances matching the given "Role" and "TeamIdentifier" tags * * @param role searched value of the "Role" tag * @param teamIdentifier searched value of the "TeamIdentifier" tag * @param ec2 */ public static void terminateInstancesByRoleAndTeam(@Nonnull String role, @Nonnull String teamIdentifier, @Nonnull AmazonEC2 ec2) { List<Filter> filters = new ArrayList<Filter>(); filters.add(getFilter("Role", role)); filters.add(getFilter("TeamIdentifier", teamIdentifier)); terminateInstancesByFilter(ec2, filters); } /** * Terminate all instances matching the given "Role" and "TeamIdentifier" tags * * @param role searched value of the "Role" tag * @param teamIdentifiers searched values of the "TeamIdentifier" tag * @param ec2 */ public static void terminateInstancesByRoleAndTeam(@Nonnull String role, @Nonnull Collection<String> teamIdentifiers, @Nonnull AmazonEC2 ec2) { for (String teamIdentifier : teamIdentifiers) { terminateInstancesByRoleAndTeam(role, teamIdentifier, ec2); } } /** * Terminate all instances matching the given tag. * * @param tagName * @param tagValue if <code>null</code>, terminate all the instances having the * given tag defined. * @param ec2 */ public static void terminateInstancesByTagValue(@Nonnull String tagName, @Nullable String tagValue, @Nonnull AmazonEC2 ec2) { Filter filter = getFilter(tagName, tagValue); terminateInstancesByFilter(ec2, Arrays.asList(filter)); } /** * Terminate EC2 instances matching all the filters given in parameters * * @param ec2 ec2 root object * @param filters all filters to be matched by the searched instances */ private static void terminateInstancesByFilter(AmazonEC2 ec2, List<Filter> filters) { DescribeInstancesRequest describeInstancesWithRoleRequest = new DescribeInstancesRequest(). // withFilters(filters); DescribeInstancesResult describeInstancesResult = ec2.describeInstances(describeInstancesWithRoleRequest); Iterable<Instance> exstingInstances = AmazonAwsUtils.toEc2Instances(describeInstancesResult.getReservations()); List<String> instanceIds = Lists .newArrayList(Iterables.transform(exstingInstances, AmazonAwsFunctions.EC2_INSTANCE_TO_INSTANCE_ID)); if (instanceIds.isEmpty()) { logger.debug("No server tagged with filter '{}' to terminate", filters); } else { logger.info("Terminate servers tagged with filter '{}'", filters); ec2.terminateInstances(new TerminateInstancesRequest(instanceIds)); } } /** * Give a filter over Tags attribute of EC2 instance with the given tag name and value * * @param tagName tag name to be searched * @param tagValue value for the given tag name * @return EC2 Filter */ private static Filter getFilter(String tagName, String tagValue) { Filter filter; if (tagValue == null) { filter = new Filter("tag:" + tagName); } else { filter = new Filter("tag:" + tagName, Arrays.asList(tagValue)); } return filter; } /** * Converts a collection of {@link Reservation} into a collection of their * underlying * * @param reservations * @return */ @Nonnull public static Iterable<Instance> toEc2Instances(@Nonnull Iterable<Reservation> reservations) { Iterable<List<Instance>> collectionOfListOfInstances = Iterables.transform(reservations, new Function<Reservation, List<Instance>>() { @Override public List<Instance> apply(Reservation reservation) { return reservation.getInstances(); } }); return Iterables.concat(collectionOfListOfInstances); } private static final Logger logger = LoggerFactory.getLogger(AmazonAwsUtils.class); /** * <p> * Wait for the database creation and returns a {@link DBInstance} with up * to date values. * </p> * <p> * Note: some information are missing of the {@link DBInstance} returned by * {@link AmazonRDS#describeDBInstances(DescribeDBInstancesRequest)} as long * as the instance is "creating" rather than "available" (e.g. * {@link DBInstance#getEndpoint()} that holds the ip address). * </p> * * @param dbInstance * @param rds * @return up to date 'available' dbInstance * @throws DBInstanceNotFoundException the given <code>dbInstance</code> no longer exists (e.g. it * has been destroyed, etc). */ @Nonnull public static DBInstance awaitForDbInstanceCreation(@Nonnull DBInstance dbInstance, @Nonnull AmazonRDS rds) throws DBInstanceNotFoundException { logger.info("Get Instance " + dbInstance.getDBInstanceIdentifier() + "/" + dbInstance.getDBName() + " status"); while (!"available".equals(dbInstance.getDBInstanceStatus())) { logger.info("Instance " + dbInstance.getDBInstanceIdentifier() + "/" + dbInstance.getDBName() + " not yet available, sleep..."); try { // 20 s because MySQL creation takes much more than 60s Thread.sleep(20 * 1000); } catch (InterruptedException e) { throw Throwables.propagate(e); } DescribeDBInstancesRequest describeDbInstanceRequest = new DescribeDBInstancesRequest().withDBInstanceIdentifier(dbInstance .getDBInstanceIdentifier()); DescribeDBInstancesResult describeDBInstancesResult = rds.describeDBInstances(describeDbInstanceRequest); List<DBInstance> dbInstances = describeDBInstancesResult.getDBInstances(); Preconditions.checkState(dbInstances.size() == 1, "Exactly 1 db instance expected : %S", dbInstances); dbInstance = Iterables.getFirst(dbInstances, null); } return dbInstance; } /** * <p> * Test for SSH availability on <i>instance</i>. * If after 5 minutes the instance is not available, and {@link IllegalStateException} is thrown * </p> * <p> * See <a href= * "http://sthen.blogspot.com/2008/03/sftp-i-java-with-jsch-using-private-key.html" * >SFTP in Java with JSch Using Private Key Authentication </a> * </p> * <p> * See {@link JSch#addIdentity(String, byte[], byte[], byte[])} with the * priv key as bytes. * </p> * * @param instance The ec2 instance to test * @param username * @see JSch#addIdentity(String, byte[], byte[], byte[]) */ public static void awaitForSshAvailability(Instance instance, String username) { try { final String ip = instance.getPublicIpAddress(); final String host = instance.getPublicDnsName(); //Read key file InputStream keyFile = Thread.currentThread().getContextClassLoader().getResourceAsStream(instance.getKeyName() + ".pem"); byte[] keyAsByte = new byte[keyFile.available()]; keyFile.read(keyAsByte); JSch jSch = new JSch(); java.util.Properties config = new java.util.Properties(); //Dont check host name config.put("StrictHostKeyChecking", "no"); //Use ip instead of host to prevent dns replication latency Session session = jSch.getSession(username, ip); session.setConfig(config); jSch.addIdentity(username, keyAsByte, null, new byte[0]); for (int i = 0; i < 60; i++) { try { session.connect(5000); session.disconnect(); logger.info("Instance " + host + " is valid: 'ssh -i " + instance.getKeyName() + ".pem" + " ec2-user@" + instance.getPublicIpAddress() + "' SUCCESSFUL"); return; } catch (JSchException jsche) { logger.debug("SSH Test - Instance not (yet) ready (" + host + ") : " + jsche.getMessage()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } } logger.error("Instance " + host + " is not valid !"); throw new IllegalStateException("Instance is read after 5 minutes"); } catch (FileNotFoundException e) { Throwables.propagate(e); } catch (IOException e) { Throwables.propagate(e); } catch (JSchException e) { Throwables.propagate(e); } } /** * Delete given application and wait for its removal. * * @param applicationName * @param beanstalk */ public static void deleteBeanstalkApplicationIfExists(@Nonnull String applicationName, @Nonnull AWSElasticBeanstalk beanstalk) { ApplicationDescription application = Iterables.getFirst(beanstalk.describeApplications(new DescribeApplicationsRequest().withApplicationNames(applicationName)).getApplications(), null); if (application == null) { logger.debug("No application named '{}' found", applicationName); return; } synchronousTerminateEnvironments(applicationName, beanstalk); beanstalk.deleteApplication(new DeleteApplicationRequest().withApplicationName(applicationName)); logger.info("Existing application {} deleted", applicationName); int counter = 0; while (application != null && counter < 1 * 60) { logger.debug("Wait for deletion of application {}", application); try { Thread.sleep(1000); } catch (InterruptedException e) { throw Throwables.propagate(e); } application = Iterables.getFirst(beanstalk.describeApplications(new DescribeApplicationsRequest().withApplicationNames(applicationName)).getApplications(), null); } if (application == null) { logger.info("Application {} successfully deleted", applicationName); } else { logger.warn("Failure to delete application {}", applicationName); } } public static void synchronousTerminateEnvironments(@Nonnull String applicationName, @Nonnull AWSElasticBeanstalk beanstalk) { Set<String> statusToTerminate = Sets.newHashSet("Launching", "Updating", "Ready"); Set<String> statusTerminating = Sets.newHashSet("Terminating"); List<EnvironmentDescription> environments = beanstalk.describeEnvironments(new DescribeEnvironmentsRequest().withApplicationName(applicationName)).getEnvironments(); List<EnvironmentDescription> environmentsToWaitFor = Collections.emptyList(); int counter = 0; while (counter < 1 * 60) { environmentsToWaitFor = Lists.newArrayList(); for (EnvironmentDescription environment : environments) { if (statusToTerminate.contains(environment.getStatus())) { TerminateEnvironmentResult terminateEnvironmentResult = beanstalk.terminateEnvironment(new TerminateEnvironmentRequest().withEnvironmentId(environment.getEnvironmentId())); logger.debug("Terminate environment {}, status:{} - ", new Object[]{environment.getEnvironmentName(), environment.getStatus(), terminateEnvironmentResult}); environmentsToWaitFor.add(environment); } else if (statusTerminating.contains(environment.getStatus())) { environmentsToWaitFor.add(environment); logger.debug("Skip termination of not running environment {}", environment); } else { logger.trace("skip terminated environment {}", environment); } } if(environmentsToWaitFor .isEmpty()) { break; } else { try { Thread.sleep(500); } catch (Exception e) { throw Throwables.propagate(e); } environments = beanstalk.describeEnvironments(new DescribeEnvironmentsRequest().withApplicationName(applicationName)).getEnvironments(); } } if (!environmentsToWaitFor.isEmpty()) { logger.warn("Failure to terminate {}", environmentsToWaitFor); } } public static void createTags(Instance instance, CreateTagsRequest createTagsRequest, AmazonEC2 ec2) { // "AWS Error Code: InvalidInstanceID.NotFound, AWS Error Message: The instance ID 'i-d1638198' does not exist" AmazonAwsUtils.awaitForEc2Instance(instance, ec2); try { ec2.createTags(createTagsRequest); } catch (AmazonServiceException e) { // retries 5s later try { Thread.sleep(5 * 1000); } catch (InterruptedException e1) { e1.printStackTrace(); } ec2.createTags(createTagsRequest); } } public static void deleteCnameIfExist(Iterable<String> cnames, HostedZone hostedZone, AmazonRoute53 route53) { // List all ListResourceRecordSetsRequest listResourceRecordSetsRequest = new ListResourceRecordSetsRequest() // .withStartRecordType(RRType.CNAME) .withHostedZoneId(hostedZone.getId()); ListResourceRecordSetsResult listResourceRecordSetsResult = route53.listResourceRecordSets(listResourceRecordSetsRequest); if (listResourceRecordSetsResult.isTruncated()) { logger.warn("truncated result"); } Function<ResourceRecordSet, String> cnameExtractor = new Function<ResourceRecordSet, String>() { @Override public String apply(@Nullable ResourceRecordSet resourceRecordSet) { if (resourceRecordSet == null) { return null; } if (!RRType.CNAME.equals(RRType.fromValue(resourceRecordSet.getType()))) { return null; } return resourceRecordSet.getName(); } }; Iterable<ResourceRecordSet> existingCnamesAsResourceRecordSet = Iterables.filter(listResourceRecordSetsResult.getResourceRecordSets(), new Predicate<ResourceRecordSet>() { @Override public boolean apply(@Nullable ResourceRecordSet resourceRecordSet) { return RRType.CNAME.equals(RRType.fromValue(resourceRecordSet.getType())); } }); final ImmutableMap<String, ResourceRecordSet> existingCnames = Maps.uniqueIndex(existingCnamesAsResourceRecordSet, cnameExtractor); Sets.SetView<String> cnamesToDelete = Sets.intersection(Sets.newHashSet(cnames), existingCnames.keySet()); Function<String, Change> cnameToDeleteCnameChange = new Function<String, Change>() { @Override public Change apply(@Nullable String cname) { ResourceRecordSet existingResourceRecordSet = existingCnames.get(cname); return new Change() .withAction(ChangeAction.DELETE) .withResourceRecordSet(new ResourceRecordSet() .withType(RRType.CNAME) .withName(cname) .withTTL(existingResourceRecordSet.getTTL()) .withResourceRecords(existingResourceRecordSet.getResourceRecords())); } }; List<Change> changes = Lists.newArrayList(Iterables.transform(cnamesToDelete, cnameToDeleteCnameChange)); if (changes.isEmpty()) { logger.debug("No CNAME to delete"); return; } logger.info("Delete CNAME changes {}", changes); ChangeResourceRecordSetsRequest changeResourceRecordSetsRequest = new ChangeResourceRecordSetsRequest() .withHostedZoneId(hostedZone.getId()) .withChangeBatch(new ChangeBatch().withChanges(changes)); route53.changeResourceRecordSets(changeResourceRecordSetsRequest); } public static void createCnamesForInstances(Map<String, Instance> cnameToInstances, HostedZone hostedZone, AmazonRoute53 route53) { Function<Map.Entry<String, Instance>, Change> cnameAndInstanceToChange = new Function<Map.Entry<String, Instance>, Change>() { @Override public Change apply(@Nullable Map.Entry<String, Instance> entry) { String cname = entry.getKey(); Instance instance = entry.getValue(); return new Change() .withAction(ChangeAction.CREATE) .withResourceRecordSet(new ResourceRecordSet() .withType(RRType.CNAME) .withName(cname) .withTTL(300L) .withResourceRecords(new ResourceRecord(instance.getPublicDnsName()))); } }; List<Change> changes = Lists.newArrayList(Iterables.transform(cnameToInstances.entrySet(), cnameAndInstanceToChange)); logger.debug("Create CNAME {}", changes); ChangeResourceRecordSetsRequest changeResourceRecordSetsRequest = new ChangeResourceRecordSetsRequest() .withHostedZoneId(hostedZone.getId()) .withChangeBatch(new ChangeBatch().withChanges(changes)); route53.changeResourceRecordSets(changeResourceRecordSetsRequest); } }