過去に類似のテーマで、CloudTrailによるイベント監視 + 通知メールカスタマイズをしてみた。今回はイベントソースをAWS Configにしてみる。

(10月頃から金太郎飴のように類似の検証を重ね重ねやっていて脳内パズル状態甚だしいが、やらないことには整理ができない…)

AWSイベント監視 - CloudTrail + EventBridge + Lambdaでメールカスタマイズ(2)

 

EventBridgeのルールで変更を検知したいが、ConfigとEventBridgeのルールはそれぞれ独立している様子で、仕組みが今いち不明。基本的にConfigで設定するルールはコンプライアンスに沿っているかどうかをチェックするためのものであり、変更を検知する目的とは意味合いが違うみたいだ。これまでその辺もよくわかっていなかった。実を言うと今もよくわかってはいないが、何はともあれConfigを用意して試す。Configのルールはマネージドのルールから「ec2-instance-profile-attached」をつけておいた。S3バケット、IAMロールは自動で生成させた。

 

今回作成したリソース名称。

アイテム 名称
SNSトピック custom-event-notification
Lambda用IAMロール custom-event-mail-role
Lambda関数 config-event-function
eventルール config-change-notify-rule

 

eventルール(EventBridge)は、当初イベント内容を絞って試したが検知されなかったのでとりあえずAnyにした。

EventBridgeルール

{
  "source": ["aws.config"],
  "detail-type": ["Config Configuration Item Change"]
}

 

イベント内容を確認するため、最初は以下のLambdaコードでメールを飛ばしてみる。SNSトピックは環境変数で指定。

import json   
import boto3
import os

print('Loading function')

sns_arn = os.environ['SNS_TOPIC_ARN']

def lambda_handler(event, context):   
        type = event['detail-type']  
        Msg = json.dumps(event)
        sub = '[AWS Config]' + str(type)

        client = boto3.client('sns')
        response = client.publish(
            TopicArn = sns_arn,
            Message = Msg,
            MessageStructure = 'context',
            Subject = sub
        )  
        return

 

EC2インスタンスにつけたIAMロールをデタッチしてイベントルールが検知すると、JSONデータが丸ごと送信された。これをJSONファイルとして保存し、解析する。

