Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS SQS Terraform Module

Secure-by-default Amazon SQS queue pair — an SSE-encrypted primary queue plus an automatic dead-letter queue, dedicated redrive and redrive-allow policies, and an optional access policy, all from a single composite call. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 📬 Provisions an aws_sqs_queue keystone (this) plus an automatic dead-letter queue (dlq) — a complete, DLQ-backed queue pair from one module call instead of a bare queue a caller has to wire up by hand.
  • 🔁 Dedicated redrive resourcesaws_sqs_queue_redrive_policy and aws_sqs_queue_redrive_allow_policy — used instead of the inline redrive_policy / redrive_allow_policy JSON arguments, per the provider's own documented preference.
  • 🔒 Encryption at rest always on — SSE-SQS (Amazon-managed keys) by default, or SSE-KMS when a customer-managed key ARN is supplied — applied identically to both the primary queue and the DLQ.
  • ☠️ DLQ + redrive ON by default (enable_dlq = true, max_receive_count = 5) — poison messages land somewhere inspectable instead of looping forever or silently expiring.
  • 🎯 byQueue redrive-allow scoping by default — never AWS's own allowAll default — only the ARNs you list (or this queue's own ARN, as a safe fallback) may redrive into the DLQ.
  • 🗓️ DLQ retention defaults to the 14-day maximum (1209600 seconds), longer than the primary queue's 4-day AWS default, per AWS guidance that a DLQ should outlive its source so redriven messages aren't dropped before investigation.
  • 🧵 Supports both standard and FIFO queues via a single fifo_queue switch applied identically to the primary queue and the DLQ, so queue-type parity — an API-enforced requirement — can never drift.
  • 🏷️ Universal taggingvar.tags flows to both queues; tags_all surfaced as an output.

💡 Why it matters: A queue without a DLQ is a queue that silently drops or infinitely retries poison messages — in a regulated FI, that's a lost audit trail on a financial event. This module makes the safe topology (DLQ + redrive + encryption) the path of least resistance, not an opt-in afterthought.


❤️ 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

terraform-aws-sqs is a Phase 2 App Integration & Messaging module. It consumes nothing to function — a queue with no KMS key and no policy is already a complete, secure-by-default deployment — but it wires optionally to terraform-aws-kms for a CMK, and it is a common fan-out target for terraform-aws-sns subscriptions.

flowchart LR
 kms["terraform-aws-kms"]
 sns["terraform-aws-sns"]
 sqs["terraform-aws-sqs"]
 kstream["terraform-aws-kinesis-stream"]
 kfirehose["terraform-aws-kinesis-firehose"]
 msk["terraform-aws-msk"]
 mq["terraform-aws-mq"]

 kms -. "kms_key_arn (SSE-KMS)".-> sns
 kms -. "kms_key_arn (SSE-KMS)".-> sqs
 kms -. "kms_key_arn".-> kstream
 kms -. "kms_key_arn".-> kfirehose
 kms -. "kms_key_arn".-> msk
 kms -. "kms_key_arn".-> mq

 sns -- "subscription (protocol=sqs)" --> sqs

 style sqs fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading

🧬 What this module builds

flowchart TB
 subgraph SQSMOD["terraform-aws-sqs"]
 q["aws_sqs_queue.this<br/>(keystone — primary/source queue)"]
 dlq["aws_sqs_queue.dlq<br/>(for_each keyed on enable_dlq, default ON)"]
 rp["aws_sqs_queue_redrive_policy.this<br/>queue_url = this.id<br/>deadLetterTargetArn = dlq.arn"]
 rap["aws_sqs_queue_redrive_allow_policy.dlq<br/>queue_url = dlq.id<br/>sourceQueueArns (byQueue, default)"]
 pol["aws_sqs_queue_policy.this<br/>queue_url = this.id<br/>(rendered only when queue_policy != null)"]

 q -- "arn" --> rp
 dlq -- "arn" --> rp
 q -- "arn (default source)" --> rap
 dlq -- "queue_url" --> rap
 q -- "queue_url" --> pol
 end

 style q fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading
