├── .gitignore ├── scripts ├── stop_application ├── write_codedeploy_config.sh ├── basic_health_check.sh ├── configure_http_port.xsl ├── start_application └── install_dependencies ├── setup-block-production-resources-stack-parameters.json ├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── block-production-demo-resources-parameters.json ├── appspec.yml ├── src └── main │ ├── webapp │ └── WEB-INF │ │ ├── pages │ │ └── index.jsp │ │ └── web.xml │ └── java │ └── com │ └── amazonaws │ └── labs │ └── sampleapp │ ├── IndexController.java │ └── MvcConfiguration.java ├── register-canary-approval.js ├── synthetic-test-runner.js ├── CONTRIBUTING.md ├── pom.xml ├── README.md ├── setup-block-production-resources-stack.yml ├── process-canary-approval.js ├── LICENSE └── block-production-demo-resources.yml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /scripts/stop_application: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /sbin/service tomcat7 stop -------------------------------------------------------------------------------- /setup-block-production-resources-stack-parameters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "DemoResourcesCodeCommitRepo", 4 | "ParameterValue": "aws-codepipeline-block-production" 5 | } 6 | ] -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /scripts/write_codedeploy_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /bin/mkdir -p /var/codedeploy/tomcat-sample 6 | 7 | /bin/cat </var/codedeploy/tomcat-sample/env.properties 8 | APPLICATION_NAME=$APPLICATION_NAME 9 | DEPLOYMENT_GROUP_NAME=$DEPLOYMENT_GROUP_NAME 10 | DEPLOYMENT_ID=$DEPLOYMENT_ID 11 | EOF 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /scripts/basic_health_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in `seq 1 10`; 4 | do 5 | HTTP_CODE=`/usr/bin/curl --write-out '%{http_code}' -o /dev/null -m 10 -q -s http://localhost:80` 6 | if [ "$HTTP_CODE" == "200" ]; then 7 | /bin/echo "Successfully pulled root page." 8 | exit 0; 9 | fi 10 | /bin/echo "Attempt to curl endpoint returned HTTP Code $HTTP_CODE. Backing off and retrying." 11 | /bin/sleep 10 12 | done 13 | /bin/echo "Server did not come up after expected time. Failing." 14 | exit 1 15 | -------------------------------------------------------------------------------- /block-production-demo-resources-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters" : { 3 | "KeyName" : "", 4 | "AppName" : "", 5 | "DemoResourcesCodeCommitRepo" : "aws-codepipeline-block-production", 6 | "DemoResourcesCodeCommitRepoBranch" : "master", 7 | "CanaryApprovalConfiguration": "{\"timeoutMinutes\" : 20,\"metricsRequired\" : 5}", 8 | "YourIP" : "IP address to connect to SSH from. Check http://checkip.amazonaws.com/ to find yours." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scripts/configure_http_port.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: /target/SampleMavenTomcatApp.war 5 | destination: /tmp/codedeploy-deployment-staging-area/ 6 | - source: /scripts/configure_http_port.xsl 7 | destination: /tmp/codedeploy-deployment-staging-area/ 8 | hooks: 9 | ApplicationStop: 10 | - location: scripts/stop_application 11 | timeout: 300 12 | BeforeInstall: 13 | - location: scripts/install_dependencies 14 | timeout: 300 15 | ApplicationStart: 16 | - location: scripts/write_codedeploy_config.sh 17 | - location: scripts/start_application 18 | timeout: 300 19 | ValidateService: 20 | - location: scripts/basic_health_check.sh 21 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/pages/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample Deployment 6 | 26 | 27 | 28 |
29 |

Congratulations

30 |

This application was deployed using AWS CodeDeploy by AWS CodePipeline.

31 |

AWS CodeDeploy Application: ${applicationName}

32 |

AWS CodeDeploy Deployment Group: ${deploymentGroupName}

33 |

For next steps, read the AWS CodePipeline Documentation.

34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 6 | Sample Tomcat Web Application 7 | 8 | mvc-dispatcher 9 | org.springframework.web.servlet.DispatcherServlet 10 | 11 | contextClass 12 | org.springframework.web.context.support.AnnotationConfigWebApplicationContext 13 | 14 | 15 | contextConfigLocation 16 | com.amazonaws.labs.sampleapp.MvcConfiguration 17 | 18 | 1 19 | 20 | 21 | mvc-dispatcher 22 | / 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/start_application: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CATALINA_HOME='/usr/share/tomcat7-codedeploy' 6 | DEPLOY_TO_ROOT='true' 7 | #CONTEXT_PATH='##CONTEXT_PATH##' 8 | SERVER_HTTP_PORT='80' 9 | 10 | TEMP_STAGING_DIR='/tmp/codedeploy-deployment-staging-area' 11 | WAR_STAGED_LOCATION="$TEMP_STAGING_DIR/SampleMavenTomcatApp.war" 12 | HTTP_PORT_CONFIG_XSL_LOCATION="$TEMP_STAGING_DIR/configure_http_port.xsl" 13 | 14 | # In Tomcat, ROOT.war maps to the server root 15 | if [[ "$DEPLOY_TO_ROOT" = 'true' ]]; then 16 | CONTEXT_PATH='ROOT' 17 | fi 18 | 19 | # Remove unpacked application artifacts 20 | if [[ -f $CATALINA_HOME/webapps/$CONTEXT_PATH.war ]]; then 21 | /bin/rm $CATALINA_HOME/webapps/$CONTEXT_PATH.war 22 | fi 23 | if [[ -d $CATALINA_HOME/webapps/$CONTEXT_PATH ]]; then 24 | /bin/rm -rfv $CATALINA_HOME/webapps/$CONTEXT_PATH 25 | fi 26 | 27 | # Copy the WAR file to the webapps directory 28 | /bin/cp $WAR_STAGED_LOCATION $CATALINA_HOME/webapps/$CONTEXT_PATH.war 29 | 30 | # Configure the Tomcat server HTTP connector 31 | { /usr/bin/which xsltproc; } || { /usr/bin/yum install xsltproc; } 32 | /bin/cp $CATALINA_HOME/conf/server.xml $CATALINA_HOME/conf/server.xml.bak 33 | /usr/bin/xsltproc $HTTP_PORT_CONFIG_XSL_LOCATION $CATALINA_HOME/conf/server.xml.bak > $CATALINA_HOME/conf/server.xml 34 | 35 | /sbin/service tomcat7 start 36 | -------------------------------------------------------------------------------- /register-canary-approval.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | var aws = require("aws-sdk"); 16 | var ddb = new aws.DynamoDB.DocumentClient(); 17 | const PIPELINE_APPROVAL_DDB_TABLE = "BlockProductionDemo-PipelineApprovals"; 18 | 19 | exports.handler = (event, context, callback) => { 20 | 21 | if (event.Records[0]) { 22 | var notificationData = JSON.parse(event.Records[0].Sns.Message); 23 | ddb.put({ 24 | TableName: PIPELINE_APPROVAL_DDB_TABLE, 25 | Item: { 26 | ApprovalToken: notificationData.approval.token, 27 | ApprovalContent: notificationData.approval, 28 | StartTime: new Date().getTime() 29 | } 30 | }, function(err, data) { 31 | if (err) { 32 | var message = "Unable to register pipeline approval request. Error JSON:" + JSON.stringify(err); 33 | console.error(message); 34 | callback(error, message); 35 | } else { 36 | var message = "Successfully registered pipeline approval request: " + JSON.stringify(notificationData.approval); 37 | console.log(message); 38 | callback(null, message); 39 | } 40 | }); 41 | } else { 42 | callback(null); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /scripts/install_dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CATALINA_HOME=/usr/share/tomcat7-codedeploy 6 | 7 | # Tar file name 8 | TOMCAT7_CORE_TAR_FILENAME='apache-tomcat-7.0.73.tar.gz' 9 | # Download URL for Tomcat7 core 10 | TOMCAT7_CORE_DOWNLOAD_URL="http://mirror.olnevhost.net/pub/apache/tomcat/tomcat-7/v7.0.73/bin/$TOMCAT7_CORE_TAR_FILENAME" 11 | # The top-level directory after unpacking the tar file 12 | TOMCAT7_CORE_UNPACKED_DIRNAME='apache-tomcat-7.0.73' 13 | 14 | 15 | # Check whether there exists a valid instance 16 | # of Tomcat7 installed at the specified directory 17 | [[ -d $CATALINA_HOME ]] && { /sbin/service tomcat7 status; } && { 18 | /bin/echo "Tomcat7 is already installed at $CATALINA_HOME. Skip reinstalling it." 19 | exit 0 20 | } 21 | 22 | # Clear install directory 23 | if [ -d $CATALINA_HOME ]; then 24 | /bin/rm -rf $CATALINA_HOME 25 | fi 26 | /bin/mkdir -p $CATALINA_HOME 27 | 28 | # Download the latest Tomcat7 version 29 | cd /tmp 30 | { /usr/bin/which wget; } || { /usr/bin/yum install wget; } 31 | /usr/bin/wget $TOMCAT7_CORE_DOWNLOAD_URL 32 | if [[ -d /tmp/$TOMCAT7_CORE_UNPACKED_DIRNAME ]]; then 33 | /usr/rm -rf /tmp/$TOMCAT7_CORE_UNPACKED_DIRNAME 34 | fi 35 | /bin/tar xzf $TOMCAT7_CORE_TAR_FILENAME 36 | 37 | # Copy over to the CATALINA_HOME 38 | /bin/cp -r /tmp/$TOMCAT7_CORE_UNPACKED_DIRNAME/* $CATALINA_HOME 39 | 40 | # Install Java if not yet installed 41 | { /usr/bin/which java; } || { /usr/bin/yum install java; } 42 | 43 | # Create the service init.d script 44 | /bin/cat > /etc/init.d/tomcat7 <<'EOF' 45 | #!/bin/bash 46 | # description: Tomcat7 Start Stop Restart 47 | # processname: tomcat7 48 | PATH=$JAVA_HOME/bin:$PATH 49 | export PATH 50 | CATALINA_HOME='/usr/share/tomcat7-codedeploy' 51 | 52 | case $1 in 53 | start) 54 | /bin/sh $CATALINA_HOME/bin/startup.sh 55 | ;; 56 | stop) 57 | /bin/sh $CATALINA_HOME/bin/shutdown.sh 58 | ;; 59 | restart) 60 | /bin/sh $CATALINA_HOME/bin/shutdown.sh 61 | /bin/sh $CATALINA_HOME/bin/startup.sh 62 | ;; 63 | esac 64 | exit 0 65 | EOF 66 | 67 | # Change permission mode for the service script 68 | /bin/chmod 755 /etc/init.d/tomcat7 69 | 70 | -------------------------------------------------------------------------------- /synthetic-test-runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | var http = require("http"); 16 | var aws = require("aws-sdk"); 17 | var cloudWatch = new aws.CloudWatch(); 18 | 19 | exports.handler = (event, context, callback) => { 20 | 21 | var options = { 22 | host: process.env.WEBSITE_URL, 23 | port: 80, 24 | path: '/' 25 | }; 26 | 27 | // In real world scenarios, deeper health checks should be done. 28 | var content = ""; 29 | var req = http.request(options, function(res) { 30 | res.setEncoding("utf8"); 31 | res.on("data", function(chunk) { 32 | content += chunk; 33 | }); 34 | 35 | res.on("end", function() { 36 | evaluateContent(content); 37 | }); 38 | }); 39 | 40 | req.end(); 41 | 42 | var evaluateContent = function(content) { 43 | if (content.indexOf("AWS CodePipeline") != -1) { 44 | emitCloudWatchMetrics(0); 45 | } else { 46 | emitCloudWatchMetrics(1); 47 | } 48 | }; 49 | 50 | var buildMetricsParams = function(count) { 51 | return { 52 | MetricData: [{ 53 | MetricName: process.env.METRIC_NAME, 54 | Timestamp: new Date, 55 | Value: count, 56 | Unit: "Count" 57 | }], 58 | Namespace: process.env.METRIC_NAMESPACE 59 | } 60 | }; 61 | 62 | var emitCloudWatchMetrics = function(failure) { 63 | cloudWatch.putMetricData(buildMetricsParams(failure), function(err, data) { 64 | if (err) { 65 | console.log(err, err.stack); // an error occurred 66 | callback("Synthetic Test Failure"); 67 | } else { 68 | console.log(data); // successful response 69 | callback(null, "Synthetic Test Successful"); 70 | } 71 | }); 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/labs/sampleapp/IndexController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazonaws.labs.sampleapp; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Date; 19 | import java.util.List; 20 | import java.util.logging.Logger; 21 | 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.beans.factory.annotation.Value; 24 | import org.springframework.stereotype.Controller; 25 | import org.springframework.ui.Model; 26 | import org.springframework.web.bind.annotation.RequestMapping; 27 | import org.springframework.web.bind.annotation.RequestMethod; 28 | 29 | import com.amazonaws.services.cloudwatch.AmazonCloudWatch; 30 | import com.amazonaws.services.cloudwatch.model.Dimension; 31 | import com.amazonaws.services.cloudwatch.model.MetricDatum; 32 | import com.amazonaws.services.cloudwatch.model.PutMetricDataRequest; 33 | import com.amazonaws.services.cloudwatch.model.StandardUnit; 34 | 35 | @Controller 36 | public class IndexController { 37 | private final static Logger LOGGER = Logger.getLogger(IndexController.class.getName()); 38 | 39 | @Value("${APPLICATION_NAME}") 40 | private String applicationName; 41 | 42 | @Value("${DEPLOYMENT_GROUP_NAME}") 43 | private String deploymentGroupName; 44 | 45 | @Autowired 46 | private AmazonCloudWatch amazonCloudWatch; 47 | 48 | @RequestMapping(value = "/", method = RequestMethod.GET) 49 | public String displayIndex(Model model) { 50 | LOGGER.info("Application name set to: " + applicationName); 51 | model.addAttribute("applicationName", applicationName); 52 | LOGGER.info("Deployment Group Name set to: " + deploymentGroupName); 53 | model.addAttribute("deploymentGroupName", deploymentGroupName); 54 | 55 | emitMetrics(applicationName, deploymentGroupName); 56 | return "/index"; 57 | } 58 | 59 | private void emitMetrics(final String applicationName, final String deploymentGroupName) { 60 | final PutMetricDataRequest request = new PutMetricDataRequest(); 61 | request.setNamespace(applicationName); 62 | 63 | MetricDatum metricDatum = new MetricDatum(); 64 | metricDatum.setMetricName("Request"); 65 | metricDatum.setTimestamp(new Date()); 66 | metricDatum.setValue(1.0); 67 | metricDatum.setUnit(StandardUnit.Count); 68 | 69 | final Dimension dimension = new Dimension(); 70 | dimension.setName("DeploymentGroup"); 71 | dimension.setValue(deploymentGroupName); 72 | 73 | final List dimensions = new ArrayList<>(); 74 | dimensions.add(dimension); 75 | metricDatum.setDimensions(dimensions);; 76 | 77 | final List metrics = new ArrayList<>(); 78 | metrics.add(metricDatum); 79 | request.setMetricData(metrics); 80 | 81 | amazonCloudWatch.putMetricData(request); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/issues), or [recently closed](https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/labs/sampleapp/MvcConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazonaws.labs.sampleapp; 16 | 17 | 18 | import java.io.IOException; 19 | 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.ComponentScan; 22 | import org.springframework.context.annotation.Configuration; 23 | import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; 24 | import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 25 | import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; 26 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 27 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 28 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 29 | import org.springframework.web.servlet.view.InternalResourceViewResolver; 30 | 31 | import com.amazonaws.regions.Region; 32 | import com.amazonaws.regions.Regions; 33 | import com.amazonaws.services.cloudwatch.AmazonCloudWatch; 34 | import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient; 35 | import com.amazonaws.services.codedeploy.AmazonCodeDeploy; 36 | import com.amazonaws.services.codedeploy.AmazonCodeDeployClient; 37 | 38 | @EnableWebMvc 39 | @ComponentScan(basePackages = {"com.amazonaws.labs.sampleapp"}) 40 | @Configuration 41 | public class MvcConfiguration extends WebMvcConfigurerAdapter { 42 | private static Region region = Regions.getCurrentRegion(); 43 | 44 | @Override 45 | public void addResourceHandlers(final ResourceHandlerRegistry registry) { 46 | registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); 47 | } 48 | 49 | @Override 50 | public void configureDefaultServletHandling(final DefaultServletHandlerConfigurer configurer) { 51 | configurer.enable(); 52 | } 53 | 54 | @Bean 55 | public InternalResourceViewResolver viewResolver() { 56 | InternalResourceViewResolver resolver = new InternalResourceViewResolver(); 57 | resolver.setPrefix("/WEB-INF/pages"); 58 | resolver.setSuffix(".jsp"); 59 | return resolver; 60 | } 61 | 62 | @Bean 63 | public AmazonCodeDeploy codeDeploy() { 64 | final AmazonCodeDeploy client = new AmazonCodeDeployClient(); 65 | client.setRegion(region); 66 | return client; 67 | } 68 | 69 | @Bean 70 | public AmazonCloudWatch amazonCloudWatch() { 71 | final AmazonCloudWatch client = new AmazonCloudWatchClient(); 72 | client.setRegion(region); 73 | return client; 74 | } 75 | 76 | @Bean 77 | public PropertySourcesPlaceholderConfigurer properties() { 78 | final PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); 79 | try { 80 | configurer.setLocations(new PathMatchingResourcePatternResolver().getResources("file:/var/codedeploy/tomcat-sample/env.properties")); 81 | } catch (IOException e) { 82 | throw new RuntimeException("Failed to load resources.", e); 83 | } 84 | return configurer; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.amazonaws.labs 5 | SampleMavenTomcatApp 6 | war 7 | 1.0-SNAPSHOT 8 | SampleMavenTomcatApp Maven Webapp 9 | http://maven.apache.org 10 | 11 | 12 | 4.1.1.RELEASE 13 | 14 | 15 | 16 | 17 | 18 | com.amazonaws 19 | aws-java-sdk-bom 20 | 1.10.10 21 | pom 22 | import 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.webjars 30 | bootstrap 31 | 3.2.0 32 | 33 | 34 | org.webjars 35 | jquery 36 | 37 | 38 | 39 | 40 | org.webjars 41 | jquery 42 | 2.1.1 43 | 44 | 45 | junit 46 | junit 47 | 3.8.1 48 | test 49 | 50 | 51 | org.springframework 52 | spring-core 53 | ${spring.version} 54 | 55 | 56 | commons-logging 57 | commons-logging 58 | 59 | 60 | 61 | 62 | org.springframework 63 | spring-webmvc 64 | ${spring.version} 65 | 66 | 67 | com.amazonaws 68 | aws-java-sdk-autoscaling 69 | 70 | 71 | com.amazonaws 72 | aws-java-sdk-cloudwatch 73 | 74 | 75 | com.amazonaws 76 | aws-java-sdk-codedeploy 77 | 78 | 79 | javax.servlet 80 | javax.servlet-api 81 | 3.1.0 82 | provided 83 | 84 | 85 | 86 | SampleMavenTomcatApp 87 | 88 | 89 | maven-compiler-plugin 90 | 3.5.1 91 | 92 | 1.7 93 | 1.7 94 | 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-eclipse-plugin 99 | 2.9 100 | 101 | true 102 | false 103 | 2.0 104 | 105 | 106 | 107 | org.apache.tomcat.maven 108 | tomcat7-maven-plugin 109 | 2.2 110 | 111 | / 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS CodePipeline Block Production 2 | 3 | The resources in this repository will help you setup required AWS resources for blocking prod deployments until the canary deployment validation succeeds. 4 | 5 | ## Prerequisites 6 | 7 | 1. Create an AWS CodeCommit repository with any name of your preference using AWS console or CLI. This document assumes that the name you chose is `aws-codepipeline-block-production`. 8 | 2. Clone the content of this repository to AWS CodeCommit repository created in the above step. See this [article](http://docs.aws.amazon.com/codecommit/latest/userguide/how-to-migrate-repository.html) for details on cloning a GitHub repository to AWS CodeCommit. 9 | 3. Create an Amazon EC2 key pair if you don't have one already. 10 | 11 | ## Steps 12 | Run following steps in the local workspace where GitHub repository was cloned: 13 | 14 | 1. If you chose a different AWS CodeCommit repository name, replace `ParameterValue` in `setup-block-production-resources-stack-parameters.json` file with the name you chose. 15 | 2. Update `block-production-demo-resources-parameters.json` file to replace parameter values: 16 | * `DemoResourcesCodeCommitRepo`: Update if you chose a different repository name in the step 1 in Prerequisites section. 17 | * `DemoResourcesCodeCommitRepoBranch` : Default branch is `master`. Update if the branch name is different. 18 | * `CanaryApprovalConfiguration` : Canary approval configuration in JSON format which specifies timeout in minutes and number of metrics required before deployment is considered successful. 19 | * `timeoutMinutes` : The time in minutes to wait before considering Approval to be timed out. 20 | * `metricsRequired` : Minimum number of metrics required from canary deploy action before the canary approval is successfully completed. 21 | * `KeyName`: Amazon EC2 key pair name. 22 | * `AppName`: Default is `BlockProduction`. Some of the AWS resources will be prefixed with this name. 23 | * `YourIP` : IP address to connect to SSH from. Check http://checkip.amazonaws.com/ to find yours. 24 | 3. Create a new CloudFormation stack using AWS CloudFormation template `setup-block-production-resources-stack.yml` 25 | and parameter file `setup-block-production-resources-stack-parameters.json`. See this [article](https://aws.amazon.com/blogs/devops/passing-parameters-to-cloudformation-stacks-with-the-aws-cli-and-powershell/) for the details on how to pass parameters file using CLI. 26 | 27 | ``` 28 | aws cloudformation create-stack --stack-name SetupBlockProductionDemoResourcesStack --template-body file:///aws-codepipeline-block-production/setup-block-production-resources-stack.yml --capabilities CAPABILITY_IAM --parameters file:///aws-codepipeline-block-production/setup-block-production-resources-stack-parameters.json 29 | ``` 30 | 4. Step 3 will create an AWS CodePipeline named `SetupBlockProductionDemoResources-Pipeline`. This pipeline will use AWS CloudFormation integration with AWS CodePipeline to publish AWS Lambda functions to Amazon S3 and create a new stack using template `block-production-demo-resources.yml` that contains actual AWS resources used in demo including a new AWS CodePipeline with the name prefixed by `AppName` specified above. 31 | 5. Above step will set up following things: 32 | * A new AWS CodePipeline named `BlockProduction-Pipeline` with a stage that contains canary deploy, canary approval and prod deploy actions. Once canary deployment succeeds, canary approval action runs and sends a notification to Amazon SNS topic configured in Approval action. 33 | * An AWS Lambda function (`register-canary-approval.js`) is subscribed to this topic which registers this request in an Amazon DynamoDB table. 34 | * AWS Resources for running synthetic tests periodically including an Amazon CloudWatch alarm. 35 | * AWS Lambda function (`process-canary-approval.js`) that runs periodically and scans the table for open approval requests. If there are required number of metrics available and the synthetic tests alarm is OK then it approves the request using AWS CodePipeline API `PutApprovalResult` which allows the pipeline run to proceed to the next prod deploy action. 36 | 37 | ## Cleanup 38 | When no longer required, please remember to delete the stacks using AWS CloudFormation console or CLI to avoid getting charged. 39 | 40 | ## License 41 | This plugin is open sourced and licensed under Apache 2.0. See the LICENSE file for more information. 42 | -------------------------------------------------------------------------------- /setup-block-production-resources-stack.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'AWS Cloudformation template to help create synthetic tests demo resources.' 3 | 4 | Parameters: 5 | DemoResourcesCodeCommitRepo: 6 | Description: AWS CodeCommit repository name which contains the demo resources 7 | Type: String 8 | Default: "aws-codepipeline-block-production" 9 | DemoResourcesCodeCommitRepoBranch: 10 | Description: AWS CodeCommit repository branch 11 | Type: String 12 | Default: "master" 13 | 14 | Resources: 15 | 16 | ArtifactStoreBucket: 17 | Type: AWS::S3::Bucket 18 | 19 | Pipeline: 20 | Type: AWS::CodePipeline::Pipeline 21 | Properties: 22 | ArtifactStore: 23 | Location: !Ref 'ArtifactStoreBucket' 24 | Type: S3 25 | Name: "SetupBlockProductionDemoResources-Pipeline" 26 | RoleArn: !GetAtt [PipelineRole, Arn] 27 | Stages: 28 | - Name: Source 29 | Actions: 30 | - Name: DemoResourcesSource 31 | ActionTypeId: 32 | Category: Source 33 | Owner: AWS 34 | Provider: CodeCommit 35 | Version: '1' 36 | Configuration: 37 | RepositoryName: !Ref DemoResourcesCodeCommitRepo 38 | BranchName: !Ref DemoResourcesCodeCommitRepoBranch 39 | OutputArtifacts: 40 | - Name: DemoArtifacts 41 | RunOrder: '1' 42 | - Name: CloudformationDeploy 43 | Actions: 44 | - Name: CreateDemoResources 45 | ActionTypeId: 46 | Category: Deploy 47 | Owner: AWS 48 | Provider: CloudFormation 49 | Version: '1' 50 | InputArtifacts: 51 | - Name: DemoArtifacts 52 | Configuration: 53 | ActionMode: CREATE_UPDATE 54 | RoleArn: !GetAtt [CloudFormationRole, Arn] 55 | StackName: BlockProductionDemoResources 56 | TemplatePath: DemoArtifacts::block-production-demo-resources.yml 57 | TemplateConfiguration: DemoArtifacts::block-production-demo-resources-parameters.json 58 | ParameterOverrides: | 59 | { 60 | "DemoResourcesS3BucketName" : { "Fn::GetArtifactAtt" : ["DemoArtifacts", "BucketName"]}, 61 | "DemoResourcesS3ObjectKey" : { "Fn::GetArtifactAtt" : ["DemoArtifacts", "ObjectKey"]} 62 | } 63 | Capabilities: CAPABILITY_IAM 64 | RunOrder: '1' 65 | 66 | CloudFormationRole: 67 | Type: AWS::IAM::Role 68 | Properties: 69 | AssumeRolePolicyDocument: 70 | Statement: 71 | - Action: ['sts:AssumeRole'] 72 | Effect: Allow 73 | Principal: 74 | Service: [cloudformation.amazonaws.com] 75 | Version: '2012-10-17' 76 | Path: / 77 | Policies: 78 | - PolicyName: CloudFormationRole 79 | PolicyDocument: 80 | Version: '2012-10-17' 81 | Statement: 82 | - Action: '*' 83 | Effect: Allow 84 | Resource: '*' 85 | 86 | PipelineRole: 87 | Type: AWS::IAM::Role 88 | Properties: 89 | AssumeRolePolicyDocument: 90 | Statement: 91 | - Action: ['sts:AssumeRole'] 92 | Effect: Allow 93 | Principal: 94 | Service: [codepipeline.amazonaws.com] 95 | Version: '2012-10-17' 96 | Path: / 97 | Policies: 98 | - PolicyName: CodePipelineAccess 99 | PolicyDocument: 100 | Version: '2012-10-17' 101 | Statement: 102 | - Effect: Allow 103 | Action: 104 | - "s3:GetObject" 105 | - "s3:PutObject" 106 | Resource: 107 | - "Fn::Join": ["", ["arn:aws:s3:::", {"Ref": "ArtifactStoreBucket"}, "/*" ]] 108 | - Effect: "Allow" 109 | Action: 110 | - "s3:ListBucket" 111 | Resource: 112 | - "Fn::Join": ["", ["arn:aws:s3:::", {"Ref": "ArtifactStoreBucket"}]] 113 | - Effect: Allow 114 | Action: 115 | - 'cloudformation:CreateStack' 116 | - 'cloudformation:DescribeStacks' 117 | - 'cloudformation:DeleteStack' 118 | - 'cloudformation:UpdateStack' 119 | - 'cloudformation:CreateChangeSet' 120 | - 'cloudformation:ExecuteChangeSet' 121 | - 'cloudformation:DeleteChangeSet' 122 | - 'cloudformation:DescribeChangeSet' 123 | - 'cloudformation:SetStackPolicy' 124 | Resource: 125 | - "Fn::Join": ["", ["arn:aws:cloudformation:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":stack/", "BlockProductionDemoResources"]] 126 | - "Fn::Join": ["", ["arn:aws:cloudformation:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":stack/", "BlockProductionDemoResources/*"]] 127 | - Effect: Allow 128 | Action: 129 | - 'iam:PassRole' 130 | Resource: 131 | - !GetAtt [CloudFormationRole, Arn] 132 | - Effect: Allow 133 | Action: 134 | - "codecommit:GetBranch" 135 | - "codecommit:GetCommit" 136 | - "codecommit:UploadArchive" 137 | - "codecommit:GetUploadArchiveStatus" 138 | - "codecommit:CancelUploadArchive" 139 | Resource: 140 | - "Fn::Join": ["", ["arn:aws:codecommit:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":", {"Ref" : "DemoResourcesCodeCommitRepo"}]] -------------------------------------------------------------------------------- /process-canary-approval.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | var aws = require('aws-sdk'); 16 | var codepipeline = new aws.CodePipeline(); 17 | var ddb = new aws.DynamoDB.DocumentClient(); 18 | var cloudwatch = new aws.CloudWatch(); 19 | 20 | const PIPELINE_APPROVAL_DDB_TABLE = "BlockProductionDemo-PipelineApprovals"; 21 | const APPROVED = "Approved"; 22 | const REJECTED = "Rejected"; 23 | const WAIT = "Wait"; 24 | 25 | exports.handler = (event, context, callback) => { 26 | 27 | var approvalResult = function(result, reason) { 28 | return { 29 | status : result, 30 | summary : reason 31 | } 32 | }; 33 | 34 | var cleanUpFinishedApproval = function(approvalToken) { 35 | return ddb.delete({ 36 | TableName: PIPELINE_APPROVAL_DDB_TABLE, 37 | Key: { 38 | ApprovalToken: approvalToken 39 | } 40 | }).promise(); 41 | }; 42 | 43 | var completeApproval = function(approval, resultDetails) { 44 | 45 | var approvalResult = { 46 | pipelineName: approval.pipelineName, 47 | stageName: approval.stageName, 48 | actionName: approval.actionName, 49 | token: approval.token, 50 | result: resultDetails 51 | }; 52 | 53 | console.log("Completing canary approval for approval token: " + approvalResult.token); 54 | 55 | return new Promise(function(resolve, reject) { 56 | codepipeline.putApprovalResult(approvalResult, function(err, data) { 57 | cleanUpFinishedApproval(approvalResult.token).then(function() { 58 | if (err) { 59 | console.log("Error putting approval result: " + JSON.stringify(err)); 60 | reject(err); 61 | } else { 62 | resolve(data); 63 | } 64 | }, function(err) { 65 | console.log("Error deleting the record: " + JSON.stringify(err)); 66 | reject(err); 67 | }); 68 | }); 69 | }); 70 | }; 71 | 72 | var hasCanaryApprovalTimedOut = function (canaryApprovalConfig, startTime) { 73 | var expiry = new Date(startTime + canaryApprovalConfig.timeoutMinutes*60000); 74 | return new Date() > expiry; 75 | }; 76 | 77 | var areSyntheticTestsInAlarm = function() { 78 | return cloudwatch.describeAlarms({ 79 | AlarmNames : [process.env.SYNTHETIC_TESTS_ALARM] 80 | }).promise().then(function (data) { 81 | var metricAlarm = data.MetricAlarms[0]; 82 | if(metricAlarm.StateValue === 'OK') { 83 | return Promise.resolve(true); 84 | } else { 85 | return Promise.resolve(false); 86 | } 87 | }).catch(function (err) { 88 | console.log("Error getting alarm status. Error: " + JSON.stringify(err)); 89 | return Promise.reject(err); 90 | }); 91 | }; 92 | 93 | var evaluateMetrics = function (canaryApprovalConfig, startTime) { 94 | var now = new Date(); 95 | var start = new Date(startTime); 96 | var period = Math.round((now - start) / 1000); 97 | period = period - (period % 60); // Round down to nearest multiple of 60 98 | 99 | var params = { 100 | EndTime: now.toISOString(), 101 | StartTime: start.toISOString(), 102 | Statistics: ['Sum'], 103 | Namespace: process.env.METRIC_NAMESPACE, 104 | MetricName: process.env.METRIC_NAME, 105 | Period: period, 106 | Dimensions: [ 107 | { 108 | Name: process.env.DIMENSION_NAME, 109 | Value: process.env.DIMENSION_VALUE 110 | } 111 | ] 112 | }; 113 | 114 | return cloudwatch.getMetricStatistics(params).promise() 115 | .then(function (data) { 116 | var totalMetrics = data.Datapoints.reduce(function (a, b) { 117 | return a + b.Sum; 118 | }, 0); 119 | 120 | if (totalMetrics >= canaryApprovalConfig.metricsRequired) { 121 | console.log("Got required " + canaryApprovalConfig.metricsRequired + " metrics."); 122 | return Promise.resolve(approvalResult(APPROVED, "Got required " + canaryApprovalConfig.metricsRequired + " metrics.")); 123 | } else { 124 | console.log("Got only " + totalMetrics + " metrics. Required: " + canaryApprovalConfig.metricsRequired + ". Waiting for more metrics..."); 125 | return Promise.resolve(WAIT); 126 | } 127 | }).catch(function (err) { 128 | console.log("Error getting metrics data. Error: " + JSON.stringify(err)); 129 | return Promise.reject(); 130 | }); 131 | }; 132 | 133 | var evaluateCanaryPerformance = function(record) { 134 | var approvalToken = record.ApprovalToken; 135 | var canaryApprovalConfig = JSON.parse(record.ApprovalContent.customData); 136 | 137 | if(!canaryApprovalConfig || !canaryApprovalConfig.metricsRequired || !canaryApprovalConfig.timeoutMinutes) { 138 | return Promise.resolve(approvalResult(REJECTED, "No canary approval configuration found.")); 139 | } 140 | 141 | console.log("Evaluating canary configuration: " + JSON.stringify(canaryApprovalConfig) + " for approval token: " + approvalToken); 142 | 143 | if(hasCanaryApprovalTimedOut(canaryApprovalConfig, record.StartTime)) { 144 | return Promise.resolve(approvalResult(REJECTED, "Did not receive required number of metrics within the configured timeout.")); 145 | } else { 146 | return areSyntheticTestsInAlarm() 147 | .then(function(isAlarmOk) { 148 | if(isAlarmOk) { 149 | return evaluateMetrics(canaryApprovalConfig, record.StartTime); 150 | } else { 151 | return Promise.resolve(approvalResult(REJECTED, "Alarm: " + process.env.SYNTHETIC_TESTS_ALARM + " was found in non OK state.")); 152 | } 153 | }).catch(function (err) { 154 | console.log("Error evaluating canary performance. Error: " + JSON.stringify(err)); 155 | return Promise.reject(err); 156 | }); 157 | } 158 | }; 159 | 160 | var processRecord = function(record) { 161 | return evaluateCanaryPerformance(record) 162 | .then(function (result) { 163 | if(result === WAIT) { 164 | return Promise.resolve(); 165 | } else { 166 | return completeApproval(record.ApprovalContent, result); 167 | } 168 | }).catch(function(err) { 169 | console.log("Error processing canary approval record. Error: " + JSON.stringify(err)); 170 | return Promise.reject(err); 171 | }); 172 | }; 173 | 174 | var processRecords = function(data) { 175 | var processRecordPromises = []; 176 | 177 | data.Items.forEach(function(record) { 178 | processRecordPromises.push(processRecord(record)); 179 | }); 180 | 181 | return new Promise(function(resolve, reject) { 182 | Promise.all(processRecordPromises) 183 | .then(function() { 184 | // continue scanning if we have more records 185 | if (typeof data.LastEvaluatedKey != "undefined") { 186 | console.log("Scanning for more..."); 187 | resolve(scanAndProcessRecords(data.LastEvaluatedKey)); 188 | } else { 189 | resolve(); 190 | } 191 | }).catch(function(err) { 192 | reject(err); 193 | }); 194 | }); 195 | }; 196 | 197 | var scanAndProcessRecords = function (lastEvaluatedKey) { 198 | return ddb.scan({ 199 | TableName: PIPELINE_APPROVAL_DDB_TABLE, 200 | ExclusiveStartKey: lastEvaluatedKey 201 | }).promise() 202 | .then(function(data) { 203 | return processRecords(data); 204 | }).catch(function(err) { 205 | console.log("Error processing canary approval requests. Error: " + JSON.stringify(err)); 206 | return Promise.reject(err); 207 | }); 208 | } 209 | 210 | scanAndProcessRecords() 211 | .then(function() { 212 | callback(null, "Successfully processed all canary approval requests."); 213 | }).catch(function(err) { 214 | console.log("Error processing canary approval requests. Error: " + JSON.stringify(err)); 215 | callback(err, "Error processing canary approval requests."); 216 | }); 217 | }; 218 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /block-production-demo-resources.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "AWS CloudFormation template which contains the resources for block production demo." 3 | 4 | Parameters: 5 | KeyName: 6 | Description: "Name of an existing EC2 KeyPair for SSH access to the instances." 7 | Type: "AWS::EC2::KeyPair::KeyName" 8 | AppName: 9 | Type: String 10 | Description: "Name of the application." 11 | MinLength: 2 12 | MaxLength: 30 13 | Default: "BlockProduction" 14 | ConstraintDescription: "Must be between 2 and 30 characters long, lowercase and may contain alphanumeric characters, hyphens (-), and dots (.), but must start with alphanumeric." 15 | DemoResourcesCodeCommitRepo: 16 | Type: String 17 | Default: "aws-codepipeline-block-production" 18 | Description: "Name of AWS CodeCommit repository that your application is in." 19 | DemoResourcesCodeCommitRepoBranch: 20 | Type: String 21 | Default: master 22 | Description: "AWS CodeCommit repository branch" 23 | DemoResourcesS3BucketName: 24 | Type: String 25 | Description: "Amazon S3 bucket where demo resources zip file was uploaded to." 26 | DemoResourcesS3ObjectKey: 27 | Type: String 28 | Description: "Amazon S3 object key with which demo resources zip file was uploaded to." 29 | CanaryApprovalConfiguration: 30 | Type: String 31 | Description: "Canary approval configuration in JSON format." 32 | YourIP: 33 | Description: "IP address to connect to SSH from. Check http://checkip.amazonaws.com/ to find yours." 34 | Type: String 35 | Default: "0.0.0.0/0" 36 | 37 | Mappings: 38 | VPCIpSpace: 39 | us-east-1: 40 | RANGE: "10.42" 41 | us-west-2: 42 | RANGE: "10.43" 43 | SubnetTypeIpRanges: 44 | public: 45 | RANGE: "0.0/17" 46 | publicSubnetConfig: 47 | publicSubnet01: 48 | CIDR: "10.0/24" 49 | AWSRegionVirt2HVMAMI: 50 | us-east-1: 51 | HVM: "ami-8fcee4e5" 52 | us-west-2: 53 | HVM: "ami-63b25203" 54 | Constants: 55 | CloudWatchMetrics: 56 | Namespace: BlockProdDemo-SyntheticTestMetrics 57 | Name: Failure 58 | 59 | Resources: 60 | 61 | MyVPC: 62 | Type: "AWS::EC2::VPC" 63 | Properties: 64 | CidrBlock: 65 | "Fn::Join": [ "", [ { "Fn::FindInMap": [ "VPCIpSpace", { "Ref": "AWS::Region" }, "RANGE" ] }, ".", "0.0/16" ] ] 66 | EnableDnsSupport: true 67 | EnableDnsHostnames: true 68 | publicSubnet01: 69 | Type: "AWS::EC2::Subnet" 70 | Properties: 71 | VpcId: 72 | Ref: MyVPC 73 | CidrBlock: 74 | "Fn::Join": [ "", [ { "Fn::FindInMap": [ "VPCIpSpace", { "Ref": "AWS::Region" }, "RANGE" ] }, ".", { "Fn::FindInMap": [ "publicSubnetConfig", "publicSubnet01", "CIDR" ] } ] ] 75 | AvailabilityZone: 76 | "Fn::Select": [ "0", { "Fn::GetAZs": { "Ref": "AWS::Region" } } ] 77 | 78 | InternetGateway: 79 | Type: "AWS::EC2::InternetGateway" 80 | 81 | AttachGateway: 82 | Type: "AWS::EC2::VPCGatewayAttachment" 83 | Properties: 84 | VpcId: 85 | Ref: MyVPC 86 | InternetGatewayId: 87 | Ref: InternetGateway 88 | 89 | PublicRouteTable: 90 | Type: "AWS::EC2::RouteTable" 91 | Properties: 92 | VpcId: 93 | Ref: MyVPC 94 | 95 | PublicRoute: 96 | Type: "AWS::EC2::Route" 97 | Properties: 98 | RouteTableId: 99 | Ref: PublicRouteTable 100 | DestinationCidrBlock: "0.0.0.0/0" 101 | GatewayId: 102 | Ref: InternetGateway 103 | 104 | PublicSubnetRTAssociation01: 105 | Type: "AWS::EC2::SubnetRouteTableAssociation" 106 | Properties: 107 | SubnetId: 108 | Ref: publicSubnet01 109 | RouteTableId: 110 | Ref: PublicRouteTable 111 | 112 | PublicNetworkAcl: 113 | Type: "AWS::EC2::NetworkAcl" 114 | Properties: 115 | VpcId: 116 | Ref: MyVPC 117 | 118 | InboundPublicNAclEntry: 119 | Type: "AWS::EC2::NetworkAclEntry" 120 | Properties: 121 | NetworkAclId: 122 | Ref: PublicNetworkAcl 123 | RuleNumber: 2000 124 | Protocol: "-1" 125 | RuleAction: allow 126 | Egress: false 127 | CidrBlock: "0.0.0.0/0" 128 | PortRange: 129 | From: 0 130 | To: 65535 131 | 132 | OutboundPublicNetworkAclEntry: 133 | Type: "AWS::EC2::NetworkAclEntry" 134 | Properties: 135 | NetworkAclId: 136 | Ref: PublicNetworkAcl 137 | RuleNumber: 2000 138 | Protocol: "-1" 139 | RuleAction: allow 140 | Egress: true 141 | CidrBlock: "0.0.0.0/0" 142 | PortRange: 143 | From: 0 144 | To: 65535 145 | 146 | publicSubnetNetworkAclAssociation01: 147 | Type: "AWS::EC2::SubnetNetworkAclAssociation" 148 | Properties: 149 | SubnetId: 150 | Ref: publicSubnet01 151 | NetworkAclId: 152 | Ref: PublicNetworkAcl 153 | 154 | ArtifactStoreBucket: 155 | Type: "AWS::S3::Bucket" 156 | 157 | WebAppRole: 158 | Type: "AWS::IAM::Role" 159 | Properties: 160 | AssumeRolePolicyDocument: 161 | Statement: 162 | - Effect: Allow 163 | Principal: 164 | Service: "ec2.amazonaws.com" 165 | Action: "sts:AssumeRole" 166 | ManagedPolicyArns: 167 | - "arn:aws:iam::aws:policy/AWSCodeDeployReadOnlyAccess" 168 | - "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 169 | Path: "/" 170 | 171 | WebAppRolePolicies: 172 | Type: "AWS::IAM::Policy" 173 | Properties: 174 | PolicyName: BackendRole 175 | PolicyDocument: 176 | Statement: 177 | - Effect: Allow 178 | Action: 179 | - "cloudwatch:PutMetricData" 180 | Resource: "*" 181 | - Effect: Allow 182 | Action: 183 | - "s3:GetObject" 184 | Resource: 185 | - "Fn::Join": ["", ["arn:aws:s3:::", {"Ref": "ArtifactStoreBucket"}, "/*" ]] 186 | - "Fn::Join": ["", ["arn:aws:s3:::aws-codedeploy-", {"Ref": "AWS::Region"}, "/*" ]] 187 | Roles: 188 | - Ref: WebAppRole 189 | 190 | WebAppInstanceProfile: 191 | Type: "AWS::IAM::InstanceProfile" 192 | Properties: 193 | Path: "/" 194 | Roles: 195 | - Ref: WebAppRole 196 | 197 | WebAppSG: 198 | Type: "AWS::EC2::SecurityGroup" 199 | Properties: 200 | GroupDescription: "Enable HTTP access on port 80" 201 | SecurityGroupIngress: 202 | - IpProtocol: tcp 203 | FromPort: 22 204 | ToPort: 22 205 | CidrIp: !Ref YourIP 206 | - IpProtocol: tcp 207 | FromPort: 80 208 | ToPort: 80 209 | CidrIp: "0.0.0.0/0" 210 | 211 | LaunchConfiguration: 212 | Type: "AWS::AutoScaling::LaunchConfiguration" 213 | Properties: 214 | IamInstanceProfile: 215 | Ref: WebAppInstanceProfile 216 | ImageId: 217 | "Fn::FindInMap": [ "AWSRegionVirt2HVMAMI", { "Ref": "AWS::Region" }, "HVM" ] 218 | InstanceType: "t2.micro" 219 | KeyName: 220 | Ref: KeyName 221 | SecurityGroups: 222 | - Ref: WebAppSG 223 | UserData: 224 | "Fn::Base64": 225 | "Fn::Join": 226 | - "" 227 | - [ 228 | "#!/bin/bash -xe\n", 229 | 230 | "/usr/bin/yum -y update\n", 231 | "/usr/bin/yum install -y ruby\n", 232 | "/usr/bin/yum install -y aws-cli\n", 233 | "cd /home/ec2-user/\n", 234 | "/usr/bin/aws s3 cp s3://aws-codedeploy-", { "Ref" : "AWS::Region" }, "/latest/install . --region ", { "Ref" : "AWS::Region" }, "\n", 235 | "/bin/chmod +x ./install\n", 236 | "./install auto\n" 237 | ] 238 | 239 | CanaryAutoScalingGroup: 240 | Type: "AWS::AutoScaling::AutoScalingGroup" 241 | Properties: 242 | LoadBalancerNames: 243 | - Ref: ElasticLoadBalancer 244 | AvailabilityZones: 245 | "Fn::GetAZs": { "Ref": "AWS::Region" } 246 | DesiredCapacity: 1 247 | LaunchConfigurationName: 248 | Ref: LaunchConfiguration 249 | MinSize: 1 250 | MaxSize: 3 251 | 252 | ProdAutoScalingGroup: 253 | Type: "AWS::AutoScaling::AutoScalingGroup" 254 | Properties: 255 | LoadBalancerNames: 256 | - Ref: ElasticLoadBalancer 257 | AvailabilityZones: 258 | "Fn::GetAZs": { "Ref": "AWS::Region" } 259 | DesiredCapacity: 1 260 | LaunchConfigurationName: 261 | Ref: LaunchConfiguration 262 | MinSize: 1 263 | MaxSize: 3 264 | 265 | ElasticLoadBalancer: 266 | Type: "AWS::ElasticLoadBalancing::LoadBalancer" 267 | Properties: 268 | AvailabilityZones: 269 | "Fn::GetAZs": { "Ref": "AWS::Region" } 270 | Listeners: 271 | - LoadBalancerPort: 80 272 | InstancePort: 80 273 | Protocol: HTTP 274 | SecurityGroups: 275 | - {"Fn::GetAtt" : ["WebAppSG", "GroupId"]} 276 | 277 | JenkinsRole: 278 | Type: "AWS::IAM::Role" 279 | Properties: 280 | AssumeRolePolicyDocument: 281 | Statement: 282 | - Effect: Allow 283 | Principal: 284 | Service: "ec2.amazonaws.com" 285 | Action: "sts:AssumeRole" 286 | Path: "/" 287 | 288 | JenkinsRolePolicies: 289 | Type: "AWS::IAM::Policy" 290 | Properties: 291 | PolicyName: JenkinsRole 292 | PolicyDocument: 293 | Statement: 294 | - Effect: Allow 295 | Action: 296 | - "codepipeline:AcknowledgeJob" 297 | - "codepipeline:GetJobDetails" 298 | - "codepipeline:PollForJobs" 299 | - "codepipeline:PutJobFailureResult" 300 | - "codepipeline:PutJobSuccessResult" 301 | Resource: "*" 302 | Roles: 303 | - Ref: JenkinsRole 304 | 305 | JenkinsInstanceProfile: 306 | Type: "AWS::IAM::InstanceProfile" 307 | Properties: 308 | Path: "/" 309 | Roles: 310 | - Ref: JenkinsRole 311 | 312 | JenkinsSG: 313 | Type: "AWS::EC2::SecurityGroup" 314 | Properties: 315 | GroupDescription: "Enable HTTP access on port 80" 316 | VpcId: 317 | Ref: MyVPC 318 | SecurityGroupIngress: 319 | - IpProtocol: tcp 320 | FromPort: 22 321 | ToPort: 22 322 | CidrIp: 323 | Ref: YourIP 324 | - IpProtocol: tcp 325 | FromPort: 80 326 | ToPort: 80 327 | CidrIp: 328 | Ref: YourIP 329 | SecurityGroupEgress: 330 | - IpProtocol: tcp 331 | FromPort: 80 332 | ToPort: 80 333 | CidrIp: "0.0.0.0/0" 334 | - IpProtocol: tcp 335 | FromPort: 443 336 | ToPort: 443 337 | CidrIp: "0.0.0.0/0" 338 | - IpProtocol: udp 339 | FromPort: 123 340 | ToPort: 123 341 | CidrIp: "0.0.0.0/0" 342 | - IpProtocol: udp 343 | FromPort: 9418 344 | ToPort: 9418 345 | CidrIp: "0.0.0.0/0" 346 | - IpProtocol: icmp 347 | FromPort: "-1" 348 | ToPort: "-1" 349 | CidrIp: "0.0.0.0/0" 350 | 351 | JenkinsServer: 352 | Type: "AWS::EC2::Instance" 353 | Metadata: 354 | "AWS::CloudFormation::Init": 355 | config: 356 | packages: 357 | yum: 358 | "java-1.7.0-openjdk": [] 359 | "java-1.7.0-openjdk-devel": [] 360 | files: 361 | "/tmp/config.xml": 362 | source: "https://s3.amazonaws.com/aws-codedeploy-samples-us-east-1/templates/latest/Jenkins_Helper_Scripts/config.xml" 363 | mode: 644 364 | "/tmp/hudson.tasks.Maven.xml": 365 | source: "https://s3.amazonaws.com/aws-codedeploy-samples-us-east-1/templates/latest/Jenkins_Helper_Scripts/hudson.tasks.Maven.xml" 366 | mode: 644 367 | "/tmp/jenkins.mvn.GlobalMavenConfig.xml": 368 | source: "https://s3.amazonaws.com/aws-codedeploy-samples-us-east-1/templates/latest/Jenkins_Helper_Scripts/jenkins.mvn.GlobalMavenConfig.xml" 369 | mode: 644 370 | Properties: 371 | KeyName: 372 | Ref: KeyName 373 | ImageId: 374 | "Fn::FindInMap": [ "AWSRegionVirt2HVMAMI", { "Ref": "AWS::Region" }, "HVM" ] 375 | NetworkInterfaces: 376 | - SubnetId: 377 | Ref: publicSubnet01 378 | GroupSet: 379 | - Ref: JenkinsSG 380 | AssociatePublicIpAddress: true 381 | DeviceIndex: 0 382 | InstanceType: "t2.large" 383 | IamInstanceProfile: 384 | Ref: JenkinsInstanceProfile 385 | UserData: 386 | "Fn::Base64": { 387 | "Fn::Join": [ 388 | "", 389 | [ 390 | "#!/bin/bash -ex\n", 391 | "/usr/bin/yum update -y aws-cfn-bootstrap\n", 392 | "# Update the AWS CLI to the latest version\n", 393 | "/usr/bin/yum install -y aws-cli\n", 394 | "function error_exit\n", 395 | "{\n", 396 | " /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '", 397 | { 398 | "Ref": "JenkinsHostWaitHandle" 399 | }, 400 | "'\n", 401 | " exit 1\n", 402 | "}\n", 403 | "/opt/aws/bin/cfn-init -v -s ", 404 | { 405 | "Ref": "AWS::StackName" 406 | }, 407 | " -r JenkinsServer --region ", 408 | { 409 | "Ref": "AWS::Region" 410 | }, 411 | "\n", 412 | "# Install Maven\n", 413 | "cd /tmp/\n", 414 | "MAVEN_VERSION=3.3.9\n", 415 | "/usr/bin/wget https://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz\n", 416 | "/bin/tar xzvf apache-maven-$MAVEN_VERSION-bin.tar.gz -C /opt/\n", 417 | "/bin/ln -s /opt/apache-maven-$MAVEN_VERSION /opt/apache-maven\n", 418 | "/bin/rm /tmp/apache-maven-$MAVEN_VERSION-bin.tar.gz\n", 419 | "# Install Jenkins\n", 420 | "/usr/bin/wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo\n", 421 | "/bin/rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key\n", 422 | "/usr/bin/yum install -y http://pkg.jenkins-ci.org/redhat/jenkins-1.658-1.1.noarch.rpm\n", 423 | "/sbin/service jenkins start\n", 424 | "/sbin/chkconfig jenkins on\n", 425 | "# Wait 30 seconds to allow Jenkins to startup\n", 426 | "/bin/echo \"Waiting 30 seconds for Jenkins to start.....\"\n", 427 | "/bin/sleep 30\n", 428 | "# Install the required plugins\n", 429 | "cd /var/lib/jenkins/plugins\n", 430 | "/usr/bin/curl -O -L https://updates.jenkins-ci.org/latest/aws-codepipeline.hpi\n", 431 | "/bin/chown jenkins:jenkins *.hpi\n", 432 | "/bin/mv /tmp/hudson.tasks.Maven.xml /var/lib/jenkins/\n", 433 | "/bin/mv /tmp/jenkins.mvn.GlobalMavenConfig.xml /var/lib/jenkins/\n", 434 | "/bin/chown jenkins:jenkins /var/lib/jenkins/*.xml\n", 435 | "# Restarting Jenkins\n", 436 | "/sbin/service jenkins restart\n", 437 | "/bin/echo \"Waiting 30 seconds for Jenkins to start.....\"\n", 438 | "/bin/sleep 30\n", 439 | "# configure our job\n", 440 | "/bin/sed -i \"s/APPNAME/", 441 | { 442 | "Ref": "AppName" 443 | }, 444 | "/g\" /tmp/config.xml\n", 445 | "/bin/sed -i \"s/REGION/", 446 | { 447 | "Ref": "AWS::Region" 448 | }, 449 | "/g\" /tmp/config.xml\n", 450 | "/usr/bin/java -jar /var/cache/jenkins/war/WEB-INF/jenkins-cli.jar -s http://localhost:8080 create-job ", 451 | { 452 | "Ref": "AppName" 453 | }, 454 | "< /tmp/config.xml\n", 455 | "/bin/rm /tmp/config.xml\n", 456 | "# Set up port forwarding\n", 457 | "/sbin/iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080\n", 458 | "/sbin/iptables-save > /etc/sysconfig/iptables\n", 459 | "# If all went well, signal success\n", 460 | "/opt/aws/bin/cfn-signal -e $? -r 'Instance configuration complete' '", 461 | { 462 | "Ref": "JenkinsHostWaitHandle" 463 | }, 464 | "'\n", 465 | "\n" 466 | ] 467 | ] 468 | } 469 | 470 | JenkinsHostWaitHandle: 471 | Type: "AWS::CloudFormation::WaitConditionHandle" 472 | 473 | JenkinsHostWaitCondition: 474 | Type: "AWS::CloudFormation::WaitCondition" 475 | DependsOn: JenkinsServer 476 | Properties: 477 | Handle: 478 | Ref: JenkinsHostWaitHandle 479 | Timeout: 600 480 | 481 | CodeDeployTrustRole: 482 | Type: "AWS::IAM::Role" 483 | Properties: 484 | AssumeRolePolicyDocument: 485 | Statement: 486 | - Sid: "" 487 | Effect: "Allow" 488 | Principal: 489 | Service: 490 | - "codedeploy.amazonaws.com" 491 | Action: "sts:AssumeRole" 492 | ManagedPolicyArns: 493 | - "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole" 494 | Path: "/" 495 | 496 | CodeDeployApp: 497 | Type: "AWS::CodeDeploy::Application" 498 | Properties: 499 | ApplicationName: 500 | Ref: AppName 501 | 502 | CanaryDeploymentGroup: 503 | DependsOn: CodeDeployTrustRole 504 | Type: "AWS::CodeDeploy::DeploymentGroup" 505 | Properties: 506 | ApplicationName: 507 | Ref: CodeDeployApp 508 | DeploymentGroupName: 509 | "Fn::Join": [ "", [ { "Ref": "AppName" }, "-CanaryEnv" ] ] 510 | DeploymentConfigName: "CodeDeployDefault.OneAtATime" 511 | ServiceRoleArn: 512 | "Fn::GetAtt": [ "CodeDeployTrustRole", "Arn" ] 513 | AutoScalingGroups: 514 | - Ref: CanaryAutoScalingGroup 515 | 516 | ProdDeploymentGroup: 517 | DependsOn: CodeDeployTrustRole 518 | Type: "AWS::CodeDeploy::DeploymentGroup" 519 | Properties: 520 | ApplicationName: 521 | Ref: CodeDeployApp 522 | DeploymentGroupName: 523 | "Fn::Join": [ "", [ { "Ref": "AppName" }, "-ProdEnv" ] ] 524 | DeploymentConfigName: "CodeDeployDefault.OneAtATime" 525 | ServiceRoleArn: 526 | "Fn::GetAtt": [ "CodeDeployTrustRole", "Arn" ] 527 | AutoScalingGroups: 528 | - Ref: ProdAutoScalingGroup 529 | 530 | CodePipelineTrustRole: 531 | Type: "AWS::IAM::Role" 532 | Properties: 533 | AssumeRolePolicyDocument: 534 | Statement: 535 | - Effect: Allow 536 | Principal: 537 | Service: 538 | - "codepipeline.amazonaws.com" 539 | Action: "sts:AssumeRole" 540 | Path: "/" 541 | 542 | CodePipelineRolePolicies: 543 | Type: "AWS::IAM::Policy" 544 | Properties: 545 | PolicyName: CodePipelinePolicy 546 | PolicyDocument: 547 | Statement: 548 | - Effect: Allow 549 | Action: 550 | - "codecommit:GetBranch" 551 | - "codecommit:GetCommit" 552 | - "codecommit:UploadArchive" 553 | - "codecommit:GetUploadArchiveStatus" 554 | - "codecommit:CancelUploadArchive" 555 | Resource: "*" 556 | - Effect: Allow 557 | Action: 558 | - "s3:GetObject" 559 | - "s3:PutObject" 560 | Resource: 561 | - "Fn::Join": ["", ["arn:aws:s3:::", {"Ref": "ArtifactStoreBucket"}, "/*" ]] 562 | - Effect: "Allow" 563 | Action: 564 | - "s3:ListBucket" 565 | Resource: 566 | - "Fn::Join": ["", ["arn:aws:s3:::", {"Ref": "ArtifactStoreBucket"}]] 567 | - Effect: Allow 568 | Action: 569 | - "codedeploy:CreateDeployment" 570 | - "codedeploy:GetApplicationRevision" 571 | - "codedeploy:GetDeployment" 572 | - "codedeploy:GetDeploymentConfig" 573 | - "codedeploy:RegisterApplicationRevision" 574 | Resource: "*" 575 | - Effect: Allow 576 | Action: 577 | - "sns:Publish" 578 | Resource: !Ref PipelineApprovalSNSNotification 579 | Roles: 580 | - Ref: CodePipelineTrustRole 581 | 582 | CustomJenkinsActionType: 583 | Type: "AWS::CodePipeline::CustomActionType" 584 | DependsOn: JenkinsHostWaitCondition 585 | Properties: 586 | Category: Build 587 | Provider: 588 | "Fn::Join": [ "", [ { "Ref": "AppName" }, "-Jenkins" ] ] 589 | Version: 1 590 | ConfigurationProperties: 591 | - Key: true 592 | Name: ProjectName 593 | Queryable: true 594 | Required: true 595 | Secret: false 596 | Type: String 597 | InputArtifactDetails: 598 | MaximumCount: 5 599 | MinimumCount: 0 600 | OutputArtifactDetails: 601 | MaximumCount: 5 602 | MinimumCount: 0 603 | Settings: 604 | EntityUrlTemplate: 605 | "Fn::Join": [ "", [ "http://", { "Fn::GetAtt": [ "JenkinsServer", "PublicIp" ] }, "/job/{Config:ProjectName}" ]] 606 | ExecutionUrlTemplate: 607 | "Fn::Join": [ "", [ "http://", { "Fn::GetAtt": [ "JenkinsServer", "PublicIp" ] }, "/job/{Config:ProjectName}/{ExternalExecutionId}" ]] 608 | 609 | Pipeline: 610 | Type: "AWS::CodePipeline::Pipeline" 611 | DependsOn: CustomJenkinsActionType 612 | Properties: 613 | Name: 614 | "Fn::Join": [ "", [ { "Ref": "AppName" }, "-Pipeline" ] ] 615 | RoleArn: 616 | "Fn::GetAtt": [ "CodePipelineTrustRole", "Arn" ] 617 | ArtifactStore: 618 | Type: S3 619 | Location: 620 | Ref: ArtifactStoreBucket 621 | Stages: 622 | - Name: Source 623 | Actions: 624 | - Name: DemoResourcesSource 625 | ActionTypeId: 626 | Version: 1 627 | Category: Source 628 | Owner: AWS 629 | Provider: CodeCommit 630 | OutputArtifacts: 631 | - Name: DemoSourceArtifacts 632 | Configuration: 633 | RepositoryName: 634 | Ref: DemoResourcesCodeCommitRepo 635 | BranchName: 636 | Ref: DemoResourcesCodeCommitRepoBranch 637 | RunOrder: 1 638 | - Name: Build 639 | Actions: 640 | - Name: ApplicationBuild 641 | InputArtifacts: 642 | - Name: DemoSourceArtifacts 643 | ActionTypeId: 644 | Category: Build 645 | Owner: Custom 646 | Version: 1 647 | Provider: {"Fn::Join": ["", [{"Ref": "AppName"}, "-Jenkins"]]} 648 | OutputArtifacts: 649 | - Name: DemoBuiltArtifacts 650 | Configuration: 651 | ProjectName: 652 | Ref: AppName 653 | RunOrder: 1 654 | - Name: Deploy 655 | Actions: 656 | - Name: CanaryDeploy 657 | InputArtifacts: 658 | - Name: DemoBuiltArtifacts 659 | ActionTypeId: 660 | Category: Deploy 661 | Owner: AWS 662 | Version: 1 663 | Provider: CodeDeploy 664 | Configuration: 665 | ApplicationName: 666 | Ref: CodeDeployApp 667 | DeploymentGroupName: 668 | Ref: CanaryDeploymentGroup 669 | RunOrder: 1 670 | - Name: CanaryApproval 671 | ActionTypeId: 672 | Category: Approval 673 | Owner: AWS 674 | Version: 1 675 | Provider: Manual 676 | Configuration: 677 | NotificationArn: 678 | Ref: PipelineApprovalSNSNotification 679 | CustomData: 680 | Ref: CanaryApprovalConfiguration 681 | RunOrder: 2 682 | - Name: ProdDeploy 683 | InputArtifacts: 684 | - Name: DemoBuiltArtifacts 685 | ActionTypeId: 686 | Category: Deploy 687 | Owner: AWS 688 | Version: 1 689 | Provider: CodeDeploy 690 | Configuration: 691 | ApplicationName: 692 | Ref: CodeDeployApp 693 | DeploymentGroupName: 694 | Ref: ProdDeploymentGroup 695 | RunOrder: 3 696 | 697 | PipelineAppovalTable: 698 | Type: "AWS::DynamoDB::Table" 699 | Properties: 700 | AttributeDefinitions: 701 | - AttributeName: ApprovalToken 702 | AttributeType: S 703 | KeySchema: 704 | - AttributeName: ApprovalToken 705 | KeyType: HASH 706 | ProvisionedThroughput: 707 | ReadCapacityUnits: 5 708 | WriteCapacityUnits: 5 709 | TableName: "BlockProductionDemo-PipelineApprovals" 710 | 711 | RegisterCanaryApprovalLambdaFunction: 712 | Type: "AWS::Lambda::Function" 713 | Properties: 714 | Handler: "register-canary-approval.handler" 715 | Role: 716 | "Fn::GetAtt": 717 | - RegisterCanaryApprovalFunctionExecutionRole 718 | - Arn 719 | Code: 720 | S3Bucket: 721 | Ref: DemoResourcesS3BucketName 722 | S3Key: 723 | Ref: DemoResourcesS3ObjectKey 724 | Runtime: "nodejs4.3" 725 | Timeout: 25 726 | 727 | RegisterCanaryApprovalFunctionExecutionRole: 728 | Type: "AWS::IAM::Role" 729 | Properties: 730 | AssumeRolePolicyDocument: 731 | Statement: 732 | - Effect: Allow 733 | Action: 734 | - "sts:AssumeRole" 735 | Principal: 736 | Service: ["lambda.amazonaws.com"] 737 | Version: "2012-10-17" 738 | Path: "/" 739 | Policies: 740 | - PolicyName: CloudWatchAccess 741 | PolicyDocument: 742 | Version: "2012-10-17" 743 | Statement: 744 | - Effect: Allow 745 | Action: 746 | - "logs:CreateLogGroup" 747 | - "logs:CreateLogStream" 748 | - "logs:PutLogEvents" 749 | Resource: "arn:aws:logs:*:*:*" 750 | - Effect: Allow 751 | Action: 752 | - "dynamodb:PutItem" 753 | Resource: 754 | - "Fn::Join": ["", ["arn:aws:dynamodb:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":table/", {"Ref": "PipelineAppovalTable"}]] 755 | 756 | RegisterCanaryApprovalInvokePermission: 757 | Type: "AWS::Lambda::Permission" 758 | Properties: 759 | FunctionName: 760 | "Fn::GetAtt": 761 | - RegisterCanaryApprovalLambdaFunction 762 | - Arn 763 | Action: "lambda:InvokeFunction" 764 | Principal: "sns.amazonaws.com" 765 | SourceArn: 766 | Ref: PipelineApprovalSNSNotification 767 | 768 | PipelineApprovalSNSNotification: 769 | Type: "AWS::SNS::Topic" 770 | Properties: 771 | Subscription: 772 | - Protocol: lambda 773 | Endpoint: 774 | "Fn::GetAtt": 775 | - RegisterCanaryApprovalLambdaFunction 776 | - Arn 777 | 778 | SyntheticTestFailureCloudWatchAlarm: 779 | DependsOn: Pipeline 780 | Type: AWS::CloudWatch::Alarm 781 | Properties: 782 | ComparisonOperator: GreaterThanOrEqualToThreshold 783 | # Set to a higher value to allow instances and load balancer to become healthy 784 | EvaluationPeriods: 10 785 | Namespace: !FindInMap [ Constants, CloudWatchMetrics, Namespace] 786 | MetricName: !FindInMap [ Constants, CloudWatchMetrics, Name] 787 | Period: 60 788 | Statistic: Sum 789 | Threshold: 1 790 | 791 | SyntheticTestFunctionExecutionRole: 792 | Type: AWS::IAM::Role 793 | Properties: 794 | AssumeRolePolicyDocument: 795 | Statement: 796 | - Action: ['sts:AssumeRole'] 797 | Effect: Allow 798 | Principal: 799 | Service: [lambda.amazonaws.com] 800 | Version: '2012-10-17' 801 | Path: / 802 | Policies: 803 | - PolicyName: CloudWatchAccess 804 | PolicyDocument: 805 | Version: '2012-10-17' 806 | Statement: 807 | - Effect: Allow 808 | Action: 809 | - "logs:CreateLogGroup" 810 | - "logs:CreateLogStream" 811 | - "logs:PutLogEvents" 812 | Resource: "arn:aws:logs:*:*:*" 813 | - Effect: Allow 814 | Action: 815 | - "cloudwatch:PutMetricData" 816 | Resource: '*' 817 | 818 | SyntheticTestRunnerLambdaFunction: 819 | Type: AWS::Lambda::Function 820 | Properties: 821 | Handler: "synthetic-test-runner.handler" 822 | Role: !GetAtt [SyntheticTestFunctionExecutionRole, Arn] 823 | Code: 824 | S3Bucket: !Ref DemoResourcesS3BucketName 825 | S3Key: !Ref DemoResourcesS3ObjectKey 826 | Runtime: "nodejs4.3" 827 | Timeout: "25" 828 | Environment: 829 | Variables: 830 | WEBSITE_URL: !GetAtt [ElasticLoadBalancer, DNSName] 831 | METRIC_NAMESPACE: !FindInMap [ Constants, CloudWatchMetrics, Namespace] 832 | METRIC_NAME: !FindInMap [ Constants, CloudWatchMetrics, Name] 833 | 834 | SyntheticTestRunnerInvokePermission: 835 | Type: AWS::Lambda::Permission 836 | Properties: 837 | FunctionName: !GetAtt [SyntheticTestRunnerLambdaFunction, Arn] 838 | Action: "lambda:InvokeFunction" 839 | Principal: "events.amazonaws.com" 840 | SourceArn: !GetAtt [SyntheticTestScheduleTrigger, Arn] 841 | 842 | SyntheticTestScheduleTrigger: 843 | DependsOn: Pipeline 844 | Type: AWS::Events::Rule 845 | Properties: 846 | ScheduleExpression: rate(1 minute) 847 | State: ENABLED 848 | Targets: 849 | - Arn: !GetAtt [SyntheticTestRunnerLambdaFunction, Arn] 850 | Id: "InvokeSyntheticTestRunner" 851 | 852 | ProcessCanaryApprovalLambdaFunction: 853 | Type: AWS::Lambda::Function 854 | Properties: 855 | Handler: "process-canary-approval.handler" 856 | Role: !GetAtt [ProcessCanaryApprovalFunctionExecutionRole, Arn] 857 | Code: 858 | S3Bucket: !Ref DemoResourcesS3BucketName 859 | S3Key: !Ref DemoResourcesS3ObjectKey 860 | Runtime: "nodejs4.3" 861 | Timeout: "25" 862 | Environment: 863 | Variables: 864 | METRIC_NAMESPACE: !Ref CodeDeployApp 865 | DIMENSION_NAME: "DeploymentGroup" 866 | DIMENSION_VALUE: !Ref CanaryDeploymentGroup 867 | METRIC_NAME: "Request" 868 | SYNTHETIC_TESTS_ALARM: !Ref SyntheticTestFailureCloudWatchAlarm 869 | 870 | ProcessCanaryApprovalFunctionExecutionRole: 871 | Type: AWS::IAM::Role 872 | Properties: 873 | AssumeRolePolicyDocument: 874 | Statement: 875 | - Action: ['sts:AssumeRole'] 876 | Effect: Allow 877 | Principal: 878 | Service: [lambda.amazonaws.com] 879 | Version: '2012-10-17' 880 | Path: / 881 | Policies: 882 | - PolicyName: CloudWatchAccess 883 | PolicyDocument: 884 | Version: '2012-10-17' 885 | Statement: 886 | - Effect: Allow 887 | Action: 888 | - "logs:CreateLogGroup" 889 | - "logs:CreateLogStream" 890 | - "logs:PutLogEvents" 891 | Resource: "arn:aws:logs:*:*:*" 892 | - Effect: Allow 893 | Action: 894 | - "codepipeline:PutApprovalResult" 895 | Resource: 896 | - "Fn::Join": ["", ["arn:aws:codepipeline:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":", {"Ref": "Pipeline"}, "/*"]] 897 | - Effect: Allow 898 | Action: 899 | - "dynamodb:Scan" 900 | - "dynamodb:DeleteItem" 901 | Resource: 902 | - "Fn::Join": ["", ["arn:aws:dynamodb:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":table/", {"Ref": "PipelineAppovalTable"}]] 903 | - Effect: Allow 904 | Action: 905 | - "cloudwatch:GetMetricStatistics" 906 | - "cloudwatch:DescribeAlarms" 907 | Resource: 908 | - "*" 909 | 910 | ProcessCanaryApprovalInvokePermission: 911 | Type: AWS::Lambda::Permission 912 | Properties: 913 | FunctionName: !GetAtt [ProcessCanaryApprovalLambdaFunction, Arn] 914 | Action: "lambda:InvokeFunction" 915 | Principal: "events.amazonaws.com" 916 | SourceArn: !GetAtt [ProcessCanaryApprovalScheduleTrigger, Arn] 917 | 918 | ProcessCanaryApprovalScheduleTrigger: 919 | Type: AWS::Events::Rule 920 | Properties: 921 | ScheduleExpression: rate(1 minute) 922 | State: ENABLED 923 | Targets: 924 | - Arn: !GetAtt [ProcessCanaryApprovalLambdaFunction, Arn] 925 | Id: "InvokeProcessCanaryApprovals" 926 | --------------------------------------------------------------------------------