1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS Organizations×Terraformで始めるマルチアカウント管理【本体編】

Last updated at Posted at 2025-09-07

この記事について

AWS を本格的に利用する場合、マルチアカウント戦略は避けて通れません。
この記事では、TerraformAWS Organizationsのベースラインを一括構築し、OU/アカウント、SCP、までを最小構成で自動化**していきます。

SSOの有効化については、次回の記事で扱います。

アーキテクチャ

まずは全体像です。
以下のように OU(Organizational Unit)ごとに用途を整理しています。
AWS公式ドキュメントに則り、下記のような設計をベースとします。
Organization.png

ディレクトリ構成(Organization関連)

Organization/                              # 組織ルートモジュール(OU/アカウント/SCPエントリ)
├─ policies/                               # 追加SCPのJSON置き場
├─ SSO/                                    # AWS IAM Identity Center
└─ Admin-delegation/                       # 管理委任スタック雛形(例示用)

Accounts/
├─ Security/                               # 中央集権セキュリティリソース定義
└─(他アカウント...)

modules/                                   # 再利用モジュール群
├─ organizations/                          # 組織本体/OU/アカウント/委任作成                                
└─ scp/                                    # ベースラインSCP+追加SCP

各種設定値

Organization 基盤

Organization本体: 全機能を有効化(feature_set = ALL)。SCP/Tag Policyなど有効化するポリシー種別と、サービスアクセス(Service Access)対象を入力で制御。

サービスアクセス: GuardDuty/Config/CloudTrail/SecurityHub/SSOなどを有効化。委任対象サービスは漏れなくService Accessに含める設計(setunionで自動合流)。

破壊防止: Organization/OU/アカウントはprevent_destroy = trueで誤削除を防止。

OU/アカウント

ベースOU: Root配下に Security / Workloads(配下に Prod, Dev)/ Sandbox / Suspended を作成(ベストプラクティス構成)

追加OU: 入力のマップで任意に追加、親OU指定可(未指定はRoot直下)

Securityアカウント: 固定で作成。名/メールで作成し Security OU に配置。

追加メンバーアカウント: name/email/ou/tags を指定し作成、自由なOUへ配置。

SCP(サービスコントロールポリシー)

ベースライン: 代表的な制御をRootや特定OUへ付与。
ルートユーザ使用禁止(Rootにアタッチ)
組織離脱禁止(Root)
未承認リージョン拒否(東京以外Deny、バイパス用Role例あり、Root)
セキュリティサービス無効化の禁止(Root)
Suspended OUでの全アクション拒否(Suspended OU にアタッチ)

カスタムSCP: Organization/policies 配下のJSONを追加SCPとして作成・任意のOU/Accountにアタッチ可能。例: CloudTrail停止/削除禁止。

Terraform実装例

modules/organizations

main.tf
# AWS Organization本体作成
resource "aws_organizations_organization" "this" {
  feature_set                   = "ALL"                            # 組織の全機能を有効化(おまじない・とりあえずALLで良い)
  enabled_policy_types          = tolist(var.enabled_policy_types) # なんのポリシー(scp、tagポリシー等)を有効化するか
  aws_service_access_principals = local.service_access_principals  # delegated_services を含めた有効化対象(漏れ防止)

  # Organizationの削除はterraformでは行えないようにする
  lifecycle {
    prevent_destroy = true
  }
}

# OU作成
# 下記のような、標準的なベストプラクティスなOU構成を作成
# Root
# ├─ Security  # 組織のセキュリティリソースを一元管理する
# ├─ Workloads  # アプリケーションが動作するメインの環境群
# │   ├─ Prod # 本番環境
# │   └─ Dev # 開発環境
# ├─ Sandbox  # 開発用に自由に使えるアカウントを配置
# └─ Suspended  # 利用停止中のアカウントを配置

# ループで作成する方法もあるが、式の複雑化による可読性低下を避けるために一つずつ作成
# securityアカウント環境
resource "aws_organizations_organizational_unit" "security" {
  name      = "Security"
  parent_id = local.root_id
  tags      = var.tags
  lifecycle { prevent_destroy = true }
}

