├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DistributedWellArchitected-Horizontal-Eventbridge-annotated.png ├── DistributedWellArchitected-Horizontal-Eventbridge.svg ├── LICENSE ├── README.md ├── WAFR-Central.yaml ├── WAFR-Decorators └── python │ └── decorators.py ├── WAFR-GetWorkloadIDs └── lambda_function.py ├── WAFR-ShareAccept └── lambda_function.py ├── WAFR-ShareTemplate └── lambda_function.py ├── WAFR-ShareWorkload └── lambda_function.py ├── WAFR-TemplateShareAccept └── lambda_function.py ├── WAFR-UpdateAnswers └── lambda_function.py └── WAFR-Workload.yaml /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 | -------------------------------------------------------------------------------- /DistributedWellArchitected-Horizontal-Eventbridge-annotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-well-architected-tool-template-automation/c82b3f1f531ec0d9cd7ad66b47bfd1983f585080/DistributedWellArchitected-Horizontal-Eventbridge-annotated.png -------------------------------------------------------------------------------- /DistributedWellArchitected-Horizontal-Eventbridge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Central Well Architected Review template account
Central Well Architected Review template account
Well Architected Tool
Well Archit...
Central template
Central t...
Update shared workloads
Update shared workloads
AWS Step Functions workflow
AWS Step Functions workf...
UpdateAnswers fanout
Update...
Complete answers
Set Milestone
Complete answers...
Central answer owner
Central ans...
Workload account n
Workload account n
Workload owner
Workload ow...
Cloudtrail
Cloudtrail
Workload review
Workload...
Share workload with
 contributor access
