AWS Organizations なしで「タグ必須」を守らせる ─ IaC・IAM・AWS Config による多層防御
はじめに
こんにちは!コンサルティング部のヒスです。
ガバナンス文書には、たいてい次の一文が出てきます。
「すべてのリソースに必須タグを。タグの無いリソースは禁止。」
書くのは簡単ですが、「その"禁止"を実際にどう守らせるか」となると話は別です。人は付け忘れますし、急ぎでコンソールから作ったリソースにはタグが無いこともあります。
さらに今回は、タグ強制の王道であるタグポリシーが前提とする AWS Organizations が使えない環境でした。
ただ、Organizations が無くても打つ手はあります。
本記事ではその考え方と、実際に動かした手順を紹介します。先に結論です。
作る手段に応じて「IaC(
default_tags)」と「IAM ポリシー条件」で作成時にタグを担保し、AWS Config で未タグを検知、SNS で通知して是正する。この多層防御で、実質的にタグを守らせる仕組みを実現する。
タグ強制の手段を整理する ─ 使えるもの・使えないもの
タグ統制のアプローチは、予防と検知の2段階に分かれます。本記事では以下の3パターンを扱います。
- 予防(組織全体) — タグポリシー / SCP。最も強力だが Organizations が前提。今回は使えない。
- 予防(アカウント単位) — IaC の
default_tags、IAM のタグ条件。Organizations 不要で、今回も使える。 - 検知 — AWS Config の
required-tags。Organizations 不要。ただし作成は止めず、見つけるだけ。
検知が「止めない」点は公式にも明記されています。
つまり Organizations 無しでも、アカウント単位の予防+検知を組み合わせれば「タグの無いリソースが放置されない状態」には到達できます。
環境別:どの組み合わせを使うか
具体的な手段に入る前に、全体像を整理しておきます。リソースを「どうやって作っているか」によって、効かせるべき予防策が変わります。
| 環境 | ① 予防 | ② 検知 | ③ 是正 |
|---|---|---|---|
| IaC 中心 | ケースA:default_tags |
AWS Config | SNS 通知 |
| コンソール中心 | ケースB:IAM ポリシー条件で Deny | AWS Config | SNS 通知 |
| 両方が混在 | ケースA+ケースB の両方 | AWS Config | SNS 通知 |
②検知と③是正は、どの環境でも共通で効かせます。違いが出るのは①予防だけです。
ご自身の環境に合わせて、次章の「ケースA」「ケースB」のどちらか(または両方)を読んでください。
3段構えで「実質、守らせる」
① 予防:作成時にタグを担保する
予防の要はそもそも未タグのリソースを作らせないこと。作る手段に応じて、2つのケースで考えます。
ケースA:IaC で作る場合(default_tags)
Terraform などIac経由でしか作れない運用にすれば、タグはコードで管理されるため、作成と同時に必ず付与されます。
AWS Provider の default_tags を設定すれば、配下の対応リソース全部に共通タグが自動付与され、リソースごとに tags を書く必要もありません。
ケースB:コンソールで作る場合(IAM ポリシー条件で Deny)
コンソール作業は残ります。そこは IAM のタグ条件キー aws:RequestTag が効きます。「指定タグを付けずに作成しようとしたら Deny」でき、アカウント単位・Organizations 不要。検知ではなく作成時点でブロックする予防です(具体的な HCL は後述の「やってみた」で示します)。
ただし注意点があります。aws:RequestTag(tag-on-create)に対応したアクションでしか効かず、止めたい作成アクションを1つずつ明示する必要があります。
なぜ「全リソース一括 Deny」にできないのか:
Action: "*"+ タグ Null 条件で書くと、s3:GetObjectなどの読み取り系でも条件が評価され(タグが無い=Null)、アカウントが丸ごとロックされてしまいます。だから作成アクションを個別に列挙するしかありません。
この「タグが無ければ一律に作らせない」こそ本来 Organizations のタグポリシーの役割で、使えない今回は 広いカバーは ①ケースA のdefault_tagsに任せ、IAM はコンソールで特に止めたい代表的な作成アクションだけを押さえる、という分担が現実的です。
② 検知:AWS Config で未タグを見つける
予防を効かせても漏れは出ます(対応外サービス、移行で持ち込んだリソース、検証時の消し忘れなど)。
そこを AWS Config の required-tags ルールで継続チェックします。
- Organizations 不要、アカウント単位で有効化できる。
- タグは最大6キーまで指定可能。
- 作成は止めない(未タグの可視化のみ)。
- 対象リソースタイプは限定的(S3 / EC2 / RDS / ELB / VPC 系など)。
特に注意したいのがカバレッジの穴です。Lambda・SNS・IAM ロール・KMS など「タグは付けられるのに required-tags の対象タイプに無い」リソースは、未タグでも検知されません。これを補うのが ①ケースA の default_tags で、対象タイプに関係なくタグを付けられるリソース全てに作成時付与するため、穴をそもそも作りません。検知はあくまで補助で、主役は予防です。
③ 是正:見つけたら通知して直す
検知だけでは放置されます。**「見つける → 知らせる → 直す」**まで回して初めて"守らせる"に近づきます。required-tags が NON_COMPLIANT を出したら、EventBridge で拾って SNS でメール/Slack 通知し、期限を決めて担当者がタグを付ける、という流れです。
なお required-tags には標準の自動是正(AWS-SetRequiredTags)がそのまま使えず、自動化するには独自の SSM Automation を作る必要があります。
ガイドラインに落とすときは「タグの無いリソースは禁止」と一行で書くより、「IaC と IAM で予防/Config で検知/通知して期限内に是正」という運用フローとして書き下すほうが、Organizations 無しでも機能する文書になります。
やってみた
考え方はわかっても、実際に効果があるのか気になるところです。そこで、実際に動かして確認してみました。
予防(IaC / IAM)で作成時にタグを担保し、AWS Config で未タグを検知できるかを見ていきます。
コードの構成(全文は載せません)
検証は、単一アカウントで完結する Terraform 一式で構成しました。ファイル構成は次のとおりです。
tag-enforce-demo/
├── terraform.tf # terraform / provider + default_tags(①ケースAのポイント)
├── compliant.tf # default_tags が効くタグ付き EC2(+最小 VPC/サブネット)
├── iam.tf # タグなし作成を Deny する IAM ポリシー(①ケースBのポイント)
├── rule.tf # required-tags マネージドルール(②のポイント)
├── notify.tf # EventBridge → SNS 通知(③のポイント)
├── variables.tf # リージョン・必須タグ値・通知先メール
└── outputs.tf # 検証用コマンドや作成リソースの出力
記事では全文は貼りません。IAM ロールやインスタンス起動用の VPC/サブネットは定型のボイラープレートなので、
この記事の主役である「①ケースA:default_tags」、「①ケースB:IAM ポリシー条件」、「②required-tags ルール」、そして補足で「③EventBridge Input Transformer」の4か所だけ抜粋します。
AWS Config レコーダーについて:
今回の検証環境には、すでに有効化済みの AWS Config レコーダーが存在していました。AWS Config のレコーダーはリージョンごとに1つしか持てないため、自前で新規作成せず、既存レコーダーをそのまま利用しています。
ただし、このレコーダーは記録対象を特定のリソースタイプに絞っており、EC2 インスタンスが記録対象に含まれていませんでした。required-tagsルールは「レコーダーが記録した構成情報」に対してしか評価できないため、既存レコーダーの記録対象(resourceTypes)にAWS::EC2::Instanceを追加しています。
まっさらなアカウントで一から始める場合は、レコーダー・配信先 S3・IAM ロールを自分で作成する必要があります(本記事では割愛)。
① default_tags で「作成時にタグを担保」(ケースA)
まず provider に共通タグを定義します。
ここが必須タグを設定している箇所であり、抜粋して紹介するのはこのブロックだけで十分です。
provider "aws" {
region = "ap-northeast-1"
# ★ 配下の対応リソース全部に、このタグが自動で付く
default_tags {
tags = {
Name = "<名前>"
System = "<システム名>"
Env = "dev"
}
}
}
あとは EC2 インスタンスを、tags = {...} を記述せずに作成するだけです。
それでもタグが付与される点がポイントです(このインスタンスは、後述の required-tags ルールでも Name/System/Env が揃っているため COMPLIANT になります)。

