AWS ResourceExplorerを使って前日のリソース増減をTeamsに投稿したい

# はじめに
　[AWS Resource Explorer](https://aws.amazon.com/jp/blogs/news/introducing-aws-resource-explorer-quickly-find-resources-in-your-aws-account/)をご存じでしょうか。2022年12月に発表されたサービスで、複数のリージョンをまたいで、アカウントのAWSリソースを検索できるようなサービスです。  
　今回は、このResource Explorerを使って、昨日増減したAWSのリソースをTeamsに通知するような仕組みを作るために考えたことや、（結局微妙な点が多くあまり活用できないかもしれないという結論になったのですが)現在残っている課題等を共有させていただきます。

# やりたかったこと
　今回やりたいことはシンプルで、毎日、昨日から今日にかけて作成あるいは削除されたリソースをTeamsに投稿できるようにすることです。
![yaritakatta.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/cb4712ef-21dc-a720-b68f-98cb11a00043.png)
　Teams等のコミュニケーションツールだと毎日触ることが多いですし、もしなにか意図しないリソースが作成されていた場合に、違和感を抱くことのできるきっかけを増やすことが目的です。
　AWS Configを利用しても同様の仕組みはできそうですが、Configをすべてのリージョンに展開して、一つのリージョンに情報を集約するようにするには結構構築が面倒なのと、費用も掛かります。そこまでする必要はないんだけど何となく環境のことは把握しておきたいなというニーズを満足できるような（費用的に）軽いツールをめざしました。