>>> event_file = open('config_sample.json','r')
>>> type(event_file)
<class '_io.TextIOWrapper'>
>>> event = json.load(event_file)
>>> msg = json.dumps(event, indent=3)  #JSONを見やすく整形する
>>> print(msg)
{
   "version": "0",
   "id": "07cedb49-ed89-6b1f-3ef7-b02fch9ae318",
   "detail-type": "Config Configuration Item Change",
   "source": "aws.config",
   "account": "012345678910",
   "time": "2021-12-26T04:36:35Z",
   "region": "ap-northeast-1",
   "resources": [
      "arn:aws:ec2:ap-northeast-1:012345678910:instance/i-xxxxxxxxxxxxxxxxx"
   ],
   "detail": {
      "recordVersion": "1.3",
      "messageType": "ConfigurationItemChangeNotification",
      "configurationItemDiff": {
         "changedProperties": {
            "Configuration.IamInstanceProfile": {
               "updatedValue": {
                  "arn": "arn:aws:iam::012345678910:instance-profile/test-instance-role",
                  "id": "AIPBIC67ZVZTAHPSHTWDM"
               },
               "changeType": "CREATE"
            }
         },
         "changeType": "UPDATE"
},
(snip)

 

ちなみに実際に取得したevent生データは以下。(一部マスク実施)

{'version': '0', 'id': '03696e62-00e3-a8c1-3d1d-854a5153b93e', 'detail-type': 'Config Configuration Item Change', 'source': 'aws.config', 'account': '012345678901', 'time': '2021-12-26T04:12:26Z', 'region': 'ap-northeast-1', 'resources': ['arn:aws:ec2:ap-northeast-1:012345678901:instance/i-xxxxxxxxxxxxxxxxx'], 'detail': {'recordVersion': '1.3', 'messageType': 'ConfigurationItemChangeNotification', 'configurationItemDiff': {'changedProperties': {'Configuration.IamInstanceProfile': {'previousValue': {'arn': 'arn:aws:iam::012345678901:instance-profile/system-role', 'id': 'AIPAQNBL2HJCF36XERVQR'}, 'changeType': 'DELETE'}}, 'changeType': 'UPDATE'}, 'notificationCreationTime': '2021-12-26T04:12:26.909Z', 'configurationItem': {'relatedEvents': [], 'relationships': [{'resourceId': 'eni-6v292a2739eb1c95h', 'resourceType': 'AWS::EC2::NetworkInterface', 'name': 'Contains NetworkInterface'}, {'resourceId': 'sg-02a83b08a8bw1379f', 'resourceType': 'AWS::EC2::SecurityGroup', 'name': 'Is associated with SecurityGroup'}, {'resourceId': 'subnet-0bw1xc364270607cb', 'resourceType': 'AWS::EC2::Subnet', 'name': 'Is contained in Subnet'}, {'resourceId': 'vol-027326eaebf1903f2', 'resourceType': 'AWS::EC2::Volume', 'name': 'Is attached to Volume'}, {'resourceId': 'vpc-1247b576cb69f93d3', 'resourceType': 'AWS::EC2::VPC', 'name': 'Is contained in Vpc'}], 'configuration': {'amiLaunchIndex': 0.0, 'imageId': 'ami-032s715c7f0f18cue', 'instanceId': 'i-xxxxxxxxxxxxxxxxx', 'instanceType': 't2.micro', 'keyName': 'My.pem', 'launchTime': '2021-12-19T02:43:42.000Z', 'monitoring': {'state': 'disabled'}, 'placement': {'availabilityZone': 'ap-northeast-1a', 'groupName': '', 'tenancy': 'default'}, 'privateDnsName': 'ip-10-0-0-80.ap-northeast-1.compute.internal', 'privateIpAddress': '10.0.0.80', 'productCodes': [], 'publicDnsName': '', 'state': {'code': 80.0, 'name': 'stopped'}, 'stateTransitionReason': 'User initiated (2021-12-19 14:36:10 GMT)', 'subnetId': 'subnet-0bw1xc364270607cb', 'vpcId': 'vpc-1247b576cb69f93d3', 'architecture': 'x86_64', 'blockDeviceMappings': [{'deviceName': '/dev/xvda', 'ebs': {'attachTime': '2020-08-22T09:55:48.000Z', 'deleteOnTermination': True, 'status': 'attached', 'volumeId': 'vol-027326eaebf1903f2'}}], 'clientToken': '', 'ebsOptimized': False, 'enaSupport': True, 'hypervisor': 'xen', 'elasticGpuAssociations': [], 'elasticInferenceAcceleratorAssociations': [], 'networkInterfaces': [{'attachment': {'attachTime': '2020-08-22T09:55:47.000Z', 'attachmentId': 'eni-attach-0d0b8282ec9760266', 'deleteOnTermination': True, 'deviceIndex': 0.0, 'status': 'attached', 'networkCardIndex': 0.0}, 'description': 'Primary network interface', 'groups': [{'groupName': 'dev-sg-mainte', 'groupId': 'sg-02a83b08a8bw1379f'}], 'ipv6Addresses': [], 'macAddress': '03:02:a0:fc:31:48', 'networkInterfaceId': 'eni-6v292a2739eb1c95h', 'ownerId': '012345678901', 'privateDnsName': 'ip-10-0-0-80.ap-northeast-1.compute.internal', 'privateIpAddress': '10.0.0.80', 'privateIpAddresses': [{'primary': True, 'privateDnsName': 'ip-10-0-0-80.ap-northeast-1.compute.internal', 'privateIpAddress': '10.0.0.80'}], 'sourceDestCheck': True, 'status': 'in-use', 'subnetId': 'subnet-0bw1xc364270607cb', 'vpcId': 'vpc-1247b576cb69f93d3', 'interfaceType': 'interface'}], 'rootDeviceName': '/dev/xvda', 'rootDeviceType': 'ebs', 'securityGroups': [{'groupName': 'dev-sg-mainte', 'groupId': 'sg-02a83b08a8bw1379f'}], 'sourceDestCheck': True, 'stateReason': {'code': 'Client.UserInitiatedShutdown', 'message': 'Client.UserInitiatedShutdown: User initiated shutdown'}, 'tags': [{'key': 'Name', 'value': 'terraform-kubectl'}], 'virtualizationType': 'hvm', 'cpuOptions': {'coreCount': 1.0, 'threadsPerCore': 1.0}, 'capacityReservationSpecification': {'capacityReservationPreference': 'open'}, 'hibernationOptions': {'configured': False}, 'licenses': [], 'metadataOptions': {'state': 'applied', 'httpTokens': 'optional', 'httpPutResponseHopLimit': 1.0, 'httpEndpoint': 'enabled'}, 'enclaveOptions': {'enabled': False}}, 'supplementaryConfiguration': {}, 'tags': {'Name': 'terraform-kubectl'}, 'configurationItemVersion': '1.3', 'configurationItemCaptureTime': '2021-12-26T04:12:25.750Z', 'configurationStateId': 1640491945750.0, 'awsAccountId': '012345678901', 'configurationItemStatus': 'OK', 'resourceType': 'AWS::EC2::Instance', 'resourceId': 'i-xxxxxxxxxxxxxxxxx', 'ARN': 'arn:aws:ec2:ap-northeast-1:012345678901:instance/i-xxxxxxxxxxxxxxxxx', 'awsRegion': 'ap-northeast-1', 'availabilityZone': 'ap-northeast-1a', 'configurationStateMd5Hash': '', 'resourceCreationTime': '2021-12-19T02:43:42.000Z'}}}

 

これで大分見やすくなるが、階層が深いためさらに掘りたい場合は以下のようにkey:valueを抽出する。

>>> dtl = event['detail']
>>> for k,v in dtl.items():
>>>    print(k,v)

 

その結果、以下のように項目を抽出して表示させることにした。コード内のコメント「# さらに通知内容の項目を抽出」以降のあたり。生データだと余計な情報がめっちゃあるけど、通知に必要な情報だけ送信できればいい。

Lambdaコード(カスタマイズ版)

import boto3
import json
import os
from botocore.exceptions import ClientError
from datetime import datetime, timezone, timedelta
from dateutil import parser

print('Loading function')

sns_arn = os.environ['SNS_TOPIC_ARN']

def lambda_handler(event, context):
    data = event
    s = json.dumps(data)
    e = json.loads(s)
    print(e)
    
    # eventから詳細項目を抽出
    dtl = e['detail']
    
    # さらに通知内容の項目を抽出
    t = e['time']                                       # 発生時刻
    rce_type = dtl['configurationItem']['resourceType'] # リソースタイプ
    rce_arn = str(e['resources'])                       # リソースARN
    diff = str(dtl['configurationItemDiff'])            # 変更内容

    # 時刻変換前処理
    JST = timezone(timedelta(hours=+9), 'JST')
    utcstr_parsed = parser.parse(t)
    ux_time = utcstr_parsed.timestamp()
    epoch = int(ux_time)

    # unixタイムスタンプをJSTに変換
    dt = datetime.fromtimestamp(epoch).replace(tzinfo=timezone.utc).astimezone(tz=JST)

    # dtを整形
    dt_str = dt.strftime('%Y-%m-%d %H:%M:%S')

    # 件名整形
    subject_str = "検証環境 Config変更通知 - " + rce_type

    # メッセージ本文整形
    fix_msg = "AWS Configで変更を検知しました" + "\n"

    time_msg = "発生時刻(JST):" + "\n" + dt_str
    type_msg = "リソースタイプ:" + "\n" + rce_type
    arn_msg = "リソースARN:" "\n" + rce_arn
    diff_msg ="変更内容:" "\n" + diff

    msg = fix_msg + "\n\n" + time_msg + "\n\n" + type_msg + "\n\n" + arn_msg + "\n\n" + diff_msg + "\n\n"

    try:
        sns = boto3.client('sns')
        
        #SNS Publish
        publishResponse = sns.publish(
            TopicArn = os.environ['SNS_TOPIC_ARN'],
            Message = msg,
            Subject = subject_str
        )
    
    except Exception as e:
        print(e)

 

変更を加えても通知が来なくて「おかしいなー」と言いつつ、ゴニョゴニョやっているうちにカスタマイズ版の通知メールが届いた。EC2以外の変更でも通用するかわからんが、一応その前提で作ってはいる。ダメだったらまた考えよう。(イベント検知のためインスタンスのIAMロールの「デタッチ/アタッチ」を何度も繰り返してたら、変なエラーメッセージが出るようになった。スパム行為と見做されてしまったかもしれない)

変更通知メール

 

追記
Configの「変更内容」にあたるJSONは、発生したイベントによってはさらにボリュームが増して内容がわかりにくくなる。メールでより内容を把握しやすくするのであれば、このJSONもインデントして表示させるとよい。

diff_tmp = dtl['configurationItemDiff']   #この時点では辞書型
diff = json.dumps(diff_tmp, indent=3)     #この時点でStringになる

 

このようにすると、上記サンプルメールの「変更内容」以下のメッセージがインデントされて見やすくなる。

 

以下参考記事。Configの監視はLambdaと直接連携するケースもあり、その場合eventの中身も変わってくるようだ。当然Lambdaコードの内容も変わるので注意。以下のなかでは、最初の記事がEventBridgeと連携する例となる。

参考
AWS Configの通知内容をLambdaで整形
【AWS config】設定変更時のみ独自の形式で通知を送る
AWS Config Rules カスタムルールを作成してみよう

 

追記(2024年6月)
タイムゾーンの変換処理は、Python3.9以降はZoneinfoモジュールを利用するとよい。
Lambda(Python3.9)でzoneinfoを使ってタイムゾーン設定するとかんたん

 

Airplane Airplane


関連がありそうな記事