# メインとなるワークロードのアカウント環境
resource "aws_organizations_organizational_unit" "workloads" {
  name      = "Workloads"
  parent_id = local.root_id
  tags      = var.tags
  lifecycle { prevent_destroy = true }
}

# workloads配下にProd, Devを作成
resource "aws_organizations_organizational_unit" "prod" {
  name      = "Prod"
  parent_id = aws_organizations_organizational_unit.workloads.id
  tags      = var.tags
  lifecycle { prevent_destroy = true }
}

# workloads配下にProd, Devを作成
resource "aws_organizations_organizational_unit" "dev" {
  name      = "Dev"
  parent_id = aws_organizations_organizational_unit.workloads.id
  tags      = var.tags
  lifecycle { prevent_destroy = true }
}

# sandboxアカウント環境
resource "aws_organizations_organizational_unit" "sandbox" {
  name      = "Sandbox"
  parent_id = local.root_id
  tags      = var.tags
  lifecycle { prevent_destroy = true }
}

# suspendedアカウント環境
resource "aws_organizations_organizational_unit" "suspended" {
  name      = "Suspended"
  parent_id = local.root_id
  tags      = var.tags
  lifecycle { prevent_destroy = true }
}

# 追加作成用OU
resource "aws_organizations_organizational_unit" "additional_ou" {
  for_each  = var.additional_ous
  name      = each.key
  parent_id = lookup(local.ou_ids, lower(each.value.parent_ou), local.root_id) # 親OUが指定されていればそこに、なければrootにぶら下げる
  tags      = var.tags
  lifecycle { prevent_destroy = true }
}

# Securityアカウントを作成
resource "aws_organizations_account" "security" {
  name      = var.security_account_name
  email     = var.security_account_email
  role_name = "OrganizationAccountAccessRole"                   # 作られたアカウントに管理アカウントがアクセスするためのロール名 標準ではOrganizationAccountAccessRole
  parent_id = aws_organizations_organizational_unit.security.id # どこのouに所属させるかを指定
  tags      = merge(var.tags, { AccountType = "Security" })     # 固定タグ群にAccountType=Securityを追加

  lifecycle { prevent_destroy = true }

  timeouts { create = "2h" } # アカウント作成は時間がかかるのでタイムアウトを2時間に延長
}

# メンバーアカウントを作成
# 変数(mapでアカウント情報を定義)で与えられたアカウント情報を元にアカウントを作成
resource "aws_organizations_account" "members" {
  for_each  = var.member_accounts
  name      = each.value.name
  email     = each.value.email
  role_name = "OrganizationAccountAccessRole"
  parent_id = local.ou_ids[lower(each.value.ou)] # mapからou名を取得

  tags = merge(
    var.tags,
    { AccountType = each.value.tags } # 固定タグ群にアカウント固有のtagを追加
  )

  lifecycle { prevent_destroy = true }
  timeouts { create = "2h" }
}

# Securityアカウントを委任管理者に登録
# 変数で書いたサービスをループで登録
resource "aws_organizations_delegated_administrator" "security_delegate" {
  for_each          = var.delegated_services
  account_id        = aws_organizations_account.security.id
  service_principal = each.key # set(string)なのでkeyは値そのもの
  # Service Access の有効化(aws_organizations_organization.this の適用)完了を待ってから登録
  depends_on = [aws_organizations_organization.this]
}
local.tf
# local.tf
locals {

  root_id = aws_organizations_organization.this.roots[0].id

  ou_ids = {
    security  = aws_organizations_organizational_unit.security.id
    workloads = aws_organizations_organizational_unit.workloads.id
    prod      = aws_organizations_organizational_unit.prod.id
    dev       = aws_organizations_organizational_unit.dev.id
    sandbox   = aws_organizations_organizational_unit.sandbox.id
    suspended = aws_organizations_organizational_unit.suspended.id
  }

  member_tags = merge(var.tags, { AccountType = "Member" })

  # Delegated Admin を登録するサービスは、事前に Service Access を有効化が必要。
  # ユーザー指定の aws_service_access_principals に delegated_services を自動的に合流して漏れを防ぐ。
  service_access_principals = tolist(
    setunion(
      toset(var.aws_service_access_principals),
      var.delegated_services
    )
  )
}