# 解決のために考えたアプローチ
## 考えたアーキテクチャ
　ResourceExplorerは、[resource-explorer-2](https://docs.aws.amazon.com/cli/latest/reference/resource-explorer-2/index.html)の[list-supportted-resource-types](https://docs.aws.amazon.com/cli/latest/reference/resource-explorer-2/list-supported-resource-types.html)というAPIを利用して、サポートされているリソース種別の一覧をリストで取ってくることができるみたいです。これを使ってまずはサポートされているリソース種別の一覧をlambdaで取得して、リソース種別ごとに現在の状態を取得してくるlambdaを並列で起動させることにしました。  
　取得してきた情報はDynamoDBに格納して、新しく追加されたものかどうかわかるようにupdateフラグのようなものを一緒にデータとして持つようにします。  
　最後に、DynamoDBの情報から新しく追加されたものと削除されたものを判別してTeamsに投稿したり、リソースの一覧をcsvファイルにしてS3バケットに配置するLambdaを起動させます。
![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/ca89f002-f31e-ecb6-a077-467adda6b029.png)
　次から各リソースの設定内容について紹介していきます。

## StepFunctions
### ステートマシン定義
　ステートマシンの定義はシンプルで、大きく3つの部分に分かれます。すべて、Lambdaを呼び出すステップです。
① GetResourceType  
　resource-explorer-2 APIでサポートされているリソースタイプの一覧を取得するステップです。  
　リストを次のステップに渡します。  
② GetResourceInfoMap  
　resource-explorer-2 APIでリソースタイプごとに検索を行い、結果をdynamoDBに書き込むステップです。  
③ CreateResourceList  
　このステップはいくつか役割があります。
　・ dynamoDBの情報から、削除されたリソースと追加されたリソースの情報を取得し、Teamsの投稿メッセージを作成
　・ すべてのリソースの一覧リストをcsvファイルで作成し、S3バケットに格納
　・ S3オブジェクトの署名付きURLと、削除、追加されたリソースの情報をTeamsに投稿
これら3つのステップを含むステートマシンの定義は、下記の通りになりました。

```json: ステートマシンの定義
{
    "Comment": "DeployedResourceNotifier",
    "StartAt": "GetResourceType",
    "States": {
        "GetResourceType": {
            "Type": "Task",
            "Resource": "${GetResourceTypeFunctionArn}",
            "Next": "GetResourceInfoMap"
        },
        "GetResourceInfoMap": {
            "Type": "Map",
            "Iterator": {
                "StartAt": "GetResourceInfo",
                "States": {
                    "GetResourceInfo": {
                        "Type": "Task",
                        "Resource": "${GetResourceInfoFunctionArn}",
                        "End": true,
                        "Retry": [
                            {
                                "ErrorEquals": [ "States.ALL" ],
                                "IntervalSeconds": 10,
                                "MaxAttempts": 3,
                                "BackoffRate": 3
                            }
                        ]
                    }
                }
            },
        "Next": "CreateResourceList"
        },
        "CreateResourceList": {
            "Type": "Task",
            "Resource": "${CreateResourceListFunctionArn}",
            "End": true
        }
    }
}
```

### cdkテンプレート
　StepFunctionのCDKテンプレートは下記のようになります。
　先ほどのステートマシン定義のjsonをreadFileSyncで読み込むようにしています。

```typescript:StepFunction部分
    // Step Functions
    const stateMachineFile = fs.readFileSync('./statemachine/DeployedResourceNotifier.asl.json');

    const stateMachineRole = new iam.Role(this, `${pjName}-StateMachineRole`, {
      roleName: `${pjName}-StateMachineRole`,
      assumedBy: new iam.ServicePrincipal('states.amazonaws.com'),
      description: `${pjName}-StateMachineRole`,
      path: '/',
      managedPolicies: [
        awsLambdaRole
      ]
    });

    const statemachine = new sfn.CfnStateMachine(this, `${pjName}-StateMachine`, {
      definitionString: stateMachineFile.toString(),
      stateMachineName: `${pjName}-StateMachine`,
      definitionSubstitutions: {
        GetResourceTypeFunctionArn: getResourceTypeFunc.functionArn,
        GetResourceInfoFunctionArn: getResourceInfoFunc.functionArn,
        CreateResourceListFunctionArn: createResourceListFunc.functionArn
      },
      roleArn: stateMachineRole.roleArn
    });
```

## リソース情報を保持するDynamoDB
### アイテムのアトリビュート
　DynamoDBには、1レコードに下記情報を持つような設計としました。

- arn
- name
- region
- resourcetype
- service
- updateflag
- updatetime

　下記はデータのイメージの表です。

|arn|name|region|resourcetype|service|updateflag|updatetime|
|---|---|---|---|---|---|---|
|arn:aws:ec2:ap-northeast-1:123456789012:subnet/subnet-xxxxxxxxxxxx|hogehoge|ap-northeast-1|ec2:subnet|ec2|1|2023-03-02T00:01:15.142099|
|arn:aws:ec2:ap-northeast-1:123456789012:key-pair/xxxxxxxxx|NoNameTag|ap-northeast-1|ec2:key-pair|ec2|2|2023-03-02T00:01:15.168216|

　arn, name, region, resourcetype,serviceに関しては、resource-explorer-2 APIで取得可能な情報をそのまま格納しています。

### 更新フラグ
　updateFlagについては、0,1,2の3パターンの状態を持つようにして、新しく追加されたリソースの判別や、削除されたリソースの判別ができるようにしました。前章では①～③まで3つのLambdaが登場しましたが、その中の②、③では、このUpdateFlagの更新処理も実施するようにしています。

　②のLambda関数では、下記ルールに従ってupdateFlagを処理します。  
- 追加しようとしたリソースがdynamoDB上に存在しなかった場合
　⇒ updateFlagを1にしてitemを新規追加
- 追加しようとしたリソースがdynamoDB上に存在し、updateFlagが2だった場合
　⇒ updateFlagを0に更新
- 追加しようとしたリソースがdynamoDB上に存在し、updateFlagが2以外だった場合
　⇒ 何もしない

　③のLambda関数では、下記ルールに従ってupdateFlagを処理します。
- リソースのupdateFlagが0だった場合
　⇒ updateFlagを2に更新
- リソースのupdateFlagが1だった場合
　⇒ updateFlagを2に更新した後、該当のリソースを新規追加リソースのリストに加える
- リソースのupdateFlagが2だった場合
　⇒ 該当のリソースをdynamoDBから削除し、削除済リソースのリストに加える

私が検討していた時のupdateFlagの状態遷移のイメージがあったので、参考までに掲載します。（矢印の近くにある文字が小さくて見えづらいですが、②および③の関数名なので特に気にしなくても大丈夫です。）
![stateflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/dfbc957e-e8e0-3f37-7cb9-b57fc66889e4.png)

### GSI
　更新フラグに関しては、更新フラグを利用した検索を行う可能性があったので、Global Secondary Indexを作成しました。

### cdkテンプレート
　dynamoDBに関するCDKのテンプレートは下記のようになります。
```typescript: DynamoDB部分
    // DynamoDB table
    const indexName = "updateflag-index";
    const dynamoTable = new dynamodb.Table(this, `${pjName}-ResourcesTable`, {
      partitionKey: {
        name: 'arn',
        type: dynamodb.AttributeType.STRING
      },
      tableName: 'DeployedResourcesTable',
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });

    dynamoTable.addGlobalSecondaryIndex({
      indexName,
      partitionKey: {
        name: 'updateflag',
        type: dynamodb.AttributeType.STRING
      }
    });

```

## GetResourceType Lambda関数①
### Lambda関数の内容
　リソース一覧を取得するLambda関数は、boto3でapiを叩くだけです。リソースタイプがリストで返ってくるので、それをresponseにしています。

```python: get_resource_type
import boto3

resource_explorer = boto3.client('resource-explorer-2')

def get_resource_type():
    response = resource_explorer.list_supported_resource_types()

    return response

def lambda_handler(event, context):
    response = get_resource_type()

    return response["ResourceTypes"]
```

resource-explorer-2.list_supported_resource_types()で取得できる情報は、下記のような形になります。（実際には140個ほどのリストになります。)

