README

scp

Creates one Service Control Policy and optionally attaches it to a target — matching aws_organizations_policy and aws_organizations_policy_attachment.

Usage

locals {
  scp_deny_root_access = {
    Version = "2012-10-17"
    Statement = [
      {
        Sid      = "DenyRootUser"
        Effect   = "Deny"
        Action   = ["*"]
        Resource = "*"
        Condition = {
          StringLike = {
            "aws:PrincipalArn" = "arn:aws:iam::*:root"
          }
        }
      }
    ]
  }
}

module "scp_deny_root" {
  source = "../hcascm-iac-modules-5b6f30c6dce3/modules/scp"

  service_control_policy = {
    name            = "DenyRootUserAccess"
    description     = "Deny root user access in member accounts"
    policy_document = local.scp_deny_root_access
  }

  target_id = "ou-xxxx"
}

Use one module block per SCP. Set target_id = null to create the policy without attaching.

Import

Use import when an SCP already exists in AWS (console, CLI, or pre-Terraform) and you want Terraform to manage it without recreating p-xxxx. For new policies, use Usage and terraform apply — no import needed.

The import command only needs the policy id, but aws_organizations_policy requires content in configuration (provider docs). Declare what is being adopted (policy document) together with which policy (p-xxxx) and where it is attached (target_id, if any).

What to gather from AWS

# List SCPs
aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
  --query 'Policies[].{Name:Name,Id:Id}' --output table

# Name, description, and policy JSON (convert Content to HCL policy_document)
aws organizations describe-policy --policy-id p-xxxxxxxx \
  --query 'Policy.{Name:PolicySummary.Name,Description:PolicySummary.Description,Content:Content}'

# Attachment target (skip if unattached)
aws organizations list-targets-for-policy --policy-id p-xxxxxxxx \
  --query 'Targets[].{TargetId:TargetId,Type:Type}' --output table
Value Maps to module input
Policy name service_control_policy.name
Description service_control_policy.description
Content JSON service_control_policy.policy_document (as HCL)
Policy id p-xxxx Policy import id
Target ou-xxxx / r-xxxx / account id target_id + attachment import id

Step 1 — Write module config matching AWS

Config must match the existing policy before import. Resource addresses use count, so [0] is required.

locals {
  scp_deny_root_access = {
    Version = "2012-10-17"
    Statement = [
      {
        Sid      = "DenyRootUser"
        Effect   = "Deny"
        Action   = ["*"]
        Resource = "*"
        Condition = {
          StringLike = {
            "aws:PrincipalArn" = "arn:aws:iam::*:root"
          }
        }
      }
    ]
  }
}

module "scp_deny_root" {
  source = "../hcascm-iac-modules-5b6f30c6dce3/modules/scp"

  service_control_policy = {
    name            = "DenyRootUserAccess"
    description     = "Deny root user access"
    policy_document = local.scp_deny_root_access
  }

  target_id = "ou-xxxxxxxx"   # null if not attached
}

Step 2 — Import into state

Always import the policy first. When the SCP is attached in AWS, import the attachment in the same plan / apply (do not import attachment alone).

import {
  to = module.scp_deny_root.aws_organizations_policy.this[0]
  id = "p-xxxxxxxx"
}

# Only when attached in AWS — id format is target_id:policy_id (not p-xxxx:target_id)
import {
  to = module.scp_deny_root.aws_organizations_policy_attachment.this[0]
  id = "ou-xxxxxxxx:p-xxxxxxxx"
}

Examples for other targets: r-xxxx:p-xxxxxxxx (root), 123456789012:p-xxxxxxxx (account).

Step 3 — Plan, apply, verify

terraform plan    # expect import (+ tag updates only)
terraform apply
terraform plan    # expect no changes

Remove import blocks after a successful apply. Keep the module block.

Policy-only vs attached

AWS state target_id Import
Policy only null Policy import only
Policy + attachment ou-xxxx, r-xxxx, or account id Policy + attachment import (target_id:p-xxxx)

Troubleshooting

Plan shows Fix
Policy content change policy_document does not match AWS — align with describe-policy Content
Replace policy name or description mismatch
Create attachment Missing attachment import or wrong target_id
ListTargetsForPolicy / pattern error on import Attachment import id order wrong — use target_id:p-xxxx, not p-xxxx:target_id
Create new policy during attachment import Policy not imported — uncomment/add policy import block
Tag-only changes Accept apply or add matching tags

Requirements

| --------- | --------- | | terraform | >= 1.5 | | aws | ~> 5.0 |

Inputs

Name Description Type Default Required
service_control_policy Single SCP to create object(...) null no
target_id Attachment target (r-xxxx, ou-xxxx, account id) string null no
tags Tags for the policy map(string) {} no

Outputs

Name Description
policy_id SCP policy ID
policy_arn SCP policy ARN
policy_name SCP policy name
attachment_target_id Attached target ID, or null