検証環境のリソースに自動でタグ付けするシステム構築

検証環境でリソースを作成する際、誰が作成したのかを明確にするためにタグ付けを行う環境は多いと思います。しかし、実際には作成者タグのないリソースが散見されることはありませんか?

そこで今回は、検証環境に新規リソースを作成する際に、自動的に作成者と作成時刻をタグとして付与するスクリプトを作成しました。このスクリプトを使えば、誰がいつリソースを作成したのかが一目瞭然となり、リソース管理の効率化に繋がります。

さらに、EC2とRDSインスタンスには停止時刻もタグ付けすることで、不要なリソースを容易に発見できるようにしました。

この自動タグ付けシステムは、CloudFormationテンプレートを使用して簡単にデプロイできます。

作成時刻と作成者が自動タグ付けされるリソース

  • EC2
  • EIP
  • VPC
  • NatGateway
  • VPCEndpoint
  • AMI
  • Snapshot
  • EBS
  • ALB
  • NLB
  • RDSインスタンス
  • RDSスナップショット
  • S3バケット

停止時刻が記録されるリソース

  • EC2
  • RDSインスタンス

使い方

※ 実行環境はCloudShellを想定しています。
※ 検証環境での実行を想定しており、実行ユーザーの権限は「AdministratorAccess」を設定してください。

  1. 以下のコマンドを実行し、ファイルをダウンロードします。
    wget https://iret.media/wp-content/uploads/2025/02/Automatic-Tagging-System.zip
  2. ファイルを解凍します。
    unzip Automatic-Tagging-System.zip
  3. ディレクトリに移動します。
    cd Automatic-Tagging-System
  4. ディレクトリ内で、以下のコマンドを実行します。
    • 全リージョンにデプロイする場合:
      python Deploy_All_Region.py
    • 特定のリージョンにデプロイする場合:
      python Deploy_Individual_Region.py
    • デプロイしたスタックを削除する場合:
      python Delete_All_Region.py

      ※ 特定のリージョンのみ削除したい場合は、リージョンのCloudFormationスタックを削除してください。

構成要素

このスクリプトは、以下の要素で構成されています。

  • デプロイ用Pythonスクリプト: CloudFormationスタックの作成・更新、Lambda関数のデプロイを自動化します。
  • CloudFormationテンプレート: Lambda関数、IAMロール、CloudWatch EventsルールなどのAWSリソースを定義します。
  • Lambda関数: 対象リソースへのタグ設定処理を実行します。

●デプロイ用Pythonスクリプトの説明

デプロイ用Pythonスクリプトのフローチャートは以下となります。S3からLambda関数をデプロイする場合、LambdaとS3は同一リージョンに存在する必要があるため、デプロイするリージョンごとにS3バケットを作成しています。

作成されたS3バケットのバケット名とキー情報はCfnのパラメータとしてデプロイ時に参照されます。

フローチャート

●CFnテンプレート及びLambda関数の説明

CFnテンプレートでは、EventBridge Ruleでパブリック AWS サービス API へのリクエストを読み取り、タグ設定用のLambda関数を呼び出しています。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Automatic-Tagging-System-Deploy'
Parameters:
  TempS3BucketName:
    Type: String
  TempS3KeyName:
    Type: String
 
Resources:
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/ResourceGroupsandTagEditorFullAccess'
        - 'arn:aws:iam::aws:policy/ResourceGroupsTaggingAPITagUntagSupportedResources'
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'       
 
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      Code:
        S3Bucket: !Sub '${TempS3BucketName}'
        S3Key: !Sub '${TempS3KeyName}'
      Role: !GetAtt LambdaFunctionRole.Arn
      Runtime: python3.11
      MemorySize: 128
      Timeout: 30
 
  LogGroupFirehosLambda:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${LambdaFunction}'
      RetentionInDays: 7
 
  LambdaFunctionPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt
        - LambdaFunction
        - Arn
      Principal: "events.amazonaws.com"
      SourceArn: !GetAtt
        - EventRule
        - Arn
 
  EventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - "aws.ec2"
          - "aws.rds"
          - "aws.elasticloadbalancing"
          - "aws.s3"
        detail-type:
          - "AWS API Call via CloudTrail"
        detail:
          eventSource:
            - "ec2.amazonaws.com"
            - "rds.amazonaws.com"
            - "elasticloadbalancing.amazonaws.com"
            - "s3.amazonaws.com"
      State: "ENABLED"
      Targets:
        - Arn: !GetAtt
            - LambdaFunction
            - Arn
          Id: TargetFunctionV1

Lambda関数では、与えられたペイロードの中身をCase文で分岐して処理しています。タグ設定対象を追加する場合は、CFnテンプレートの「AWS::Events::Rule」部分とLambdaのCase文を修正してデプロイします。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import boto3, json
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
 
