はじめに
AWS環境を運用していると、「このリソース、誰が作ったんだっけ?」となる場面が結構あります。
IAMユーザー単位で使っているなら CloudTrail を追えばわかるのですが、毎回コンソールで追いかけるのは地味に手間ですし、コスト配賦や棚卸しのたびに調べていると時間が溶けていきます。
そこで、リソースが作成されたタイミングで、作成者のIAM識別情報を自動でタグとして付与する仕組みを作ってみました。
本記事では、以下の3点を実装したCloudFormationテンプレートとLambdaコードを紹介します。
- CloudTrail の書き込みイベントを EventBridge でキャッチ
- Lambda が Resource Groups Tagging API で該当リソースにタグを付与
- タグ API が効かないサービスはサービス個別 API でフォールバック
この記事でやること
- 仕組みの全体像と各コンポーネントの役割の整理
- CloudFormation テンプレート(メイン/グローバル)のポイント解説
- Lambda(Python)での作成者抽出とタグ付け処理のポイント解説
- タイムスタンプをJSTで付与する小技
- 運用してみての所感
前提条件
- CloudTrail が有効化されていること(管理イベント)
- CloudFormation を実行できる IAM 権限を持っていること
- リソースの作成者を
Ownerタグに入れることを許容できる運用ポリシーであること
全体構成
今回の構成は大きく2スタックに分かれています。
| スタック | デプロイ先 | 役割 |
|---|---|---|
| メインスタック | ap-northeast-1(任意の主リージョン) | Lambda、EventBridgeルール、IAM、カスタムイベントバスを作成 |
| グローバルスタック | us-east-1(固定) | IAMやCloudFrontなどのグローバルサービスのイベントを主リージョンへ転送 |
構成のポイントは以下です。
- リージョナルなイベント は、主リージョンの default イベントバスで直接キャッチして Lambda を起動
- グローバルサービスのイベント(IAM / CloudFront / Route 53 / WAF / Organizations など)は us-east-1 にしか流れないので、us-east-1 側の EventBridge で拾い、主リージョンのカスタムイベントバスへクロスリージョン転送
- Lambda は、リージョナル用ルールとグローバル用ルールのどちらから呼ばれても同じ処理を行う
この2系統にしておくと、「グローバルサービスだけタグが漏れる」問題を避けられます。
イベントパターンの考え方
EventBridge のルールでは、以下の方針でフィルタしています。
-
detail-typeはAWS API Call via CloudTrailのみ -
readOnly: falseで読み取り系イベントを除外 -
errorCodeが存在するイベント(失敗)は除外 -
eventNameの prefix で作成・更新系のみを拾う
eventName の prefix は次のような広めの一覧にしました。
eventName:
- prefix: "Create"
- prefix: "Run"
- prefix: "Allocate"
- prefix: "Register"
- prefix: "Import"
- prefix: "Request"
- prefix: "Restore"
- prefix: "Put"
- prefix: "Copy"
- prefix: "Start"
- prefix: "Launch"
Create* や Run* だけでは AllocateAddress、ImportKeyPair、RegisterTaskDefinition などが拾えないので、少し広めに取っています。Put* は CloudWatch のアラーム作成などを拾うために入れていますが、データプレーン系(PutObject など)は後述する Lambda 側の SKIP_EVENT_NAMES ではじく運用にしました。
IAMポリシーの考え方
Lambda の実行ロールに紐づくポリシーは、基本的に以下の3系統を許可します。
-
tag:*(Resource Groups Tagging API の本体) - 各サービスのタグ付け系アクション(
service:*Tag*のワイルドカード) - 一部、名前に「Tag」を含まないサービスの個別アクション
ポイントは service:*Tag* というワイルドカードの書き方 です。
IAM の Action には * が使えるため、ec2:*Tag* と書けば CreateTags / DeleteTags / DescribeTags がまとめてヒットします。S3 系の PutBucketTagging / GetBucketTagging、SQS の TagQueue / UntagQueue、IAM の TagRole / UntagRole なども同じパターンで一括カバーできます。
サービス横断の一例を抜粋します。
- Sid: ServiceTaggingWildcard
Effect: Allow
Action:
# ---- Compute ----
- ec2:*Tag*
- lambda:*Tag*
- batch:*Tag*
# ---- Storage ----
- s3:*Tag*
- elasticfilesystem:*Tag*
- fsx:*Tag*
# ---- Database ----
- rds:*Tag*
- dynamodb:*Tag*
- redshift:*Tag*
# ---- Security & Identity ----
- iam:*Tag*
- kms:*Tag*
- secretsmanager:*Tag*
# ---- (他サービスも同様に続く)
Resource: "*"
Resource Groups Tagging API でタグ付けする場合、実は API 呼び出しだけでは足りず、裏側で各サービスの Tag* 系アクションが必要です。公式ドキュメントの Services that support the Resource Groups Tagging API を参照しながら、タグ付け対応サービスを網羅するようにしました。
一方で、API Gateway V1 のように REST ベースで apigateway:PUT / apigateway:GET を要求するサービスもあるので、そちらは別 Statement として明示的に追加しています。
- Sid: NonStandardTaggingActions
Effect: Allow
Action:
- apigateway:PUT
- apigateway:GET
- codebuild:UpdateProject
- codebuild:BatchGetProjects
Resource: "*"
なお、IAM Managed Policy には 6,144文字の上限 があります。サービス数を増やしすぎるとこの上限に引っかかるので、増やす場合は Statement を分割して複数の Managed Policy として Role にアタッチする形になります。
Lambdaの処理フロー
Lambda(Python 3.13)の処理は大きく以下の順番です。
- イベントから
eventName/eventSource/userIdentityを取り出す - 除外イベント(
PutObject,CreateLogStream,CreateServiceLinkedRole等)はスキップ - 作成者(IAM識別情報)を抽出
- 付与するタグを組み立てる(
Owner,AutoTagTimestamp, 任意の静的タグ) - リソース ARN を抽出
- Resource Groups Tagging API でバッチタグ付け(最大20件ずつ)
- 失敗したものだけ、サービス個別 API でフォールバック
作成者の抽出
userIdentity.type によって取り方が変わります。代表的な例は以下の通りです。
| type | 取得対象 |
|---|---|
IAMUser |
userName |
AssumedRole |
principalId を : で分割した後半(セッション名 ≒ SSO ユーザー名) |
FederatedUser |
principalId を : で分割した後半 |
Root |
固定文字列 root
|
AWSService |
invokedBy |
SSO(IAM Identity Center)主体で運用している環境だと、AssumedRole 経由がほとんどだと思います。セッション名にユーザー名やメールが入っていることが多いので、そこを拾うと「誰が作ったか」がタグで一目で分かるようになります。
リソース ARN の抽出
CloudTrail のイベントは、サービスによって ARN の載り方がバラバラです。今回は以下の3段構えで抽出しました。
-
detail.resources配列(そもそも CloudTrail 側で付けてくれていればこれを使う) - サービス個別の extractor(EC2 の
RunInstancesや S3 のCreateBucket等、ARN をレスポンスから組み立てる必要があるもの) -
responseElementsを再帰的に走査してarn:で始まる文字列を拾う
例えば EC2 の RunInstances は、ARN がレスポンスに直接入っていないのでインスタンスIDから組み立てます。
def _ec2_run_instances(detail):
arns = []
region = detail.get("awsRegion", "")
resp = detail.get("responseElements", {}) or {}
instances_set = resp.get("instancesSet", {}).get("items", [])
for inst in instances_set:
iid = inst.get("instanceId")
if iid:
arns.append(_build_ec2_arn(region, "instance", iid))
return arns
S3 の CreateBucket は arn:aws:s3:::{bucket} という特殊な形なので、専用の関数で組み立てます。
タグ付けのフォールバック
Resource Groups Tagging API は多くのサービスに対応していますが、サポート外のリソースや、タイミングによって「まだ API で見つからない」リソースもあります。その場合は各サービスのネイティブ API にフォールバックします。
if "ec2.amazonaws.com" in event_source:
_tag_ec2(arn, tags, region)
elif "s3.amazonaws.com" in event_source:
_tag_s3(arn, tags)
elif "rds.amazonaws.com" in event_source:
_tag_rds(arn, tags, region)
# ... 以降、約30サービス分の分岐
サービスによってタグ形式が [{Key, Value}](標準)/{k: v}(Lambda, Backup 等)/[{TagKey, TagValue}](KMS)とバラバラで、ここが意外と書いていて大変でした。
JSTタイムスタンプの小技
Lambda の実行環境はタイムゾーンが UTC 固定です。そのため、datetime.now(timezone.utc) で取ると当然 Z(UTC)付きの値になります。今回は JST で付与したかったので、Python 側で明示的にオフセットを定義しました。
from datetime import datetime, timezone, timedelta
# JST (UTC+9) タイムゾーン定数
# Lambda実行環境のTZはUTC固定のため、明示的にJSTオフセットを定義する
JST = timezone(timedelta(hours=9))
タグ付与時はこれを使います。
tags["AutoTagTimestamp"] = datetime.now(JST).strftime("%Y-%m-%dT%H:%M:%S%z")
%z は +0900 形式(コロンなし)で出力されます。ISO 8601 として厳密な +09:00 にしたい場合は後処理でコロンを差し込む必要がありますが、タグ値としてはどちらでも運用上支障はないかなと思います。
環境変数で TZ=Asia/Tokyo を指定する方法も一応ありますが、Lambda では推奨されていないのと、他ライブラリへの影響を避けたいのもあって、コード側で明示する方針にしました。
デプロイ手順
デプロイは以下の流れになります。
1. Lambda コードをZIPにして S3 に配置
resource_auto_tag.zip
└── resource_auto_tag.py
S3 バケットへアップロードします(バケット名は環境に合わせて置き換えてください)。
aws s3 cp resource_auto_tag.zip s3://<your-bucket>/resource_auto_tag.zip
2. メインスタックを主リージョンにデプロイ
aws cloudformation deploy \
--template-file cfn-auto-tag-main.yaml \
--stack-name auto-tag-main \
--region ap-northeast-1 \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
S3BucketName=<your-bucket> \
S3KeyName=resource_auto_tag.zip \
TagKey=Owner
デプロイ後、GlobalEventBusArn という Output が取れるので控えておきます。
3. グローバルスタックを us-east-1 にデプロイ
aws cloudformation deploy \
--template-file cfn-auto-tag-global.yaml \
--stack-name auto-tag-global \
--region us-east-1 \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
TargetEventBusArn=<主リージョンのGlobalEventBusArn>
4. Lambda コードだけ後から差し替えたい場合
CloudFormation は Lambda の Code.S3Key に変更がないとデプロイパッケージを差し替えてくれません。コードだけ直した時は、ZIP を再アップロードしてから直接 update-function-code を叩くのが早いです。
aws lambda update-function-code \
--function-name auto-tag-main-auto-tag \
--s3-bucket <your-bucket> \
--s3-key resource_auto_tag.zip \
--region ap-northeast-1
S3 をバージョニング有効にして S3ObjectVersion パラメータを更新する運用でも大丈夫です。
動作確認
テスト用に、主リージョンで S3 バケットを作ってみます。
aws s3api create-bucket \
--bucket auto-tag-test-XXXXXXXX \
--region ap-northeast-1 \
--create-bucket-configuration LocationConstraint=ap-northeast-1
数十秒待ってから、バケットのタグを確認します。
aws s3api get-bucket-tagging --bucket auto-tag-test-XXXXXXXX
以下のようなタグが付いていれば成功です。
{
"TagSet": [
{ "Key": "Owner", "Value": "XXXXXXXX" },
{ "Key": "AutoTagTimestamp", "Value": "2026-04-19T15:18:39+0900" }
]
}
AutoTagTimestamp が +0900 付きで出ていれば JST 化もきちんと効いています。
運用してみての所感
運用してみて気づいた点をいくつかメモしておきます。
良かった点
- コスト配賦が圧倒的に楽になりました。
Ownerタグでフィルタするだけで誰がいくら使っているかが見える - 「これ誰が作ったの?」を CloudTrail 遡りで調査する時間がほぼゼロに
- IAMポリシーが
service:*Tag*で書けるので、新サービスが出ても行を1つ追加するだけで済む
気になった点・ハマりポイント
- Lambda が CloudTrail イベントを受け取るまでに、だいたい 1〜5 分のタイムラグがあります。作成直後に確認スクリプトを走らせるとタグがまだ付いていない場合があるので注意
- Lambda 自身が作った CloudWatch Logs グループにもタグが付こうとして、無限ループっぽくなりかけました。
SKIP_EVENT_NAMESにCreateLogStreamやTagResource系を入れて回避しています - IAM Managed Policy の 6,144 文字上限は、サービスを増やしていくとわりとすぐに到達します。増やしたい時は Statement を分割して別ポリシーに逃がす想定が必要
今後やってみたいこと
- Slack通知との連携(タグ付けに失敗したリソースだけ Slack に投げる)
- タグポリシー(Organizations)との組み合わせで、必須タグが欠けているリソースを定期検知
- AWS Config のカスタムルールで「
Ownerタグがないリソース」を自動検出する
さいごに
CloudTrail + EventBridge + Lambda の組み合わせは、「AWS 全体に横串で効くちょっとした仕組み」を作るのに本当に便利だなと改めて感じました。
特にタグ付けは地味ですが、後から棚卸しするときのしやすさがまったく変わるので、サンドボックス環境で一度試してみるのをおすすめします。
IAMの *Tag* ワイルドカードを使う書き方は、同じような「横断的な権限付与」を書くときにも応用が効くので、覚えておいて損はない書き方だと思います。