/*
* 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.workshop.infrastructureascode;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import org.apache.commons.lang.ObjectUtils;
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.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.CreateTagsRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.Filter;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceStateName;
import com.amazonaws.services.ec2.model.InstanceType;
import com.amazonaws.services.ec2.model.Placement;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.RunInstancesResult;
import com.amazonaws.services.ec2.model.Tag;
import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancing;
import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClient;
import com.amazonaws.services.elasticloadbalancing.model.AppCookieStickinessPolicy;
import com.amazonaws.services.elasticloadbalancing.model.ConfigureHealthCheckRequest;
import com.amazonaws.services.elasticloadbalancing.model.CreateLBCookieStickinessPolicyRequest;
import com.amazonaws.services.elasticloadbalancing.model.CreateLoadBalancerRequest;
import com.amazonaws.services.elasticloadbalancing.model.DeleteLoadBalancerPolicyRequest;
import com.amazonaws.services.elasticloadbalancing.model.DeleteLoadBalancerRequest;
import com.amazonaws.services.elasticloadbalancing.model.DeregisterInstancesFromLoadBalancerRequest;
import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest;
import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersResult;
import com.amazonaws.services.elasticloadbalancing.model.DisableAvailabilityZonesForLoadBalancerRequest;
import com.amazonaws.services.elasticloadbalancing.model.EnableAvailabilityZonesForLoadBalancerRequest;
import com.amazonaws.services.elasticloadbalancing.model.HealthCheck;
import com.amazonaws.services.elasticloadbalancing.model.LBCookieStickinessPolicy;
import com.amazonaws.services.elasticloadbalancing.model.Listener;
import com.amazonaws.services.elasticloadbalancing.model.ListenerDescription;
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription;
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerNotFoundException;
import com.amazonaws.services.elasticloadbalancing.model.Policies;
import com.amazonaws.services.elasticloadbalancing.model.RegisterInstancesWithLoadBalancerRequest;
import com.amazonaws.services.elasticloadbalancing.model.SetLoadBalancerPoliciesOfListenerRequest;
import com.amazonaws.services.rds.AmazonRDS;
import com.amazonaws.services.rds.AmazonRDSClient;
import com.amazonaws.services.rds.model.CreateDBInstanceRequest;
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.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import fr.xebia.cloud.cloudinit.CloudInitUserDataBuilder;
import fr.xebia.cloud.cloudinit.FreemarkerUtils;
/**
* <p>
* Builds a java petclinic infrastructure on Amazon EC2.
* </p>
* <p>
* creates:
* <ul>
* <li>1 MySQL database</li>
* <li>several Tomcat / xebia-pet-clinic servers connected to the mysql database
* (connected via the injection of the jdbc parameters in catalina.properties
* via cloud-init)</li>
* <li>1 load balancer</li>
* </ul>
* </p>
*
* @author <a href="mailto:cyrille@cyrilleleclerc.com">Cyrille Le Clerc</a>
*/
public class AmazonAwsPetclinicInfrastructureEnforcer {
public enum Distribution {
/**
* <a href="http://aws.amazon.com/amazon-linux-ami/">Amazon Linux
* AMI</a>
*/
AMZN_LINUX("ami-47cefa33", "cloud-config-amzn-linux.txt", "/usr/share/tomcat6", InstanceType.T1Micro), //
/**
* <a href="http://cloud.ubuntu.com/ami/">Ubuntu Natty (11.04) AMI</a>
*/
UBUNTU_11_04("ami-359ea941", "cloud-config-ubuntu-11.04.txt", "/var/lib/tomcat6", InstanceType.M1Small), //
/**
* <a href="http://cloud.ubuntu.com/ami/">Ubuntu Oneiric (11.10) AMI</a>
*/
UBUNTU_11_10("ami-0aa7967e", "cloud-config-ubuntu-11.10.txt", "/var/lib/tomcat7", InstanceType.M1Small);
private final static Map<String, Distribution> DISTRIBUTIONS_BY_AMI_ID = Maps.uniqueIndex(Arrays.asList(Distribution.values()),
new Function<Distribution, String>() {
@Override
public String apply(Distribution distribution) {
return distribution.getAmiId();
}
});
@Nonnull
public static Distribution fromAmiId(@Nonnull String amiId) throws NullPointerException, IllegalArgumentException {
Distribution distribution = DISTRIBUTIONS_BY_AMI_ID.get(Preconditions.checkNotNull(amiId, "amiId is null"));
Preconditions.checkArgument(distribution != null, "No distribution found for amiId '%s'", amiId);
return distribution;
}
private final String amiId;
private final String catalinaBase;
private final String cloudConfigFilePath;
private final InstanceType instanceType;
/**
* ID of the AMI in the eu-west-1 region.
*/
private Distribution(@Nonnull String amiId, @Nonnull String cloudConfigFilePath, @Nonnull String catalinaBase,
@Nonnull InstanceType instanceType) {
this.amiId = amiId;
this.cloudConfigFilePath = cloudConfigFilePath;
this.catalinaBase = catalinaBase;
this.instanceType = instanceType;
}
/**
* ID of the AMI in the eu-west-1 region.
*/
@Nonnull
public String getAmiId() {
return amiId;
}
/**
* <p>
* "catalina_base" folder.
* </p>
* <p>
* Differs between redhat/ubuntu and between versions.
* </p>
* <p>
* e.g."/var/lib/tomcat7", "/usr/share/tomcat6".
* </p>
*/
@Nonnull
public String getCatalinaBase() {
return catalinaBase;
}
/**
* <p>
* Classpath relative path to the "cloud-config" file.
* </p>
* <p>
* "cloud-config" files differ between distributions due to the
* different name of the packages and the different versions available.
* </p>
* <p>
* e.g."cloud-config-ubuntu-10.04.txt", "cloud-config-redhat-5.txt".
* </p>
*/
@Nonnull
public String getCloudConfigFilePath() {
return cloudConfigFilePath;
}
@Nonnull
public InstanceType getInstanceType() {
return instanceType;
}
}
protected final static String AMI_CUSTOM_LINUX_SUN_JDK6_TOMCAT7 = "ami-44506030";
/**
* Extract the {@link Placement#getAvailabilityZone()} zone of the given ec2
* <code>instance</code> .
*/
public static final Function<Instance, String> EC2_INSTANCE_TO_AVAILABILITY_ZONE = new Function<Instance, String>() {
@Override
public String apply(Instance instance) {
return instance.getPlacement().getAvailabilityZone();
}
};
/**
* Extract the {@link Instance#getInstanceId()} of the given ec2
* <code>instance</code>.
*/
public final static Function<Instance, String> EC2_INSTANCE_TO_INSTANCE_ID = new Function<Instance, String>() {
@Override
public String apply(Instance instance) {
return instance.getInstanceId();
}
};
/**
* Extract the
* {@link com.amazonaws.services.elasticloadbalancing.model.Instance#getInstanceId()}
* of the given elb <code>instance</code>.
*/
public final static Function<com.amazonaws.services.elasticloadbalancing.model.Instance, String> ELB_INSTANCE_TO_INSTANCE_ID = new Function<com.amazonaws.services.elasticloadbalancing.model.Instance, String>() {
@Override
public String apply(com.amazonaws.services.elasticloadbalancing.model.Instance instance) {
return instance.getInstanceId();
}
};
/**
* Converts the given <code>instanceId</code> into an elb
* {@link com.amazonaws.services.elasticloadbalancing.model.Instance}.
*/
public final static Function<String, com.amazonaws.services.elasticloadbalancing.model.Instance> INSTANCE_ID_TO_ELB_INSTANCE = new Function<String, com.amazonaws.services.elasticloadbalancing.model.Instance>() {
@Override
public com.amazonaws.services.elasticloadbalancing.model.Instance apply(String instanceId) {
return new com.amazonaws.services.elasticloadbalancing.model.Instance(instanceId);
}
};
public static void main(String[] args) throws Exception {
AmazonAwsPetclinicInfrastructureEnforcer infrastructureMaker = new AmazonAwsPetclinicInfrastructureEnforcer();
infrastructureMaker.createPetclinicInfrastructure(Distribution.AMZN_LINUX);
}
/**
* 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);
}
protected final AmazonEC2 ec2;
protected final AmazonElasticLoadBalancing elb;
protected final Logger logger = LoggerFactory.getLogger(getClass());
protected final AmazonRDS rds;
@VisibleForTesting
protected AmazonAwsPetclinicInfrastructureEnforcer(AmazonEC2 ec2, AmazonElasticLoadBalancing elb, AmazonRDS rds) {
super();
this.ec2 = ec2;
this.elb = elb;
this.rds = rds;
}
public AmazonAwsPetclinicInfrastructureEnforcer() {
try {
InputStream credentialsAsStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("AwsCredentials.properties");
Preconditions.checkNotNull(credentialsAsStream, "File 'AwsCredentials.properties' NOT found in the classpath");
AWSCredentials credentials = new PropertiesCredentials(credentialsAsStream);
ec2 = new AmazonEC2Client(credentials);
ec2.setEndpoint("ec2.eu-west-1.amazonaws.com");
rds = new AmazonRDSClient(credentials);
rds.setEndpoint("rds.eu-west-1.amazonaws.com");
elb = new AmazonElasticLoadBalancingClient(credentials);
elb.setEndpoint("elasticloadbalancing.eu-west-1.amazonaws.com");
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* <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
* @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 DBInstance awaitForDbInstanceCreation(@Nonnull DBInstance dbInstance) 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>
* Wait for the ec2 instances creation 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 List<Instance> awaitForEc2Instances(@Nonnull Iterable<Instance> instances) {
List<Instance> result = Lists.newArrayList();
for (Instance instance : instances) {
while (InstanceStateName.Pending.equals(instance.getState())) {
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()));
}
result.add(instance);
}
return result;
}
/**
* Returns a base-64 version of the mime-multi-part cloud-init file to put
* in the user-data attribute of the ec2 instance.
*
* @param distribution
* @param dbInstance
* @param jdbcUsername
* @param jdbcPassword
* @param warUrl
* @return
*/
@Nonnull
protected String buildCloudInitUserData(Distribution distribution, DBInstance dbInstance, String jdbcUsername, String jdbcPassword,
String warUrl, String rootContext) {
// SHELL SCRIPT
Map<String, Object> rootMap = Maps.newHashMap();
rootMap.put("catalinaBase", distribution.getCatalinaBase());
rootMap.put("warUrl", warUrl);
rootMap.put("warName", rootContext + ".war");
Map<String, String> systemProperties = Maps.newHashMap();
rootMap.put("systemProperties", systemProperties);
String jdbcUrl = "jdbc:mysql://" + dbInstance.getEndpoint().getAddress() + ":" + dbInstance.getEndpoint().getPort() + "/"
+ dbInstance.getDBName();
systemProperties.put("jdbc.url", jdbcUrl);
systemProperties.put("jdbc.username", jdbcUsername);
systemProperties.put("jdbc.password", jdbcPassword);
String shellScript = FreemarkerUtils.generate(rootMap, "/provision_tomcat.py.ftl");
// CLOUD CONFIG
InputStream cloudConfigAsStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(distribution.getCloudConfigFilePath());
Preconditions.checkNotNull(cloudConfigAsStream, "'" + distribution.getCloudConfigFilePath() + "' not found in path");
Readable cloudConfig = new InputStreamReader(cloudConfigAsStream);
return CloudInitUserDataBuilder.start() //
.addShellScript(shellScript) //
.addCloudConfig(cloudConfig) //
.buildBase64UserData();
}
@Nonnull
public DBInstance createMySqlDatabaseInstanceIfNotExists(String dbInstanceIdentifier, String dbName, String jdbcUserName,
String jdbcPassword) {
logger.info("ENFORCE DATABASE {}/{}", dbInstanceIdentifier, dbName);
DescribeDBInstancesRequest describeDbInstanceRequest = new DescribeDBInstancesRequest()
.withDBInstanceIdentifier(dbInstanceIdentifier);
try {
DescribeDBInstancesResult describeDBInstances = rds.describeDBInstances(describeDbInstanceRequest);
if (describeDBInstances.getDBInstances().isEmpty()) {
// unexpected, db does not exist but we expected a
// DBInstanceNotFoundException
} else {
DBInstance dbInstance = Iterables.getFirst(describeDBInstances.getDBInstances(), null);
logger.info("Database instance '" + dbInstanceIdentifier + "' already exists! Skip creation");
return dbInstance;
}
} catch (DBInstanceNotFoundException e) {
// good, db does not exist
}
CreateDBInstanceRequest createDBInstanceRequest = new CreateDBInstanceRequest() //
.withDBInstanceIdentifier(dbInstanceIdentifier) //
.withDBName(dbName) //
.withEngine("MySQL") //
.withEngineVersion("5.1.57") //
.withDBInstanceClass("db.m1.small") //
.withMasterUsername(jdbcUserName) //
.withMasterUserPassword(jdbcPassword) //
.withAllocatedStorage(5) //
.withBackupRetentionPeriod(0) //
.withDBSecurityGroups("default") //
.withLicenseModel("general-public-license") //
;
DBInstance dbInstance = rds.createDBInstance(createDBInstanceRequest);
logger.info("Created {}", dbInstance);
return dbInstance;
}
public void createPetclinicInfrastructure(Distribution... distributions) {
String applicationId = "petclinic";
String rootContext = "/petclinic";
String warUrl = "http://xebia-france.googlecode.com/svn/repository/maven2/fr/xebia/demo/xebia-petclinic/1.0.2/xebia-petclinic-1.0.2.war";
String healthCheckUri = rootContext + "/healthcheck.jsp";
createTomcatMySqlInfrastructure(applicationId, rootContext, warUrl, healthCheckUri, distributions);
}
void createTomcatMySqlInfrastructure(String applicationId, String rootContext, String warUrl, String healthCheckUri,
Distribution... distributions) {
String dbInstanceIdentifier = applicationId;
String dbName = applicationId;
String jdbcUsername = applicationId;
String jdbcPassword = applicationId;
DBInstance dbInstance = createMySqlDatabaseInstanceIfNotExists(dbInstanceIdentifier, dbName, jdbcUsername, jdbcPassword);
dbInstance = awaitForDbInstanceCreation(dbInstance);
logger.info("MySQL instance: " + dbInstance);
List<Instance> tomcatInstances = createTomcatServers(dbInstance, applicationId, jdbcUsername, jdbcPassword, warUrl, rootContext,
distributions);
logger.info("EC2 instances: " + tomcatInstances);
LoadBalancerDescription loadBalancerDescription = createOrUpdateElasticLoadBalancer(healthCheckUri, applicationId);
logger.info("Load Balancer DNS name: " + loadBalancerDescription.getDNSName());
// PRINT INFRASTRUCTURE
System.out.println("DATABASE");
System.out.println("========");
String jdbcUrl = "jdbc:mysql://" + dbInstance.getEndpoint().getAddress() + ":" + dbInstance.getEndpoint().getPort() + "/"
+ dbInstance.getDBName();
System.out.println("jdbc.url= " + jdbcUrl);
System.out.println("jdbc.username= " + jdbcUsername);
System.out.println("jdbc.password= " + jdbcPassword);
System.out.println();
System.out.println("TOMCAT SERVERS");
System.out.println("==============");
tomcatInstances = awaitForEc2Instances(tomcatInstances);
for (Instance instance : tomcatInstances) {
System.out.println("http://" + instance.getPublicDnsName() + ":8080" + rootContext);
}
System.out.println("LOAD BALANCER");
System.out.println("=============");
System.out.println("http://" + loadBalancerDescription.getDNSName() + rootContext);
}
/**
*
* @param healthCheckUri
* start with slash. E.g. "/myapp/healthcheck.jsp
* @param applicationIdentifier
* used to name the load balancer and to filter the instances on
* their "Role" tag.
* @return created load balancer description
*/
@Nonnull
public LoadBalancerDescription createOrUpdateElasticLoadBalancer(@Nonnull String healthCheckUri, @Nonnull String applicationIdentifier) {
logger.info("ENFORCE LOAD BALANCER");
DescribeInstancesRequest describeInstancesWithRoleRequest = new DescribeInstancesRequest(). //
withFilters(new Filter("tag:Role", Arrays.asList(applicationIdentifier)));
DescribeInstancesResult describeInstancesResult = ec2.describeInstances(describeInstancesWithRoleRequest);
Iterable<Instance> expectedEc2Instances = toEc2Instances(describeInstancesResult.getReservations());
Set<String> expectedAvailabilityZones = Sets.newHashSet(Iterables
.transform(expectedEc2Instances, EC2_INSTANCE_TO_AVAILABILITY_ZONE));
Listener expectedListener = new Listener("HTTP", 80, 8080);
String loadBalancerName = applicationIdentifier;
LoadBalancerDescription actualLoadBalancerDescription;
try {
DescribeLoadBalancersResult describeLoadBalancers = elb.describeLoadBalancers(new DescribeLoadBalancersRequest(Arrays
.asList(loadBalancerName)));
if (describeLoadBalancers.getLoadBalancerDescriptions().isEmpty()) {
// unexpected, this should have been a
// LoadBalancerNotFoundException
actualLoadBalancerDescription = null;
} else {
// re-query to get updated config
actualLoadBalancerDescription = Iterables.getFirst(describeLoadBalancers.getLoadBalancerDescriptions(), null);
}
} catch (LoadBalancerNotFoundException e) {
actualLoadBalancerDescription = null;
}
Set<String> actualAvailabilityZones;
Set<String> actualInstanceIds;
Policies actualPolicies;
HealthCheck actualHealthCheck;
ListenerDescription actualListenerDescription = null;
if (actualLoadBalancerDescription == null) {
CreateLoadBalancerRequest createLoadBalancerRequest = new CreateLoadBalancerRequest() //
.withLoadBalancerName(loadBalancerName) //
.withAvailabilityZones(expectedAvailabilityZones) //
.withListeners(expectedListener);
elb.createLoadBalancer(createLoadBalancerRequest);
actualListenerDescription = new ListenerDescription().withListener(expectedListener);
actualAvailabilityZones = expectedAvailabilityZones;
actualInstanceIds = Collections.emptySet();
actualHealthCheck = new HealthCheck();
actualPolicies = new Policies();
} else {
// check listeners
List<ListenerDescription> actualListenerDescriptions = actualLoadBalancerDescription.getListenerDescriptions();
boolean loadBalancerMustBeRecreated;
if (actualListenerDescriptions.size() == 1) {
actualListenerDescription = Iterables.getOnlyElement(actualListenerDescriptions);
Listener actualListener = actualListenerDescription.getListener();
if (ObjectUtils.equals(expectedListener.getProtocol(), actualListener.getProtocol()) && //
ObjectUtils.equals(expectedListener.getLoadBalancerPort(), actualListener.getLoadBalancerPort()) && //
ObjectUtils.equals(expectedListener.getInstancePort(), actualListener.getInstancePort())) {
loadBalancerMustBeRecreated = false;
} else {
loadBalancerMustBeRecreated = true;
}
} else {
loadBalancerMustBeRecreated = true;
}
if (loadBalancerMustBeRecreated) {
logger.info("Recreate miss configured load balancer actualListeners:{}, expectedListener:{}", actualListenerDescriptions,
expectedListener);
elb.deleteLoadBalancer(new DeleteLoadBalancerRequest(loadBalancerName));
return createOrUpdateElasticLoadBalancer(healthCheckUri, applicationIdentifier);
}
//
actualAvailabilityZones = Sets.newHashSet(actualLoadBalancerDescription.getAvailabilityZones());
actualInstanceIds = Sets.newHashSet(Iterables.transform(actualLoadBalancerDescription.getInstances(),
ELB_INSTANCE_TO_INSTANCE_ID));
actualHealthCheck = actualLoadBalancerDescription.getHealthCheck();
actualPolicies = actualLoadBalancerDescription.getPolicies();
}
// HEALTH CHECK
if (!healthCheckUri.startsWith("/")) {
healthCheckUri = "/" + healthCheckUri;
}
HealthCheck expectedHealthCheck = new HealthCheck() //
.withTarget("HTTP:8080" + healthCheckUri) //
.withHealthyThreshold(2) //
.withUnhealthyThreshold(2) //
.withInterval(30) //
.withTimeout(2);
if (Objects.equal(expectedHealthCheck.getTarget(), actualHealthCheck.getTarget()) && //
Objects.equal(expectedHealthCheck.getHealthyThreshold(), actualHealthCheck.getHealthyThreshold()) && //
Objects.equal(expectedHealthCheck.getInterval(), actualHealthCheck.getInterval()) && //
Objects.equal(expectedHealthCheck.getTimeout(), actualHealthCheck.getTimeout()) && //
Objects.equal(expectedHealthCheck.getUnhealthyThreshold(), actualHealthCheck.getHealthyThreshold())) {
// health check is ok
} else {
logger.info("Set Healthcheck: " + expectedHealthCheck);
elb.configureHealthCheck(new ConfigureHealthCheckRequest(loadBalancerName, expectedHealthCheck));
}
// AVAILABILITY ZONES
// enable
Iterable<String> availabilityZonesToEnable = Sets.difference(expectedAvailabilityZones, actualAvailabilityZones);
logger.info("Enable availability zones: " + availabilityZonesToEnable);
if (!Iterables.isEmpty(availabilityZonesToEnable)) {
elb.enableAvailabilityZonesForLoadBalancer(new EnableAvailabilityZonesForLoadBalancerRequest(loadBalancerName, Lists
.newArrayList(availabilityZonesToEnable)));
}
// disable
Iterable<String> availabilityZonesToDisable = Sets.difference(actualAvailabilityZones, expectedAvailabilityZones);
logger.info("Disable availability zones: " + availabilityZonesToDisable);
if (!Iterables.isEmpty(availabilityZonesToDisable)) {
elb.disableAvailabilityZonesForLoadBalancer(new DisableAvailabilityZonesForLoadBalancerRequest(loadBalancerName, Lists
.newArrayList(availabilityZonesToDisable)));
}
// STICKINESS
List<AppCookieStickinessPolicy> appCookieStickinessPoliciesToDelete = actualPolicies.getAppCookieStickinessPolicies();
logger.info("Delete app cookie stickiness policies:" + appCookieStickinessPoliciesToDelete);
for (AppCookieStickinessPolicy appCookieStickinessPolicyToDelete : appCookieStickinessPoliciesToDelete) {
elb.deleteLoadBalancerPolicy(new DeleteLoadBalancerPolicyRequest(loadBalancerName, appCookieStickinessPolicyToDelete
.getPolicyName()));
}
final LBCookieStickinessPolicy expectedLbCookieStickinessPolicy = new LBCookieStickinessPolicy(applicationIdentifier
+ "-stickiness-policy", null);
Predicate<LBCookieStickinessPolicy> isExpectedPolicyPredicate = new Predicate<LBCookieStickinessPolicy>() {
@Override
public boolean apply(LBCookieStickinessPolicy lbCookieStickinessPolicy) {
return Objects.equal(expectedLbCookieStickinessPolicy.getPolicyName(), lbCookieStickinessPolicy.getPolicyName()) && //
Objects.equal(expectedLbCookieStickinessPolicy.getCookieExpirationPeriod(),
lbCookieStickinessPolicy.getCookieExpirationPeriod());
}
};
Collection<LBCookieStickinessPolicy> lbCookieStickinessPoliciesToDelete = Collections2.filter(
actualPolicies.getLBCookieStickinessPolicies(), Predicates.not(isExpectedPolicyPredicate));
logger.info("Delete lb cookie stickiness policies: " + lbCookieStickinessPoliciesToDelete);
for (LBCookieStickinessPolicy lbCookieStickinessPolicy : lbCookieStickinessPoliciesToDelete) {
elb.deleteLoadBalancerPolicy(new DeleteLoadBalancerPolicyRequest(loadBalancerName, lbCookieStickinessPolicy.getPolicyName()));
}
Collection<LBCookieStickinessPolicy> matchingLbCookieStyckinessPolicy = Collections2.filter(
actualPolicies.getLBCookieStickinessPolicies(), isExpectedPolicyPredicate);
if (matchingLbCookieStyckinessPolicy.isEmpty()) {
// COOKIE STICKINESS
CreateLBCookieStickinessPolicyRequest createLbCookieStickinessPolicy = new CreateLBCookieStickinessPolicyRequest() //
.withLoadBalancerName(loadBalancerName) //
.withPolicyName(expectedLbCookieStickinessPolicy.getPolicyName()) //
.withCookieExpirationPeriod(expectedLbCookieStickinessPolicy.getCookieExpirationPeriod());
logger.info("Create LBCookieStickinessPolicy: " + createLbCookieStickinessPolicy);
elb.createLBCookieStickinessPolicy(createLbCookieStickinessPolicy);
} else {
// what ?
}
// TODO verify load balancer policy is associated with the listener
List<String> expectedListenerDescriptionPolicyNames = Lists.newArrayList(expectedLbCookieStickinessPolicy.getPolicyName());
boolean mustOverWriteListenerPolicy = !ObjectUtils.equals(expectedListenerDescriptionPolicyNames,
actualListenerDescription.getPolicyNames());
if (mustOverWriteListenerPolicy) {
SetLoadBalancerPoliciesOfListenerRequest setLoadBalancerPoliciesOfListenerRequest = new SetLoadBalancerPoliciesOfListenerRequest() //
.withLoadBalancerName(loadBalancerName) //
.withLoadBalancerPort(expectedListener.getLoadBalancerPort()) //
.withPolicyNames(expectedLbCookieStickinessPolicy.getPolicyName());
logger.debug("setLoadBalancerPoliciesOfListener: {}", setLoadBalancerPoliciesOfListenerRequest);
elb.setLoadBalancerPoliciesOfListener(setLoadBalancerPoliciesOfListenerRequest);
}
// INSTANCES
Set<String> expectedEc2InstanceIds = Sets.newHashSet(Iterables.transform(expectedEc2Instances, EC2_INSTANCE_TO_INSTANCE_ID));
// register
Iterable<String> instanceIdsToRegister = Sets.difference(expectedEc2InstanceIds, actualInstanceIds);
logger.info("Register " + applicationIdentifier + " instances: " + instanceIdsToRegister);
if (!Iterables.isEmpty(instanceIdsToRegister)) {
elb.registerInstancesWithLoadBalancer(new RegisterInstancesWithLoadBalancerRequest(loadBalancerName, Lists
.newArrayList(Iterables.transform(instanceIdsToRegister, INSTANCE_ID_TO_ELB_INSTANCE))));
}
// deregister
Iterable<String> instanceIdsToDeregister = Sets.difference(actualInstanceIds, expectedEc2InstanceIds);
logger.info("Deregister " + applicationIdentifier + " instances: " + instanceIdsToDeregister);
if (!Iterables.isEmpty(instanceIdsToDeregister)) {
elb.deregisterInstancesFromLoadBalancer(new DeregisterInstancesFromLoadBalancerRequest(loadBalancerName, Lists
.newArrayList(Iterables.transform(instanceIdsToDeregister, INSTANCE_ID_TO_ELB_INSTANCE))));
}
// QUERY TO GET UP TO DATE LOAD BALANCER DESCRIPTION
LoadBalancerDescription elasticLoadBalancerDescription = Iterables.getOnlyElement(elb.describeLoadBalancers(
new DescribeLoadBalancersRequest(Arrays.asList(loadBalancerName))).getLoadBalancerDescriptions());
return elasticLoadBalancerDescription;
}
public List<Instance> createTomcatServers(DBInstance dbInstance, String applicationIdentifier, String jdbcUsername,
String jdbcPassword, String warUrl, String rootContext, Distribution... distributions) {
logger.info("ENFORCE TOMCAT SERVERS");
List<Instance> instances = Lists.newArrayList();
for (Distribution distribution : distributions) {
String userData = buildCloudInitUserData(distribution, dbInstance, jdbcUsername, jdbcPassword, warUrl, rootContext);
// CREATE EC2 INSTANCES
RunInstancesRequest runInstancesRequest = new RunInstancesRequest() //
.withInstanceType(distribution.getInstanceType().toString()) //
.withImageId(distribution.getAmiId()) //
.withMinCount(1) //
.withMaxCount(1) //
.withSecurityGroupIds("tomcat") //
.withKeyName("xebia-france") //
.withUserData(userData) //
;
RunInstancesResult runInstances = ec2.runInstances(runInstancesRequest);
instances.addAll(runInstances.getReservation().getInstances());
}
// TAG EC2 INSTANCES
int idx = 1;
for (Instance instance : instances) {
CreateTagsRequest createTagsRequest = new CreateTagsRequest();
createTagsRequest.withResources(instance.getInstanceId()) //
.withTags(//
new Tag("Name", applicationIdentifier + "-" + idx), //
new Tag("Role", applicationIdentifier), //
new Tag("Distribution", Distribution.fromAmiId(instance.getImageId()).name()));
ec2.createTags(createTagsRequest);
idx++;
}
logger.info("Created {}", instances);
return instances;
}
public void listDbInstances() {
DescribeDBInstancesResult describeDBInstancesResult = rds.describeDBInstances();
logger.info("db instances:");
for (DBInstance dbInstance : describeDBInstancesResult.getDBInstances()) {
logger.info(dbInstance.toString());
}
}
}