Java tutorial
/** * Copyright 2015-2017 Robin Steel * * 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 squash.deployment.lambdas; import squash.deployment.lambdas.utils.CloudFormationResponder; import squash.deployment.lambdas.utils.ExceptionUtils; import squash.deployment.lambdas.utils.LambdaInputLogger; import org.apache.commons.lang3.tuple.ImmutablePair; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.cloudwatchevents.AmazonCloudWatchEvents; import com.amazonaws.services.cloudwatchevents.AmazonCloudWatchEventsClientBuilder; import com.amazonaws.services.cloudwatchevents.model.DeleteRuleRequest; import com.amazonaws.services.cloudwatchevents.model.ListTargetsByRuleRequest; import com.amazonaws.services.cloudwatchevents.model.ListTargetsByRuleResult; import com.amazonaws.services.cloudwatchevents.model.PutRuleRequest; import com.amazonaws.services.cloudwatchevents.model.PutTargetsRequest; import com.amazonaws.services.cloudwatchevents.model.RemoveTargetsRequest; import com.amazonaws.services.cloudwatchevents.model.RuleState; import com.amazonaws.services.cloudwatchevents.model.Target; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Manages the AWS Cloudformation CloudwatchEvents custom resource. * * <p>The Cloudwatch scheduled event rules are created and deleted by * Cloudformation using a custom resource backed by this lambda function. * * <p>Scheduled events are used by the: * <ul> * <li>Bookings service to: * <ul> * <li>Apply the next day's booking rules just before every midnight.</li> * <li>Backup all bookings and booking rules just before every midnight.</li> * <li>Keep the lambda functions warm by running them every 5 minutes.</li> * </ul> * </li> * <li>Front-end service to: * <ul> * <li>Move the website forward one day just after every midnight.</li> * </ul> * </li> * </ul> * * <p>N.B. You should create at most one CloudwatchEvents custom resource per stack. * * @author robinsteel19@outlook.com (Robin Steel) */ public class ScheduledCloudwatchEventCustomResourceLambda implements RequestHandler<Map<String, Object>, Object> { /** * Implementation for the AWS Lambda function backing the CloudwatchEvents resource. * * <p>This lambda requires the following environment variables: * <ul> * <li>ApiGatewayBaseUrl - base Url of the ApiGateway Api.</li> * <li>ApplyBookingRulesLambdaArn - arn of the lambda function to apply the booking rules.</li> * <li>DatabaseBackupLambdaArn - arn of the lambda function to backup all bookings.</li> * <li>CreateOrDeleteBookingsLambdaArn - arn of the lambda function to keep warm.</li> * <li>UpdateBookingsLambdaArn - arn of the lambda function to move the site forward by a day.</li> * <li>Region - the AWS region in which the Cloudformation stack is created.</li> * <li>Revision - integer incremented to force stack updates to update this resource.</li> * </ul> * * <p>On success, it returns the following outputs to Cloudformation: * <ul> * <li>UpdateBookingsServiceEventRuleArn - arn of the pre-midnight rule to update the bookings service.</li> * <li>UpdateFrontendServiceEventRuleArn - arn of the post-midnight rule to update the website.</li> * <li>PrewarmerEventRuleArn - arn of the lambda prewarmer rule.</li> * </ul> * * @param request request parameters as provided by the CloudFormation service * @param context context as provided by the CloudFormation service */ @Override public Object handleRequest(Map<String, Object> request, Context context) { LambdaLogger logger = context.getLogger(); logger.log("Starting ScheduledCloudwatchEvent custom resource handleRequest"); // Handle standard request parameters Map<String, String> standardRequestParameters = LambdaInputLogger.logStandardRequestParameters(request, logger); String requestType = standardRequestParameters.get("RequestType"); // Handle required environment variables logger.log("Logging required environment variables for custom resource request"); String apiGatewayBaseUrl = System.getenv("ApiGatewayBaseUrl"); String applyBookingRulesLambdaArn = System.getenv("ApplyBookingRulesLambdaArn"); String databaseBackupLambdaArn = System.getenv("DatabaseBackupLambdaArn"); String createOrDeleteBookingsLambdaArn = System.getenv("CreateOrDeleteBookingsLambdaArn"); String updateBookingsLambdaArn = System.getenv("UpdateBookingsLambdaArn"); String region = System.getenv("AWS_REGION"); String revision = System.getenv("Revision"); // Log out our required environment variables logger.log("ApiGatewayBaseUrl: " + apiGatewayBaseUrl); logger.log("ApplyBookingRulesLambdaArn: " + applyBookingRulesLambdaArn); logger.log("DatabaseBackupLambdaArn: " + databaseBackupLambdaArn); logger.log("CreateOrDeleteBookingsLambdaArn: " + createOrDeleteBookingsLambdaArn); logger.log("UpdateBookingsLambdaArn: " + updateBookingsLambdaArn); logger.log("Region: " + region); logger.log("Revision: " + revision); // API calls below can sometimes give access denied errors during stack // creation which I think is bc required new roles have not yet propagated // across AWS. We sleep here to allow time for this propagation. try { Thread.sleep(10000); } catch (InterruptedException e) { logger.log("Sleep to allow new roles to propagate has been interrupted."); } // Prepare our response to be sent in the finally block CloudFormationResponder cloudFormationResponder = new CloudFormationResponder(standardRequestParameters, "DummyPhysicalResourceId"); // Initialise failure response, which will be changed on success String responseStatus = "FAILED"; // Ensure unique names, so multiple stacks do not clash // Stack id is like: // "arn:aws:cloudformation:us-west-2:EXAMPLE/stack-name/guid". We // want the last guid section. String stackId = standardRequestParameters.get("StackId"); String guid = stackId.substring(stackId.lastIndexOf('/') + 1); String preMidnightRuleName = "PreMidnightRunner_" + guid; String preMidnightapplyBookingRulesTargetId = "ApplyRulesTarget_" + guid; String preMidnightDatabaseBackupTargetId = "BackupTarget_" + guid; String postMidnightRuleName = "PostMidnightRunner_" + guid; String postMidnightWebsiteRefreshTargetId = "WebsiteRefreshTarget_" + guid; String prewarmerRuleName = "Prewarmer_" + guid; String prewarmerTargetId = "PrewarmerTarget_" + guid; Map<String, String> ruleArns = null; try { cloudFormationResponder.initialise(); AmazonCloudWatchEvents amazonCloudWatchEventsClient = AmazonCloudWatchEventsClientBuilder.standard() .withRegion(region).build(); if (requestType.equals("Create")) { ruleArns = new HashMap<>(); ImmutablePair<String, String> ruleArn = setUpPreMidnightRuleAndTargets(preMidnightRuleName, preMidnightapplyBookingRulesTargetId, applyBookingRulesLambdaArn, apiGatewayBaseUrl, preMidnightDatabaseBackupTargetId, databaseBackupLambdaArn, amazonCloudWatchEventsClient, logger); ruleArns.put(ruleArn.left, ruleArn.right); ruleArn = setUpPostMidnightRuleAndTargets(postMidnightRuleName, postMidnightWebsiteRefreshTargetId, updateBookingsLambdaArn, apiGatewayBaseUrl, amazonCloudWatchEventsClient, logger); ruleArns.put(ruleArn.left, ruleArn.right); ruleArn = setUpPrewarmerRuleAndTargets(prewarmerRuleName, prewarmerTargetId, createOrDeleteBookingsLambdaArn, amazonCloudWatchEventsClient, logger); ruleArns.put(ruleArn.left, ruleArn.right); } else if (requestType.equals("Update")) { // First remove any existing targets from the rules logger.log("Removing existing targets from rules"); ListTargetsByRuleRequest listTargetsByRuleRequest = new ListTargetsByRuleRequest(); listTargetsByRuleRequest.setRule(preMidnightRuleName); ListTargetsByRuleResult listTargetsByRuleResult = amazonCloudWatchEventsClient .listTargetsByRule(listTargetsByRuleRequest); List<String> targets = listTargetsByRuleResult.getTargets().stream().map(Target::getId) .collect(Collectors.toList()); RemoveTargetsRequest removeTargetsRequest = new RemoveTargetsRequest(); removeTargetsRequest.setRule(preMidnightRuleName); removeTargetsRequest.setIds(targets); amazonCloudWatchEventsClient.removeTargets(removeTargetsRequest); logger.log("Successfully removed targets from pre-midnight rule"); listTargetsByRuleRequest.setRule(postMidnightRuleName); listTargetsByRuleResult = amazonCloudWatchEventsClient.listTargetsByRule(listTargetsByRuleRequest); targets = listTargetsByRuleResult.getTargets().stream().map(Target::getId) .collect(Collectors.toList()); removeTargetsRequest = new RemoveTargetsRequest(); removeTargetsRequest.setRule(postMidnightRuleName); removeTargetsRequest.setIds(targets); amazonCloudWatchEventsClient.removeTargets(removeTargetsRequest); logger.log("Successfully removed targets from post-midnight rule"); listTargetsByRuleRequest.setRule(prewarmerRuleName); listTargetsByRuleResult = amazonCloudWatchEventsClient.listTargetsByRule(listTargetsByRuleRequest); targets = listTargetsByRuleResult.getTargets().stream().map(Target::getId) .collect(Collectors.toList()); removeTargetsRequest = new RemoveTargetsRequest(); removeTargetsRequest.setRule(prewarmerRuleName); removeTargetsRequest.setIds(targets); amazonCloudWatchEventsClient.removeTargets(removeTargetsRequest); logger.log("Successfully removed targets from prewarmer rule"); // Re-put the rules and then add back updated targets to them logger.log("Adding back updated rules and their targets"); ruleArns = new HashMap<>(); ImmutablePair<String, String> ruleArn = setUpPreMidnightRuleAndTargets(preMidnightRuleName, preMidnightapplyBookingRulesTargetId, applyBookingRulesLambdaArn, apiGatewayBaseUrl, preMidnightDatabaseBackupTargetId, databaseBackupLambdaArn, amazonCloudWatchEventsClient, logger); ruleArns.put(ruleArn.left, ruleArn.right); ruleArn = setUpPostMidnightRuleAndTargets(postMidnightRuleName, postMidnightWebsiteRefreshTargetId, updateBookingsLambdaArn, apiGatewayBaseUrl, amazonCloudWatchEventsClient, logger); ruleArns.put(ruleArn.left, ruleArn.right); ruleArn = setUpPrewarmerRuleAndTargets(prewarmerRuleName, prewarmerTargetId, createOrDeleteBookingsLambdaArn, amazonCloudWatchEventsClient, logger); ruleArns.put(ruleArn.left, ruleArn.right); } else if (requestType.equals("Delete")) { logger.log("Delete request - so deleting the scheduled cloudwatch rules"); // Delete target from pre-midnight rule logger.log("Removing lambda targets from pre-midnight rule"); RemoveTargetsRequest removePreMidnightTargetsRequest = new RemoveTargetsRequest(); removePreMidnightTargetsRequest.setRule(preMidnightRuleName); Collection<String> preMidnightTargetIds = new ArrayList<>(); preMidnightTargetIds.add(preMidnightapplyBookingRulesTargetId); preMidnightTargetIds.add(preMidnightDatabaseBackupTargetId); removePreMidnightTargetsRequest.setIds(preMidnightTargetIds); amazonCloudWatchEventsClient.removeTargets(removePreMidnightTargetsRequest); logger.log("Removed lambda target from pre-midnight rule"); // Delete pre-midnight scheduled rule logger.log("Deleting pre-midnight rule"); DeleteRuleRequest deletePreMidnightRuleRequest = new DeleteRuleRequest(); deletePreMidnightRuleRequest.setName(preMidnightRuleName); amazonCloudWatchEventsClient.deleteRule(deletePreMidnightRuleRequest); logger.log("Deleted pre-midnight rule"); // Delete target from post-midnight rule logger.log("Removing lambda targets from post-midnight rule"); RemoveTargetsRequest removePostMidnightTargetsRequest = new RemoveTargetsRequest(); removePostMidnightTargetsRequest.setRule(postMidnightRuleName); Collection<String> postMidnightTargetIds = new ArrayList<>(); postMidnightTargetIds.add(postMidnightWebsiteRefreshTargetId); removePostMidnightTargetsRequest.setIds(postMidnightTargetIds); amazonCloudWatchEventsClient.removeTargets(removePostMidnightTargetsRequest); logger.log("Removed lambda target from post-midnight rule"); // Delete post-midnight scheduled rule logger.log("Deleting post-midnight rule"); DeleteRuleRequest deletePostMidnightRuleRequest = new DeleteRuleRequest(); deletePostMidnightRuleRequest.setName(postMidnightRuleName); amazonCloudWatchEventsClient.deleteRule(deletePostMidnightRuleRequest); logger.log("Deleted post-midnight rule"); // Delete target from prewarmer rule logger.log("Removing lambda target from Prewarmer rule"); RemoveTargetsRequest removePrewarmerTargetsRequest = new RemoveTargetsRequest(); removePrewarmerTargetsRequest.setRule(prewarmerRuleName); Collection<String> prewarmerTargetIds = new ArrayList<>(); prewarmerTargetIds.add(prewarmerTargetId); removePrewarmerTargetsRequest.setIds(prewarmerTargetIds); amazonCloudWatchEventsClient.removeTargets(removePrewarmerTargetsRequest); logger.log("Removed lambda target from Prewarmer rule"); // Delete prewarmer scheduled rule logger.log("Deleting Prewarmer rule"); DeleteRuleRequest deletePrewarmerRuleRequest = new DeleteRuleRequest(); deletePrewarmerRuleRequest.setName(prewarmerRuleName); amazonCloudWatchEventsClient.deleteRule(deletePrewarmerRuleRequest); logger.log("Deleted Prewarmer rule"); logger.log("Finished removing the scheduled cloudwatch rules"); } responseStatus = "SUCCESS"; return null; } catch (AmazonServiceException ase) { ExceptionUtils.logAmazonServiceException(ase, logger); return null; } catch (AmazonClientException ace) { ExceptionUtils.logAmazonClientException(ace, logger); return null; } catch (Exception e) { logger.log("Exception caught in the scheduled cloudwatch event Lambda: " + e.getMessage()); return null; } finally { // Send response to CloudFormation cloudFormationResponder.addKeyValueOutputsPair("UpdateBookingsServiceEventRuleArn", (requestType.equals("Delete") || (ruleArns == null)) ? "Not available" : ruleArns.get("UpdateBookingsServiceEventRuleArn")); cloudFormationResponder.addKeyValueOutputsPair("UpdateFrontendServiceEventRuleArn", (requestType.equals("Delete") || (ruleArns == null)) ? "Not available" : ruleArns.get("UpdateFrontendServiceEventRuleArn")); cloudFormationResponder.addKeyValueOutputsPair("PrewarmerEventRuleArn", (requestType.equals("Delete") || (ruleArns == null)) ? "Not available" : ruleArns.get("PrewarmerEventRuleArn")); cloudFormationResponder.sendResponse(responseStatus, logger); } } ImmutablePair<String, String> setUpPreMidnightRuleAndTargets(String ruleName, String applyBookingRulesTargetId, String applyBookingRulesLambdaArn, String apiGatewayBaseUrl, String databaseBackupTargetId, String databaseBackupLambdaArn, AmazonCloudWatchEvents amazonCloudWatchEventsClient, LambdaLogger logger) { // Create pre-midnight rule with Cron expression logger.log("Creating pre-midnight rule"); PutRuleRequest putRuleRequest = new PutRuleRequest(); // Put just before midnight to allow rule-based bookings to be created // before anyone else has a chance to create bookings that might clash. // This is 9pm UTC - i.e. 10pm BST. putRuleRequest.setScheduleExpression("cron(0 21 * * ? *)"); putRuleRequest.setName(ruleName); putRuleRequest.setState(RuleState.ENABLED); putRuleRequest.setDescription( "This runs just before midnight every day to apply booking rules for the following day and to backup all bookings."); ImmutablePair<String, String> ruleArn = new ImmutablePair<>("UpdateBookingsServiceEventRuleArn", amazonCloudWatchEventsClient.putRule(putRuleRequest).getRuleArn()); // Create target with applyBookingRules and backupBookings lambdas, and // attach rule to it. logger.log("Attaching applyBookingRules lambda to the pre-midnight rule"); Target applyBookingRulesTarget = new Target(); applyBookingRulesTarget.setArn(applyBookingRulesLambdaArn); applyBookingRulesTarget.setInput("{\"apiGatewayBaseUrl\" : \"" + apiGatewayBaseUrl + "\"}"); applyBookingRulesTarget.setId(applyBookingRulesTargetId); Collection<Target> midnightTargets = new ArrayList<>(); midnightTargets.add(applyBookingRulesTarget); logger.log("Attaching database backup lambda to the pre-midnight rule"); Target databaseBackupTarget = new Target(); databaseBackupTarget.setArn(databaseBackupLambdaArn); databaseBackupTarget.setId(databaseBackupTargetId); midnightTargets.add(databaseBackupTarget); PutTargetsRequest putTargetsRequest = new PutTargetsRequest(); putTargetsRequest.setRule(ruleName); putTargetsRequest.setTargets(midnightTargets); amazonCloudWatchEventsClient.putTargets(putTargetsRequest); logger.log("Targets attached to the pre-midnight rule"); return ruleArn; } ImmutablePair<String, String> setUpPostMidnightRuleAndTargets(String ruleName, String websiteRefreshTargetId, String updateBookingsLambdaArn, String apiGatewayBaseUrl, AmazonCloudWatchEvents amazonCloudWatchEventsClient, LambdaLogger logger) { // Create post-midnight rule with Cron expression logger.log("Creating post-midnight rule"); PutRuleRequest putRuleRequest = new PutRuleRequest(); // Put just after midnight to avoid any timing glitch i.e. somehow still // thinking it's the previous day when it runs. This is 10 minutes after // midnight UTC - i.e. 1.10AM BST. putRuleRequest.setScheduleExpression("cron(10 0 * * ? *)"); putRuleRequest.setName(ruleName); putRuleRequest.setState(RuleState.ENABLED); putRuleRequest.setDescription( "This runs just after midnight every day to refresh all the squash booking pages in S3"); ImmutablePair<String, String> ruleArn = new ImmutablePair<>("UpdateFrontendServiceEventRuleArn", amazonCloudWatchEventsClient.putRule(putRuleRequest).getRuleArn()); // Create target with updateBookings lambda, and attach rule to it. logger.log("Attaching updataBookings lambda to the post-midnight rule"); Target updateBookingsTarget = new Target(); updateBookingsTarget.setArn(updateBookingsLambdaArn); updateBookingsTarget.setInput("{\"apiGatewayBaseUrl\" : \"" + apiGatewayBaseUrl + "\"}"); updateBookingsTarget.setId(websiteRefreshTargetId); Collection<Target> midnightTargets = new ArrayList<>(); midnightTargets.add(updateBookingsTarget); PutTargetsRequest putMidnightTargetsRequest = new PutTargetsRequest(); putMidnightTargetsRequest.setRule(ruleName); putMidnightTargetsRequest.setTargets(midnightTargets); amazonCloudWatchEventsClient.putTargets(putMidnightTargetsRequest); return ruleArn; } ImmutablePair<String, String> setUpPrewarmerRuleAndTargets(String ruleName, String prewarmerTargetId, String createOrDeleteBookingsLambdaArn, AmazonCloudWatchEvents amazonCloudWatchEventsClient, LambdaLogger logger) { // Create prewarmer rule with Rate expression logger.log("Creating prewarmer rule"); PutRuleRequest putRuleRequest = new PutRuleRequest(); putRuleRequest.setScheduleExpression("rate(5 minutes)"); putRuleRequest.setName(ruleName); putRuleRequest.setState(RuleState.ENABLED); putRuleRequest.setDescription("This runs every 5 minutes to prewarm the squash bookings lambdas"); ImmutablePair<String, String> ruleArn = new ImmutablePair<>("PrewarmerEventRuleArn", amazonCloudWatchEventsClient.putRule(putRuleRequest).getRuleArn()); // Create target with bookings lambda, and attach rule to it logger.log("Attaching bookings lambda to the prewarmer rule"); Target prewarmerTarget = new Target(); prewarmerTarget.setArn(createOrDeleteBookingsLambdaArn); prewarmerTarget.setInput("{\"slot\" : \"-1\"}"); prewarmerTarget.setId(prewarmerTargetId); Collection<Target> prewarmerTargets = new ArrayList<>(); prewarmerTargets.add(prewarmerTarget); PutTargetsRequest putPrewarmerTargetsRequest = new PutTargetsRequest(); putPrewarmerTargetsRequest.setRule(ruleName); putPrewarmerTargetsRequest.setTargets(prewarmerTargets); amazonCloudWatchEventsClient.putTargets(putPrewarmerTargetsRequest); return ruleArn; } }