Resource Role Cardinality
aws_sqs_queue.this Keystone — the primary/source queue 1
aws_sqs_queue.dlq Dead-letter queue, same queue type as this 0–1 (enable_dlq, default true)
aws_sqs_queue_redrive_policy.this Binds this to the DLQ with maxReceiveCount 0–1 (mirrors enable_dlq)
aws_sqs_queue_redrive_allow_policy.dlq Scopes which source queue ARNs may redrive into the DLQ 0–1 (mirrors enable_dlq)
aws_sqs_queue_policy.this Optional caller-supplied JSON access policy on this 0–1 (queue_policy != null)

✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

No provider {} block is declared inside the module — the caller's configured provider (region/credentials) is inherited.


🔑 Required IAM Permissions

Least-privilege actions the Terraform identity needs to create, read, update, and delete everything this module manages:

Action Required for Notes
sqs:CreateQueue, sqs:DeleteQueue Primary queue + DLQ lifecycle Two queues per call when enable_dlq = true (default)
sqs:GetQueueAttributes, sqs:GetQueueUrl Read-back on every plan/apply Drift detection reads queue attributes
sqs:SetQueueAttributes Applying RedrivePolicy, RedriveAllowPolicy, and any other attribute change (retention, visibility timeout, encryption) Both aws_sqs_queue_redrive_policy and aws_sqs_queue_redrive_allow_policy call SetQueueAttributes under the hood — there is no separate IAM action per resource
sqs:TagQueue, sqs:UntagQueue, sqs:ListQueueTags Tagging both queues
sqs:AddPermission, sqs:RemovePermission aws_sqs_queue_policy Only needed when queue_policy is supplied
kms:DescribeKey Validating a caller-supplied CMK Only when kms_key_arn is set
kms:CreateGrant, kms:GenerateDataKey, kms:Decrypt Producers/consumers reading/writing encrypted messages at runtime Only when kms_key_arn is set — these apply to the runtime principals (Lambda, ECS tasks, etc.), not just the Terraform identity, and must also be present in the CMK's key policy

ℹ️ SQS creates no service-linked role, and this module requires no iam:PassRole. A caller-supplied queue_policy may itself grant sqs:SendMessage to a service principal (e.g. sns.amazonaws.com), but granting that in a resource policy does not require the Terraform identity to hold iam:PassRole.


📋 AWS Prerequisites

  • No service-linked role required for SQS.
  • FIFO naming constraint: when fifo_queue = true, name (and dlq_name, if set explicitly) must end in .fifo — enforced by variables.tf validation at plan time, not just a convention.
  • DLQ queue-type parity is API-enforced, not a convention. SQS rejects a redrive policy where the source and dead-letter queue are not both standard or both FIFO. This module always derives the DLQ's fifo_queue from var.fifo_queue, so parity cannot drift.
  • DLQ same-account/same-Region requirement. Per AWS's Developer Guide, the dead-letter queue must be in the same AWS account and Region as its source queue — this module creates both together under one provider, so this is automatically satisfied.
  • Redrive policy propagation is eventually consistentSetQueueAttributes calls (used by both dedicated redrive resources) can take up to 60 seconds to be reflected on all SQS hosts after apply completes.
  • Message retention ceiling and the enqueue-timestamp gotcha: message_retention_seconds maxes out at 1,209,600 seconds (14 days) on both queues. A message's retention clock is based on its original enqueue timestamp and does not reset when the message moves to the DLQ — if the DLQ's retention is shorter than (or too close to) the time already spent on the source queue, a redriven message can expire almost immediately. Set dlq_message_retention_seconds >= message_retention_seconds (enforced by validation, and the module's defaults already satisfy it: 14 days vs. the AWS default 4 days).
  • AWS's own inline-argument default for redrive_allow_policy is allowAll (any queue in the account/Region may target a DLQ) if you set nothing. This module deliberately overrides that default to byQueue, scoped to the source queue's own ARN unless you list others.
  • Don't put a DLQ on a FIFO queue if strict ordering matters — a redrive breaks the original message ordering; AWS's own guidance is to avoid DLQs on FIFO queues where exact order is a hard requirement. Weigh this against enable_dlq = false for such queues.
  • Quotas: 120,000 in-flight (received-but-undeleted) messages per standard queue, 20,000 per FIFO queue (both soft, raisable); queue name length 1–80 characters (.fifo suffix counts toward the limit); redrive_allow_source_queue_arns (byQueue) supports at most 10 ARNs (hard SQS limit, enforced by validation); FIFO throughput is 300 TPS per API action per message group in standard FIFO mode, higher with fifo_throughput_limit = "perMessageGroupId" (high-throughput mode).
  • Region: SQS is a regional, provider-inherited service — no region variable in this module and no us-east-1 global-resource coupling (unlike CloudFront/ACM/WAFv2).

📁 Module Structure

terraform-aws-sqs/
├── providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # name, fifo_queue, queue behavior, encryption, DLQ/redrive, tags, timeouts
├── main.tf # locals (DLQ/policy for_each maps) + aws_sqs_queue.this/.dlq + redrive resources
├── outputs.tf # id + arn, name, url, dlq_id/arn/name (null when enable_dlq=false), tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, emits, gotchas, design decisions

⚙️ Quick Start

module "orders_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name = "orders"

  tags = {
    Environment = "prod"
    CostCenter  = "infra"
  }
}

