この記事について
AWS を本格的に利用する場合、マルチアカウント戦略は避けて通れません。
この記事では、TerraformでAWS Organizationsのベースラインを一括構築し、OU/アカウント、SCP、までを最小構成で自動化**していきます。
※ SSOの有効化については、次回の記事で扱います。
アーキテクチャ
まずは全体像です。
以下のように OU(Organizational Unit)ごとに用途を整理しています。
AWS公式ドキュメントに則り、下記のような設計をベースとします。
ディレクトリ構成(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はDestroy不可: prevent_destroy に加え、AWSの制約上もフル自動削除は困難。解体は手動順序で。
・それぞれのアカウントは、メールは未使用で一意である必要あります。(エイリアス可: +label)
・次回のSSO有効化のため、サービスアクセスに "sso.amazonaws.com"を必ず追加してください。
次回予告
ここまでで、AWS Organizations のベースライン構築ができました。
次回は、AWS IAM Identity Center(旧AWS SSO)の有効化と権限割当をTerraformで実装していきます。