先日のAWS障害を受けて、AWS Healthで障害情報が上がった場合に一次情報として自動通知できるようにしたいと上司から言われ、作ってみたものです。
####1.要件
・AWS Health上に新しく障害情報が掲載された際に、自動で通知を行う。(今回はとりあえずメール通知)
・その他のメンテナンス情報等は通知対象外
・あくまで一次的にAWS障害に気づくための用途であり、そもそも監視・モニタリングでAWS障害に起因するシステム異常を検知するのは別のお話
####2.構成
CloudWatchEventsでAWS Healthイベントを検知可能ですが、メンテナンス等の一部イベントしか検知できないため先日のAZ障害のようなケースでは役に立ちません。
そのため、定期的にLambdaでAWS HealthのAPIを叩く構成にしました。
####3.Lambdaの中身
AWSサービスで障害が発生した場合のイベントカテゴリは'Issue'、かつ対応中であるものはイベントステータスが'Open'なものなので、それらをフィルター指定してDescribe Events APIを実行しています。
対象が存在する場合にはそのイベントの開始時刻をチェックして、N分前~現在時刻の範囲(=直近のN分間で新たに発生したイベント)である場合のみ検知対象とします。(時刻判定をすることで、重複検知させない)
また、N分の部分は環境変数とすることで可変としています。
※該当時間内に発生したイベントを拾えれば良いので、ステータス判定はいらないかもしれません
import json
import boto3
import datetime
import dateutil.tz
import os
health = boto3.client('health')
sns = boto3.client('sns')
def json_serial(obj):
return obj.isoformat()
def lambda_handler(event, context):
aws_health_events = list(health.describe_events(
filter={
'eventTypeCategories':[
'issue'
],
'eventStatusCodes':[
'open'
]
}
)['events'])
open_issue_list = []
for aws_health_event in aws_health_events:
start_time = aws_health_event['startTime']
current_check_time = datetime.datetime.now(dateutil.tz.tzlocal())
pre_check_time = current_check_time - datetime.timedelta(minutes=int(os.environ['CHECK_CYCLE']))
if pre_check_time < start_time <= current_check_time:
open_issue_list.append(aws_health_event)
if len(open_issue_list) == 0:
return 'AWS Health Check is OK.'
else:
sns.publish(
TopicArn=os.environ['SNS_TOPIC_ARN'],
Subject='AWS Health check Alert!',
Message=json.dumps(open_issue_list, default=json_serial)
)
return 'New issue detected.'
####4.CFnテンプレート作成
社内各部への展開もできるようにテンプレート化しておきたかったため、各リソース作成も併せてCFnテンプレートにしました。
・とりあえずLambdaのコード部分は直書きですが、実際に使う場合はS3からの取得に直す予定です
・AWS Healthをチェックさせるサイクル(分)と通知先のメールアドレスは入力パラメータにしています
・入力値のサイクル(分)はLambdaの環境変数にも埋め込み、PGM内の判定ロジックに使用しています
AWSTemplateFormatVersion: '2010-09-09'
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
NotificationEmailAddress:
Type: String
CheckCycle:
Type: String
Default: 60
AllowedPattern: "[0-9]*"
Description: 'Input Check Cycle. (minutes)'
# ------------------------------------------------------------#
# Resource
# ------------------------------------------------------------#
Resources:
AWSHealthCheckRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
RoleName: "aws-health-check-role"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
AWSHealthCheckPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: "aws-health-check-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: Allow
Action:
- "health:DescribeEvents"
Resource:
- "*"
-
Effect: Allow
Action:
- "sns:Publish"
Resource:
- !Ref AWSHealthAlertTopic
Roles:
- !Ref AWSHealthCheckRole
AWSHealthCheckFunction:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: !Sub |
import json
import boto3
import datetime
import dateutil.tz
import os
health = boto3.client('health')
sns = boto3.client('sns')
def json_serial(obj):
return obj.isoformat()
def lambda_handler(event, context):
aws_health_events = list(health.describe_events(
filter={
'eventTypeCategories':[
'issue'
],
'eventStatusCodes':[
'open'
]
}
)['events'])
open_issue_list = []
for aws_health_event in aws_health_events:
start_time = aws_health_event['startTime']
current_check_time = datetime.datetime.now(dateutil.tz.tzlocal())
pre_check_time = current_check_time - datetime.timedelta(minutes=int(os.environ['CHECK_CYCLE']))
if pre_check_time < start_time <= current_check_time:
open_issue_list.append(aws_health_event)
if len(open_issue_list) == 0:
return 'AWS Health Check is OK.'
else:
sns.publish(
TopicArn=os.environ['SNS_TOPIC_ARN'],
Subject='AWS Health check Alert!',
Message=json.dumps(open_issue_list, default=json_serial)
)
return 'New issue detected.'
Description: "Lambda Function for AWS Health Check"
FunctionName: AWS_Health_Check
Handler: index.lambda_handler
MemorySize: 128
Role: !GetAtt AWSHealthCheckRole.Arn
Runtime: python3.7
Timeout: 30
Environment:
Variables:
TZ: Asia/Tokyo
SNS_TOPIC_ARN: !Ref AWSHealthAlertTopic
CHECK_CYCLE: !Ref CheckCycle
AWSHealthCheckFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: "/aws/lambda/AWS_Health_Check"
RetentionInDays: 7
AWSHealthCheckEventRule:
Type: AWS::Events::Rule
Properties:
Description: "time scheduled event for aws health check"
Name: "AWS_Health_Check-event"
ScheduleExpression: {"Fn::Join": [ "" , ["rate(", !Ref CheckCycle, " minutes)"]]}
State: ENABLED
Targets:
- Arn: !GetAtt AWSHealthCheckFunction.Arn
Id: "AWS_Health_Check-event"
AWSHealthCheckEventPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref AWSHealthCheckFunction
Principal: "events.amazonaws.com"
SourceArn: !GetAtt AWSHealthCheckEventRule.Arn
AWSHealthAlertTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: 'aws-health-alert-topic'
TopicName: 'aws-health-alert-topic'
AWSHealthAlertTopicSubscription:
Type: AWS::SNS::Subscription
Properties:
Endpoint: !Ref NotificationEmailAddress
Protocol: email
Region: !Ref "AWS::Region"
TopicArn: !Ref AWSHealthAlertTopic
####5.その他
・AWS HealthのAPIはバージニア北部リージョンからしか実行できないため、CFnスタック作成もバージニア北部で実行要です
・社内NWでSlackが使えないためメール通知にしていますが、ここはSlack通知にしたい...
・発生の検知だけでなくcloseの検知もできるようにしようと思っています(検知状況のテキストファイルをS3に配置して、毎回差分を確認するイメージ)