上のスクリーンショットのとおり、EC2リソースの定義には tags を一切書いていないにもかかわらず、作成された EC2 インスタンスには Name / System / Env の3つが自動で付与されています。
これが default_tags による「作成時のタグ担保」です。
コードに書き忘れる余地そのものが無くなる、というのがケースA の効きどころです。
① IAM ポリシー条件で「タグなし作成を Deny」(ケースB)
次に、コンソール作業者を想定して、タグ無しの作成を IAM で拒否します。
抜粋して紹介するのは、ポリシードキュメントと aws_iam_policy リソースの2ブロックです。
必須タグ(Name / System / Env)の いずれかが欠けていれば拒否したいので、dynamic でタグごとに Deny ステートメントを生成しています。
# 必須タグ(rule.tf の required-tags と揃える)
locals {
required_tag_keys = ["Name", "System", "Env"]
}
# 必須タグのいずれかが欠けていれば Deny(タグごとに 1 ステートメント= OR 条件)
data "aws_iam_policy_document" "deny_untagged_ec2" {
dynamic "statement" {
for_each = local.required_tag_keys
content {
sid = "DenyRunInstancesWithout${statement.value}Tag"
effect = "Deny"
actions = ["ec2:RunInstances"]
resources = ["arn:aws:ec2:*:*:instance/*"]
condition {
test = "Null"
variable = "aws:RequestTag/${statement.value}"
values = ["true"]
}
}
}
}
# ★ ここで付けた name が、ブロック時のエラーにそのまま表示される
resource "aws_iam_policy" "deny_untagged_ec2" {
name = "tag-demo-deny-untagged-ec2"
policy = data.aws_iam_policy_document.deny_untagged_ec2.json
}
dynamic "statement"はfor_eachのリスト(ここではName/System/Env)を回して、同じ形のステートメントをキーの数だけ生成する書き方です。上記はタグ 3 つ分、計 3 つの Deny ステートメントに展開されます。
このポリシー(tag-demo-deny-untagged-ec2)をアタッチしたユーザー/ロールで、Name / System / Env のいずれかを欠いたまま EC2 インスタンスを起動しようとすると作成自体が拒否されます。
実際、ブロック時のエラーには次のように、先ほど付けたポリシー名がそのまま表示されます。

