構成図
概要
Configでサポートされているリソースに限り、削除漏れのリソースを通知します。
リソースが作成されたらDynamoDBにアイテムが保存され、削除したらアイテムも削除されます。
指定された通知日時までに削除されなかったリソースのみ、Slackに通知される仕組みです。
通知間隔は1日で設定していますが、EventBridgeのスケジュールルールで起動しているので変更可能。
事前準備
SlackのWebhook URLの取得
こちらの記事が分かりやすかったです。
SlackのWebhook URLをSSM Parameter Storeに格納
コンソールからでいいので作成しておきます。
パラメータ名をSAMでデプロイする時に渡します。
SAMテンプレート
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: resources-notification
Parameters:
ParameterName:
Type: String
Description: SSM parameter name whose value is the Slack Web Hook URL.
TableName:
Type: String
Description: DynamoDB table name.
Globals:
Function:
Runtime: python3.9
Handler: index.lambda_handler
Environment:
Variables:
TABLE_NAME: !Ref TableName
Resources:
NewResourcesStore:
Type: AWS::Serverless::SimpleTable
Properties:
TableName: !Ref TableName
PrimaryKey:
Name: resourceId
Type: String
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
NewResourceRecordFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: new-resource-record-dynamodb-v2
CodeUri: functions/new-resource-record-dynamodb
Policies:
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: dynamodb:PutItem
Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}
Events:
ResourceDescoverd:
Type: EventBridgeRule
Properties:
Pattern:
source:
- aws.config
detail-type:
- 'Config Configuration Item Change'
detail:
messageType:
- ConfigurationItemChangeNotification
configurationItem:
configurationItemStatus:
- ResourceDiscovered
NewResourceDeleteFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: new-resource-delete-dynamodb-v2
CodeUri: functions/new-resource-delete-dynamodb
Policies:
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: dynamodb:DeleteItem
Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}
Events:
ResourceDeleted:
Type: EventBridgeRule
Properties:
Pattern:
source:
- aws.config
detail-type:
- 'Config Configuration Item Change'
detail:
messageType:
- ConfigurationItemChangeNotification
configurationItem:
configurationItemStatus:
- ResourceDeleted
ResourceNotificationStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/resources_notification.asl.json
DefinitionSubstitutions:
NewResourceNotificationFuncationArn: !GetAtt NewResourceNotificaitonFunction.Arn
ScanResourcesDeleteFunctionArn: !GetAtt ScanResourcesDeleteFunction.Arn
Events:
Schedule:
Type: Schedule
Properties:
Description: Schedule to run the resource notification state machine every
Schedule: "cron(0 15 * * ? *)"
Policies:
- LambdaInvokePolicy:
FunctionName: !Ref NewResourceNotificaitonFunction
- LambdaInvokePolicy:
FunctionName: !Ref ScanResourcesDeleteFunction
NewResourceNotificaitonFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: new-resources-notification-v2
CodeUri: functions/new-resources-notification
Environment:
Variables:
SLACK_WEBHOOK_URL: !Ref ParameterName
Policies:
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: dynamodb:Scan
Resource:
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}/index/*
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: ssm:GetParameter
Resource:
- !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ParameterName}
ScanResourcesDeleteFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: scan-resources-delete-dynamodb-v2
CodeUri: functions/scan-resources-delete-dynamodb
Policies:
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: dynamodb:BatchWriteItem
Resource:
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}
Outputs:
ResourceNotificationStateMachineArn:
Value: !Ref ResourceNotificationStateMachine
NewResourceRecordFunctionName:
Value: !Ref NewResourceRecordFunction
NewResourceDeleteFunctionName:
Value: !Ref NewResourceDeleteFunction
NewResourceNotificaitonFunctionName:
Value: !Ref NewResourceNotificaitonFunction
ScanResourcesDeleteFunctionName:
Value: !Ref ScanResourcesDeleteFunction
ステートマシン定義
{
"Comment": "resource notification(config)",
"StartAt": "Notification",
"States": {
"Notification": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"Payload.$": "$",
"FunctionName": "${NewResourceNotificationFuncationArn}"
},
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException"
],
"IntervalSeconds": 2,
"MaxAttempts": 6,
"BackoffRate": 2
}
],
"Next": "Choice"
},
"Choice": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.Payload",
"IsNull": true,
"Next": "Pass"
}
],
"Default": "DynamoDB Delete"
},
"Pass": {
"Type": "Pass",
"End": true
},
"DynamoDB Delete": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"OutputPath": "$.Payload",
"Parameters": {
"Payload.$": "$",
"FunctionName": "${ScanResourcesDeleteFunctionArn}"
},
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException"
],
"IntervalSeconds": 2,
"MaxAttempts": 6,
"BackoffRate": 2
}
],
"End": true
}
}
}
コード
リソース作成時にDynamoDBに記録する。
import os
import logging
import datetime
import boto3
from botocore.exceptions import ClientError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
client = boto3.client('dynamodb')
table_name = os.environ.get('TABLE_NAME')
def lambda_handler(event, context):
logger.info(event)
configurationItem = event['detail']['configurationItem']
arn = event['resources'][0]
capture_time = configurationItem['configurationItemCaptureTime']
resource_type = configurationItem['resourceType']
resource_id = configurationItem['resourceId']
if 'resourceName' in configurationItem.keys():
resource_name = configurationItem['resourceName']
else:
resource_name = '-'
# capture_timeをdatetime.datetime型に変換
fmt_time = datetime.datetime.strptime(
capture_time,
'%Y-%m-%dT%H:%M:%S.%f%z'
)
# JST時間
jst_time = fmt_time + datetime.timedelta(hours=9)
# 不要部分を削除して文字列型に変換
day_time = jst_time.strftime('%Y-%m-%d %H:%M:%S.%f')
day, time = day_time.split()
logger.info(f'リソース {arn} が作成されたため、DynamoDB {table_name}に追加します')
try:
response = client.put_item(
TableName=table_name,
Item={
'resourceId': {'S': resource_id},
'resourceType': {'S': resource_type},
'resourceName': {'S': resource_name},
'arn': {'S': arn},
'day': {'S': day},
'time': {'S': time}
}
)
logger.info('DynamoDBに追加しました')
logger.info(response)
except ClientError as e:
logger.error(e)
リソース削除時にDynamoDBに記録したアイテムを削除する。
import os
import boto3
import logging
from botocore.exceptions import ClientError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
client = boto3.client('dynamodb')
table_name = os.environ.get('TABLE_NAME')
def lambda_handler(event, context):
logger.info(event)
resource_id = event['detail']['configurationItem']['resourceId']
arn = event['resources'][0]
logger.info(f'リソース {arn} が削除されたため、DynamoDB {table_name}からも削除を開始します')
try:
response = client.delete_item(
TableName=table_name,
Key={
'resourceId': {
'S': resource_id
}
},
ConditionExpression='resourceId = :id',
ExpressionAttributeValues={
':id': {
'S': resource_id
}
}
)
logger.info('DynamoDBから削除しました')
logger.info(response)
except client.exceptions.ConditionalCheckFailedException as e:
logger.error(
f'resourceId: {resource_id} がDynamoDB {table_name}に存在しません')
logger.error(e)
except ClientError as e:
logger.error('その他のエラーが発生しました')
logger.error(e)
DynamoDBに保存されているアイテムを全てSlackに通知する(定期実行)。
import json
import os
import logging
import re
import urllib.request
import boto3
from botocore.exceptions import ClientError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
dynamodb = boto3.client('dynamodb')
table_name = os.environ.get('TABLE_NAME')
ssm = boto3.client('ssm')
ssm_parameter_name = os.environ.get('SLACK_WEBHOOK_URL')
def lambda_handler(event, context):
item_list = scan(table_name)
if not item_list:
return
# Scanしたアイテムのリスト.次のLambdaで削除
delete_list = []
for item in item_list:
delete_list.append({'resourceId': item['resourceId']['S']})
message = create_message(item_list)
response = ssm.get_parameter(
Name=ssm_parameter_name,
WithDecryption=True
)
web_hook_url = response['Parameter']['Value']
post_slack(web_hook_url, message)
# 削除するLambdaに渡す
return delete_list
def scan(table_name):
try:
response = dynamodb.scan(
TableName=table_name,
Select='ALL_ATTRIBUTES'
)
logger.info(json.dumps(response))
if response['Count'] == 0:
return
return response['Items']
except ClientError as e:
logger.error(e)
return
def create_message(item_list):
message_list = []
for item in item_list:
day = item['day']['S']
resource_type = item['resourceType']['S']
pre_arn = item['arn']['S']
arn = re.sub('^.+:\d{12}:', '', pre_arn)
message_list.append(f'・ *{day}* *{resource_type}* \n{arn}\n')
sort_message_list = sorted(message_list)
sort_message_list.insert(0, ':bulb: *削除されていないリソース* :bulb:\n\n')
message = '\n'.join(sort_message_list)
return message
def post_slack(web_hook_url, message):
send_data = {
"username": "aws-new-resource-notify",
"text": message
}
send_text = 'payload=' + json.dumps(send_data)
request = urllib.request.Request(
web_hook_url,
data=send_text.encode('utf-8'),
method='POST'
)
with urllib.request.urlopen(request) as response:
response_body = response.read().decode('utf-8')
logger.info(response_body)
Slackに通知したアイテムをDynamoDBから削除する。
import os
import logging
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
table_name = os.environ.get('TABLE_NAME')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(table_name)
def lambda_handler(event, context):
logger.info(event)
# DynamoDBに該当のアイテムが存在しない状態で削除してもエラーにならない
try:
with table.batch_writer() as batch:
for key in event['Payload']:
logger.info(f'DynamoDBテーブル {table_name} に保存された resourceId:{key["resourceId"]} の削除を開始します')
batch.delete_item(Key=key)
logger.info(f'resourceId:{key["resourceId"]} の削除が完了しました')
except Exception as e:
logger.exception(e)
デプロイ方法
リソースや通知の内容など
DynamoDBのアイテム。
StepFunction。
Slackへの通知内容。
使用しての感想
削除漏れの把握以外で良かったのは、作成したつもりのなかったリソースも把握できたことです。
AWSでリソースを作成すると、そのリソースに必要なリソースなどが自動作成されることが多いと思います。
そのリソースも通知してくれるので、このリソースを作成するとこんなリソースも作成されるのかと勉強になりました。
ただ通知がConfigに依存しているため、Configがサポートしているリソースに限るのが残念な点です。
2週間ほど使用していますが、料金は無料の範囲内で使用できています(大量リソースの作成・削除は行っていない、かつ私用アカウントでの場合)。