├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── aws-codepipeline-integration ├── AWSResilienceHubCICDStepFunction.yaml ├── README.md └── images │ ├── AWS-CodePipeline-stage-step-function-input.png │ └── ResilienceHubCICDArchitecture.jpg ├── cop316 ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── activate_post_launch_actions.py ├── add_app_to_arh.py ├── replication_monitoring_drs.py └── start_recovery_verify_launch.py ├── create-resiliency-policy ├── README.md ├── resiliency_policy.yaml └── resiliency_policy_granular.yaml ├── create-resource-group-for-resilience-hub ├── README.md └── resource-group.yaml ├── github-actions-integration ├── README.md ├── deploy.yml └── resilience-check.py ├── inputsource-eventbridge-integration ├── README.md ├── add-tag.png ├── add-tags-s3.png ├── architecture.png ├── arh_input_source_eb_template.yaml ├── eventbridge-on.png ├── res-policies.png ├── step-functions-workflow-simple.png ├── step-functions-workflow-with-tf.png ├── step-functions-workflow-with-tf.png.bkp └── step-functions-workflow.png ├── jenkins-pipeline-integration ├── Jenkinsfile └── README.md ├── policy-bulk-update ├── README.md ├── bulk-arh-policy-update.py └── pylint.config ├── resilience-hub-application-iam-roles ├── AWSResilienceHubCrossAccountAssessmentRole.yaml ├── AWSResilienceHubPrimaryAccountAssessmentRole.yaml └── README.md ├── resilience-hub-csv-export ├── README.md ├── csv-generator.py └── sample-resilience-report.csv ├── resilience-hub-pdf-export ├── README.md └── pdf-generator.py ├── resilience-reporter ├── README.md ├── images │ └── arch.png ├── resilience-csv-generator.yaml ├── resilience-dashboard.yaml └── sample-resilience-report.csv ├── resource-group-enhancer ├── README.md └── resource-group-enhancer.py └── scheduled-assessment-role ├── AwsResilienceHubPeriodicAssessmentRole.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Resilience Hub tools 2 | 3 | Collection of solutions and tools to customize and enhance your usage of [AWS Resilience Hub](https://aws.amazon.com/resilience-hub/). 4 | 5 | ## List of assets 6 | 7 | * [create-resiliency-policy](./create-resiliency-policy) - Creates a resiliency policy within Resilience Hub using AWS CloudFormation 8 | * [create-resource-group-for-resilience-hub](./create-resource-group-for-resilience-hub) - Creates an AWS Resource Group that only contains resources supported by Resilience Hub 9 | * [aws-codepipeline-integration](./aws-codepipeline-integration) - Example of how Resilience Hub can be integrated with AWS CodePipeline for continuous resilience 10 | * [github-actions-integration](./github-actions-integration) - Example of how Resilience Hub can be integrated into Github Actions for continuous resilience 11 | * [jenkins-pipeline-integration](./jenkins-pipeline-integration) - Example of how Resilience Hub can be integrated into Jenkins Pipeline for continuous resilience 12 | * [resilience-hub-csv-export](./resilience-hub-csv-export) - Export data from Resilience Hub for all applications in a CSV file that can be used for reporting and analytics 13 | * [resilience-reporter](./resilience-reporter) - Creates an Amazon QuickSight dashboard that contains data from Resilience Hub for all applications 14 | * [resource-group-enhancer](./resource-group-enhancer) - Add resources not supported by AWS Resource Group to an application in Resilience Hub 15 | * [scheduled-assessment-role](./scheduled-assessment-role) - Creates an Identity and Access Management (IAM) role to be used by Resilience Hub for running scheduled (daily) assessments 16 | * [inputsource-eventbridge-integration](./inputsource-eventbridge-integration) - Automate resilience assessments of your AWS CloudFormation Stacks/Terraform Statefiles using AWS Resilience Hub and Amazon EventBridge 17 | * [resilience-hub-application-iam-roles](./resilience-hub-application-iam-roles) - Example of how Resilience Hub can be integrated with Eventbridge to track Cloudformation deployments for contiuous resilience 18 | 19 | 20 | ## License 21 | 22 | This library is licensed under the MIT-0 License. 23 | -------------------------------------------------------------------------------- /aws-codepipeline-integration/AWSResilienceHubCICDStepFunction.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: A sample template for a Step Functions state machine that could integrate automatic AWS Resilience Hub application assessment as part of a CodePipeline execution 3 | Resources: 4 | AppAssessmentRole: 5 | Type: AWS::IAM::Role 6 | Properties: 7 | AssumeRolePolicyDocument: 8 | Version: "2012-10-17" 9 | Statement: 10 | - Effect: Allow 11 | Principal: 12 | Service: 13 | - states.amazonaws.com 14 | Action: 15 | - 'sts:AssumeRole' 16 | Description: String 17 | ManagedPolicyArns: 18 | - arn:aws:iam::aws:policy/ReadOnlyAccess 19 | Policies: 20 | - PolicyName: ResilienceHubPolicy 21 | PolicyDocument: 22 | Version: "2012-10-17" 23 | Statement: 24 | - Effect: Allow 25 | Action: 26 | - 'resiliencehub:ImportResourcesToDraftAppVersion' 27 | - 'resiliencehub:PublishAppVersion' 28 | - 'resiliencehub:DescribeDraftAppVersionResourcesImportStatus' 29 | - 'resiliencehub:DescribeApp' 30 | - 'resiliencehub:DescribeAppAssessment' 31 | - 'resiliencehub:StartAppAssessment' 32 | Resource: '*' 33 | - PolicyName: SNSPolicy 34 | PolicyDocument: 35 | Version: "2012-10-17" 36 | Statement: 37 | - Effect: Allow 38 | Action: 39 | - 'SNS:Publish' 40 | Resource: !Ref AssessmentSNSTopic 41 | AssessmentSNSTopic: 42 | Type: AWS::SNS::Topic 43 | Properties: 44 | KmsMasterKeyId: "alias/aws/sns" 45 | AppAssessmentSFN: 46 | Type: AWS::StepFunctions::StateMachine 47 | Properties: 48 | StateMachineName: AppAssessement 49 | DefinitionSubstitutions: 50 | sns_arn: !Ref AssessmentSNSTopic 51 | DefinitionString: |- 52 | { 53 | "Comment": "A description of my state machine", 54 | "StartAt": "ImportResourcesToDraftAppVersion", 55 | "States": { 56 | "ImportResourcesToDraftAppVersion": { 57 | "Type": "Task", 58 | "Next": "WaitForImportCompletion", 59 | "Parameters": { 60 | "AppArn.$": "$.AppArn", 61 | "SourceArns.$": "States.Array($.StackArn)" 62 | }, 63 | "Resource": "arn:aws:states:::aws-sdk:resiliencehub:importResourcesToDraftAppVersion" 64 | }, 65 | "WaitForImportCompletion": { 66 | "Type": "Wait", 67 | "Seconds": 5, 68 | "Next": "DescribeDraftAppVersionResourcesImportStatus" 69 | }, 70 | "DescribeDraftAppVersionResourcesImportStatus": { 71 | "Type": "Task", 72 | "Next": "CheckResourceResoluionStatus", 73 | "Parameters": { 74 | "AppArn.$": "$.AppArn" 75 | }, 76 | "Resource": "arn:aws:states:::aws-sdk:resiliencehub:describeDraftAppVersionResourcesImportStatus" 77 | }, 78 | "CheckResourceResoluionStatus": { 79 | "Type": "Choice", 80 | "Choices": [ 81 | { 82 | "Variable": "$.Status", 83 | "StringEquals": "Success", 84 | "Next": "PublishAppVersion" 85 | }, 86 | { 87 | "Variable": "$.Status", 88 | "StringEquals": "Failed", 89 | "Next": "SNS Publish" 90 | } 91 | ], 92 | "Default": "WaitForImportCompletion" 93 | }, 94 | "PublishAppVersion": { 95 | "Type": "Task", 96 | "Next": "StartAppAssessment", 97 | "Parameters": { 98 | "AppArn.$": "$.AppArn" 99 | }, 100 | "Resource": "arn:aws:states:::aws-sdk:resiliencehub:publishAppVersion", 101 | "ResultPath": null 102 | }, 103 | "StartAppAssessment": { 104 | "Type": "Task", 105 | "Next": "WaitForAssessmentCompletion", 106 | "Parameters": { 107 | "AppArn.$": "$.AppArn", 108 | "AppVersion": "release", 109 | "AssessmentName": "Codepipeline-Assessment" 110 | }, 111 | "Resource": "arn:aws:states:::aws-sdk:resiliencehub:startAppAssessment" 112 | }, 113 | "WaitForAssessmentCompletion": { 114 | "Type": "Wait", 115 | "Seconds": 5, 116 | "Next": "DescribeAppAssessment" 117 | }, 118 | "DescribeAppAssessment": { 119 | "Type": "Task", 120 | "Parameters": { 121 | "AssessmentArn.$": "$.Assessment.AssessmentArn" 122 | }, 123 | "Resource": "arn:aws:states:::aws-sdk:resiliencehub:describeAppAssessment", 124 | "Next": "CheckAppAssessmentStatus" 125 | }, 126 | "CheckAppAssessmentStatus": { 127 | "Type": "Choice", 128 | "Choices": [ 129 | { 130 | "Or": [ 131 | { 132 | "Variable": "$.Assessment.AssessmentStatus", 133 | "StringEquals": "InProgress" 134 | }, 135 | { 136 | "Variable": "$.Assessment.AssessmentStatus", 137 | "StringEquals": "Pending" 138 | } 139 | ], 140 | "Next": "WaitForAssessmentCompletion" 141 | }, 142 | { 143 | "And": [ 144 | { 145 | "Variable": "$.Assessment.AssessmentStatus", 146 | "StringEquals": "Success" 147 | }, 148 | { 149 | "Variable": "$.Assessment.ComplianceStatus", 150 | "StringEquals": "PolicyMet" 151 | } 152 | ], 153 | "Next": "Success" 154 | } 155 | ], 156 | "Default": "SNS Publish" 157 | }, 158 | "SNS Publish": { 159 | "Type": "Task", 160 | "Resource": "arn:aws:states:::sns:publish", 161 | "Parameters": { 162 | "Message.$": "$", 163 | "TopicArn": "${sns_arn}" 164 | }, 165 | "Next": "Fail" 166 | }, 167 | "Success": { 168 | "Type": "Succeed" 169 | }, 170 | "Fail": { 171 | "Type": "Fail", 172 | "Error": "", 173 | "Cause": "" 174 | } 175 | } 176 | } 177 | RoleArn: !GetAtt AppAssessmentRole.Arn -------------------------------------------------------------------------------- /aws-codepipeline-integration/README.md: -------------------------------------------------------------------------------- 1 | # Continually assessing application resilience with AWS Resilience Hub and AWS CodePipeline 2 | 3 | The below diagram shows the resilience assessments automation architecture. AWS CodePipeline, AWS Step Functions, and AWS Resilience Hub are defined in your deployment account while the application AWS CloudFormation stacks are imported from your workload account. This pattern relies on AWS Resilience Hub ability to import CloudFormation stacks from a different accounts, regions, or both, when discovering an application structure. 4 | 5 | For the sake of simplicity the solution will describe the steps to deploy this pattern in a single account. 6 | 7 | For multi-account setup, please follow the steps described in this [blog post](https://aws.amazon.com/blogs/architecture/continually-assessing-application-resilience-with-aws-resilience-hub-and-aws-codepipeline/). The key different is to grant proper permissions for cross-account resilience hub access as described in the [AWS documentation](https://docs.aws.amazon.com/resilience-hub/latest/userguide/security-iam-resilience-hub-permissions.html#security-iam-resilience-hub-multi-account) 8 | 9 | A step by step workshop is also available on [AWS Workshop Studio](https://catalog.us-east-1.prod.workshops.aws/workshops/2a54eaaf-51ee-4373-a3da-2bf4e8bb6dd3/en-US/cicd-integration) 10 | ## Architecture 11 | 12 | ![Architecture](./images/ResilienceHubCICDArchitecture.jpg) 13 | 14 | After applications have been created within AWS Resilience Hub, a AWS CodePipeline Step Function stage will call AWS Resilience hub to assess the updated application resiliency posture. 15 | The Step Function workflow consists of the following steps: 16 | 1. The first step in the workflow is to update the resources associated with the application defined in AWS Resilience Hub by calling ImportResourcesToDraftApplication. 17 | 2. Check for the import process to complete using a wait state, a call to DescribeDraftAppVersionResourcesImportStatus and then a choice state to decide whether to progress or continue waiting. 18 | 3. Once complete, publish the draft application by calling PublishAppVersion to ensure we are assessing the latest version. 19 | 4. Once published, call StartAppAssessment to kick-off a resilience assessment. 20 | 5. Check for the assessment to complete using a wait state, a call to DescribeAppAssessment and then a choice state to decide whether to progress or continue waiting. 21 | 6. In the choice state, use assessment status from the response to determine if the assessment is pending, in progress or successful. 22 | 7. If successful, use the compliance status from the response to determine whether to progress to success or fail. Compliance status will be either “PolicyMet” or “PolicyBreached”. 23 | 8. If policy breached, publish onto SNS to alert the development team before moving to fail. 24 | 25 | For more information about these AWS SDK calls, please refer to: 26 | - [AWS Resilience Hub API](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/Welcome.html) 27 | - [AWS Step Functions SDK integrations support AWS Resilience Hub](https://aws.amazon.com/about-aws/whats-new/2022/04/aws-step-functions-expands-support-over-20-new-aws-sdk-integrations/) 28 | 29 | ## Prerequisites 30 | 31 | This solution assumes that you already have: 32 | 33 | 1. An application CI/CD CodePipeline 34 | 2. An AWS Resilience Hub Application defined based on the cloudformation stack resources. 35 | 36 | ## Deployment 37 | 38 | The solution is provided as CloudFormation templates that can be used to create CloudFormation stacks, which then provisions all the necessary resources and most importantly AWS Step Function to be called by the CodePipeline Stage. 39 | 40 | **NOTE: This solution must only be deployed once per AWS account, the Step Function can be called by various pipelines to trigger the application resiliency assessment** 41 | 42 | ### Stage 1: 43 | 44 | 1. Deploy a CloudFormation stack using the [AWSResilienceHubCICDStepFunction.yaml](./AWSResilienceHubCICDStepFunction.yaml) template. 45 | 2. After the stack reaches **CREATE_COMPLETE**, navigate to the **Resources** tab and note down the Physical ID (ARN) for **AWS::StepFunctions::StateMachine** resource. 46 | 47 | ### Stage 2: 48 | 49 | 1. Now that we have the AWS Step Function created, we need to integrate it into our pipeline. When adding the stage, you need to pass the ARN of the stack which was deployed in the previous Deploy stage as well as the ARN of the application in AWS Resilience Hub. These will be required on the AWS SDK calls and you can pass this in as a literal. 50 | 51 | In other words, the Step Function will need a json input similar to the one highlighted below, which we are configuring at the level of the new CodePipeline stage input field. 52 | ```json 53 | {"StackArn":"","AppArn":""} 54 | ``` 55 | ![ResilienceHubARN](./images/AWS-CodePipeline-stage-step-function-input.png) 56 | 57 | 2. Make sure that the CodePipeline IAM Role has the correct permissions trigger a step function. 58 | 1. Check the CodePipeline IAM Role used by clicking on the [Settings](https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create-service-role-console.html) menu after opening the pipeline in question 59 | 2. Add the below permissions to the pipeline IAM role idenfitied in step 1. 60 | 61 | --- 62 | **NOTE** 63 | 64 | Make sure to replace **** with the Physical ID (ARN) for **AWS::StepFunctions::StateMachine** resource 65 | 66 | --- 67 | 68 | ```json 69 | { 70 | "Effect": "Allow", 71 | "Action": [ 72 | "states:DescribeExecution", 73 | "states:DescribeStateMachine", 74 | "states:StartExecution" 75 | ], 76 | "Resource": "" 77 | }, 78 | ``` 79 | 80 | ## References 81 | - [Continually assessing application resilience with AWS Resilience Hub and AWS CodePipeline blog post](https://aws.amazon.com/blogs/architecture/continually-assessing-application-resilience-with-aws-resilience-hub-and-aws-codepipeline/). 82 | - [Using AWS Resilience Hub to monitor resilient architectures Workshop](https://catalog.us-east-1.prod.workshops.aws/workshops/2a54eaaf-51ee-4373-a3da-2bf4e8bb6dd3/en-US) 83 | -------------------------------------------------------------------------------- /aws-codepipeline-integration/images/AWS-CodePipeline-stage-step-function-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/aws-codepipeline-integration/images/AWS-CodePipeline-stage-step-function-input.png -------------------------------------------------------------------------------- /aws-codepipeline-integration/images/ResilienceHubCICDArchitecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/aws-codepipeline-integration/images/ResilienceHubCICDArchitecture.jpg -------------------------------------------------------------------------------- /cop316/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 | -------------------------------------------------------------------------------- /cop316/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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /cop316/LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /cop316/README.md: -------------------------------------------------------------------------------- 1 | ## AWS re:Invent COP316 code samples 2 | 3 | In this code repository, you will find code presented in the 2024 COP316 code talk given during AWS re:Invent 2024. Code is provided as a sample, and should not be used in prodution 4 | 5 | -------------------------------------------------------------------------------- /cop316/activate_post_launch_actions.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import sys 4 | 5 | def activate_post_launch_actions(source_server_id, drs_client): 6 | """ 7 | Activates the "Volume integrity validation" and "Validate disk space" post-launch actions for a given source server. 8 | """ 9 | post_launch_actions = [ 10 | "AWSMigration-VerifyMountedVolumes", 11 | "AWSMigration-ValidateDiskSpace" 12 | ] 13 | 14 | drs_client.update_launch_configuration( 15 | sourceServerID=source_server_id, 16 | postLaunchEnabled=True, 17 | ) 18 | 19 | response = drs_client.list_launch_actions( 20 | resourceId=source_server_id 21 | ) 22 | 23 | for action in response['items']: 24 | if action['actionCode'] in post_launch_actions: 25 | drs_client.put_launch_action( 26 | actionCode=action['actionCode'], 27 | actionId=action['actionId'], 28 | actionVersion=action['actionVersion'], 29 | active=True, 30 | category=action['category'], 31 | description=action['description'], 32 | name=action['name'], 33 | optional=action['optional'], 34 | order=action['order'], 35 | parameters=action['parameters'], 36 | resourceId=source_server_id 37 | ) 38 | 39 | def main(): 40 | """ 41 | Main function that connects to AWS DRS using temporary credentials from environment variables, 42 | activates the post-launch actions, and adds two validation actions. 43 | """ 44 | # Get temporary credentials from environment variables 45 | aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") 46 | aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") 47 | aws_session_token = os.getenv("AWS_SESSION_TOKEN") 48 | 49 | # Get region and source server IDs from command-line arguments 50 | if len(sys.argv) < 3: 51 | print("Usage: python script.py [ ...]") 52 | sys.exit(1) 53 | 54 | region = sys.argv[1] 55 | source_server_ids = sys.argv[2:] 56 | 57 | # Connect to AWS DRS 58 | drs_client = boto3.client( 59 | "drs", 60 | region_name=region, 61 | aws_access_key_id=aws_access_key_id, 62 | aws_secret_access_key=aws_secret_access_key, 63 | aws_session_token=aws_session_token 64 | ) 65 | 66 | # Activate post-launch actions and add 2 validate actions 67 | for source_server_id in source_server_ids: 68 | activate_post_launch_actions(source_server_id, drs_client) 69 | 70 | 71 | if __name__ == "__main__": 72 | main() -------------------------------------------------------------------------------- /cop316/add_app_to_arh.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import argparse 4 | import time 5 | import json 6 | 7 | def wait_for_app_creation(resilience_hub, app_arn, max_attempts=30, delay=10): 8 | for _ in range(max_attempts): 9 | try: 10 | response = resilience_hub.describe_app(appArn=app_arn) 11 | if response['app']['status'] == 'Active': 12 | return True 13 | except resilience_hub.exceptions.ResourceNotFoundException: 14 | pass 15 | time.sleep(delay) 16 | return False 17 | 18 | def wait_for_import_completion(resilience_hub, app_arn, max_attempts=30, delay=10): 19 | for _ in range(max_attempts): 20 | try: 21 | response = resilience_hub.describe_draft_app_version_resources_import_status(appArn=app_arn) 22 | if response['status'] == 'Success': 23 | return True 24 | except resilience_hub.exceptions.ResourceNotFoundException: 25 | pass 26 | time.sleep(delay) 27 | if response: 28 | print(f"Import failed: {response.get('statusMessage', 'Unknown error')}") 29 | return False 30 | 31 | 32 | def wait_for_assessment_completion(resilience_hub, assessment_arn, max_attempts=30, delay=10): 33 | for _ in range(max_attempts): 34 | try: 35 | assessment_response = resilience_hub.describe_app_assessment(assessmentArn=assessment_arn) 36 | if assessment_response['assessment']['assessmentStatus'] == 'Success': 37 | return True 38 | except resilience_hub.exceptions.ResourceNotFoundException: 39 | pass 40 | time.sleep(delay) 41 | if assessment_response: 42 | print(f"assessment failed: {assessment_response['assessment'].get('message', 'Unknown error')}") 43 | return False 44 | 45 | 46 | def add_app_to_resilience_hub(stack_name, app_name, region): 47 | resilience_hub = boto3.client('resiliencehub', region_name=region) 48 | cfn = boto3.client('cloudformation', region_name=region) 49 | 50 | try: 51 | # Get the CloudFormation stack ARN 52 | stack_response = cfn.describe_stacks(StackName=stack_name) 53 | stack_arn = stack_response['Stacks'][0]['StackId'] 54 | 55 | # Create the application in Resilience Hub 56 | app_response = resilience_hub.create_app( 57 | name=app_name, 58 | description=f"Application created from CloudFormation stack {stack_name}", 59 | assessmentSchedule="Daily" 60 | ) 61 | 62 | app_arn = app_response['app']['appArn'] 63 | print(f"Application '{app_name}' creation initiated with ARN: {app_arn}") 64 | 65 | # Wait for the application to be created 66 | print("Waiting for the application to be created...") 67 | if not wait_for_app_creation(resilience_hub, app_arn): 68 | print("Timed out waiting for application creation.") 69 | return None 70 | 71 | print("Application created successfully.") 72 | 73 | 74 | # Import the CloudFormation stack to the application 75 | print("Importing CloudFormation stack resources...") 76 | resilience_hub.import_resources_to_draft_app_version( 77 | appArn=app_arn, 78 | sourceArns=[stack_arn] 79 | ) 80 | 81 | # Wait for the import to complete 82 | if not wait_for_import_completion(resilience_hub, app_arn): 83 | print("Timed out or failed waiting for resource import.") 84 | return None 85 | 86 | print("Resources imported successfully.") 87 | 88 | # Create a resiliency policy 89 | policy_response = resilience_hub.create_resiliency_policy( 90 | policyName=f"{app_name}-policy", 91 | policyDescription=f"Resiliency policy for {app_name}", 92 | policy={ 93 | "Software": { 94 | "rpoInSecs": 45, 95 | "rtoInSecs": 2700 96 | }, 97 | "AZ": { 98 | "rpoInSecs": 3600, 99 | "rtoInSecs": 14400 100 | }, 101 | "Hardware": { 102 | "rpoInSecs": 3600, 103 | "rtoInSecs": 14400 104 | } 105 | }, 106 | tier="Critical" 107 | ) 108 | if policy_response['policy']: 109 | policy_arn = policy_response['policy']['policyArn'] 110 | print(f"Resiliency policy created with ARN: {policy_arn}") 111 | 112 | # Associate the policy with the application 113 | resilience_hub.update_app( 114 | appArn=app_arn, 115 | policyArn=policy_arn 116 | ) 117 | print(f"Resiliency policy associated with the application") 118 | 119 | # Publish the application 120 | version_response = resilience_hub.publish_app_version(appArn=app_arn) 121 | app_version = version_response["appVersion"] 122 | print(f"Application version published {app_version}") 123 | 124 | assessment_response = resilience_hub.start_app_assessment( 125 | appArn=app_arn, 126 | appVersion=app_version, 127 | assessmentName='demo-assessment' 128 | ) 129 | 130 | assessment_arn = assessment_response['assessment']['assessmentArn'] 131 | print(f"Assessment '{app_name}' creation initiated with ARN: {assessment_arn}") 132 | 133 | if not wait_for_assessment_completion(resilience_hub, assessment_arn): 134 | print("Timed out or failed waiting for assessment.") 135 | return None 136 | 137 | print(f"assessment completed successfully") 138 | 139 | recommendation_response = resilience_hub.list_app_component_recommendations( 140 | assessmentArn=assessment_arn, 141 | maxResults=100 142 | ) 143 | 144 | formatted_json = json.dumps(recommendation_response, indent=4) 145 | 146 | json_output = open('recommendations.json', 'w') 147 | json_output.write(formatted_json) 148 | json_output.close 149 | 150 | return app_arn 151 | 152 | except Exception as e: 153 | print(f"Error: {str(e)}") 154 | return None 155 | 156 | def main(): 157 | parser = argparse.ArgumentParser(description="Add an application to AWS Resilience Hub") 158 | parser.add_argument("stack_name", help="Name of the deployed CloudFormation stack") 159 | parser.add_argument("app_name", help="Name for the Resilience Hub application") 160 | parser.add_argument("region", help="AWS region of the stack and Resilience Hub") 161 | args = parser.parse_args() 162 | 163 | # Ensure AWS credentials are available 164 | if not (os.environ.get('AWS_ACCESS_KEY_ID') and 165 | os.environ.get('AWS_SECRET_ACCESS_KEY') and 166 | os.environ.get('AWS_SESSION_TOKEN')): 167 | print("AWS credentials not found. Please set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables.") 168 | return 169 | 170 | # Call the function to add the application to Resilience Hub 171 | app_arn = add_app_to_resilience_hub(args.stack_name, args.app_name, args.region) 172 | 173 | if app_arn: 174 | print(f"Application successfully added to Resilience Hub with ARN: {app_arn}") 175 | else: 176 | print("Failed to add the application to Resilience Hub.") 177 | 178 | if __name__ == "__main__": 179 | main() -------------------------------------------------------------------------------- /cop316/replication_monitoring_drs.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import sys 3 | import time 4 | import os 5 | 6 | def verify_replication_continuous(source_server_ids, drs_client): 7 | """ 8 | Verifies that the specified source servers have a continuous replication. 9 | """ 10 | while True: 11 | all_servers_ready = True 12 | response = drs_client.describe_source_servers( 13 | filters={ 14 | 'sourceServerIDs': source_server_ids 15 | } 16 | ) 17 | for source_server_details in response["items"]: 18 | data_replication_info = source_server_details["dataReplicationInfo"] 19 | if data_replication_info["dataReplicationState"] != "CONTINUOUS": 20 | print(f"Source server {source_server_details["sourceServerID"]} is not in a continuous replication state.") 21 | all_servers_ready = False 22 | else: 23 | print(f"Source server {source_server_details["sourceServerID"]} is in a continuous replication state.") 24 | if all_servers_ready: 25 | return True 26 | time.sleep(60) # Wait for 60 seconds before checking again 27 | return False 28 | 29 | def main(): 30 | """ 31 | Main function that connects to AWS DRS using temporary credentials, accepts source server IDs from the command line, 32 | and verifies that the servers have a continuous replication. 33 | """ 34 | # Get temporary credentials from environment variables 35 | aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") 36 | aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") 37 | aws_session_token = os.getenv("AWS_SESSION_TOKEN") 38 | 39 | region = sys.argv[1] 40 | source_server_ids = sys.argv[2:] 41 | 42 | if not source_server_ids: 43 | print("Please provide at least one source server ID as a command-line argument.") 44 | sys.exit(1) 45 | 46 | # Connect to AWS DRS 47 | drs_client = boto3.client( 48 | "drs", 49 | region_name=region, 50 | aws_access_key_id=aws_access_key_id, 51 | aws_secret_access_key=aws_secret_access_key, 52 | aws_session_token=aws_session_token 53 | ) 54 | 55 | # Verify the replication is continuous for each source server 56 | if verify_replication_continuous(source_server_ids, drs_client): 57 | print("All source servers have a continuous replication.") 58 | else: 59 | print("One or more source servers do not have a continuous replication.") 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /cop316/start_recovery_verify_launch.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import sys 3 | import time 4 | import os 5 | 6 | def start_recovery(source_server_ids, drs_client): 7 | """ 8 | Starts recovery for all specified source servers in a single job, setting isDrill to True. 9 | """ 10 | try: 11 | # Prepare the sourceServers parameter 12 | source_servers = [{"sourceServerID": server_id} for server_id in source_server_ids] 13 | 14 | # Prepare the request parameters 15 | request_params = { 16 | "isDrill": True, 17 | "sourceServers": source_servers 18 | } 19 | 20 | # Make the API call 21 | response = drs_client.start_recovery(**request_params) 22 | 23 | job = response['job'] 24 | job_id = job['jobID'] 25 | job_status = job['status'] 26 | job_type = job['type'] 27 | 28 | print(f"Started recovery drill. Job ID: {job_id}, Status: {job_status}, Type: {job_type}.") 29 | 30 | # Print information about participating servers 31 | for server in job['participatingServers']: 32 | source_id = server['sourceServerID'] 33 | launch_status = server['launchStatus'] 34 | print(f"Server {source_id} launch status: {launch_status}") 35 | 36 | return job_id 37 | except Exception as e: 38 | print(f"Failed to start recovery drill: {str(e)}.") 39 | return None 40 | 41 | def check_recovery_job_status(job_id, source_server_ids, drs_client): 42 | """ 43 | Checks the status of the recovery job and individual server recoveries within the job. 44 | """ 45 | while True: 46 | response = drs_client.describe_jobs(filters={"jobIDs": [job_id]}) 47 | jobs = response['items'] 48 | 49 | if not jobs: 50 | print(f"Job {job_id} not found") 51 | return False 52 | 53 | job = jobs[0] 54 | job_status = job['status'] 55 | 56 | if job_status == 'COMPLETED': 57 | all_servers_ready = True 58 | for server in job['participatingServers']: 59 | server_id = server['sourceServerID'] 60 | launch_status = server['launchStatus'] 61 | 62 | if launch_status == 'LAUNCHED': 63 | print(f"Recovery drill for server {server_id} completed successfully.") 64 | 65 | # Check launch actions status 66 | launch_actions_status = server.get('launchActionsStatus', {}) 67 | for run in launch_actions_status.get('runs', []): 68 | action = run['action'] 69 | run_status = run.get('status', 'UNKNOWN') # Handle optional status 70 | print(f" Action '{action['name']}' status: {run_status}") 71 | if run_status != 'SUCCEEDED': 72 | all_servers_ready = False 73 | 74 | elif launch_status in ['FAILED', 'STOPPED']: 75 | print(f"Recovery drill for server {server_id} failed or was stopped.") 76 | return False 77 | else: 78 | print(f"Unexpected status for server {server_id}: {launch_status}.") 79 | all_servers_ready = False 80 | 81 | if all_servers_ready: 82 | return True 83 | elif job_status in ['FAILED', 'STOPPED']: 84 | print(f"Recovery drill job {job_id} failed or was stopped") 85 | return False 86 | else: 87 | print(f"Recovery drill job {job_id} still in progress. Status: {job_status}.") 88 | 89 | time.sleep(60) # Wait for 60 seconds before checking again 90 | 91 | def main(): 92 | """ 93 | Main function that connects to AWS DRS using temporary credentials, accepts source server IDs from the command line, 94 | starts recovery drill, and checks the recovery status and post-launch actions. 95 | """ 96 | # Get temporary credentials from environment variables 97 | aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") 98 | aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") 99 | aws_session_token = os.getenv("AWS_SESSION_TOKEN") 100 | 101 | region = sys.argv[1] 102 | source_server_ids = sys.argv[2:] 103 | 104 | if not source_server_ids: 105 | print("Please provide at least one source server ID as a command-line argument.") 106 | sys.exit(1) 107 | 108 | # Connect to AWS DRS 109 | drs_client = boto3.client( 110 | "drs", 111 | region_name=region, 112 | aws_access_key_id=aws_access_key_id, 113 | aws_secret_access_key=aws_secret_access_key, 114 | aws_session_token=aws_session_token 115 | ) 116 | 117 | # Start recovery drill for all source servers in a single job 118 | job_id = start_recovery(source_server_ids, drs_client) 119 | if job_id: 120 | print("Recovery drill started for all source servers in a single job.") 121 | else: 122 | print("Failed to start recovery drill.") 123 | sys.exit(1) 124 | 125 | # Check recovery job status and individual server recovery status 126 | if check_recovery_job_status(job_id, source_server_ids, drs_client): 127 | print("Recovery drill job and all individual server recoveries have completed successfully.") 128 | else: 129 | print("Recovery drill job or one or more individual server recoveries failed or were stopped.") 130 | sys.exit(1) 131 | 132 | if __name__ == "__main__": 133 | main() -------------------------------------------------------------------------------- /create-resiliency-policy/README.md: -------------------------------------------------------------------------------- 1 | # Create resiliency policy 2 | 3 | Create a [resiliency policy](https://docs.aws.amazon.com/resilience-hub/latest/userguide/create-policy.html) using AWS CloudFormation to associate with applications defined in AWS Resilience Hub. Depending on the use case, you can create a resiliency policy that uses the same Recovery Time Objective (RTO) and Recovery Point Objective (RPO) values for each [disruption type](https://docs.aws.amazon.com/resilience-hub/latest/userguide/concepts-terms.html#disruption), or create a granular policy with different RTO and RPO values for different disruption types. 4 | 5 | ## Deployment 6 | 7 | Using these templates, you can create resiliency policies in a single AWS account and region by creating a [CloudFormation stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html), or deploy it to multiple accounts and regions using [CloudFormation StackSets](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html). 8 | 9 | * **Single RTO/RPO** - [resiliency_policy.yaml](./resiliency_policy.yaml) 10 | * **Granular RTO/RPO** - [resiliency_policy_granular.yaml](./resiliency_policy_granular.yaml) 11 | 12 | ## Parameters 13 | 14 | When using these templates, you will need to provide values for the parameters required by each use case. 15 | 16 | ### Single RTO/RPO 17 | 18 | * **PolicyName** - A name for the policy 19 | * **PolicyDescription** - A description for the policy 20 | * **RPOInSecs** - The Recovery Point Objective (RPO) in seconds 21 | * **RTOInSecs** - The Recovery Time Objective (RTO) in seconds 22 | * **PolicyTier** - A tier for the policy that represents the criticality. Supported values are MissionCritical, Critical, Important, CoreServices, and NonCritical 23 | 24 | ### Granular RTO/RPO 25 | 26 | * **PolicyName** - A name for the policy 27 | * **PolicyDescription** - A description for the policy 28 | * **AvailabilityZoneRPOInSecs** - The Recovery Point Objective (RPO) for Availability Zone disruptions in seconds 29 | * **AvailabilityZoneRTOInSecs** - The Recovery Time Objective (RTO) for Availability Zone disruptions in seconds 30 | * **HardwareRPOInSecs** - The Recovery Point Objective (RPO) for Hardware disruptions in seconds 31 | * **HardwareRTOInSecs** - The Recovery Time Objective (RTO) for Hardware disruptions in seconds 32 | * **SoftwareRPOInSecs** - The Recovery Point Objective (RPO) for Software disruptions in seconds 33 | * **SoftwareRTOInSecs** - The Recovery Time Objective (RTO) for Software disruptions in seconds 34 | * **RegionRPOInSecs** - The Recovery Point Objective (RPO) for Regional disruptions in seconds 35 | * **RegionRTOInSecs** - The Recovery Time Objective (RTO) for Regional disruptions in seconds 36 | * **PolicyTier** - A tier for the policy that represents the criticality. Supported values are MissionCritical, Critical, Important, CoreServices, and NonCritical 37 | 38 | ## Resources 39 | 40 | The templates create a single resource of type [AWS::ResilienceHub::ResiliencyPolicy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-resiliencehub-resiliencypolicy.html). 41 | 42 | ## Outputs 43 | 44 | The [Outputs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html) returns the ARN of the Resiliency policy that was created. 45 | -------------------------------------------------------------------------------- /create-resiliency-policy/resiliency_policy.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | PolicyName: 3 | Type: String 4 | Description: 'Enter a name for the policy.' 5 | PolicyDescription: 6 | Type: String 7 | Description: 'A description for the resiliency policy.' 8 | RPOInSecs: 9 | Type: Number 10 | Description: 'The Recovery Point Objective (RPO), in seconds.' 11 | RTOInSecs: 12 | Type: Number 13 | Description: 'The Recovery Time Objective (RTO), in seconds.' 14 | PolicyTier: 15 | Type: String 16 | Description: 'The tier for the policy based on criticality.' 17 | AllowedValues: 18 | - 'MissionCritical' 19 | - 'Critical' 20 | - 'Important' 21 | - 'CoreServices' 22 | - 'NonCritical' 23 | Resources: 24 | ResiliencyPolicy: 25 | Type: 'AWS::ResilienceHub::ResiliencyPolicy' 26 | Properties: 27 | Policy: 28 | AZ: 29 | RpoInSecs: !Ref RPOInSecs 30 | RtoInSecs: !Ref RTOInSecs 31 | Hardware: 32 | RpoInSecs: !Ref RPOInSecs 33 | RtoInSecs: !Ref RTOInSecs 34 | Software: 35 | RpoInSecs: !Ref RPOInSecs 36 | RtoInSecs: !Ref RTOInSecs 37 | Region: 38 | RpoInSecs: !Ref RPOInSecs 39 | RtoInSecs: !Ref RTOInSecs 40 | PolicyName: !Ref PolicyName 41 | PolicyDescription: !Ref PolicyDescription 42 | Tier: !Ref PolicyTier 43 | 44 | Outputs: 45 | PolicyARN: 46 | Value: !Ref ResiliencyPolicy 47 | -------------------------------------------------------------------------------- /create-resiliency-policy/resiliency_policy_granular.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | PolicyName: 3 | Type: String 4 | Description: 'Enter a name for the policy.' 5 | PolicyDescription: 6 | Type: String 7 | Description: 'A description for the resiliency policy.' 8 | AvalabilityZoneRPOInSecs: 9 | Type: Number 10 | Description: 'The Recovery Point Objective (RPO) for Availability Zone disruptions, in seconds.' 11 | AvalabilityZoneRTOInSecs: 12 | Type: Number 13 | Description: 'The Recovery Time Objective (RTO) for Availability Zone disruptions, in seconds.' 14 | HardwareRPOInSecs: 15 | Type: Number 16 | Description: 'The Recovery Point Objective (RPO) for Hardware disruptions, in seconds.' 17 | HardwareRTOInSecs: 18 | Type: Number 19 | Description: 'The Recovery Time Objective (RTO) for Hardware disruptions, in seconds.' 20 | SoftwareRPOInSecs: 21 | Type: Number 22 | Description: 'The Recovery Point Objective (RPO) for Software disruptions, in seconds.' 23 | SoftwareRTOInSecs: 24 | Type: Number 25 | Description: 'The Recovery Time Objective (RTO) for Software disruptions, in seconds.' 26 | RegionRPOInSecs: 27 | Type: Number 28 | Description: 'The Recovery Point Objective (RPO) for Regional disruptions, in seconds.' 29 | RegionRTOInSecs: 30 | Type: Number 31 | Description: 'The Recovery Time Objective (RTO) for Regional disruptions, in seconds.' 32 | PolicyTier: 33 | Type: String 34 | Description: 'The tier for the policy based on criticality.' 35 | AllowedValues: 36 | - 'MissionCritical' 37 | - 'Critical' 38 | - 'Important' 39 | - 'CoreServices' 40 | - 'NonCritical' 41 | 42 | Metadata: 43 | AWS::CloudFormation::Interface: 44 | ParameterGroups: 45 | - 46 | Label: 47 | default: "Policy metadata" 48 | Parameters: 49 | - PolicyName 50 | - PolicyDescription 51 | - PolicyTier 52 | - 53 | Label: 54 | default: "Target RPO" 55 | Parameters: 56 | - SoftwareRPOInSecs 57 | - HardwareRPOInSecs 58 | - AvalabilityZoneRPOInSecs 59 | - RegionRPOInSecs 60 | - 61 | Label: 62 | default: "Target RTO" 63 | Parameters: 64 | - SoftwareRTOInSecs 65 | - HardwareRTOInSecs 66 | - AvalabilityZoneRTOInSecs 67 | - RegionRTOInSecs 68 | ParameterLabels: 69 | VPCID: 70 | default: "Which VPC should this be deployed to?" 71 | 72 | Resources: 73 | ResiliencyPolicy: 74 | Type: 'AWS::ResilienceHub::ResiliencyPolicy' 75 | Properties: 76 | Policy: 77 | AZ: 78 | RpoInSecs: !Ref AvalabilityZoneRPOInSecs 79 | RtoInSecs: !Ref AvalabilityZoneRTOInSecs 80 | Hardware: 81 | RpoInSecs: !Ref HardwareRPOInSecs 82 | RtoInSecs: !Ref HardwareRTOInSecs 83 | Software: 84 | RpoInSecs: !Ref SoftwareRPOInSecs 85 | RtoInSecs: !Ref SoftwareRTOInSecs 86 | Region: 87 | RpoInSecs: !Ref RegionRPOInSecs 88 | RtoInSecs: !Ref RegionRTOInSecs 89 | PolicyName: !Ref PolicyName 90 | PolicyDescription: !Ref PolicyDescription 91 | Tier: !Ref PolicyTier 92 | 93 | Outputs: 94 | PolicyARN: 95 | Value: !Ref ResiliencyPolicy 96 | -------------------------------------------------------------------------------- /create-resource-group-for-resilience-hub/README.md: -------------------------------------------------------------------------------- 1 | # Create an AWS Resource Group to define an application for AWS Resilience Hub 2 | 3 | Creates an AWS Resource Group that describes an application that can be used for running assessments using AWS Resilience Hub. This is useful in situations where you might have your infrastruscture provisioned without using any of the infrastrucure-as-code tools supported by Resilience Hub, such as AWS CloudFormation and Terraform. 4 | 5 | This solution creates a new Resource Group and applies appropriate filters to only include resource types that are supported by Resilience Hub and will help mitigate quota exhaustion. The Resource Group created will contain the following resource types: 6 | 7 | * AWS::ApiGateway::RestApi 8 | * AWS::DynamoDB::Table 9 | * AWS::EC2::Instance 10 | * AWS::EC2::NatGateway 11 | * AWS::EC2::Volume 12 | * AWS::ECS::Service 13 | * AWS::EFS::FileSystem 14 | * AWS::Lambda::Function 15 | * AWS::RDS::DBCluster 16 | * AWS::RDS::DBInstance 17 | * AWS::S3::Bucket 18 | * AWS::SQS::Queue 19 | * AWS::ElasticLoadBalancingV2::LoadBalancer 20 | * AWS::ElasticLoadBalancing::LoadBalancer 21 | 22 | **NOTE: There are additional resource types supported by Resilience Hub (such as Amazon EC2 AutoScaling groups) that are currently not supported by Resource Groups. Use the [resource-group-enhancer](/resource-group-enhancer/README.md) solution to add these unsupported resource types to the application on Resilience Hub.** 23 | 24 | ## Deployment 25 | 26 | Use the [resource-group.yaml](./resource-group.yaml) template to create a CloudFormation stack. 27 | 28 | ## Parameters 29 | 30 | When using this template, you will need to provide values for the following parameters: 31 | 32 | * **ApplicationName** - the name of the application which will be used to name the resource group 33 | * **TagKey** - the tag key used to identify resources 34 | * **TagValue** - the tag value used to identify resources 35 | 36 | ## Resources 37 | 38 | The templates create a single resource of type [AWS::ResourceGroups::Group](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-resourcegroups-group.html). 39 | 40 | ## Outputs 41 | 42 | The [Outputs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html) returns the name and ARN of the Resource Group that was created. 43 | -------------------------------------------------------------------------------- /create-resource-group-for-resilience-hub/resource-group.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | ApplicationName: 3 | Type: String 4 | Description: >- 5 | The name of the application. This value will be used to name the resource 6 | group. 7 | TagKey: 8 | Type: String 9 | Description: The tag key for resources in your application. 10 | TagValue: 11 | Type: String 12 | Description: The tag value for resources in your application 13 | Resources: 14 | ARHResourceGroup: 15 | Type: 'AWS::ResourceGroups::Group' 16 | Properties: 17 | Description: >- 18 | Resource group that defines an application to be assessed by Resiliennce 19 | Hub 20 | Name: !Join 21 | - '' 22 | - - !Ref ApplicationName 23 | - '-resilience-hub' 24 | ResourceQuery: 25 | Query: 26 | ResourceTypeFilters: 27 | - 'AWS::ApiGateway::RestApi' 28 | - 'AWS::DynamoDB::Table' 29 | - 'AWS::EC2::Instance' 30 | - 'AWS::EC2::NatGateway' 31 | - 'AWS::EC2::Volume' 32 | - 'AWS::ECS::Service' 33 | - 'AWS::EFS::FileSystem' 34 | - 'AWS::Lambda::Function' 35 | - 'AWS::RDS::DBCluster' 36 | - 'AWS::RDS::DBInstance' 37 | - 'AWS::S3::Bucket' 38 | - 'AWS::SQS::Queue' 39 | - 'AWS::ElasticLoadBalancingV2::LoadBalancer' 40 | - 'AWS::ElasticLoadBalancing::LoadBalancer' 41 | - 'AWS::SNS::Topic' 42 | TagFilters: 43 | - Key: !Ref TagKey 44 | Values: 45 | - !Ref TagValue 46 | Type: TAG_FILTERS_1_0 47 | Outputs: 48 | ResourceGroupName: 49 | Description: >- 50 | The name of the resource group that can be used for defining applications 51 | in Resilience Hub. 52 | Value: !Ref ARHResourceGroup 53 | ResourceGroupARN: 54 | Description: >- 55 | The ARN of the resource group that can be used for defining applications 56 | in Resilience Hub. 57 | Value: !GetAtt 58 | - ARHResourceGroup 59 | - Arn 60 | -------------------------------------------------------------------------------- /github-actions-integration/README.md: -------------------------------------------------------------------------------- 1 | # Integrate AWS Resilience Hub with GitHub Actions 2 | 3 | Achieve continual resilience by integrating resilience checks into your Continuous Integration/Continuoys Delivery (CI/CD) pipelines. This solution describes how you can integrate AWS Resilience Hub with your CI/CD pipeline if you are using [GitHub Actions](https://docs.github.com/en/actions) and follows a process similar to what is described in [this blog post](https://aws.amazon.com/blogs/architecture/continually-assessing-application-resilience-with-aws-resilience-hub-and-aws-codepipeline/). 4 | 5 | ## Deployment 6 | 7 | 1. Update your deploy.yaml file as per the example provided in [deploy.yml](./deploy.yml). Specifically, add the last job titled **resilience-check** into your own deploy.yaml file, after all the deployment jobs. Ensure that you have updated the following placeholders with real values based on your use case: 8 | * **aws-region** - specify the AWS Region where the application exists on Resilience Hub 9 | * **StackARN** - specify the ARN of the AWS CloudFormation stack that you are updating as part of your deployment 10 | * **AppARN** - specify the ARN of the application you have defined within Resilience Hub 11 | 12 | **NOTE: You might have to update the step *Configure AWS Credentials* under the *resilience-check* job depending on the methodology for authenticating access to your AWS environment.** 13 | 14 | 1. Update the [resilience-check.py](./resilience-check.py) and specify the AWS Region where the application exists on Resilience Hub. 15 | 1. Add the script [resilience-check.py](./resilience-check.py) to the **scripts** folder under **.github** (this is located at the root of your project). If the directory **scripts** does not exist, create it under **.github** and add the resilience-check.py script to it. 16 | 1. Commit and push these changes to your repository. 17 | -------------------------------------------------------------------------------- /github-actions-integration/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 'Deploy to AWS CloudFormation' 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code from master branch 12 | uses: actions/checkout@v2 13 | 14 | - name: Configure AWS Credentials 15 | uses: aws-actions/configure-aws-credentials@v1 16 | with: 17 | aws-access-key-id: ${{ secrets.ACCESS_KEY }} 18 | aws-secret-access-key: ${{ secrets.ACCESS_KEY_SECRET }} 19 | aws-region: ##### Specify AWS Region ##### 20 | 21 | - name: Deploy to AWS CloudFormation 22 | uses: aws-actions/aws-cloudformation-github-deploy@v1 23 | with: 24 | name: arh-github-actions 25 | template: bucket.yaml 26 | 27 | resilience-check: 28 | runs-on: ubuntu-latest 29 | needs: deploy 30 | steps: 31 | - name: Checkout code from master branch 32 | uses: actions/checkout@v2 33 | - name: Install python 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: '3.10' 37 | - name: Install boto 38 | run: | 39 | pip install boto3 40 | - name: Configure AWS Credentials 41 | uses: aws-actions/configure-aws-credentials@v1 42 | with: 43 | aws-access-key-id: ${{ secrets.ACCESS_KEY }} 44 | aws-secret-access-key: ${{ secrets.ACCESS_KEY_SECRET }} 45 | aws-region: ##### Specify AWS Region ##### 46 | - name: Resilience check 47 | env: 48 | StackARN: ##### Specify CloudFormation stack ARN ##### 49 | AppARN: ##### Specify application ARN on Resilience Hub ##### 50 | run: | 51 | python .github/scripts/resilience-check.py 52 | -------------------------------------------------------------------------------- /github-actions-integration/resilience-check.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import time 4 | 5 | def resilience_check(): 6 | app_arn = os.environ['AppARN'] 7 | StackARN = os.environ['StackARN'] 8 | 9 | arh = boto3.client('resiliencehub', region_name='##### Specify AWS Region #####') 10 | 11 | print('Starting import') 12 | import_resources = arh.import_resources_to_draft_app_version( 13 | appArn=app_arn, 14 | sourceArns=[ 15 | StackARN, 16 | ] 17 | ) 18 | 19 | import_status = arh.describe_draft_app_version_resources_import_status( 20 | appArn=app_arn 21 | ) 22 | 23 | print('Waiting for import to complete, current status = ' + import_status['status']) 24 | 25 | while (import_status['status'] in {'Pending', 'InProgress'}): 26 | time.sleep(5) 27 | import_status = arh.describe_draft_app_version_resources_import_status( 28 | appArn=app_arn 29 | ) 30 | print('Waiting for import to complete, current status = ' + import_status['status']) 31 | 32 | if import_status['status'] == 'Failed': 33 | raise Exception('Resource import failed - ' + str(import_status)) 34 | 35 | print('Import status - ' + import_status['status']) 36 | 37 | publish_app = arh.publish_app_version( 38 | appArn=app_arn 39 | ) 40 | 41 | print('Starting assessment') 42 | start_assessment = arh.start_app_assessment( 43 | appArn=app_arn, 44 | appVersion='release', 45 | assessmentName='GitHub-actions-assessment' 46 | ) 47 | 48 | assessmentARN = start_assessment['assessment']['assessmentArn'] 49 | 50 | assessment_status = arh.describe_app_assessment( 51 | assessmentArn=assessmentARN 52 | )['assessment'] 53 | 54 | print('Waiting for assessment to complete, current status - ' + assessment_status['assessmentStatus']) 55 | 56 | while (assessment_status['assessmentStatus'] in {'Pending', 'InProgress'}): 57 | time.sleep(5) 58 | assessment_status = arh.describe_app_assessment( 59 | assessmentArn=assessmentARN 60 | )['assessment'] 61 | print('Waiting for assessment to complete, current status - ' + assessment_status['assessmentStatus']) 62 | 63 | print('Assessment status - ' + assessment_status['assessmentStatus']) 64 | 65 | if assessment_status['assessmentStatus'] == 'Failed': 66 | raise Exception('Assessment failed - ' + str(assessment_status)) 67 | 68 | if assessment_status['complianceStatus'] == 'PolicyBreached': 69 | print('Resiliency policy breached') 70 | raise Exception('Resiliency policy breached') 71 | else: 72 | print('Resiliency policy met') 73 | 74 | if __name__ == "__main__": 75 | resilience_check() 76 | -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/README.md: -------------------------------------------------------------------------------- 1 | # Automate resilience assessments of your AWS CloudFormation Stacks/Terraform Statefiles using AWS Resilience Hub and Amazon EventBridge 2 | This AWS CloudFormation template deploys a solution to automatically assess the resilience posture of new or updated CloudFormation stacks, or Terraform state files stored in AWS S3 bucket. The solution automatically creates an application in Resilience Hub and assesses it against the provided resiliency policies. The application owner receives a notification with their resilience assessment. 3 | 4 | This solution uses Amazon EventBridge to launch an AWS Step Functions workflow to orchestrate AWS Resilience Hub (ARH). Notification of detected drift and assessment failures are published to Amazon Simple Notification Service (SNS). 5 | 6 | The solution deploys five resiliency policies in ARH to check applications against: `MissionCritical | Critical | Important | CoreServices | NonCritical` 7 | ![Resiliency Policies](res-policies.png) 8 | 9 | ## Prerequisites 10 | 1. AWS account 11 | 2. IAM admin privileges (or sufficient to deploy this solution) 12 | 3. SNS topic with permission to receive resilience assessment notifications (see https://docs.aws.amazon.com/resilience-hub/latest/userguide/enabling-sns-in-arh.html) 13 | 14 | ## Setup 15 | 1. Navigate to the CloudFormation console in AWS 16 | 2. Deploy `arh_input_source_eb_template.yaml` 17 | 18 | ## How to use Solution 19 | #### Using CloudFormation stacks 20 | When deploying or updating applications to AWS using CloudFormation, add the two following tags to the CloudFormation. This will result in automatic creation or update to existing applications in ARH which notifies the application owner of any detected drift or assessment failures. 21 | 22 | Steps: 23 | 1. Deploy a CloudFormation template 24 | 2. During the CloudFormation deployment, add the following tags: 25 | 1. **(Required)**`app_criticality`, value: STRING from a pre-formatted list from ARH tiers: Valid Values: `MissionCritical | Critical | Important | CoreServices | NonCritical` 26 | 2. **(Optional)** `app_owner`, value: ARN of valid SNS Topic that will receive the resilience assessment notifications 27 | 3. **(Required)** `app_name`, value: Name of the new or existing Resilience Hub application 28 | 29 | ![Add Tags Image](add-tag.png) 30 | 31 | 3. Once the template is deployed, the solution will automatically handle the create/update/delete process for resources in ARH. 32 | 33 | #### Using Terraform state files store in S3 bucket 34 | Steps: 35 | 1. Turn on Amazon EventBridge notifications on the Amazon S3 bucket 36 | ![Enable EventBridge Notifications Image](eventbridge-on.png) 37 | 2. Upload Terraform state file to Amazon S3 bucket. 38 | 3. During the object upload, add the following tags: 39 | 1. **(Required)**`app_criticality`, value: STRING from a pre-formatted list from ARH tiers: Valid Values: `MissionCritical | Critical | Important | CoreServices | NonCritical` 40 | 2. **(Optional)** `app_owner`, value: ARN of valid SNS Topic that will receive the resilience assessment notification 41 | 3. **(Required)** `app_name`, value: Name of the new or existing Resilience Hub application 42 | 43 | This will result an automatic creation or update to existing applications in ARH which notifies the application owner of any detected drift or assessment failures. 44 | 45 | ![Add Tags To S3 Image](add-tags-s3.png) 46 | 47 | 4. Once the object is uploaded, the solution will automatically handle the create/update/delete process for resources in ARH. 48 | 49 | ## Reference Architecture 50 | #### AWS Architecture 51 | ![Architecture Image](architecture.png) 52 | #### AWS Step Functions Workflow 53 | ![Step Functions Workflow](step-functions-workflow-with-tf.png) 54 | For CloudFormation stack, the above Step Function branches based off of the status of the stack: 55 | >CREATE_COMPLETE --> Create a new application in ARH 56 | >DELETE_COMPLETE --> Delete the application in ARH 57 | >UPDATE_COMPLETE --> Create a new version for the application in ARH and re-import input sources. If application does not exist in ARH then create a new application. 58 | 59 | For Terraform state file stored in S3 bucket, the Step Function branches off off the object event: 60 | > PutObject --> Create a new version for the application in ARH and re-import input sources. If application does not exist in ARH then create a new application. 61 | > DeleteObject --> Delete the application in ARH 62 | 63 | 64 | ## Important Notes 65 | #### CloudFormation Stacks 66 | - Deleting a template that was deployed with the mandatory tags will also result in the application created in ARH being deleted as well. 67 | - To import existing stacks into ARH, update the stack and add the `app_name` and `app_criticality` tags. 68 | #### Terraform state file in S3 bucket 69 | - Deleting an object with the mandatory tags will also result in the application created in ARH being deleted as well. 70 | - To import existing Terraform state files into ARH, re-upload the object and add the `app_name` and `app_criticality` tags. 71 | -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/add-tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/add-tag.png -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/add-tags-s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/add-tags-s3.png -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/architecture.png -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/arh_input_source_eb_template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Parameters: 4 | TerraformS3Bucket: 5 | Type: String 6 | Description: Enter the S3 bucket name containing the Terraform state file 7 | Resources: 8 | CustomEventBus: 9 | Type: AWS::Events::EventBus 10 | Properties: 11 | Name: !Sub ${AWS::StackName}-cloudFormationEventBus 12 | 13 | EventBridgeRole: 14 | Type: AWS::IAM::Role 15 | Properties: 16 | AssumeRolePolicyDocument: 17 | Version: "2012-10-17" 18 | Statement: 19 | - Effect: Allow 20 | Principal: 21 | Service: 22 | - events.amazonaws.com 23 | Action: 24 | - "sts:AssumeRole" 25 | Policies: 26 | - PolicyName: AllowCloudwatchPutEvents 27 | PolicyDocument: 28 | Version: "2012-10-17" 29 | Statement: 30 | - Effect: Allow 31 | Action: 32 | - "events:PutEvents" 33 | Resource: 34 | - !GetAtt CustomEventBus.Arn 35 | 36 | EventBridgeToStepFunctionsRole: 37 | Type: AWS::IAM::Role 38 | Properties: 39 | AssumeRolePolicyDocument: 40 | Version: "2012-10-17" 41 | Statement: 42 | - Effect: Allow 43 | Principal: 44 | Service: 45 | - events.amazonaws.com 46 | Action: 47 | - "sts:AssumeRole" 48 | Policies: 49 | - PolicyName: AllowStartStateMachine 50 | PolicyDocument: 51 | Version: "2012-10-17" 52 | Statement: 53 | - Effect: Allow 54 | Action: 55 | - "states:StartExecution" 56 | Resource: 57 | - !GetAtt ArhCfnStateMachine.Arn 58 | 59 | CFStateChangeRule: 60 | Type: AWS::Events::Rule 61 | Properties: 62 | EventBusName: !Ref CustomEventBus 63 | EventPattern: 64 | source: 65 | - aws.cloudformation 66 | detail-type: 67 | - CloudFormation Stack Status Change 68 | detail: 69 | status-details: 70 | status: 71 | - CREATE_COMPLETE 72 | - UPDATE_COMPLETE 73 | - DELETE_COMPLETE 74 | State: ENABLED 75 | Targets: 76 | - Id: 0 77 | Arn: !GetAtt ArhCfnStateMachine.Arn 78 | RoleArn: !GetAtt EventBridgeToStepFunctionsRole.Arn 79 | 80 | SendToCFBusRule: 81 | Type: AWS::Events::Rule 82 | Properties: 83 | EventBusName: default 84 | EventPattern: 85 | source: 86 | - aws.cloudformation 87 | detail-type: 88 | - CloudFormation Stack Status Change 89 | detail: 90 | status-details: 91 | status: 92 | - CREATE_COMPLETE 93 | - UPDATE_COMPLETE 94 | - DELETE_COMPLETE 95 | State: ENABLED 96 | Targets: 97 | - Id: 0 98 | Arn: !GetAtt CustomEventBus.Arn 99 | RoleArn: !GetAtt EventBridgeRole.Arn 100 | 101 | SendToTFBusRule: 102 | Type: AWS::Events::Rule 103 | Properties: 104 | EventBusName: default 105 | EventPattern: 106 | source: 107 | - aws.s3 108 | detail-type: 109 | - Object Created 110 | - Object Deleted 111 | detail: 112 | bucket: 113 | name: 114 | - Ref: TerraformS3Bucket 115 | object: 116 | key: 117 | - suffix: .tfstate 118 | State: ENABLED 119 | Targets: 120 | - Id: 0 121 | Arn: !GetAtt CustomEventBus.Arn 122 | RoleArn: !GetAtt EventBridgeRole.Arn 123 | 124 | TFStateChangeRule: 125 | Type: AWS::Events::Rule 126 | Properties: 127 | EventBusName: !Ref CustomEventBus 128 | EventPattern: 129 | source: 130 | - aws.s3 131 | detail-type: 132 | - Object Created 133 | - Object Deleted 134 | detail: 135 | bucket: 136 | name: 137 | - Ref: TerraformS3Bucket 138 | object: 139 | key: 140 | - suffix: .tfstate 141 | State: ENABLED 142 | Targets: 143 | - Id: 0 144 | Arn: !GetAtt ArhCfnStateMachine.Arn 145 | RoleArn: !GetAtt EventBridgeToStepFunctionsRole.Arn 146 | 147 | ResiliencyPolicyNonCritical: 148 | Type: 'AWS::ResilienceHub::ResiliencyPolicy' 149 | Properties: 150 | Policy: 151 | Software: 152 | RpoInSecs: 86400 153 | RtoInSecs: 172800 154 | Hardware: 155 | RpoInSecs: 86400 156 | RtoInSecs: 172800 157 | AZ: 158 | RpoInSecs: 86400 159 | RtoInSecs: 172800 160 | PolicyName: NonCritical 161 | Tier: NonCritical 162 | 163 | ResiliencyPolicyCoreServices: 164 | Type: 'AWS::ResilienceHub::ResiliencyPolicy' 165 | Properties: 166 | Policy: 167 | Software: 168 | RpoInSecs: 900 169 | RtoInSecs: 3600 170 | Hardware: 171 | RpoInSecs: 5 172 | RtoInSecs: 30 173 | AZ: 174 | RpoInSecs: 5 175 | RtoInSecs: 30 176 | PolicyName: CoreServices 177 | Tier: CoreServices 178 | 179 | ResiliencyPolicyImportant: 180 | Type: 'AWS::ResilienceHub::ResiliencyPolicy' 181 | Properties: 182 | Policy: 183 | Software: 184 | RpoInSecs: 14400 185 | RtoInSecs: 172800 186 | Hardware: 187 | RpoInSecs: 7200 188 | RtoInSecs: 172800 189 | AZ: 190 | RpoInSecs: 7200 191 | RtoInSecs: 172800 192 | PolicyName: Important 193 | Tier: Important 194 | 195 | ResiliencyPolicyCritical: 196 | Type: 'AWS::ResilienceHub::ResiliencyPolicy' 197 | Properties: 198 | Policy: 199 | Software: 200 | RpoInSecs: 3600 201 | RtoInSecs: 14400 202 | Hardware: 203 | RpoInSecs: 3600 204 | RtoInSecs: 3600 205 | AZ: 206 | RpoInSecs: 3600 207 | RtoInSecs: 3600 208 | PolicyName: Critical 209 | Tier: Critical 210 | 211 | ResiliencyPolicyMissionCritical: 212 | Type: 'AWS::ResilienceHub::ResiliencyPolicy' 213 | Properties: 214 | Policy: 215 | Software: 216 | RpoInSecs: 900 217 | RtoInSecs: 3600 218 | Hardware: 219 | RpoInSecs: 300 220 | RtoInSecs: 300 221 | AZ: 222 | RpoInSecs: 300 223 | RtoInSecs: 300 224 | PolicyName: MissionCritical 225 | Tier: MissionCritical 226 | 227 | ArhCfnStateMachineRole: 228 | Type: AWS::IAM::Role 229 | Properties: 230 | AssumeRolePolicyDocument: 231 | Version: "2012-10-17" 232 | Statement: 233 | - Effect: Allow 234 | Principal: 235 | Service: 236 | - states.amazonaws.com 237 | Action: 238 | - "sts:AssumeRole" 239 | ManagedPolicyArns: 240 | - arn:aws:iam::aws:policy/ReadOnlyAccess 241 | - arn:aws:iam::aws:policy/AWSLambda_FullAccess 242 | Policies: 243 | - PolicyName: AllowResilienceHub 244 | PolicyDocument: 245 | Version: "2012-10-17" 246 | Statement: 247 | - Effect: Allow 248 | Action: 249 | - "resiliencehub:CreateApp" 250 | - "resiliencehub:DeleteApp" 251 | - "resiliencehub:DeleteAppAssessment" 252 | - "resiliencehub:ImportResourcesToDraftAppVersion" 253 | - "resiliencehub:PublishAppVersion" 254 | - "resiliencehub:StartAppAssessment" 255 | - "resiliencehub:TagResource" 256 | - "resiliencehub:UpdateApp" 257 | - "resiliencehub:DeleteAppInputSource" 258 | - "resiliencehub:ListAppInputSources" 259 | Resource: 260 | - !Sub "arn:${AWS::Partition}:resiliencehub:${AWS::Region}:${AWS::AccountId}:*" 261 | 262 | ArhCfnStateMachine: 263 | Type: AWS::Serverless::StateMachine 264 | Properties: 265 | Definition: 266 | Comment: >- 267 | Create, update, or delete Resilience Hub applications in response to 268 | CloudFormation events 269 | StartAt: ProcessEventData 270 | States: 271 | Check Assessment Status: 272 | Choices: 273 | - Next: Create/Update Successful 274 | StringMatches: Success 275 | Variable: $.Assessment.AssessmentStatus 276 | Default: Wait for Assessment 277 | Type: Choice 278 | Check Import Status: 279 | Choices: 280 | - Next: PublishAppVersion 281 | StringMatches: Success 282 | Variable: $.Import.Status 283 | Default: Wait for Import 284 | OutputPath: $.Apps.App 285 | Type: Choice 286 | Check Mandatory Tags: 287 | Choices: 288 | - Comment: Check if both Policy and name tags are present 289 | Next: ListApps 290 | And: 291 | - IsPresent: true 292 | Variable: $.ForEachTag.PolicyArn[0] 293 | - IsNull: false 294 | Variable: $.ArhPayload.ArhApplicationName 295 | Default: Create/Update Successful 296 | Type: Choice 297 | Create, Update, Delete: 298 | Choices: 299 | - Comment: CREATE or UPDATE COMPLETED 300 | Next: ForEach Tag 301 | Or: 302 | - StringEquals: UPDATE_COMPLETE 303 | Variable: $.ArhPayload.SourceEventReceived 304 | - StringEquals: CREATE_COMPLETE 305 | Variable: $.ArhPayload.SourceEventReceived 306 | - Comment: DELETE_COMPLETE 307 | Next: ListApps (Delete) 308 | StringEquals: DELETE_COMPLETE 309 | Variable: $.ArhPayload.SourceEventReceived 310 | Default: Fail 311 | Type: Choice 312 | Create/Update Successful: 313 | Type: Succeed 314 | CreateApp: 315 | Next: Update Tags 316 | Parameters: 317 | Description: DO NOT DELETE -- This application is managed by CloudFormation 318 | EventSubscriptions.$: $.ForEachTag.Notify 319 | Name.$: $.ArhPayload.ArhApplicationName 320 | PolicyArn.$: $.ForEachTag.PolicyArn[0].PolicyArn 321 | Resource: arn:aws:states:::aws-sdk:resiliencehub:createApp 322 | ResultPath: $.Apps 323 | Type: Task 324 | Delete App Input Source: 325 | Choices: 326 | - Next: Delete App Input Source (CF) 327 | StringMatches: aws.cloudformation 328 | Variable: $.ArhPayload.ArhAppInputSourceType 329 | - Next: Delete App Input Source (TF) 330 | StringMatches: aws.s3 331 | Variable: $.ArhPayload.ArhAppInputSourceType 332 | Type: Choice 333 | Delete App Input Source (CF): 334 | Catch: 335 | - ErrorEquals: 336 | - Resiliencehub.ResourceNotFoundException 337 | Next: List App Input Sources 338 | ResultPath: null 339 | Next: List App Input Sources 340 | Parameters: 341 | AppArn.$: $.Apps.AppSummaries[0].AppArn 342 | SourceArn.$: $.ArhPayload.ArhAppInputSource 343 | Resource: arn:aws:states:::aws-sdk:resiliencehub:deleteAppInputSource 344 | ResultPath: $.DeletedApp 345 | Type: Task 346 | Delete App Input Source (TF): 347 | Catch: 348 | - ErrorEquals: 349 | - Resiliencehub.ResourceNotFoundException 350 | Next: List App Input Sources 351 | ResultPath: null 352 | Next: List App Input Sources 353 | Parameters: 354 | AppArn.$: $.Apps.AppSummaries[0].AppArn 355 | TerraformSource.$: >- 356 | States.StringToJson(States.Format('\{"S3StateFileUrl":"{}"\}',$.ArhPayload.ArhAppInputSource)) 357 | Resource: arn:aws:states:::aws-sdk:resiliencehub:deleteAppInputSource 358 | ResultPath: $.DeletedApp 359 | Type: Task 360 | Delete Successful: 361 | Type: Succeed 362 | DeleteApp: 363 | Next: Delete Successful 364 | Parameters: 365 | AppArn.$: $.AppArn 366 | Resource: arn:aws:states:::aws-sdk:resiliencehub:deleteApp 367 | Type: Task 368 | DescribeAppAssessment: 369 | Next: Check Assessment Status 370 | Parameters: 371 | AssessmentArn.$: $.Assessment.AssessmentArn 372 | Resource: arn:aws:states:::aws-sdk:resiliencehub:describeAppAssessment 373 | Type: Task 374 | DescribeDraftAppVersionResourcesImportStatus: 375 | Next: Check Import Status 376 | Parameters: 377 | AppArn.$: $.Apps.App.AppArn 378 | Resource: >- 379 | arn:aws:states:::aws-sdk:resiliencehub:describeDraftAppVersionResourcesImportStatus 380 | ResultPath: $.Import 381 | Type: Task 382 | Fail: 383 | Type: Fail 384 | ForEach Assessment: 385 | ItemProcessor: 386 | ProcessorConfig: 387 | Mode: INLINE 388 | StartAt: DeleteAppAssessment 389 | States: 390 | DeleteAppAssessment: 391 | End: true 392 | Parameters: 393 | AssessmentArn.$: $.AssessmentArn 394 | Resource: arn:aws:states:::aws-sdk:resiliencehub:deleteAppAssessment 395 | ResultPath: null 396 | Retry: 397 | - BackoffRate: 2 398 | ErrorEquals: 399 | - Resiliencehub.ResiliencehubException 400 | IntervalSeconds: 10 401 | MaxAttempts: 3 402 | Type: Task 403 | ItemsPath: $.result.AssessmentSummaries 404 | Next: DeleteApp 405 | ResultPath: null 406 | Type: Map 407 | ForEach Tag: 408 | ItemProcessor: 409 | ProcessorConfig: 410 | Mode: INLINE 411 | StartAt: Check for 'app_criticality' 412 | States: 413 | Check for 'app_criticality': 414 | Choices: 415 | - Next: ListResiliencyPolicies 416 | StringMatches: app_criticality 417 | Variable: $.Key 418 | - Next: Format Notification Types 419 | StringMatches: app_owner 420 | Variable: $.Key 421 | Default: Pass 422 | Type: Choice 423 | Format Notification Types: 424 | Branches: 425 | - StartAt: Type 1 426 | States: 427 | Type 1: 428 | End: true 429 | Parameters: 430 | EventType: DriftDetected 431 | Name: DriftDetected 432 | SnsTopicArn.$: $.Value 433 | Type: Pass 434 | - StartAt: Type 2 435 | States: 436 | Type 2: 437 | End: true 438 | Parameters: 439 | EventType: ScheduledAssessmentFailure 440 | Name: ScheduledAssessmentFailure 441 | SnsTopicArn.$: $.Value 442 | Type: Pass 443 | End: true 444 | Type: Parallel 445 | ListResiliencyPolicies: 446 | End: true 447 | Parameters: 448 | PolicyName.$: $.Value 449 | Resource: arn:aws:states:::aws-sdk:resiliencehub:listResiliencyPolicies 450 | ResultSelector: 451 | PolicyArn.$: $.ResiliencyPolicies[0].PolicyArn 452 | Type: Task 453 | Pass: 454 | End: true 455 | Type: Pass 456 | ItemsPath: $.ArhPayload.Tags 457 | Next: Check Mandatory Tags 458 | ResultPath: $.ForEachTag 459 | ResultSelector: 460 | Notify.$: $..[?(@.EventType)] 461 | PolicyArn.$: $..[?(@.PolicyArn)] 462 | Type: Map 463 | If App Found (Delete): 464 | Choices: 465 | - IsPresent: true 466 | Next: Delete App Input Source 467 | Variable: $.Apps.AppSummaries[0] 468 | Default: Delete Successful 469 | Type: Choice 470 | If Assessment(s) Found: 471 | Choices: 472 | - IsPresent: true 473 | Next: ForEach Assessment 474 | Variable: $.result.AssessmentSummaries[0] 475 | Default: DeleteApp 476 | Type: Choice 477 | ImportResourcesToDraftAppVersion: 478 | Choices: 479 | - Next: ImportResourcesToDraftAppVersion (CF) 480 | StringMatches: aws.cloudformation 481 | Variable: $.ArhPayload.ArhAppInputSourceType 482 | - Next: ImportResourcesToDraftAppVersion (TF) 483 | StringMatches: aws.s3 484 | Variable: $.ArhPayload.ArhAppInputSourceType 485 | Type: Choice 486 | ImportResourcesToDraftAppVersion (CF): 487 | Next: Wait for Import 488 | Parameters: 489 | AppArn.$: $.Apps.App.AppArn 490 | SourceArns.$: States.Array($.ArhPayload.ArhAppInputSource) 491 | Resource: arn:aws:states:::aws-sdk:resiliencehub:importResourcesToDraftAppVersion 492 | ResultPath: $.Import 493 | Type: Task 494 | ImportResourcesToDraftAppVersion (TF): 495 | Next: Wait for Import 496 | Parameters: 497 | AppArn.$: $.Apps.App.AppArn 498 | TerraformSources.$: >- 499 | States.Array(States.StringToJson(States.Format('\{"S3StateFileUrl":"{}"\}',$.ArhPayload.ArhAppInputSource))) 500 | Resource: arn:aws:states:::aws-sdk:resiliencehub:importResourcesToDraftAppVersion 501 | ResultPath: $.Import 502 | Type: Task 503 | Input Sources Found: 504 | Choices: 505 | - IsPresent: true 506 | Next: PublishAppVersion 507 | Variable: $.App.Sources.AppInputSources[0].ImportType 508 | Default: ListAppAssessments 509 | OutputPath: $.Apps.AppSummaries[0] 510 | Type: Choice 511 | List App Input Sources: 512 | Next: Input Sources Found 513 | Parameters: 514 | AppArn.$: $.Apps.AppSummaries[0].AppArn 515 | AppVersion: draft 516 | Resource: arn:aws:states:::aws-sdk:resiliencehub:listAppInputSources 517 | ResultPath: $.App.Sources 518 | Type: Task 519 | ListAppAssessments: 520 | Next: If Assessment(s) Found 521 | Parameters: 522 | AppArn.$: $.AppArn 523 | Resource: arn:aws:states:::aws-sdk:resiliencehub:listAppAssessments 524 | ResultPath: $.result 525 | Type: Task 526 | ListApps: 527 | Next: UPDATE/CREATE 528 | Parameters: 529 | Name.$: $.ArhPayload.ArhApplicationName 530 | Resource: arn:aws:states:::aws-sdk:resiliencehub:listApps 531 | ResultPath: $.Apps 532 | Type: Task 533 | ListApps (Delete): 534 | Next: If App Found (Delete) 535 | Parameters: 536 | Name.$: $.ArhPayload.ArhApplicationName 537 | Resource: arn:aws:states:::aws-sdk:resiliencehub:listApps 538 | ResultPath: $.Apps 539 | Type: Task 540 | ProcessEventData: 541 | Next: Create, Update, Delete 542 | Parameters: 543 | FunctionName: !Sub ${ProcessEventDataFunction.Arn}:$LATEST 544 | Payload.$: $ 545 | Resource: arn:aws:states:::lambda:invoke 546 | ResultSelector: 547 | ArhPayload.$: $.Payload 548 | Retry: 549 | - BackoffRate: 2 550 | ErrorEquals: 551 | - Lambda.ServiceException 552 | - Lambda.AWSLambdaException 553 | - Lambda.SdkClientException 554 | - Lambda.TooManyRequestsException 555 | IntervalSeconds: 1 556 | MaxAttempts: 3 557 | Type: Task 558 | PublishAppVersion: 559 | Next: StartAppAssessment 560 | Parameters: 561 | AppArn.$: $.AppArn 562 | Resource: arn:aws:states:::aws-sdk:resiliencehub:publishAppVersion 563 | ResultPath: null 564 | Type: Task 565 | StartAppAssessment: 566 | Next: Wait for Assessment 567 | Parameters: 568 | AppArn.$: $.AppArn 569 | AppVersion: release 570 | AssessmentName.$: States.Format('Assessment-report-{}', States.UUID()) 571 | Resource: arn:aws:states:::aws-sdk:resiliencehub:startAppAssessment 572 | Type: Task 573 | UPDATE/CREATE: 574 | Choices: 575 | - IsPresent: true 576 | Next: UpdateApp 577 | Variable: $.Apps.AppSummaries[0] 578 | Default: CreateApp 579 | Type: Choice 580 | Update Tags: 581 | Next: ImportResourcesToDraftAppVersion 582 | Parameters: 583 | FunctionName: !Sub ${UpdateTagsFunction.Arn}:$LATEST 584 | Payload.$: $ 585 | Resource: arn:aws:states:::lambda:invoke 586 | ResultPath: null 587 | Retry: 588 | - BackoffRate: 2 589 | ErrorEquals: 590 | - Lambda.ServiceException 591 | - Lambda.AWSLambdaException 592 | - Lambda.SdkClientException 593 | - Lambda.TooManyRequestsException 594 | IntervalSeconds: 1 595 | MaxAttempts: 3 596 | Type: Task 597 | UpdateApp: 598 | Next: Update Tags 599 | Parameters: 600 | AppArn.$: $.Apps.AppSummaries[0].AppArn 601 | EventSubscriptions.$: $.ForEachTag.Notify 602 | PolicyArn.$: $.ForEachTag.PolicyArn[0].PolicyArn 603 | Resource: arn:aws:states:::aws-sdk:resiliencehub:updateApp 604 | ResultPath: $.Apps 605 | Type: Task 606 | Wait for Assessment: 607 | Next: DescribeAppAssessment 608 | Seconds: 5 609 | Type: Wait 610 | Wait for Import: 611 | Next: DescribeDraftAppVersionResourcesImportStatus 612 | Seconds: 5 613 | Type: Wait 614 | TimeoutSeconds: 120 615 | Role: !GetAtt ArhCfnStateMachineRole.Arn 616 | 617 | ProcessEventDataFunctionRole: 618 | Type: AWS::IAM::Role 619 | Properties: 620 | AssumeRolePolicyDocument: 621 | Version: "2012-10-17" 622 | Statement: 623 | - Effect: Allow 624 | Principal: 625 | Service: 626 | - lambda.amazonaws.com 627 | Action: 628 | - "sts:AssumeRole" 629 | ManagedPolicyArns: 630 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 631 | Policies: 632 | - PolicyName: AllowDescribeStacks 633 | PolicyDocument: 634 | Version: "2012-10-17" 635 | Statement: 636 | - Effect: Allow 637 | Action: 638 | - "cloudformation:DescribeStacks" 639 | Resource: 640 | - !Sub "arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:*" 641 | - PolicyName: AllowReadS3Tags 642 | PolicyDocument: 643 | Version: "2012-10-17" 644 | Statement: 645 | - Effect: Allow 646 | Action: 647 | - "s3:ListBucket" 648 | Resource: 649 | - !Sub "arn:${AWS::Partition}:s3:::${TerraformS3Bucket}" 650 | - Effect: Allow 651 | Action: 652 | - "s3:GetObjectTagging" 653 | Resource: 654 | - !Sub "arn:${AWS::Partition}:s3:::${TerraformS3Bucket}/*" 655 | 656 | UpdateTagsFunctionRole: 657 | Type: AWS::IAM::Role 658 | Properties: 659 | AssumeRolePolicyDocument: 660 | Version: "2012-10-17" 661 | Statement: 662 | - Effect: Allow 663 | Principal: 664 | Service: 665 | - lambda.amazonaws.com 666 | Action: 667 | - "sts:AssumeRole" 668 | ManagedPolicyArns: 669 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 670 | Policies: 671 | - PolicyName: AllowResilienceHubTagging 672 | PolicyDocument: 673 | Version: "2012-10-17" 674 | Statement: 675 | - Effect: Allow 676 | Action: 677 | - "resiliencehub:ListTagsForResource" 678 | - "resiliencehub:TagResource" 679 | - "resiliencehub:UntagResource" 680 | Resource: 681 | - !Sub "arn:${AWS::Partition}:resiliencehub:${AWS::Region}:${AWS::AccountId}:*" 682 | 683 | ProcessEventDataFunction: 684 | Type: AWS::Lambda::Function 685 | Metadata: 686 | cfn_nag: 687 | rules_to_suppress: 688 | - id: W89 689 | reason: "This function does not need to communicate with any resources within a customer VPC - only AWS services" 690 | Properties: 691 | Role: !GetAtt ProcessEventDataFunctionRole.Arn 692 | Environment: 693 | Variables: 694 | SNS_TOPIC: blank 695 | Code: 696 | ZipFile: | 697 | import json 698 | import boto3 699 | 700 | s3_client = boto3.client("s3") 701 | cfn_client = boto3.client("cloudformation") 702 | def lambda_handler(event, context): 703 | event_source = event["source"] 704 | arhAppName = None 705 | resourcePath = "" 706 | arhAppOperation="" 707 | arh_app_tags = [] 708 | if event_source == "aws.s3": 709 | try: 710 | bucket_name = event["detail"]["bucket"]["name"] 711 | object_key = event["detail"]["object"]["key"] 712 | resourcePath = 's3://'+bucket_name+'/'+object_key 713 | if event["detail"]["reason"] == 'DeleteObject': 714 | arhAppOperation = 'DELETE_COMPLETE' 715 | elif event["detail"]["reason"] == 'PutObject': 716 | arhAppOperation = 'UPDATE_COMPLETE' 717 | 718 | #get the tags for the state file 719 | tags = s3_client.get_object_tagging( 720 | Bucket = bucket_name, 721 | Key = object_key 722 | ) 723 | arh_app_tags = tags["TagSet"] 724 | except s3_client.exceptions.NoSuchKey as e: 725 | print("The error is: ",e) 726 | elif event_source == "aws.cloudformation" : 727 | try: 728 | cfn_client.describe_stacks(StackName=event["detail"]["stack-id"]) 729 | cfn_stack_id = event["detail"]["stack-id"] 730 | cfn_client_response = cfn_client.describe_stacks( 731 | StackName = cfn_stack_id 732 | ) 733 | 734 | if ("Stacks" in cfn_client_response and len(cfn_client_response["Stacks"]) > 0): 735 | arhAppOperation = cfn_client_response["Stacks"][0]["StackStatus"] 736 | arh_app_tags = cfn_client_response["Stacks"][0]["Tags"] 737 | resourcePath = cfn_client_response["Stacks"][0]["StackId"] 738 | except Exception as e: 739 | print("The error is: ",e) 740 | try: 741 | arhAppNameTemp = [tag['Value'] for tag in arh_app_tags if tag['Key'] == 'app_name'] 742 | if len(arhAppNameTemp) == 1: 743 | arhAppName = arhAppNameTemp[0] 744 | except KeyError as e: 745 | print("The error is: ",e) 746 | return { 747 | 'statusCode': 200, 748 | 'ArhApplicationName': arhAppName, 749 | 'ArhAppInputSourceType': event_source, 750 | 'ArhAppInputSource': resourcePath, 751 | 'SourceEventReceived': arhAppOperation, 752 | 'Tags': arh_app_tags 753 | } 754 | Handler: index.lambda_handler 755 | Runtime: python3.12 756 | ReservedConcurrentExecutions: 2 757 | TracingConfig: 758 | Mode: Active 759 | 760 | UpdateTagsFunction: 761 | Type: AWS::Lambda::Function 762 | Metadata: 763 | cfn_nag: 764 | rules_to_suppress: 765 | - id: W89 766 | reason: "This function does not need to communicate with any resources within a customer VPC - only AWS services" 767 | Properties: 768 | Role: !GetAtt UpdateTagsFunctionRole.Arn 769 | Environment: 770 | Variables: 771 | SNS_TOPIC: blank 772 | Code: 773 | ZipFile: | 774 | import boto3 775 | import json 776 | 777 | arh = boto3.client("resiliencehub") 778 | 779 | def lambda_handler(event, context): 780 | # Get ARH app arn from event 781 | app_arn = event["Apps"]["App"]["AppArn"] 782 | # Get list of tags from the event, reformat into key:value 783 | tags = {} 784 | if "Tags" in event["ArhPayload"] and len(event["ArhPayload"]["Tags"]) > 0: 785 | for t in event["ArhPayload"]["Tags"]: 786 | key = t["Key"] 787 | value = t["Value"] 788 | tags[key] = value 789 | try: 790 | # Check for existing tags, remove if present 791 | existing_tags = arh.list_tags_for_resource(resourceArn=app_arn) 792 | if len(existing_tags['tags']) > 0: 793 | arh.untag_resource(resourceArn=app_arn, tagKeys=list(existing_tags['tags'].keys())) 794 | 795 | # Don't update tags if none were listed in the event 796 | if len(tags) > 0: 797 | update_tags = arh.tag_resource(resourceArn=app_arn, tags=tags) 798 | except Exception as e: 799 | print("The error is: ",e) 800 | 801 | return { 802 | 'statusCode': 200, 803 | 'body': "Current tags: {}".format(json.dumps(tags)) 804 | } 805 | Handler: index.lambda_handler 806 | Runtime: python3.11 807 | ReservedConcurrentExecutions: 2 808 | TracingConfig: 809 | Mode: Active -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/eventbridge-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/eventbridge-on.png -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/res-policies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/res-policies.png -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/step-functions-workflow-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/step-functions-workflow-simple.png -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/step-functions-workflow-with-tf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/step-functions-workflow-with-tf.png -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/step-functions-workflow-with-tf.png.bkp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/step-functions-workflow-with-tf.png.bkp -------------------------------------------------------------------------------- /inputsource-eventbridge-integration/step-functions-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/inputsource-eventbridge-integration/step-functions-workflow.png -------------------------------------------------------------------------------- /jenkins-pipeline-integration/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { label "main" } 3 | environment { 4 | AWS_DEFAULT_REGION="##your AWS region##" 5 | APP_ARN="#Resilienve Hub Application ARN##" 6 | STACK_ARN="##CloudFormation Stack ARN##" 7 | } 8 | stages { 9 | /* 10 | * Perform CloudFormation deployment and any other prerequisites as a part of your Infrastucture Pipeline first. 11 | * And then add the 'Assessment' stage below. 12 | */ 13 | stage('Assessment') { 14 | steps { 15 | withCredentials([aws(accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: '##CREDENTIAL_ID##', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY')]) { 16 | script { 17 | //import resources to Resilience Hub application draft version 18 | importDetails = sh ( 19 | script: ''' 20 | aws resiliencehub import-resources-to-draft-app-version --app-arn ${APP_ARN} --source-arns ${STACK_ARN} 21 | ''', 22 | returnStdout: true 23 | ) 24 | 25 | //Wait for import to complete 26 | importStatus = sh ( 27 | script: ''' 28 | aws resiliencehub describe-draft-app-version-resources-import-status --app-arn ${APP_ARN} 29 | ''', 30 | returnStdout: true 31 | ) 32 | def importObj = readJSON text: importStatus 33 | echo "Waiting for the import to complete: current status - ${importObj['status']}" 34 | 35 | while (importObj['status'] in ['Pending','InProgress']){ 36 | sleep(5) 37 | importStatus = sh ( 38 | script: ''' 39 | aws resiliencehub describe-draft-app-version-resources-import-status --app-arn ${APP_ARN} 40 | ''', 41 | returnStdout: true 42 | ) 43 | importObj = readJSON text: importStatus 44 | } 45 | 46 | if(importObj['status'] == 'Failed'){ 47 | currentBuild.result = "FAILURE" 48 | throw new Exception("Resources import failed -${importObj['status']}") 49 | } 50 | 51 | //Publish a new app version 52 | publishApp = sh ( 53 | script: ''' 54 | aws resiliencehub publish-app-version --app-arn ${APP_ARN} 55 | ''', 56 | returnStdout: true 57 | ) 58 | 59 | 60 | //start the app assessment 61 | assessmentResults = sh ( 62 | script: ''' 63 | aws resiliencehub start-app-assessment --app-arn ${APP_ARN} --app-version release --assessment-name jenkins-pipeline-assessment 64 | ''', 65 | returnStdout: true 66 | ) 67 | def assessmentObj = readJSON text: assessmentResults 68 | def assessmentDetails = assessmentObj['assessment'] 69 | env.assessmentArn = assessmentDetails['assessmentArn'] 70 | def assessmentStatus = assessmentDetails['assessmentStatus'] 71 | 72 | // Wait until the assessment is complete 73 | echo "Waiting for the assessment to complete: current status - ${assessmentStatus}" 74 | while (assessmentStatus in ['Pending','InProgress']){ 75 | sleep(5) 76 | assessmentResults = sh ( 77 | script: ''' 78 | aws resiliencehub describe-app-assessment --assessment-arn ${assessmentArn} 79 | ''', 80 | returnStdout: true 81 | ) 82 | assessmentObj = readJSON text: assessmentResults 83 | assessmentDetails = assessmentObj['assessment'] 84 | assessmentStatus = assessmentDetails['assessmentStatus'] 85 | } 86 | echo "assessment completed status: ${assessmentStatus}" 87 | 88 | //Fail the build if the assessment status is failed 89 | if (assessmentStatus == 'Failed'){ 90 | currentBuild.result = "FAILURE" 91 | throw new Exception("Assessment failed -${assessmentStatus}") 92 | } 93 | //Fail the build if the compliance status is breached 94 | if (assessmentDetails['complianceStatus'] == 'PolicyBreached'){ 95 | currentBuild.result = "FAILURE" 96 | throw new Exception("Resiliency policy breached") 97 | } 98 | else 99 | echo "Resiliency policy met" 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /jenkins-pipeline-integration/README.md: -------------------------------------------------------------------------------- 1 | # Integrate AWS Resilience Hub with Jenkins Pipelines 2 | 3 | Achieve continual resilience by integrating resilience checks into your Continuous Integration/Continuoys Delivery (CI/CD) pipelines. This solution describes how you can integrate AWS Resilience Hub with your CI/CD pipeline if you are using [Jenkins Pipeline](https://www.jenkins.io/doc/book/pipeline/) and follows a process similar to what is described in [this blog post](https://aws.amazon.com/blogs/architecture/continually-assessing-application-resilience-with-aws-resilience-hub-and-aws-codepipeline/). 4 | 5 | ## Prerequisites 6 | 7 | **A. Create an AWS user account for Jenkins with the appropriate permissions** 8 | 1. Sign in to AWS Management console and open the IAM console. 9 | 2. Select **Policies** in the navigation pane and choose **Create policy**. 10 | 3. Select the **JSON** tab. 11 | 4. Replace the example placeholder with the permissions provided in the [Resilience Hub user guide](https://docs.aws.amazon.com/resilience-hub/latest/userguide/security-iam-resilience-hub-permissions.html#security-iam-resilience-hub-same-account). 12 | 5. Choose **Next:Tags**. Followed by **Next:Review**. 13 | 6. On the **Review policy** page, for Name, enter a name for the policy, such as **JenkinsResHubPolicy**. 14 | 7. Choose **Create policy**. 15 | 8. In the navigation pane, choose **Users** and then choose **Add users**. 16 | 9. In the **Set user details** section, specify a user name (for example, JenkinsUser). 17 | 10. In the **Select AWS access type** section, choose **Access key - Programmatic access**. 18 | 11. Choose **Next:Permissions**. 19 | 12. In the **Set permissions** for section, choose **Attach existing policies directly**. 20 | 13. In the filter field, enter the name of the policy you created earlier. 21 | 14. Select the check box next to the policy, and then choose **Next: Tags** followed by **Next:Review**. 22 | 15. Verify the details, and then choose **Create user**. 23 | 16. Save the access and secret keys. You will use these credentials in the next step. 24 | 25 | **B. Install and configure AWS CLI and CloudBees AWS Credentials Plugin on the Jenkins Agent** 26 | 1. Install AWS CLI on the Jenkins agent following the [instructions](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). 27 | 2. Install the [aws-credentials](https://plugins.jenkins.io/aws-credentials/) plugin on the Jenkins agent. 28 | 3. Choose **Manage Jenkins**, then **Manage Credentials** and followed by **Add Credentials**. 29 | 4. Select **AWS Credentials**. 30 | 5. Specify an ID and then add the credentials obtained in Step A. Save the credential ID. You will use this in the pipeline script. 31 | 32 | ## Deployment 33 | Update your pipeline scripts as per the example provided in [Jenkinsfile](./Jenkinsfile). Specifically, add the **assessment** stage into the script for your pipelines. Ensure that you have updated the following placeholders: 34 | * **AWS_DEFAULT_REGION** - specify the AWS Region where the application exists on Resilience Hub 35 | * **STACK_ARN** - specify the ARN of the AWS CloudFormation stack that you are updating as a part of your deployment 36 | * **APP_ARN** - specify the ARN of the application you have defined within Resilience Hub 37 | * **CREDENTIAL_ID** - specify the credential id you added in Step B 5 above. 38 | 39 | Note: Install the [Pipeline Utility Steps](https://www.jenkins.io/doc/pipeline/steps/pipeline-utility-steps/) plugin if you do not already have that. 40 | -------------------------------------------------------------------------------- /policy-bulk-update/README.md: -------------------------------------------------------------------------------- 1 | # arh-bulk-policy-update 2 | 3 | 4 | 5 | ## Use Case Details 6 | 7 | Adding a capability of bulk policy update to multiple Resilience Hub applications. For example, the customer would like to assign same policy XYZ to 20 applications already onboarded in Resilience Hub. 8 | The script takes the below as input: 9 | 1. ARN of Resilience Hub policy 10 | 2. List of target app ARNs with following alternatives: 11 | a. Array of script parameters 12 | b. Filename with app ARNs per line 13 | 3. Optional parameter: --assessmentSchedule with valid values Disabled | Daily 14 | 15 | Script will call Resilience Hub API [UpdateApp](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_UpdateApp.html) to update policy of every given Resilience Hub application and optionally turn on/off daily assessments. -------------------------------------------------------------------------------- /policy-bulk-update/bulk-arh-policy-update.py: -------------------------------------------------------------------------------- 1 | ''' Bulk policy update to multiple Resilience Hub applications 2 | Inputs: 3 | -p, --policy-arn: ARN of the Resilience Hub Policy 4 | -a, --target-app-arns: Target ARH App ARNs seperated by comma (,) OR 5 | -f, --target-app-file: File containing the ARH App ARNs - one per line 6 | -s, --schedule: Schedule for the policy 7 | -v, --verbose: Verbose output 8 | ''' 9 | 10 | from pprint import pprint 11 | import argparse 12 | 13 | import boto3 14 | 15 | 16 | def update_policy_for_an_app(arh, policy_arn, app_arn, schedule, verbose): 17 | if verbose: 18 | print(f"Updating policy {policy_arn} for {app_arn}") 19 | try: 20 | if schedule is not None: 21 | response = arh.update_app( 22 | policyArn=policy_arn, 23 | appArn=app_arn, 24 | assessmentSchedule=schedule 25 | ) 26 | else: 27 | response = arh.update_app( 28 | policyArn=policy_arn, 29 | appArn=app_arn 30 | ) 31 | except: 32 | print(f"Error updating policy {policy_arn} for {app_arn}") 33 | raise 34 | 35 | if verbose: 36 | pprint(response) 37 | 38 | def update_policy_for_apps (arh, policy_arn, app_arns, schedule, verbose): 39 | for app_arn in app_arns.split(','): 40 | update_policy_for_an_app(arh, policy_arn, app_arn, schedule, verbose) 41 | 42 | def update_policy_for_apps_from_file(arh, policy_arn, file_name, schedule, verbose): 43 | with open(file_name) as f: 44 | for app_arn in f: 45 | update_policy_for_an_app(arh, policy_arn, app_arn.strip(), schedule, verbose) 46 | 47 | if __name__ == "__main__": 48 | parser = argparse.ArgumentParser(description="Bulk policy update script for AWS Resilience Hub") 49 | parser.add_argument("-p", "--policy-arn", type=str, 50 | help="ARN of the Resilience Hub Policy", required=True) 51 | 52 | # You need either the list of app ARNs or the file containing the list of app ARNs 53 | group = parser.add_mutually_exclusive_group(required=True) 54 | group.add_argument("-a", "--target-app-arns", type=str, help="Target ARH App ARNs seperated by comma") 55 | group.add_argument("-f", "--target-app-file", type=str, help="File containing the ARH App ARNs - one per line") 56 | 57 | parser.add_argument("-s", "--schedule", type=str, 58 | help="Assesmant Schedule - valid values Disabled|Daily",choices=['Disabled', 'Daily'],) 59 | parser.add_argument("-v", "--verbose", action="store_true", help="verbose mode") 60 | args = parser.parse_args() 61 | 62 | # Parse the ARN of the policy to get the AWS region 63 | region = args.policy_arn.split(':')[3] 64 | if args.verbose: 65 | print(f"AWS Region {region}") 66 | client = boto3.client('resiliencehub',region) 67 | 68 | if args.target_app_arns is not None: 69 | update_policy_for_apps(client, args.policy_arn, args.target_app_arns, 70 | args.schedule, args.verbose) 71 | elif args.target_app_file is not None: 72 | update_policy_for_apps_from_file(client, args.policy_arn, args.target_app_file, 73 | args.schedule, args.verbose) 74 | print("Policy update completed") 75 | -------------------------------------------------------------------------------- /policy-bulk-update/pylint.config: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook='import sys; sys.path.append(".")' 3 | [MESSAGES CONTROL] 4 | # C0114: Missing module docstring (missing-module-docstring) 5 | # C0116: Missing function or method docstring (missing-function-docstring) 6 | # C0301: Line too long (line-too-long) 7 | # E0401: Unable to import 'module' (import-error) 8 | # R0801: Similar lines in 2 files 9 | disable=C0114,C0116,C0301,E0401,R0801 -------------------------------------------------------------------------------- /resilience-hub-application-iam-roles/AWSResilienceHubCrossAccountAssessmentRole.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Parameters: 3 | RoleName: 4 | Type: String 5 | Default: AWSResilienceHubCrossAccountAssessmentRole 6 | Description: The role name for a cross account IAM role used in the AWS Resilience Hub application. 7 | AWSResilienceHubPrimaryAccountAssessmentRoleARN: 8 | Type: String 9 | Description: The IAM role ARN used for the AWS Resilience Hub application in your primary account. 10 | Resources: 11 | AWSResilienceHubAssessmentRole: 12 | Type: "AWS::IAM::Role" 13 | Properties: 14 | RoleName: !Ref RoleName 15 | AssumeRolePolicyDocument: 16 | Version: "2012-10-17" 17 | Statement: 18 | - Effect: "Allow" 19 | Principal: 20 | AWS: 21 | - !Ref AWSResilienceHubPrimaryAccountAssessmentRoleARN 22 | Action: 23 | - "sts:AssumeRole" 24 | ManagedPolicyArns: 25 | - arn:aws:iam::aws:policy/AWSResilienceHubAsssessmentExecutionPolicy -------------------------------------------------------------------------------- /resilience-hub-application-iam-roles/AWSResilienceHubPrimaryAccountAssessmentRole.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Parameters: 3 | RoleName: 4 | Type: String 5 | Default: AWSResilienceHubAssessmentRole 6 | Description: The AWS Resilience Hub application IAM role name 7 | CrossAccountIAMRoleARNs: 8 | Type: List 9 | Default: "" 10 | Description: Enter a comma separated list of cross account IAM role ARNs, if your AWS Resilience Hub application resources are deployed in multiple infrastructure accounts. 11 | AWSResilienceHubCrossAccountAssumeRolePolicyName: 12 | Type: String 13 | Default: AWSResilienceHubCrossAccountAssumeRolePolicy 14 | Description: The name of the inline policy which will be created, allowing the AWS Resilience Hub primary account role to assume cross account IAM role ARNs. 15 | Conditions: 16 | IsCrossAccountIAMRoleListEmpty: !Equals 17 | - !Join [ "", !Ref CrossAccountIAMRoleARNs ] 18 | - "" 19 | IsCrossAccountIAMRoleListNotEmpty: 20 | !Not [ Condition: IsCrossAccountIAMRoleListEmpty ] 21 | Resources: 22 | AWSResilienceHubAssessmentRole: 23 | Type: "AWS::IAM::Role" 24 | Properties: 25 | RoleName: !Ref RoleName 26 | AssumeRolePolicyDocument: 27 | Version: "2012-10-17" 28 | Statement: 29 | - Effect: "Allow" 30 | Principal: 31 | Service: 32 | - "resiliencehub.amazonaws.com" 33 | Action: 34 | - "sts:AssumeRole" 35 | ManagedPolicyArns: 36 | - arn:aws:iam::aws:policy/AWSResilienceHubAsssessmentExecutionPolicy 37 | 38 | 39 | AWSResilienceHubCrossAccountAssumeRolePolicy: 40 | Type: "AWS::IAM::Policy" 41 | Condition: IsCrossAccountIAMRoleListNotEmpty 42 | Properties: 43 | PolicyName: !Ref AWSResilienceHubCrossAccountAssumeRolePolicyName 44 | PolicyDocument: 45 | Version: "2012-10-17" 46 | Statement: 47 | - Effect: "Allow" 48 | Action: "sts:AssumeRole" 49 | Resource: !Ref CrossAccountIAMRoleARNs 50 | Roles: 51 | - !Ref AWSResilienceHubAssessmentRole 52 | -------------------------------------------------------------------------------- /resilience-hub-application-iam-roles/README.md: -------------------------------------------------------------------------------- 1 | # IAM role template for AWS Resilience Hub applications 2 | 3 | Creates [AWS Identity and Access Management (IAM) roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) using AWS CloudFormation. 4 | 5 | These roles can be passed to AWS Resilience Hub as part of creating an application, and will be used by AWS Resilience Hub when assessing your AWS resources. 6 | For further documentation on how to create AWS Resilience Hub applications using custom IAM roles, see [Setup IAM roles and permissions](https://docs.aws.amazon.com/resilience-hub/latest/userguide/security_iam_service-with-iam.html#setting-up-permissions). 7 | 8 | ## Deployment 9 | 10 | Using this template, you can create an IAM role in a single AWS account by creating a [CloudFormation stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html), or deploy it to multiple accounts using [CloudFormation StackSets](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html). 11 | 12 | * The [AWSResilienceHubPrimaryAccountAssessmentRole.yaml](AWSResilienceHubPrimaryAccountAssessmentRole.yaml) role needs to be created once per primary AWS account (the account used for creating Resilience Hub applications). 13 | 14 | * (Optional) If your AWS Resilience Hub application, is composed of resources across multiple accounts (cross accounts) the [AWSResilienceHubCrossAccountAssessmentRole.yaml](AWSResilienceHubCrossAccountAssessmentRole.yaml) role needs to be deployed on every cross account (application infrastructure accounts which contain resources that are part of the AWS Resilience Hub application). 15 | 16 | 17 | ## Resources 18 | 19 | Each of the provided templates create a single resource of type [AWS::IAM::Role](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html). 20 | 21 | 1) **AWSResilienceHubPrimaryAccountAssessmentRole**: The trust policy of this role is minimally scoped so that Resilience Hub is the only principal that can assume it. For more information on the permissions associated with this role, please refer to [this documentation](https://docs.aws.amazon.com/resilience-hub/latest/userguide/security-iam-resilience-hub-permissions.html#security-iam-resilience-hub-primary-account). 22 | The following policies are attached to this role: 23 | * **AWSResilienceHubAsssessmentExecutionPolicy**. This policy contains all read-only permissions required by AWS Resilience Hub to assess the resiliency of your application. 24 | * (Optional) **AWSResilienceHubCrossAccountAssumeRolePolicy**. This IAM policy contains sts:AssumeRole permissions for all cross account IAM roles. If your application is composed of cross account resources, AWS Resilience Hub uses the primary role 25 | to assume any cross account IAM roles, to access resources in cross accounts. If created, this policy will be applied to the created IAM role. 26 | 27 | The AWSResilienceHubPrimaryAccountAssessmentRole.yaml accepts the following parameters: 28 | 1) RoleName (required) - The name of the IAM role which will be created in your primary account, and can be passed when creating AWS Resilience Hub applications. 29 | 2) CrossAccountIAMRoleARNs (optional) - A comma seperated list of cross account IAM roles which will be used by AWS Resilience Hub to access cross account resources. 30 | 3) AWSResilienceHubCrossAccountAssumeRolePolicyName (optional) - The name of the policy which will be created to allow the primary IAM role to assume any cross account IAM roles. 31 | 32 | 1) **AWSResilienceHubCrossAccountAssessmentRole**: The trust policy of this role is minimally scoped so that only the primary IAM role created is allowed to assume the role. For more information on the permissions associated with this role, please refer to [this documentation](https://docs.aws.amazon.com/resilience-hub/latest/userguide/security-iam-resilience-hub-permissions.html#security-iam-resilience-hub-primary-account). 33 | The following policies are attached to this role: 34 | * **AWSResilienceHubAsssessmentExecutionPolicy**. This policy contains all read-only permissions required by AWS Resilience Hub to assess the resiliency of your application. 35 | 36 | The AWSResilienceHubPrimaryAccountAssessmentRole.yaml accepts the following parameters: 37 | 1) RoleName (required) - The name of the IAM role which will be created in your cross account, and can be passed when creating AWS Resilience Hub applications. 38 | 2) AWSResilienceHubPrimaryAccountAssessmentRoleARN (required) - The primary account IAM role ARN. The trust policy of the created role, will allow the primary IAM role previously created, to assume this cross account role. -------------------------------------------------------------------------------- /resilience-hub-csv-export/README.md: -------------------------------------------------------------------------------- 1 | # Export AWS Resilience Hub assessment results into a CSV file 2 | 3 | Export assessment results for all applications defined within AWS Resilience Hub in an account into a CSV file. This CSV file can then be used to perform analytics and reporting. The solution can be expanded to include results from other accounts as well. 4 | 5 | ## Execution 6 | 7 | Run [csv-generator.py](./csv-generator.py) in an environment that includes permissions to make API calls to Resilience Hub. The environment also needs to have the [AWS SDK for Python (Boto3)](https://aws.amazon.com/sdk-for-python/) installed. 8 | 9 | ``` 10 | python csv-generator.py 11 | ``` 12 | 13 | ## Output 14 | 15 | The script will generate a CSV file that contains details of all assessments for all applications defined within Resilience Hub in a single region. The CSV file will be stored in the same location as where the script was executed and contains the following information: 16 | 17 | * **Application Name** - the name of the application as defined within Resilience Hub 18 | * **Assessment Name** - the name of an assessment for an application 19 | * **Assessment Compliance Status** - indicates if the associated assessment was evaluated as being compliant with the resiliency policy 20 | * **End Time** - identifies the end time of an assessment 21 | * **Estimated Overall RTO** - indicates the estimated achievable Recovery Time Objective (RTO) at the time of assessment (the maximum value across all disruption types is selected) 22 | * **Estimated Overall RPO** - indicates the estimated achievable Recovery Point Objective (RPO) at the time of assessment (the maximum value across all disruption types is selected) 23 | * **Target RTO** - indicates the targeted RTO based on the associated resiliency policy (the minimum value across all disruption types is selected) 24 | * **Target RPO** - indicates the targeted RPO based on the associated resiliency policy (the minimum value across all disruption types is selected) 25 | * **Application Compliance Status** - indicates if the application (based on the latest assessment) was evaluated as being compliant with the resiliency policy 26 | * **Resiliency Score** - the [resiliency score](https://docs.aws.amazon.com/resilience-hub/latest/userguide/resil-score.html) of the application 27 | * **Last Assessed** - timestamp of the most recent assessment 28 | * **Application Tier** - application tier as defined by the resiliency policy associated with the application 29 | * **Region** - the AWS Region where the application is defined in Resilience Hub 30 | 31 | You can see a sample report here - [sample-resilience-report.csv](./sample-resilience-report.csv) 32 | -------------------------------------------------------------------------------- /resilience-hub-csv-export/csv-generator.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import csv 3 | 4 | # Get list of regions 5 | ec2 = boto3.client('ec2') 6 | regions = [] 7 | region_details = ec2.describe_regions()['Regions'] 8 | for region_detail in region_details: 9 | regions.append(region_detail['RegionName']) 10 | 11 | # Generate a new CSV file and populate headers 12 | with open('resilience-report.csv', 'w', newline='') as file: 13 | writer = csv.writer(file) 14 | writer.writerow(["Application Name", "Assessment Name", "Assessment Compliance Status", "End Time", "Estimated Overall RTO", "Estimated Overall RPO", "Target RTO", "Target RPO", "Application Compliance Status", "Resiliency Score", "Last Assessed", "Application Tier", "Region"]) 15 | 16 | # Loop over each region 17 | for region in regions: 18 | try: 19 | arh = boto3.client('resiliencehub', region_name=region) 20 | apps = arh.list_apps() 21 | except: 22 | continue 23 | 24 | # Get list of apps in a region 25 | app_arns = [] 26 | for app in apps['appSummaries']: 27 | app_arns.append(app['appArn']) 28 | while 'NextToken' in apps: 29 | apps = arh.list_apps(NextToken = apps['NextToken']) 30 | for app in apps['appSummaries']: 31 | app_arns.append(app['appArn']) 32 | 33 | # Loop over list of apps to retrieve details 34 | for app in app_arns: 35 | app_details = arh.describe_app(appArn=app)['app'] 36 | app_name = app_details['name'] 37 | app_compliance = app_details['complianceStatus'] 38 | app_res_score = app_details['resiliencyScore'] 39 | app_tier = 'unknown' 40 | 41 | # Check if a resiliency policy is associated with the application 42 | if 'policyArn' in app_details: 43 | app_res_policy = app_details['policyArn'] 44 | policy_details = arh.describe_resiliency_policy( 45 | policyArn=app_res_policy 46 | ) 47 | app_tier = policy_details['policy']['tier'] 48 | 49 | # Check if an application has been assessed 50 | if app_compliance == 'NotAssessed': 51 | with open('resilience-report.csv', 'a', newline='') as file: 52 | writer = csv.writer(file) 53 | writer.writerow([app_name, '', 'NotAssessed', '', '', '', '', '', app_compliance, app_res_score, '', app_tier, region]) 54 | continue 55 | 56 | app_last_assessed = app_details['lastAppComplianceEvaluationTime'] 57 | 58 | # Get list of assessments for the application 59 | assessment_summaries = arh.list_app_assessments(appArn=app)['assessmentSummaries'] 60 | 61 | while 'NextToken' in assessment_summaries: 62 | assessment_summaries.append(arh.list_app_assessments(appArn=app, NextToken = assessment_summaries['NextToken'])['assessmentSummaries']) 63 | 64 | # Loop over list of assessments to get details 65 | for assessment in assessment_summaries: 66 | assessment_arn = assessment['assessmentArn'] 67 | assessment_status = assessment['assessmentStatus'] 68 | 69 | # Get assessment details if it is a successful assessment 70 | if assessment_status == 'Success': 71 | assessment_details = arh.describe_app_assessment(assessmentArn=assessment_arn) 72 | 73 | if assessment_details['assessment']['compliance'] == {}: 74 | continue 75 | 76 | assessment_name = assessment_details['assessment']['assessmentName'] 77 | assessment_compliance_status = assessment_details['assessment']['complianceStatus'] 78 | end_time = assessment_details['assessment']['endTime'] 79 | current_rto_az = assessment_details['assessment']['compliance']['AZ']['currentRtoInSecs'] 80 | current_rto_hardware = assessment_details['assessment']['compliance']['Hardware']['currentRtoInSecs'] 81 | current_rto_software = assessment_details['assessment']['compliance']['Software']['currentRtoInSecs'] 82 | current_rpo_az = assessment_details['assessment']['compliance']['AZ']['currentRpoInSecs'] 83 | current_rpo_hardware = assessment_details['assessment']['compliance']['Hardware']['currentRpoInSecs'] 84 | current_rpo_software = assessment_details['assessment']['compliance']['Software']['currentRpoInSecs'] 85 | target_rto_az = assessment_details['assessment']['policy']['policy']['AZ']['rtoInSecs'] 86 | target_rto_hardware = assessment_details['assessment']['policy']['policy']['Hardware']['rtoInSecs'] 87 | target_rto_software = assessment_details['assessment']['policy']['policy']['Software']['rtoInSecs'] 88 | target_rpo_az = assessment_details['assessment']['policy']['policy']['AZ']['rpoInSecs'] 89 | target_rpo_hardware = assessment_details['assessment']['policy']['policy']['Hardware']['rpoInSecs'] 90 | target_rpo_software = assessment_details['assessment']['policy']['policy']['Software']['rpoInSecs'] 91 | 92 | # Aggregate RTO and RPO values for current and target 93 | current_rto = max(current_rto_az, current_rto_hardware, current_rto_software) 94 | current_rpo = max(current_rpo_az, current_rpo_hardware, current_rpo_software) 95 | target_rto = min(target_rto_az, target_rto_hardware, target_rto_software) 96 | target_rpo = min(target_rpo_az, target_rpo_hardware, target_rpo_software) 97 | 98 | # Check if application is multi-region and updated aggregates accordingly 99 | if 'Region' in assessment_details['assessment']['policy']['policy']: 100 | current_rto_region = assessment_details['assessment']['compliance']['Region']['currentRtoInSecs'] 101 | current_rpo_region = assessment_details['assessment']['compliance']['Region']['currentRpoInSecs'] 102 | target_rto_region = assessment_details['assessment']['policy']['policy']['Region']['rtoInSecs'] 103 | target_rpo_region = assessment_details['assessment']['policy']['policy']['Region']['rpoInSecs'] 104 | 105 | if current_rto < current_rto_region: 106 | current_rto = current_rto_region 107 | if current_rpo < current_rpo_region: 108 | current_rpo = current_rpo_region 109 | if target_rto > target_rto_region: 110 | target_rto = target_rto_region 111 | if target_rpo > target_rpo_region: 112 | target_rpo = target_rpo_region 113 | 114 | # Populate data into the CSV file 115 | with open('resilience-report.csv', 'a', newline='') as file: 116 | writer = csv.writer(file) 117 | writer.writerow([app_name, assessment_name, assessment_compliance_status, end_time.strftime("%Y-%m-%d %H:%M:%S"), current_rto, current_rpo, target_rto, target_rpo, app_compliance, app_res_score, app_last_assessed.strftime("%Y-%m-%d %H:%M:%S"), app_tier, region]) 118 | -------------------------------------------------------------------------------- /resilience-hub-csv-export/sample-resilience-report.csv: -------------------------------------------------------------------------------- 1 | Application Name,Assessment Name,Assessment Compliance Status,End Time,Overall RTO,Overall RPO,Target RTO,Target RPO,Application Compliance Status,Resiliency Score,Last Assessed,Application Tier,Region 2 | ecs-test-multi-region,run-1,PolicyBreached,10/21/22 15:12,2592631,0,30,30,PolicyBreached,0,10/21/22 15:12,MissionCritical,us-east-1 3 | github-actions,GitHub-actions-assessment,PolicyMet,2/3/23 16:45,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 4 | github-actions,GitHub-actions-assessment,PolicyBreached,2/3/23 16:47,2592001,2592001,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 5 | github-actions,GitHub-actions-assessment,PolicyBreached,2/3/23 16:52,2592001,2592001,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 6 | github-actions,GitHub-actions-assessment,PolicyMet,2/3/23 16:54,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 7 | github-actions,GitHub-actions-assessment,PolicyMet,3/16/23 19:58,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 8 | github-actions,GitHub-actions-assessment,PolicyBreached,3/20/23 18:12,2592001,2592001,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 9 | github-actions,GitHub-actions-assessment,PolicyMet,3/20/23 18:14,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 10 | myServerlessApp,run-1,PolicyBreached,9/19/22 19:40,5187002,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 11 | myServerlessApp,run-2,PolicyBreached,9/19/22 19:52,2592131,300,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 12 | myServerlessApp,run-3,PolicyBreached,9/19/22 20:57,2592131,300,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 13 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 4:23,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 14 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 4:25,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 15 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 14:16,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 16 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 14:20,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 17 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 14:46,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 18 | myServerlessApp,GitHub-actions-assessment,PolicyMet,11/3/22 14:52,4800,300,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 19 | myServerlessApp,Assessment-report-pa477a8i8h,PolicyBreached,12/5/22 16:28,7783803,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 20 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,1/3/23 22:53,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 21 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 15:56,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 22 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:02,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 23 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:23,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 24 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:25,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 25 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:31,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 26 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:34,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 27 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:37,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 28 | myWebApp,run-1,PolicyBreached,9/18/22 23:56,5187302,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 29 | myWebApp,Assessment-report-ye5v4j1biys,PolicyMet,9/19/22 0:19,5400,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 30 | myWebApp,Codepipeline-Assessment,PolicyBreached,9/19/22 1:30,2597401,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 31 | myWebApp,Codepipeline-Assessment,PolicyMet,9/19/22 1:33,5700,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 32 | myWebApp,tony,PolicyMet,9/30/22 17:24,5700,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 33 | myWebApp,Codepipeline-Assessment,PolicyMet,10/5/22 22:14,5700,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 34 | myWebApp,Codepipeline-Assessment,PolicyMet,10/5/22 22:20,5400,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 35 | myWebApp,Codepipeline-Assessment,PolicyMet,11/2/22 19:16,5400,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 36 | myWebApp,Codepipeline-Assessment,PolicyBreached,11/2/22 19:42,5189402,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 37 | myWebApp,Codepipeline-Assessment,PolicyBreached,3/20/23 18:01,2592001,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 38 | myWebApp,Codepipeline-Assessment,PolicyBreached,3/20/23 18:04,1800,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 39 | myWebApp,Codepipeline-Assessment,PolicyBreached,3/20/23 20:45,2592001,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 40 | new-api-test-11,,NotAssessed,,,,,,NotAssessed,0,,Important,us-east-1 41 | new-api-test-9,,NotAssessed,,,,,,NotAssessed,0,,NonCritical,us-east-1 42 | resource-group-volume,,NotAssessed,,,,,,NotAssessed,0,,MissionCritical,us-east-1 43 | terraform-1,Assessment-report-84ektbglj58,PolicyBreached,11/16/22 20:32,1800,0,600,300,PolicyBreached,0,11/16/22 20:42,MissionCritical,us-east-1 44 | terraform-1,Assessment-report-ujwfmoiz10n,PolicyBreached,11/16/22 20:34,1800,0,600,300,PolicyBreached,0,11/16/22 20:42,MissionCritical,us-east-1 45 | terraform-1,Assessment-report-vhi98hgat4,PolicyBreached,11/16/22 20:42,1800,0,600,300,PolicyBreached,0,11/16/22 20:42,MissionCritical,us-east-1 46 | tony,Assessment-report-75yecegwcp3,PolicyMet,10/11/22 19:56,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 47 | tony,System-Assessment-GJzG5kxy6f,PolicyMet,3/21/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 48 | tony,System-Assessment-359j3eBT6c,PolicyMet,3/22/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 49 | tony,System-Assessment-GxSXSWAvdu,PolicyMet,3/23/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 50 | tony,System-Assessment-b9ubSJYlyw,PolicyMet,3/24/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 51 | tony,System-Assessment-WfqLXIqneb,PolicyMet,3/25/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 52 | tony,System-Assessment-VnTDLEwuXF,PolicyMet,3/26/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 53 | tony,System-Assessment-YqIQZ8J4HL,PolicyMet,3/27/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 54 | tony,System-Assessment-KYQylAEhkK,PolicyMet,3/28/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 55 | workshop-test,Assessment-report-jia4mbal18g,PolicyBreached,3/27/23 15:28,2592001,2592001,300,300,PolicyBreached,0.22,3/27/23 15:28,MissionCritical,us-east-1 56 | dsfgdfhgfg,Assessment-report-hl8n0ldgswq,PolicyBreached,1/24/23 19:39,2592001,2592001,3600,3600,PolicyBreached,0.2,1/24/23 19:39,Critical,us-east-2 57 | jyhudsgdfgh,,NotAssessed,,,,,,NotAssessed,0,,Critical,us-east-2 58 | Bingo,assessment-report-16752,PolicyBreached,2/1/23 18:39,2592001,2592001,14400,3600,PolicyBreached,0.37,2/1/23 18:39,Critical,us-west-2 -------------------------------------------------------------------------------- /resilience-hub-pdf-export/README.md: -------------------------------------------------------------------------------- 1 | # Export AWS Resilience Hub assessment results into a PDF file 2 | 3 | This guide shows how to export assessment results for all applications defined within AWS Resilience Hub in an account into a PDF file. This PDF file can then be used for better visualisation, analytics, and reporting. The solution can be extended to include results from other accounts as well. 4 | 5 | ## Execution 6 | 7 | 1. Ensure you have the necessary libraries installed in your Python environment. You can do so using the pip install command: 8 | ``` 9 | pip install boto3 reportlab 10 | ``` 11 | 12 | 2. Run [pdf-generator.py](./pdf-generator.py) in an environment that includes permissions to make API calls to Resilience Hub. The environment also needs to have the [AWS SDK for Python (Boto3)](https://aws.amazon.com/sdk-for-python/) and [ReportLab library](https://www.reportlab.com/) installed. 13 | 14 | ``` 15 | python pdf-generator.py 16 | ``` 17 | 18 | ## Output 19 | 20 | The script will generate a PDF file that contains details of all latest assessments for all applications defined within Resilience Hub. The PDF file will be stored in the same location as where the script was executed and contains the following information in a table format: 21 | 22 | * **Application Name** - the name of the application as defined within Resilience Hub. 23 | * **Application ARN** - the ARN of an application. 24 | * **Assessment Name** - the name of an assessment for an application. 25 | * **Assessment Compliance Status** - indicates if the associated assessment was evaluated as being compliant with the resiliency policy. 26 | * **End Time** - identifies the end time of an assessment. 27 | * **Estimated Overall RTO** - indicates the estimated achievable Recovery Time Objective (RTO) at the time of assessment (the maximum value across all disruption types is selected). 28 | * **Estimated Overall RPO** - indicates the estimated achievable Recovery Point Objective (RPO) at the time of assessment (the maximum value across all disruption types is selected). 29 | * **Target RTO** - indicates the targeted RTO based on the associated resiliency policy (the minimum value across all disruption types is selected). 30 | * **Target RPO** - indicates the targeted RPO based on the associated resiliency policy (the minimum value across all disruption types is selected). 31 | * **Application Compliance Status** - indicates if the application (based on the latest assessment) was evaluated as being compliant with the resiliency policy. 32 | * **Resiliency Score** - the [resiliency score](https://docs.aws.amazon.com/resilience-hub/latest/userguide/resil-score.html) of the application. 33 | * **Last Assessed** - timestamp of the most recent assessment. 34 | * **Application Tier** - application tier as defined by the resiliency policy associated with the application. 35 | 36 | The PDF file has a clean layout, separated for each application, note that it might take a few minutes as we scan every application in every region. 37 | -------------------------------------------------------------------------------- /resilience-hub-pdf-export/pdf-generator.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer 3 | from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle 4 | from reportlab.lib.pagesizes import letter 5 | from reportlab.lib.colors import red, green 6 | from datetime import timedelta 7 | 8 | def seconds_to_time(seconds): 9 | return str(timedelta(seconds=seconds)) 10 | 11 | # Get list of regions 12 | ec2 = boto3.client('ec2') 13 | regions = [region['RegionName'] for region in ec2.describe_regions()['Regions']] 14 | 15 | doc = SimpleDocTemplate("resilience-report.pdf", pagesize=letter) 16 | flowables = [] 17 | 18 | style = getSampleStyleSheet()["BodyText"] 19 | header_style = ParagraphStyle('header', 20 | parent=style, 21 | fontSize=14, 22 | spaceAfter=12) 23 | 24 | # Loop over each region 25 | for region in regions: 26 | try: 27 | arh = boto3.client('resiliencehub', region_name=region) 28 | apps = arh.list_apps() 29 | except: 30 | continue 31 | 32 | # Get list of apps in a region 33 | app_arns = [app['appArn'] for app in apps['appSummaries']] 34 | while 'NextToken' in apps: 35 | apps = arh.list_apps(NextToken = apps['NextToken']) 36 | for app in apps['appSummaries']: 37 | app_arns.append(app['appArn']) 38 | 39 | # Loop over list of apps to retrieve details 40 | for app_arn in app_arns: 41 | app_details = arh.describe_app(appArn=app_arn)['app'] 42 | app_name = app_details['name'] 43 | app_arn = app_details['appArn'] 44 | app_compliance = app_details['complianceStatus'] 45 | app_res_score = app_details['resiliencyScore'] 46 | app_tier = 'unknown' 47 | 48 | # Check if a resiliency policy is associated with the application 49 | if 'policyArn' in app_details: 50 | policy_details = arh.describe_resiliency_policy(policyArn=app_details['policyArn']) 51 | app_tier = policy_details['policy']['tier'] 52 | 53 | flowables.append(Paragraph(f"Application: {app_name}", header_style)) 54 | flowables.append(Spacer(1, 12)) 55 | flowables.append(Paragraph(f"ARN: {app_arn}", style)) 56 | flowables.append(Paragraph(f"Compliance Status: {app_compliance}", style)) 57 | flowables.append(Paragraph(f"Resiliency Score: {app_res_score}", style)) 58 | flowables.append(Paragraph(f"Application Tier: {app_tier}", style)) 59 | flowables.append(Spacer(1, 12)) 60 | 61 | if app_compliance != 'NotAssessed': 62 | assessment_arns = [] 63 | assessment_summaries = arh.list_app_assessments(appArn=app_arn)['assessmentSummaries'] 64 | 65 | for assessment in assessment_summaries: 66 | assessment_arns.append(assessment['assessmentArn']) 67 | 68 | while 'NextToken' in assessment_summaries: 69 | assessment_summaries = arh.list_app_assessments(appArn=app_arn, NextToken = assessment_summaries['NextToken'])['assessmentSummaries'] 70 | for assessment in assessment_summaries: 71 | assessment_arns.append(assessment['assessmentArn']) 72 | 73 | assessment_details_list = [arh.describe_app_assessment(assessmentArn=arn)['assessment'] for arn in assessment_arns] 74 | 75 | # Get the latest assessment 76 | latest_assessment = max(assessment_details_list, key=lambda x: x['endTime']) 77 | 78 | # Filter successful assessments 79 | successful_assessments = [a for a in assessment_details_list if a['assessmentStatus'] == 'Success'] 80 | 81 | # Get the latest successful assessment 82 | if successful_assessments: 83 | latest_assessment = max(successful_assessments, key=lambda x: x['endTime']) 84 | assessment_name = latest_assessment['assessmentName'] 85 | assessment_arn = latest_assessment['assessmentArn'] 86 | assessment_compliance_status = latest_assessment['complianceStatus'] 87 | end_time = latest_assessment['endTime'] 88 | flowables.append(Paragraph(f"Latest Assessment Name: {assessment_name}", style)) 89 | flowables.append(Paragraph(f"Assessment Compliance Status: {assessment_compliance_status}", style)) 90 | flowables.append(Paragraph(f"End Time: {end_time}", style)) 91 | flowables.append(Spacer(1, 12)) 92 | 93 | component_compliances = [] 94 | response = arh.list_app_component_compliances(assessmentArn=assessment_arn) 95 | component_compliances.extend(response['componentCompliances']) 96 | 97 | while 'NextToken' in response: 98 | response = arh.list_app_component_compliances(assessmentArn=assessment_arn, NextToken=response['NextToken']) 99 | component_compliances.extend(response['componentCompliances']) 100 | 101 | for dtype in ['AZ', 'Region', 'Hardware', 'Software']: 102 | if dtype in latest_assessment['compliance']: 103 | current_rto = latest_assessment['compliance'][dtype]['currentRtoInSecs'] 104 | target_rto = latest_assessment['policy']['policy'][dtype]['rtoInSecs'] 105 | current_rpo = latest_assessment['compliance'][dtype]['currentRpoInSecs'] 106 | target_rpo = latest_assessment['policy']['policy'][dtype]['rpoInSecs'] 107 | 108 | rto_color = green if latest_assessment['compliance'][dtype]['currentRtoInSecs'] <= latest_assessment['policy']['policy'][dtype]['rtoInSecs'] else red 109 | rpo_color = green if latest_assessment['compliance'][dtype]['currentRpoInSecs'] <= latest_assessment['policy']['policy'][dtype]['rpoInSecs'] else red 110 | 111 | flowables.append(Paragraph(f"{dtype} Disruption Type:", style)) 112 | flowables.append(Paragraph(f" Current RTO: {seconds_to_time(current_rto)} Target RTO: {seconds_to_time(target_rto)}", style)) 113 | flowables.append(Paragraph(f" Current RPO: {seconds_to_time(current_rpo)} Target RPO: {seconds_to_time(target_rpo)}", style)) 114 | flowables.append(Spacer(1, 12)) 115 | 116 | for component_compliance in component_compliances: 117 | app_component_name = component_compliance['appComponentName'] 118 | component_compliance_status = component_compliance['status'] 119 | component_current_rto = component_compliance['compliance'][dtype]['currentRtoInSecs'] 120 | component_current_rpo = component_compliance['compliance'][dtype]['currentRpoInSecs'] 121 | component_rto_description = component_compliance['compliance'][dtype]['rtoDescription'] 122 | component_rpo_description = component_compliance['compliance'][dtype]['rpoDescription'] 123 | 124 | # Check if component compliance matches the current RTO and RPO 125 | if (component_current_rto == current_rto): 126 | flowables.append(Paragraph(f"RTO Description: {component_rto_description} ({app_component_name})", style)) 127 | 128 | if (component_current_rpo == current_rpo): 129 | flowables.append(Paragraph(f"RPO Description: {component_rpo_description} ({app_component_name})", style)) 130 | 131 | flowables.append(Spacer(1, 12)) 132 | 133 | doc.build(flowables) 134 | 135 | -------------------------------------------------------------------------------- /resilience-reporter/README.md: -------------------------------------------------------------------------------- 1 | # Resilience reporting dashboard 2 | 3 | Create a reporting dashboard on [Amazon QuickSight](https://aws.amazon.com/quicksight/) for applications defined and assessed using AWS Resilience Hub. A resilience dashboard provides a central place that leadership and other stakeholders can visit to get information regarding your organization's mission critical applications. For example, if the compliance team needs to report on certain applications' resilience each month, this dashboard would allow them to self-service that information. For more information on this solution, check out this blog post - [Build a resilience reporting dashboard with AWS Resilience Hub and Amazon QuickSight](https://aws.amazon.com/blogs/mt/resilience-reporting-dashboard-aws-resilience-hub/). 4 | 5 | ## Architecture 6 | 7 | ![Architecture](./images/arch.png) 8 | 9 | 1. After applications have been defined and assessed within Resilience Hub, a time-based event from Amazon EventBridge invokes an AWS Lambda function. 10 | 1. The Lambda function makes API calls to Resilience Hub, and retrieves and aggregates resilience data for all applications defined within Resilience Hub and generates a CSV file. 11 | 1. The Lambda function uploads this CSV file to an Amazon Simple Storage Service (S3) bucket. 12 | 1. The data in S3 is then ingested into SPICE (Super-fast, Parallel, In-memory Calculation Engine) within QuickSight. 13 | 1. A dashboard is created that contains visuals providing an aggregate view of resilience across all applications defined in Resilience Hub. 14 | 15 | ## Prerequisites 16 | 17 | You will need a QuickSight subscription for this solution to work. 18 | 19 | 1. Navigate to the [QuickSight console](https://quicksight.aws.amazon.com/). 20 | 1. If you already have a QuickSight subscription, you can skip the following steps and move on to the **Deployment** section. 21 | 1. If you do not have a QuickSight subscription, [sign up for a QuickSight subscription](https://docs.aws.amazon.com/quicksight/latest/user/signing-up.html) before proceeding to the **Deployment** section. 22 | 23 | ## Deployment 24 | 25 | The solution is provided as CloudFormation templates that can be used to create CloudFormation stacks, which then provisions all the necessary resources. The deployment happens in three stages, and they need to be executed in this specific order. 26 | 27 | **NOTE: This solution must only be deployed once per AWS account, as resilience data is collected from all AWS regions where you are using Resilience Hub in the account.** 28 | 29 | ### Stage 1: 30 | 31 | 1. Deploy a CloudFormation stack using the [resilience-csv-generator.yaml](./resilience-csv-generator.yaml) template. 32 | 1. Parameters: 33 | * **Schedule** - This determines the frequency at which the Lambda function will be invoked to refresh data. Refer to [this documentation](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html#eb-rate-expressions) for information on supported values. The default value invokes the Lambda function at 24-hour intervals. 34 | * **Timeout** - This is the timeout for the Lambda function. Increase this depending on how many applications and assessments you have in Resilience Hub. (up to a maximum of 900) 35 | 1. After the stack reaches **CREATE_COMPLETE**, navigate to the **Outputs** tab and note down the value for **ResilienceReportBucket**. 36 | 37 | ### Stage 2: 38 | 39 | 1. Navigate to [the QuickSight admin panel](https://quicksight.aws.amazon.com/sn/admin) and click on **Security & permissions**. 40 | **NOTE: You might have to switch to a different region depending on what your identity region is.** 41 | 1. Under **QuickSight access to AWS services** check the **AWS Identity and Access Management (IAM) role in use**: 42 | * If the value there is **Quicksight-managed role (default)**, click **Manage**. 43 | * Check the box next to **Amazon S3** and click **Select S3 buckets**. 44 | * Select the S3 bucket where the resilience report is stored (obtained from the **Outputs** section of **Stage 1** of the deployment). 45 | * Click **Finish** and then **Save**. 46 | * Move on to **Stage 3** of the deployment. 47 | 48 | * If the value there is the ARN of a role: 49 | * Navigate to the IAM console, create a new policy with the following permissions, and attach it to the role displayed on the QuickSight admin panel. 50 | **NOTE: you need to first replace the string *YOUR_S3_BUCKET_NAME* with the actual name of the S3 bucket where the resilience report is stored (obtained from the *Outputs* section of *Stage 1* of the deployment).** 51 | 52 | ``` 53 | { 54 | "Version": "2012-10-17", 55 | "Statement": [ 56 | { 57 | "Effect": "Allow", 58 | "Action": "s3:ListAllMyBuckets", 59 | "Resource": "arn:aws:s3:::*" 60 | }, 61 | { 62 | "Action": [ 63 | "s3:ListBucket" 64 | ], 65 | "Effect": "Allow", 66 | "Resource": "arn:aws:s3:::YOUR_S3_BUCKET_NAME" 67 | }, 68 | { 69 | "Action": [ 70 | "s3:GetObject", 71 | "s3:GetObjectVersion" 72 | ], 73 | "Effect": "Allow", 74 | "Resource": "arn:aws:s3:::YOUR_S3_BUCKET_NAME/*" 75 | } 76 | ] 77 | } 78 | ``` 79 | 80 | ### Stage 3: 81 | 82 | 1. Deploy a CloudFormation stack using the [resilience-dashboard.yaml](./resilience-dashboard.yaml) template. 83 | 1. Parameters: 84 | * **QuickSightUserARN** - ARN of the QuickSight user with who the dashboard should be shared. You can find your list of QuickSight users by running the [list-users](https://docs.aws.amazon.com/cli/latest/reference/quicksight/list-users.html) API. 85 | 86 | ## Outputs 87 | 88 | After the CloudFormation stack reaches **CREATE_COMPLETE**, navigate to the **Outputs** tab of the stack deployed in **Stage 3** of the deployment to find the URL for the dashboard. 89 | 90 | You can see a sample report here - [sample-resilience-report.csv](./sample-resilience-report.csv) 91 | 92 | ## Configure data refresh 93 | 94 | To ensure data in the dashboard is being refreshed, you will need to [configure a data refresh schedule](https://docs.aws.amazon.com/quicksight/latest/user/refreshing-imported-data.html#schedule-data-refresh) for the **ResilienceDataSet**. 95 | 96 | ## Troubleshooting 97 | 98 | * **Insufficient SPICE capacity** - If you get an error during stack creation that says "Insufficient SPICE capacity", you will need to [purchase more SPICE capacity](https://docs.aws.amazon.com/quicksight/latest/user/managing-spice-capacity.html#spice-capacity-purchasing). Note that SPICE capacity is regional and you will need to purchase capacity in the AWS Region you are deploying this solution. If you do not want to purchase SPICE capacity in a secondary region, deploy the **resilience-dashboard.yaml** template (Stage 3) in the default region for your QuikcSight subscription. 99 | * **Lambda timeout** - If you have a large number of applications and assessments, the Lambda function may time out before it can finish executing. In this situation, increase the value for the **Timeout** parameter (up to a maximum of 900). 100 | -------------------------------------------------------------------------------- /resilience-reporter/images/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-resilience-hub-tools/6d9252bc003a3c3d714697c4f613c93baf4edb48/resilience-reporter/images/arch.png -------------------------------------------------------------------------------- /resilience-reporter/resilience-csv-generator.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | Schedule: 3 | Type: String 4 | Default: rate(1 day) 5 | Description: Frequency at which you want a new report to be generated. 6 | Timeout: 7 | Type: Number 8 | Default: 120 9 | Description: Timeout for the Lambda functions that retrieve resilience data 10 | Resources: 11 | ReportBucket: 12 | Type: 'AWS::S3::Bucket' 13 | Properties: 14 | BucketName: !Join 15 | - '-' 16 | - - resilience-reporter 17 | - !Ref 'AWS::AccountId' 18 | ResilienceReporterTrigger: 19 | Type: 'AWS::Events::Rule' 20 | Properties: 21 | ScheduleExpression: !Ref Schedule 22 | State: ENABLED 23 | Targets: 24 | - Arn: !GetAtt 25 | - ResilienceReporterLambda 26 | - Arn 27 | Id: ResilienceReporter 28 | PermissionForEventsToInvokeLambda: 29 | Type: 'AWS::Lambda::Permission' 30 | Properties: 31 | FunctionName: !Ref ResilienceReporterLambda 32 | Action: 'lambda:InvokeFunction' 33 | Principal: events.amazonaws.com 34 | SourceArn: !GetAtt 35 | - ResilienceReporterTrigger 36 | - Arn 37 | ResilienceReporterLambda: 38 | Type: 'AWS::Lambda::Function' 39 | Properties: 40 | Code: 41 | ZipFile: | 42 | import boto3 43 | import csv 44 | import json 45 | import os 46 | 47 | def lambda_handler(event, context): 48 | # Print incoming event 49 | print('Incoming Event:' + json.dumps(event)) 50 | 51 | # Get list of regions 52 | ec2 = boto3.client('ec2') 53 | regions = [] 54 | region_details = ec2.describe_regions()['Regions'] 55 | for region_detail in region_details: 56 | regions.append(region_detail['RegionName']) 57 | 58 | # Generate a new CSV file and populate headers 59 | with open('/tmp/resilience-report.csv', 'w', newline='') as file: 60 | writer = csv.writer(file) 61 | writer.writerow(["Application Name", "Assessment Name", "Assessment Compliance Status", "End Time", "Estimated Overall RTO", "Estimated Overall RPO", "Target RTO", "Target RPO", "Application Compliance Status", "Resiliency Score", "Last Assessed", "Application Tier", "Region"]) 62 | 63 | # Loop over each region 64 | for region in regions: 65 | try: 66 | arh = boto3.client('resiliencehub', region_name=region) 67 | apps = arh.list_apps() 68 | except: 69 | continue 70 | 71 | # Get list of apps in a region 72 | app_arns = [] 73 | for app in apps['appSummaries']: 74 | app_arns.append(app['appArn']) 75 | while 'NextToken' in apps: 76 | apps = arh.list_apps(NextToken = apps['NextToken']) 77 | for app in apps['appSummaries']: 78 | app_arns.append(app['appArn']) 79 | 80 | # Loop over list of apps to retrieve details 81 | for app in app_arns: 82 | app_details = arh.describe_app(appArn=app)['app'] 83 | app_name = app_details['name'] 84 | app_compliance = app_details['complianceStatus'] 85 | app_res_score = app_details['resiliencyScore'] 86 | app_tier = 'unknown' 87 | 88 | # Check if a resiliency policy is associated with the application 89 | if 'policyArn' in app_details: 90 | app_res_policy = app_details['policyArn'] 91 | policy_details = arh.describe_resiliency_policy( 92 | policyArn=app_res_policy 93 | ) 94 | app_tier = policy_details['policy']['tier'] 95 | 96 | # Check if an application has been assessed 97 | if app_compliance == 'NotAssessed': 98 | with open('/tmp/resilience-report.csv', 'a', newline='') as file: 99 | writer = csv.writer(file) 100 | writer.writerow([app_name, '', 'NotAssessed', '', '', '', '', '', app_compliance, app_res_score, '', app_tier, region]) 101 | continue 102 | 103 | app_last_assessed = app_details['lastAppComplianceEvaluationTime'] 104 | 105 | # Get list of assessments for the application 106 | assessment_summaries = arh.list_app_assessments(appArn=app)['assessmentSummaries'] 107 | 108 | while 'NextToken' in assessment_summaries: 109 | assessment_summaries.append(arh.list_app_assessments(appArn=app, NextToken = assessment_summaries['NextToken'])['assessmentSummaries']) 110 | 111 | # Loop over list of assessments to get details 112 | for assessment in assessment_summaries: 113 | assessment_arn = assessment['assessmentArn'] 114 | assessment_status = assessment['assessmentStatus'] 115 | 116 | # Get assessment details if it is a successful assessment 117 | if assessment_status == 'Success': 118 | assessment_details = arh.describe_app_assessment(assessmentArn=assessment_arn) 119 | 120 | if assessment_details['assessment']['compliance'] == {}: 121 | continue 122 | 123 | assessment_name = assessment_details['assessment']['assessmentName'] 124 | assessment_compliance_status = assessment_details['assessment']['complianceStatus'] 125 | end_time = assessment_details['assessment']['endTime'] 126 | current_rto_az = assessment_details['assessment']['compliance']['AZ']['currentRtoInSecs'] 127 | current_rto_hardware = assessment_details['assessment']['compliance']['Hardware']['currentRtoInSecs'] 128 | current_rto_software = assessment_details['assessment']['compliance']['Software']['currentRtoInSecs'] 129 | current_rpo_az = assessment_details['assessment']['compliance']['AZ']['currentRpoInSecs'] 130 | current_rpo_hardware = assessment_details['assessment']['compliance']['Hardware']['currentRpoInSecs'] 131 | current_rpo_software = assessment_details['assessment']['compliance']['Software']['currentRpoInSecs'] 132 | target_rto_az = assessment_details['assessment']['policy']['policy']['AZ']['rtoInSecs'] 133 | target_rto_hardware = assessment_details['assessment']['policy']['policy']['Hardware']['rtoInSecs'] 134 | target_rto_software = assessment_details['assessment']['policy']['policy']['Software']['rtoInSecs'] 135 | target_rpo_az = assessment_details['assessment']['policy']['policy']['AZ']['rpoInSecs'] 136 | target_rpo_hardware = assessment_details['assessment']['policy']['policy']['Hardware']['rpoInSecs'] 137 | target_rpo_software = assessment_details['assessment']['policy']['policy']['Software']['rpoInSecs'] 138 | 139 | # Aggregate RTO and RPO values for current and target 140 | current_rto = max(current_rto_az, current_rto_hardware, current_rto_software) 141 | current_rpo = max(current_rpo_az, current_rpo_hardware, current_rpo_software) 142 | target_rto = min(target_rto_az, target_rto_hardware, target_rto_software) 143 | target_rpo = min(target_rpo_az, target_rpo_hardware, target_rpo_software) 144 | 145 | # Check if application is multi-region and updated aggregates accordingly 146 | if 'Region' in assessment_details['assessment']['policy']['policy']: 147 | current_rto_region = assessment_details['assessment']['compliance']['Region']['currentRtoInSecs'] 148 | current_rpo_region = assessment_details['assessment']['compliance']['Region']['currentRpoInSecs'] 149 | target_rto_region = assessment_details['assessment']['policy']['policy']['Region']['rtoInSecs'] 150 | target_rpo_region = assessment_details['assessment']['policy']['policy']['Region']['rpoInSecs'] 151 | 152 | if current_rto < current_rto_region: 153 | current_rto = current_rto_region 154 | if current_rpo < current_rpo_region: 155 | current_rpo = current_rpo_region 156 | if target_rto > target_rto_region: 157 | target_rto = target_rto_region 158 | if target_rpo > target_rpo_region: 159 | target_rpo = target_rpo_region 160 | 161 | # Populate data into the CSV file 162 | with open('/tmp/resilience-report.csv', 'a', newline='') as file: 163 | writer = csv.writer(file) 164 | writer.writerow([app_name, assessment_name, assessment_compliance_status, end_time.strftime("%Y-%m-%d %H:%M:%S"), current_rto, current_rpo, target_rto, target_rpo, app_compliance, app_res_score, app_last_assessed.strftime("%Y-%m-%d %H:%M:%S"), app_tier, region]) 165 | 166 | # Write data to S3 167 | bucketName = os.environ['bucketName'] 168 | s3 = boto3.resource('s3') 169 | write_response = s3.Bucket(bucketName).upload_file('/tmp/resilience-report.csv', 'resilience-report.csv') 170 | print(write_response) 171 | Environment: 172 | Variables: 173 | bucketName: !Ref ReportBucket 174 | FunctionName: ResilienceReporter 175 | Handler: index.lambda_handler 176 | Role: !GetAtt 177 | - ResilienceReporterRole 178 | - Arn 179 | Runtime: python3.9 180 | Timeout: !Ref Timeout 181 | ResilienceReporterRole: 182 | Type: 'AWS::IAM::Role' 183 | Properties: 184 | AssumeRolePolicyDocument: 185 | Version: 2012-10-17 186 | Statement: 187 | - Effect: Allow 188 | Principal: 189 | Service: 190 | - lambda.amazonaws.com 191 | Action: 192 | - 'sts:AssumeRole' 193 | Policies: 194 | - PolicyName: ResilienceReporterPolicy 195 | PolicyDocument: 196 | Version: 2012-10-17 197 | Statement: 198 | - Effect: Allow 199 | Action: 's3:PutObject' 200 | Resource: 201 | - !Sub 'arn:aws:s3:::${ReportBucket}' 202 | - !Sub 'arn:aws:s3:::${ReportBucket}/*' 203 | - Effect: Allow 204 | Action: 205 | - 'resiliencehub:ListApps' 206 | - 'resiliencehub:DescribeResiliencyPolicy' 207 | - 'resiliencehub:ListAppAssessments' 208 | - 'ec2:DescribeRegions' 209 | - 'resiliencehub:DescribeApp' 210 | - 'resiliencehub:DescribeAppAssessment' 211 | Resource: '*' 212 | - Effect: Allow 213 | Action: 214 | - 'logs:CreateLogGroup' 215 | - 'logs:CreateLogStream' 216 | - 'logs:PutLogEvents' 217 | Resource: '*' 218 | RoleName: ResilienceReporterRole 219 | ResilienceReporterFirstInvoke: 220 | Type: 'AWS::CloudFormation::CustomResource' 221 | Properties: 222 | ServiceToken: !GetAtt 223 | - ResilienceReporterFirstInvokeLambda 224 | - Arn 225 | ResilienceReporterFirstInvokeLambda: 226 | Type: 'AWS::Lambda::Function' 227 | Properties: 228 | Code: 229 | ZipFile: | 230 | import boto3 231 | import os 232 | import json 233 | import cfnresponse 234 | 235 | def lambda_handler(event, context): 236 | # Print incoming event 237 | print('Incoming Event:' + json.dumps(event)) 238 | 239 | try: 240 | if event['RequestType'] == 'Delete': 241 | print("delete") 242 | elif event['RequestType'] == 'Create': 243 | print("create") 244 | client = boto3.client('lambda') 245 | resilienceReporter = os.environ['resilienceReporter'] 246 | response = client.invoke( 247 | FunctionName=resilienceReporter, 248 | Payload='{}' 249 | ) 250 | print(response) 251 | elif event['RequestType'] == 'Update': 252 | print("update") 253 | responseValue = 'SUCCESS' 254 | except Exception as e: 255 | print(e) 256 | responseValue = 'FAILURE' 257 | 258 | responseData = {} 259 | responseData['Data'] = responseValue 260 | custom_resource_response = cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "first-invoke") 261 | Environment: 262 | Variables: 263 | resilienceReporter: !Ref ResilienceReporterLambda 264 | FunctionName: ResilienceReporterFirstInvoke 265 | Handler: index.lambda_handler 266 | Role: !GetAtt 267 | - ResilienceReporterFirstInvokeLambdaRole 268 | - Arn 269 | Runtime: python3.9 270 | Timeout: !Ref Timeout 271 | ResilienceReporterFirstInvokeLambdaRole: 272 | Type: 'AWS::IAM::Role' 273 | Properties: 274 | AssumeRolePolicyDocument: 275 | Version: 2012-10-17 276 | Statement: 277 | - Effect: Allow 278 | Principal: 279 | Service: 280 | - lambda.amazonaws.com 281 | Action: 282 | - 'sts:AssumeRole' 283 | Policies: 284 | - PolicyName: ResilienceManifestUploaderPolicy 285 | PolicyDocument: 286 | Version: 2012-10-17 287 | Statement: 288 | - Effect: Allow 289 | Action: 'lambda:InvokeFunction' 290 | Resource: 291 | - !GetAtt 292 | - ResilienceReporterLambda 293 | - Arn 294 | - Effect: Allow 295 | Action: 296 | - 'logs:CreateLogGroup' 297 | - 'logs:CreateLogStream' 298 | - 'logs:PutLogEvents' 299 | Resource: '*' 300 | RoleName: ResilienceReporterFirstInvokeLambdaRole 301 | Outputs: 302 | ResilienceReportBucket: 303 | Description: Name of the S3 bucket where the resilience report is stored 304 | Value: !Ref ReportBucket 305 | ResilienceReportLocation: 306 | Description: S3 bucket location where the resilience report is stored 307 | Value: !Sub 'https://console.aws.amazon.com/s3/home?bucket=${ReportBucket}' 308 | -------------------------------------------------------------------------------- /resilience-reporter/resilience-dashboard.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | QuickSightUserARN: 3 | Type: String 4 | Description: >- 5 | ARN of the QuickSight user with who the dashboard will be shared. 6 | Resources: 7 | ResilienceManifestUploader: 8 | Type: 'AWS::CloudFormation::CustomResource' 9 | Properties: 10 | ServiceToken: !GetAtt 11 | - ResilienceManifestUploaderLambda 12 | - Arn 13 | ResilienceManifestUploaderLambda: 14 | Type: 'AWS::Lambda::Function' 15 | Properties: 16 | Code: 17 | ZipFile: | 18 | import boto3 19 | import os 20 | import json 21 | import cfnresponse 22 | 23 | def lambda_handler(event, context): 24 | # Print incoming event 25 | print('Incoming Event:' + json.dumps(event)) 26 | 27 | try: 28 | if event['RequestType'] == 'Delete': 29 | print("delete") 30 | elif event['RequestType'] == 'Create': 31 | print("create") 32 | bucketName = os.environ['bucketName'] 33 | manifest = str.encode('{\"fileLocations\": [{\"URIs\": [\"s3://' + bucketName + '/resilience-report.csv\"]}]}') 34 | s3 = boto3.resource('s3') 35 | object = s3.Object(bucketName, 'manifest.json') 36 | put_response = object.put(Body=manifest) 37 | print(put_response) 38 | elif event['RequestType'] == 'Update': 39 | print("update") 40 | responseValue = 'SUCCESS' 41 | except Exception as e: 42 | print(e) 43 | responseValue = 'FAILURE' 44 | 45 | responseData = {} 46 | responseData['Data'] = responseValue 47 | custom_resource_response = cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "manifest") 48 | Environment: 49 | Variables: 50 | bucketName: !Sub 'resilience-reporter-${AWS::AccountId}' 51 | FunctionName: ResilienceManifestUploader 52 | Handler: index.lambda_handler 53 | Role: !GetAtt 54 | - ResilienceManifestUploaderRole 55 | - Arn 56 | Runtime: python3.9 57 | Timeout: '5' 58 | ResilienceManifestUploaderRole: 59 | Type: 'AWS::IAM::Role' 60 | Properties: 61 | AssumeRolePolicyDocument: 62 | Version: 2012-10-17 63 | Statement: 64 | - Effect: Allow 65 | Principal: 66 | Service: 67 | - lambda.amazonaws.com 68 | Action: 69 | - 'sts:AssumeRole' 70 | Policies: 71 | - PolicyName: ResilienceManifestUploaderPolicy 72 | PolicyDocument: 73 | Version: 2012-10-17 74 | Statement: 75 | - Effect: Allow 76 | Action: 's3:PutObject' 77 | Resource: 78 | - !Sub 'arn:aws:s3:::resilience-reporter-${AWS::AccountId}' 79 | - !Sub 'arn:aws:s3:::resilience-reporter-${AWS::AccountId}/*' 80 | - Effect: Allow 81 | Action: 82 | - 'logs:CreateLogGroup' 83 | - 'logs:CreateLogStream' 84 | - 'logs:PutLogEvents' 85 | Resource: '*' 86 | RoleName: ResilienceManifestUploaderRole 87 | ResilienceDataSource: 88 | Type: 'AWS::QuickSight::DataSource' 89 | DependsOn: 90 | - ResilienceManifestUploader 91 | Properties: 92 | AwsAccountId: !Ref 'AWS::AccountId' 93 | DataSourceId: ResilienceDataSource 94 | DataSourceParameters: 95 | S3Parameters: 96 | ManifestFileLocation: 97 | Bucket: !Sub 'resilience-reporter-${AWS::AccountId}' 98 | Key: manifest.json 99 | Name: ResilienceDataSource 100 | Permissions: 101 | - Principal: !Ref QuickSightUserARN 102 | Actions: 103 | - 'quicksight:UpdateDataSourcePermissions' 104 | - 'quicksight:DescribeDataSource' 105 | - 'quicksight:DescribeDataSourcePermissions' 106 | - 'quicksight:PassDataSource' 107 | - 'quicksight:UpdateDataSource' 108 | - 'quicksight:DeleteDataSource' 109 | Type: S3 110 | ResilienceDataSet: 111 | Type: 'AWS::QuickSight::DataSet' 112 | Properties: 113 | AwsAccountId: !Ref 'AWS::AccountId' 114 | DataSetId: ResilienceDataSet 115 | ImportMode: SPICE 116 | Name: ResilienceDataSet 117 | PhysicalTableMap: 118 | ResilienceDataTable: 119 | S3Source: 120 | DataSourceArn: !GetAtt 121 | - ResilienceDataSource 122 | - Arn 123 | InputColumns: 124 | - Name: Application Name 125 | Type: STRING 126 | - Name: Assessment Name 127 | Type: STRING 128 | - Name: Assessment Compliance Status 129 | Type: STRING 130 | - Name: End Time 131 | Type: STRING 132 | - Name: Overall RTO 133 | Type: STRING 134 | - Name: Overall RPO 135 | Type: STRING 136 | - Name: Target RTO 137 | Type: STRING 138 | - Name: Target RPO 139 | Type: STRING 140 | - Name: Application Compliance Status 141 | Type: STRING 142 | - Name: Resiliency Score 143 | Type: STRING 144 | - Name: Last Assessed 145 | Type: STRING 146 | - Name: Application Tier 147 | Type: STRING 148 | - Name: Region 149 | Type: STRING 150 | UploadSettings: 151 | ContainsHeader: true 152 | Delimiter: ',' 153 | Format: CSV 154 | LogicalTableMap: 155 | LogicalTable1: 156 | Alias: Resilience-DataSet 157 | DataTransforms: 158 | - CastColumnTypeOperation: 159 | ColumnName: Overall RTO 160 | NewColumnType: INTEGER 161 | - CastColumnTypeOperation: 162 | ColumnName: Overall RPO 163 | NewColumnType: INTEGER 164 | - CastColumnTypeOperation: 165 | ColumnName: Target RTO 166 | NewColumnType: INTEGER 167 | - CastColumnTypeOperation: 168 | ColumnName: Target RPO 169 | NewColumnType: INTEGER 170 | - CastColumnTypeOperation: 171 | ColumnName: Resiliency Score 172 | NewColumnType: DECIMAL 173 | - CreateColumnsOperation: 174 | Columns: 175 | - ColumnId: Target RTO in minutes 176 | ColumnName: Target RTO in minutes 177 | Expression: '{Target RTO} / 60' 178 | - CreateColumnsOperation: 179 | Columns: 180 | - ColumnId: Target RPO in minutes 181 | ColumnName: Target RPO in minutes 182 | Expression: '{Target RPO} / 60' 183 | Source: 184 | PhysicalTableId: ResilienceDataTable 185 | Permissions: 186 | - Principal: !Ref QuickSightUserARN 187 | Actions: 188 | - 'quicksight:UpdateDataSetPermissions' 189 | - 'quicksight:DescribeDataSet' 190 | - 'quicksight:DescribeDataSetPermissions' 191 | - 'quicksight:PassDataSet' 192 | - 'quicksight:DescribeIngestion' 193 | - 'quicksight:ListIngestions' 194 | - 'quicksight:UpdateDataSet' 195 | - 'quicksight:DeleteDataSet' 196 | - 'quicksight:CreateIngestion' 197 | - 'quicksight:CancelIngestion' 198 | ResilienceAnalysis: 199 | Type: 'AWS::QuickSight::Analysis' 200 | Properties: 201 | AnalysisId: Resilience-analysis 202 | Name: Resilience-analysis 203 | AwsAccountId: !Ref 'AWS::AccountId' 204 | SourceEntity: 205 | SourceTemplate: 206 | Arn: >- 207 | arn:aws:quicksight:us-east-1:061578351048:template/Resilience-analysis 208 | DataSetReferences: 209 | - DataSetPlaceholder: Resilience-dataset 210 | DataSetArn: !GetAtt 211 | - ResilienceDataSet 212 | - Arn 213 | Permissions: 214 | - Principal: !Ref QuickSightUserARN 215 | Actions: 216 | - 'quicksight:RestoreAnalysis' 217 | - 'quicksight:UpdateAnalysisPermissions' 218 | - 'quicksight:DeleteAnalysis' 219 | - 'quicksight:DescribeAnalysisPermissions' 220 | - 'quicksight:QueryAnalysis' 221 | - 'quicksight:DescribeAnalysis' 222 | - 'quicksight:UpdateAnalysis' 223 | ResilienceDashboard: 224 | Type: 'AWS::QuickSight::Dashboard' 225 | Properties: 226 | DashboardId: Resilience-dashboard 227 | Name: Resilience-dashboard 228 | AwsAccountId: !Ref 'AWS::AccountId' 229 | SourceEntity: 230 | SourceTemplate: 231 | Arn: >- 232 | arn:aws:quicksight:us-east-1:061578351048:template/Resilience-analysis 233 | DataSetReferences: 234 | - DataSetPlaceholder: Resilience-dataset 235 | DataSetArn: !GetAtt 236 | - ResilienceDataSet 237 | - Arn 238 | Permissions: 239 | - Principal: !Ref QuickSightUserARN 240 | Actions: 241 | - 'quicksight:DescribeDashboard' 242 | - 'quicksight:ListDashboardVersions' 243 | - 'quicksight:UpdateDashboardPermissions' 244 | - 'quicksight:QueryDashboard' 245 | - 'quicksight:UpdateDashboard' 246 | - 'quicksight:DeleteDashboard' 247 | - 'quicksight:DescribeDashboardPermissions' 248 | - 'quicksight:UpdateDashboardPublishedVersion' 249 | Outputs: 250 | DashboardURL: 251 | Description: URL to access the Resilience dashboard 252 | Value: !Sub 'https://${AWS::Region}.quicksight.aws.amazon.com/sn/dashboards/Resilience-dashboard' 253 | -------------------------------------------------------------------------------- /resilience-reporter/sample-resilience-report.csv: -------------------------------------------------------------------------------- 1 | Application Name,Assessment Name,Assessment Compliance Status,End Time,Overall RTO,Overall RPO,Target RTO,Target RPO,Application Compliance Status,Resiliency Score,Last Assessed,Application Tier,Region 2 | ecs-test-multi-region,run-1,PolicyBreached,10/21/22 15:12,2592631,0,30,30,PolicyBreached,0,10/21/22 15:12,MissionCritical,us-east-1 3 | github-actions,GitHub-actions-assessment,PolicyMet,2/3/23 16:45,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 4 | github-actions,GitHub-actions-assessment,PolicyBreached,2/3/23 16:47,2592001,2592001,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 5 | github-actions,GitHub-actions-assessment,PolicyBreached,2/3/23 16:52,2592001,2592001,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 6 | github-actions,GitHub-actions-assessment,PolicyMet,2/3/23 16:54,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 7 | github-actions,GitHub-actions-assessment,PolicyMet,3/16/23 19:58,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 8 | github-actions,GitHub-actions-assessment,PolicyBreached,3/20/23 18:12,2592001,2592001,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 9 | github-actions,GitHub-actions-assessment,PolicyMet,3/20/23 18:14,300,0,300,300,PolicyMet,0.5,3/20/23 18:14,MissionCritical,us-east-1 10 | myServerlessApp,run-1,PolicyBreached,9/19/22 19:40,5187002,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 11 | myServerlessApp,run-2,PolicyBreached,9/19/22 19:52,2592131,300,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 12 | myServerlessApp,run-3,PolicyBreached,9/19/22 20:57,2592131,300,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 13 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 4:23,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 14 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 4:25,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 15 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 14:16,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 16 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 14:20,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 17 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,11/3/22 14:46,2596501,2592001,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 18 | myServerlessApp,GitHub-actions-assessment,PolicyMet,11/3/22 14:52,4800,300,300,300,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 19 | myServerlessApp,Assessment-report-pa477a8i8h,PolicyBreached,12/5/22 16:28,7783803,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 20 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,1/3/23 22:53,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 21 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 15:56,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 22 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:02,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 23 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:23,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 24 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:25,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 25 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:31,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 26 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:34,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 27 | myServerlessApp,GitHub-actions-assessment,PolicyBreached,2/3/23 16:37,2592001,2592001,30,30,PolicyBreached,0.15,2/3/23 16:37,MissionCritical,us-east-1 28 | myWebApp,run-1,PolicyBreached,9/18/22 23:56,5187302,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 29 | myWebApp,Assessment-report-ye5v4j1biys,PolicyMet,9/19/22 0:19,5400,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 30 | myWebApp,Codepipeline-Assessment,PolicyBreached,9/19/22 1:30,2597401,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 31 | myWebApp,Codepipeline-Assessment,PolicyMet,9/19/22 1:33,5700,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 32 | myWebApp,tony,PolicyMet,9/30/22 17:24,5700,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 33 | myWebApp,Codepipeline-Assessment,PolicyMet,10/5/22 22:14,5700,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 34 | myWebApp,Codepipeline-Assessment,PolicyMet,10/5/22 22:20,5400,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 35 | myWebApp,Codepipeline-Assessment,PolicyMet,11/2/22 19:16,5400,300,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 36 | myWebApp,Codepipeline-Assessment,PolicyBreached,11/2/22 19:42,5189402,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 37 | myWebApp,Codepipeline-Assessment,PolicyBreached,3/20/23 18:01,2592001,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 38 | myWebApp,Codepipeline-Assessment,PolicyBreached,3/20/23 18:04,1800,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 39 | myWebApp,Codepipeline-Assessment,PolicyBreached,3/20/23 20:45,2592001,2592001,300,300,PolicyBreached,0.25,3/20/23 20:45,MissionCritical,us-east-1 40 | new-api-test-11,,NotAssessed,,,,,,NotAssessed,0,,Important,us-east-1 41 | new-api-test-9,,NotAssessed,,,,,,NotAssessed,0,,NonCritical,us-east-1 42 | resource-group-volume,,NotAssessed,,,,,,NotAssessed,0,,MissionCritical,us-east-1 43 | terraform-1,Assessment-report-84ektbglj58,PolicyBreached,11/16/22 20:32,1800,0,600,300,PolicyBreached,0,11/16/22 20:42,MissionCritical,us-east-1 44 | terraform-1,Assessment-report-ujwfmoiz10n,PolicyBreached,11/16/22 20:34,1800,0,600,300,PolicyBreached,0,11/16/22 20:42,MissionCritical,us-east-1 45 | terraform-1,Assessment-report-vhi98hgat4,PolicyBreached,11/16/22 20:42,1800,0,600,300,PolicyBreached,0,11/16/22 20:42,MissionCritical,us-east-1 46 | tony,Assessment-report-75yecegwcp3,PolicyMet,10/11/22 19:56,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 47 | tony,System-Assessment-GJzG5kxy6f,PolicyMet,3/21/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 48 | tony,System-Assessment-359j3eBT6c,PolicyMet,3/22/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 49 | tony,System-Assessment-GxSXSWAvdu,PolicyMet,3/23/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 50 | tony,System-Assessment-b9ubSJYlyw,PolicyMet,3/24/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 51 | tony,System-Assessment-WfqLXIqneb,PolicyMet,3/25/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 52 | tony,System-Assessment-VnTDLEwuXF,PolicyMet,3/26/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 53 | tony,System-Assessment-YqIQZ8J4HL,PolicyMet,3/27/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 54 | tony,System-Assessment-KYQylAEhkK,PolicyMet,3/28/23 20:48,0,0,300,300,PolicyMet,0.67,3/28/23 20:48,MissionCritical,us-east-1 55 | workshop-test,Assessment-report-jia4mbal18g,PolicyBreached,3/27/23 15:28,2592001,2592001,300,300,PolicyBreached,0.22,3/27/23 15:28,MissionCritical,us-east-1 56 | dsfgdfhgfg,Assessment-report-hl8n0ldgswq,PolicyBreached,1/24/23 19:39,2592001,2592001,3600,3600,PolicyBreached,0.2,1/24/23 19:39,Critical,us-east-2 57 | jyhudsgdfgh,,NotAssessed,,,,,,NotAssessed,0,,Critical,us-east-2 58 | Bingo,assessment-report-16752,PolicyBreached,2/1/23 18:39,2592001,2592001,14400,3600,PolicyBreached,0.37,2/1/23 18:39,Critical,us-west-2 -------------------------------------------------------------------------------- /resource-group-enhancer/README.md: -------------------------------------------------------------------------------- 1 | # Add resources that are not supported by Resource Groups 2 | 3 | There are 4 resource types that are supported by AWS Resilience Hub but not supported by AWS Resource Groups. If you are using Resource Groups as the input for applications on Resilience Hub, and your application consists of one of the following resource types, you can use this solution to add these resource types directly to the application on Resilience Hub in an automated way. 4 | 5 | * AWS::AutoScaling::AutoScalingGroup 6 | * AWS::ApiGatewayV2::Api 7 | * AWS::RDS::GlobalCluster 8 | * AWS::Route53::RecordSet 9 | 10 | ## Pre-requisites 11 | 12 | * Execution environment needs to have the [AWS SDK for Python (Boto3)](https://aws.amazon.com/sdk-for-python/) installed 13 | * You need the following IAM permissions for the script to work: 14 | * Permissions to describe AutoScaling groups and AWS Regions ([DescribeRegions](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeRegions.html), [DescribeAutoScalingGroups](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_DescribeAutoScalingGroups.html)) 15 | * API Gateway permissions to get API Gateway V2 APIs ([GetApis](https://docs.aws.amazon.com/apigatewayv2/latest/api-reference/apis.html#GetApis)) 16 | * Resilience Hub permissions ([ResolveAppVersionResources](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_ResolveAppVersionResources.html), [DescribeAppVersionResourcesResolutionStatus](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_DescribeAppVersionResourcesResolutionStatus.html), [ListAppVersionResources](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_ListAppVersionResources.html), [PublishAppVersion](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_PublishAppVersion.html), [CreateAppVersionAppComponent](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_CreateAppVersionAppComponent.html), [CreateAppVersionResource](https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_CreateAppVersionResource.html)) 17 | 18 | ## Execution 19 | 20 | Run [resource-group-enhancer.py](./resource-group-enhancer.py) in an environment that meets the pre-requisites. 21 | 22 | ``` 23 | python resource-group-enhancer.py 24 | ``` 25 | 26 | The script prompts for the following inputs: 27 | 28 | * ARN of the application on Resilience Hub (this is REQUIRED) 29 | * Tags associated with AutoScaling groups and API Gateways V2. This accepts a list of key:value pairs. For example, if you have two tags associated with your resources, you would enter them as - app:myapp, environment:prod (this is optional, only use this if you want to add AutoScaling groups or API Gateways V2) 30 | * List of global database names. You can enter a comma-separated list of global database names (this is optional, only use this if you want to add RDS global databases to your application) 31 | * List of Route 53 RecordSets. You can enter a comma-separated list of Route 53 RecordSets (this is optional, only use this if you want to add Route 53 RecordSets to your application) 32 | 33 | ## Output 34 | 35 | The script adds resources to the application on Resilience Hub based on inputs provided during execution. After adding the resources, a new version of the application will be published on Resilience Hub. 36 | -------------------------------------------------------------------------------- /resource-group-enhancer/resource-group-enhancer.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import time 3 | 4 | # Add AutoScaling groups 5 | def get_asgs(): 6 | asg_filter = [] 7 | 8 | # Create AutoScaling groups filter based on tags 9 | for i in tags: 10 | tag = { 11 | 'Name': 'tag:' + i, 12 | 'Values': [tags[i]] 13 | } 14 | asg_filter.append(tag) 15 | 16 | # Find AutoScaling groups with matching tags and add them to Resilience Hub 17 | for region in autoscaling_regions: 18 | try: 19 | autoscaling = boto3.client('autoscaling', region_name=region) 20 | asg_list = autoscaling.describe_auto_scaling_groups(Filters=asg_filter)['AutoScalingGroups'] 21 | for asg in asg_list: 22 | asg_resource_exists = check_resource_exists(asg['AutoScalingGroupName']) 23 | if asg_resource_exists: 24 | print('Resource already exists in the application - ' + asg['AutoScalingGroupName']) 25 | else: 26 | print('Adding resource - ' + asg['AutoScalingGroupName']) 27 | add_resource(asg['AutoScalingGroupName'], 'AWS::ResilienceHub::ComputeAppComponent', 'AWS::AutoScaling::AutoScalingGroup', region) 28 | except Exception as error: 29 | print(error) 30 | 31 | # Add API gateways v2 32 | def get_apigwv2(): 33 | for region in apigateway_regions: 34 | try: 35 | api_list = [] 36 | apigwv2 = boto3.client('apigatewayv2', region_name=region) 37 | apis = apigwv2.get_apis() 38 | for api in apis['Items']: 39 | if tags.items() <= api['Tags'].items(): 40 | api_list.append(api['ApiId']) 41 | for api in api_list: 42 | api_resource_exists = check_resource_exists(api) 43 | if api_resource_exists: 44 | print('Resource already exists in the application - ' + api) 45 | else: 46 | print('Adding resource - ' + api) 47 | add_resource(api, 'AWS::ResilienceHub::ComputeAppComponent', 'AWS::ApiGatewayV2::Api', region) 48 | except Exception as error: 49 | print(error) 50 | 51 | # Add RDS global databases 52 | def get_global_dbs(): 53 | for db in global_dbs: 54 | db_resource_exists = check_resource_exists(db) 55 | if db_resource_exists: 56 | print('Resource already exists in the application - ' + db) 57 | else: 58 | print('Adding resource - ' + db) 59 | add_resource(db, 'AWS::ResilienceHub::DatabaseAppComponent', 'AWS::RDS::GlobalCluster', 'us-east-1') 60 | 61 | # Add Route53 recordsets 62 | def get_recordsets(): 63 | for recordset in recordsets: 64 | recordset_resource_exists = check_resource_exists(recordset) 65 | if recordset_resource_exists: 66 | print('Resource already exists in the application - ' + recordset) 67 | else: 68 | print('Adding resource - ' + recordset) 69 | add_resource(recordset, 'AWS::ResilienceHub::NetworkingAppComponent', 'AWS::Route53::RecordSet', 'us-east-1') 70 | 71 | # Check if a resource already exists in the application 72 | def check_resource_exists(resourceId): 73 | if resourceId in str(current_resources): 74 | return True 75 | return False 76 | 77 | # Add a resource to the app 78 | def add_resource(resourceId, componentType, resourceType, region): 79 | # Create new app component 80 | arh.create_app_version_app_component( 81 | appArn=app_ARN, 82 | name=resourceId.replace('.', '-'), 83 | type=componentType 84 | ) 85 | 86 | # Add resource to draft app version 87 | arh.create_app_version_resource( 88 | appArn=app_ARN, 89 | appComponents=[ 90 | resourceId.replace('.', '-') 91 | ], 92 | awsRegion=region, 93 | logicalResourceId={ 94 | 'identifier': resourceId.replace('.', '-') 95 | }, 96 | physicalResourceId=resourceId, 97 | resourceName=resourceId.replace('.', '-'), 98 | resourceType=resourceType 99 | ) 100 | 101 | # Resolve resources 102 | resolve_resource = arh.resolve_app_version_resources( 103 | appArn=app_ARN, 104 | appVersion='draft' 105 | ) 106 | 107 | resolve_status = arh.describe_app_version_resources_resolution_status( 108 | appArn=app_ARN, 109 | appVersion='draft', 110 | resolutionId=resolve_resource['resolutionId'] 111 | )['status'] 112 | 113 | # Wait for resolve to complete 114 | while resolve_status in { 'Pending', 'InProgress' }: 115 | print('Waiting for resolution to complete. Current status - ' + resolve_status) 116 | time.sleep(2) 117 | resolve_status = arh.describe_app_version_resources_resolution_status( 118 | appArn=app_ARN, 119 | appVersion='draft', 120 | resolutionId=resolve_resource['resolutionId'] 121 | )['status'] 122 | 123 | if resolve_status == 'Success': 124 | print('Resource resolution succeeded for: ' + resourceType + ' - ' + resourceId) 125 | elif resolve_status == 'Failed': 126 | print('Resource resolution failed for: ' + resourceType + ' - ' + resourceId) 127 | 128 | if __name__ == "__main__": 129 | # Get resource metadata 130 | app_ARN = input('Enter the ARN of the application [REQUIRED]: ') 131 | tag_list = input('Enter tags (AutoScaling and API Gateway V2) as a comma-separated list of key:value pairs [Press Enter to skip]: ') 132 | global_dbs_list = input('Enter global database names as a comma-separated list [Press Enter to skip]: ') 133 | recordsets_list = input('Enter Route53 recordsets as a comma-separated list [Press Enter to skip]: ') 134 | 135 | # Create Resilience Hub client 136 | arh_region = app_ARN.split(':')[3] 137 | arh = boto3.client('resiliencehub', region_name=arh_region) 138 | 139 | # Get list of current resources in the application 140 | initial_resolve = arh.resolve_app_version_resources( 141 | appArn=app_ARN, 142 | appVersion='draft' 143 | ) 144 | 145 | initial_resolve_status = arh.describe_app_version_resources_resolution_status( 146 | appArn=app_ARN, 147 | appVersion='draft', 148 | resolutionId=initial_resolve['resolutionId'] 149 | )['status'] 150 | 151 | while initial_resolve_status in { 'Pending', 'InProgress' }: 152 | print('Waiting for resolution to complete. Current status - ' + initial_resolve_status) 153 | time.sleep(2) 154 | initial_resolve_status = arh.describe_app_version_resources_resolution_status( 155 | appArn=app_ARN, 156 | appVersion='draft', 157 | resolutionId=initial_resolve['resolutionId'] 158 | )['status'] 159 | 160 | current_resources = [] 161 | 162 | get_current_resources = arh.list_app_version_resources( 163 | appArn=app_ARN, 164 | appVersion='draft' 165 | ) 166 | 167 | current_resources.append(get_current_resources) 168 | 169 | while 'NextToken' in get_current_resources: 170 | get_current_resources = arh.list_app_version_resources( 171 | appArn=app_ARN, 172 | appVersion='draft' 173 | ) 174 | current_resources.append(get_current_resources) 175 | 176 | # Add resources based on user selections 177 | if tag_list != '': 178 | tags = {i.split(':')[0]: i.split(':')[1] for i in tag_list.replace(' ', '').split(',')} 179 | autoscaling_regions = boto3.Session().get_available_regions('autoscaling') 180 | apigateway_regions = boto3.Session().get_available_regions('apigateway') 181 | 182 | ec2 = boto3.client('ec2') 183 | available_regions = [region['RegionName'] for region in ec2.describe_regions(Filters=[ 184 | { 185 | 'Name': 'opt-in-status', 186 | 'Values': [ 187 | 'opt-in-not-required', 'opted-in' 188 | ] 189 | }, 190 | ])['Regions']] 191 | 192 | # Filter region list to only include available regions 193 | autoscaling_regions = [x for x in autoscaling_regions if x in available_regions] 194 | apigateway_regions = [x for x in apigateway_regions if x in available_regions] 195 | 196 | print('Adding AutoScaling groups') 197 | get_asgs() 198 | print('Adding API Gateway v2') 199 | get_apigwv2() 200 | 201 | if global_dbs_list != '': 202 | global_dbs = global_dbs_list.replace(' ', '').split(",") 203 | print('Adding RDS Global databases') 204 | get_global_dbs() 205 | 206 | if recordsets_list != '': 207 | recordsets = recordsets_list.replace(' ', '').split(",") 208 | print('Adding Route53 RecordSets') 209 | get_recordsets() 210 | 211 | # Publish new app version 212 | arh.publish_app_version( 213 | appArn=app_ARN 214 | ) 215 | print('New version of the application has been published.') 216 | -------------------------------------------------------------------------------- /scheduled-assessment-role/AwsResilienceHubPeriodicAssessmentRole.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | ScheduledAssessmentRole: 3 | Type: 'AWS::IAM::Role' 4 | Properties: 5 | AssumeRolePolicyDocument: 6 | Version: 2012-10-17 7 | Statement: 8 | - Effect: Allow 9 | Principal: 10 | Service: resiliencehub.amazonaws.com 11 | Action: 'sts:AssumeRole' 12 | Description: Role used by AWS Resilience Hub to run periodic assessments 13 | Policies: 14 | - PolicyName: AwsResilienceHubPeriodicAssessmentPolicy 15 | PolicyDocument: 16 | Version: 2012-10-17 17 | Statement: 18 | - Effect: Allow 19 | Action: 20 | - 'resiliencehub:*' 21 | Resource: '*' 22 | - Effect: Allow 23 | Action: 24 | - 'sns:GetTopicAttributes' 25 | - 'sns:ListSubscriptionsByTopic' 26 | - 'sns:GetSubscriptionAttributes' 27 | Resource: '*' 28 | - Effect: Allow 29 | Action: 30 | - 'cloudformation:DescribeStacks' 31 | - 'cloudformation:ListStackResources' 32 | - 'cloudformation:ValidateTemplate' 33 | Resource: '*' 34 | - Effect: Allow 35 | Action: 36 | - 'servicecatalog:GetApplication' 37 | - 'servicecatalog:ListAssociatedResources' 38 | Resource: '*' 39 | - Effect: Allow 40 | Action: 41 | - 'resource-groups:ListGroupResources' 42 | - 'resource-groups:GetGroup' 43 | - 'tag:GetResources' 44 | Resource: '*' 45 | - Effect: Allow 46 | Action: 47 | - 'cloudwatch:DescribeAlarms' 48 | - 'cloudwatch:GetMetricData' 49 | - 'cloudwatch:GetMetricStatistics' 50 | - 'cloudwatch:PutMetricData' 51 | Resource: '*' 52 | - Effect: Allow 53 | Action: 54 | - 'fis:GetExperimentTemplate' 55 | - 'fis:ListExperimentTemplates' 56 | - 'fis:ListExperiments' 57 | Resource: '*' 58 | - Effect: Allow 59 | Action: 60 | - 'ssm:GetParametersByPath' 61 | Resource: 'arn:aws:ssm:*::parameter/ResilienceHub/*' 62 | - Effect: Allow 63 | Action: 64 | - 's3:GetBucketPolicyStatus' 65 | - 's3:PutBucketVersioning' 66 | - 's3:GetBucketTagging' 67 | - 's3:GetBucketVersioning' 68 | - 's3:GetReplicationConfiguration' 69 | - 's3:ListBucket' 70 | - 's3:ListAllMyBuckets' 71 | - 's3:GetBucketLocation' 72 | Resource: '*' 73 | - Effect: Allow 74 | Action: 75 | - 's3:CreateBucket' 76 | - 's3:PutObject' 77 | - 's3:GetObject' 78 | Resource: 'arn:aws:s3:::aws-resilience-hub-artifacts-*' 79 | - Effect: Allow 80 | Action: 81 | - 'autoscaling:DescribeAutoScalingGroups' 82 | Resource: '*' 83 | - Effect: Allow 84 | Action: 85 | - 'ec2:DescribeAvailabilityZones' 86 | - 'ec2:DescribeVpcEndpoints' 87 | - 'ec2:DescribeFastSnapshotRestores' 88 | - 'ec2:DescribeInstances' 89 | - 'ec2:DescribeSnapshots' 90 | - 'ec2:DescribeVolumes' 91 | - 'ec2:DescribeNatGateways' 92 | - 'ec2:DescribeSubnets' 93 | - 'ec2:DescribeRegions' 94 | - 'ec2:DescribeTags' 95 | Resource: '*' 96 | - Effect: Allow 97 | Action: 98 | - 'rds:DescribeDBClusters' 99 | - 'rds:DescribeDBInstanceAutomatedBackups' 100 | - 'rds:DescribeDBInstances' 101 | - 'rds:DescribeGlobalClusters' 102 | - 'rds:DescribeDBClusterSnapshots' 103 | Resource: '*' 104 | - Effect: Allow 105 | Action: 106 | - 'elasticloadbalancing:DescribeTargetGroups' 107 | - 'elasticloadbalancing:DescribeLoadBalancers' 108 | - 'elasticloadbalancing:DescribeTargetHealth' 109 | Resource: '*' 110 | - Effect: Allow 111 | Action: 112 | - 'lambda:GetFunction' 113 | - 'lambda:GetFunctionConcurrency' 114 | - 'lambda:ListAliases' 115 | - 'lambda:ListVersionsByFunction' 116 | Resource: '*' 117 | - Effect: Allow 118 | Action: 119 | - 'ecr:DescribeRegistry' 120 | Resource: '*' 121 | - Effect: Allow 122 | Action: 123 | - 'backup:DescribeBackupVault' 124 | - 'backup:GetBackupPlan' 125 | - 'backup:GetBackupSelection' 126 | - 'backup:ListBackupPlans' 127 | - 'backup:ListBackupSelections' 128 | Resource: '*' 129 | - Effect: Allow 130 | Action: 131 | - 'dynamodb:ListTagsOfResource' 132 | - 'dynamodb:DescribeTable' 133 | - 'dynamodb:DescribeGlobalTable' 134 | - 'dynamodb:ListGlobalTables' 135 | - 'dynamodb:DescribeContinuousBackups' 136 | - 'dynamodb:DescribeLimits' 137 | Resource: '*' 138 | - Effect: Allow 139 | Action: 140 | - 'elasticfilesystem:DescribeMountTargets' 141 | - 'elasticfilesystem:DescribeFileSystems' 142 | Resource: '*' 143 | - Effect: Allow 144 | Action: 145 | - 'sqs:GetQueueUrl' 146 | - 'sqs:GetQueueAttributes' 147 | Resource: '*' 148 | - Effect: Allow 149 | Action: 150 | - 'apigateway:GET' 151 | Resource: '*' 152 | - Effect: Allow 153 | Action: 154 | - 'ecs:DescribeClusters' 155 | - 'ecs:ListServices' 156 | - 'ecs:DescribeServices' 157 | - 'ecs:DescribeCapacityProviders' 158 | - 'ecs:DescribeContainerInstances' 159 | - 'ecs:ListContainerInstances' 160 | - 'ecs:DescribeTaskDefinition' 161 | Resource: '*' 162 | - Effect: Allow 163 | Action: 164 | - 'route53-recovery-control-config:ListControlPanels' 165 | - 'route53-recovery-control-config:ListRoutingControls' 166 | - 'route53-recovery-readiness:ListReadinessChecks' 167 | - 'route53-recovery-readiness:GetResourceSet' 168 | - 'route53-recovery-readiness:GetReadinessCheckStatus' 169 | - 'route53-recovery-control-config:ListClusters' 170 | - 'route53:ListHealthChecks' 171 | - 'route53:ListHostedZones' 172 | - 'route53:ListResourceRecordSets' 173 | - 'route53:GetHealthCheck' 174 | Resource: '*' 175 | - Effect: Allow 176 | Action: 177 | - 'drs:DescribeSourceServers' 178 | - 'drs:DescribeJobs' 179 | - 'drs:GetReplicationConfiguration' 180 | Resource: '*' 181 | RoleName: AwsResilienceHubPeriodicAssessmentRole 182 | -------------------------------------------------------------------------------- /scheduled-assessment-role/README.md: -------------------------------------------------------------------------------- 1 | # IAM role for scheduled assessments 2 | 3 | Creates an [AWS Identity and Access Management (IAM) role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) using AWS CloudFormation for AWS Resilience Hub to use when running scheduled (daily) assessments of an application. 4 | 5 | ## Deployment 6 | 7 | Using this template, you can create an IAM role in a single AWS account by creating a [CloudFormation stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html), or deploy it to multiple accounts using [CloudFormation StackSets](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html). The role only needs to be created once per AWS account and will be used by Resilience Hub for all application assessments where scheduled assessments have been configured. 8 | 9 | * [AwsResilienceHubPeriodicAssessmentRole.yaml](./AwsResilienceHubPeriodicAssessmentRole.yaml) 10 | 11 | ## Resources 12 | 13 | The templates create a single resource of type [AWS::IAM::Role](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html). The role has a specific naming convention required by Resilience Hub - **AwsResilienceHubPeriodicAssessmentRole**. The trust policy of this role is minimally scoped so that Resilience Hub is the only principal that can assume it. For more information on the permissions associated with this role, please refer to [this documentation](https://docs.aws.amazon.com/resilience-hub/latest/userguide/security-iam-resilience-hub-permissions.html#security-iam-resilience-hub-primary-account). 14 | --------------------------------------------------------------------------------