├── ExcludeAccountIDs(sample).csv ├── readme-images ├── aha-logo.png ├── workflow.png ├── aha_banner.png ├── architecture.png ├── aha-arch-multi-region.png └── aha-arch-single-region.png ├── CODE_OF_CONDUCT.md ├── terraform ├── Terraform_MGMT_ROLE │ ├── terraform.tfvars │ └── Terraform_MGMT_ROLE.tf └── Terraform_DEPLOY_AHA │ ├── terraform.tfvars │ └── Terraform_DEPLOY_AHA.tf ├── LICENSE ├── .gitignore ├── CFN_MGMT_ROLE.yml ├── CONTRIBUTING.md ├── new_aha_event_schema.md ├── CFN_DEPLOY_AHA.yml ├── messagegenerator.py ├── README.md └── handler.py /ExcludeAccountIDs(sample).csv: -------------------------------------------------------------------------------- 1 | 000000000000 2 | 111111111111 3 | -------------------------------------------------------------------------------- /readme-images/aha-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-health-aware/HEAD/readme-images/aha-logo.png -------------------------------------------------------------------------------- /readme-images/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-health-aware/HEAD/readme-images/workflow.png -------------------------------------------------------------------------------- /readme-images/aha_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-health-aware/HEAD/readme-images/aha_banner.png -------------------------------------------------------------------------------- /readme-images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-health-aware/HEAD/readme-images/architecture.png -------------------------------------------------------------------------------- /readme-images/aha-arch-multi-region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-health-aware/HEAD/readme-images/aha-arch-multi-region.png -------------------------------------------------------------------------------- /readme-images/aha-arch-single-region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-health-aware/HEAD/readme-images/aha-arch-single-region.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /terraform/Terraform_MGMT_ROLE/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # Region for IAM role creation - it's global service, so we will be just using one region. 2 | aha_primary_region="us-east-1" 3 | 4 | # Tags applied to all resources - using module provider. Update them per your requirement. 5 | default_tags = { 6 | Application = "AHA-Solution" 7 | Environment = "DEV" 8 | auto-delete = "no" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /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 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to 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 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # AHA Lambda Function package 3 | lambda_function.zip 4 | 5 | # ---> Terraform 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | # .tfstate files 10 | *.tfstate 11 | *.tfstate.* 12 | 13 | # tf lock file 14 | .terraform.lock.hcl 15 | 16 | # Crash log files 17 | crash.log 18 | 19 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 20 | # .tfvars files are managed as part of configuration and so should be included in 21 | # version control. 22 | # 23 | # example.tfvars 24 | 25 | # Ignore override files as they are usually used to override resources locally and so 26 | # are not checked in 27 | override.tf 28 | override.tf.json 29 | *_override.tf 30 | *_override.tf.json 31 | 32 | # Include override files you do wish to add to version control using negated pattern 33 | # 34 | # !example_override.tf 35 | 36 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 37 | # example: *tfplan* 38 | -------------------------------------------------------------------------------- /terraform/Terraform_DEPLOY_AHA/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # Input variables for Terraform_DEPLOY_AHA.tf (AHA Solution deploy using terraform) 2 | # 3 | # Customize Alerts/Notifications 4 | aha_primary_region="us-east-1" 5 | aha_secondary_region="" 6 | AWSOrganizationsEnabled="No" 7 | AWSHealthEventType="issue | accountNotification | scheduledChange" 8 | 9 | # Communication Channels - Slack/Microsoft Teams/Amazon Chime And/or EventBridge 10 | SlackWebhookURL="" 11 | MicrosoftTeamsWebhookURL="" 12 | AmazonChimeWebhookURL="" 13 | EventBusName="" 14 | 15 | # Email Setup - For Alerting via Email 16 | FromEmail="none@domain.com" 17 | ToEmail="none@domain.com" 18 | Subject="AWS Health Alert" 19 | 20 | # More Configurations - Optional 21 | # By default, AHA reports events affecting all AWS regions. 22 | # If you want to report on certain regions you can enter up to 10 in a comma separated format. 23 | EventSearchBack="1" 24 | Regions="all regions" 25 | ManagementAccountRoleArn="" 26 | ExcludeAccountIDs="" 27 | 28 | # Tags applied to all resources - using module provider. Update them per your requirement. 29 | default_tags = { 30 | Application = "AHA-Solution" 31 | Environment = "PROD" 32 | auto-delete = "no" 33 | } 34 | 35 | # commands to apply changes 36 | # terraform init 37 | # terraform plan 38 | # terraform apply 39 | -------------------------------------------------------------------------------- /CFN_MGMT_ROLE.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Deploy Cross-Account Role for PHD access 3 | Parameters: 4 | OrgMemberAccountId: 5 | Type: String 6 | AllowedPattern: '^\d{12}$' 7 | Description: AWS Account ID of the AWS Organizations Member Account that will run AWS Health Aware 8 | Resources: 9 | AWSHealthAwareRoleForPHDEvents: 10 | Type: "AWS::IAM::Role" 11 | Properties: 12 | Description: "Grants access to PHD events" 13 | Path: / 14 | AssumeRolePolicyDocument: 15 | Version: '2012-10-17' 16 | Statement: 17 | - Action: 18 | - sts:AssumeRole 19 | Effect: Allow 20 | Principal: 21 | AWS: !Sub 'arn:aws:iam::${OrgMemberAccountId}:root' 22 | Policies: 23 | - PolicyName: AllowHealthCalls 24 | PolicyDocument: 25 | Statement: 26 | - Effect: Allow 27 | Action: 28 | - health:DescribeAffectedAccountsForOrganization 29 | - health:DescribeAffectedEntitiesForOrganization 30 | - health:DescribeEventDetailsForOrganization 31 | - health:DescribeEventsForOrganization 32 | - health:DescribeEventDetails 33 | - health:DescribeEvents 34 | - health:DescribeEventTypes 35 | - health:DescribeAffectedEntities 36 | Resource: "*" 37 | - PolicyName: AllowsDescribeOrg 38 | PolicyDocument: 39 | Statement: 40 | - Effect: Allow 41 | Action: 42 | - organizations:ListAccounts 43 | - organizations:ListAWSServiceAccessForOrganization 44 | - organizations:DescribeAccount 45 | Resource: "*" 46 | Outputs: 47 | AWSHealthAwareRoleForPHDEventsArn: 48 | Value: !GetAtt AWSHealthAwareRoleForPHDEvents.Arn 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /terraform/Terraform_MGMT_ROLE/Terraform_MGMT_ROLE.tf: -------------------------------------------------------------------------------- 1 | # Deploy Cross-Account Role for PHD access 2 | 3 | # Parameters 4 | provider "aws" { 5 | region = var.aha_primary_region 6 | default_tags { 7 | tags = "${var.default_tags}" 8 | } 9 | } 10 | 11 | variable "aha_primary_region" { 12 | description = "Primary region where AHA solution will be deployed" 13 | type = string 14 | default = "us-east-1" 15 | } 16 | 17 | variable "default_tags" { 18 | description = "Tags used for the AWS resources created by this template" 19 | type = map 20 | default = { 21 | Application = "AHA-Solution" 22 | } 23 | } 24 | 25 | variable "OrgMemberAccountId" { 26 | type = string 27 | description = "AWS Account ID of the AWS Organizations Member Account that will run AWS Health Aware" 28 | 29 | validation { 30 | condition = length(var.OrgMemberAccountId) == 12 31 | error_message = "The OrgMemberAccountId must be a valid AWS Account ID." 32 | } 33 | } 34 | 35 | # Random id generator 36 | resource "random_string" "resource_code" { 37 | length = 8 38 | special = false 39 | upper = false 40 | } 41 | 42 | # aws_iam_role.AWSHealthAwareRoleForPHDEvents: 43 | resource "aws_iam_role" "AWSHealthAwareRoleForPHDEvents" { 44 | assume_role_policy = jsonencode( 45 | { 46 | Statement = [ 47 | { 48 | Action = "sts:AssumeRole" 49 | Effect = "Allow" 50 | Principal = { 51 | AWS = "arn:aws:iam::${var.OrgMemberAccountId}:root" 52 | } 53 | }, 54 | ] 55 | Version = "2012-10-17" 56 | } 57 | ) 58 | name = "AWSHealthAwareRoleForPHDEvents-${random_string.resource_code.result}" 59 | description = "Grants access to PHD event" 60 | path = "/" 61 | 62 | inline_policy { 63 | name = "AllowHealthCalls" 64 | policy = jsonencode( 65 | { 66 | Statement = [ 67 | { 68 | Action = [ 69 | "health:DescribeAffectedAccountsForOrganization", 70 | "health:DescribeAffectedEntitiesForOrganization", 71 | "health:DescribeEventDetailsForOrganization", 72 | "health:DescribeEventsForOrganization", 73 | "health:DescribeEventDetails", 74 | "health:DescribeEvents", 75 | "health:DescribeEventTypes", 76 | "health:DescribeAffectedEntities", 77 | ] 78 | Effect = "Allow" 79 | Resource = "*" 80 | }, 81 | ] 82 | } 83 | ) 84 | } 85 | inline_policy { 86 | name = "AllowsDescribeOrg" 87 | policy = jsonencode( 88 | { 89 | Statement = [ 90 | { 91 | Action = [ 92 | "organizations:ListAccounts", 93 | "organizations:ListAWSServiceAccessForOrganization", 94 | "organizations:DescribeAccount", 95 | ] 96 | Effect = "Allow" 97 | Resource = "*" 98 | }, 99 | ] 100 | } 101 | ) 102 | } 103 | } 104 | 105 | 106 | output "AWSHealthAwareRoleForPHDEventsArn" { 107 | value = aws_iam_role.AWSHealthAwareRoleForPHDEvents.arn 108 | } 109 | 110 | -------------------------------------------------------------------------------- /new_aha_event_schema.md: -------------------------------------------------------------------------------- 1 | # Readme for new AHA Event schema 2 | 3 | ## New AHA Event Schema 4 | 5 | With release X.Y.Z, AHA includes an updated format for events published to EventBridge. Building on the [existing event format](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#health-event-types) published by AWS Health, AHA enriches it with additional data from the Health API and AWS Organizations to enable new options for filtering in EventBridge. 6 | 7 | 8 | >Note: If you used the previous { "title": "Title", "value": "Value" } schema in your rules, you must update your rules to reflect the new schema when deploying the new version of AHA 9 | 10 | ### Schema: 11 | 12 | ``` 13 | { 14 | "version": "0", 15 | "id": "7bf73129-1428-4cd3-a780-95db273d1602", 16 | "detail-type": "AHA Event", 17 | "source": "aha", 18 | "account": "123456789012", 19 | "time": "2022-07-14T03:56:10Z", 20 | "region": "region of the eventbus", 21 | "resources": [ 22 | "i-1234567890abcdef0" 23 | ], 24 | "detail": { 25 | "eventArn": "arn:aws:health:region::event/id", 26 | "service": "service", 27 | "eventTypeCode": "typecode", 28 | "eventTypeCategory": "category", 29 | "region": "region of the Health event", 30 | "startTime": "2022-07-02 12:33:26.951000+00:00", 31 | "endTime": "2022-07-02 12:33:26.951000+00:00", 32 | "lastUpdatedTime": "2022-07-02 12:36:18.576000+00:00", 33 | "statusCode": "status", 34 | "eventScopeCode": "scopecode", 35 | "eventDescription": { 36 | "latestDescription": "description" 37 | }, 38 | "affectedEntities": [{ 39 | "entityValue": "i-1234567890abcdef0", 40 | "awsAccountId": "account number", 41 | "awsAccountName": "account name" 42 | }] 43 | } 44 | } 45 | ``` 46 | 47 | ### AHA added properties 48 | 49 | **eventScopeCode:** Specifies if the Health event is a public AWS service event or an account-specific event. 50 | Values: *string -* `PUBLIC | ACCOUNT_SPECIFIC` 51 | 52 | **statusCode:** Reflects whether the event is ongoing, resolved or in the case of scheduled maintenance, upcoming. 53 | Values: *string -* `open | closed | upcoming` 54 | 55 | **affectedEntities:** For ACCOUNT_SPECIFIC events, AHA includes expanded detail on resources. **affectedEntities** includes the listed **resources**, each as an **entitityValue** with the resource ID (as it appears in events for single accounts). AHA adds the related **awsAccountId** and In AWS Organizations, **awsAccountName** of the resource. 56 | Values: *entity object(s). May be empty if no resources are listed* 57 | 58 | 59 | ## EventBridge pattern examples 60 | 61 | As a primer we recommended you review the [EventBridge EventPatterns](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html) documentation and examples on [Content filtering in Amazon EventBridge event patterns](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html) 62 | 63 | Use the following sample event published by AWS Health Aware to test matching in the provided examples: 64 | 65 | ``` 66 | { 67 | "version": "0", 68 | "id": "e47c4390-b295-ce6f-7e94-f13083d7bb90", 69 | "detail-type": "AHA Event", 70 | "source": "aha", 71 | "account": "`234567890123`", 72 | "time": "2022-07-20T18:26:17Z", 73 | "region": "us-east-1", 74 | "resources": [ 75 | "vpn-0d0e3eeefe6aabb0d" 76 | ], 77 | "detail": { 78 | "arn": "arn:aws:health:us-east-1::event/VPN/AWS_VPN_REDUNDANCY_LOSS/AWS_VPN_REDUNDANCY_LOSS-1656151378267-7672191-IAD", 79 | "service": "VPN", 80 | "eventTypeCode": "AWS_VPN_REDUNDANCY_LOSS", 81 | "eventTypeCategory": "accountNotification", 82 | "region": "us-east-1", 83 | "startTime": "2022-06-25 10:00:48.868000+00:00", 84 | "lastUpdatedTime": "2022-06-25 10:02:58.371000+00:00", 85 | "statusCode": "open", 86 | "eventScopeCode": "ACCOUNT_SPECIFIC", 87 | "eventDescription": { 88 | "latestDescription": "Your VPN Connection associated with this event in the us-east-1 Region had a momentary lapse of redundancy as one of two tunnel endpoints was replaced. Connectivity on the second tunnel was not affected during this time. Both tunnels are now operating normally.\n\nReplacements can occur for several reasons, including health, software upgrades, customer-initiated modifications, and when underlying hardware is retired. If you have configured your VPN Customer Gateway to use both tunnels, then your VPN Connection will have utilized the alternate tunnel during the replacement process. For more on tunnel endpoint replacements, please see our documentation [1].\n\nIf you have not configured your VPN Customer Gateway to use both tunnels, then your VPN Connection may have been interrupted during the replacement. We encourage you to configure your router to use both tunnels. You can obtain the VPN Connection configuration recommendations for several types of VPN devices from the AWS Management Console [2]. On the \"Amazon VPC\" tab, select \"VPN Connections\". Then highlight the VPN Connection and choose \"Download Configuration\".\n\n[1] https://docs.aws.amazon.com/vpn/latest/s2svpn/monitoring-vpn-health-events.html\n[2] https://console.aws.amazon.com" 89 | }, 90 | "affectedEntities": [{ 91 | "entityValue": "vpn-0d0e3eeefe6aabb0d", 92 | "awsAccountId": "987654321987", 93 | "awsAccountName": "Prod-Apps" 94 | }] 95 | } 96 | } 97 | ``` 98 | 99 | 100 | To write a rule that matches resources found in the event, reference the **resources** key of the JSON event, and provide an event pattern to the EventBridge rule. Example 1 matches on an exact resource - “*vpn-0d0e3eeefe6aabb0d*”. Example 2 matches any resource starting with "*vpn-*" 101 | **Example 1:** 102 | 103 | ``` 104 | { 105 | "resources": [ 106 | "vpn-0d0e3eeefe6aabb0d" 107 | ] 108 | } 109 | ``` 110 | 111 | 112 | **Example 2:** 113 | 114 | ``` 115 | { 116 | "resources": [ 117 | {"prefix": "vpn-"} 118 | ] 119 | } 120 | ``` 121 | 122 | 123 | To match based on a specific service, note that **service** is nested within the **detail** key in the JSON structure, so we reference both **detail** and **service**. Example 3 matches the VPN service, and Example 4 matches EC2 OR S3 (will not match the sample event). To get a list of all service names used by AWS Health you can use the cli command - `aws health describe-event-types` 124 | 125 | **Example 3:** 126 | 127 | ``` 128 | { 129 | "detail": { 130 | "service": ["VPN"] 131 | } 132 | } 133 | ``` 134 | 135 | 136 | **Example 4:** 137 | 138 | ``` 139 | { 140 | "detail": { 141 | "service": ["EC2", "S3"] 142 | } 143 | } 144 | ``` 145 | 146 | 147 | To match events based on an AWS account name or number, use the following patterns. Take note of the additional levels of nesting based on the sample event. Example 5 matches a specific account number as **awsAccountId**. Example 6 matches a specific account name, and Example 7 adds an additional field of **eventTypeCategory** along with the “prefix” filter pattern which will match any value in the **awsAccountName** field that starts with “*Prod*” similar to a wildcard match of “*Prod**” 148 | 149 | **Example 5:** 150 | 151 | ``` 152 | { 153 | "detail": { 154 | "affectedEntities": { 155 | "awsAccountId": ["987654321987"] 156 | } 157 | } 158 | } 159 | ``` 160 | 161 | 162 | **Example 6:** 163 | 164 | ``` 165 | { 166 | "detail": { 167 | "affectedEntities": { 168 | "awsAccountName": ["Prod-Apps"] 169 | } 170 | } 171 | } 172 | ``` 173 | 174 | 175 | **Example 7:** 176 | 177 | ``` 178 | { 179 | "detail": { 180 | "eventTypeCategory": ["accountNotification"], 181 | "affectedEntities": { 182 | "awsAccountName": [{"prefix": "Prod"}] 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | 189 | Combine any of the patterns listed to create more specific rules. Note that all patterns in the rule must match for the EventBridge rule to trigger. Example 8 will only match when all 3 conditions exist in an AHA event - **service** = *VPN*, **region** = *us-east-1* and **awsAccountId** = *987654321098* 190 | 191 | **Example 8:** 192 | 193 | ``` 194 | { 195 | "detail": { 196 | "service": ["VPN"], 197 | "region": ["us-east-1"], 198 | "affectedEntities": { 199 | "awsAccountId": ["987654321987"] 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | 206 | As a best practice, also include `"source": ["aha"]` in your pattern if the event bus contains events generated by other sources. 207 | 208 | **Example 9:** 209 | 210 | ``` 211 | { 212 | "source": ["aha"], 213 | "detail": { 214 | "service": ["VPN"], 215 | "region": ["us-east-1"], 216 | "affectedEntities": { 217 | "awsAccountId": ["987654321987"] 218 | } 219 | } 220 | } 221 | ``` 222 | -------------------------------------------------------------------------------- /CFN_DEPLOY_AHA.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: CloudFormation Template for AWS Health Aware (AHA) 3 | Metadata: 4 | 'AWS::CloudFormation::Interface': 5 | ParameterGroups: 6 | - Label: 7 | default: Customize Alerts/Notifications 8 | Parameters: 9 | - AWSOrganizationsEnabled 10 | - AWSHealthEventType 11 | - Label: 12 | default: Package Information 13 | Parameters: 14 | - S3Bucket 15 | - S3Key 16 | - Label: 17 | default: >- 18 | Communication Channels - Slack/Microsoft Teams/Amazon Chime And/or 19 | EventBridge 20 | Parameters: 21 | - SlackWebhookURL 22 | - MicrosoftTeamsWebhookURL 23 | - AmazonChimeWebhookURL 24 | - EventBusName 25 | - Label: 26 | default: Email Setup - For Alerting via Email 27 | Parameters: 28 | - FromEmail 29 | - ToEmail 30 | - Subject 31 | - Label: 32 | default: More Configurations - Optional 33 | Parameters: 34 | - EventSearchBack 35 | - Regions 36 | - ManagementAccountRoleArn 37 | - SecondaryRegion 38 | - AccountIDs 39 | ParameterLabels: 40 | AWSOrganizationsEnabled: 41 | default: AWS Organizations Enabled? 42 | ManagementAccountRoleArn: 43 | default: ARN of the AWS Organizations Management Account assume role (if using) 44 | AWSHealthEventType: 45 | default: The types of events to get alerted on 46 | S3Bucket: 47 | default: Name of S3 Bucket 48 | S3Key: 49 | default: Name of .zip file in S3 Bucket 50 | SlackWebhookURL: 51 | default: Slack Webhook URL 52 | MicrosoftTeamsWebhookURL: 53 | default: Microsoft Teams Webhook URL 54 | AmazonChimeWebhookURL: 55 | default: Amazon Chime Webhook URL 56 | FromEmail: 57 | default: Email From 58 | ToEmail: 59 | default: Email To 60 | Subject: 61 | default: Subject of Email 62 | HealthAPIFrequency: 63 | default: Hours back to search for events 64 | Regions: 65 | default: Which regions to search for events in 66 | SecondaryRegion: 67 | default: Deploy in secondary region? 68 | AccountIDs: 69 | default: Exclude any account numbers? 70 | Conditions: 71 | UsingSlack: !Not [!Equals [!Ref SlackWebhookURL, None]] 72 | UsingTeams: !Not [!Equals [!Ref MicrosoftTeamsWebhookURL, None]] 73 | UsingChime: !Not [!Equals [!Ref AmazonChimeWebhookURL, None]] 74 | UsingEventBridge: !Not [!Equals [!Ref EventBusName, None]] 75 | UsingSecrets: !Or [!Condition UsingSlack, !Condition UsingTeams, !Condition UsingChime, !Condition UsingEventBridge, !Condition UsingCrossAccountRole] 76 | UsingCrossAccountRole: !Not [!Equals [!Ref ManagementAccountRoleArn, None]] 77 | NotUsingMultiRegion: !Equals [!Ref SecondaryRegion, 'No'] 78 | UsingMultiRegion: !Not [!Equals [!Ref SecondaryRegion, 'No']] 79 | TestCondition: !Equals ['true', 'false'] 80 | UsingMultiRegionTeams: !And [!Condition UsingTeams, !Condition UsingMultiRegion] 81 | UsingMultiRegionSlack: !And [!Condition UsingSlack, !Condition UsingMultiRegion] 82 | UsingMultiRegionEventBridge: !And [!Condition UsingEventBridge, !Condition UsingMultiRegion] 83 | UsingMultiRegionChime: !And [!Condition UsingChime, !Condition UsingMultiRegion] 84 | UsingMultiRegionCrossAccountRole: !And [!Condition UsingCrossAccountRole, !Condition UsingMultiRegion] 85 | UsingAccountIds: !Not [!Equals [!Ref AccountIDs, None]] 86 | Parameters: 87 | AWSOrganizationsEnabled: 88 | Description: >- 89 | You can receive both PHD and SHD alerts if you're using AWS Organizations. 90 | If you are, make sure to enable Organizational Health View: 91 | (https://docs.aws.amazon.com/health/latest/ug/aggregate-events.html) to 92 | aggregate all PHD events in your AWS Organization. If not, you can still 93 | get SHD alerts. 94 | Default: 'No' 95 | AllowedValues: 96 | - 'Yes' 97 | - 'No' 98 | Type: String 99 | SecondaryRegion: 100 | Description: You can deploy this in a secondary region for resiliency. As a result, 101 | the DynamoDB table will become a Global DynamoDB table. Regions that support 102 | Global DynamoDB tables are listed 103 | Default: 'No' 104 | AllowedValues: 105 | - 'No' 106 | - us-east-1 107 | - us-east-2 108 | - us-west-1 109 | - us-west-2 110 | - ap-south-1 111 | - ap-northeast-2 112 | - ap-southeast-1 113 | - ap-southeast-2 114 | - ap-northeast-1 115 | - ca-central-1 116 | - eu-central-1 117 | - eu-west-1 118 | - eu-west-2 119 | - eu-west-3 120 | - sa-east-1 121 | Type: String 122 | ManagementAccountRoleArn: 123 | Description: Arn of the IAM role in the top-level management account for collecting PHD Events. 'None' if deploying into the top-level management account. 124 | Type: String 125 | Default: None 126 | AWSHealthEventType: 127 | Description: >- 128 | Select the event type that you want AHA to report on. Refer to 129 | https://docs.aws.amazon.com/health/latest/APIReference/API_EventType.html for more information on EventType. 130 | Default: 'issue | accountNotification | scheduledChange' 131 | AllowedValues: 132 | - 'issue | accountNotification | scheduledChange' 133 | - 'issue' 134 | Type: String 135 | S3Bucket: 136 | Description: >- 137 | Name of your S3 Bucket where the AHA Package .zip resides. Just the name 138 | of the bucket (e.g. my-s3-bucket) 139 | Type: String 140 | S3Key: 141 | Description: >- 142 | Name of the .zip in your S3 Bucket. Just the name of the file (e.g. 143 | aha-v1.0.zip) 144 | Type: String 145 | EventBusName: 146 | Description: >- 147 | This is to ingest alerts into AWS EventBridge. Enter the event bus name if 148 | you wish to send the alerts to the AWS EventBridge. Note: By ingesting 149 | these alerts to AWS EventBridge, you can integrate with 35 SaaS vendors 150 | such as DataDog/NewRelic/PagerDuty. If you don't prefer to use EventBridge, leave the default (None). 151 | Type: String 152 | Default: None 153 | SlackWebhookURL: 154 | Description: >- 155 | Enter the Slack Webhook URL. If you don't prefer to use Slack, leave the default (None). 156 | Type: String 157 | Default: None 158 | MicrosoftTeamsWebhookURL: 159 | Description: >- 160 | Enter Microsoft Teams Webhook URL. If you don't prefer to use MS Teams, 161 | leave the default (None). 162 | Type: String 163 | Default: None 164 | AmazonChimeWebhookURL: 165 | Description: >- 166 | Enter the Chime Webhook URL, If you don't prefer to use Amazon Chime, 167 | leave the default (None). 168 | Type: String 169 | Default: None 170 | Regions: 171 | Description: >- 172 | By default, AHA reports events affecting all AWS regions. 173 | If you want to report on certain regions you can enter up to 10 in a comma separated format. 174 | Available Regions: us-east-1,us-east-2,us-west-1,us-west-2,af-south-1,ap-east-1,ap-south-1,ap-northeast-3, 175 | ap-northeast-2,ap-southeast-1,ap-southeast-2,ap-northeast-1,ca-central-1,eu-central-1,eu-west-1,eu-west-2, 176 | eu-south-1,eu-south-3,eu-north-1,me-south-1,sa-east-1,global 177 | Default: all regions 178 | AllowedPattern: ".+" 179 | ConstraintDescription: No regions were entered, please read the documentation about selecting all regions or filtering on some. 180 | Type: String 181 | AccountIDs: 182 | Description: >- 183 | If you would like to EXCLUDE any accounts from alerting, upload a .csv file of comma-seperated account numbers to the same S3 bucket 184 | where the AHA.zip package is located. Sample AccountIDs file name: aha_account_ids.csv. If not, leave the default of None. 185 | Default: None 186 | Type: String 187 | AllowedPattern: (None)|(.+(\.csv))$ 188 | EventSearchBack: 189 | Description: How far back to search for events in hours. Default is 1 hour 190 | Default: '1' 191 | Type: Number 192 | FromEmail: 193 | Description: Enter FROM Email Address 194 | Type: String 195 | Default: none@domain.com 196 | AllowedPattern: ^([\w+-.%]+@[\w-.]+\.[A-Za-z]+)(, ?[\w+-.%]+@[\w-.]+\.[A-Za-z]+)*$ 197 | ConstraintDescription: 'FromEmail is not a valid, please verify entry. If not sending to email, leave as the default, none@domain.com.' 198 | ToEmail: 199 | Description: >- 200 | Enter email addresses separated by commas (for ex: abc@amazon.com, 201 | bcd@amazon.com) 202 | Type: String 203 | Default: none@domain.com 204 | AllowedPattern: ^([\w+-.%]+@[\w-.]+\.[A-Za-z]+)(, ?[\w+-.%]+@[\w-.]+\.[A-Za-z]+)*$ 205 | ConstraintDescription: 'ToEmail is not a valid, please verify entry. If not sending to email, leave as the default, none@domain.com.' 206 | Subject: 207 | Description: Enter the subject of the email address 208 | Type: String 209 | Default: AWS Health Alert 210 | Resources: 211 | GlobalDDBTable: 212 | Type: AWS::DynamoDB::GlobalTable 213 | Condition: UsingMultiRegion 214 | Properties: 215 | AttributeDefinitions: 216 | - AttributeName: arn 217 | AttributeType: S 218 | KeySchema: 219 | - AttributeName: arn 220 | KeyType: HASH 221 | Replicas: 222 | - Region: !Ref SecondaryRegion 223 | ReadProvisionedThroughputSettings: 224 | ReadCapacityUnits: 5 225 | - Region: !Ref "AWS::Region" 226 | ReadProvisionedThroughputSettings: 227 | ReadCapacityUnits: 5 228 | StreamSpecification: 229 | StreamViewType: "NEW_AND_OLD_IMAGES" 230 | TimeToLiveSpecification: 231 | AttributeName: ttl 232 | Enabled: true 233 | WriteProvisionedThroughputSettings: 234 | WriteCapacityAutoScalingSettings: 235 | MaxCapacity: 10 236 | MinCapacity: 10 237 | TargetTrackingScalingPolicyConfiguration: 238 | DisableScaleIn: false 239 | ScaleInCooldown: 30 240 | ScaleOutCooldown: 30 241 | TargetValue: 10 242 | DynamoDBTable: 243 | Type: 'AWS::DynamoDB::Table' 244 | Condition: NotUsingMultiRegion 245 | Properties: 246 | AttributeDefinitions: 247 | - AttributeName: arn 248 | AttributeType: S 249 | KeySchema: 250 | - AttributeName: arn 251 | KeyType: HASH 252 | ProvisionedThroughput: 253 | ReadCapacityUnits: 5 254 | WriteCapacityUnits: 5 255 | TimeToLiveSpecification: 256 | AttributeName: ttl 257 | Enabled: TRUE 258 | AHASecondaryRegionStackSet: 259 | Condition: UsingMultiRegion 260 | DependsOn: GlobalDDBTable 261 | Type: AWS::CloudFormation::StackSet 262 | Properties: 263 | Description: Secondary Region CloudFormation Template for AWS Health Aware (AHA) 264 | PermissionModel: SELF_MANAGED 265 | Capabilities: [CAPABILITY_IAM] 266 | StackInstancesGroup: 267 | - Regions: 268 | - !Ref 'SecondaryRegion' 269 | DeploymentTargets: 270 | Accounts: 271 | - !Ref 'AWS::AccountId' 272 | StackSetName: 'aha-multi-region' 273 | TemplateBody: 274 | !Sub | 275 | Resources: 276 | AHA2ndRegionBucket: 277 | Type: AWS::S3::Bucket 278 | CopyAHA: 279 | Type: Custom::CopyAHA 280 | Properties: 281 | DestBucket: !Ref 'AHA2ndRegionBucket' 282 | ServiceToken: !GetAtt 'CopyAHAFunction.Arn' 283 | SourceBucket: ${S3Bucket} 284 | Object: 285 | - ${S3Key} 286 | CopyAHARole: 287 | Type: AWS::IAM::Role 288 | Properties: 289 | AssumeRolePolicyDocument: 290 | Version: '2012-10-17' 291 | Statement: 292 | - Effect: Allow 293 | Principal: 294 | Service: lambda.amazonaws.com 295 | Action: sts:AssumeRole 296 | ManagedPolicyArns: 297 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 298 | Path: / 299 | Policies: 300 | - PolicyName: aha-lambda-copier 301 | PolicyDocument: 302 | Version: '2012-10-17' 303 | Statement: 304 | - Effect: Allow 305 | Action: 306 | - s3:GetObject 307 | Resource: 308 | - 'arn:aws:s3:::${S3Bucket}*' 309 | - Effect: Allow 310 | Action: 311 | - s3:PutObject 312 | - s3:DeleteObject 313 | Resource: 314 | - !Join ['', [ 'arn:aws:s3:::', !Ref AHA2ndRegionBucket, '*']] 315 | CopyAHAFunction: 316 | Type: AWS::Lambda::Function 317 | DependsOn: AHA2ndRegionBucket 318 | Properties: 319 | Description: Copies AHA .zip from a source S3 bucket to a destination 320 | Handler: index.handler 321 | Runtime: python3.11 322 | Role: !GetAtt 'CopyAHARole.Arn' 323 | Timeout: 240 324 | Code: 325 | ZipFile: | 326 | import json 327 | import logging 328 | import threading 329 | import boto3 330 | import cfnresponse 331 | 332 | def copy_object(source_bucket, dest_bucket, object): 333 | s3 = boto3.client('s3') 334 | for o in object: 335 | key = o 336 | copy_source = { 337 | 'Bucket': source_bucket, 338 | 'Key': key 339 | } 340 | print('copy_source: %s' % copy_source) 341 | print('dest_bucket = %s'%dest_bucket) 342 | print('key = %s' %key) 343 | s3.copy_object(CopySource=copy_source, Bucket=dest_bucket, 344 | Key=key) 345 | 346 | def delete_object(bucket, object): 347 | s3 = boto3.client('s3') 348 | objects = {'Objects': [{'Key': o} for o in object]} 349 | s3.delete_objects(Bucket=bucket, Delete=objects) 350 | 351 | def timeout(event, context): 352 | logging.error('Execution is about to time out, sending failure response to CloudFormation') 353 | cfnresponse.send(event, context, cfnresponse.FAILED, {}, None) 354 | 355 | def handler(event, context): 356 | # make sure we send a failure to CloudFormation if the function 357 | # is going to timeout 358 | timer = threading.Timer((context.get_remaining_time_in_millis() 359 | / 1000.00) - 0.5, timeout, args=[event, context]) 360 | timer.start() 361 | 362 | print('Received event: %s' % json.dumps(event)) 363 | status = cfnresponse.SUCCESS 364 | try: 365 | source_bucket = event['ResourceProperties']['SourceBucket'] 366 | dest_bucket = event['ResourceProperties']['DestBucket'] 367 | object = event['ResourceProperties']['Object'] 368 | if event['RequestType'] == 'Delete': 369 | delete_object(dest_bucket, object) 370 | else: 371 | copy_object(source_bucket, dest_bucket, object) 372 | except Exception as e: 373 | logging.error('Exception: %s' % e, exc_info=True) 374 | status = cfnresponse.FAILED 375 | finally: 376 | timer.cancel() 377 | cfnresponse.send(event, context, status, {}, None) 378 | LambdaSchedule: 379 | Type: AWS::Events::Rule 380 | Properties: 381 | Description: Lambda trigger Event 382 | ScheduleExpression: rate(1 minute) 383 | State: ENABLED 384 | Targets: 385 | - Arn: !GetAtt 'LambdaFunction.Arn' 386 | Id: LambdaSchedule 387 | LambdaSchedulePermission: 388 | Type: AWS::Lambda::Permission 389 | Properties: 390 | Action: lambda:InvokeFunction 391 | FunctionName: !GetAtt 'LambdaFunction.Arn' 392 | Principal: events.amazonaws.com 393 | SourceArn: !GetAtt 'LambdaSchedule.Arn' 394 | LambdaFunction: 395 | Type: AWS::Lambda::Function 396 | DependsOn: CopyAHA 397 | Properties: 398 | Description: Lambda function that runs AHA 399 | Code: 400 | S3Bucket: 401 | Ref: AHA2ndRegionBucket 402 | S3Key: "${S3Key}" 403 | Handler: handler.main 404 | MemorySize: 128 405 | Timeout: 600 406 | Role: ${LambdaExecutionRole.Arn} 407 | Runtime: python3.11 408 | Environment: 409 | Variables: 410 | REGIONS: ${Regions} 411 | FROM_EMAIL: "${FromEmail}" 412 | TO_EMAIL: "${ToEmail}" 413 | EMAIL_SUBJECT: "${Subject}" 414 | DYNAMODB_TABLE: "${GlobalDDBTable}" 415 | EVENT_SEARCH_BACK: ${EventSearchBack} 416 | ORG_STATUS: ${AWSOrganizationsEnabled} 417 | HEALTH_EVENT_TYPE: "${AWSHealthEventType}" 418 | MANAGEMENT_ROLE_ARN: "${ManagementAccountRoleArn}" 419 | LambdaExecutionRole: 420 | Type: 'AWS::IAM::Role' 421 | Properties: 422 | AssumeRolePolicyDocument: 423 | Version: '2012-10-17' 424 | Statement: 425 | - Effect: Allow 426 | Principal: 427 | Service: 428 | - lambda.amazonaws.com 429 | Action: 430 | - 'sts:AssumeRole' 431 | Path: / 432 | Policies: 433 | - PolicyName: AHA-LambdaPolicy 434 | PolicyDocument: 435 | Version: '2012-10-17' 436 | Statement: 437 | - Effect: Allow 438 | Action: 439 | - logs:CreateLogGroup 440 | - logs:CreateLogStream 441 | - logs:PutLogEvents 442 | Resource: 443 | - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 444 | - !If [UsingMultiRegion, !Sub 'arn:aws:logs:${SecondaryRegion}:${AWS::AccountId}:*', !Ref AWS::NoValue] 445 | - !If 446 | - UsingSecrets 447 | - Effect: Allow 448 | Action: 449 | - 'secretsmanager:GetResourcePolicy' 450 | - 'secretsmanager:DescribeSecret' 451 | - 'secretsmanager:ListSecretVersionIds' 452 | - 'secretsmanager:GetSecretValue' 453 | Resource: 454 | - !If [UsingTeams, !Sub '${MicrosoftChannelSecret}', !Ref AWS::NoValue] 455 | - !If [UsingSlack, !Sub '${SlackChannelSecret}', !Ref AWS::NoValue] 456 | - !If [UsingEventBridge, !Sub '${EventBusNameSecret}', !Ref AWS::NoValue] 457 | - !If [UsingChime, !Sub '${ChimeChannelSecret}', !Ref AWS::NoValue] 458 | - !If [UsingCrossAccountRole, !Sub '${AssumeRoleSecret}', !Ref AWS::NoValue] 459 | - !If 460 | - UsingMultiRegionTeams 461 | - !Sub 462 | - 'arn:aws:secretsmanager:${SecondaryRegion}:${AWS::AccountId}:secret:${SecretNameWithSha}' 463 | - { SecretNameWithSha: !Select [1, !Split [':secret:', !Sub '${MicrosoftChannelSecret}' ]]} 464 | - !Ref AWS::NoValue 465 | - !If 466 | - UsingMultiRegionSlack 467 | - !Sub 468 | - 'arn:aws:secretsmanager:${SecondaryRegion}:${AWS::AccountId}:secret:${SecretNameWithSha}' 469 | - { SecretNameWithSha: !Select [1, !Split [':secret:', !Sub '${SlackChannelSecret}' ]]} 470 | - !Ref AWS::NoValue 471 | - !If 472 | - UsingMultiRegionEventBridge 473 | - !Sub 474 | - 'arn:aws:secretsmanager:${SecondaryRegion}:${AWS::AccountId}:secret:${SecretNameWithSha}' 475 | - { SecretNameWithSha: !Select [1, !Split [':secret:', !Sub '${EventBusNameSecret}' ]]} 476 | - !Ref AWS::NoValue 477 | - !If 478 | - UsingMultiRegionChime 479 | - !Sub 480 | - 'arn:aws:secretsmanager:${SecondaryRegion}:${AWS::AccountId}:secret:${SecretNameWithSha}' 481 | - { SecretNameWithSha: !Select [1, !Split [':secret:', !Sub '${ChimeChannelSecret}' ]]} 482 | - !Ref AWS::NoValue 483 | - !If 484 | - UsingMultiRegionCrossAccountRole 485 | - !Sub 486 | - 'arn:aws:secretsmanager:${SecondaryRegion}:${AWS::AccountId}:secret:${SecretNameWithSha}' 487 | - { SecretNameWithSha: !Select [1, !Split [':secret:', !Sub '${AssumeRoleSecret}' ]]} 488 | - !Ref AWS::NoValue 489 | - !Ref 'AWS::NoValue' 490 | - Effect: Allow 491 | Action: 492 | - health:DescribeAffectedAccountsForOrganization 493 | - health:DescribeAffectedEntitiesForOrganization 494 | - health:DescribeEventDetailsForOrganization 495 | - health:DescribeEventsForOrganization 496 | - health:DescribeEventDetails 497 | - health:DescribeEvents 498 | - health:DescribeEventTypes 499 | - health:DescribeAffectedEntities 500 | - organizations:ListAccounts 501 | - organizations:DescribeAccount 502 | Resource: "*" 503 | - Effect: Allow 504 | Action: 505 | - dynamodb:ListTables 506 | Resource: 507 | - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:*' 508 | - !If [UsingMultiRegion, !Sub 'arn:aws:dynamodb:${SecondaryRegion}:${AWS::AccountId}:*', !Ref AWS::NoValue] 509 | - Effect: Allow 510 | Action: 511 | - ses:SendEmail 512 | Resource: 513 | - !Sub 'arn:aws:ses:${AWS::Region}:${AWS::AccountId}:*' 514 | - !If [UsingMultiRegion, !Sub 'arn:aws:ses:${SecondaryRegion}:${AWS::AccountId}:*', !Ref AWS::NoValue] 515 | - Effect: Allow 516 | Action: 517 | - dynamodb:UpdateTimeToLive 518 | - dynamodb:PutItem 519 | - dynamodb:DeleteItem 520 | - dynamodb:GetItem 521 | - dynamodb:Scan 522 | - dynamodb:Query 523 | - dynamodb:UpdateItem 524 | - dynamodb:UpdateTable 525 | - dynamodb:GetRecords 526 | Resource: !If [UsingMultiRegion, !GetAtt GlobalDDBTable.Arn, !GetAtt DynamoDBTable.Arn] 527 | - Effect: Allow 528 | Action: 529 | - events:PutEvents 530 | Resource: 531 | - !Sub 'arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/${EventBusName}' 532 | - !If [UsingMultiRegion, !Sub 'arn:aws:events:${SecondaryRegion}:${AWS::AccountId}:event-bus/${EventBusName}', !Ref AWS::NoValue] 533 | - !If 534 | - UsingAccountIds 535 | - Effect: Allow 536 | Action: 537 | - s3:GetObject 538 | Resource: !Sub 'arn:aws:s3:::${S3Bucket}/${AccountIDs}' 539 | - !Ref 'AWS::NoValue' 540 | - !If 541 | - UsingCrossAccountRole 542 | - Effect: Allow 543 | Action: 544 | - sts:AssumeRole 545 | Resource: !Ref ManagementAccountRoleArn 546 | - !Ref 'AWS::NoValue' 547 | LambdaSchedule: 548 | Type: 'AWS::Events::Rule' 549 | Properties: 550 | Description: Lambda trigger Event 551 | ScheduleExpression: rate(1 minute) 552 | State: ENABLED 553 | Targets: 554 | - Arn: !GetAtt LambdaFunction.Arn 555 | Id: LambdaSchedule 556 | LambdaSchedulePermission: 557 | Type: 'AWS::Lambda::Permission' 558 | Properties: 559 | Action: 'lambda:InvokeFunction' 560 | FunctionName: !GetAtt LambdaFunction.Arn 561 | Principal: events.amazonaws.com 562 | SourceArn: !GetAtt LambdaSchedule.Arn 563 | MicrosoftChannelSecret: 564 | Type: 'AWS::SecretsManager::Secret' 565 | Condition: UsingTeams 566 | Properties: 567 | Name: MicrosoftChannelID 568 | Description: Microsoft Channel ID Secret 569 | ReplicaRegions: 570 | !If 571 | - UsingMultiRegion 572 | - [{ Region: !Sub '${SecondaryRegion}' }] 573 | - !Ref "AWS::NoValue" 574 | SecretString: 575 | Ref: MicrosoftTeamsWebhookURL 576 | Tags: 577 | - Key: HealthCheckMicrosoft 578 | Value: ChannelID 579 | SlackChannelSecret: 580 | Type: 'AWS::SecretsManager::Secret' 581 | Condition: UsingSlack 582 | Properties: 583 | Name: SlackChannelID 584 | Description: Slack Channel ID Secret 585 | ReplicaRegions: 586 | !If 587 | - UsingMultiRegion 588 | - [{ Region: !Sub '${SecondaryRegion}' }] 589 | - !Ref "AWS::NoValue" 590 | SecretString: 591 | Ref: SlackWebhookURL 592 | Tags: 593 | - Key: HealthCheckSlack 594 | Value: ChannelID 595 | EventBusNameSecret: 596 | Type: 'AWS::SecretsManager::Secret' 597 | Condition: UsingEventBridge 598 | Properties: 599 | Name: EventBusName 600 | Description: EventBus Name Secret 601 | ReplicaRegions: 602 | !If 603 | - UsingMultiRegion 604 | - [{ Region: !Sub '${SecondaryRegion}' }] 605 | - !Ref "AWS::NoValue" 606 | SecretString: 607 | Ref: EventBusName 608 | Tags: 609 | - Key: EventBusName 610 | Value: ChannelID 611 | ChimeChannelSecret: 612 | Type: 'AWS::SecretsManager::Secret' 613 | Condition: UsingChime 614 | Properties: 615 | Name: ChimeChannelID 616 | Description: Chime Channel ID Secret 617 | ReplicaRegions: 618 | !If 619 | - UsingMultiRegion 620 | - [{ Region: !Sub '${SecondaryRegion}' }] 621 | - !Ref "AWS::NoValue" 622 | SecretString: 623 | Ref: AmazonChimeWebhookURL 624 | Tags: 625 | - Key: HealthCheckChime 626 | Value: ChannelID 627 | AssumeRoleSecret: 628 | Type: 'AWS::SecretsManager::Secret' 629 | Condition: UsingCrossAccountRole 630 | Properties: 631 | Name: AssumeRoleArn 632 | Description: Management account role for AHA to assume 633 | ReplicaRegions: 634 | !If 635 | - UsingMultiRegion 636 | - [{ Region: !Sub '${SecondaryRegion}' }] 637 | - !Ref "AWS::NoValue" 638 | SecretString: 639 | Ref: ManagementAccountRoleArn 640 | Tags: 641 | - Key: AssumeRoleArn 642 | Value: ChannelID 643 | LambdaFunction: 644 | Type: 'AWS::Lambda::Function' 645 | Properties: 646 | Description: Lambda function that runs AHA 647 | Code: 648 | S3Bucket: 649 | Ref: S3Bucket 650 | S3Key: 651 | Ref: S3Key 652 | Handler: handler.main 653 | MemorySize: 128 654 | Timeout: 600 655 | Role: 656 | 'Fn::Sub': '${LambdaExecutionRole.Arn}' 657 | Runtime: python3.11 658 | Environment: 659 | Variables: 660 | Slack: !If [UsingSlack, "True", !Ref 'AWS::NoValue'] 661 | Teams: !If [UsingTeams, "True", !Ref 'AWS::NoValue'] 662 | Chime: !If [UsingChime, "True", !Ref 'AWS::NoValue'] 663 | Eventbridge: !If [UsingEventBridge, "True", !Ref 'AWS::NoValue'] 664 | ACCOUNT_IDS: 665 | Ref: AccountIDs 666 | REGIONS: 667 | Ref: Regions 668 | S3_BUCKET: 669 | Ref: S3Bucket 670 | FROM_EMAIL: 671 | Ref: FromEmail 672 | TO_EMAIL: 673 | Ref: ToEmail 674 | EMAIL_SUBJECT: 675 | Ref: Subject 676 | DYNAMODB_TABLE: 677 | !If [UsingMultiRegion, !Ref GlobalDDBTable, !Ref DynamoDBTable] 678 | EVENT_SEARCH_BACK: 679 | Ref: EventSearchBack 680 | ORG_STATUS: 681 | Ref: AWSOrganizationsEnabled 682 | HEALTH_EVENT_TYPE: 683 | Ref: AWSHealthEventType 684 | MANAGEMENT_ROLE_ARN: 685 | Ref: ManagementAccountRoleArn 686 | 687 | -------------------------------------------------------------------------------- /terraform/Terraform_DEPLOY_AHA/Terraform_DEPLOY_AHA.tf: -------------------------------------------------------------------------------- 1 | # Terraform script to deploy AHA Solution 2 | # 1.0 - Initial version 3 | 4 | # Variables defined below, you can overwrite them using tfvars or imput variables 5 | 6 | data "aws_caller_identity" "current" {} 7 | 8 | provider "aws" { 9 | region = var.aha_primary_region 10 | default_tags { 11 | tags = "${var.default_tags}" 12 | } 13 | } 14 | 15 | # Secondary region - provider config 16 | locals { 17 | secondary_region = "${var.aha_secondary_region == "" ? var.aha_primary_region : var.aha_secondary_region}" 18 | } 19 | 20 | provider "aws" { 21 | alias = "secondary_region" 22 | region = local.secondary_region 23 | default_tags { 24 | tags = "${var.default_tags}" 25 | } 26 | } 27 | 28 | # Comment below - if needed to use s3_bucket, s3_key for consistency with cf 29 | locals { 30 | source_files = ["${path.module}/../../handler.py", "${path.module}/../../messagegenerator.py"] 31 | } 32 | data "archive_file" "lambda_zip" { 33 | type = "zip" 34 | output_path = "${path.module}/lambda_function.zip" 35 | source { 36 | filename = "${basename(local.source_files[0])}" 37 | content = file("${local.source_files[0]}") 38 | } 39 | source { 40 | filename = "${basename(local.source_files[1])}" 41 | content = file("${local.source_files[1]}") 42 | } 43 | } 44 | 45 | variable "aha_primary_region" { 46 | description = "Primary region where AHA solution will be deployed" 47 | type = string 48 | default = "us-east-1" 49 | } 50 | 51 | variable "aha_secondary_region" { 52 | description = "Secondary region where AHA solution will be deployed" 53 | type = string 54 | default = "" 55 | } 56 | 57 | variable "default_tags" { 58 | description = "Tags used for the AWS resources created by this template" 59 | type = map 60 | default = { 61 | Application = "AHA-Solution" 62 | } 63 | } 64 | 65 | variable "dynamodbtable" { 66 | type = string 67 | default = "AHA-DynamoDBTable" 68 | } 69 | 70 | variable "AWSOrganizationsEnabled" { 71 | type = string 72 | default = "No" 73 | description = "You can receive both PHD and SHD alerts if you're using AWS Organizations. \n If you are, make sure to enable Organizational Health View: \n (https://docs.aws.amazon.com/health/latest/ug/aggregate-events.html) to \n aggregate all PHD events in your AWS Organization. If not, you can still \n get SHD alerts." 74 | validation { 75 | condition = ( 76 | var.AWSOrganizationsEnabled == "Yes" || var.AWSOrganizationsEnabled == "No" 77 | ) 78 | error_message = "AWSOrganizationsEnabled variable can only accept Yes or No as values." 79 | } 80 | } 81 | 82 | variable "ManagementAccountRoleArn" { 83 | type = string 84 | default = "" 85 | description = "Arn of the IAM role in the top-level management account for collecting PHD Events. 'None' if deploying into the top-level management account." 86 | } 87 | 88 | variable "AWSHealthEventType" { 89 | type = string 90 | default = "issue | accountNotification | scheduledChange" 91 | description = "Select the event type that you want AHA to report on. Refer to \n https://docs.aws.amazon.com/health/latest/APIReference/API_EventType.html for more information on EventType." 92 | validation { 93 | condition = ( 94 | var.AWSHealthEventType == "issue | accountNotification | scheduledChange" || var.AWSHealthEventType == "issue" 95 | ) 96 | error_message = "AWSHealthEventType variable can only accept issue | accountNotification | scheduledChange or issue as values." 97 | } 98 | } 99 | 100 | #variable "S3Bucket" { 101 | # type = string 102 | # description = "Name of your S3 Bucket where the AHA Package .zip resides. Just the name of the bucket (e.g. my-s3-bucket)" 103 | # validation { 104 | # condition = length(var.S3Bucket) > 0 105 | # error_message = "The S3Bucket cannot be empty." 106 | # } 107 | #} 108 | # 109 | #variable "S3Key" { 110 | # type = string 111 | # description = "Name of the .zip in your S3 Bucket. Just the name of the file (e.g. aha-v1.0.zip)" 112 | # validation { 113 | # condition = length(var.S3Key) > 0 114 | # error_message = "The S3Key cannot be empty." 115 | # } 116 | #} 117 | 118 | variable "EventBusName" { 119 | type = string 120 | default = "" 121 | description = "This is to ingest alerts into AWS EventBridge. Enter the event bus name if you wish to send the alerts to the AWS EventBridge. Note: By ingesting you wish to send the alerts to the AWS EventBridge. Note: By ingesting these alerts to AWS EventBridge, you can integrate with 35 SaaS vendors such as DataDog/NewRelic/PagerDuty. If you don't prefer to use EventBridge, leave the default (None)." 122 | } 123 | 124 | variable "SlackWebhookURL" { 125 | type = string 126 | default = "" 127 | description = "Enter the Slack Webhook URL. If you don't prefer to use Slack, leave the default (empty)." 128 | } 129 | 130 | variable "MicrosoftTeamsWebhookURL" { 131 | type = string 132 | default = "" 133 | description = "Enter Microsoft Teams Webhook URL. If you don't prefer to use MS Teams, leave the default (empty)." 134 | } 135 | 136 | variable "AmazonChimeWebhookURL" { 137 | type = string 138 | default = "" 139 | description = "Enter the Chime Webhook URL, If you don't prefer to use Amazon Chime, leave the default (empty)." 140 | } 141 | 142 | variable "Regions" { 143 | type = string 144 | default = "all regions" 145 | description = "By default, AHA reports events affecting all AWS regions. \n If you want to report on certain regions you can enter up to 10 in a comma separated format. \n Available Regions: us-east-1,us-east-2,us-west-1,us-west-2,af-south-1,ap-east-1,ap-south-1,ap-northeast-3, \n ap-northeast-2,ap-southeast-1,ap-southeast-2,ap-northeast-1,ca-central-1,eu-central-1,eu-west-1,eu-west-2, \n eu-south-1,eu-south-3,eu-north-1,me-south-1,sa-east-1,global" 146 | } 147 | 148 | variable "EventSearchBack" { 149 | type = number 150 | default = "1" 151 | description = "How far back to search for events in hours. Default is 1 hour" 152 | } 153 | 154 | variable "FromEmail" { 155 | type = string 156 | default = "none@domain.com" 157 | description = "Enter FROM Email Address" 158 | } 159 | 160 | variable "ToEmail" { 161 | type = string 162 | default = "none@domain.com" 163 | description = "Enter email addresses separated by commas (for ex: abc@amazon.com, bcd@amazon.com)" 164 | } 165 | 166 | variable "Subject" { 167 | type = string 168 | default = "AWS Health Alert" 169 | description = "Enter the subject of the email address" 170 | } 171 | 172 | #variable "S3Bucket" { 173 | # type = string 174 | # description = "Name of your S3 Bucket where the AHA Package .zip resides. Just the name of the bucket (e.g. my-s3-bucket)" 175 | # default = "" 176 | #} 177 | 178 | variable "ExcludeAccountIDs" { 179 | type = string 180 | default = "" 181 | description = "If you would like to EXCLUDE any accounts from alerting, enter a .csv filename created with comma-seperated account numbers. Sample AccountIDs file name: aha_account_ids.csv. If not, leave the default empty." 182 | } 183 | 184 | ##### Resources for AHA Solution created below. 185 | 186 | # Random id generator 187 | resource "random_string" "resource_code" { 188 | length = 8 189 | special = false 190 | upper = false 191 | } 192 | 193 | # S3 buckets creation 194 | resource "aws_s3_bucket" "AHA-S3Bucket-PrimaryRegion" { 195 | count = "${var.ExcludeAccountIDs != "" ? 1 : 0}" 196 | bucket = "aha-bucket-${var.aha_primary_region}-${random_string.resource_code.result}" 197 | tags = { 198 | Name = "aha-bucket" 199 | } 200 | } 201 | 202 | resource "aws_s3_bucket_acl" "AHA-S3Bucket-PrimaryRegion" { 203 | count = var.ExcludeAccountIDs != "" ? 1 : 0 204 | bucket = aws_s3_bucket.AHA-S3Bucket-PrimaryRegion[0].id 205 | acl = "private" 206 | } 207 | 208 | resource "aws_s3_bucket" "AHA-S3Bucket-SecondaryRegion" { 209 | count = "${var.aha_secondary_region != "" && var.ExcludeAccountIDs != "" ? 1 : 0}" 210 | provider = aws.secondary_region 211 | bucket = "aha-bucket-${var.aha_secondary_region}-${random_string.resource_code.result}" 212 | tags = { 213 | Name = "aha-bucket" 214 | } 215 | } 216 | 217 | resource "aws_s3_bucket_acl" "AHA-S3Bucket-SecondaryRegion" { 218 | count = "${var.aha_secondary_region != "" && var.ExcludeAccountIDs != "" ? 1 : 0}" 219 | provider = aws.secondary_region 220 | bucket = aws_s3_bucket.AHA-S3Bucket-SecondaryRegion[0].id 221 | acl = "private" 222 | } 223 | 224 | resource "aws_s3_object" "AHA-S3Object-PrimaryRegion" { 225 | count = "${var.ExcludeAccountIDs != "" ? 1 : 0}" 226 | key = var.ExcludeAccountIDs 227 | bucket = aws_s3_bucket.AHA-S3Bucket-PrimaryRegion[0].bucket 228 | source = var.ExcludeAccountIDs 229 | tags = { 230 | Name = "${var.ExcludeAccountIDs}" 231 | } 232 | } 233 | 234 | resource "aws_s3_object" "AHA-S3Object-SecondaryRegion" { 235 | count = "${var.aha_secondary_region != "" && var.ExcludeAccountIDs != "" ? 1 : 0}" 236 | provider = aws.secondary_region 237 | key = var.ExcludeAccountIDs 238 | bucket = aws_s3_bucket.AHA-S3Bucket-SecondaryRegion[0].bucket 239 | source = var.ExcludeAccountIDs 240 | tags = { 241 | Name = "${var.ExcludeAccountIDs}" 242 | } 243 | } 244 | 245 | # DynamoDB table - Create if secondary region not set 246 | resource "aws_dynamodb_table" "AHA-DynamoDBTable" { 247 | count = "${var.aha_secondary_region == "" ? 1 : 0}" 248 | billing_mode = "PROVISIONED" 249 | hash_key = "arn" 250 | name = "${var.dynamodbtable}-${random_string.resource_code.result}" 251 | read_capacity = 5 252 | write_capacity = 5 253 | stream_enabled = false 254 | tags = { 255 | Name = "${var.dynamodbtable}" 256 | } 257 | 258 | attribute { 259 | name = "arn" 260 | type = "S" 261 | } 262 | 263 | point_in_time_recovery { 264 | enabled = false 265 | } 266 | 267 | timeouts {} 268 | 269 | ttl { 270 | attribute_name = "ttl" 271 | enabled = true 272 | } 273 | } 274 | 275 | # DynamoDB table - Multi region Global Table - Create if secondary region is set 276 | resource "aws_dynamodb_table" "AHA-GlobalDynamoDBTable" { 277 | count = "${var.aha_secondary_region == "" ? 0 : 1}" 278 | billing_mode = "PAY_PER_REQUEST" 279 | hash_key = "arn" 280 | name = "${var.dynamodbtable}-${random_string.resource_code.result}" 281 | stream_enabled = true 282 | stream_view_type = "NEW_AND_OLD_IMAGES" 283 | tags = { 284 | Name = "${var.dynamodbtable}" 285 | } 286 | 287 | attribute { 288 | name = "arn" 289 | type = "S" 290 | } 291 | 292 | point_in_time_recovery { 293 | enabled = false 294 | } 295 | 296 | replica { 297 | region_name = var.aha_secondary_region 298 | } 299 | 300 | timeouts {} 301 | 302 | ttl { 303 | attribute_name = "ttl" 304 | enabled = true 305 | } 306 | } 307 | # Tags for DynamoDB - secondary region 308 | resource "aws_dynamodb_tag" "AHA-GlobalDynamoDBTable" { 309 | count = "${var.aha_secondary_region == "" ? 0 : 1}" 310 | provider = aws.secondary_region 311 | resource_arn = replace(aws_dynamodb_table.AHA-GlobalDynamoDBTable[count.index].arn, var.aha_primary_region, var.aha_secondary_region) 312 | key = "Name" 313 | value = "${var.dynamodbtable}" 314 | } 315 | # Tags for DynamoDB - secondary region - default_tags 316 | resource "aws_dynamodb_tag" "AHA-GlobalDynamoDBTable-Additional-tags" { 317 | for_each = { for key, value in var.default_tags : key => value if var.aha_secondary_region != "" } 318 | provider = aws.secondary_region 319 | resource_arn = replace(aws_dynamodb_table.AHA-GlobalDynamoDBTable[0].arn, var.aha_primary_region, var.aha_secondary_region) 320 | key = each.key 321 | value = each.value 322 | } 323 | 324 | # Secrets - SlackChannelSecret 325 | resource "aws_secretsmanager_secret" "SlackChannelID" { 326 | count = "${var.SlackWebhookURL == "" ? 0 : 1}" 327 | name = "SlackChannelID" 328 | description = "Slack Channel ID Secret" 329 | recovery_window_in_days = 0 330 | tags = { 331 | "HealthCheckSlack" = "ChannelID" 332 | } 333 | dynamic "replica" { 334 | for_each = var.aha_secondary_region == "" ? [] : [1] 335 | content { 336 | region = var.aha_secondary_region 337 | } 338 | } 339 | } 340 | resource "aws_secretsmanager_secret_version" "SlackChannelID" { 341 | count = "${var.SlackWebhookURL == "" ? 0 : 1}" 342 | secret_id = "${aws_secretsmanager_secret.SlackChannelID.*.id[count.index]}" 343 | secret_string = "${var.SlackWebhookURL}" 344 | } 345 | 346 | # Secrets - MicrosoftChannelSecret 347 | resource "aws_secretsmanager_secret" "MicrosoftChannelID" { 348 | count = "${var.MicrosoftTeamsWebhookURL == "" ? 0 : 1}" 349 | name = "MicrosoftChannelID" 350 | description = "Microsoft Channel ID Secret" 351 | recovery_window_in_days = 0 352 | tags = { 353 | "HealthCheckMicrosoft" = "ChannelID" 354 | "Name" = "AHA-MicrosoftChannelID" 355 | } 356 | dynamic "replica" { 357 | for_each = var.aha_secondary_region == "" ? [] : [1] 358 | content { 359 | region = var.aha_secondary_region 360 | } 361 | } 362 | } 363 | resource "aws_secretsmanager_secret_version" "MicrosoftChannelID" { 364 | count = "${var.MicrosoftTeamsWebhookURL == "" ? 0 : 1}" 365 | secret_id = "${aws_secretsmanager_secret.MicrosoftChannelID.*.id[count.index]}" 366 | secret_string = "${var.MicrosoftTeamsWebhookURL}" 367 | } 368 | 369 | # Secrets - EventBusNameSecret 370 | resource "aws_secretsmanager_secret" "EventBusName" { 371 | count = "${var.EventBusName == "" ? 0 : 1}" 372 | name = "EventBusName" 373 | description = "EventBus Name Secret" 374 | recovery_window_in_days = 0 375 | tags = { 376 | "EventBusName" = "ChannelID" 377 | "Name" = "AHA-EventBusName" 378 | } 379 | dynamic "replica" { 380 | for_each = var.aha_secondary_region == "" ? [] : [1] 381 | content { 382 | region = var.aha_secondary_region 383 | } 384 | } 385 | } 386 | 387 | resource "aws_secretsmanager_secret_version" "EventBusName" { 388 | count = "${var.EventBusName == "" ? 0 : 1}" 389 | secret_id = "${aws_secretsmanager_secret.EventBusName.*.id[count.index]}" 390 | secret_string = "${var.EventBusName}" 391 | } 392 | 393 | # Secrets - ChimeChannelSecret 394 | resource "aws_secretsmanager_secret" "ChimeChannelID" { 395 | count = "${var.AmazonChimeWebhookURL == "" ? 0 : 1}" 396 | name = "ChimeChannelID" 397 | description = "Chime Channel ID Secret" 398 | recovery_window_in_days = 0 399 | tags = { 400 | "HealthCheckChime" = "ChannelID" 401 | "Name" = "AHA-ChimeChannelID-${random_string.resource_code.result}" 402 | } 403 | dynamic "replica" { 404 | for_each = var.aha_secondary_region == "" ? [] : [1] 405 | content { 406 | region = var.aha_secondary_region 407 | } 408 | } 409 | } 410 | resource "aws_secretsmanager_secret_version" "ChimeChannelID" { 411 | count = "${var.AmazonChimeWebhookURL == "" ? 0 : 1}" 412 | secret_id = "${aws_secretsmanager_secret.ChimeChannelID.*.id[count.index]}" 413 | secret_string = "${var.AmazonChimeWebhookURL}" 414 | } 415 | 416 | # Secrets - AssumeRoleSecret 417 | resource "aws_secretsmanager_secret" "AssumeRoleArn" { 418 | count = "${var.ManagementAccountRoleArn == "" ? 0 : 1}" 419 | name = "AssumeRoleArn" 420 | description = "Management account role for AHA to assume" 421 | recovery_window_in_days = 0 422 | tags = { 423 | "AssumeRoleArn" = "" 424 | "Name" = "AHA-AssumeRoleArn" 425 | } 426 | dynamic "replica" { 427 | for_each = var.aha_secondary_region == "" ? [] : [1] 428 | content { 429 | region = var.aha_secondary_region 430 | } 431 | } 432 | } 433 | resource "aws_secretsmanager_secret_version" "AssumeRoleArn" { 434 | count = "${var.ManagementAccountRoleArn == "" ? 0 : 1}" 435 | secret_id = "${aws_secretsmanager_secret.AssumeRoleArn.*.id[count.index]}" 436 | secret_string = "${var.ManagementAccountRoleArn}" 437 | } 438 | 439 | # IAM Role for Lambda function execution 440 | resource "aws_iam_role" "AHA-LambdaExecutionRole" { 441 | name = "AHA-LambdaExecutionRole-${random_string.resource_code.result}" 442 | path = "/" 443 | assume_role_policy = jsonencode( 444 | { 445 | Version = "2012-10-17" 446 | Statement = [ 447 | { 448 | Action = "sts:AssumeRole" 449 | Effect = "Allow" 450 | Principal = { 451 | Service = "lambda.amazonaws.com" 452 | } 453 | }, 454 | ] 455 | } 456 | ) 457 | inline_policy { 458 | name = "AHA-LambdaPolicy" 459 | policy = data.aws_iam_policy_document.AHA-LambdaPolicy-Document.json 460 | } 461 | tags = { 462 | "Name" = "AHA-LambdaExecutionRole" 463 | } 464 | } 465 | 466 | data "aws_iam_policy_document" "AHA-LambdaPolicy-Document" { 467 | version = "2012-10-17" 468 | statement { 469 | effect = "Allow" 470 | actions = [ 471 | "logs:CreateLogGroup", 472 | "logs:CreateLogStream", 473 | "logs:PutLogEvents", 474 | ] 475 | resources = [ 476 | "arn:aws:logs:${var.aha_primary_region}:${data.aws_caller_identity.current.account_id}:*", 477 | "arn:aws:logs:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:*" 478 | ] 479 | } 480 | statement { 481 | effect = "Allow" 482 | actions = [ 483 | "health:DescribeAffectedAccountsForOrganization", 484 | "health:DescribeAffectedEntitiesForOrganization", 485 | "health:DescribeEventDetailsForOrganization", 486 | "health:DescribeEventsForOrganization", 487 | "health:DescribeEventDetails", 488 | "health:DescribeEvents", 489 | "health:DescribeEventTypes", 490 | "health:DescribeAffectedEntities", 491 | "organizations:ListAccounts", 492 | "organizations:DescribeAccount", 493 | ] 494 | resources = [ "*" ] 495 | } 496 | statement { 497 | effect = "Allow" 498 | actions = [ 499 | "dynamodb:ListTables", 500 | ] 501 | resources = [ 502 | "arn:aws:dynamodb:${var.aha_primary_region}:${data.aws_caller_identity.current.account_id}:*", 503 | "arn:aws:dynamodb:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:*", 504 | ] 505 | } 506 | statement { 507 | effect = "Allow" 508 | actions = [ 509 | "ses:SendEmail", 510 | ] 511 | resources = [ 512 | "arn:aws:ses:${var.aha_primary_region}:${data.aws_caller_identity.current.account_id}:*", 513 | "arn:aws:ses:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:*", 514 | ] 515 | } 516 | statement { 517 | effect = "Allow" 518 | actions = [ 519 | "dynamodb:UpdateTimeToLive", 520 | "dynamodb:PutItem", 521 | "dynamodb:DeleteItem", 522 | "dynamodb:GetItem", 523 | "dynamodb:Scan", 524 | "dynamodb:Query", 525 | "dynamodb:UpdateItem", 526 | "dynamodb:UpdateTable", 527 | "dynamodb:GetRecords", 528 | ] 529 | resources = [ 530 | #aws_dynamodb_table.AHA-DynamoDBTable.arn 531 | "arn:aws:dynamodb:${var.aha_primary_region}:${data.aws_caller_identity.current.account_id}:table/${var.dynamodbtable}-${random_string.resource_code.result}", 532 | "arn:aws:dynamodb:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:table/${var.dynamodbtable}-${random_string.resource_code.result}", 533 | ] 534 | } 535 | dynamic "statement" { 536 | for_each = var.SlackWebhookURL == "" ? [] : [1] 537 | content { 538 | effect = "Allow" 539 | actions = [ 540 | "secretsmanager:GetResourcePolicy", 541 | "secretsmanager:DescribeSecret", 542 | "secretsmanager:ListSecretVersionIds", 543 | "secretsmanager:GetSecretValue", 544 | ] 545 | resources = [ 546 | aws_secretsmanager_secret.SlackChannelID[0].arn, 547 | "arn:aws:secretsmanager:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:secret:${element(split(":", aws_secretsmanager_secret.SlackChannelID[0].arn),6)}" 548 | # var.aha_secondary_region != "" ? "arn:aws:secretsmanager:${var.aha_secondary_region}:${data.aws_caller_identity.current.account_id}:secret:${element(split(":", aws_secretsmanager_secret.SlackChannelID[0].arn),6)}" : null 549 | ] 550 | } 551 | } 552 | dynamic "statement" { 553 | for_each = var.MicrosoftTeamsWebhookURL == "" ? [] : [1] 554 | content { 555 | effect = "Allow" 556 | actions = [ 557 | "secretsmanager:GetResourcePolicy", 558 | "secretsmanager:DescribeSecret", 559 | "secretsmanager:ListSecretVersionIds", 560 | "secretsmanager:GetSecretValue", 561 | ] 562 | resources = [ 563 | aws_secretsmanager_secret.MicrosoftChannelID[0].arn, 564 | "arn:aws:secretsmanager:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:secret:${element(split(":", aws_secretsmanager_secret.MicrosoftChannelID[0].arn),6)}" 565 | ] 566 | } 567 | } 568 | dynamic "statement" { 569 | for_each = var.AmazonChimeWebhookURL == "" ? [] : [1] 570 | content { 571 | effect = "Allow" 572 | actions = [ 573 | "secretsmanager:GetResourcePolicy", 574 | "secretsmanager:DescribeSecret", 575 | "secretsmanager:ListSecretVersionIds", 576 | "secretsmanager:GetSecretValue", 577 | ] 578 | resources = [ 579 | aws_secretsmanager_secret.ChimeChannelID[0].arn, 580 | "arn:aws:secretsmanager:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:secret:${element(split(":", aws_secretsmanager_secret.ChimeChannelID[0].arn),6)}" 581 | ] 582 | } 583 | } 584 | dynamic "statement" { 585 | for_each = var.EventBusName == "" ? [] : [1] 586 | content { 587 | effect = "Allow" 588 | actions = [ 589 | "secretsmanager:GetResourcePolicy", 590 | "secretsmanager:DescribeSecret", 591 | "secretsmanager:ListSecretVersionIds", 592 | "secretsmanager:GetSecretValue", 593 | ] 594 | resources = [ 595 | aws_secretsmanager_secret.EventBusName[0].arn, 596 | "arn:aws:secretsmanager:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:secret:${element(split(":", aws_secretsmanager_secret.EventBusName[0].arn),6)}" 597 | ] 598 | } 599 | } 600 | dynamic "statement" { 601 | for_each = var.ManagementAccountRoleArn == "" ? [] : [1] 602 | content { 603 | effect = "Allow" 604 | actions = [ 605 | "secretsmanager:GetResourcePolicy", 606 | "secretsmanager:DescribeSecret", 607 | "secretsmanager:ListSecretVersionIds", 608 | "secretsmanager:GetSecretValue", 609 | ] 610 | resources = [ 611 | aws_secretsmanager_secret.AssumeRoleArn[0].arn, 612 | "arn:aws:secretsmanager:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:secret:${element(split(":", aws_secretsmanager_secret.AssumeRoleArn[0].arn),6)}" 613 | ] 614 | } 615 | } 616 | dynamic "statement" { 617 | for_each = var.EventBusName == "" ? [] : [1] 618 | content { 619 | effect = "Allow" 620 | actions = [ 621 | "events:PutEvents", 622 | ] 623 | resources = [ 624 | "arn:aws:events:${var.aha_primary_region}:${data.aws_caller_identity.current.account_id}:event-bus/${var.EventBusName}", 625 | "arn:aws:events:${local.secondary_region}:${data.aws_caller_identity.current.account_id}:event-bus/${var.EventBusName}" 626 | ] 627 | } 628 | } 629 | dynamic "statement" { 630 | for_each = var.ManagementAccountRoleArn == "" ? [] : [1] 631 | content { 632 | effect = "Allow" 633 | actions = [ 634 | "sts:AssumeRole", 635 | ] 636 | resources = [ 637 | "${var.ManagementAccountRoleArn}", 638 | ] 639 | } 640 | } 641 | dynamic "statement" { 642 | for_each = var.ExcludeAccountIDs == "" ? [] : [1] 643 | content { 644 | effect = "Allow" 645 | actions = [ 646 | "s3:GetObject", 647 | ] 648 | resources = [ 649 | "arn:aws:s3:::aha-bucket-${var.aha_primary_region}-${random_string.resource_code.result}/${var.ExcludeAccountIDs}", 650 | "arn:aws:s3:::aha-bucket-${local.secondary_region}-${random_string.resource_code.result}/${var.ExcludeAccountIDs}", 651 | ] 652 | } 653 | } 654 | } 655 | # aws_lambda_function - AHA-LambdaFunction - Primary region 656 | resource "aws_lambda_function" "AHA-LambdaFunction-PrimaryRegion" { 657 | description = "Lambda function that runs AHA" 658 | function_name = "AHA-LambdaFunction-${random_string.resource_code.result}" 659 | handler = "handler.main" 660 | memory_size = 128 661 | timeout = 600 662 | filename = data.archive_file.lambda_zip.output_path 663 | source_code_hash = data.archive_file.lambda_zip.output_base64sha256 664 | # s3_bucket = var.S3Bucket 665 | # s3_key = var.S3Key 666 | reserved_concurrent_executions = -1 667 | role = aws_iam_role.AHA-LambdaExecutionRole.arn 668 | runtime = "python3.11" 669 | 670 | environment { 671 | variables = { 672 | "Slack" = var.SlackWebhookURL != "" ? "True" : null 673 | "Team" = var.MicrosoftTeamsWebhookURL != "" ? "True" : null 674 | "Chime" = var.AmazonChimeWebhookURL != "" ? "True" : null 675 | "Eventbridge" = var.EventBusName != "" ? "True" : null 676 | "DYNAMODB_TABLE" = "${var.dynamodbtable}-${random_string.resource_code.result}" 677 | "EMAIL_SUBJECT" = var.Subject 678 | "EVENT_SEARCH_BACK" = var.EventSearchBack 679 | "FROM_EMAIL" = var.FromEmail 680 | "HEALTH_EVENT_TYPE" = var.AWSHealthEventType 681 | "ORG_STATUS" = var.AWSOrganizationsEnabled 682 | "REGIONS" = var.Regions 683 | "TO_EMAIL" = var.ToEmail 684 | "MANAGEMENT_ROLE_ARN" = var.ManagementAccountRoleArn == "" ? "None" : var.ManagementAccountRoleArn 685 | "ACCOUNT_IDS" = var.ExcludeAccountIDs 686 | "S3_BUCKET" = join("",aws_s3_bucket.AHA-S3Bucket-PrimaryRegion[*].bucket) 687 | } 688 | } 689 | 690 | timeouts {} 691 | 692 | tracing_config { 693 | mode = "PassThrough" 694 | } 695 | tags = { 696 | "Name" = "AHA-LambdaFunction" 697 | } 698 | depends_on = [ 699 | aws_dynamodb_table.AHA-DynamoDBTable, 700 | aws_dynamodb_table.AHA-GlobalDynamoDBTable, 701 | ] 702 | } 703 | 704 | # aws_lambda_function - AHA-LambdaFunction - Secondary region 705 | resource "aws_lambda_function" "AHA-LambdaFunction-SecondaryRegion" { 706 | count = "${var.aha_secondary_region == "" ? 0 : 1}" 707 | provider = aws.secondary_region 708 | description = "Lambda function that runs AHA" 709 | function_name = "AHA-LambdaFunction-${random_string.resource_code.result}" 710 | handler = "handler.main" 711 | memory_size = 128 712 | timeout = 600 713 | filename = data.archive_file.lambda_zip.output_path 714 | source_code_hash = data.archive_file.lambda_zip.output_base64sha256 715 | # s3_bucket = var.S3Bucket 716 | # s3_key = var.S3Key 717 | reserved_concurrent_executions = -1 718 | role = aws_iam_role.AHA-LambdaExecutionRole.arn 719 | runtime = "python3.11" 720 | 721 | environment { 722 | variables = { 723 | "Slack" = var.SlackWebhookURL != "" ? "True" : null 724 | "Team" = var.MicrosoftTeamsWebhookURL != "" ? "True" : null 725 | "Chime" = var.AmazonChimeWebhookURL != "" ? "True" : null 726 | "Eventbridge" = var.EventBusName != "" ? "True" : null 727 | "DYNAMODB_TABLE" = "${var.dynamodbtable}-${random_string.resource_code.result}" 728 | "EMAIL_SUBJECT" = var.Subject 729 | "EVENT_SEARCH_BACK" = var.EventSearchBack 730 | "FROM_EMAIL" = var.FromEmail 731 | "HEALTH_EVENT_TYPE" = var.AWSHealthEventType 732 | "ORG_STATUS" = var.AWSOrganizationsEnabled 733 | "REGIONS" = var.Regions 734 | "TO_EMAIL" = var.ToEmail 735 | "MANAGEMENT_ROLE_ARN" = var.ManagementAccountRoleArn 736 | "ACCOUNT_IDS" = var.ExcludeAccountIDs 737 | "S3_BUCKET" = join("",aws_s3_bucket.AHA-S3Bucket-SecondaryRegion[*].bucket) 738 | } 739 | } 740 | 741 | timeouts {} 742 | 743 | tracing_config { 744 | mode = "PassThrough" 745 | } 746 | tags = { 747 | "Name" = "AHA-LambdaFunction" 748 | } 749 | depends_on = [ 750 | aws_dynamodb_table.AHA-DynamoDBTable, 751 | aws_dynamodb_table.AHA-GlobalDynamoDBTable, 752 | ] 753 | } 754 | 755 | # EventBridge - Schedule to run lambda 756 | resource "aws_cloudwatch_event_rule" "AHA-LambdaSchedule-PrimaryRegion" { 757 | description = "Lambda trigger Event" 758 | event_bus_name = "default" 759 | state = "ENABLED" 760 | name = "AHA-LambdaSchedule-${random_string.resource_code.result}" 761 | schedule_expression = "rate(1 minute)" 762 | tags = { 763 | "Name" = "AHA-LambdaSchedule" 764 | } 765 | } 766 | resource "aws_cloudwatch_event_rule" "AHA-LambdaSchedule-SecondaryRegion" { 767 | description = "Lambda trigger Event" 768 | count = "${var.aha_secondary_region == "" ? 0 : 1}" 769 | provider = aws.secondary_region 770 | event_bus_name = "default" 771 | state = "ENABLED" 772 | name = "AHA-LambdaSchedule-${random_string.resource_code.result}" 773 | schedule_expression = "rate(1 minute)" 774 | tags = { 775 | "Name" = "AHA-LambdaSchedule" 776 | } 777 | } 778 | 779 | resource "aws_cloudwatch_event_target" "AHA-LambdaFunction-PrimaryRegion" { 780 | arn = aws_lambda_function.AHA-LambdaFunction-PrimaryRegion.arn 781 | rule = aws_cloudwatch_event_rule.AHA-LambdaSchedule-PrimaryRegion.name 782 | } 783 | resource "aws_cloudwatch_event_target" "AHA-LambdaFunction-SecondaryRegion" { 784 | count = "${var.aha_secondary_region == "" ? 0 : 1}" 785 | provider = aws.secondary_region 786 | arn = aws_lambda_function.AHA-LambdaFunction-SecondaryRegion[0].arn 787 | rule = aws_cloudwatch_event_rule.AHA-LambdaSchedule-SecondaryRegion[0].name 788 | } 789 | 790 | resource "aws_lambda_permission" "AHA-LambdaSchedulePermission-PrimaryRegion" { 791 | action = "lambda:InvokeFunction" 792 | principal = "events.amazonaws.com" 793 | function_name = aws_lambda_function.AHA-LambdaFunction-PrimaryRegion.arn 794 | source_arn = aws_cloudwatch_event_rule.AHA-LambdaSchedule-PrimaryRegion.arn 795 | } 796 | resource "aws_lambda_permission" "AHA-LambdaSchedulePermission-SecondaryRegion" { 797 | count = "${var.aha_secondary_region == "" ? 0 : 1}" 798 | provider = aws.secondary_region 799 | action = "lambda:InvokeFunction" 800 | principal = "events.amazonaws.com" 801 | function_name = aws_lambda_function.AHA-LambdaFunction-SecondaryRegion[0].arn 802 | source_arn = aws_cloudwatch_event_rule.AHA-LambdaSchedule-SecondaryRegion[0].arn 803 | } 804 | 805 | -------------------------------------------------------------------------------- /messagegenerator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | import sys 4 | import logging 5 | 6 | logger = logging.getLogger() 7 | 8 | 9 | def get_message_for_slack(event_details, event_type, affected_accounts, affected_entities, slack_webhook): 10 | message = "" 11 | summary = "" 12 | if slack_webhook == "services": #Handle "Incoming Webhook" webhooks 13 | if len(affected_entities) >= 1: 14 | affected_entities = "\n".join(affected_entities) 15 | if affected_entities == "UNKNOWN": 16 | affected_entities = "All resources\nin region" 17 | else: 18 | affected_entities = "All resources\nin region" 19 | if len(affected_accounts) >= 1: 20 | affected_accounts = "\n".join(affected_accounts) 21 | else: 22 | affected_accounts = "All accounts\nin region" 23 | if event_type == "create": 24 | summary += ( 25 | f":rotating_light:*[NEW] AWS Health reported an issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 26 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region.*" 27 | ) 28 | message = { 29 | "text": summary, 30 | "attachments": [ 31 | { 32 | "color": "danger", 33 | "fields": [ 34 | { "title": "Account(s)", "value": affected_accounts, "short": True }, 35 | { "title": "Resource(s)", "value": affected_entities, "short": True }, 36 | { "title": "Service", "value": event_details['successfulSet'][0]['event']['service'], "short": True }, 37 | { "title": "Region", "value": event_details['successfulSet'][0]['event']['region'], "short": True }, 38 | { "title": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), "short": True }, 39 | { "title": "Status", "value": event_details['successfulSet'][0]['event']['statusCode'], "short": True }, 40 | { "title": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn'], "short": False }, 41 | { "title": "Updates", "value": get_last_aws_update(event_details), "short": False } 42 | ], 43 | } 44 | ] 45 | } 46 | 47 | elif event_type == "resolve": 48 | summary += ( 49 | f":heavy_check_mark:*[RESOLVED] The AWS Health issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 50 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region is now resolved.*" 51 | ) 52 | message = { 53 | "text": summary, 54 | "attachments": [ 55 | { 56 | "color": "00ff00", 57 | "fields": [ 58 | { "title": "Account(s)", "value": affected_accounts, "short": True }, 59 | { "title": "Resource(s)", "value": affected_entities, "short": True }, 60 | { "title": "Service", "value": event_details['successfulSet'][0]['event']['service'], "short": True }, 61 | { "title": "Region", "value": event_details['successfulSet'][0]['event']['region'], "short": True }, 62 | { "title": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), "short": True }, 63 | { "title": "End Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event'].get('endTime')), "short": True }, 64 | { "title": "Status", "value": event_details['successfulSet'][0]['event']['statusCode'], "short": True }, 65 | { "title": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn'], "short": False }, 66 | { "title": "Updates", "value": get_last_aws_update(event_details), "short": False } 67 | ], 68 | } 69 | ] 70 | } 71 | else: #Handle 'workflows' or 'triggers' webhooks 72 | if len(affected_entities) >= 1: 73 | affected_entities = "\n".join(affected_entities) 74 | if affected_entities == "UNKNOWN": 75 | affected_entities = "All resources\nin region" 76 | else: 77 | affected_entities = "All resources in region" 78 | if len(affected_accounts) >= 1: 79 | affected_accounts = "\n".join(affected_accounts) 80 | else: 81 | affected_accounts = "All accounts in region" 82 | if event_type == "create": 83 | summary += ( 84 | f":rotating_light:*[NEW] AWS Health reported an issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 85 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region.*" 86 | ) 87 | message = { 88 | "text": summary, 89 | "accounts": affected_accounts, 90 | "resources": affected_entities, 91 | "service": event_details['successfulSet'][0]['event']['service'], 92 | "region": event_details['successfulSet'][0]['event']['region'], 93 | "start_time": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), 94 | "status": event_details['successfulSet'][0]['event']['statusCode'], 95 | "event_arn": event_details['successfulSet'][0]['event']['arn'], 96 | "updates": get_last_aws_update(event_details) 97 | } 98 | 99 | elif event_type == "resolve": 100 | summary += ( 101 | f":heavy_check_mark:*[RESOLVED] The AWS Health issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 102 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region is now resolved.*" 103 | ) 104 | message = { 105 | "text": summary, 106 | "accounts": affected_accounts, 107 | "resources": affected_entities, 108 | "service": event_details['successfulSet'][0]['event']['service'], 109 | "region": event_details['successfulSet'][0]['event']['region'], 110 | "start_time": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), 111 | "status": event_details['successfulSet'][0]['event']['statusCode'], 112 | "event_arn": event_details['successfulSet'][0]['event']['arn'], 113 | "updates": get_last_aws_update(event_details) 114 | } 115 | 116 | print("Message sent to Slack: ", message) 117 | return message 118 | 119 | # COMMON compose the event detail field for org and non-org 120 | def get_detail_for_eventbridge(event_details, affected_entities): 121 | 122 | message = {} 123 | 124 | #replace the key "arn" with eventArn to match event format from aws.health 125 | message["eventArn"] = "" 126 | message.update(event_details['successfulSet'][0]['event']) 127 | message["eventArn"] = message.pop("arn") 128 | #message = event_details['successfulSet'][0]['event'] 129 | 130 | message["eventDescription"] = event_details["successfulSet"][0]["eventDescription"] 131 | message["affectedEntities"] = affected_entities 132 | 133 | # Log length of json message for debugging if eventbridge may reject the message as messages 134 | # are limited in size to 256KB 135 | json_message = json.dumps(message) 136 | print("PHD/SHD Message generated for EventBridge with estimated size ", str(sys.getsizeof(json_message) / 1024), "KB: ", message) 137 | 138 | return message 139 | 140 | def get_org_message_for_slack(event_details, event_type, affected_org_accounts, affected_org_entities, slack_webhook): 141 | message = "" 142 | summary = "" 143 | if slack_webhook == "webhook": 144 | if len(affected_org_entities) >= 1: 145 | affected_org_entities = "\n".join(affected_org_entities) 146 | else: 147 | affected_org_entities = "All resources\nin region" 148 | if len(affected_org_accounts) >= 1: 149 | affected_org_accounts = "\n".join(affected_org_accounts) 150 | else: 151 | affected_org_accounts = "All accounts\nin region" 152 | if event_type == "create": 153 | summary += ( 154 | f":rotating_light:*[NEW] AWS Health reported an issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 155 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region.*" 156 | ) 157 | message = { 158 | "text": summary, 159 | "attachments": [ 160 | { 161 | "color": "danger", 162 | "fields": [ 163 | { "title": "Account(s)", "value": affected_org_accounts, "short": True }, 164 | { "title": "Resource(s)", "value": affected_org_entities, "short": True }, 165 | { "title": "Service", "value": event_details['successfulSet'][0]['event']['service'], "short": True }, 166 | { "title": "Region", "value": event_details['successfulSet'][0]['event']['region'], "short": True }, 167 | { "title": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), "short": True }, 168 | { "title": "Status", "value": event_details['successfulSet'][0]['event']['statusCode'], "short": True }, 169 | { "title": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn'], "short": False }, 170 | { "title": "Updates", "value": get_last_aws_update(event_details), "short": False } 171 | ], 172 | } 173 | ] 174 | } 175 | 176 | elif event_type == "resolve": 177 | summary += ( 178 | f":heavy_check_mark:*[RESOLVED] The AWS Health issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 179 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region is now resolved.*" 180 | ) 181 | message = { 182 | "text": summary, 183 | "attachments": [ 184 | { 185 | "color": "00ff00", 186 | "fields": [ 187 | { "title": "Account(s)", "value": affected_org_accounts, "short": True }, 188 | { "title": "Resource(s)", "value": affected_org_entities, "short": True }, 189 | { "title": "Service", "value": event_details['successfulSet'][0]['event']['service'], "short": True }, 190 | { "title": "Region", "value": event_details['successfulSet'][0]['event']['region'], "short": True }, 191 | { "title": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), "short": True }, 192 | { "title": "End Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event'].get('endTime')), "short": True }, 193 | { "title": "Status", "value": event_details['successfulSet'][0]['event']['statusCode'], "short": True }, 194 | { "title": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn'], "short": False }, 195 | { "title": "Updates", "value": get_last_aws_update(event_details), "short": False } 196 | ], 197 | } 198 | ] 199 | } 200 | else: 201 | if len(affected_org_entities) >= 1: 202 | affected_org_entities = "\n".join(affected_org_entities) 203 | else: 204 | affected_org_entities = "All resources in region" 205 | if len(affected_org_accounts) >= 1: 206 | affected_org_accounts = "\n".join(affected_org_accounts) 207 | else: 208 | affected_org_accounts = "All accounts in region" 209 | if event_type == "create": 210 | summary += ( 211 | f":rotating_light:*[NEW] AWS Health reported an issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 212 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region.*" 213 | ) 214 | message = { 215 | "text": summary, 216 | "accounts": affected_org_accounts, 217 | "resources": affected_org_entities, 218 | "service": event_details['successfulSet'][0]['event']['service'], 219 | "region": event_details['successfulSet'][0]['event']['region'], 220 | "start_time": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), 221 | "status": event_details['successfulSet'][0]['event']['statusCode'], 222 | "event_arn": event_details['successfulSet'][0]['event']['arn'], 223 | "updates": get_last_aws_update(event_details) 224 | } 225 | 226 | elif event_type == "resolve": 227 | summary += ( 228 | f":heavy_check_mark:*[RESOLVED] The AWS Health issue with the {event_details['successfulSet'][0]['event']['service'].upper()} service in " 229 | f"the {event_details['successfulSet'][0]['event']['region'].upper()} region is now resolved.*" 230 | ) 231 | message = { 232 | "text": summary, 233 | "accounts": affected_org_accounts, 234 | "resources": affected_org_entities, 235 | "service": event_details['successfulSet'][0]['event']['service'], 236 | "region": event_details['successfulSet'][0]['event']['region'], 237 | "start_time": cleanup_time(event_details['successfulSet'][0]['event']['startTime']), 238 | "status": event_details['successfulSet'][0]['event']['statusCode'], 239 | "event_arn": event_details['successfulSet'][0]['event']['arn'], 240 | "updates": get_last_aws_update(event_details) 241 | } 242 | json.dumps(message) 243 | print("Message sent to Slack: ", message) 244 | return message 245 | 246 | 247 | def get_message_for_chime(event_details, event_type, affected_accounts, affected_entities): 248 | message = "" 249 | if len(affected_entities) >= 1: 250 | affected_entities = "\n".join(affected_entities) 251 | if affected_entities == "UNKNOWN": 252 | affected_entities = "All resources\nin region" 253 | else: 254 | affected_entities = "All resources\nin region" 255 | if len(affected_accounts) >= 1: 256 | affected_accounts = "\n".join(affected_accounts) 257 | else: 258 | affected_accounts = "All accounts\nin region" 259 | summary = "" 260 | if event_type == "create": 261 | 262 | message = str("/md" + "\n" + "**:rotating_light:\[NEW\] AWS Health reported an issue with the " + event_details['successfulSet'][0]['event']['service'].upper() + " service in " + event_details['successfulSet'][0]['event']['region'].upper() + " region.**" + "\n" 263 | "---" + "\n" 264 | "**Account(s)**: " + affected_accounts + "\n" 265 | "**Resource(s)**: " + affected_entities + "\n" 266 | "**Service**: " + event_details['successfulSet'][0]['event']['service'] + "\n" 267 | "**Region**: " + event_details['successfulSet'][0]['event']['region'] + "\n" 268 | "**Start Time (UTC)**: " + cleanup_time(event_details['successfulSet'][0]['event']['startTime']) + "\n" 269 | "**Status**: " + event_details['successfulSet'][0]['event']['statusCode'] + "\n" 270 | "**Event ARN**: " + event_details['successfulSet'][0]['event']['arn'] + "\n" 271 | "**Updates:**" + "\n" + get_last_aws_update(event_details) 272 | ) 273 | 274 | elif event_type == "resolve": 275 | 276 | message = str("/md" + "\n" + "**:heavy_check_mark:\[RESOLVED\] The AWS Health issue with the " + event_details['successfulSet'][0]['event']['service'].upper() + " service in " + event_details['successfulSet'][0]['event']['region'].upper() + " region is now resolved.**" + "\n" 277 | "---" + "\n" 278 | "**Account(s)**: " + affected_accounts + "\n" 279 | "**Resource(s)**: " + affected_entities + "\n" 280 | "**Service**: " + event_details['successfulSet'][0]['event']['service'] + "\n" 281 | "**Region**: " + event_details['successfulSet'][0]['event']['region'] + "\n" 282 | "**Start Time (UTC)**: " + cleanup_time(event_details['successfulSet'][0]['event']['startTime']) + "\n" 283 | "**End Time (UTC)**: " + cleanup_time(event_details['successfulSet'][0]['event'].get('endTime')) + "\n" 284 | "**Status**: " + event_details['successfulSet'][0]['event']['statusCode'] + "\n" 285 | "**Event ARN**: " + event_details['successfulSet'][0]['event']['arn'] + "\n" 286 | "**Updates:**" + "\n" + get_last_aws_update(event_details) 287 | ) 288 | message = truncate_message_if_needed(message, 4096) 289 | print("Message sent to Chime: ", message) 290 | return message 291 | 292 | 293 | def get_org_message_for_chime(event_details, event_type, affected_org_accounts, affected_org_entities): 294 | message = "" 295 | summary = "" 296 | if len(affected_org_entities) >= 1: 297 | affected_org_entities = "\n".join(affected_org_entities) 298 | else: 299 | affected_org_entities = "All resources in region" 300 | if len(affected_org_accounts) >= 1: 301 | affected_org_accounts = "\n".join(affected_org_accounts) 302 | else: 303 | affected_org_accounts = "All accounts in region" 304 | if event_type == "create": 305 | 306 | message = str("/md" + "\n" + "**:rotating_light:\[NEW\] AWS Health reported an issue with the " + event_details['successfulSet'][0]['event']['service'].upper()) + " service in " + str(event_details['successfulSet'][0]['event']['region'].upper() + " region**" + "\n" 307 | "---" + "\n" 308 | "**Account(s)**: " + affected_org_accounts + "\n" 309 | "**Resource(s)**: " + affected_org_entities + "\n" 310 | "**Service**: " + event_details['successfulSet'][0]['event']['service'] + "\n" 311 | "**Region**: " + event_details['successfulSet'][0]['event']['region'] + "\n" 312 | "**Start Time (UTC)**: " + cleanup_time(event_details['successfulSet'][0]['event']['startTime']) + "\n" 313 | "**Status**: " + event_details['successfulSet'][0]['event']['statusCode'] + "\n" 314 | "**Event ARN**: " + event_details['successfulSet'][0]['event']['arn'] + "\n" 315 | "**Updates:**" + "\n" + get_last_aws_update(event_details) 316 | ) 317 | 318 | elif event_type == "resolve": 319 | 320 | message = str("/md" + "\n" + "**:heavy_check_mark:\[RESOLVED\] The AWS Health issue with the " + event_details['successfulSet'][0]['event']['service'].upper()) + " service in " + str(event_details['successfulSet'][0]['event']['region'].upper() + " region is now resolved.**" + "\n" 321 | "---" + "\n" 322 | "**Account(s)**: " + affected_org_accounts + "\n" 323 | "**Resource(s)**: " + affected_org_entities + "\n" 324 | "**Service**: " + event_details['successfulSet'][0]['event']['service'] + "\n" 325 | "**Region**: " + event_details['successfulSet'][0]['event']['region'] + "\n" 326 | "**Start Time (UTC)**: " + cleanup_time(event_details['successfulSet'][0]['event']['startTime']) + "\n" 327 | "**End Time (UTC)**: " + cleanup_time(event_details['successfulSet'][0]['event'].get('endTime')) + "\n" 328 | "**Status**: " + event_details['successfulSet'][0]['event']['statusCode'] + "\n" 329 | "**Event ARN**: " + event_details['successfulSet'][0]['event']['arn'] + "\n" 330 | "**Updates:**" + "\n" + get_last_aws_update(event_details) 331 | ) 332 | message = truncate_message_if_needed(message, 4096) 333 | print("Message sent to Chime: ", message) 334 | return message 335 | 336 | 337 | 338 | def get_message_for_teams(event_details, event_type, affected_accounts, affected_entities): 339 | message = "" 340 | if len(affected_entities) >= 1: 341 | affected_entities = "\n".join(affected_entities) 342 | if affected_entities == "UNKNOWN": 343 | affected_entities = "All resources\nin region" 344 | else: 345 | affected_entities = "All resources\nin region" 346 | if len(affected_accounts) >= 1: 347 | affected_accounts = "\n".join(affected_accounts) 348 | else: 349 | affected_accounts = "All accounts\nin region" 350 | summary = "" 351 | if event_type == "create": 352 | title = "🚨 [NEW] AWS Health reported an issue with the " + event_details['successfulSet'][0]['event'][ 353 | 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ 354 | 'region'].upper() + " region." 355 | message = { 356 | "@type": "MessageCard", 357 | "@context": "http://schema.org/extensions", 358 | "themeColor": "FF0000", 359 | "summary": "AWS Health Aware Alert", 360 | "sections": [ 361 | { 362 | "activityTitle": str(title), 363 | "markdown": False, 364 | "facts": [ 365 | {"name": "Account(s)", "value": affected_accounts}, 366 | {"name": "Resource(s)", "value": affected_entities}, 367 | {"name": "Service", "value": event_details['successfulSet'][0]['event']['service']}, 368 | {"name": "Region", "value": event_details['successfulSet'][0]['event']['region']}, 369 | {"name": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}, 370 | {"name": "Status", "value": event_details['successfulSet'][0]['event']['statusCode']}, 371 | {"name": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn']}, 372 | {"name": "Updates", "value": get_last_aws_update(event_details)} 373 | ], 374 | } 375 | ] 376 | } 377 | 378 | elif event_type == "resolve": 379 | title = "✅ [RESOLVED] The AWS Health issue with the " + event_details['successfulSet'][0]['event'][ 380 | 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ 381 | 'region'].upper() + " region is now resolved." 382 | message = { 383 | "@type": "MessageCard", 384 | "@context": "http://schema.org/extensions", 385 | "themeColor": "00ff00", 386 | "summary": "AWS Health Aware Alert", 387 | "sections": [ 388 | { 389 | "activityTitle": str(title), 390 | "markdown": False, 391 | "facts": [ 392 | {"name": "Account(s)", "value": affected_accounts}, 393 | {"name": "Resource(s)", "value": affected_entities}, 394 | {"name": "Service", "value": event_details['successfulSet'][0]['event']['service']}, 395 | {"name": "Region", "value": event_details['successfulSet'][0]['event']['region']}, 396 | {"name": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}, 397 | {"name": "End Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event'].get('endTime'))}, 398 | {"name": "Status", "value": event_details['successfulSet'][0]['event']['statusCode']}, 399 | {"name": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn']}, 400 | {"name": "Updates", "value": get_last_aws_update(event_details)} 401 | ], 402 | } 403 | ] 404 | } 405 | print("Message sent to Teams: ", message) 406 | return message 407 | 408 | 409 | def get_org_message_for_teams(event_details, event_type, affected_org_accounts, affected_org_entities): 410 | message = "" 411 | summary = "" 412 | if len(affected_org_entities) >= 1: 413 | affected_org_entities = "\n".join(affected_org_entities) 414 | else: 415 | affected_org_entities = "All resources in region" 416 | if len(affected_org_accounts) >= 1: 417 | affected_org_accounts = "\n".join(affected_org_accounts) 418 | else: 419 | affected_org_accounts = "All accounts in region" 420 | if event_type == "create": 421 | title = "🚨 [NEW] AWS Health reported an issue with the " + event_details['successfulSet'][0]['event'][ 422 | 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ 423 | 'region'].upper() + " region." 424 | message = { 425 | "@type": "MessageCard", 426 | "@context": "http://schema.org/extensions", 427 | "themeColor": "FF0000", 428 | "summary": "AWS Health Aware Alert", 429 | "sections": [ 430 | { 431 | "activityTitle": title, 432 | "markdown": False, 433 | "facts": [ 434 | {"name": "Account(s)", "value": affected_org_accounts}, 435 | {"name": "Resource(s)", "value": affected_org_entities}, 436 | {"name": "Service", "value": event_details['successfulSet'][0]['event']['service']}, 437 | {"name": "Region", "value": event_details['successfulSet'][0]['event']['region']}, 438 | {"name": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}, 439 | {"name": "Status", "value": event_details['successfulSet'][0]['event']['statusCode']}, 440 | {"name": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn']}, 441 | {"name": "Updates", "value": event_details['successfulSet'][0]['eventDescription']['latestDescription']} 442 | ], 443 | } 444 | ] 445 | } 446 | 447 | elif event_type == "resolve": 448 | title = "✅ [RESOLVED] The AWS Health issue with the " + event_details['successfulSet'][0]['event'][ 449 | 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ 450 | 'region'].upper() + " region is now resolved." 451 | message = { 452 | "@type": "MessageCard", 453 | "@context": "http://schema.org/extensions", 454 | "themeColor": "00ff00", 455 | "summary": "AWS Health Aware Alert", 456 | "sections": [ 457 | { 458 | "activityTitle": title, 459 | "markdown": False, 460 | "facts": [ 461 | {"name": "Account(s)", "value": affected_org_accounts}, 462 | {"name": "Resource(s)", "value": affected_org_entities}, 463 | {"name": "Service", "value": event_details['successfulSet'][0]['event']['service']}, 464 | {"name": "Region", "value": event_details['successfulSet'][0]['event']['region']}, 465 | {"name": "Start Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}, 466 | {"name": "End Time (UTC)", "value": cleanup_time(event_details['successfulSet'][0]['event'].get('endTime'))}, 467 | {"name": "Status", "value": event_details['successfulSet'][0]['event']['statusCode']}, 468 | {"name": "Event ARN", "value": event_details['successfulSet'][0]['event']['arn']}, 469 | {"name": "Updates", "value": event_details['successfulSet'][0]['eventDescription']['latestDescription']} 470 | ], 471 | } 472 | ] 473 | } 474 | return message 475 | print("Message sent to Teams: ", message) 476 | 477 | 478 | def get_message_for_email(event_details, event_type, affected_accounts, affected_entities): 479 | # Not srue why we have the new line in the affected entities code here 480 | if len(affected_entities) >= 1: 481 | affected_entities = "\n".join(affected_entities) 482 | if affected_entities == "UNKNOWN": 483 | affected_entities = "All resources\nin region" 484 | else: 485 | affected_entities = "All resources\nin region" 486 | if len(affected_accounts) >= 1: 487 | affected_accounts = "\n".join(affected_accounts) 488 | else: 489 | affected_accounts = "All accounts\nin region" 490 | if event_type == "create": 491 | BODY_HTML = f""" 492 | 493 |
494 |There is an AWS incident that is in effect which may likely impact your resources. Here are the details:
496 | Account(s): {affected_accounts}
497 | Resource(s): {affected_entities}
498 | Service: {event_details['successfulSet'][0]['event']['service']}
499 | Region: {event_details['successfulSet'][0]['event']['region']}
500 | Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}
501 | Status: {event_details['successfulSet'][0]['event']['statusCode']}
502 | Event ARN: {event_details['successfulSet'][0]['event']['arn']}
503 | Updates: {event_details['successfulSet'][0]['eventDescription']['latestDescription']}
504 | For updates, please visit the AWS Service Health Dashboard
505 | If you are experiencing issues related to this event, please open an AWS Support case within your account.
506 | Thanks,
AHA: AWS Health Aware
507 |
Good news! The AWS Health incident from earlier has now been marked as resolved.
517 | Account(s): {affected_accounts}
518 | Resource(s): {affected_entities}
519 | Service: {event_details['successfulSet'][0]['event']['service']}
520 | Region: {event_details['successfulSet'][0]['event']['region']}
521 | Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}
522 | End Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event'].get('endTime'))}
523 | Status: {event_details['successfulSet'][0]['event']['statusCode']}
524 | Event ARN: {event_details['successfulSet'][0]['event']['arn']}
525 | Updates: {event_details['successfulSet'][0]['eventDescription']['latestDescription']}
526 | If you are still experiencing issues related to this event, please open an AWS Support case within your account.
527 |
528 | Thanks,
AHA: AWS Health Aware
529 |
There is an AWS incident that is in effect which may likely impact your resources. Here are the details:
552 | Account(s): {affected_org_accounts}
553 | Resource(s): {affected_org_entities}
554 | Service: {event_details['successfulSet'][0]['event']['service']}
555 | Region: {event_details['successfulSet'][0]['event']['region']}
556 | Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}
557 | Status: {event_details['successfulSet'][0]['event']['statusCode']}
558 | Event ARN: {event_details['successfulSet'][0]['event']['arn']}
559 | Updates: {event_details['successfulSet'][0]['eventDescription']['latestDescription']}
560 | For updates, please visit the AWS Service Health Dashboard
561 | If you are experiencing issues related to this event, please open an AWS Support case within your account.
562 | Thanks,
AHA: AWS Health Aware
563 |
Good news! The AWS Health incident from earlier has now been marked as resolved.
573 | Account(s): {affected_org_accounts}
574 | Resource(s): {affected_org_entities}
575 | Service: {event_details['successfulSet'][0]['event']['service']}
576 | Region: {event_details['successfulSet'][0]['event']['region']}
577 | Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}
578 | End Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event'].get('endTime'))}
579 | Status: {event_details['successfulSet'][0]['event']['statusCode']}
580 | Event ARN: {event_details['successfulSet'][0]['event']['arn']}
581 | Updates: {event_details['successfulSet'][0]['eventDescription']['latestDescription']}
582 | If you are still experiencing issues related to this event, please open an AWS Support case within your account.
583 | Thanks,
AHA: AWS Health Aware
584 |