Skip to content

Commit

Permalink
Merge pull request #105 from aws-samples/v2.3
Browse files Browse the repository at this point in the history
publish AHA v2.3
  • Loading branch information
andrewcr7 authored Nov 25, 2024
2 parents cfb7863 + ce79010 commit 928494c
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 121 deletions.
2 changes: 2 additions & 0 deletions ExcludeAccountIDs(sample).csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
000000000000
111111111111
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ AWS Health Aware (AHA) is an automated notification tool for sending well-format

# What's New

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.
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).
Release 2.3 introduces runtime performance improvements, terraform updates, allows use of Slack Workflow 2.0 webhooks (triggers), general fixes and documentation updates.

# Architecture

Expand Down Expand Up @@ -447,6 +446,8 @@ $ terraform apply
**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.**

# New Features
*Release 2.2*

We are happy to announce the launch of new enhancements to AHA. Please try them out and keep sending us your feedback!
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.
2. Multi-region deployment option
Expand All @@ -462,6 +463,8 @@ We are happy to announce the launch of new enhancements to AHA. Please try them
* If for whatever reason you need to update the Webhook URL; just update the CloudFormation or terraform Template with the new Webhook URL.
* 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:[email protected]) for help.
* 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.
```
$ aws secretsmanager delete-secret --secret-id SlackChannelID --force-delete-without-recovery --region us-east-1
```
```
$ aws secretsmanager delete-secret --secret-id SlackChannelID --force-delete-without-recovery --region us-east-1
```
* 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).
* 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.
189 changes: 100 additions & 89 deletions handler.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import json
import logging
from functools import lru_cache

import boto3
import os
import re
import time
import decimal
import socket
import configparser
from dateutil import parser
from datetime import datetime, timedelta
from urllib.parse import urlencode
from urllib.request import Request, urlopen, URLError, HTTPError
from botocore.config import Config
from botocore.exceptions import ClientError
from boto3.dynamodb.conditions import Key, Attr
from messagegenerator import (
get_message_for_slack,
get_org_message_for_slack,
Expand All @@ -25,6 +22,40 @@
get_detail_for_eventbridge,
)

logger = logging.getLogger()
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO").upper())

class CachedSecrets:
def __init__(self, client):
self.client = client

@lru_cache
def get_secret_value(self, *args, **kwargs):
logger.debug(f"Getting secret {kwargs}")
return self.client.get_secret_value(*args, **kwargs)


class AWSApi:
@lru_cache
def client(self, *args, **kwargs):
logger.debug(f"Returning new boto3 client for: {args}")
return boto3.client(*args, **kwargs)

@lru_cache
def resource(self, resource_name):
logger.debug(f"Returning new boto3 resource for: {resource_name}")
return boto3.resource(resource_name)

def cache_clear(self):
self.client.cache_clear()
self.resource.cache_clear()

@lru_cache
def secretsmanager(self, **kwargs):
client = boto3.client("secretsmanager", **kwargs)
return CachedSecrets(client)


print("boto3 version: ", boto3.__version__)

# query active health API endpoint
Expand All @@ -44,6 +75,8 @@
),
)

aws_api = AWSApi()


# TODO decide if account_name should be blank on error
# Get Account Name
Expand Down Expand Up @@ -85,42 +118,31 @@ def send_alert(event_details, affected_accounts, affected_entities, event_type):
except URLError as e:
print("Server connection failed: ", e.reason)
pass
if "hooks.slack.com/services" in slack_url:
try:
print("Sending the alert to Slack Webhook Channel")
send_to_slack(
get_message_for_slack(
event_details,
event_type,
affected_accounts,
resources,
slack_webhook="webhook",
),
slack_url,
)
except HTTPError as e:
print("Got an error while sending message to Slack: ", e.code, e.reason)
except URLError as e:
print("Server connection failed: ", e.reason)
pass
if "hooks.slack.com/workflows" in slack_url:
try:
print("Sending the alert to Slack Workflows Channel")
send_to_slack(
get_message_for_slack(
event_details,
event_type,
affected_accounts,
resources,
slack_webhook="workflow",
),
slack_url,
)
except HTTPError as e:
print("Got an error while sending message to Slack: ", e.code, e.reason)
except URLError as e:
print("Server connection failed: ", e.reason)
pass
#Slack Notification Handling
if slack_url != "None":
for slack_webhook_type in ["services", "triggers", "workflows"]:
if ("hooks.slack.com/" + slack_webhook_type) in slack_url:
print("Sending the alert to Slack Webhook Channel")
try:
send_to_slack(
get_message_for_slack(
event_details,
event_type,
affected_accounts,
resources,
slack_webhook_type,
),
slack_url,
)
break
except HTTPError as e:
print("Got an error while sending message to Slack: ", e.code, e.reason)
except URLError as e:
print("Server connection failed: ", e.reason)
pass
else:
print("Unsupported format in Slack Webhook")

