Enrolls a single AWS resource into Shield Advanced DDoS protection and optionally layers a protection group and application-layer (Layer-7) automatic response around it — secure-by-default with COUNT-before-BLOCK. Built for the AWS provider v6.x.
- 🛡️ Enrolls one resource (
aws_shield_protection.this) into AWS Shield Advanced — an ALB/CLB/NLB, CloudFront distribution, Elastic IP, Global Accelerator accelerator, or Route 53 hosted zone, referenced by ARN from a sibling module. - 🧮 Optional protection groups (
aws_shield_protection_group,for_each) that aggregate protected resources (ALL,ARBITRARY, orBY_RESOURCE_TYPE) for more accurate detection and fewer false positives. - 🚦 Optional application-layer automatic response (
aws_shield_application_layer_automatic_response,for_each) for CloudFront distributions and Application Load Balancers — defaults toCOUNT(observe, never block live traffic). - 🔗 Enrollment-only boundary — this module creates zero networking or compute; it wires Shield onto resources built elsewhere in the suite.
- 🏷️ Tags flow to every taggable Shield resource;
tags_allis surfaced for cost-allocation and governance. ⚠️ Requires an active, paid Shield Advanced subscription — every resource here fails to create on an unsubscribed account (see AWS Prerequisites).
💡 Why it matters: DDoS attacks on a member-facing application are an availability risk with regulatory and reputational blast radius. This module makes the observe-first (
COUNT) posture the default, so operators can enable automatic Layer-7 mitigation without risking a false-positive that drops legitimate borrower traffic — a deliberate, reviewed switch toBLOCKis always an explicit opt-in.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
Shield is a pure enrollment layer — it consumes protected-resource ARNs from sibling modules and originates no infrastructure of its own. Automatic Layer-7 response additionally relies on the protected resource already carrying an AWS WAF web ACL (tf-mod-aws-wafv2).
flowchart LR
lb["tf-mod-aws-lb<br/>(ALB / NLB / CLB)"]
cf["tf-mod-aws-cloudfront<br/>(distribution)"]
eip["tf-mod-aws-elastic-ip<br/>(EIP allocation)"]
ga["tf-mod-aws-global-accelerator<br/>(accelerator)"]
r53["tf-mod-aws-route53-zone<br/>(hosted zone)"]
waf["tf-mod-aws-wafv2<br/>(web ACL)"]
shield["tf-mod-aws-shield"]
lb -->|resource arn| shield
cf -->|resource arn| shield
eip -->|resource arn| shield
ga -->|resource arn| shield
r53 -->|resource arn| shield
waf -.->|web ACL enables L7 auto-response| shield
style shield fill:#FF9900,color:#fff
flowchart TD
src["protected resource arn<br/>(ALB / CloudFront / EIP / Global Accelerator / Route 53)"]
subgraph module["tf-mod-aws-shield"]
prot["aws_shield_protection.this<br/>(keystone — one resource_arn)"]
grp["aws_shield_protection_group.this<br/>(for_each, optional)"]
alar["aws_shield_application_layer_automatic_response.this<br/>(for_each, optional — CloudFront / ALB only)"]
end
src -->|resource_arn| prot
prot -.->|depends_on| grp
prot -.->|depends_on| alar
style prot fill:#FF9900,color:#fff
| Resource | Role | Created when |
|---|---|---|
aws_shield_protection.this |
Keystone — enrolls one resource_arn into Shield Advanced |
Always |
aws_shield_protection_group.this |
Aggregates protected resources (for_each) |
protection_groups non-empty |
aws_shield_application_layer_automatic_response.this |
Enables Layer-7 automatic mitigation (for_each) |
automatic_responses non-empty |
ℹ️ The keystone is a single protection (not a
for_eachmap): AWS enforces a 1:1 relationship between a protection and aresource_arn. One module call protects one resource; groups and automatic responses layer around it.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
No configuration_aliases are declared — Shield protection follows the protected resource's home Region/partition, so the caller passes whichever provider (default or an aws.us_east_1 alias) matches the region the protected resource lives in. For CloudFront distributions that means the same us-east-1 provider tf-mod-aws-cloudfront uses.
The Terraform identity needs the following actions. Shield API actions largely operate account-wide (the API does not support resource-level ARN conditions on all actions), so scope the executing role/policy to the CI role assuming this module, not to individual engineers.
| Action | Required for | Notes |
|---|---|---|
shield:CreateProtection, shield:DeleteProtection, shield:DescribeProtection, shield:ListProtections |
aws_shield_protection lifecycle |
— |
shield:CreateProtectionGroup, shield:DeleteProtectionGroup, shield:DescribeProtectionGroup, shield:UpdateProtectionGroup, shield:ListProtectionGroups |
aws_shield_protection_group lifecycle |
— |
shield:EnableApplicationLayerAutomaticResponse, shield:DisableApplicationLayerAutomaticResponse, shield:UpdateApplicationLayerAutomaticResponse |
aws_shield_application_layer_automatic_response lifecycle |
State is read back via shield:DescribeProtection, not a dedicated Describe* call |
shield:TagResource, shield:UntagResource, shield:ListTagsForResource |
Tagging protections and protection groups | — |
shield:DescribeSubscription, shield:GetSubscriptionState |
Preflight — confirm the account is subscribed to Shield Advanced | Recommended CI gate; not called by the module itself |
wafv2:GetWebACLForResource, wafv2:AssociateWebACL |
Verify the WAF web ACL association that automatic response depends on for CloudFront/ALB | AWS-managed; granted for troubleshooting — this module does not create the web ACL |
ℹ️ No
iam:PassRoleis required and no service-linked role is auto-created by these three resources. The SRT/DRT access-role association and proactive engagement are separate account-level Shield settings, out of scope for this module.
⚠️ COST GATE — READ FIRST.aws_shield_protectionandaws_shield_protection_groupcreate-calls fail outright on an unsubscribed account (ResourceNotFoundException/ subscription-required API error) — there is no free or trial-tier path to these resources. Shield Advanced is a paid, account-level subscription billed at a flat US $3,000/month minimum per consolidated-billing family, with a mandatory 1-year commitment (auto-renewing unless explicitly disabled viaUpdateSubscription), plus usage-based data-transfer-out fees on top. Shield Standard (network/transport-layer DDoS protection) is automatic and free for every AWS account and is not what this module manages — this module's entire resource surface exists to layer Shield Advanced on top. In practice Shield Advanced is usually paired with a Business or Enterprise AWS Support plan for the faster DDoS case-response SLAs, but the two are billed separately by AWS — document both spend lines distinctly so Finance approves the correct total. Route this decision through Finance / Risk / Cloud leadership beforeplan/apply— it is a commercial commitment, not an infrastructure toggle.
- Subscription is a hard precondition, not a soft warning.
terraform validate/fmtsucceed offline with no credentials, butapplyagainst a real account fails immediately without an active Shield Advanced subscription. There is noaws_shield_subscriptionresource — subscribing is a one-time billing action taken via the console, CLI, or AWS Organizations/Firewall Manager, and this module intentionally does not model or verify it. - Automatic response is CloudFront + ALB only. As of provider
hashicorp/awsv6.53.0,aws_shield_application_layer_automatic_responsesupports only CloudFront distributions and Application Load Balancers — not NLB, CLB, EIP, Global Accelerator, or Route 53 hosted zones. The Shield API rejects other resource types at apply. The protected resource must also have an associated AWS WAF web ACL for automatic response to function. - Region / us-east-1. Shield Advanced is global/edge-aware, but the protection and protection-group resources are created against whichever provider/region context the caller supplies. For CloudFront distributions the protection is typically managed from
us-east-1(align withtf-mod-aws-cloudfront, which already documents this). No us-east-1 aliasing is baked into this module. - Quotas. Shield Advanced protections are capped per account per resource type by default service quotas (commonly low double digits per type). Request an increase via Service Quotas before bulk-protecting many resources of one type.
- Firewall Manager alternative. For AWS Organizations customers, AWS recommends AWS Firewall Manager to automate Shield Advanced across accounts. Firewall Manager does not cover Route 53 or Global Accelerator protections, so this module remains the correct tool for those two resource types even in an FMS-managed estate.
- Unsubscribing requires an AWS Support case.
terraform destroyremoves the individual protections/groups but does not cancel the underlying subscription or its billing.
tf-mod-aws-shield/
├── providers.tf # required_providers only; no provider {} block, no aliases
├── variables.tf # name, resource_arn, protection_groups, automatic_responses, tags, timeouts
├── main.tf # aws_shield_protection.this + protection_group.this + automatic_response.this
├── outputs.tf # id, arn, name, protected_resource_arn, protection_group_*, automatic_response_*, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, gotchas, design decisions
Smallest working call — protect a single Application Load Balancer:
module "shield_alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn # from tf-mod-aws-lb
tags = {
Environment = "prod"
CostCenter = "1234"
}
}
⚠️ Requires an active Shield Advanced subscription —applyfails without it.⚠️ Always pin the source with?ref=v1.0.0— never a branch.
| Input | Type | Source module |
|---|---|---|
resource_arn |
string (ARN) |
tf-mod-aws-lb (ALB/NLB/CLB arn), tf-mod-aws-cloudfront (distribution arn), tf-mod-aws-elastic-ip (EIP allocation arn), tf-mod-aws-global-accelerator (accelerator arn), tf-mod-aws-route53-zone (hosted zone arn) |
protection_groups[*].members |
list(string) (ARNs) |
Same set of sibling modules — each member must already have an active protection |
automatic_responses[*].resource_arn |
string (ARN) |
tf-mod-aws-cloudfront or tf-mod-aws-lb (ALB) only |
This module originates no networking or compute — it is purely an enrollment/wiring layer over resources built by sibling modules.
| Output | Description | Consumed by |
|---|---|---|
id |
Shield protection object id | Reference/lookup (rarely referenced onward) |
arn |
Shield protection's own ARN (distinct from the protected resource's ARN) | Audit/governance; tf-mod-aws-config / tf-mod-aws-cloudtrail rule scoping |
name |
Friendly protection name | Governance/audit |
protected_resource_arn |
ARN of the resource being protected (echoes resource_arn) |
Cross-reference |
protection_group_ids |
Map of key → protection_group_id |
Audit/governance |
protection_group_arns |
Map of key → protection_group_arn |
Audit/governance |
automatic_response_actions |
Map of key → effective action (COUNT/BLOCK) |
Posture verification |
automatic_response_resource_arns |
Map of key → protected resource_arn |
Cross-reference |
tags_all |
All tags incl. provider default_tags |
Cost-allocation / governance |
1 · Minimal — protect one resource
module "shield" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn
}2 · Protect an Application Load Balancer (wire tf-mod-aws-lb)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
#... ALB configuration...
}
module "shield_alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn # the ALB's ARN — the cross-resource reference
}3 · Protect a CloudFront distribution (us-east-1)
# CloudFront lives in us-east-1; the Shield protection for it is managed from the
# same provider that tf-mod-aws-cloudfront uses.
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
module "cloudfront" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-cloudfront?ref=v1.0.0"
providers = { aws = aws.us_east_1 }
#... distribution configuration...
}
module "shield_cf" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
providers = { aws = aws.us_east_1 }
name = "prod-cloudfront"
resource_arn = module.cloudfront.arn
}4 · Protect an Elastic IP (wire tf-mod-aws-elastic-ip)
module "eip" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-elastic-ip?ref=v1.0.0"
#... EIP configuration...
}
module "shield_eip" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-nat-eip"
resource_arn = module.eip.arn # EIP allocation ARN
}5 · Protection group — ALL pattern
module "shield" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn
# ALL: every protected resource in the account. Forbids members and resource_type.
protection_groups = {
all-resources = {
aggregation = "MAX"
pattern = "ALL"
}
}
}6 · Protection group — ARBITRARY pattern (explicit member list)
module "shield" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn # the keystone protection
# ARBITRARY requires a non-empty members list (the PROTECTED resource ARNs) and
# forbids resource_type. Each member must already have an active protection —
# the module adds depends_on = [aws_shield_protection.this] to the group so the
# keystone's own resource is protected first. Members protected by OTHER module
# calls must exist before this apply (order with depends_on at the root).
protection_groups = {
edge-tier = {
aggregation = "MEAN"
pattern = "ARBITRARY"
members = [module.alb.arn, module.cloudfront.arn]
}
}
}7 · Protection group — BY_RESOURCE_TYPE pattern
module "shield" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-eip"
resource_arn = module.eip.arn
# BY_RESOURCE_TYPE requires resource_type and forbids members.
protection_groups = {
all-eips = {
aggregation = "SUM"
pattern = "BY_RESOURCE_TYPE"
resource_type = "ELASTIC_IP_ALLOCATION"
}
}
}8 · Application-layer automatic response — COUNT (secure default)
module "shield_cf" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-cloudfront"
resource_arn = module.cloudfront.arn
# COUNT is the default — Shield observes and reports what it WOULD block without
# dropping any live traffic. Requires an AWS WAF web ACL on the distribution.
automatic_responses = {
cloudfront = {
resource_arn = module.cloudfront.arn
# action defaults to "COUNT"
}
}
}9 · Application-layer automatic response — BLOCK (deliberate opt-in)
# ⚠️ BLOCK can drop legitimate traffic on a false-positive detection — an
# availability risk for member-facing apps. Switch to BLOCK only after observing
# COUNT-mode behavior and getting sign-off. This is an explicit, per-entry opt-in.
module "shield_alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn
automatic_responses = {
alb = {
resource_arn = module.alb.arn
action = "BLOCK" # OPT-IN: drops requests Shield classifies as attack traffic
}
}
}10 · With tags (merges with provider default_tags)
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
ManagedBy = "Terraform"
Owner = "platform-team"
}
}
}
module "shield" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn
# Resource tags MERGE with default_tags; resource tags win on key conflict.
tags = {
Environment = "prod"
DataClass = "PII"
}
# Per-group tags merge over these (per-group wins):
protection_groups = {
edge = {
aggregation = "MAX"
pattern = "ALL"
tags = { Tier = "edge" }
}
}
}
# module.shield.tags_all == { ManagedBy, Owner, Environment, DataClass }11 · for_each across multiple protected resources
# One module instance per protected resource (the keystone is 1:1 with a resource).
locals {
protected = {
alb = module.alb.arn
cloudfront = module.cloudfront.arn
nat-eip = module.eip.arn
}
}
module "shield" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
for_each = local.protected
name = "prod-${each.key}"
resource_arn = each.value
}12 · Import an existing protection / protection group
# Shield protections import by their protection ID; protection groups by their id.
import {
to = module.shield.aws_shield_protection.this
id = "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"
}
import {
to = module.shield.aws_shield_protection_group.this["edge"]
id = "edge"
}13 · Custom operation timeouts on automatic response
module "shield_cf" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-cloudfront"
resource_arn = module.cloudfront.arn
automatic_responses = {
cloudfront = { resource_arn = module.cloudfront.arn }
}
# Applies ONLY to aws_shield_application_layer_automatic_response (the sole
# resource in this family with timeouts). Defaults are 30m each when unset.
timeouts = {
create = "45m"
update = "45m"
delete = "45m"
}
}14 · End-to-end composition — ALB + CloudFront + protection group + automatic response (finale)
provider "aws" { region = "us-east-1" } # us-east-1 covers both the ALB and CloudFront here
# Upstream resources (built by sibling modules)
module "alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
#... ALB configuration...
}
module "cloudfront" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-cloudfront?ref=v1.0.0"
#... distribution with an associated WAF web ACL (tf-mod-aws-wafv2)...
}
# 1) Protect the ALB, group it with the CloudFront distribution, and turn on
# COUNT-mode automatic response for the ALB.
module "shield_alb" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-alb"
resource_arn = module.alb.arn
protection_groups = {
edge-tier = {
aggregation = "MEAN"
pattern = "ARBITRARY"
members = [module.alb.arn, module.cloudfront.arn]
}
}
automatic_responses = {
alb = {
resource_arn = module.alb.arn
action = "COUNT" # observe first
}
}
tags = { Environment = "prod", DataClass = "PII" }
# The ARBITRARY group references the CloudFront ARN as a member, so its
# protection must exist before this applies:
depends_on = [module.shield_cf]
}
# 2) Protect the CloudFront distribution with BLOCK-mode automatic response
# (its WAF web ACL is already tuned — a deliberate, reviewed opt-in).
module "shield_cf" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-shield?ref=v1.0.0"
name = "prod-cloudfront"
resource_arn = module.cloudfront.arn
automatic_responses = {
cloudfront = {
resource_arn = module.cloudfront.arn
action = "BLOCK"
}
}
tags = { Environment = "prod", DataClass = "PII" }
}ℹ️ High-level grouping below.
- Identity (required):
name(the Shield protection's own name),resource_arn(FORCE-NEW — the resource to protect). - Protection groups (optional):
protection_groups—map(object)keyed by stable id;aggregation(SUM/MEAN/MAX),pattern(ALL/ARBITRARY/BY_RESOURCE_TYPE),members,resource_type, per-grouptags. - Automatic response (optional):
automatic_responses—map(object)keyed by stable name;resource_arn(CloudFront/ALB only),action(COUNTdefault /BLOCK). - Universal:
tags. - Timeouts:
timeouts— applies only toaws_shield_application_layer_automatic_response(create/update/delete).
id— the Shield protection object id.arn— the Shield protection's own ARN (not the protected resource's ARN).name— the friendly protection name.protected_resource_arn— the ARN of the resource being protected.protection_group_ids— map of key →protection_group_id(empty when none).protection_group_arns— map of key →protection_group_arn(empty when none).automatic_response_actions— map of key → effective actionCOUNT/BLOCK(empty when none).automatic_response_resource_arns— map of key → protectedresource_arn(empty when none).tags_all— merged resource + providerdefault_tags.
- ARN / id formats.
- Shield protection ARN:
arn:<partition>:shield::<account>:protection/<id>— this is the protection object's own ARN (arnoutput), not the protected resource's ARN. Do not confuse the two:protected_resource_arnechoes the resource being protected. - Protection group ARN:
arn:<partition>:shield::<account>:protection-group/<protection-group-id>. The provider exposes it asprotection_group_arn(not a plainarn) — the module normalizes this intoprotection_group_arns. - Protection id: a UUID; protection group id: the caller's stable key (1–36 chars of letters, digits, hyphens).
- Force-new / immutable fields.
resource_arnis FORCE-NEW onaws_shield_protection— changing which resource a protection covers destroys and recreates the protection (a newid/arnis issued); Shield does not move protection to a new ARN in place. - Pattern-driven argument legality (protection groups). The provider schema does not cross-validate the
pattern/members/resource_typerelationship — this module enforces it withvalidation {}blocks so a misconfigured group fails at plan, not with an opaque API error at apply:ALLforbids bothmembersandresource_type;ARBITRARYrequiresmembersand forbidsresource_type;BY_RESOURCE_TYPErequiresresource_typeand forbidsmembers. actionis a closed two-value enum (COUNT/BLOCK) with no "disabled" value. Disabling automatic response means removing the entry from theautomatic_responsesmap entirely, not setting a "none" action — which is exactly why it is modeled as afor_each-keyed map.tags↔tags_all↔default_tags.var.tagsflows toaws_shield_protectionandaws_shield_protection_group(both taggable); per-grouptagsmerge overvar.tags(per-group wins).tags_allon the keystone is the computed merge of resource tags over the provider'sdefault_tags(resource tags win).aws_shield_application_layer_automatic_responseexposes notagsargument at all — it is a pure enable/configure action, not an independently taggable object.- Eventual consistency on protection status.
CreateProtectionreturns success synchronously, but Shield's internal detection/mitigation systems take a short time to fully activate. This does not block Terraform; operational validation of "is this resource actually protected" should queryshield:DescribeProtection/GetSubscriptionStaterather than trustingapplycompletion alone. - Destroy ordering. Automatic responses and
ARBITRARYprotection-group members must be torn down before the underlying protection. The module wiresdepends_on = [aws_shield_protection.this]on both child resources so Terraform's implicit graph orders creation and deletion correctly when a group/response references the resource this module protects. - No us-east-1 aliasing is baked in. Unlike
tf-mod-aws-cloudfront/tf-mod-aws-wafv2/tf-mod-aws-acm, this module declares noconfiguration_aliases— the caller passes whichever provider matches the region the protected resource lives in (CloudFront →us-east-1).
Secure-by-default posture — each hardened default and the exact way to relax it:
| Posture | Hardened default | Opt-out |
|---|---|---|
| Automatic-response action | COUNT (observe/report; never blocks live traffic) for every automatic_responses entry |
Set action = "BLOCK" explicitly, per entry — an intentional, visible opt-in, never the map default |
| Protection group creation | Disabled (protection_groups = {}) — opt in per resource set |
Populate protection_groups |
| Automatic response | Disabled (automatic_responses = {}) — no DISABLED action exists |
Add an entry to automatic_responses |
| Tagging | tags wired to every taggable Shield resource for cost-allocation and governance |
Additive — never opt-out; never blocks apply |
Additional principles:
- Enrollment-only boundary. Creates zero networking or compute — it wires Shield onto resources built by
tf-mod-aws-lb,tf-mod-aws-cloudfront,tf-mod-aws-elastic-ip,tf-mod-aws-global-accelerator, andtf-mod-aws-route53-zone, keeping the blast radius to DDoS-protection enrollment. - Single protection per call, plural groups/responses. The keystone protects exactly one resource (mirroring the AWS 1:1 API), while
protection_groupsandautomatic_responsesarefor_eachmaps that layer around it. - Automatic response kept separate from the protection group. AWS models these as independent resources with independent lifecycles; the module mirrors that separation rather than inventing a merged abstraction.
- No account-subscription resource is modeled. There is no
aws_shield_subscription— the subscription check belongs in a CI preflight gate or runbook step, not in Terraform state.
terraform init -backend=false
terraform validate
terraform fmt -check
# plan/apply require valid AWS credentials (profile / SSO / OIDC), a region,
# AND an active Shield Advanced subscription on the target account:
terraform plan
terraform apply
terraform output
⚠️ Pin the module source with?ref=v1.0.0— never a branch.⚠️ plan/applyfail without an active Shield Advanced subscription — confirm the subscription (and the spend approval) before running them.
terraform init -backend=false && terraform validate— offline schema validation (no credentials, no subscription required).terraform fmt -check— formatting gate.- Validate the pattern-driven legality: a
protection_groupsentry withpattern = "ARBITRARY"but nomembers(orpattern = "ALL"with aresource_type) must fail at plan with the module's validation error — not reachapply. - Validate the enum:
automatic_responses[*].actionother thanCOUNT/BLOCKmust fail at plan. terraform planagainst a subscribed sandbox account — confirm the keystone protection, any groups, and any responses render as expected and thatautomatic_response_actionsshowsCOUNTunlessBLOCKwas set explicitly.- Post-apply, confirm protection with
aws shield describe-protectionand subscription state withaws shield get-subscription-state.
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
arn = "arn:aws:shield::111122223333:protection/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"
automatic_response_actions = {
"cloudfront" = "COUNT"
}
id = "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"
name = "prod-cloudfront"
protected_resource_arn = "arn:aws:cloudfront::111122223333:distribution/E2EXAMPLE1234"
protection_group_arns = {
"edge-tier" = "arn:aws:shield::111122223333:protection-group/edge-tier"
}
tags_all = {
"DataClass" = "PII"
"Environment" = "prod"
"ManagedBy" = "Terraform"
}
ResourceNotFoundException/ subscription-required error onCreateProtection. The account is not subscribed to Shield Advanced. This is the cost gate — there is no free tier for these resources. Confirm the subscription is active (aws shield get-subscription-state) and that the spend was approved before retrying.- Automatic response fails with an unsupported resource type.
aws_shield_application_layer_automatic_responseis CloudFront + ALB only. Pointing it at an NLB, CLB, EIP, Global Accelerator, or Route 53 zone is rejected at apply. Also confirm the protected resource has an associated AWS WAF web ACL. ARBITRARYprotection group apply fails referencing a member.memberstakes the protected resource ARNs (not theaws_shield_protectionARNs), and each member must already have an active protection. Order it withdepends_onat the root (see Example 14) — the module's built-independs_ononly covers the keystone's own resource.- Pattern/argument validation error at plan.
pattern = "ALL"forbidsmembers/resource_type;ARBITRARYrequiresmembers;BY_RESOURCE_TYPErequiresresource_type. Fix theprotection_groupsentry to match its pattern. - Tag drift from
default_tagsoverlap. A key set in bothvar.tagsand providerdefault_tagsshows the resource value intags_all(resource wins). A plan that keeps re-adding a tag means the same key is defined in both places with different values — remove the duplicate. - Credential-chain failures.
NoCredentialProviders/ExpiredTokenon plan/apply means no valid AWS credentials — setAWS_PROFILE, refresh SSO, or confirm the OIDC role assumption succeeded in CI. - Region / us-east-1 errors on a CloudFront protection. Manage the CloudFront protection from the same
us-east-1providertf-mod-aws-cloudfrontuses — passproviders = { aws = aws.us_east_1 }. - IAM permission denials. A
shield:CreateProtection/shield:CreateProtectionGroupdenial means the Terraform identity lacks the action — see Required IAM Permissions. Shield actions are largely account-wide; scope the policy to the CI role. destroyleaves billing running.terraform destroyremoves protections/groups but does not cancel the account subscription — unsubscribing requires an AWS Support case.
- Terraform Registry —
aws_shield_protection,aws_shield_protection_group,aws_shield_application_layer_automatic_response - AWS Shield Advanced Developer Guide — subscribing to Shield Advanced, protecting resources, protection groups
- AWS Shield Advanced Developer Guide — application-layer (Layer-7) automatic response and the WAF web ACL requirement
- AWS Firewall Manager — automating Shield Advanced across an AWS Organization
- AWS Service Quotas — Shield Advanced protections per resource type
- module suite —
tf-mod-aws-lb,tf-mod-aws-cloudfront,tf-mod-aws-elastic-ip,tf-mod-aws-global-accelerator,tf-mod-aws-route53-zone,tf-mod-aws-wafv2
🧡 "Infrastructure as Code should be standardized, consistent, and secure."