variable.tf
# AWS Organization本体作成
variable "enabled_policy_types" {
  description = "なんのポリシー(scp、tagポリシー等)を有効化するか"
  type        = list(string)
  default = [
    "SERVICE_CONTROL_POLICY",
    "TAG_POLICY"
  ]
}

variable "aws_service_access_principals" {
  description = "サービスアクセスを有効化するリソース指定(guardduty,configなど、組織内で一元管理したいリソース)"
  type        = list(string)
  default = [
    "guardduty.amazonaws.com",
    "config.amazonaws.com",
    "cloudtrail.amazonaws.com",
    "securityhub.amazonaws.com",
    # 必要に応じて追加
  ]
}

# OU作成
variable "additional_ous" {
  description = "追加で作成したいOUのマップ。keyがOU名、valueが親OU名"
  type = map(object({
    parent_ou = string # 親OU名。指定されなければrootにぶら下げる
  }))
  default = {}
}

variable "tags" {
  type = map(string)
}

# Securityアカウント作成
variable "security_account_name" {
  type    = string
  default = "Security"
}

variable "security_account_email" {
  type = string
}

# メンバーアカウント作成
variable "member_accounts" {
  type = map(object({
    name  = string
    email = string
    ou    = string
    tags  = string
  }))
}

# Securityアカウントを委任管理者に登録
variable "delegated_services" {
  description = "Securityアカウントを委任管理者に登録するサービス"
  type        = set(string)
}
outputs.tf
# modules/organizations/outputs.tf

// このモジュールが作成した Organization/OU/アカウントの識別子をまとめて出力します。
// 下流モジュールや別スタックから参照できるように、よく使う値を厳選して公開しています。

// Organization 本体の ID(例: o-xxxxxxxxxx)
output "organization_id" {
  description = "AWS Organization の ID"
  value       = aws_organizations_organization.this.id
}

// ルート(Root OU)の ID(例: r-xxxx)
output "root_id" {
  description = "Organization ルート(Root)の ID"
  value       = local.root_id
}

// セキュリティアカウント(Security OU 配下に作成した管理用アカウント)の ID
output "security_account_id" {
  description = "Security アカウントの AWS Account ID"
  value       = aws_organizations_account.security.id
}

// メンバーアカウントの ID 一覧
// key は variable "member_accounts" のキー(任意に定義した論理名)、value は実アカウント ID
output "member_account_ids" {
  description = "メンバーアカウントの ID マップ (key: 論理名, value: Account ID)"
  value       = { for k, v in aws_organizations_account.members : k => v.id }
}

// 主要 OU と追加 OU の ID 一覧
// 主要 OU: security, workloads, prod, dev, sandbox, suspended(すべて lower-case キー)
// 追加 OU: variable "additional_ous" のキーを lower-case に正規化して格納
output "ou_ids" {
  description = "主要 OU と追加 OU の ID マップ"
  value = merge(
    local.ou_ids,
    { for k, ou in aws_organizations_organizational_unit.additional_ou : lower(k) => ou.id }
  )
}

modules/scp

main.tf
# scp module
# ベースラインSCPとして、AWSが推奨する代表的な制御内容を実装

# AWS Organizations の情報を取得
data "aws_organizations_organization" "this" {}


# ルート直下の OU 一覧を取得
data "aws_organizations_organizational_units" "root_ous" {
  parent_id = data.aws_organizations_organization.this.roots[0].id
}

# Suspended OU の ID を抽出
locals {
  suspended_ou_id = one([
    for ou in data.aws_organizations_organizational_units.root_ous.children : ou.id
    if ou.name == "Suspended"
  ])
}

