Skip to content

Commit 928494c

Browse files
authored
Merge pull request #105 from aws-samples/v2.3
publish AHA v2.3
2 parents cfb7863 + ce79010 commit 928494c

File tree

5 files changed

+161
-121
lines changed

5 files changed

+161
-121
lines changed

ExcludeAccountIDs(sample).csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
000000000000
2+
111111111111

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ AWS Health Aware (AHA) is an automated notification tool for sending well-format
4646

4747
# What's New
4848

49-
Release 2.2 introduces an updated schema for Health events delivered to an EventBridge bus. This allows simplified matching of events which you can then consume with other AWS services or SaaS solutions.
50-
Read more about the [new feature and how to filter events using EventBridge](https://github.com/aws-samples/aws-health-aware/blob/main/new_aha_event_schema.md).
49+
Release 2.3 introduces runtime performance improvements, terraform updates, allows use of Slack Workflow 2.0 webhooks (triggers), general fixes and documentation updates.
5150

5251
# Architecture
5352

@@ -447,6 +446,8 @@ $ terraform apply
447446
**If for some reason, you still have issues after updating, you can easily just delete the stack and redeploy. The infrastructure can be destroyed and rebuilt within minutes through Terraform.**
448447

449448
# New Features
449+
*Release 2.2*
450+
450451
We are happy to announce the launch of new enhancements to AHA. Please try them out and keep sending us your feedback!
451452
1. A revised schema for AHA events sent to EventBridge which enables new filtering and routing options. See the [new AHA event schema readme](new_aha_event_schema.md) for more detail.
452453
2. Multi-region deployment option
@@ -462,6 +463,8 @@ We are happy to announce the launch of new enhancements to AHA. Please try them
462463
* If for whatever reason you need to update the Webhook URL; just update the CloudFormation or terraform Template with the new Webhook URL.
463464
* If you are expecting an event and it did not show up it may be an oddly formed event. Take a look at *CloudWatch > Log groups* and search for the name of your Lambda function. See what the error is and reach out to us [email](mailto:aha-builders@amazon.com) for help.
464465
* If for any errors related to duplicate secrets during deployment, try deleting manually and redeploy the solution. Example command to delete SlackChannelID secret in us-east-1 region.
465-
```
466-
$ aws secretsmanager delete-secret --secret-id SlackChannelID --force-delete-without-recovery --region us-east-1
467-
```
466+
```
467+
$ aws secretsmanager delete-secret --secret-id SlackChannelID --force-delete-without-recovery --region us-east-1
468+
```
469+
* If you want to Exclude certain accounts from notifications, confirm your exlcusions file matches the format of the [sample ExcludeAccountIDs.csv file](ExcludeAccountIDs(sample).csv) with one account ID per line with no trailing commas (trailing commas indicate a null cell).
470+
* If your accounts listed in the CSV file are not excluded, check the CloudWatch log group for the AHA Lambda function for the message "Key filename is not a .csv file" as an indicator of any issues with your file.

handler.py

Lines changed: 100 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import json
2+
import logging
3+
from functools import lru_cache
4+
25
import boto3
36
import os
4-
import re
5-
import time
6-
import decimal
77
import socket
8-
import configparser
98
from dateutil import parser
109
from datetime import datetime, timedelta
11-
from urllib.parse import urlencode
1210
from urllib.request import Request, urlopen, URLError, HTTPError
1311
from botocore.config import Config
1412
from botocore.exceptions import ClientError
15-
from boto3.dynamodb.conditions import Key, Attr
1613
from messagegenerator import (
1714
get_message_for_slack,
1815
get_org_message_for_slack,
@@ -25,6 +22,40 @@
2522
get_detail_for_eventbridge,
2623
)
2724

25+
logger = logging.getLogger()
26+
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO").upper())
27+
28+
class CachedSecrets:
29+
def __init__(self, client):
30+
self.client = client
31+
32+
@lru_cache
33+
def get_secret_value(self, *args, **kwargs):
34+
logger.debug(f"Getting secret {kwargs}")
35+
return self.client.get_secret_value(*args, **kwargs)
36+
37+
38+
class AWSApi:
39+
@lru_cache
40+
def client(self, *args, **kwargs):
41+
logger.debug(f"Returning new boto3 client for: {args}")
42+
return boto3.client(*args, **kwargs)
43+
44+
@lru_cache
45+
def resource(self, resource_name):
46+
logger.debug(f"Returning new boto3 resource for: {resource_name}")
47+
return boto3.resource(resource_name)
48+
49+
def cache_clear(self):
50+
self.client.cache_clear()
51+
self.resource.cache_clear()
52+
53+
@lru_cache
54+
def secretsmanager(self, **kwargs):
55+
client = boto3.client("secretsmanager", **kwargs)
56+
return CachedSecrets(client)
57+
58+
2859
print("boto3 version: ", boto3.__version__)
2960

3061
# query active health API endpoint
@@ -44,6 +75,8 @@
4475
),
4576
)
4677