Share workload with...
Publish incoming
share message
Publish incoming...
Well Architected Tool
Well Archit...
SNS Topic
SNS Topic
Accept
Accept
Triggers
Triggers
ShareAccept
ShareAc...
Cloudtrail
Cloudtrail
Triggers
Triggers
Update shared workload
Update shared workload
UpdateAnswers
UpdateA...
Management
Management
Templates updated
SNS topic
Templat...
Eventbridge
Eventbridge
Eventbridge
Eventbridge
Creates share
Creates share
ShareTemplate
ShareTe...
TemplateShareAccept
Templat...
1
1
2,8
2,8
3,7
3,7
4
4
5
5
10
10
9
9
SNS Topic
SNS Topic
ShareWorkload
ShareWo...
GetWorkloadIDs
GetWor...
6
6
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS-Well-Architected-Tool-Template-Automation 2 | 3 | ## Here for the first time? 4 | 5 | In October 2023, AWS released [Well-Architected Templates](https://aws.amazon.com/about-aws/whats-new/2023/10/reduce-duplication-well-architected-templates/) as a service feature which provides much of the functionality and features of this project and more. If you are looking at using Well-Architected Templates in your org, we strongly suggest you use the in-built service feature rather than this project. This project will remain for customers that may have adopted it before the launch of the service feature. 6 | 7 | You can read more about using AWS Well-Architected Templates at the [AWS docs](https://docs.aws.amazon.com/wellarchitected/latest/userguide/review-templates.html). 8 | 9 | ## Overview 10 | 11 | This sample provides a way for customers with distributed development teams to maintain consistency with Well-Architected Review answers. 12 | 13 | Many customers centralise aspects of their platform, security, operations and financial operations. However, how these capabilities are implemented within an environment can be tricky to communicate to individual development teams. When conducting an AWS Well Architected Review, it can be difficult to know where the boundary lays between a best practice being provided as a service from another team, or whether it is the development teams responsibility themselves to implement. Traditionally, Well-Architected Reviews would involve individuals from all involved teams. This does not always scale in larger enterprises with all involved teams being difficult to identify and locate. 14 | 15 | This solution allows central, service providing, best practice owners, to define templated answers in the AWS Well Architected tool. These answers are then replicated to each individual workload review. 16 | 17 | ## Prerequisites 18 | 19 | This solution requires you are already using [AWS Organizations](https://aws.amazon.com/organizations/) to manage your multi-account AWS environment. If you have not previously setup AWS Organizations, please review the [getting started documentation](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_getting-started.html). You will need your AWS Organizations ID to use as input parameters for the CloudFormation templates. 20 | 21 | ## Getting Started 22 | 23 | The solution is deployed using two [AWS CloudFormation](https://aws.amazon.com/cloudformation/) stacks. 24 | `WAFR-Central.yaml` should be deployed to an AWS account you designate as holding the Well Architected answer templates. 25 | `WAFR-Workload.yaml` should be deployed into your workload accounts, potentially as part of an account setup solution. 26 | 27 | These templates should be deployed using either the [AWS CLI](https://aws.amazon.com/cli/) or [rain](https://aws-cloudformation.github.io/rain/rain_deploy.html) as described in this readme. Attempting to deploy the templates via the CloudFormation console will result in errors as the Lambda function code first requires packaging. 28 | 29 | ### Template Options 30 | 31 | When deploying the solution, you can choose between overwriting workload answers with answers provided in the template, or appending templated answers to individual workloads. Appending answers is the default behaviour. 32 | 33 | #### WAFR-Central.yaml template 34 | * **OrganizationID** Your AWS Organizations ID, example: `o-ab12cdefgh` 35 | * **TemplatePrefix** The prefix used to identify a Well Architected Tool Workload acting as a template. Example & default: `CentralTemplate` 36 | 37 | #### WAFR-Workload.yaml template 38 | * **OrganizationID** Your AWS Organizations ID, example: `o-ab12cdefgh` 39 | * **TemplateAccountID** Account ID of the account hosting the template workloads, example: `111122223333` 40 | * **AWSTeamAccountID** (optional but empty string must be passed if not used) Account ID of your AWS Account Team Well Architected sharing account. This is useful if you are working with an AWS Solutions Architect or Technical Account Manager for providing workload visibility. Please speak to your account team for more information. Example: `444455556666` 41 | * **WAFRNewWorkloadShareTopicARN** This is output from the WellArchitectedTemplateAutomationCentral template. Example: `arn:aws:sns:eu-west-1:111122223333:WAFRNewWorkloadShareTopic` 42 | 43 | ### WAFR-Central Template Outputs 44 | 45 | The WAFR-Central CloudFormation template outputs two SNS topic ARNs. 46 | * WAFRNewWorkloadShareTopicARN 47 | * This is used for workload accounts to tell the template account a new workload has been shared and needs to be accepted. This value must be used as an input parameter for each workload stack deployment. 48 | * WAFRTemplateUpdateNotificationTopicARN 49 | * This topic is created on your behalf but its use is optional. A message will be placed onto this topic when a milestone has been created on a template, and those changes have been propagated to all shared workloads. This topic can be used to notify workload owners that an update to a template has occurred and that a review of the changes and how it affects their own workloads reviews should likely occur. 50 | 51 | ## How it works 52 | The solution operates as follows: 53 | 1. A central team logs into the designated “central template” AWS account. They create a new workload in the AWS WA Tool. The name of the workload must begin with “CentralTemplate” (or any other unique string of your choosing). 54 | 1. The central team answers their questions appropriate to their responsibility, and marks all others as “Question does not apply to this workload” 55 | 1. When an application team is ready to perform an AWS WA framework review, they open the AWS WA Tool in their own AWS account and create a new workload 56 | 1. The workload creation triggers an AWS Lambda function. This shares the workload with the central account (with contributor access), and publishes a message to an Amazon Simple Notification Service (Amazon SNS) topic in the central account 57 | 1. In the central account, a Lambda function is subscribed to the Amazon SNS topic from step 4. The Lambda function accepts the incoming share, then shares all templates back to the workload account (with read-only access). It posts a message to an Amazon SNS topic in the workload account, which triggers the acceptance of the incoming share(s). 58 | 1. The Lambda function in step 5 then triggers a second Lambda function to perform the update. It loops through all workloads with names beginning with “CentralTemplate.” For each template, it reads the selected choices for each question, as well as the notes. Then, it writes them to the newly created workload from step 3. Questions in the template marked as “question does not apply to this workload” are ignored. 59 | 1. The application team completes their AWS WA framework review as usual. As they proceed through the questions, they will see the pre-populated answers from the template. 60 | 1. Should a central team need to update their answers, they can simply update their template. When they are finished, they should create a milestone. 61 | 1. The milestone creation triggers an AWS Step Functions workflow. The workflow first calls a Lambda function to collect the workload IDs of all application workloads currently shared with the central account. Next, it calls a fan-out Lambda execution to update all application workloads based on the current answers in the templates. There is an option (specified at deployment time) to control whether this process should overwrite any existing answers or not. 62 | 1. As all workloads are now visible in the central account, the dashboards referenced in AWS WA labs can be used to further examine them and derive insights on common issues. 63 | 64 | ![Architecture Diagram](DistributedWellArchitected-Horizontal-Eventbridge-annotated.png) 65 | 66 | ## How to deploy 67 | 68 | This sample has been provided in raw CloudFormation to remain flexible for differing deployment mechanisms across customers. This documentation provides two ways to deploy the templates, however these are not the only way to deploy. The workload template could be part of an [AWS Service Catalogue](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/introduction.html) product for example and vended into workload account on a self-service basis. 69 | 70 | ### Option 1 - CloudFormation package & deploy 71 | 72 | Using CloudFormation to deploy, you will need an S3 bucket the role you are using to deploy has appropriate permissions to access. This bucket holds the Lambda code artifacts for use in the CloudFormation template. The Lambda artifacts can be packaged and uploaded to your S3 bucket using [`cloudformation package`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-cli-package.html). 73 | 74 | The `cloudformation package` commands in this readme expect an S3 bucket in each account you are deploying the workload to. You *could* [package](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-package.html) and store the Lambda assets to a bucket in a single account, just ensure to update the `Code` [property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#S3Bucket) in the CloudFormation templates to reflect the location of the packaged Lambda code. 75 | 76 | The [AWS CLI must be installed](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) to deploy via CloudFormation. 77 | 78 | #### WAFR-Central template with CloudFormation package & deploy 79 | 80 | From the repository root, run: 81 | ```sh 82 | aws cloudformation package \ 83 | --template-file WAFR-Central.yaml \ 84 | --output-template-file WAFR-Central.packaged.yaml \ 85 | --s3-bucket {your-bucket} 86 | 87 | aws cloudformation deploy \ 88 | --template-file WAFR-Central.packaged.yaml \ 89 | --stack-name WAFR-Central \ 90 | --parameter-overrides \ 91 | OrganizationID={your-organizationID} \ 92 | TemplatePrefix='CentralTemplate' \ 93 | --capabilities CAPABILITY_IAM 94 | ``` 95 | 96 | #### WAFR-Workload template with CloudFormation package & deploy 97 | 98 | From the repository root, run: 99 | ```sh 100 | aws cloudformation package \ 101 | --template-file WAFR-Workload.yaml \ 102 | --output-template-file WAFR-Workload.packaged.yaml \ 103 | --s3-bucket {your-bucket} 104 | 105 | aws cloudformation deploy \ 106 | --template-file WAFR-Workload.packaged.yaml \ 107 | --stack-name WAFR-Workload \ 108 | --parameter-overrides \ 109 | OrganizationID={your-organizationID} \ 110 | TemplateAccountID='111122223333' \ 111 | AWSTeamAccountID='444455556666' \ 112 | WAFRNewWorkloadShareTopicARN='arn:aws:sns:eu-west-1:111122223333:WAFRNewWorkloadShareTopic' \ 113 | --capabilities CAPABILITY_IAM 114 | ``` 115 | 116 | `WAFRNewWorkloadShareTopicARN` is required as a parameter for every workload account stack. This can be retrieved from the CloudFormation console in the template account account (where WAFR-Central) is deployed, or by running the following command in the template account. 117 | 118 | ```sh 119 | aws cloudformation describe-stacks --stack-name WAFR-Central --query "Stacks[0].Outputs[?OutputKey=='WAFRNewWorkloadShareTopicARN'].OutputValue" --output text 120 | ``` 121 | 122 | `AWSTeamAccountID` must be passed as a parameter even if you are not sharing your workload reviews with your AWS account team. If you are not sharing your workloads with AWS, please use `AWSTeamAccountID=''` 123 | 124 | 125 | 126 | ### Option 2 - Rain 127 | 128 | Using [rain](https://aws-cloudformation.github.io/rain/rain_deploy.html) simplifies the deployment of the templates into your accounts by abstracting the packaging and uploading of the Lambda artifacts. Rain will automatically create a bucket and package the assets for you in the account you are deploying the template to. 129 | 130 | #### WAFR-Central template with Rain 131 | 132 | To deploy the central account CloudFormation template with rain, ensure your session credentials are setup for the correct account and run from the repository root: 133 | 134 | `rain deploy WAFR-Central.yaml` 135 | 136 | Rain will then ask for the parameter inputs described above, package the lambda functions and deploy the stack to the account. 137 | 138 | #### WAFR-Workload template with Rain 139 | 140 | To deploy the workload account CloudFormation template with rain, ensure your session credentials are setup for the correct account and run from the repository root: 141 | 142 | `rain deploy WAFR-Workload.yaml` 143 | 144 | Rain will then ask for the parameter inputs described above, package the lambda functions and deploy the stack to the account. 145 | 146 | ## Resource tags 147 | 148 | We have not included any resource tags as part of the solution stacks. Tags can be added according to your standards following the usual methods for applying tags within CloudFormation templates. 149 | Syntax for adding tags can be found at the corresponding resource CloudFormation documentation pages such as: 150 | * [Lambda](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html) 151 | * [StepFunctions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html) 152 | * [SNS Topic](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sns-topic.html) 153 | 154 | 155 | ## Solution cost 156 | 157 | The overall cost of this solution is very low given its serverless architecture. 158 | There is no charge for the AWS Well-Architected Tool. 159 | 160 | Charges *may* be incurred for these components if outside of free tier: 161 | 162 | * [AWS Step Functions](https://aws.amazon.com/step-functions/pricing/) 163 | * [AWS Lambda](https://aws.amazon.com/lambda/pricing/) 164 | * [AWS SNS](https://aws.amazon.com/sns/pricing/) 165 | * [Amazon EventBridge](https://aws.amazon.com/eventbridge/pricing/) 166 | 167 | [AWS CloudTrail](https://aws.amazon.com/cloudtrail/pricing/) usage is within free tier. 168 | 169 | ## FAQ 170 | 171 | * Can we automate sharing workloads to an aws-owned account for account team visibility? 172 | * Yes, where your AWS account team has provisioned an account to share, this account ID can be provided in the WAFR-Workload template. 173 | 174 | 175 | * What happens if something is decentralised? eg. an organisational change means that the answer to SEC10 now resides with application teams, rather than the security team 176 | * Security team should change the answer to “none of these” then save a milestone, causing HRIs in every application workload. 177 | * Security team should then select “question does not apply to this workload” to bring that question out of scope. There is no need to save another milestone. 178 | 179 | 180 | * What if a question is partially owned by a central team, and partially by an application team? 181 | * The central team should mark the best practices within the question and complete the notes section to reflect the shared responsibility of the question. 182 | 183 | 184 | * What if a best practice is a shared responsibility between the central team and application team? 185 | * The central team should leave the best practice unmarked and use the notes section to define the position. 186 | 187 | 188 | * Once the review is completed, how can an application team identify the answers that came from central templates vs their own answers, for example when performing a subsequent review? 189 | * The notes section in questions populated from the "central" templates includes a delimiter to this effect. 190 | 191 | 192 | * What if an application team selects additional best practices in a question owned by a central team? 193 | * These selections will be overwritten the next time *any* central team creates a milestone 194 | 195 | 196 | * What if there’s a mismatch in lenses between central and application workloads? 197 | * The decision as to which lenses should apply to a workload should be made exclusively by the application team, so lenses will not be added or removed in application workloads. If there are central answers for any of the lenses selected by an application team, they will be propagated. 198 | -------------------------------------------------------------------------------- /WAFR-Central.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: '2010-09-09' 4 | Description: Template defining WAFR automation resources for the central template account 5 | 6 | Parameters: 7 | OrganizationID: 8 | Type: String 9 | Description: Enter the Organization ID which will be granted publish permission to an SNS topic 10 | TemplatePrefix: 11 | Type: String 12 | Default: CentralTemplate 13 | Description: Enter the prefix used to identify Template workloads eg. Template 14 | UpdateMode: 15 | Type: String 16 | Default: append 17 | AllowedValues: 18 | - append 19 | - overwrite 20 | Description: Set the update lambda function to append template answers to workloads or overwrite workload answers with template answers. Allowed values are [append, overwrite] 21 | LambdaLogLevel: 22 | Type: String 23 | Default: ERROR 24 | AllowedValues: 25 | - DEBUG 26 | - INFO 27 | - WARNING 28 | - ERROR 29 | - CRITICAL 30 | Description: Default log level for all lambda functions deployed 31 | 32 | Resources: 33 | StateMachine: 34 | Type: AWS::StepFunctions::StateMachine 35 | Properties: 36 | StateMachineName: UpdateWAFRAnswers 37 | StateMachineType: STANDARD 38 | DefinitionString: |- 39 | { 40 | "Comment": "Update Shared Well Architected Reviews from a centrally controlled template", 41 | "StartAt": "GetWorkloads", 42 | "States": { 43 | "GetWorkloads": { 44 | "Type": "Task", 45 | "Resource": "${GetWAFRWorkloadIDs}", 46 | "Next": "Update-All" 47 | }, 48 | "Update-All": { 49 | "Type": "Map", 50 | "ItemsPath": "$.Workloads", 51 | "MaxConcurrency": 5, 52 | "Iterator": { 53 | "StartAt": "Update", 54 | "States": { 55 | "Update": { 56 | "Type": "Task", 57 | "Resource": "${WAFRUpdateAnswers}", 58 | "End": true 59 | } 60 | } 61 | }, 62 | "Next": "NotifySNS" 63 | }, 64 | "NotifySNS": { 65 | "Type": "Task", 66 | "Resource": "arn:aws:states:::sns:publish", 67 | "Parameters": { 68 | "TopicArn": "${WAFRTemplateUpdateNotification}", 69 | "Message": "{\"default\": \"Well Architected Templates have been updated. Please review in your workload account\"}" 70 | }, 71 | "End": true 72 | } 73 | } 74 | } 75 | DefinitionSubstitutions: 76 | GetWAFRWorkloadIDs: !GetAtt GetWAFRWorkloadIDs.Arn 77 | WAFRUpdateAnswers: !GetAtt WAFRUpdateAnswers.Arn 78 | WAFRTemplateUpdateNotification: !Ref TemplateUpdateNotificationTopic 79 | RoleArn: !GetAtt WAFRAutomationStepFunctionRole.Arn 80 | DependsOn: 81 | - WAFRUpdateAnswers 82 | - GetWAFRWorkloadIDs 83 | 84 | WAFRUpdateAnswers: 85 | Type: AWS::Lambda::Function 86 | Properties: 87 | Code: ./WAFR-UpdateAnswers/lambda_function.py 88 | Description: Updates the answers in a given WAFR based on the template 89 | FunctionName: WAFRUpdateAnswers 90 | Handler: lambda_function.lambda_handler 91 | MemorySize: 128 92 | Role: !GetAtt WAFRLambdaExecutionRole.Arn 93 | Runtime: python3.9 94 | Layers: 95 | - !Ref LambdaLayerDecorators 96 | Timeout: 180 97 | Environment: 98 | Variables: 99 | MODE: !Ref UpdateMode 100 | TEMPLATE_PREFIX: !Ref TemplatePrefix 101 | LOGLEVEL: !Ref LambdaLogLevel 102 | Architectures: 103 | - arm64 104 | 105 | GetWAFRWorkloadIDs: 106 | Type: AWS::Lambda::Function 107 | Properties: 108 | Code: ./WAFR-GetWorkloadIDs/lambda_function.py 109 | Description: Gets the workload IDs for WAFR workloads shared with this account 110 | FunctionName: GetWAFRWorkloadIDs 111 | Handler: lambda_function.lambda_handler 112 | MemorySize: 128 113 | Role: !GetAtt WAFRLambdaExecutionRole.Arn 114 | Runtime: python3.9 115 | Layers: 116 | - !Ref LambdaLayerDecorators 117 | Timeout: 10 118 | Environment: 119 | Variables: 120 | TEMPLATE_PREFIX: !Ref TemplatePrefix 121 | LOGLEVEL: !Ref LambdaLogLevel 122 | Architectures: 123 | - arm64 124 | 125 | WAFRShareAccept: 126 | Type: AWS::Lambda::Function 127 | Properties: 128 | Code: ./WAFR-ShareAccept/lambda_function.py 129 | Description: Accepts incoming WAFR share 130 | FunctionName: WAFRShareAccept 131 | Handler: lambda_function.lambda_handler 132 | MemorySize: 128 133 | Role: !GetAtt WAFRLambdaShareAcceptExecutionRole.Arn 134 | Runtime: python3.9 135 | Layers: 136 | - !Ref LambdaLayerDecorators 137 | Timeout: 5 138 | Environment: 139 | Variables: 140 | UPDATE_FUNCTION : !Ref WAFRUpdateAnswers 141 | SHARE_TEMPLATE_FUNCTION: !Ref WAFRShareTemplate 142 | LOGLEVEL: !Ref LambdaLogLevel 143 | Architectures: 144 | - arm64 145 | 146 | WAFRShareTemplate: 147 | Type: AWS::Lambda::Function 148 | Properties: 149 | Code: ./WAFR-ShareTemplate/lambda_function.py 150 | Description: Shares WAFR Templates with workload accounts 151 | FunctionName: WAFRShareTemplate 152 | Handler: lambda_function.lambda_handler 153 | MemorySize: 128 154 | Role: !GetAtt WAFRLambdaExecutionRole.Arn 155 | Runtime: python3.9 156 | Layers: 157 | - !Ref LambdaLayerDecorators 158 | Timeout: 5 159 | Environment: 160 | Variables: 161 | TEMPLATE_PREFIX : !Ref TemplatePrefix 162 | LOGLEVEL: !Ref LambdaLogLevel 163 | Architectures: 164 | - arm64 165 | 166 | LambdaInvokePermission: 167 | Type: AWS::Lambda::Permission 168 | Properties: 169 | Action: lambda:InvokeFunction 170 | Principal: sns.amazonaws.com 171 | SourceArn: !Ref IncomingShareTopic 172 | FunctionName: !GetAtt WAFRShareAccept.Arn 173 | 174 | WAFRLambdaExecutionRole: 175 | Type: AWS::IAM::Role 176 | Properties: 177 | AssumeRolePolicyDocument: 178 | Version: '2012-10-17' 179 | Statement: 180 | - Effect: Allow 181 | Principal: 182 | Service: 183 | - lambda.amazonaws.com 184 | Action: 185 | - sts:AssumeRole 186 | Path: "/service-role/" 187 | Policies: 188 | - PolicyName: logs 189 | PolicyDocument: 190 | Version: '2012-10-17' 191 | Statement: 192 | - Effect: Allow 193 | Action: 194 | - logs:CreateLogGroup 195 | - logs:CreateLogStream 196 | - logs:PutLogEvents 197 | Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* 198 | - PolicyName: well-architected 199 | PolicyDocument: 200 | Version: '2012-10-17' 201 | Statement: 202 | - Effect: Allow 203 | Action: 204 | - wellarchitected:GetAnswer 205 | - wellarchitected:ListAnswers 206 | - wellarchitected:ListLensReviews 207 | - wellarchitected:UpdateAnswer 208 | - wellarchitected:UpdateShareInvitation 209 | - wellarchitected:CreateWorkloadShare 210 | - wellarchitected:ListWorkloadShares 211 | Resource: !Sub arn:aws:wellarchitected:${AWS::Region}:*:* 212 | - Effect: Allow 213 | Action: wellarchitected:ListWorkloads 214 | Resource: "*" 215 | - PolicyName: sns-publish 216 | PolicyDocument: 217 | Version: '2012-10-17' 218 | Statement: 219 | - Effect: Allow 220 | Action: 221 | - "sns:Publish" 222 | Resource: !Sub "arn:aws:sns:${AWS::Region}:*:WAFRTemplateShareTopic" 223 | 224 | WAFRLambdaShareAcceptExecutionRole: 225 | Type: AWS::IAM::Role 226 | Properties: 227 | AssumeRolePolicyDocument: 228 | Version: '2012-10-17' 229 | Statement: 230 | - Effect: Allow 231 | Principal: 232 | Service: 233 | - lambda.amazonaws.com 234 | Action: 235 | - sts:AssumeRole 236 | Path: "/service-role/" 237 | Policies: 238 | - PolicyName: logs 239 | PolicyDocument: 240 | Version: '2012-10-17' 241 | Statement: 242 | - Effect: Allow 243 | Action: 244 | - logs:CreateLogGroup 245 | - logs:CreateLogStream 246 | - logs:PutLogEvents 247 | Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* 248 | - PolicyName: well-architected 249 | PolicyDocument: 250 | Version: '2012-10-17' 251 | Statement: 252 | - Effect: Allow 253 | Action: 254 | - wellarchitected:UpdateShareInvitation 255 | Resource: !Sub arn:aws:wellarchitected:${AWS::Region}:${AWS::AccountId}:* 256 | - PolicyName: lambda-invoke 257 | PolicyDocument: 258 | Version: '2012-10-17' 259 | Statement: 260 | - Effect: Allow 261 | Action: 262 | - "lambda:InvokeFunction" 263 | Resource: 264 | - !GetAtt WAFRUpdateAnswers.Arn 265 | - !GetAtt WAFRShareTemplate.Arn 266 | 267 | IncomingShareTopicPolicy: 268 | Type: AWS::SNS::TopicPolicy 269 | Properties: 270 | PolicyDocument: 271 | Id: IncomingShareTopicPolicy 272 | Version: '2012-10-17' 273 | Statement: 274 | - Sid: allow-org-publish 275 | Effect: Allow 276 | Principal: 277 | AWS: "*" 278 | Action: sns:Publish 279 | Resource: !Ref IncomingShareTopic 280 | Condition: 281 | StringEquals: 282 | "aws:PrincipalOrgId": !Ref OrganizationID 283 | Topics: 284 | - !Ref IncomingShareTopic 285 | 286 | TemplateUpdateNotificationTopic: 287 | Type: AWS::SNS::Topic 288 | Properties: 289 | TopicName: WAFRTemplateUpdatedTopic 290 | 291 | TemplateUpdateNotificationTopicPolicy: 292 | Type: AWS::SNS::TopicPolicy 293 | Properties: 294 | PolicyDocument: 295 | Id: TemplateUpdateNotificationTopicPolicy 296 | Version: '2012-10-17' 297 | Statement: 298 | - Sid: allow-stepfunctions-publish 299 | Effect: Allow 300 | Principal: 301 | Service: 302 | - states.amazonaws.com 303 | Action: sns:Publish 304 | Resource: !Ref TemplateUpdateNotificationTopic 305 | - Sid: allow-org-subscribe 306 | Effect: Allow 307 | Principal: 308 | AWS: "*" 309 | Action: 310 | - "sns:GetTopicAttributes" 311 | - "sns:Subscribe" 312 | Resource: !Ref IncomingShareTopic 313 | Condition: 314 | StringEquals: 315 | "aws:PrincipalOrgId": !Ref OrganizationID 316 | Topics: 317 | - !Ref TemplateUpdateNotificationTopic 318 | 319 | WAFRAutomationStepFunctionRole: 320 | Type: AWS::IAM::Role 321 | Properties: 322 | AssumeRolePolicyDocument: 323 | Version: '2012-10-17' 324 | Statement: 325 | - Effect: Allow 326 | Principal: 327 | Service: 328 | - states.amazonaws.com 329 | Action: 330 | - sts:AssumeRole 331 | Path: "/service-role/" 332 | Policies: 333 | - PolicyName: sns 334 | PolicyDocument: 335 | Version: '2012-10-17' 336 | Statement: 337 | - Effect: Allow 338 | Action: 339 | - sns:Publish 340 | Resource: !Ref TemplateUpdateNotificationTopic 341 | - PolicyName: logs 342 | PolicyDocument: 343 | Version: '2012-10-17' 344 | Statement: 345 | - Effect: Allow 346 | Action: 347 | - logs:CreateLogDelivery 348 | - logs:GetLogDelivery 349 | - logs:UpdateLogDelivery 350 | - logs:DeleteLogDelivery 351 | - logs:ListLogDeliveries 352 | - logs:PutResourcePolicy 353 | - logs:DescribeResourcePolicies 354 | - logs:DescribeLogGroups 355 | Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* 356 | - PolicyName: lambda-invoke 357 | PolicyDocument: 358 | Version: '2012-10-17' 359 | Statement: 360 | - Effect: Allow 361 | Action: 362 | - "lambda:InvokeFunction" 363 | Resource: 364 | - !GetAtt WAFRUpdateAnswers.Arn 365 | - !GetAtt GetWAFRWorkloadIDs.Arn 366 | 367 | WAFRMilestoneEventbridgeRule: 368 | Type: AWS::Events::Rule 369 | Properties: 370 | Description: EventBridge rule to trigger update workflow on milestone creation 371 | EventPattern: { 372 | "source": ["aws.wellarchitected"], 373 | "detail": { 374 | "eventName": ["CreateMilestone"] 375 | } 376 | } 377 | Name: WAFR-NewMilestoneRule 378 | RoleArn: !GetAtt WAFRAutomationEventbridgeRole.Arn 379 | Targets: 380 | - Arn: !GetAtt StateMachine.Arn 381 | RoleArn: !GetAtt WAFRAutomationEventbridgeRole.Arn 382 | Id: StepFunctionStateMachine 383 | InputPath: $.detail.responseElements 384 | 385 | WAFRAutomationEventbridgeRole: 386 | Type: AWS::IAM::Role 387 | Properties: 388 | AssumeRolePolicyDocument: 389 | Version: '2012-10-17' 390 | Statement: 391 | - Effect: Allow 392 | Principal: 393 | Service: 394 | - events.amazonaws.com 395 | Action: 396 | - sts:AssumeRole 397 | Path: "/service-role/" 398 | Policies: 399 | - PolicyName: stepfunctions-invoke 400 | PolicyDocument: 401 | Version: '2012-10-17' 402 | Statement: 403 | - Effect: Allow 404 | Action: 405 | - "states:StartExecution" 406 | Resource: 407 | - !GetAtt StateMachine.Arn 408 | 409 | IncomingShareTopic: 410 | Type: AWS::SNS::Topic 411 | Properties: 412 | TopicName: WAFRNewWorkloadShareTopic 413 | Subscription: 414 | - Endpoint: !GetAtt WAFRShareAccept.Arn 415 | Protocol: "lambda" 416 | 417 | LambdaLayerDecorators: 418 | Type: AWS::Lambda::LayerVersion 419 | Properties: 420 | Content: ./WAFR-Decorators 421 | CompatibleArchitectures: 422 | - arm64 423 | CompatibleRuntimes: 424 | - python3.9 425 | Description: "Decorator functions including error handling for boto3 calls" 426 | LayerName: WAFR-Decorators 427 | 428 | Outputs: 429 | WAFRNewWorkloadShareTopicARN: 430 | Description: ARN of the SNS topic used to accept new workload shares. This is used as an input for workload account stacks 431 | Value: !Ref IncomingShareTopic 432 | 433 | WAFRTemplateUpdateNotificationTopicARN: 434 | Description: ARN of the SNS topic used to notify subscribers of any updates to the template workloads 435 | Value: !Ref TemplateUpdateNotificationTopic 436 | -------------------------------------------------------------------------------- /WAFR-Decorators/python/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | from botocore.exceptions import ClientError 4 | 5 | def catch_errors(handler): 6 | """ 7 | Decorator which performs catch all exception handling 8 | """ 9 | 10 | @functools.wraps(handler) 11 | def wrapper(*args, **kwargs): 12 | try: 13 | return handler(*args, **kwargs) 14 | except ClientError as e: 15 | return { 16 | "statusCode": e.response["ResponseMetadata"].get("HTTPStatusCode", 400), 17 | "body": json.dumps({"Message": "Client error: {}".format(str(e)),}), 18 | } 19 | except ValueError as e: 20 | return { 21 | "statusCode": 400, 22 | "body": json.dumps({"Message": "Invalid request: {}".format(str(e)),}), 23 | } 24 | except Exception as e: 25 | return { 26 | "statusCode": 400, 27 | "body": json.dumps( 28 | {"Message": "Unable to process request: {}".format(str(e)),} 29 | ), 30 | } 31 | 32 | return wrapper 33 | -------------------------------------------------------------------------------- /WAFR-GetWorkloadIDs/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import boto3 4 | import logging 5 | import os 6 | from decorators import catch_errors 7 | 8 | # Configure logging 9 | LOGLEVEL = os.environ.get('LOGLEVEL', 'ERROR').upper() 10 | logging.basicConfig(level=LOGLEVEL) 11 | logger = logging.getLogger() 12 | logger.setLevel(LOGLEVEL) 13 | 14 | # Setup boto3 Clients 15 | wa_client = boto3.client('wellarchitected') 16 | 17 | @catch_errors 18 | def getWorkloadIDs(): 19 | workloads = wa_client.list_workloads() 20 | workloadIDs = [] 21 | 22 | for workload in workloads['WorkloadSummaries']: 23 | workloadIDs.append({ 24 | "WorkloadName": workload["WorkloadName"], 25 | "WorkloadId": workload["WorkloadId"] 26 | }) 27 | 28 | #Strip template ID 29 | workloadIDs = stripCentralTemplate(workloadIDs) 30 | workloadObject = {'Workloads': workloadIDs} 31 | logger.debug(workloadObject) 32 | logger.info('Found {} Well-Architected workloads.'.format(len(workloadIDs))) 33 | return (workloadObject) 34 | 35 | 36 | def stripCentralTemplate(workloadIDs): 37 | logger.debug(workloadIDs) 38 | templatePrefix = os.environ.get('TEMPLATE_PREFIX', 'CentralTemplate') 39 | res = [i for i in workloadIDs if not (templatePrefix in i['WorkloadName'] )] 40 | return (res) 41 | 42 | 43 | def lambda_handler(event, context): 44 | return (getWorkloadIDs()) 45 | -------------------------------------------------------------------------------- /WAFR-ShareAccept/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import boto3 4 | import json 5 | import logging 6 | import os 7 | from decorators import catch_errors 8 | 9 | # Setup logging 10 | LOGLEVEL = os.environ.get('LOGLEVEL', 'ERROR').upper() 11 | logging.basicConfig(level=LOGLEVEL) 12 | logger = logging.getLogger() 13 | logger.setLevel(LOGLEVEL) 14 | 15 | # Setup boto3 clients 16 | wa_client = boto3.client('wellarchitected') 17 | lambda_client = boto3.client('lambda') 18 | 19 | 20 | @catch_errors 21 | def invokeWAFRShareTemplate(workloadAccountId, workloadSNSTopic): 22 | # Call Lambda to share the templates back to the workload account 23 | payload = { 24 | 'AccountId': workloadAccountId, 25 | 'SNSTopic': workloadSNSTopic 26 | } 27 | response = lambda_client.invoke( 28 | FunctionName=os.environ['SHARE_TEMPLATE_FUNCTION'], 29 | InvocationType='Event', 30 | LogType='None', 31 | Payload=json.dumps(payload) 32 | ) 33 | return (response) 34 | 35 | 36 | @catch_errors 37 | def invokeWAFRUpdateAnswers(workloadId): 38 | # Invoke Lambda to update shared workload answers 39 | # Construct Payload 40 | payload = {'WorkloadId': workloadId} 41 | response = lambda_client.invoke( 42 | FunctionName=os.environ["UPDATE_FUNCTION"], 43 | InvocationType='Event', 44 | LogType='None', 45 | Payload=json.dumps(payload) 46 | ) 47 | return (response) 48 | 49 | 50 | @catch_errors 51 | def acceptShare(shareId): 52 | wa_acceptance = wa_client.update_share_invitation(ShareInvitationId=shareId, ShareInvitationAction='ACCEPT') 53 | logger.info("Accepted workload: {}".format(wa_acceptance)) 54 | return (wa_acceptance) 55 | 56 | 57 | def lambda_handler(event, context): 58 | logger.debug(event) 59 | 60 | message = json.loads(event['Records'][0]['Sns']['Message']) 61 | logger.debug(message) 62 | workloadId = message['WorkloadId'] 63 | shareId = message['ShareId'] 64 | workloadAccountId = message['WorkloadAccountId'] 65 | workloadSNSTopic = message['InboundShareTopic'] 66 | 67 | acceptShare(shareId) 68 | 69 | invokeWAFRUpdateAnswers(workloadId) 70 | invokeWAFRShareTemplate(workloadAccountId, workloadSNSTopic) 71 | 72 | return { 73 | 'statusCode': 200 74 | } 75 | 76 | -------------------------------------------------------------------------------- /WAFR-ShareTemplate/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import json 4 | import boto3 5 | import os 6 | import logging 7 | from decorators import catch_errors 8 | 9 | LOGLEVEL = os.environ.get('LOGLEVEL', 'ERROR').upper() 10 | logging.basicConfig(level=LOGLEVEL) 11 | logger = logging.getLogger() 12 | logger.setLevel(LOGLEVEL) 13 | 14 | wa_client = boto3.client('wellarchitected') 15 | sns_client = boto3.client('sns') 16 | 17 | 18 | @catch_errors 19 | def getTemplateIDs(): 20 | """ Returns a list of template ID's from central account """ 21 | templateWorkloads = wa_client.list_workloads( 22 | WorkloadNamePrefix=os.environ.get("TEMPLATE_PREFIX", "CentralTemplate") 23 | ) 24 | templateIDs = [] 25 | 26 | for workload in templateWorkloads['WorkloadSummaries']: 27 | templateIDs.append(workload['WorkloadId']) 28 | 29 | return (templateIDs) 30 | 31 | 32 | @catch_errors 33 | def lambda_handler(event, context): 34 | logger.debug(event) 35 | templateIDs = getTemplateIDs() 36 | 37 | for templateID in templateIDs: 38 | 39 | # Check it hasn't already been shared with the destination account 40 | listshares = wa_client.list_workload_shares( 41 | WorkloadId = templateID, 42 | SharedWithPrefix = event['AccountId'] 43 | ) 44 | 45 | # Share it if not already shared 46 | if not 'WorkloadShareSummaries' in listshares or len(listshares['WorkloadShareSummaries']) == 0: 47 | share_response = wa_client.create_workload_share( 48 | WorkloadId = templateID, 49 | SharedWith= event['AccountId'], 50 | PermissionType='READONLY' 51 | ) 52 | 53 | # Publish message to SNS topic in destination account to trigger share acceptance 54 | share_id = share_response["ShareId"] 55 | sns_message = { 56 | "ShareId": share_id, 57 | } 58 | sns_response = sns_client.publish( 59 | TopicArn= event['SNSTopic'], 60 | Message=json.dumps(sns_message) 61 | ) 62 | 63 | logger.info("Workload account: share response: {}".format(share_response)) 64 | logger.info("SNS response: {}".format(sns_response)) 65 | logger.info("Share ID: {}".format(share_id)) 66 | logger.info("Message: {}".format(json.dumps(sns_message))) 67 | 68 | return { 69 | 'statusCode': 200, 70 | } 71 | -------------------------------------------------------------------------------- /WAFR-ShareWorkload/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import json 4 | import boto3 5 | import os 6 | import logging 7 | from decorators import catch_errors 8 | 9 | #Setup logging 10 | LOGLEVEL = os.environ.get('LOGLEVEL', 'ERROR').upper() 11 | logging.basicConfig(level=LOGLEVEL) 12 | logger = logging.getLogger() 13 | logger.setLevel(LOGLEVEL) 14 | 15 | #boto3 clients 16 | wa_client = boto3.client('wellarchitected') 17 | sts_client = boto3.client('sts') 18 | sns_client = boto3.client('sns') 19 | 20 | 21 | @catch_errors 22 | def publishSNSMessage(WorkloadAccountId, WorkloadId, shareResponse): 23 | sns_message = { 24 | "WorkloadId": WorkloadId, 25 | "ShareId": shareResponse["ShareId"], 26 | "WorkloadAccountId": WorkloadAccountId, 27 | "InboundShareTopic": os.environ['INCOMING_SHARE_TOPIC_ARN'] 28 | } 29 | sns_response = sns_client.publish( 30 | TopicArn=os.environ["NEW_WORKLOAD_TOPIC_ARN"], 31 | Message=json.dumps(sns_message) 32 | ) 33 | logger.debug("SNS response: {}".format(sns_response)) 34 | logger.debug("Message: {}".format(json.dumps(sns_message))) 35 | return sns_response 36 | 37 | 38 | @catch_errors 39 | def createWorkloadShare(workloadID, shareAccountID, permission): 40 | share_response = wa_client.create_workload_share( 41 | WorkloadId=workloadID, 42 | SharedWith=shareAccountID, 43 | PermissionType=permission 44 | ) 45 | logger.debug("Share response: {}".format(share_response)) 46 | logger.debug("Share ID: {}".format(share_response["ShareId"])) 47 | return share_response 48 | 49 | 50 | @catch_errors 51 | def listWorkloadShares(WorkloadId, sharePrefix): 52 | listShares = wa_client.list_workload_shares( 53 | WorkloadId=WorkloadId, 54 | SharedWithPrefix=sharePrefix 55 | ) 56 | return listShares 57 | 58 | 59 | @catch_errors 60 | def getWorkloadAccountID(): 61 | WorkloadAccountId = sts_client.get_caller_identity()['Account'] 62 | return WorkloadAccountId 63 | 64 | 65 | def lambda_handler(event, context): 66 | logger.debug(event) 67 | 68 | # Check it hasn't already been shared with the destination account 69 | # And share it if not already shared 70 | listShares = listWorkloadShares(event["WorkloadId"], os.environ['TEMPLATE_ACCOUNT_ID']) 71 | workloadAccountID = getWorkloadAccountID() 72 | 73 | if 'WorkloadShareSummaries' not in listShares or len(listShares['WorkloadShareSummaries']) == 0: 74 | #Publish SNS Message 75 | shareResponse = createWorkloadShare(event["WorkloadId"], os.environ["TEMPLATE_ACCOUNT_ID"], 'CONTRIBUTOR') 76 | publishSNSMessage(workloadAccountID, event["WorkloadId"], shareResponse) 77 | 78 | # Share with AWS Account Team if specified: 79 | if os.environ["AWSTeamAccountID"]: 80 | # Check it hasn't already been shared with the destination account 81 | # And share it if not already shared 82 | listShares = listWorkloadShares(event["WorkloadId"], os.environ['AWSTeamAccountID']) 83 | 84 | if 'WorkloadShareSummaries' not in listShares or len(listShares['WorkloadShareSummaries']) == 0: 85 | shareResponse = createWorkloadShare(event["WorkloadId"], os.environ["AWSTeamAccountID"], 'READONLY') 86 | logger.info("aws team: share response: {}".format(shareResponse)) 87 | 88 | return { 89 | 'statusCode': 200, 90 | } -------------------------------------------------------------------------------- /WAFR-TemplateShareAccept/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import boto3 4 | import json 5 | import logging 6 | from decorators import catch_errors 7 | 8 | #Setup logging 9 | LOGLEVEL = os.environ.get('LOGLEVEL', 'ERROR').upper() 10 | logging.basicConfig(level=LOGLEVEL) 11 | logger = logging.getLogger() 12 | logger.setLevel(LOGLEVEL) 13 | 14 | #Setup boto3 clients 15 | wa_client = boto3.client('wellarchitected') 16 | lambda_client = boto3.client('lambda') 17 | 18 | @catch_errors 19 | def lambda_handler(event, context): 20 | logger.debug(event) 21 | 22 | message = json.loads(event['Records'][0]['Sns']['Message']) 23 | logger.debug(message) 24 | shareId = message['ShareId'] 25 | 26 | wa_acceptance = wa_client.update_share_invitation(ShareInvitationId = shareId, ShareInvitationAction='ACCEPT') 27 | logger.info("Accepted workload: {}".format(wa_acceptance)) 28 | 29 | return { 30 | 'statusCode': 200 31 | } 32 | -------------------------------------------------------------------------------- /WAFR-UpdateAnswers/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import logging 5 | import os 6 | import boto3 7 | from decorators import catch_errors 8 | 9 | 10 | LOGLEVEL = os.environ.get('LOGLEVEL', 'ERROR').upper() 11 | logging.basicConfig(level=LOGLEVEL) 12 | logger = logging.getLogger() 13 | logger.setLevel(LOGLEVEL) 14 | 15 | # Boto3 Clients 16 | wa_client = boto3.client('wellarchitected') 17 | 18 | 19 | @catch_errors 20 | def getTemplateIDs(): 21 | """ Returns a list of template ID's from central account """ 22 | templateWorkloads = wa_client.list_workloads( 23 | WorkloadNamePrefix=os.environ.get("TEMPLATE_PREFIX", "CentralTemplate") 24 | ) 25 | templateIDs = [] 26 | 27 | for workload in templateWorkloads['WorkloadSummaries']: 28 | templateIDs.append(workload['WorkloadId']) 29 | 30 | return (templateIDs) 31 | 32 | 33 | @catch_errors 34 | def getTemplateLenses(templateID): 35 | """ Returns lenses are active on the template """ 36 | lens_reviews_result = wa_client.list_lens_reviews( 37 | WorkloadId=templateID 38 | )['LensReviewSummaries'] 39 | 40 | logger.debug(lens_reviews_result) 41 | logger.info(f'Template {templateID} has used {len(lens_reviews_result)} lens') 42 | return (lens_reviews_result) 43 | 44 | 45 | @catch_errors 46 | def getLensAnswers(workload_id, lens): 47 | list_answers_result = [] 48 | 49 | lens_name = lens['LensName'] 50 | logger.info(f'Looking at {lens_name} answers for template {workload_id}') 51 | logger.debug(lens) 52 | 53 | #Sort out custom lenses 54 | lens_alias = determineCustomLens(lens) 55 | 56 | logger.debug(f'Using {lens_alias} as the lens alias') 57 | 58 | # Get first 50 answers for the lens 59 | list_answers_response = wa_client.list_answers( 60 | WorkloadId=workload_id, LensAlias=lens_alias, MaxResults=50 61 | ) 62 | 63 | # Flatten the answer result to include LensAlias and Milestone Number 64 | #for answer_result in list_answers_response['AnswerSummaries']: 65 | # answer_result['LensAlias'] = list_answers_response['LensAlias'] 66 | 67 | # Find applicable answers 68 | res = [i for i in list_answers_response['AnswerSummaries'] if (i['IsApplicable'] == True)] 69 | logger.debug('Find only applicable answers') 70 | logger.debug(res) 71 | list_answers_result.extend(res) 72 | 73 | #Get next 50 answers for the lens if needed 74 | while 'NextToken' in list_answers_response: 75 | logger.info(f'NextToken found for workload {workload_id} in lens {lens_alias}') 76 | 77 | list_answers_response = wa_client.list_answers( 78 | WorkloadId=workload_id, LensAlias=lens_alias, NextToken=list_answers_response['NextToken'], MaxResults=50 79 | ) 80 | 81 | # Find applicable answers 82 | res = [i for i in list_answers_response['AnswerSummaries'] if (i['IsApplicable'] == True)] 83 | logger.debug('Find only applicable answers') 84 | logger.debug(res) 85 | list_answers_result.extend(res) 86 | 87 | return (list_answers_result) 88 | 89 | 90 | @catch_errors 91 | def updateWorkloadOverwrite(event, templateID, lens): 92 | """ Overwrites any selections or notes a workload owner may have entered 93 | into a review with answers provided by the template. 94 | 95 | This is invoked with the overwrite value for the mode environment variable 96 | """ 97 | # get answers for lens 98 | answersList = getLensAnswers(templateID, lens) 99 | 100 | # for each answer/question id in answers 101 | for question in answersList: 102 | logger.debug('Question data') 103 | logger.debug(question) 104 | 105 | # deal with custom lenses 106 | lens_alias = determineCustomLens(lens) 107 | 108 | # get notes for each answer 109 | answer = wa_client.get_answer( 110 | WorkloadId=templateID, 111 | LensAlias=lens_alias, 112 | QuestionId=question['QuestionId'], 113 | )['Answer'] 114 | logger.debug(answer) 115 | 116 | # Sort out notes 117 | payloadNotes = "" 118 | if 'Notes' in answer: 119 | # Concatenate answer notes 120 | payloadNotes = "{}\nFROM CENTRAL TEAM:\n{}".format( 121 | payloadNotes, 122 | answer['Notes'] 123 | ) 124 | 125 | if len(payloadNotes) > 2084: 126 | payloadNotes = "{}... NOTES CLIPPED".format( 127 | payloadNotes[:2066] 128 | ) 129 | 130 | # update answer with payload 131 | response = wa_client.update_answer( 132 | WorkloadId=event['WorkloadId'], 133 | LensAlias=lens_alias, 134 | QuestionId=question['QuestionId'], 135 | SelectedChoices=question['SelectedChoices'], 136 | Notes=payloadNotes, 137 | IsApplicable=True 138 | ) 139 | return (response) 140 | 141 | 142 | @catch_errors 143 | def updateWorkloadAppend(event, templateID, lens): 144 | """ Appends both selected choices for a question as well as notes. 145 | If concatenated question notes are longer than the 2084 limit, 146 | the string is clipped 147 | 148 | This is invoked with the append value for the mode environment variable 149 | """ 150 | # get answers for lens 151 | answersList = getLensAnswers(templateID, lens) 152 | 153 | # deal with custom lenses 154 | lens_alias = determineCustomLens(lens) 155 | 156 | # for each answer/question id in answers 157 | for question in answersList: 158 | templateAnswer = getAnswer( 159 | templateID, 160 | lens_alias, 161 | question['QuestionId'] 162 | ) 163 | workloadAnswer = getAnswer( 164 | event['WorkloadId'], 165 | lens_alias, 166 | question['QuestionId'] 167 | ) 168 | # Concatenate choices in a set 169 | selectedChoicesSet = set( 170 | templateAnswer['SelectedChoices']).union( 171 | set(workloadAnswer['SelectedChoices']) 172 | ) 173 | 174 | # Deal with "none of these" 175 | # Ignore this if there's only one item selected 176 | if len(selectedChoicesSet) > 1: 177 | choice_to_remove = "" 178 | # See if any of the items end in 'no' 179 | for choice in selectedChoicesSet: 180 | if choice.endswith('_no'): 181 | choice_to_remove = choice 182 | if choice_to_remove: 183 | selectedChoicesSet.remove(choice_to_remove) 184 | 185 | payloadNotes = "" 186 | if 'Notes' in workloadAnswer: 187 | payloadNotes = "{}".format(workloadAnswer['Notes']) 188 | #Find existing central notes and trim them 189 | if "FROM CENTRAL TEAM:" in payloadNotes: 190 | payloadNotes = payloadNotes[:payloadNotes.find( 191 | "\nFROM CENTRAL TEAM:")] 192 | 193 | if 'Notes' in templateAnswer: 194 | # Concatenate answer notes 195 | payloadNotes = "{}\nFROM CENTRAL TEAM:\n{}".format( 196 | payloadNotes, 197 | templateAnswer['Notes'] 198 | ) 199 | 200 | if len(payloadNotes) > 2084: 201 | payloadNotes = "{}... NOTES CLIPPED".format( 202 | payloadNotes[:2066] 203 | ) 204 | 205 | # update answer with payload 206 | response = wa_client.update_answer( 207 | WorkloadId=event['WorkloadId'], 208 | LensAlias=lens_alias, 209 | QuestionId=question['QuestionId'], 210 | SelectedChoices=list(selectedChoicesSet), 211 | Notes=payloadNotes, 212 | IsApplicable=True 213 | ) 214 | return response 215 | 216 | 217 | def determineCustomLens(lens): 218 | """ Custom Lenses don't have an alias so we need to return the Arn """ 219 | if 'LensAlias' not in lens: 220 | lens_alias = lens['LensArn'] 221 | else: 222 | lens_alias = lens['LensAlias'] 223 | return lens_alias 224 | 225 | 226 | @catch_errors 227 | def getAnswer(workloadID, lens, question): 228 | """ Returns answer section for given workload, lens and question """ 229 | logger.debug('Question data') 230 | logger.debug(question) 231 | # get notes for each answer 232 | answer = wa_client.get_answer( 233 | WorkloadId=workloadID, 234 | LensAlias=lens, 235 | QuestionId=question, 236 | )['Answer'] 237 | logger.debug(answer) 238 | return (answer) 239 | 240 | 241 | def getMode(): 242 | """Using the mode environment variable 243 | chooses between overwrite and append. 244 | 245 | Accepted values are: 246 | overwrite 247 | append - default 248 | """ 249 | mode = os.environ.get('MODE', 'append') 250 | return mode 251 | 252 | 253 | def lambda_handler(event, context): 254 | logger.debug(event) 255 | templateIDs = getTemplateIDs() 256 | mode = getMode() 257 | logger.info("Updating templates in {} mode".format(mode)) 258 | 259 | for templateID in templateIDs: 260 | lenses = getTemplateLenses(templateID) 261 | logger.debug(lenses) 262 | for lens in lenses: 263 | logger.debug(lens) 264 | if mode == "overwrite": 265 | update = updateWorkloadOverwrite(event, templateID, lens) 266 | if mode == "append": 267 | update = updateWorkloadAppend(event, templateID, lens) 268 | 269 | return update 270 | -------------------------------------------------------------------------------- /WAFR-Workload.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: '2010-09-09' 4 | Description: Template defining WAFR automation resources for the central template account 5 | 6 | Parameters: 7 | OrganizationID: 8 | Type: String 9 | Description: Enter the Organization ID which will be granted publish permission to an SNS topic 10 | TemplateAccountID: 11 | Type: String 12 | Description: Enter the AWS Account ID of the account you are holding your templates. 13 | AllowedPattern: ^[0-9]{12}$ 14 | ConstraintDescription: Must contain a 12-digit AWS account AccountId 15 | AWSTeamAccountID: 16 | Type: String 17 | Description: (Optional) Enter the AWS account ID of your AWS account team's WAFR sharing account. 18 | AllowedPattern: ^$|^[0-9]{12}$ 19 | ConstraintDescription: Must contain empty string or a 12-digit AWS account AccountId 20 | WAFRNewWorkloadShareTopicARN: 21 | Type: String 22 | Description: Enter the ARN of the WAFRNewWorkloadShareTopic SNS topic in the central Well-Architected account. 23 | LambdaLogLevel: 24 | Type: String 25 | Default: ERROR 26 | AllowedValues: 27 | - DEBUG 28 | - INFO 29 | - WARNING 30 | - ERROR 31 | - CRITICAL 32 | Description: Default log level for all lambda functions deployed 33 | 34 | Conditions: 35 | HasAWSTeamAccountID: !Not [!Equals ["", !Ref AWSTeamAccountID]] 36 | 37 | Resources: 38 | WAFRShareWorkload: 39 | Type: AWS::Lambda::Function 40 | Properties: 41 | Code: ./WAFR-ShareWorkload/lambda_function.py 42 | Description: Shares workload with central account 43 | FunctionName: WAFRShareWorkload 44 | Handler: lambda_function.lambda_handler 45 | MemorySize: 128 46 | Role: !GetAtt WAFRLambdaExecutionRole.Arn 47 | Runtime: python3.9 48 | Layers: 49 | - !Ref LambdaLayerDecorators 50 | Timeout: 10 51 | Environment: 52 | Variables: 53 | TEMPLATE_ACCOUNT_ID: !Ref TemplateAccountID 54 | AWS_TEAM_ACCOUNT_ID: !If [HasAWSTeamAccountID, !Ref AWSTeamAccountID, !Ref AWS::NoValue] 55 | NEW_WORKLOAD_TOPIC_ARN: !Ref WAFRNewWorkloadShareTopicARN 56 | INCOMING_SHARE_TOPIC_ARN: !Ref IncomingShareTopic 57 | LOGLEVEL: !Ref LambdaLogLevel 58 | Architectures: 59 | - arm64 60 | 61 | WAFRNewWorkloadEventbridgeRule: 62 | Type: AWS::Events::Rule 63 | Properties: 64 | Description: EventBridge rule to trigger sharing workflow on new workload creation 65 | EventPattern: { 66 | "source": ["aws.wellarchitected"], 67 | "detail": { 68 | "eventName": ["CreateWorkload"] 69 | } 70 | } 71 | Name: WAFR-NewWorkloadRule 72 | Targets: 73 | - Arn: !GetAtt WAFRShareWorkload.Arn 74 | Id: LambdaTrigger 75 | InputPath: $.detail.responseElements 76 | 77 | EventbridgeLambdaPermission: 78 | Type: AWS::Lambda::Permission 79 | Properties: 80 | Action: lambda:InvokeFunction 81 | FunctionName: !GetAtt WAFRShareWorkload.Arn 82 | Principal: events.amazonaws.com 83 | SourceArn: !GetAtt WAFRNewWorkloadEventbridgeRule.Arn 84 | 85 | WAFRLambdaExecutionRole: 86 | Type: AWS::IAM::Role 87 | Properties: 88 | AssumeRolePolicyDocument: 89 | Version: '2012-10-17' 90 | Statement: 91 | - Effect: Allow 92 | Principal: 93 | Service: 94 | - lambda.amazonaws.com 95 | Action: 96 | - sts:AssumeRole 97 | Path: "/service-role/" 98 | Policies: 99 | - PolicyName: logs 100 | PolicyDocument: 101 | Version: '2012-10-17' 102 | Statement: 103 | - Effect: Allow 104 | Action: 105 | - logs:CreateLogGroup 106 | - logs:CreateLogStream 107 | - logs:PutLogEvents 108 | Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* 109 | - PolicyName: sns-publish 110 | PolicyDocument: 111 | Version: '2012-10-17' 112 | Statement: 113 | - Effect: Allow 114 | Action: 115 | - "sns:Publish" 116 | Resource: !Ref WAFRNewWorkloadShareTopicARN 117 | - PolicyName: well-architected 118 | PolicyDocument: 119 | Version: '2012-10-17' 120 | Statement: 121 | - Effect: Allow 122 | Action: 123 | - wellarchitected:CreateWorkloadShare 124 | - wellarchitected:ListShareInvitations 125 | - wellarchitected:ListWorkloadShares 126 | - wellarchitected:UpdateShareInvitation 127 | Resource: !Sub arn:aws:wellarchitected:${AWS::Region}:*:* 128 | 129 | IncomingShareTopicPolicy: 130 | Type: AWS::SNS::TopicPolicy 131 | Properties: 132 | PolicyDocument: 133 | Id: IncomingShareTopicPolicy 134 | Version: '2012-10-17' 135 | Statement: 136 | - Sid: allow-org-publish 137 | Effect: Allow 138 | Principal: 139 | AWS: "*" 140 | Action: sns:Publish 141 | Resource: !Ref IncomingShareTopic 142 | Condition: 143 | StringEquals: 144 | "aws:PrincipalOrgId": !Ref OrganizationID 145 | Topics: 146 | - !Ref IncomingShareTopic 147 | 148 | IncomingShareTopic: 149 | Type: AWS::SNS::Topic 150 | Properties: 151 | TopicName: WAFRTemplateShareTopic 152 | Subscription: 153 | - Endpoint: !GetAtt WAFRTemplateShareAccept.Arn 154 | Protocol: "lambda" 155 | 156 | WAFRTemplateShareAccept: 157 | Type: AWS::Lambda::Function 158 | Properties: 159 | Code: ./WAFR-TemplateShareAccept/lambda_function.py 160 | Description: Accepts incoming WAFR share 161 | FunctionName: WAFRTemplateShareAccept 162 | Handler: lambda_function.lambda_handler 163 | MemorySize: 128 164 | Role: !GetAtt WAFRLambdaExecutionRole.Arn 165 | Runtime: python3.9 166 | Layers: 167 | - !Ref LambdaLayerDecorators 168 | Environment: 169 | Variables: 170 | LOGLEVEL: !Ref LambdaLogLevel 171 | Timeout: 5 172 | Architectures: 173 | - arm64 174 | 175 | LambdaInvokePermission: 176 | Type: AWS::Lambda::Permission 177 | Properties: 178 | Action: lambda:InvokeFunction 179 | Principal: sns.amazonaws.com 180 | SourceArn: !Ref IncomingShareTopic 181 | FunctionName: !GetAtt WAFRTemplateShareAccept.Arn 182 | 183 | LambdaLayerDecorators: 184 | Type: AWS::Lambda::LayerVersion 185 | Properties: 186 | Content: ./WAFR-Decorators 187 | CompatibleArchitectures: 188 | - arm64 189 | CompatibleRuntimes: 190 | - python3.9 191 | Description: "Decorator functions including error handling for boto3 calls" 192 | LayerName: WAFR-Decorators 193 | --------------------------------------------------------------------------------