```json
[
  {
    "ResourceType": "cloudfront:cache-policy",
    "Service": "cloudfront"
  },
  {
    "ResourceType": "cloudfront:distribution",
    "Service": "cloudfront"
  },
  {
    "ResourceType": "cloudfront:function",
    "Service": "cloudfront"
  }
]
```

### cdkテンプレート
　リソース一覧を取得するLambda関数のCDKテンプレートは下記のようになります。
```typescript
    // Lambda 関数 Get Resource Type
    const getResourceTypeFuncRole = new iam.Role(this, `${pjName}-GetResourceTypeFuncRole`, {
      roleName: `${pjName}-GetResourceTypeFuncRole`,
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: `${pjName}-GetResourceTypeFuncRole`,
      path: '/',
      managedPolicies: [
        awsLambdaBasicExecutionRole,
        awsResourceExplorerReadOnlyAccess
      ]
    });

    const getResourceTypeFunc = new lambda.Function(this, `${pjName}-GetResourceTypeFunc`, {
      functionName: `${pjName}-GetResourceTypeFunc`,
      runtime: lambda.Runtime.PYTHON_3_9,
      handler: 'app.lambda_handler',
      code: lambda.Code.fromAsset('lambda/get_resource_type'),
      tracing: lambda.Tracing.ACTIVE,
      role: getResourceTypeFuncRole,
      layers: [
        boto3layer
      ]
    });
```

## GetResourceInfoMap Lambda関数②
### Lambda関数の内容
　二つ目のLambda関数は、①のLambda関数で取得したリストの個数分並列起動します。
　下記がソースコードです。
```python
import boto3
from boto3.dynamodb.conditions import Key
import os

DYNAMO_TABLE_NAME = os.environ['DYNAMO_TABLE_NAME']
REGION_NAME = os.environ['REGION_NAME']

resource_explorer = boto3.client('resource-explorer-2')
dynamodb = boto3.resource('dynamodb', region_name=REGION_NAME)

def check_item(arn, table):
    options = {
        'KeyConditionExpression': Key('arn').eq(arn),
    }
    res = table.query(**options)
    check_result = ""
    if res["Count"] == 0:
        check_result = 0
    elif res["Items"][0]["updateflag"] == "2":
        check_result = 2
    else:
        check_result = 1

    return check_result

def update_table(arn, name, region, resource_type, service):
    table = dynamodb.Table(DYNAMO_TABLE_NAME)

    insert_item = {
        'arn': arn,
        'name': name,
        'region': region,
        'resourcetype': resource_type,
        'service': service,
        'updateflag': "1"
    }

    check_result = check_item(arn, table)

    if check_result == 0:
        response = table.put_item(Item=insert_item)

    elif check_result == 2:
        response = table.update_item(
            Key={'arn': arn},
            UpdateExpression="set updateflag = :updateflag",
            ExpressionAttributeValues={
                ':updateflag': '0'
            },
            ReturnValues="UPDATED_NEW")
    else:
        pass

    return check_result

def lambda_handler(event, context):
    resource_type = event["ResourceType"]
    service = event["Service"]

    response = resource_explorer.search(
        QueryString = 'resourcetype:' + resource_type + ' service:' + service
    )
    resources = response["Resources"]

    new_resource_count = 0
    existing_resource_count = 0

    if len(response["Resources"]) == 0:
        response = ""
    else:
        for resource in resources:
            arn = resource["Arn"]
            region = resource["Region"]
            resource_type = resource["ResourceType"]
            service = resource["Service"]
            if len(resource["Properties"]) == 0:
                name = "NoNameTag"
            else:
                data_list = resource["Properties"][0]["Data"]
                name_tag = next((x for x in data_list if x["Key"] == "Name"), None)
                if name_tag == None:
                    name = "NoNameTag"
                else:
                    name = name_tag["Value"]
            check_result = update_table(arn, name, region, resource_type, service)
            if check_result == 0:
                new_resource_count = new_resource_count + 1
            elif check_result == 2:
                existing_resource_count = existing_resource_count + 1
            else:
                pass

        response = str(len(resources)) + " Resources Detected.    " + str(new_resource_count) + " New Resources Inserted.    " + str(existing_resource_count) + " Existing Resources."

    return response

```
　resource-explorer-2 の search APIを利用して、現在AWSアカウント上にデプロイされているリソースを取得した後、dynamoDBに対象のリソースが存在するかどうかを確認。存在しない場合は新規追加、存在した場合はupdateFlagの更新を行っています。 