This one call creates the orders queue, encrypted with SSE-SQS, plus an orders-dlq dead-letter queue with a maxReceiveCount of 5 and a byQueue redrive-allow policy scoped to orders' own ARN — the full secure-by-default topology, no extra wiring required.

⚠️ Pin the source with ?ref=v1.0.0 — never a branch. Wire kms_key_arn from terraform-aws-kms for a customer-managed key, and queue_policy from an aws_iam_policy_document data source for cross-account/service access — see the Example Library.


🔌 Cross-Module Contract

Consumes

Optional, by-reference only — this module does not depend on any sibling module to function:

Input Type Source module
kms_key_arn string (optional, default null) terraform-aws-kms (CMK arn) — omit to use SSE-SQS Amazon-managed encryption
redrive_allow_source_queue_arns list(string) (optional, default []) Other terraform-aws-sqs instances' arn output, or any same-account queue ARN
queue_policy string (optional JSON, default null) Typically rendered from an aws_iam_policy_document data source in the caller's root module

Emits

Output Description Consumed by
id URL of the primary queue (https://sqs.<region>.amazonaws.com/<account-id>/<name>) — note this is a URL, not the ARN App/producer/consumer configuration, Lambda event source mappings
arn ARN of the primary queue (arn:aws:sqs:<region>:<account-id>:<name>) — the cross-resource reference type terraform-aws-iam-policy, terraform-aws-sns (subscription/topic policy), Lambda event source mapping, redrive_allow_source_queue_arns on sibling DLQ modules
name Queue name Tagging, monitoring, dashboards
url Alias of id Same consumers as id, for callers who prefer the explicit name
dlq_id URL of the DLQ (null when enable_dlq = false) Consumer tooling that reads the DLQ directly for replay/inspection
dlq_arn ARN of the DLQ (null when enable_dlq = false) redrive_allow_source_queue_arns on other modules, CloudWatch alarms on DLQ depth
dlq_name Name of the DLQ (null when enable_dlq = false) Tagging, monitoring
tags_all All tags incl. provider default_tags, computed on this Governance/audit

📚 Example Library

1 · Minimal standard queue
module "notifications_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name = "notifications"
}

Creates notifications (SSE-SQS encrypted) plus notifications-dlq with default redrive (max_receive_count = 5) and a byQueue redrive-allow policy scoped to notifications' own ARN.

2 · FIFO queue with content-based deduplication
module "payment_events" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name                        = "payment-events.fifo"
  fifo_queue                  = true
  content_based_deduplication = true
}

The DLQ is automatically named payment-events-dlq.fifo and inherits fifo_queue = true — parity cannot drift.

3 · High-throughput FIFO queue (perMessageGroupId)
module "trade_events" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name                  = "trade-events.fifo"
  fifo_queue            = true
  deduplication_scope   = "messageGroup"
  fifo_throughput_limit = "perMessageGroupId"
}
4 · Tags — merge with provider default_tags
# Caller's provider block owns default_tags; resource tags win on key conflict.
provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = { Owner = "platform", ManagedBy = "terraform" }
  }
}

