Skip to content

microsoftexpert/tf-mod-aws-shield

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS Shield Advanced Terraform Module

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.

Terraform aws module type resources


🧩 Overview

  • 🛡️ 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, or BY_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 to COUNT (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_all is 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 to BLOCK is always an explicit opt-in.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


🗺️ Where this fits in the family

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
Loading

🧬 What this module builds

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
Loading
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_each map): AWS enforces a 1:1 relationship between a protection and a resource_arn. One module call protects one resource; groups and automatic responses layer around it.


✅ Provider / Versions

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.


🔑 Required IAM Permissions

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:PassRole is 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.


📋 AWS Prerequisites

⚠️ COST GATE — READ FIRST. aws_shield_protection and aws_shield_protection_group create-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 via UpdateSubscription), 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 before plan/apply — it is a commercial commitment, not an infrastructure toggle.

  • Subscription is a hard precondition, not a soft warning. terraform validate/fmt succeed offline with no credentials, but apply against a real account fails immediately without an active Shield Advanced subscription. There is no aws_shield_subscription resource — 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/aws v6.53.0, aws_shield_application_layer_automatic_response supports 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 with tf-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 destroy removes the individual protections/groups but does not cancel the underlying subscription or its billing.

📁 Module Structure

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

⚙️ Quick Start

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 — apply fails without it. ⚠️ Always pin the source with ?ref=v1.0.0 — never a branch.


🔌 Cross-Module Contract

Consumes

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.

Emits

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

📚 Example Library

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" }
}

📥 Inputs

ℹ️ 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_groupsmap(object) keyed by stable id; aggregation (SUM/MEAN/MAX), pattern (ALL/ARBITRARY/BY_RESOURCE_TYPE), members, resource_type, per-group tags.
  • Automatic response (optional): automatic_responsesmap(object) keyed by stable name; resource_arn (CloudFront/ALB only), action (COUNT default / BLOCK).
  • Universal: tags.
  • Timeouts: timeouts — applies only to aws_shield_application_layer_automatic_response (create/update/delete).

🧾 Outputs

  • 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 action COUNT/BLOCK (empty when none).
  • automatic_response_resource_arns — map of key → protected resource_arn (empty when none).
  • tags_all — merged resource + provider default_tags.

🧠 Architecture Notes

  • ARN / id formats.
  • Shield protection ARN: arn:<partition>:shield::<account>:protection/<id> — this is the protection object's own ARN (arn output), not the protected resource's ARN. Do not confuse the two: protected_resource_arn echoes the resource being protected.
  • Protection group ARN: arn:<partition>:shield::<account>:protection-group/<protection-group-id>. The provider exposes it as protection_group_arn (not a plain arn) — the module normalizes this into protection_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_arn is FORCE-NEW on aws_shield_protection — changing which resource a protection covers destroys and recreates the protection (a new id/arn is 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_type relationship — this module enforces it with validation {} blocks so a misconfigured group fails at plan, not with an opaque API error at apply: ALL forbids both members and resource_type; ARBITRARY requires members and forbids resource_type; BY_RESOURCE_TYPE requires resource_type and forbids members.
  • action is a closed two-value enum (COUNT/BLOCK) with no "disabled" value. Disabling automatic response means removing the entry from the automatic_responses map entirely, not setting a "none" action — which is exactly why it is modeled as a for_each-keyed map.
  • tagstags_alldefault_tags. var.tags flows to aws_shield_protection and aws_shield_protection_group (both taggable); per-group tags merge over var.tags (per-group wins). tags_all on the keystone is the computed merge of resource tags over the provider's default_tags (resource tags win). aws_shield_application_layer_automatic_response exposes no tags argument at all — it is a pure enable/configure action, not an independently taggable object.
  • Eventual consistency on protection status. CreateProtection returns 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 query shield:DescribeProtection / GetSubscriptionState rather than trusting apply completion alone.
  • Destroy ordering. Automatic responses and ARBITRARY protection-group members must be torn down before the underlying protection. The module wires depends_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 no configuration_aliases — the caller passes whichever provider matches the region the protected resource lives in (CloudFront → us-east-1).

🧱 Design Principles

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, and tf-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_groups and automatic_responses are for_each maps 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.

🚀 Runbook

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/apply fail without an active Shield Advanced subscription — confirm the subscription (and the spend approval) before running them.


🧪 Testing

  • 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_groups entry with pattern = "ARBITRARY" but no members (or pattern = "ALL" with a resource_type) must fail at plan with the module's validation error — not reach apply.
  • Validate the enum: automatic_responses[*].action other than COUNT/BLOCK must fail at plan.
  • terraform plan against a subscribed sandbox account — confirm the keystone protection, any groups, and any responses render as expected and that automatic_response_actions shows COUNT unless BLOCK was set explicitly.
  • Post-apply, confirm protection with aws shield describe-protection and subscription state with aws shield get-subscription-state.

💬 Example Output

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"
}

🔍 Troubleshooting

  • ResourceNotFoundException / subscription-required error on CreateProtection. 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_response is 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.
  • ARBITRARY protection group apply fails referencing a member. members takes the protected resource ARNs (not the aws_shield_protection ARNs), and each member must already have an active protection. Order it with depends_on at the root (see Example 14) — the module's built-in depends_on only covers the keystone's own resource.
  • Pattern/argument validation error at plan. pattern = "ALL" forbids members/resource_type; ARBITRARY requires members; BY_RESOURCE_TYPE requires resource_type. Fix the protection_groups entry to match its pattern.
  • Tag drift from default_tags overlap. A key set in both var.tags and provider default_tags shows the resource value in tags_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 / ExpiredToken on plan/apply means no valid AWS credentials — set AWS_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-1 provider tf-mod-aws-cloudfront uses — pass providers = { aws = aws.us_east_1 }.
  • IAM permission denials. A shield:CreateProtection / shield:CreateProtectionGroup denial means the Terraform identity lacks the action — see Required IAM Permissions. Shield actions are largely account-wide; scope the policy to the CI role.
  • destroy leaves billing running. terraform destroy removes protections/groups but does not cancel the account subscription — unsubscribing requires an AWS Support case.

🔗 Related Docs

  • 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."

Packages

 
 
 

Contributors

Languages