### cdkテンプレート
この関数をデプロイするためのcdkコードは下記の通りです。

```typescript
    // Lambda 関数 Get Resource Info
    const getResourceInfoFuncRole = new iam.Role(this, `${pjName}-GetResourceInfoFuncRole`, {
      roleName: `${pjName}-GetResourceInfoFuncRole`,
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: `${pjName}-GetResourceInfoFuncRole`,
      path: '/',
      managedPolicies: [
        awsLambdaBasicExecutionRole,
        awsResourceExplorerReadOnlyAccess,
        amazonDynamoDBFullAccess
      ]
    });

    const getResourceInfoFunc = new lambda.Function(this, `${pjName}-GetResourceInfoFunc`, {
      functionName: `${pjName}-GetResourceInfoFunc`,
      runtime: lambda.Runtime.PYTHON_3_9,
      handler: 'app.lambda_handler',
      code: lambda.Code.fromAsset('lambda/get_resource_info'),
      tracing: lambda.Tracing.ACTIVE,
      timeout: Duration.seconds(30),
      role: getResourceInfoFuncRole,
      layers: [
        boto3layer
      ],
      environment: {
        DYNAMO_TABLE_NAME: dynamoTable.tableName,
        REGION_NAME: region
      }
    });
    dynamoTable.grantReadWriteData(getResourceInfoFunc);
```
## CreateResourceList Lambda関数③
### Lambda関数の内容
　前述した通り、この関数には役割がいくつかあります。
　・ dynamoDBの情報から、削除されたリソースと追加されたリソースの情報を取得し、Teamsの投稿メッセージを作成
　・ すべてのリソースの一覧リストをcsvファイルで作成し、S3バケットに格納
　・ S3オブジェクトの署名付きURLと、削除、追加されたリソースの情報をTeamsに投稿

　まずはコードを見てみましょう。