module "orders_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name = "orders"

  tags = {
    Environment = "prod"
    Owner       = "commerce-team" # overrides default_tags Owner on both queues
  }
}
# module.orders_queue.tags_all => { Owner="commerce-team", ManagedBy="terraform", Environment="prod" }
5 · Customer-managed KMS (SSE-KMS)
module "kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  name   = "casey-sqs"
}

module "npi_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name        = "loan-applications"
  kms_key_arn = module.kms.arn # SSE-KMS on both this and the DLQ; SSE-SQS is dropped
}
6 · DLQ + redrive with a custom max_receive_count
module "flaky_consumer_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name                       = "webhook-deliveries"
  max_receive_count          = 10 # allow more retries before declaring a message poison
  visibility_timeout_seconds = 120
}
7 · Redrive-allow policy scoping multiple source queues
module "shared_dlq" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name       = "shared-poison-queue"
  enable_dlq = false # this queue IS a DLQ; it does not need its own DLQ

  redrive_allow_policy_type = "byQueue"
  redrive_allow_source_queue_arns = [
    module.orders_queue.arn,
    module.payments_queue.arn,
  ]
}

ℹ️ Point sibling terraform-aws-sqs instances' redrive_policy-equivalent (their own enable_dlq = false and a manual aws_sqs_queue_redrive_policy in the root, or use each queue's own DLQ) at module.shared_dlq.arn if a single shared DLQ across teams is the intended topology — see SCOPE.md Design decisions for the tradeoff.

8 · Secure-by-default opt-out — no DLQ (documented exception)
module "ephemeral_test_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name       = "ci-smoke-test"
  enable_dlq = false # opt-out of the secure baseline — document the exception
}

Tradeoff: poison messages now loop up to visibility_timeout_seconds intervals until message_retention_seconds expires, then are silently dropped with no DLQ to inspect. Acceptable for short-lived CI queues; not recommended for production.

9 · Queue access policy (cross-account / service principal)
data "aws_iam_policy_document" "sns_to_sqs" {
  statement {
    sid     = "AllowSnsFanOut"
    effect  = "Allow"
    actions = ["sqs:SendMessage"]

    principals {
      type        = "Service"
      identifiers = ["sns.amazonaws.com"]
    }

    resources = [module.orders_queue.arn]

    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = [module.orders_topic.arn] # terraform-aws-sns
    }
  }

  # !! Version = "2012-10-17" is set automatically by aws_iam_policy_document,
  # but if you hand-roll JSON with jsonencode instead, you MUST include it
  # explicitly or AWS will hang indefinitely creating/updating the policy.
}

module "orders_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name         = "orders"
  queue_policy = data.aws_iam_policy_document.sns_to_sqs.json
}
10 · for_each pattern — multiple queues from a map
locals {
  queues = {
    orders   = { max_receive_count = 5 }
    payments = { max_receive_count = 3 }
    refunds  = { max_receive_count = 5 }
  }
}

module "queues" {
  source   = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"
  for_each = local.queues

  name              = each.key
  max_receive_count = each.value.max_receive_count

  tags = { Domain = each.key }
}

output "queue_arns" {
  value = { for k, m in module.queues : k => m.arn }
}
11 · Reducing empty-receive cost with long polling
module "batch_jobs_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name                       = "batch-jobs"
  receive_wait_time_seconds  = 20  # long polling — fewer empty ReceiveMessage calls
  visibility_timeout_seconds = 900 # sized to the consumer's expected max processing time
}
12 · Temporarily disabling redrive without removing the DLQ (denyAll)
module "orders_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name                      = "orders"
  redrive_allow_policy_type = "denyAll" # DLQ resource stays; no queue may redrive into it
}
13 · Import an existing queue
import {
  to = module.orders_queue.aws_sqs_queue.this
  identity = {
    url = "https://sqs.us-east-1.amazonaws.com/123456789012/orders"
  }
}

module "orders_queue" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"

  name = "orders"
  #... match the existing queue's configuration before running plan/apply
}
14 · End-to-end composition — SNS fan-out to SQS with a Lambda consumer (finale)
module "kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  name   = "casey-messaging"
}

