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 |