# 1.ルートユーザ禁止
resource "aws_organizations_policy" "deny_root" {
  name        = "SCP-DenyRootUser"
  description = "ルートユーザー使用時にすべてのアクションを拒否する"
  type        = "SERVICE_CONTROL_POLICY"
  content     = file("${path.module}/policies/deny_root.json")
  tags        = var.tags
}

resource "aws_organizations_policy_attachment" "deny_root" {
  policy_id = aws_organizations_policy.deny_root.id
  target_id = data.aws_organizations_organization.this.roots[0].id
}


# 2.組織離脱禁止
resource "aws_organizations_policy" "deny_leaving_org" {
  name        = "SCP-DenyLeavingOrganization"
  description = "AWS Organizationsからの離脱を拒否する"
  type        = "SERVICE_CONTROL_POLICY"
  content     = file("${path.module}/policies/deny_leaving_org.json")
  tags        = var.tags
}

resource "aws_organizations_policy_attachment" "deny_leaving_org" {
  policy_id = aws_organizations_policy.deny_leaving_org.id
  target_id = data.aws_organizations_organization.this.roots[0].id
}

# 3.  未承認リージョン禁止
# 参考 https://dev.classmethod.jp/articles/scp-region-limit/
resource "aws_organizations_policy" "deny_unapproved_regions" {
  name        = "SCP-DenyUnapprovedRegions"
  description = "未承認リージョンでのアクションを拒否する"
  type        = "SERVICE_CONTROL_POLICY"
  content     = file("${path.module}/policies/deny_unapproved_regions.json")
  tags        = var.tags
}

resource "aws_organizations_policy_attachment" "deny_unapproved_regions" {
  policy_id = aws_organizations_policy.deny_unapproved_regions.id
  target_id = data.aws_organizations_organization.this.roots[0].id
}

# 4.主要セキュリティサービスを止めたり削除する操作を全面的に禁止するSCP
resource "aws_organizations_policy" "deny_disable_sec_services" {
  name        = "SCP-DenyDisablingSecurityServices"
  description = "セキュリティサービスを無効化するアクションを拒否する"
  type        = "SERVICE_CONTROL_POLICY"
  content     = file("${path.module}/policies/deny_disable_security_services.json")
  tags        = var.tags
}

resource "aws_organizations_policy_attachment" "deny_disable_sec_services" {
  policy_id = aws_organizations_policy.deny_disable_sec_services.id
  target_id = data.aws_organizations_organization.this.roots[0].id
}

# 5.suspendedアカウントでの全アクション禁止
resource "aws_organizations_policy" "deny_all_suspended" {
  name        = "SCP-DenyAllSuspended"
  description = "suspendedアカウントでの全アクションを拒否する"
  type        = "SERVICE_CONTROL_POLICY"
  content     = file("${path.module}/policies/deny_all_suspended.json")
  tags        = var.tags
}

resource "aws_organizations_policy_attachment" "deny_all_suspended" {
  policy_id = aws_organizations_policy.deny_all_suspended.id
  target_id = local.suspended_ou_id
}

# 6.カスタムSCPの作成・アタッチ
resource "aws_organizations_policy" "addpolicy" {
  for_each    = var.add_scps
  name        = each.key
  description = each.value.description
  type        = "SERVICE_CONTROL_POLICY"
  content     = file("${path.root}/policies/${each.value.file}") # applyした時の/policies/以下のファイル名
  tags        = var.tags
}

resource "aws_organizations_policy_attachment" "addpolicy" {
  for_each  = aws_organizations_policy.addpolicy
  policy_id = each.value.id
  target_id = var.add_scps[each.key].target_id
}


variable.tf
# scp
variable "add_scps" {
  description = "追加で作成・アタッチする SCP の一覧"
  type = map(object({
    description = string
    file        = string # applyした時の/policies/以下のファイル名
    target_id   = string # アタッチ先 OU / Account ID
  }))
  default = {}
}