client_ResourceGroupsTaggingApi = boto3.client('resourcegroupstaggingapi')
client_EC2                      = boto3.client('ec2')
 
def ResourceGroupsTaggingApi(ARN, AutoSet_Owner_Name, CreationDate):
    client_ResourceGroupsTaggingApi.tag_resources(
        ResourceARNList=[ARN],
        Tags={'AutoSet_Owner_Name': AutoSet_Owner_Name, 'CreationDate': CreationDate}
    )
    return
 
def ec2ResourceTagging(Resources, AutoSet_Owner_Name, CreationDate):
    client_EC2.create_tags(
        Resources=[Resources],
        Tags=[
            {'Key': 'AutoSet_Owner_Name','Value': AutoSet_Owner_Name},
            {'Key': 'CreationDate','Value': CreationDate},
        ]
    )
    return
 
def ResourceGroupsTaggingApi_StopDate(ARN, AutoSet_Owner_Name, StopDate):
    client_ResourceGroupsTaggingApi.tag_resources(
        ResourceARNList=[ARN],
        Tags={'LastStopDate': StopDate}
    )
    return
 
def ec2ResourceTagging_StopDate(Resources, AutoSet_Owner_Name, StopDate):
    client_EC2.create_tags(
        Resources=[Resources],
        Tags=[
            {'Key': 'LastStopDate','Value': StopDate},
        ]
    )
    return
 
 
def lambda_handler(event, content):
    #print(event)
    if not 'detail' in event:
        return
 
    # インスタンス作成者の情報
    AutoSet_Owner_Name = event['detail']['userIdentity']['arn'].split("/")[-1]
    # インスタンス作成日の情報
    # 文字列からdatetimeオブジェクトを作成
    utc_dt = datetime.fromisoformat(event['detail']['eventTime'])
    # タイムゾーンを'JST'に変更
    jst_dt = utc_dt.astimezone(ZoneInfo("Asia/Tokyo"))
    # 出力形式を指定
    eventTime  = jst_dt.strftime('%Y-%m-%dT%H:%M:%S%z')
 
    # イベント名取得
    match event['detail']['eventName']:
        #EC2関連リソース
        case 'RunInstances':
            resource_id = event['detail']['responseElements']['instancesSet']['items'][0]['instanceId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'AllocateAddress':
            resource_id = event['detail']['responseElements']['allocationId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateVpc':
            resource_id = event['detail']['responseElements']['vpc']['vpcId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateNatGateway':
            resource_id = event['detail']['responseElements']['CreateNatGatewayResponse']['natGateway']['natGatewayId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateVpcEndpoint':
            resource_id = event['detail']['responseElements']['CreateVpcEndpointResponse']['vpcEndpoint']['vpcEndpointId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateImage':
            resource_id = event['detail']['responseElements']['imageId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateSnapshots':
            resource_id = event['detail']['responseElements']['CreateSnapshotsResponse']['snapshotSet']['snapshotId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateVolume':
            resource_id = event['detail']['responseElements']['volumeId']
            ec2ResourceTagging(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        #EC2関連以外のリソース
        #設定可能なリソース→https://docs.aws.amazon.com/ja_jp/ARG/latest/userguide/supported-resources.html
        #リソースのARN形式→https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference-arns.html
        case 'CreateLoadBalancer':
            ARN = event['detail']['responseElements']['loadBalancers'][0]['loadBalancerArn']
            ResourceGroupsTaggingApi(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateDBInstance':
            ARN = event['detail']['responseElements']['dBInstanceArn']
            ResourceGroupsTaggingApi(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateDBSnapshot':
            ARN = event['detail']['responseElements']['dBSnapshotArn']
            ResourceGroupsTaggingApi(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateDBCluster':
            ARN = event['detail']['responseElements']['dBClusterArn']
            ResourceGroupsTaggingApi(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateDBClusterSnapshot':
            ARN = event['detail']['responseElements']['dBClusterSnapshotArn']
            ResourceGroupsTaggingApi(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'CreateBucket':
            bucketName = event['detail']['requestParameters']['bucketName']
            ARN = f"arn:aws:s3:::{bucketName}"
            ResourceGroupsTaggingApi(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        #EC2とRDSは最終停止日時も設定する
        case 'StopInstances':
            resource_id = event['detail']['responseElements']['instancesSet']['items'][0]['instanceId']
            ec2ResourceTagging_StopDate(resource_id, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'StopDBInstance':
            ARN = event['detail']['responseElements']['dBInstanceArn']
            ResourceGroupsTaggingApi_StopDate(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
        case 'StopDBCluster':
            ARN = event['detail']['responseElements']['dBClusterArn']
            ResourceGroupsTaggingApi_StopDate(ARN, AutoSet_Owner_Name, eventTime)
            return { 'statusCode': 200,'body': json.dumps('Excuted!')}
    return { 'statusCode': 200,'body': json.dumps('Excuted!')}