```python
import boto3
from botocore.client import Config
import requests
import json
import os
import datetime
import pandas as pd

DYNAMO_TABLE_NAME = os.environ['DYNAMO_TABLE_NAME']
DYNAMO_GSI_INDEX_NAME = os.environ['DYNAMO_GSI_INDEX_NAME']
REGION_NAME = os.environ['REGION_NAME']
TEAMS_WEBHOOK_URL = os.environ['TEAMS_WEBHOOK_URL']
S3_BUCKET_NAME = os.environ['S3_BUCKET_NAME']
POST_TEAMS_SWITCH = os.environ['POST_TEAMS_SWITCH']
DIFF_FROM_UTC = 9

resource_explorer = boto3.client('resource-explorer-2')
s3 = boto3.resource('s3')
dynamodb = boto3.resource('dynamodb', region_name=REGION_NAME)

def get_item(table):
    res = table.scan()
    item_list = res["Items"]

    return item_list

def select_item(update_flag, item_list):
    selected_item_list = []
    for item in item_list:
        if item['updateflag'] == update_flag:
            if 'updatetime' in item:
                if datetime.datetime.fromisoformat(item['updatetime']) < datetime.datetime.now() + datetime.timedelta(minutes=-3):
                    selected_item_list.append(item)
            else:
                selected_item_list.append(item)
        else:
            pass

    return selected_item_list

def create_facts(item_list, add_delete_flag):
    facts = []
    if add_delete_flag == 1:
        for item in item_list:
            resource_type = "**【" + item['resourcetype'] + "】** "
            region = "_" + item['region'] + "_ "
            arn_list = item['arn'].split('/')
            arn = arn_list[len(arn_list)-1]
            arn_list = arn.split(':::')
            arn = arn_list[len(arn_list)-1]
            name = "(" + item['name'] + ")"
            dict_tmp = {'name': '＋＋＋', 'value': resource_type + region + arn + name}
            facts.append(dict_tmp)
    else:
        for item in item_list:
            resource_type = "**【" + item['resourcetype'] + "】** "
            region = "_" + item['region'] + "_ "
            arn_list = item['arn'].split('/')
            arn = arn_list[len(arn_list)-1]
            arn_list = arn.split(':::')
            arn = arn_list[len(arn_list)-1]
            name = "(" + item['name'] + ")"
            dict_tmp = {'name': '－－－', 'value': resource_type + region + arn + name}
            facts.append(dict_tmp)
    facts_sorted_by_resourcetype = sorted(facts, key=lambda x:x['value'], reverse=True)

    return facts_sorted_by_resourcetype

def post_teams(title: str, sub_title: str, color: str, facts_sorted_by_resourcetype: str, s3_uri: str) -> None:
    if len(facts_sorted_by_resourcetype)==0:
        payload = {
            '@type': 'MessageCard',
            "@context": "http://schema.org/extensions",
            "themeColor": color,
            "title": title,
            "summary": title,
            "sections": [{
                "activityTitle": sub_title,
                "markdown": 'true',
                "text": "対象のリソースはありません",
                "potentialAction": [{
                    "@type": "OpenUri",
                    "name": "List of resources",
                    "targets": [{
                        "os": "default",
                        "uri": s3_uri
                    }]
                }]
            }]
        }
    else:
        payload = {
            '@type': 'MessageCard',
            "@context": "http://schema.org/extensions",
            "themeColor": color,
            "title": title,
            "summary": title,
            "sections": [{
                "activityTitle": sub_title,
                "facts": facts_sorted_by_resourcetype,
                "markdown": 'true',
                "potentialAction": [{
                    "@type": "OpenUri",
                    "name": "List of resources",
                    "targets": [{
                        "os": "default",
                        "uri": s3_uri
                    }]
                }]
            }]
        }

    try:
        response = requests.post(TEAMS_WEBHOOK_URL, data=json.dumps(payload))
    except requests.exceptions.RequestException as e:
        print(e)
    else:
        print(response.status_code)

def create_csv(item_list):
    resources_df = pd.DataFrame(index=[], columns=["Service", "ResourceType", "Region", "ResourceARN", "NameTagInfo"])
    for index, item in enumerate(item_list):
        resources_df.loc[index] = [item['service'], item['resourcetype'], item['region'], item['arn'], item['name']]
    sorted_resources_df = resources_df.sort_values("ResourceType")
    sourted_resources_df = sorted_resources_df.reset_index(drop=True)
    sourted_resources_df.to_csv("/tmp/temp.csv")

    d = datetime.datetime.now() + datetime.timedelta(hours=DIFF_FROM_UTC)
    file_name = str(d.date()) + "-" + str(d.hour) + "-" + str(d.minute) + "-" + str(d.second)
    bucket = s3.Bucket(S3_BUCKET_NAME)

    bucket.upload_file("/tmp/temp.csv", str(d.date()) + "/" + file_name + ".csv")

    s3_uri = boto3.client('s3', config=Config(signature_version='s3v4')).generate_presigned_url(
        ClientMethod='get_object',
        Params={'Bucket':  S3_BUCKET_NAME, 'Key': str(d.date()) + "/" + file_name + ".csv" },
        ExpiresIn=86400)

    return s3_uri

def update_item(item_list, table):
    succeeded_process_count = 0
    for item in item_list:
        arn = item["arn"]
        update_time = datetime.datetime.now().isoformat()
        try:
            table.update_item(
                Key={'arn': arn},
                UpdateExpression="set updateflag = :updateflag, updatetime = :updatetime",
                ExpressionAttributeValues={
                    ':updateflag': '2',
                    ':updatetime': update_time
                },
                ReturnValues="UPDATED_NEW"
            )
            succeeded_process_count = succeeded_process_count + 1
        except Exception as e:
            print("Item Delete Error!")
            print(e)
    
    message = str(succeeded_process_count) + " / " + str(len(item_list)) + " resources updated successfully."

    return message

def delete_item(deleted_item_list, table):
    succeeded_process_count = 0
    for deleted_item in deleted_item_list:
        arn = deleted_item["arn"]
        try:
            table.delete_item(
                Key ={
                    'arn': arn
                }
            )
            succeeded_process_count = succeeded_process_count + 1
        except Exception as e:
            print("Item Delete Error!")
            print(e)

    message = str(succeeded_process_count) + " / " + str(len(deleted_item_list)) + " resources deleted successfully."

    return message

def lambda_handler(event, context):
    table = dynamodb.Table(DYNAMO_TABLE_NAME)
    item_list = get_item(table)

    existing_item_list = select_item("0", item_list)
    new_created_item_list = select_item("1", item_list)
    deleted_item_list = select_item("2", item_list)

    if len(deleted_item_list) != 0:
        delete_message = delete_item(deleted_item_list, table)
        print(delete_message)
    else:
        print("No deleted item.")
    if len(new_created_item_list) !=0:
        update_message = update_item(new_created_item_list, table)
        print("New created items: " + update_message)
    else:
        print("No new created item.")
    if len(existing_item_list) !=0:
        update_message = update_item(existing_item_list, table)
        print("Existing items: " + update_message)
    else:
        print("No existing item.")

    d = datetime.datetime.now() + datetime.timedelta(hours=DIFF_FROM_UTC)
    title = "デプロイリソース通知 " + str(d.year) + "年" + str(d.month) + "月" + str(d.day) + "日"

    s3_uri = create_csv(item_list)

    if POST_TEAMS_SWITCH != 0:
        facts_created_item_sorted_by_resourcetype = create_facts(new_created_item_list, 1)
        facts_deleted_item_sorted_by_resourcetype = create_facts(deleted_item_list, 0)
        facts = facts_created_item_sorted_by_resourcetype
        facts.extend(facts_deleted_item_sorted_by_resourcetype)
        post_teams(title, "追加および削除されたリソース", "008000", facts, s3_uri)
    else:
        pass

    total_item_num = str(len(item_list))
    new_created_item_num = str(len(new_created_item_list))
    deleted_item_num = str(len(deleted_item_list))

    return "TOTAL: " + total_item_num + "    NEW: " + new_created_item_num + "    DELETE: " + deleted_item_num

```
　とても長いですが、create_facts関数やpost_teams関数はTeamsへの投稿を行うためのものなので、今は無視しましょう。
　流れとしては、まず、get_item関数を呼び出してdynamoDBをscanし、格納されているアイテムのリストを取得します。
　その後、select_item関数で元から存在していたリソースや新しく追加されたリソース、削除されたリソースなどを選別しています。
　削除されたリソースについては同様にdynamoDB上からも削除する必要があるため、delte_item関数を呼び出し、新しく追加されたリソースや既存のリソースについてもupdateFlagを更新するために、update_item関数を呼び出しています。  
　create_csv関数では、すべてのitemのリストをcsvファイルにして、S3にアップロードした上で、署名付きのURLを発行しています。
　最後に、post_temas関数でCSVファイルのURLや削除、追加リソースのメッセージをTeamsに投稿するという流れです。
　
### cdkテンプレート
この関数をデプロイするcdkコードは下記の通りです。
初回は、すべてのリソースが追加リソースとして認識されるため、デフォルトではTeamsへの投稿機能がオフになっています。
一度デプロイして、動作したのを確認してからPOST_TEAMS_SWITCHの値を"1"にするような想定となっています。
```typescript
    // Lambda 関数 Create Resource List
    const createResourceListFuncRole = new iam.Role(this, `${pjName}-CreateResourceListFuncRole`, {
      roleName: `${pjName}-CreateResourceListFuncRole`,
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: `${pjName}-CreateResourceListFuncRole`,
      path: '/',
      managedPolicies: [
        awsLambdaBasicExecutionRole,
        awsResourceExplorerReadOnlyAccess,
        amazonDynamoDBFullAccess,
        amazonS3FullAccess
      ]
    });
    createResourceListFuncRole.addToPolicy(new iam.PolicyStatement(
      {
        sid: 'EncKeyAccess',
        effect: iam.Effect.ALLOW,
        actions: ['kms:Encrypt', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey', 'kms:Decrypt'],
        resources: [
          bucketEncKey.keyArn
        ]
      }
    ));

    const createResourceListFunc = new lambda.Function(this, `${pjName}-CreateResourceListFunc`, {
      functionName: `${pjName}-CreateResourceListFunc`,
      runtime: lambda.Runtime.PYTHON_3_9,
      handler: 'app.lambda_handler',
      code: lambda.Code.fromAsset('lambda/create_resource_list'),
      tracing: lambda.Tracing.ACTIVE,
      timeout: Duration.minutes(10),
      memorySize: 256,
      role: createResourceListFuncRole,
      layers: [
        boto3layer,
        requestslayer,
        pandaslayer
      ],
      environment: {
        DYNAMO_TABLE_NAME: dynamoTable.tableName,
        REGION_NAME: region,
        TEAMS_WEBHOOK_URL: teamsWebhookUrl,
        S3_BUCKET_NAME: s3Bucket.bucketName,
        DYNAMO_GSI_INDEX_NAME: indexName,
        POST_TEAMS_SWITCH: "0"
      }
    });
    dynamoTable.grantReadWriteData(createResourceListFunc);
```