if "office.com/webhook" in teams_url:
try:
print("Sending the alert to Teams")
Expand Down Expand Up @@ -190,42 +212,31 @@ def send_org_alert(
except URLError as e:
print("Server connection failed: ", e.reason)
pass
if "hooks.slack.com/services" in slack_url:
try:
print("Sending the alert to Slack Webhook Channel")
send_to_slack(
get_org_message_for_slack(
event_details,
event_type,
affected_org_accounts,
resources,
slack_webhook="webhook",
),
slack_url,
)
except HTTPError as e:
print("Got an error while sending message to Slack: ", e.code, e.reason)
except URLError as e:
print("Server connection failed: ", e.reason)
pass
if "hooks.slack.com/workflows" in slack_url:
try:
print("Sending the alert to Slack Workflow Channel")
send_to_slack(
get_org_message_for_slack(
event_details,
event_type,
affected_org_accounts,
resources,
slack_webhook="workflow",
),
slack_url,
)
except HTTPError as e:
print("Got an error while sending message to Slack: ", e.code, e.reason)
except URLError as e:
print("Server connection failed: ", e.reason)
pass
#Slack Notification Handling
if slack_url != "None":
for slack_webhook_type in ["services", "triggers", "workflows"]:
if ("hooks.slack.com/" + slack_webhook_type) in slack_url:
print("Sending the alert to Slack Webhook Channel")
try:
send_to_slack(
get_message_for_slack(
event_details,
event_type,
affected_org_accounts,
resources,
slack_webhook_type,
),
slack_url,
)
break
except HTTPError as e:
print("Got an error while sending message to Slack: ", e.code, e.reason)
except URLError as e:
print("Server connection failed: ", e.reason)
pass
else:
print("Unsupported format in Slack Webhook")

if "office.com/webhook" in teams_url:
try:
print("Sending the alert to Teams")
Expand Down Expand Up @@ -323,7 +334,7 @@ def send_email(event_details, eventType, affected_accounts, affected_entities):
BODY_HTML = get_message_for_email(
event_details, eventType, affected_accounts, affected_entities
)
client = boto3.client("ses", region_name=AWS_REGION)
client = aws_api.client("ses", AWS_REGION)
response = client.send_email(
Source=SENDER,
Destination={"ToAddresses": RECIPIENT},
Expand All @@ -350,7 +361,7 @@ def send_org_email(
BODY_HTML = get_org_message_for_email(
event_details, eventType, affected_org_accounts, affected_org_entities
)
client = boto3.client("ses", region_name=AWS_REGION)
client = aws_api.client("ses", AWS_REGION)
response = client.send_email(
Source=SENDER,
Destination={"ToAddresses": RECIPIENT},
Expand Down Expand Up @@ -469,7 +480,7 @@ def update_org_ddb(
affected_org_entities,
):
# open dynamoDB
dynamodb = boto3.resource("dynamodb")
dynamodb = aws_api.resource("dynamodb")
ddb_table = os.environ["DYNAMODB_TABLE"]
aha_ddb_table = dynamodb.Table(ddb_table)
event_latestDescription = event_details["successfulSet"][0]["eventDescription"][
Expand Down Expand Up @@ -584,7 +595,7 @@ def update_ddb(
affected_entities,
):
# open dynamoDB
dynamodb = boto3.resource("dynamodb")
dynamodb = aws_api.resource("dynamodb")
ddb_table = os.environ["DYNAMODB_TABLE"]
aha_ddb_table = dynamodb.Table(ddb_table)
event_latestDescription = event_details["successfulSet"][0]["eventDescription"][
Expand Down Expand Up @@ -698,8 +709,7 @@ def get_secrets():
secrets = {}

# create a Secrets Manager client
session = boto3.session.Session()
client = session.client(service_name="secretsmanager", region_name=region_name)
client = aws_api.secretsmanager(region_name=region_name)
# Iteration through the configured AWS Secrets
secrets["teams"] = (
get_secret(secret_teams_name, client) if "Teams" in os.environ else "None"
Expand Down Expand Up @@ -965,7 +975,7 @@ def send_to_eventbridge(message, event_type, resources, event_bus):
print(
"Sending response to Eventbridge - event_type, event_bus", event_type, event_bus
)
client = boto3.client("events")
client = aws_api.client("events")

entries = eventbridge_generate_entries(message, resources, event_bus)

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

sts_connection = boto3.client("sts")
sts_connection = aws_api.client("sts")

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

# create service client using the assumed role credentials, e.g. S3
boto3_client = boto3.client(
boto3_client = aws_api.client(
service,
config=config,
aws_access_key_id=ACCESS_KEY,
Expand All @@ -1023,13 +1033,14 @@ def get_sts_token(service):
)
print("Running in member account deployment mode")
else:
boto3_client = boto3.client(service, config=config)
boto3_client = aws_api.client(service, config=config)
print("Running in management account deployment mode")

return boto3_client


def main(event, context):
aws_api.cache_clear()
print("THANK YOU FOR CHOOSING AWS HEALTH AWARE!")
health_client = get_sts_token("health")
org_status = os.environ["ORG_STATUS"]
Expand Down
Loading

0 comments on commit 928494c

Please sign in to comment.