検知ではなく、作成時点で物理的に止める予防が、Organizations 無しでも効いていることを確認できます。
なお明示的 Deny は、AdministratorAccess を持つ IAM ユーザー/ロールにも勝つため、強い予防になります。
ただし例外がルートユーザーです。
ルートアカウントには IAM ポリシーが適用されないため、ルートの使用自体を運用ルールで禁止すること(普段は IAM ユーザー/ロールを使い、ルートは MFA で封印しておく)が前提となります。
② required-tags ルールで「未タグを検知」
次に、AWS Config のマネージドルールを1つ配置します。
今回は、ケースB の IAM Deny と検知対象を揃えて、EC2 インスタンスを評価対象にします。
resource "aws_config_config_rule" "required_tags" {
name = "required-tags"
source {
owner = "AWS"
source_identifier = "REQUIRED_TAGS"
}
# 必須にするタグキー(最大6個まで)
input_parameters = jsonencode({
tag1Key = "Name"
tag2Key = "System"
tag3Key = "Env"
})
scope {
compliance_resource_types = ["AWS::EC2::Instance"]
}
}
ここで、あえてタグを付けずに EC2 インスタンスを作成します。
ただし、ケースB の IAM Deny をアタッチしたロールでは未タグ起動がそもそもブロックされるため、
検知のデモでは Deny の対象外の権限(ルートユーザーなど)で未タグのインスタンスを作成します。
required-tags は作成を止めないため、インスタンスは問題なく作成できてしまいます。しかし、Config はそれを見逃しません。


そのインスタンスにタグを付けて再評価します。


COMPLIANT となり、検知リストから消えています。

③ 検知したら通知が飛ぶ
最後に、NON_COMPLIANT を EventBridge で拾って SNS でメール通知します。
仕組みは前章のとおり EventBridge → SNS です。
加工なしで転送すると──届くのは英語の生イベント
EventBridge をそのまま(ターゲットに SNS を指定するだけ)にすると、届くのは Config の生イベント(英語の JSON) です。
情報は入っているものの、「どのリソースが・なぜ引っかかったのか」がぱっと見では読み取りづらく、受け取った担当者がすぐに動けません。

EventBridge の Input Transformer で日本語に整形する
そこで、EventBridge ターゲットの Input Transformer を使い、イベントから必要な項目(リソース ID・判定・ルール名・検知時刻など)だけを抜き出して、日本語の本文に組み立てます。
resource "aws_cloudwatch_event_target" "to_sns" {
rule = aws_cloudwatch_event_rule.config_noncompliant.name
target_id = "sns"
arn = aws_sns_topic.tag_alert.arn
# 英語の生イベント JSON ではなく、日本語の読みやすい本文に整形して通知する
input_transformer {
# イベントから必要な値を抜き出す(JSON パス)
input_paths = {
resourceId = "$.detail.resourceId"
resourceType = "$.detail.resourceType"
ruleName = "$.detail.configRuleName"
compliance = "$.detail.newEvaluationResult.complianceType"
account = "$.detail.awsAccountId"
region = "$.detail.awsRegion"
detectedTime = "$.time"
}
# 各行を "..." で囲むと、EventBridge が改行でつないで 1 通の本文にする
input_template = <<-EOT
"【タグ統制アラート】必須タグ未設定のリソースを検知しました。"
""
"対象リソースID : <resourceId>"
"リソース種別 : <resourceType>"
"判定 : <compliance>"
"ルール : <ruleName>"
"アカウント : <account>"
"リージョン : <region>"
"検知時刻 : <detectedTime>"
""
"通知理由 : 必須タグ(Name / System / Env)のいずれかが不足しています。"
"対応 : 対象リソースに必須タグを付与し、再評価してください。"
EOT
}
}
これで、届くメールが次のような日本語の本文になります。

まとめ
AWS Organizations が使えない環境でも、IaC・IAM ポリシー・AWS Config を組み合わせることで、タグ必須を現実的に運用できることが確認できました。
完璧な強制ではないものの、予防・検知・是正の流れを整えることで、十分に実用的な構成になります。
同じ制約で悩んでいる方の参考になれば幸いです。