module "orders_topic" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-sns?ref=v1.0.0"
  name        = "orders"
  kms_key_arn = module.kms.arn
}

module "orders_queue" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-sqs?ref=v1.0.0"
  name        = "orders"
  kms_key_arn = module.kms.arn

  queue_policy = data.aws_iam_policy_document.sns_to_sqs.json

  tags = { Environment = "prod", App = "order-processing" }
}

data "aws_iam_policy_document" "sns_to_sqs" {
  statement {
    sid       = "AllowSnsFanOut"
    effect    = "Allow"
    actions   = ["sqs:SendMessage"]
    resources = ["arn:aws:sqs:*:*:orders"] # avoid a cycle on module.orders_queue.arn — see Architecture Notes

    principals {
      type        = "Service"
      identifiers = ["sns.amazonaws.com"]
    }

    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = [module.orders_topic.arn]
    }
  }
}

resource "aws_sns_topic_subscription" "orders_to_queue" {
  topic_arn = module.orders_topic.arn
  protocol  = "sqs"
  endpoint  = module.orders_queue.arn
}

resource "aws_lambda_event_source_mapping" "orders_consumer" {
  event_source_arn = module.orders_queue.arn
  function_name    = module.orders_processor.arn # terraform-aws-lambda (Phase 7)
  batch_size       = 10
}

💡 The queue policy resource is created before the queue's own ARN can be safely self-referenced in some plan orders; using the well-known ARN format (arn:aws:sqs:*:*:orders) or a two-pass apply avoids a dependency cycle. See Architecture Notes for the chicken/egg pattern.


📥 Inputs

ℹ️ High-level grouping — see variables.tf for full heredoc schemas and validation rules:

  • Core: name
  • Queue type (FIFO): fifo_queue, content_based_deduplication, deduplication_scope, fifo_throughput_limit
  • Queue behavior: delay_seconds, max_message_size, message_retention_seconds, receive_wait_time_seconds, visibility_timeout_seconds
  • Encryption: kms_key_arn, kms_data_key_reuse_period_seconds
  • Access policy: queue_policy
  • DLQ / redrive: enable_dlq, dlq_name, max_receive_count, dlq_message_retention_seconds, redrive_allow_policy_type, redrive_allow_source_queue_arns
  • Universal: tags, timeouts

🧾 Outputs

  • Primary: id (queue URL), arn (queue ARN — the cross-resource reference type)
  • Primary-queue extras: name, url (alias of id)
  • DLQ (conditional — try(..., null), null when enable_dlq = false): dlq_id, dlq_arn, dlq_name
  • Tags: tags_all

ℹ️ No outputs are marked sensitive — this module emits no secrets.