78+
aws_api = AWSApi()
79+
4780

4881
# TODO decide if account_name should be blank on error
4982
# Get Account Name
@@ -85,42 +118,31 @@ def send_alert(event_details, affected_accounts, affected_entities, event_type):
85118
except URLError as e:
86119
print("Server connection failed: ", e.reason)
87120
pass
88-
if "hooks.slack.com/services" in slack_url:
89-
try:
90-
print("Sending the alert to Slack Webhook Channel")
91-
send_to_slack(
92-
get_message_for_slack(
93-
event_details,
94-
event_type,
95-
affected_accounts,
96-
resources,
97-
slack_webhook="webhook",
98-
),
99-
slack_url,
100-
)
101-
except HTTPError as e:
102-
print("Got an error while sending message to Slack: ", e.code, e.reason)
103-
except URLError as e:
104-
print("Server connection failed: ", e.reason)
105-
pass
106-
if "hooks.slack.com/workflows" in slack_url:
107-
try:
108-
print("Sending the alert to Slack Workflows Channel")
109-
send_to_slack(
110-
get_message_for_slack(
111-
event_details,
112-
event_type,
113-
affected_accounts,
114-
resources,
115-
slack_webhook="workflow",
116-
),
117-
slack_url,
118-
)
119-
except HTTPError as e:
120-
print("Got an error while sending message to Slack: ", e.code, e.reason)
121-
except URLError as e:
122-
print("Server connection failed: ", e.reason)
123-
pass
121+
#Slack Notification Handling
122+
if slack_url != "None":
123+
for slack_webhook_type in ["services", "triggers", "workflows"]:
124+
if ("hooks.slack.com/" + slack_webhook_type) in slack_url:
125+
print("Sending the alert to Slack Webhook Channel")
126+
try:
127+
send_to_slack(
128+
get_message_for_slack(
129+
event_details,
130+
event_type,
131+
affected_accounts,
132+
resources,
133+
slack_webhook_type,
134+
),
135+
slack_url,
136+
)
137+
break
138+
except HTTPError as e:
139+
print("Got an error while sending message to Slack: ", e.code, e.reason)
140+
except URLError as e:
141+
print("Server connection failed: ", e.reason)
142+
pass
143+
else:
144+
print("Unsupported format in Slack Webhook")
145+
124146
if "office.com/webhook" in teams_url:
125147
try:
126148
print("Sending the alert to Teams")
@@ -190,42 +212,31 @@ def send_org_alert(
190212
except URLError as e:
191213
print("Server connection failed: ", e.reason)
192214
pass
193-
if "hooks.slack.com/services" in slack_url:
194-
try:
195-
print("Sending the alert to Slack Webhook Channel")
196-
send_to_slack(
197-
get_org_message_for_slack(
198-
event_details,
199-
event_type,
200-
affected_org_accounts,
201-
resources,
202-
slack_webhook="webhook",
203-
),
204-
slack_url,
205-
)
206-
except HTTPError as e:
207-
print("Got an error while sending message to Slack: ", e.code, e.reason)
208-
except URLError as e:
209-
print("Server connection failed: ", e.reason)
210-
pass
211-
if "hooks.slack.com/workflows" in slack_url:
212-
try:
213-
print("Sending the alert to Slack Workflow Channel")
214-
send_to_slack(
215-
get_org_message_for_slack(
216-
event_details,
217-
event_type,
218-
affected_org_accounts,
219-
resources,
220-
slack_webhook="workflow",
221-
),
222-
slack_url,
223-
)
224-
except HTTPError as e:
225-
print("Got an error while sending message to Slack: ", e.code, e.reason)
226-
except URLError as e:
227-
print("Server connection failed: ", e.reason)
228-
pass
215+
#Slack Notification Handling
216+
if slack_url != "None":
217+
for slack_webhook_type in ["services", "triggers", "workflows"]:
218+
if ("hooks.slack.com/" + slack_webhook_type) in slack_url:
219+
print("Sending the alert to Slack Webhook Channel")
220+
try:
221+
send_to_slack(
222+
get_message_for_slack(
223+
event_details,
224+
event_type,
225+
affected_org_accounts,
226+
resources,
227+
slack_webhook_type,
228+
),
229+
slack_url,
230+
)
231+
break
232+
except HTTPError as e:
233+
print("Got an error while sending message to Slack: ", e.code, e.reason)
234+
except URLError as e:
235+
print("Server connection failed: ", e.reason)
236+
pass
237+
else:
238+
print("Unsupported format in Slack Webhook")
239+
229240
if "office.com/webhook" in teams_url:
230241
try:
231242
print("Sending the alert to Teams")
@@ -323,7 +334,7 @@ def send_email(event_details, eventType, affected_accounts, affected_entities):
323334
BODY_HTML = get_message_for_email(
324335
event_details, eventType, affected_accounts, affected_entities
325336
)
326-
client = boto3.client("ses", region_name=AWS_REGION)
337+
client = aws_api.client("ses", AWS_REGION)
327338
response = client.send_email(
328339
Source=SENDER,
329340
Destination={"ToAddresses": RECIPIENT},
@@ -350,7 +361,7 @@ def send_org_email(
350361
BODY_HTML = get_org_message_for_email(
351362
event_details, eventType, affected_org_accounts, affected_org_entities
352363
)
353-
client = boto3.client("ses", region_name=AWS_REGION)
364+
client = aws_api.client("ses", AWS_REGION)
354365
response = client.send_email(
355366
Source=SENDER,
356367
Destination={"ToAddresses": RECIPIENT},
@@ -469,7 +480,7 @@ def update_org_ddb(
469480
affected_org_entities,
470481
):
471482
# open dynamoDB
472-
dynamodb = boto3.resource("dynamodb")
483+
dynamodb = aws_api.resource("dynamodb")
473484
ddb_table = os.environ["DYNAMODB_TABLE"]
474485
aha_ddb_table = dynamodb.Table(ddb_table)
475486
event_latestDescription = event_details["successfulSet"][0]["eventDescription"][
@@ -584,7 +595,7 @@ def update_ddb(
584595
affected_entities,
585596
):
586597
# open dynamoDB
587-
dynamodb = boto3.resource("dynamodb")
598+
dynamodb = aws_api.resource("dynamodb")
588599
ddb_table = os.environ["DYNAMODB_TABLE"]
589600
aha_ddb_table = dynamodb.Table(ddb_table)
590601
event_latestDescription = event_details["successfulSet"][0]["eventDescription"][
@@ -698,8 +709,7 @@ def get_secrets():
698709
secrets = {}
699710

700711
# create a Secrets Manager client
701-
session = boto3.session.Session()
702-
client = session.client(service_name="secretsmanager", region_name=region_name)
712+
client = aws_api.secretsmanager(region_name=region_name)
703713
# Iteration through the configured AWS Secrets
704714
secrets["teams"] = (
705715
get_secret(secret_teams_name, client) if "Teams" in os.environ else "None"
@@ -965,7 +975,7 @@ def send_to_eventbridge(message, event_type, resources, event_bus):
965975
print(
966976
"Sending response to Eventbridge - event_type, event_bus", event_type, event_bus
967977
)
968-
client = boto3.client("events")
978+
client = aws_api.client("events")
969979

970980
entries = eventbridge_generate_entries(message, resources, event_bus)
971981

@@ -980,7 +990,7 @@ def getAccountIDs():
980990
key_file_name = os.environ["ACCOUNT_IDS"]
981991
print("Key filename is - ", key_file_name)
982992
if os.path.splitext(os.path.basename(key_file_name))[1] == ".csv":
983-
s3 = boto3.client("s3")
993+
s3 = aws_api.client("s3")
984994
data = s3.get_object(Bucket=os.environ["S3_BUCKET"], Key=key_file_name)
985995
account_ids = [account.decode("utf-8") for account in data["Body"].iter_lines()]
986996
else:
@@ -998,7 +1008,7 @@ def get_sts_token(service):
9981008
SECRET_KEY = []
9991009
SESSION_TOKEN = []
10001010

1001-
sts_connection = boto3.client("sts")
1011+
sts_connection = aws_api.client("sts")
10021012

10031013
ct = datetime.now()
10041014
role_session_name = "cross_acct_aha_session"
@@ -1014,7 +1024,7 @@ def get_sts_token(service):
10141024
SESSION_TOKEN = acct_b["Credentials"]["SessionToken"]
10151025

10161026
# create service client using the assumed role credentials, e.g. S3
1017-
boto3_client = boto3.client(
1027+
boto3_client = aws_api.client(
10181028
service,
10191029
config=config,
10201030
aws_access_key_id=ACCESS_KEY,
@@ -1023,13 +1033,14 @@ def get_sts_token(service):
10231033
)
10241034
print("Running in member account deployment mode")
10251035
else:
1026-
boto3_client = boto3.client(service, config=config)
1036+
boto3_client = aws_api.client(service, config=config)
10271037
print("Running in management account deployment mode")
10281038

10291039
return boto3_client
10301040

10311041

10321042
def main(event, context):
1043+
aws_api.cache_clear()
10331044
print("THANK YOU FOR CHOOSING AWS HEALTH AWARE!")
10341045
health_client = get_sts_token("health")
10351046
org_status = os.environ["ORG_STATUS"]

0 commit comments

Comments
 (0)