variable "tags" {
  type    = map(string)
  default = {}
}
policies/deny_all_suspended.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyAllExceptSupportAndReadOnlyMeta",
    "Effect": "Deny",
    "NotAction": [
      "support:*",
      "iam:Get*",
      "iam:List*",
      "organizations:Describe*",
      "cloudtrail:LookupEvents"
    ],
    "Resource": "*"
  }]
}

policies/deny_disable_security_services.jsontf
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisablingSecurityAndAuditServices",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail",
        "config:StopConfigurationRecorder",
        "config:DeleteConfigurationRecorder",
        "config:DeleteDeliveryChannel",
        "config:DeleteConfigRule",
        "guardduty:DeleteDetector",
        "guardduty:DisassociateFromAdministratorAccount",
        "guardduty:DeleteMembers",
        "guardduty:DisassociateMembers",
        "guardduty:UpdateDetector",
        "securityhub:DisableSecurityHub",
        "securityhub:DeleteMembers",
        "securityhub:DisassociateFromAdministratorAccount",
        "securityhub:DisassociateMembers"
      ],
      "Resource": "*"
    }
  ]
}

policies/deny_leaving_org.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyLeavingOrg",
    "Effect": "Deny",
    "Action": [
      "organizations:LeaveOrganization"
    ],
    "Resource": "*"
  }]
}

policies/deny_root.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyRootUser",
    "Effect": "Deny",
    "Action": "*",
    "Resource": "*",
    "Condition": {
      "StringLike": {
        "aws:PrincipalArn": ["arn:aws:iam::*:root"]
      }
    }
  }]
}

policies/deny_unapproved_regions.json
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DenyAllOutsideTokyo",
            "Effect": "Deny",
            "NotAction": [
                "a4b:*",
                "acm:*",
                "aws-marketplace-management:*",
                "aws-marketplace:*",
                "aws-portal:*",
                "awsbillingconsole:*",
                "budgets:*",
                "ce:*",
                "chime:*",
                "cloudfront:*",
                "config:*",
                "cur:*",
                "directconnect:*",
                "ec2:DescribeRegions",
                "ec2:DescribeTransitGateways",
                "ec2:DescribeVpnGateways",
                "fms:*",
                "globalaccelerator:*",
                "health:*",
                "iam:*",
                "importexport:*",
                "kms:*",
                "mobileanalytics:*",
                "networkmanager:*",
                "organizations:*",
                "pricing:*",
                "route53:*",
                "route53domains:*",
                "s3:GetAccountPublic*",
                "s3:ListAllMyBuckets",
                "s3:PutAccountPublic*",
                "shield:*",
                "sts:*",
                "support:*",
                "trustedadvisor:*",
                "waf-regional:*",
                "waf:*",
                "wafv2:*",
                "wellarchitected:*"
            ],
            "Resource": "*",
            "Condition": {
                "StringNotEquals": {
                    "aws:RequestedRegion": "ap-northeast-1"
                },
                "ArnNotLikeIfExists": {
                    "aws:PrincipalArn": [
                        "arn:aws:iam::*:role/Role1AllowedToBypassThisSCP",
                        "arn:aws:iam::*:role/Role2AllowedToBypassThisSCP"
                    ]
                }
            }
        }
    ]
}




ルート呼び出し例

main.tf
module "organizations" {
  source                        = "../modules/organizations"
  enabled_policy_types          = var.enabled_policy_types
  aws_service_access_principals = var.aws_service_access_principals
  additional_ous                = var.additional_ous
  security_account_name         = var.security_account_name
  security_account_email        = var.security_account_email
  member_accounts               = var.member_accounts
  delegated_services            = var.delegated_services
  tags                          = var.tags
}

module "scp" {
  source   = "../modules/scp"
  add_scps = var.add_scps
  tags     = var.tags
}

variable.tf
# Metadata
variable "env" {
  type = string
}

variable "app_name" {
  type = string
}

variable "region" {
  type = string
}

variable "tags" {
  type    = map(string)
  default = {}
}

