AWS Organizations なしで「タグ必須」を守らせる ─ IaC・IAM・AWS Config による多層防御

AWS Organizations なしで「タグ必須」を守らせる ─ IaC・IAM・AWS Config による多層防御

2026.06.26

はじめに

こんにちは!コンサルティング部のヒスです。

ガバナンス文書には、たいてい次の一文が出てきます。

「すべてのリソースに必須タグを。タグの無いリソースは禁止。」

書くのは簡単ですが、「その"禁止"を実際にどう守らせるか」となると話は別です。人は付け忘れますし、急ぎでコンソールから作ったリソースにはタグが無いこともあります。
さらに今回は、タグ強制の王道である
タグポリシーが前提とする AWS Organizations が使えない
環境でした。

ただ、Organizations が無くても打つ手はあります。
本記事ではその考え方と、実際に動かした手順を紹介します。先に結論です。

作る手段に応じて「IaC(default_tags)」と「IAM ポリシー条件」で作成時にタグを担保し、AWS Config で未タグを検知、SNS で通知して是正する。この多層防御で、実質的にタグを守らせる仕組みを実現する。

タグ強制の手段を整理する ─ 使えるもの・使えないもの

タグ統制のアプローチは、予防と検知の2段階に分かれます。本記事では以下の3パターンを扱います。

  • 予防(組織全体) — タグポリシー / SCP。最も強力だが Organizations が前提。今回は使えない。
  • 予防(アカウント単位) — IaC の default_tags、IAM のタグ条件。Organizations 不要で、今回も使える。
  • 検知 — AWS Config の required-tagsOrganizations 不要。ただし作成は止めず、見つけるだけ

検知が「止めない」点は公式にも明記されています。
https://docs.aws.amazon.com/config/latest/developerguide/required-tags.html

つまり 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-tagsNON_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 に共通タグを定義します。
ここが必須タグを設定している箇所であり、抜粋して紹介するのはこのブロックだけで十分です。

terraform.tf
provider "aws" {
  region = "ap-northeast-1"

  # ★ 配下の対応リソース全部に、このタグが自動で付く
  default_tags {
    tags = {
      Name   = "<名前>"
      System = "<システム名>"
      Env    = "dev"
    }
  }
}

あとは EC2 インスタンスを、tags = {...} を記述せずに作成するだけです。
それでもタグが付与される点がポイントです(このインスタンスは、後述の required-tags ルールでも Name/System/Env が揃っているため COMPLIANT になります)。

スクリーンショット 2026-06-26 15.33.56

上のスクリーンショットのとおり、EC2リソースの定義には tags を一切書いていないにもかかわらず、作成された EC2 インスタンスには Name / System / Env の3つが自動で付与されています
これが default_tags による「作成時のタグ担保」です。
コードに書き忘れる余地そのものが無くなる、というのがケースA の効きどころです。

① IAM ポリシー条件で「タグなし作成を Deny」(ケースB)

次に、コンソール作業者を想定して、タグ無しの作成を IAM で拒否します。
抜粋して紹介するのは、ポリシードキュメントと aws_iam_policy リソースの2ブロックです。
必須タグ(Name / System / Env)の いずれかが欠けていれば拒否したいので、dynamic でタグごとに Deny ステートメントを生成しています。

iam.tf
# 必須タグ(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 インスタンスを起動しようとすると作成自体が拒否されます
実際、ブロック時のエラーには次のように、先ほど付けたポリシー名がそのまま表示されます

スクリーンショット 2026-06-26 16.54.43

検知ではなく、作成時点で物理的に止める予防が、Organizations 無しでも効いていることを確認できます。
なお明示的 Deny は、AdministratorAccess を持つ IAM ユーザー/ロールにも勝つため、強い予防になります。

ただし例外がルートユーザーです。
ルートアカウントには IAM ポリシーが適用されないため、ルートの使用自体を運用ルールで禁止すること(普段は IAM ユーザー/ロールを使い、ルートは MFA で封印しておく)が前提となります。

② required-tags ルールで「未タグを検知」

次に、AWS Config のマネージドルールを1つ配置します。
今回は、ケースB の IAM Deny と検知対象を揃えて、EC2 インスタンスを評価対象にします。

rule.tf
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 はそれを見逃しません。

スクリーンショット 2026-06-26 17.41.25

スクリーンショット 2026-06-26 17.41.59

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

スクリーンショット 2026-06-26 17.48.43

スクリーンショット 2026-06-26 17.56.17

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

スクリーンショット 2026-06-26 17.50.51

③ 検知したら通知が飛ぶ

最後に、NON_COMPLIANT を EventBridge で拾って SNS でメール通知します。
仕組みは前章のとおり EventBridge → SNS です。

加工なしで転送すると──届くのは英語の生イベント

EventBridge をそのまま(ターゲットに SNS を指定するだけ)にすると、届くのは Config の生イベント(英語の JSON) です。
情報は入っているものの、「どのリソースが・なぜ引っかかったのか」がぱっと見では読み取りづらく、受け取った担当者がすぐに動けません。

スクリーンショット 2026-06-26 18.00.51

EventBridge の Input Transformer で日本語に整形する

そこで、EventBridge ターゲットの Input Transformer を使い、イベントから必要な項目(リソース ID・判定・ルール名・検知時刻など)だけを抜き出して、日本語の本文に組み立てます。

notify.tf
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
  }
}

これで、届くメールが次のような日本語の本文になります。

スクリーンショット 2026-06-26 18.16.05

まとめ

AWS Organizations が使えない環境でも、IaC・IAM ポリシー・AWS Config を組み合わせることで、タグ必須を現実的に運用できることが確認できました。

完璧な強制ではないものの、予防・検知・是正の流れを整えることで、十分に実用的な構成になります。
同じ制約で悩んでいる方の参考になれば幸いです。


そのマルチアカウント運用、気合いで支えていませんか

Organizations や Control Tower で土台は作れても、アカウントもポリシーも増えるほど、運用は「詳しい一人」に寄りかかっていく。属人化が限界を迎える前に、組織として回す仕組み=CCoEへ。5,600社の支援から得た立ち上げの型を、無料資料にまとめました。

CCoE総合支援

組織で回す仕組みの資料をもらう

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事