🧠 Architecture Notes

  • id/url vs. arn format differ. id and url are the queue URL: https://sqs.<region>.amazonaws.com/<account-id>/<name> — this is what SDKs use for SendMessage/ReceiveMessage and what Lambda event source mappings historically referenced via ARN instead. arn is arn:aws:sqs:<region>:<account-id>:<name> — the cross-resource reference type for IAM policies, SNS subscriptions, KMS grants, and Lambda event source mappings. Never parse one out of the other; use the matching output.
  • Force-new fields: name and fifo_queue are both immutable — SQS has no in-place rename and no standard↔FIFO conversion. Changing either destroys and recreates the primary queue and (since dlq_name/fifo_queue mirror it) typically the DLQ as well. In-flight messages are lost on recreate.
  • tagstags_alldefault_tags: var.tags is written to both aws_sqs_queue.this and aws_sqs_queue.dlq. tags_all (surfaced from this only) is the provider-computed union of resource tags over the provider's default_tags, with resource tags winning on key conflict. default_tags is the caller's provider-block concern — never set inside this module. aws_sqs_queue_policy, aws_sqs_queue_redrive_policy, and aws_sqs_queue_redrive_allow_policy are not taggable.
  • Eventual consistency on redrive propagation. Both dedicated redrive resources call SetQueueAttributes, which can take up to 60 seconds to propagate across all SQS hosts. A message sent immediately after apply may not yet honor the new redrive policy.
  • visibility_timeout_seconds vs. consumer processing time. If a consumer takes longer to process and delete a message than visibility_timeout_seconds (default 30s), SQS makes the message visible again and a second consumer may pick it up — the classic duplicate-processing failure mode. Size this to at least the consumer's expected maximum processing time; the module cannot infer it from a not-yet-existing consumer module.
  • Message retention counts from the original enqueue time, not the redrive time. When SQS moves a message to the DLQ, its retention clock is not reset — a message that spent 3 days on a 4-day-retention source queue arrives at the DLQ with only 1 day left unless the DLQ's own retention is set (and counted) generously. This module's default (dlq_message_retention_seconds = 1209600, the 14-day max) and its validation (dlq_message_retention_seconds >= message_retention_seconds) exist specifically to avoid silently losing a redriven message.
  • Dedicated-resource-vs-inline-argument decision. aws_sqs_queue accepts inline redrive_policy / redrive_allow_policy JSON arguments, but the live provider docs state Terraform "will only perform drift detection of its value when present in a configuration" and that the dedicated resources are preferred. This module uses aws_sqs_queue_redrive_policy and aws_sqs_queue_redrive_allow_policy exclusively — this decouples the redrive relationship's lifecycle from the queue resource itself (toggling enable_dlq does not touch aws_sqs_queue.this's own diff) and keeps main.tf internally consistent with the same dedicated-resource pattern used for aws_sqs_queue_policy.
  • The queue_policy chicken/egg pattern. A policy that must reference "this queue's own ARN" inside a condition scoped to a specific SNS topic is straightforward (module.orders_queue.arn is knowable after the queue is planned). But if the policy document is built before the module call in the same root (as in the finale example), reference the well-known ARN format directly, or split into two applies, to avoid a dependency cycle between the queue and its own policy.
  • No us-east-1 constraint. SQS is a regional service — the module inherits the caller's provider region and declares no region variable.

🧱 Design Principles

Secure-by-default posture and the explicit opt-out for each:

Hardened default Behavior Opt-out / control
Encryption at rest SSE-SQS (Amazon-managed, sqs_managed_sse_enabled = true) when kms_key_arn is null; SSE-KMS (kms_master_key_id) when a CMK is supplied Not fully disable-able — SQS server-side encryption cannot be turned off entirely by this module; the only choice is Amazon-managed vs. customer-managed key
Dead-letter queue + redrive Enabled (enable_dlq = true) with a configurable max_receive_count (default 5) enable_dlq = false — removes the DLQ, redrive policy, and redrive-allow policy entirely (document the exception)
Redrive-allow scoping byQueue — only listed ARNs (defaulting to this queue's own ARN) may redrive into the DLQ redrive_allow_policy_type = "allowAll" — widens blast radius for cross-team queue reuse; not recommended
DLQ message retention Defaults to the maximum, 14 days (1209600 seconds), longer than the source queue's 4-day default Override dlq_message_retention_seconds (validated to stay >= the source queue's retention)
Queue access policy No policy by default (queue_policy = null) — SQS's implicit default (only the queue-owner account may access it) is already deny-by-default for cross-account access Supply queue_policy to open cross-account or service-principal (SNS/EventBridge/S3) access explicitly
Public/cross-account exposure Not possible by accident — SQS has no "public bucket"-equivalent flag; access is entirely policy-driven and this module ships no policy by default N/A — any queue_policy with a wildcard Principal is a reviewable risk

Other principles: the DLQ is modeled as a second aws_sqs_queue inside the same composite (not a separate module call) because a DLQ has no independent lifecycle meaning apart from the queue it protects; fifo_queue is a single module-level switch so queue-type parity between the primary queue and DLQ can never drift; queue_policy is kept as an opaque JSON string (not a deeply-typed object) because IAM/resource policy documents are naturally expressed via aws_iam_policy_document or jsonencode in the caller's root module.


🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires valid AWS credentials (profile / SSO / OIDC) + a region
terraform apply
terraform output