# 実行結果
　ここまで解説してきた仕組みを実際にデプロイすると、StepFunctionが回り始めます。
　140個くらいのLambdaが動きますが、並列起動なのでそこまで長い時間はかかりません。（長くて5分程度？）
![stepfunction.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/ba6e460f-f7b8-1cd1-175b-7a5375b34439.png)

　Teamsの方を見てみると、、
![teams_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/e19d724f-2864-0062-131c-ad7ce9972a0f.png)
投稿されていますね。（画像はデプロイ増減があった日のものを載せています。また、ARN等はがっつりマスクしています。）

　Teamsの投稿の見た目はもう少し何とかしたほうが良いかもしれないですが、当初やりたかったことは達成できているようにも見えます。

# 課題
　実はこの記事は「やりたいことができてよかった！」で終われればよかったのですが、しばらく何日か挙動を見てみていくつか課題があることがわかっています。そもそも今回やりたいことにResource Explorerがマッチしていない可能性も高いと考えています。
　現在挙がっている課題としては、
・ 実際のリソースの状態がResource Explorerへ反映される際のタイムラグがとても大きく、さらにサービスによっても異なる。
・ Resource Explorerが対応していないリソースがまだ多い。
という点があります。
　特に、反映のタイムラグに関しては、実際の環境からは消えていても2,3日平気で検索に引っかかり続けるものもありますし、EC2インスタンスの新規追加の反映は早いのに、KeyPairは遅い。。というようなことが全然発生します。
　前日に追加/削除されたリソースのリストを見るという今回の目的がある以上、この問題は看過しづらく、実際にこの仕組みを使っていくにはまだまだハードルがあると考えています。

# まとめ
　今回は、StepFunctionやLambda、DynamoDBを使って前日のリソース増減をTeamsに投稿するための仕組みを検討し、紹介しました。結果としては、Resource Explorer側の仕様と相性が悪く、なかなか実際に利用するにはハードルが高いという結論になりました。
　しかし、反映タイミングによってだいぶ遅くに通知されることはありますが、参考程度で利用するという場合は活用いただければ幸いです。
　また、途中で出てきたStepFunctionやLambda, DynamoDBを利用した処理の部分も、それぞれの要素を利用した仕掛けを作る際に少しでもお役に立てれば幸いです。　
　ありがとうございました。

　記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。