/** * DeployMan # Thomas Uhrig (Stuttgart, 2014) # www.tuhrig.de */ package de.tuhrig.deployman.launch; import static de.tuhrig.deployman.DeployMan.EC2_INSTANCE_KEY; import static de.tuhrig.deployman.DeployMan.REPO_PROFILE; import static de.tuhrig.deployman.DeployMan.SLASH; import static de.tuhrig.deployman.DeployMan.getUserProperty; import static de.tuhrig.deployman.DeployMan.readUserProperties; import static de.tuhrig.deployman.DeployMan.sdf; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; import org.apache.tools.ant.DefaultLogger; import org.apache.tools.ant.Project; import org.apache.tools.ant.ProjectHelper; import com.amazonaws.services.autoscaling.AmazonAutoScalingClient; import com.amazonaws.services.autoscaling.model.AlreadyExistsException; import com.amazonaws.services.autoscaling.model.AutoScalingInstanceDetails; import com.amazonaws.services.autoscaling.model.CreateAutoScalingGroupRequest; import com.amazonaws.services.autoscaling.model.CreateLaunchConfigurationRequest; import com.amazonaws.services.autoscaling.model.InstanceMonitoring; import com.amazonaws.services.autoscaling.model.PutScalingPolicyRequest; import com.amazonaws.services.autoscaling.model.PutScalingPolicyResult; import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient; import com.amazonaws.services.cloudwatch.model.ComparisonOperator; import com.amazonaws.services.cloudwatch.model.Dimension; import com.amazonaws.services.cloudwatch.model.PutMetricAlarmRequest; import com.amazonaws.services.cloudwatch.model.StandardUnit; import com.amazonaws.services.cloudwatch.model.Statistic; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.BlockDeviceMapping; import com.amazonaws.services.ec2.model.IamInstanceProfileSpecification; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.RunInstancesRequest; import com.amazonaws.services.ec2.model.RunInstancesResult; import com.amazonaws.services.rds.model.CreateDBInstanceRequest; import com.amazonaws.services.rds.model.DBInstance; import com.amazonaws.services.rds.model.Endpoint; import com.google.gson.Gson; import de.tuhrig.deployman.DeployMan; import de.tuhrig.deployman.aws.AutoScaling; import de.tuhrig.deployman.aws.CloudWatch; import de.tuhrig.deployman.aws.Ec2; import de.tuhrig.deployman.aws.Rds; import de.tuhrig.deployman.console.Console; import de.tuhrig.deployman.launch.enums.Comment; import de.tuhrig.deployman.launch.enums.Script; import de.tuhrig.deployman.launch.enums.Variable; import de.tuhrig.deployman.launch.formation.Container; import de.tuhrig.deployman.launch.formation.Credential; import de.tuhrig.deployman.launch.formation.Database; import de.tuhrig.deployman.launch.formation.Formation; import de.tuhrig.deployman.launch.formation.Machine; import de.tuhrig.deployman.launch.formation.Scaling; import de.tuhrig.deployman.repo.LocaleRepository; /** * This class launches formations. It can either launch a EC2 virtual machine setup or a RDS * database setup. It can also launch a formation file which contains both, an EC2 setup and a RDS * setup. * * @author tuhrig */ public class Launcher { public static final String HOME = "/home/ubuntu"; public static final String DEPLOYMENT_LOG_FILE = HOME + "/deployman.log"; public static final String DOCKER_LOG_FILE = HOME + "/docker.log"; private Ec2 ec2Client = new Ec2(); private Console console; public Launcher() { this(DeployMan.console); } public Launcher(Console console) { this.console = console; } public void run(String formationFile) { Formation formation = Formation.read(formationFile); run(formation); } public void run(Formation formation) { this.console.writeNl("Start formation " + formation.getFile()); DBInstance dbInstance = null; Instance instance = null; List<Instance> instances = null; if (formation.hasDatabaseDefinition()) dbInstance = runDatabaseLaunch(formation); // if there is a auto scaling group defined in the // formation file, we prever it over the machine // definition! if (formation.hasAutoScalingDefinition()) instances = runAutoScalingLaunch(formation); else if (formation.hasInstanceDefinition()) instance = runVirtualMachineLaunch(formation); // we print the results at the end to present a nice // overview to the user after the launch processes // have finished if (dbInstance != null) this.console.printDatabase(dbInstance); if (instance != null) this.console.printInstanceInfo(instance); if (instances != null) this.console.printEc2Instances(instances); } /** * Starts a auto scaling launch configuration. This method is a little but complicated, since the * process to start a auto scaling configuration involves some steps: - create a launch * configuration which defines the machines to start - create an auto scaling definition which * defines how many machines should be started if something happens. - create an auto scaling * policy to which we add a metric - create the metric which is checked by the auto scaling - * print the starting instances */ private List<Instance> runAutoScalingLaunch(Formation formation) { this.console.write("Run auto scaling..."); Machine machine = formation.getMachine(); Scaling scaling = machine.getScaling(); AmazonAutoScalingClient autoScaling = new AutoScaling().getClient(); try { CreateLaunchConfigurationRequest request = createLaunchConfigurationRequest(formation); autoScaling.createLaunchConfiguration(request); this.console.write("Created launch configuration " + scaling.getName()); } catch (AlreadyExistsException e) { this.console.write("Launch configuration " + scaling.getName() + " already exists"); //$NON-NLS-2$ } // // // try { CreateAutoScalingGroupRequest request = createAutoScalingRequest(scaling); autoScaling.createAutoScalingGroup(request); this.console.write("Created auto scaling group " + scaling.getGroup()); } catch (AlreadyExistsException e) { this.console.write("Auto scaling group " + scaling.getGroup() + " already exists"); //$NON-NLS-2$ } // // // PutScalingPolicyRequest request = new PutScalingPolicyRequest().withAutoScalingGroupName(scaling.getGroup()) .withPolicyName(scaling.getPolicy()).withScalingAdjustment(1) .withAdjustmentType("ChangeInCapacity"); PutScalingPolicyResult result = autoScaling.putScalingPolicy(request); this.console.write("Put scaling policy " + scaling.getPolicy()); // // // // Scale Up Dimension dimension = new Dimension().withName("AutoScalingGroupName").withValue(scaling.getGroup()); List<String> actions = new ArrayList<>(); actions.add(result.getPolicyARN()); PutMetricAlarmRequest upRequest = new PutMetricAlarmRequest().withAlarmName(scaling.getAlarm()) .withMetricName("CPUUtilization").withDimensions(dimension).withNamespace("AWS/EC2") .withComparisonOperator(ComparisonOperator.GreaterThanThreshold) .withStatistic(Statistic.Average).withUnit(StandardUnit.Percent).withThreshold(60d) .withPeriod(300).withEvaluationPeriods(2).withAlarmActions(actions); AmazonCloudWatchClient cloudWatch = new CloudWatch().getClient(); cloudWatch.putMetricAlarm(upRequest); this.console.write("Put alarm " + scaling.getAlarm()); this.console.newLine(); List<Instance> instances = new ArrayList<>(); for (AutoScalingInstanceDetails instance : autoScaling.describeAutoScalingInstances() .getAutoScalingInstances()) { if (instance.getAutoScalingGroupName().equals(scaling.getGroup())) { instances.add(new Ec2().getEC2InstanceById(instance.getInstanceId())); } } return instances; } private CreateLaunchConfigurationRequest createLaunchConfigurationRequest(Formation formation) { Machine machine = formation.getMachine(); Scaling scaling = machine.getScaling(); Properties generalProperties = getMetaInformationProperties(formation); CloudInitScript initScript = getParallelCloudInitScript(machine, generalProperties); IamInstanceProfileSpecification profil = this.ec2Client.getIamInstanceProfileSpecification(getUserProperty(REPO_PROFILE)); return new CreateLaunchConfigurationRequest().withLaunchConfigurationName(scaling.getName()) .withInstanceType(machine.getInstanceType()).withImageId(machine.getImageId()) .withUserData(initScript.renderAsBase64()).withSecurityGroups(machine.getSecurityGroup()) .withIamInstanceProfile(profil.getName()) // the name NOT the ARN! .withKeyName(getUserProperty(EC2_INSTANCE_KEY)) .withInstanceMonitoring(new InstanceMonitoring().withEnabled(false)); } private CreateAutoScalingGroupRequest createAutoScalingRequest(Scaling scaling) { return new CreateAutoScalingGroupRequest().withAutoScalingGroupName(scaling.getGroup()) .withLaunchConfigurationName(scaling.getName()).withAvailabilityZones(scaling.getZone()) .withMaxSize(scaling.getMax()).withMinSize(scaling.getMin()) .withLoadBalancerNames(scaling.getLb()).withHealthCheckType("EC2") .withHealthCheckGracePeriod(300).withDefaultCooldown(600); } private DBInstance runDatabaseLaunch(Formation formation) { Rds rdsClient = new Rds(); // if ( true ) // return null; Database database = formation.getDatabase(); DBInstance dbInstance = null; if (rdsClient.databaseExists(database.getInstanceIdentifier())) { this.console.writeNl("Database already exists"); dbInstance = rdsClient.getDatabase(database.getInstanceIdentifier()); } else { this.console.writeNl("Create database"); CreateDBInstanceRequest request = new CreateDBInstanceRequest().withEngine(database.getEngine()) .withEngineVersion(database.getEngineVersion()) .withLicenseModel(database.getLicense()) .withDBInstanceClass(database.getInstanceClass()).withMultiAZ(database.getMultiAz()) .withAutoMinorVersionUpgrade(database.getAutoMinorVersionUpgrade()) .withAllocatedStorage(database.getAllocatedStorage()) .withDBInstanceIdentifier(database.getInstanceIdentifier()) .withMasterUsername(database.getUsername()) .withMasterUserPassword(database.getPassword()).withDBName(database.getName()) .withPort(database.getPort()); // .withVpcSecurityGroupIds( database.getSecurityGroup() ); dbInstance = rdsClient.getClient().createDBInstance(request); } this.console.writeNl("Starting..."); waitForDatabaseState(dbInstance.getDBInstanceIdentifier(), "available"); // sleep 10 seconds to get it really ready... // no idea why the state 'available isn't enough sleep(10000); dbInstance = rdsClient.getDatabase(dbInstance.getDBInstanceIdentifier()); runSetup(dbInstance, database); return dbInstance; } private Instance runVirtualMachineLaunch(Formation formation) { Properties generalProperties = getMetaInformationProperties(formation); Machine machine = formation.getMachine(); CloudInitScript initScript = getParallelCloudInitScript(machine, generalProperties); initScript.save(); this.console.newLine(); // if ( true ) // return null; BlockDeviceMapping volumn = this.ec2Client.getBlockDeviceMapping("/dev/sda1", 20); IamInstanceProfileSpecification profil = this.ec2Client.getIamInstanceProfileSpecification(getUserProperty(REPO_PROFILE)); RunInstancesRequest request = new RunInstancesRequest().withInstanceType(machine.getInstanceType()) .withImageId(machine.getImageId()).withIamInstanceProfile(profil).withMinCount(1) .withMaxCount(1).withBlockDeviceMappings(volumn) .withUserData(initScript.renderAsBase64()) .withSecurityGroupIds(machine.getSecurityGroup()) .withKeyName(getUserProperty(EC2_INSTANCE_KEY)); String instanceId = runInstance(request); this.console.writeNl("Starting..."); waitForInstanceState(instanceId, "running"); assigneIpIfAvailable(instanceId, machine); assigneName(instanceId, machine); return this.ec2Client.getEC2InstanceById(instanceId); } private void assigneName(String instanceId, Machine machine) { // tag the machine with its name which must not be unique new Ec2().tag(instanceId, machine.getName()); } private String runInstance(RunInstancesRequest request) { AmazonEC2 ec2 = this.ec2Client.getClient(); RunInstancesResult runInstances = ec2.runInstances(request); List<Instance> instances = runInstances.getReservation().getInstances(); return instances.get(0).getInstanceId(); } /** * Returns a CloudInit script which performs the complete initialization of a machine. The script * will execute all steps in a sequential order after each other. Note: This method is currently * not used since it is slower as the parallel deployment. */ // private CloudInitScript getSequentialCloudInitScript( Machine machine, Properties // generalProperties ) // { // CloudInitScript initScript = getBaseScript( machine, generalProperties ); // // // a counter to distinguish different configurations // // with the same name // int index = 0; // // for ( Container container : machine.getContainers() ) // { // Properties containerProperties = getContainerProperties( container, index ); // // CloudInitScript script = new CloudInitScript( "# container init" ).withProperties( // containerProperties ) // .withFile( "copy_image.sh" ) // .withFile( "copy_config.sh" ) // .withFile( "docker_load.sh" ) // .withFile( "sync_config.sh" ) // .withCommand( container.getCommand() ); // // initScript = initScript.withScript( script ); // // // increment the counter // index++; // } // // CloudInitScript script = new CloudInitScript( "# done" ).withFile( "final_message.sh" ); //$NON-NLS-2$ // return initScript.withScript( script ); // } private void assigneIpIfAvailable(String instanceId, Machine machine) { // if the formation has an elastic ip (which is optional) // we associate this ip with the instance we created before String elasticIp = machine.getElasticIp(); if (elasticIp != null && !elasticIp.equals("")) this.ec2Client.associate(instanceId, elasticIp); } /** * Returns a CloudInit script which performs the complete initialization of a machine. The script * will execute the donwload and load steps in parallel in an arbitrary order. All containers are * started in the order of the formations file, but directly after each other. */ private CloudInitScript getParallelCloudInitScript(Machine machine, Properties generalProperties) { CloudInitScript initScript = generateBaseScript(machine, generalProperties); CloudInitScript getScript = new CloudInitScript(Comment.COPY_IMAGES_AND_CONFIGS); CloudInitScript runScript = new CloudInitScript(Comment.EXEC_IMAGES); // a counter to distinguish different configurations // with the same name int index = 0; for (Container container : machine.getContainers()) { Properties properties = getContainerProperties(container, index); CloudInitScript imageDownloadScript = generateDownloadScriptForContainer(container, properties); CloudInitScript imageRunScript = generateRunScriptForContainer(machine, container, properties); getScript.withScript(imageDownloadScript); runScript.withScript(imageRunScript); // increment the counter index++; } CloudInitScript wait = new CloudInitScript(Comment.WAIT_FOR_SUBSHELL).withFile(Script.WAIT); getScript.withScript(wait); initScript.withScript(getScript).withScript(runScript); CloudInitScript script = new CloudInitScript(Comment.DONE).withFile(Script.FINAL_MESSAGE); return initScript.withScript(script); } private CloudInitScript generateDownloadScriptForContainer(Container container, Properties containerProperties) { if (container.hasImage()) return getImageByDownloadFromRegistry(container, containerProperties); return getImageByDownloadOfTarball(containerProperties); } private CloudInitScript generateRunScriptForContainer(Machine machine, Container container, Properties containerProperties) { if (machine.hasAutoSync()) { return new CloudInitScript(Comment.RUN_IMAGE).withProperties(containerProperties) .withFile(Script.SYNC_CONFIG).withCommand(container.getCommand()); } return new CloudInitScript(Comment.RUN_IMAGE).withProperties(containerProperties).withCommand( container.getCommand()); } private CloudInitScript getImageByDownloadOfTarball(Properties containerProperties) { return new CloudInitScript(Comment.DOWNLOAD_TARBALL).withProperties(containerProperties) .withFile(Script.COPY_AND_LOAD_TARBALL_PARALLEL).withFile(Script.COPY_CONFIG_PARALLEL); } private CloudInitScript getImageByDownloadFromRegistry(Container container, Properties containerProperties) { if (container.hasCredential()) { return new CloudInitScript(Comment.DOWNLOAD_IMAGE).withProperties(containerProperties) .withFile(Script.DOCKER_LOGIN).withFile(Script.DOWNLOAD_IMAGE) .withFile(Script.COPY_CONFIG_PARALLEL); } return new CloudInitScript(Comment.DOWNLOAD_IMAGE).withProperties(containerProperties) .withFile(Script.DOWNLOAD_IMAGE).withFile(Script.COPY_CONFIG_PARALLEL); } private Properties getContainerProperties(Container setup, int index) { Properties properties = readUserProperties(); addContainerProperties(properties, setup, index); return properties; } private void addContainerProperties(Properties properties, Container container, int index) { properties.put(Variable.TARBALL_KEY, container.getTarball()); properties.put(Variable.IMAGE_NAME, container.getImage()); properties.put(Variable.CONFIG_KEY, container.getConfig()); properties.put(Variable.TARBALL_NAME, container.getTarballFileName()); properties.put(Variable.CONFIG_FOLDER, HOME + "/config-" + container.getTarballName() + "-" + index); properties.put(Variable.HOME_DIRECTORY, HOME); if (container.hasCredential()) { Credential credential = container.getCredential(); addCredentialProperties(properties, credential); } } private void addCredentialProperties(Properties properties, Credential credential) { properties.put(Variable.REGISTRY_EMAIL, credential.getEmail()); properties.put(Variable.REGISTRY_NAME, credential.getName()); properties.put(Variable.REGISTRY_PASSWORD, credential.getPassword()); properties.put(Variable.REGISTRY_SERVER, credential.getServer()); } private CloudInitScript generateBaseScript(Machine machine, Properties generalProperties) { CloudInitScript baseScript = new CloudInitScript(Comment.BASH).withProperties(generalProperties) .withFile(Script.LOG_HEADER).withFile(Script.SET_TIMEZONE).withFile(Script.LOG_INFO); if (machine.installDocker()) baseScript.withFile(Script.INSTALL_DOCKER); if (machine.installAwsCli()) baseScript.withFile(Script.INSTALL_AWSCLI); if (machine.openDocker()) baseScript.withFile(Script.OPEN_DOCKER); return baseScript; } private void waitForDatabaseState(String dbInstance, String state) { while (!new Rds().getDatabase(dbInstance).getDBInstanceStatus().equalsIgnoreCase(state)) { sleep(4000); } } private void waitForInstanceState(String instanceId, String state) { while (!new Ec2().getEC2InstanceById(instanceId).getState().getName().equalsIgnoreCase(state)) { sleep(3000); } } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { // } } /** * Runs the given database configuration (~setup) on an AWS database instance. Make sure the setup * fits the database (e.g. a MSSQL setup for a MSSQL database). */ private void runSetup(DBInstance dbInstance, Database database) { String setup = database.getSetup(); String buildFilePath = new LocaleRepository().getLocation() + SLASH + setup + "/build.xml"; Endpoint endpoint = dbInstance.getEndpoint(); File buildFile = new File(buildFilePath); this.console.write("Run database setup " + buildFilePath); this.console.write("Endpoint " + endpoint); Project project = new Project(); project.setUserProperty(Variable.ANT_FILE, buildFile.getAbsolutePath()); project.setUserProperty(Variable.DEST_ROOT_LOCAL, new LocaleRepository().getLocation() + SLASH + "tmp"); project.setUserProperty(Variable.DB_SERVER, endpoint.getAddress()); project.setUserProperty(Variable.DB_PORT, endpoint.getPort().toString()); project.setUserProperty(Variable.DB_USER, database.getUsername()); project.setUserProperty(Variable.DB_PASSWORD, database.getPassword()); project.setUserProperty(Variable.ENV_NLS_LANG, "American_America.UTF8"); project.setUserProperty(Variable.HEADLESS, "true"); project.init(); DefaultLogger consoleLogger = createConsoleLogger(); project.addBuildListener(consoleLogger); ProjectHelper helper = ProjectHelper.getProjectHelper(); project.addReference("ant.projectHelper", helper); helper.parse(project, buildFile); project.executeTarget(project.getDefaultTarget()); this.console.newLine(); } private DefaultLogger createConsoleLogger() { DefaultLogger consoleLogger = new DefaultLogger(); consoleLogger.setErrorPrintStream(System.err); consoleLogger.setOutputPrintStream(System.out); consoleLogger.setMessageOutputLevel(Project.MSG_INFO); return consoleLogger; } public Properties getMetaInformationProperties(Formation formation) { Gson gson = new Gson(); Machine machine = formation.getMachine(); List<Container> containers = machine.getContainers(); Properties properties = readUserProperties(); properties.put(Variable.TIMESTAMP, sdf.format(new Date())); properties.put(Variable.HOST, new DeployMan().getLocalHostName()); properties.put(Variable.FORMATION, gson.toJson(formation)); properties.put(Variable.CONTAINERS, containers.size()); properties.put(Variable.HOME_DIRECTORY, HOME); properties.put(Variable.LOG_DEPLOYMENT, DEPLOYMENT_LOG_FILE); properties.put(Variable.LOG_DOCKER, DOCKER_LOG_FILE); return properties; } }