# Organization
variable "enabled_policy_types" {
  description = "なんのポリシー(scp、tagポリシー等)を有効化するか"
  type        = list(string)
  default = [
    "SERVICE_CONTROL_POLICY",
    "TAG_POLICY"
  ]
}

variable "aws_service_access_principals" {
  description = "サービスアクセスを有効化するリソース指定(guardduty,configなど、組織内で一元管理したいリソース)"
  type        = list(string)
  default = [
    "guardduty.amazonaws.com",
    "config.amazonaws.com",
    "cloudtrail.amazonaws.com",
    "securityhub.amazonaws.com",
    # 必要に応じて追加
  ]
}

# OU作成
variable "additional_ous" {
  description = "追加で作成したいOUのマップ。keyがOU名、valueが親OU名"
  type = map(object({
    parent_ou = string # 親OU名。指定されなければrootにぶら下げる
  }))
  default = {}
}

# Securityアカウント作成
variable "security_account_name" {
  type    = string
  default = "Security"
}

variable "security_account_email" {
  type = string
}

# メンバーアカウント作成
variable "member_accounts" {
  type = map(object({
    name  = string
    email = string
    ou    = string
    tags  = string
  }))
}

# Securityアカウントを委任管理者に登録
variable "delegated_services" {
  description = "Securityアカウントを委任管理者に登録するサービス"
  type        = set(string)
}

# scp
variable "add_scps" {
  description = "追加で作成・アタッチする SCP の一覧"
  type = map(object({
    description = string
    file        = string # applyした時の/policies/以下のファイル名
    target_id   = string # アタッチ先 OU / Account ID
  }))
  default = {}
}
terraform.tfvars
# Metadata
env      = "prod"
app_name = "sample-org"
region   = "ap-northeast-1"
tags = {
  Project = "sample-project"
}

# Organization
enabled_policy_types = [
  "SERVICE_CONTROL_POLICY",
  "TAG_POLICY"
]

aws_service_access_principals = [
  "guardduty.amazonaws.com",
  "config.amazonaws.com",
  "cloudtrail.amazonaws.com",
  "securityhub.amazonaws.com",
  "sso.amazonaws.com"
]

# OU作成(例: Workloads配下に Billing OU を追加)
additional_ous = {
  "billing" = {
    parent_ou = "workloads"
  }
}

# Securityアカウント作成
security_account_name  = "Security"
security_account_email = "security+example@example.com"

# メンバーアカウント作成
member_accounts = {
  dev = {
    name  = "Dev"
    email = "dev+example@example.com"
    ou    = "dev"
    tags  = "Dev"
  }
  network = {
    name  = "Network"
    email = "network+example@example.com"
    ou    = "workloads"
    tags  = "Network"
  }
}

# Securityアカウントを委任管理者に登録
delegated_services = [
  "guardduty.amazonaws.com",
  "config.amazonaws.com",
  "config-multiaccountsetup.amazonaws.com",
  "cloudtrail.amazonaws.com",
  "securityhub.amazonaws.com",
]

# 追加SCP設定例
add_scps = {
  "SCP-DenyDisableCloudTrail" = {
    description = "CloudTrailの停止・削除を禁止"
    file        = "deny_disable_cloudtrail.json"
    target_id   = "ou-xxxx-yyyyzzzz" # 実際のOU IDに置き換え
  }
}

policies/deny_disable_cloudtrail.json(scp例)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisableCloudTrail",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail"
      ],
      "Resource": "*"
    }
  ]
}

実行結果(スクリーンショット)

organizationマネジメントコンソール.png

運用上の注意点など

OrganizationはDestroy不可: prevent_destroy に加え、AWSの制約上もフル自動削除は困難。解体は手動順序で。
・それぞれのアカウントは、メールは未使用で一意である必要あります。(エイリアス可: +label)
・次回のSSO有効化のため、サービスアクセスに "sso.amazonaws.com"を必ず追加してください。

次回予告

ここまでで、AWS Organizations のベースライン構築ができました。
次回は、AWS IAM Identity Center(旧AWS SSO)の有効化と権限割当をTerraformで実装していきます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?