⚠️ plan / apply require valid AWS credentials and a configured region (provider block / AWS_PROFILE / SSO / OIDC). Always pin the module source with ?ref=v1.0.0, never a branch.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + reference integrity.
  • terraform fmt -check — formatting.
  • terraform plan against a sandbox account — confirm the DLQ, redrive policy, and redrive-allow policy all appear when enable_dlq = true (default), and that they disappear cleanly when enable_dlq = false.
  • After apply, confirm arn and dlq_arn are both non-null (when enable_dlq = true) and that tags_all reflects the expected merge with default_tags.
  • Functional test: send more than max_receive_count messages without deleting them from a test queue and confirm they land in the DLQ within the ~60-second propagation window.
  • Destroy test in a throwaway account — SQS has no ENI/NAT-style destroy-ordering hazard, so teardown should be immediate and clean.

💬 Example Output

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:

arn = "arn:aws:sqs:us-east-1:123456789012:orders"
id = "https://sqs.us-east-1.amazonaws.com/123456789012/orders"
url = "https://sqs.us-east-1.amazonaws.com/123456789012/orders"
name = "orders"
dlq_arn = "arn:aws:sqs:us-east-1:123456789012:orders-dlq"
dlq_id = "https://sqs.us-east-1.amazonaws.com/123456789012/orders-dlq"
dlq_name = "orders-dlq"
tags_all = { "Environment" = "prod", "ManagedBy" = "terraform" }

🔍 Troubleshooting

  • terraform apply hangs with "timeout while waiting" on aws_sqs_queue_policy: the supplied queue_policy JSON is missing "Version": "2012-10-17" at the top level. This is a documented AWS behavior, not a Terraform bug — always set Version explicitly when hand-rolling JSON with jsonencode (the aws_iam_policy_document data source sets it automatically).
  • InvalidParameterValue on queue name: fifo_queue = true requires name (and dlq_name, if set) to end in .fifo; fifo_queue = false requires it must not. The module validates this at plan time, but a mismatch introduced via -var overrides or .tfvars will still fail here first.
  • Tag drift / unexpected tag values: Caused by default_tags overlap. tags_all merges resource tags over provider default_tags with resource tags winning — if a value differs from what you set in var.tags, a default_tags entry is colliding on the same key.
  • Credential-chain failures (NoCredentialProviders / ExpiredToken): No valid credentials resolved. Set AWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI. The module never takes credentials as variables.
  • AccessDenied on SetQueueAttributes: Missing the sqs:SetQueueAttributes action — required not just for basic attribute changes but also for both dedicated redrive resources (they call the same API under the hood).
  • us-east-1 / region errors: Not applicable — SQS is a regional, provider-inherited service with no global-resource coupling. If you see a region-related error, check the provider block's own region, not this module.
  • Messages landing in the DLQ sooner than expected, or not retained as long as expected: Remember retention counts from the message's original enqueue time, not from when it was redriven — a message near the source queue's message_retention_seconds limit has correspondingly little time left once it reaches the DLQ. Keep dlq_message_retention_seconds comfortably above message_retention_seconds (the module validates >=, but "equal" leaves zero margin).
  • Duplicate message processing: visibility_timeout_seconds is shorter than the consumer's actual processing time, so SQS re-exposes the message to a second consumer before the first one deletes it. Raise visibility_timeout_seconds to comfortably exceed worst-case processing time.
  • Redrive doesn't seem to be working immediately after apply: SetQueueAttributes-based propagation can take up to ~60 seconds. Retry the test after a short wait before assuming misconfiguration.

🔗 Related Docs

  • Terraform Registry — hashicorp/aws provider: aws_sqs_queue, aws_sqs_queue_policy, aws_sqs_queue_redrive_policy, aws_sqs_queue_redrive_allow_policy
  • AWS — Using dead-letter queues in Amazon SQS (Amazon SQS Developer Guide)
  • AWS — Amazon SQS FIFO queues and high-throughput FIFO queues (Developer Guide)
  • AWS — Amazon SQS server-side encryption (SSE-SQS / SSE-KMS) (Developer Guide)
  • AWS — Amazon SQS quotas (per-Region and per-queue limits)
  • terraform-aws-kms, terraform-aws-sns, terraform-aws-iam-policy (sibling modules)

🧡 "Infrastructure as Code should be standardized, consistent, and secure."