Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS X-Ray Terraform Module

Composite AWS X-Ray tracing module — trace groups, sampling rules, and the account/Region encryption configuration, all from one composite call, defaulting to a KMS-encrypted trace posture instead of AWS's own out-of-the-box NONE. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 🗂️ Provisions aws_xray_group trace groups — each a named filter expression that segments traces into its own service graph, trace summaries, and CloudWatch metrics, with optional Insights (anomaly detection).
  • 🎯 Provisions aws_xray_sampling_rule entries that tune which requests the X-Ray SDK instruments once the built-in default sampling (first request/sec + 5% of the rest) needs per-service/route control.
  • 🔐 Owns the account/Region aws_xray_encryption_config singleton — and defaults it to type = "KMS" rather than leaving a fresh account at AWS's own NONE default.
  • 🧬 Groups and sampling rules are for_each collections keyed by a caller-supplied stable string — no count, no scalar keystone identity.
  • ⚠️ Encryption config is modeled as a 0/1 toggle, never a collection — the AWS API has exactly one live value per account per Region, so manage_encryption_config (bool) decides whether this module instance owns it.
  • 🏷️ Universal tagging on groups and sampling rules onlyaws_xray_encryption_config has no tagging API at all; it is deliberately excluded from var.tags and from the tags_all output.
  • 🧱 No VPC/networking footprint — X-Ray's group/sampling-rule/encryption-config APIs are pure regional control-plane objects.

💡 Why it matters: A fresh AWS account ships X-Ray encryption at NONE and zero custom sampling rules. One module call establishes a KMS-encrypted, cost-aware, auditable tracing posture — the regulated-industry-aligned default rather than 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-xray consumes only a KMS key ARN (optional) for the encryption-config singleton. It has no direct Terraform-level dependency on VPC, IAM, or compute modules — the workloads that actually emit trace segments (Lambda, ECS, EKS) call the X-Ray data-plane API out-of-band, guided by IAM policies that reference this module's group/rule ARNs.

flowchart LR
 kms["terraform-aws-kms"]
 iamrole["terraform-aws-iam-role"]
 iampolicy["terraform-aws-iam-policy"]
 xray["terraform-aws-xray"]
 lambda["terraform-aws-lambda"]
 ecs["terraform-aws-ecs-service"]
 eks["terraform-aws-eks"]
 cwlg["terraform-aws-cloudwatch-log-group"]
 cwalarm["terraform-aws-cloudwatch-alarm"]
 sns["terraform-aws-sns"]

 kms -. "kms_key_arn (encryption_config CMK)".-> xray
 xray -- "group_arns / sampling_rule_arns" --> iampolicy
 iampolicy -- "xray:PutTraceSegments / GetSamplingRules statements" --> iamrole
 iamrole -- "execution / instance role" --> lambda
 iamrole -- "task role" --> ecs
 iamrole -- "pod / IRSA role" --> eks
 lambda -. "trace segments (data plane, not Terraform)".-> xray
 ecs -. "trace segments (data plane, not Terraform)".-> xray
 eks -. "trace segments (data plane, not Terraform)".-> xray
 xray -. "service-graph metrics".-> cwlg
 xray -. "anomaly alarms".-> cwalarm
 xray -. "insights notifications (EventBridge/Chatbot, out of scope)".-> sns

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

ℹ️ Dotted arrows are by-reference / out-of-band relationships (a KMS ARN wired in, or a data-plane API call that Terraform never sees) — not resources this module creates. This module's only real Terraform-level Consumes input is kms_key_arn.


🧬 What this module builds

Unlike most composites in this library, there is no single scalar keystone. The primary resource (aws_xray_group.this) is itself a for_each collection, exactly like the sibling sampling-rule collection. The only true singleton is the encryption configuration — rendered via a 0/1 for_each toggle, never a caller-keyed map, because the AWS API models it as one value per account per Region.

flowchart TB
 subgraph XRAYMOD["terraform-aws-xray"]
 direction TB

 subgraph GROUPS["Groups — keystone collection (for_each map(object), 0..N)"]
 g1["aws_xray_group.this[key]<br/>id == arn (same value)<br/>tags / tags_all supported"]
 end

 subgraph RULES["Sampling rules — child collection (for_each map(object), 0..N)"]
 r1["aws_xray_sampling_rule.this[key]<br/>id = rule_name (NOT arn)<br/>tags / tags_all supported"]
 end

 subgraph SINGLETON["ACCOUNT / REGION SINGLETON -- 0/1 toggle via manage_encryption_config, NEVER a caller-keyed collection"]
 enc{{"aws_xray_encryption_config.this['this']<br/>id = Region name ONLY<br/>NO arn - NO tags - NO tags_all"}}
 end
 end

 g1 -.->|"group_ids / group_arns / group_names"| OUT(("module outputs"))
 r1 -.->|"sampling_rule_ids / sampling_rule_arns"| OUT
 enc -.->|"encryption_config_id / encryption_config_type"| OUT

 style GROUPS fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
 style RULES fill:#2E7D32,color:#fff,stroke:#1b5e20,stroke-width:2px
 style SINGLETON fill:#455A64,color:#fff,stroke:#eceff1,stroke-width:2px,stroke-dasharray: 6 4
 style enc fill:#607D8B,color:#fff,stroke:#eceff1,stroke-width:2px
Loading
Resource Role Cardinality Tags?
aws_xray_group.this Keystone — trace group per filter expression per groups entry (0..N)
aws_xray_sampling_rule.this Child collection — per-service sampling tuning per sampling_rules entry (0..N)
aws_xray_encryption_config.this Account/Region singleton — 0/1 toggle via manage_encryption_config 0 or 1 ❌ (no tagging API)

✅ 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 (and its Region/credentials) is inherited. No region variable is exposed; X-Ray has no us-east-1 global-service constraint (unlike CloudFront/WAFv2/ACM).


🔑 Required IAM Permissions

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

Action Required for Notes
xray:CreateGroup, xray:UpdateGroup, xray:DeleteGroup, xray:GetGroup, xray:GetGroups aws_xray_group lifecycle
xray:CreateSamplingRule, xray:UpdateSamplingRule, xray:DeleteSamplingRule, xray:GetSamplingRules aws_xray_sampling_rule lifecycle
xray:PutEncryptionConfig, xray:GetEncryptionConfig aws_xray_encryption_config (account/Region singleton) Only exercised when manage_encryption_config = true
xray:TagResource, xray:UntagResource, xray:ListTagsForResource Tagging groups and sampling rules Encryption config has no tagging API — these actions never apply to it
kms:DescribeKey, kms:CreateGrant Customer-managed KMS key supplied via encryption_config.kms_key_arn The CMK's own key policy must separately allow the xray.amazonaws.com service principal to use it

ℹ️ The module itself creates no IAM role and no service-linked role. iam:PassRole is not required — X-Ray does not assume a service role for group/sampling-rule/encryption-config management.


📋 AWS Prerequisites

  • No service-linked role is required for aws_xray_group, aws_xray_sampling_rule, or aws_xray_encryption_config.
  • Account/Region singleton: aws_xray_encryption_config has exactly one live value per account per Region. If more than one Terraform configuration (or a manual console/CLI change) manages it for the same account/Region, the last write wins and state drifts. Only one instance of this module should set manage_encryption_config = true per account/Region.
  • KMS key policy: when encryption_config.type = "KMS" with a customer-managed key, the key's policy must grant the calling principal and X-Ray itself kms:Decrypt, kms:GenerateDataKey, and kms:DescribeKey — otherwise trace ingestion fails silently at the API layer, not at terraform apply.
  • No asymmetric KMS keys. Per AWS documentation, X-Ray does not support asymmetric KMS keys for trace encryption — the supplied CMK must be a symmetric key.
  • Region constraint: none beyond standard regional availability — X-Ray is available in all commercial Regions where the provider is configured. No us-east-1 global-service requirement.
  • Quotas (soft unless noted): 500 groups per account · 2,000 sampling rules per account · 1 encryption configuration per account per Region (hard, by design — the AWS API itself has no concept of more than one).
  • Sampling rule resource_arn wildcard: most callers set resource_arn = "*" (match all resources) unless deliberately narrowing — this is an AWS modeling choice the module passes through verbatim, not an restriction.

📁 Module Structure

terraform-aws-xray/
├── providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # groups, sampling_rules, manage_encryption_config, encryption_config, tags, timeouts
├── main.tf # aws_xray_group.this / aws_xray_sampling_rule.this / aws_xray_encryption_config.this (0/1 toggle)
├── outputs.tf # keyed maps — no scalar id/arn; group_*, sampling_rule_*, encryption_config_*, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, emits, gotchas

⚙️ Quick Start

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

  groups = {
    "checkout-slow" = {
      filter_expression = "responsetime > 5"
    }
  }

  sampling_rules = {
    "checkout-api" = {
      resource_arn   = "*"
      priority       = 100
      fixed_rate     = 0.10
      reservoir_size = 5
      service_name   = "checkout"
      service_type   = "*"
      host           = "*"
      http_method    = "*"
      url_path       = "/checkout/*"
    }
  }

  # manage_encryption_config defaults to true — a fresh account no longer
  # silently ships with X-Ray encryption at AWS's own out-of-the-box "NONE".
  encryption_config = {
    kms_key_arn = module.kms.arn # terraform-aws-kms
  }

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

⚠️ Pin the source with ?ref=v1.0.0 — never a branch. See the Example Library below for manage_encryption_config = false when another module instance already owns the account/Region singleton.


🔌 Cross-Module Contract

Consumes

This module has exactly one by-reference sibling input:

Input Type Source module
encryption_config.kms_key_arn string (KMS key ARN, optional) terraform-aws-kms (arn output) — customer-managed key for aws_xray_encryption_config

No VPC/subnet/security-group wiring is required — X-Ray is a regional control-plane/API service with no network footprint of its own to manage.

Emits

Output Description Consumed by
group_ids Map (key → id) of X-Ray group ARNs — aws_xray_group.id is the group ARN Reference / drift audit
group_arns Map (key → arn) of X-Ray group ARNs (identical value to group_ids) IAM policy Resource elements scoping xray:PutTraceSegments to a group
group_names Map (key → group_name, equal to the input map key) Dashboards, CLI/SDK trace filtering, CloudWatch metric filters
sampling_rule_ids Map (key → id, the rule_name) Reference / drift audit
sampling_rule_arns Map (key → arn) IAM conditions, cross-account sharing
encryption_config_id The encryption config id (the Region name), or null when manage_encryption_config = false Reference / audit only
encryption_config_type The effective encryption type (KMS or NONE), or null when not managed Compliance reporting / audit
tags_all Map of "group/<key>" and "sampling_rule/<key>"tags_all (the only two taggable resource types) Governance/audit

ℹ️ No singular scalar id / arn output exists — the keystone (aws_xray_group.this) is itself a collection, so every output is a map keyed by the caller-supplied stable string used on input.


📚 Example Library

1 · Minimal — single trace group
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  groups = {
    "all-traces" = {
      filter_expression = "" # empty string matches every trace
    }
  }
}
2 · Trace group with Insights (anomaly detection) enabled
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  groups = {
    "checkout-slow" = {
      filter_expression = "responsetime > 5"
      insights_configuration = {
        insights_enabled      = true
        notifications_enabled = true # EventBridge events on anomaly detection
      }
    }
  }
}
3 · Sampling rule — narrow a single service's rate
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  sampling_rules = {
    "checkout-api" = {
      resource_arn   = "*"
      priority       = 100
      fixed_rate     = 0.10
      reservoir_size = 5
      service_name   = "checkout"
      service_type   = "*"
      host           = "*"
      http_method    = "*"
      url_path       = "/checkout/*"
      attributes     = { Environment = "prod" }
    }
  }
}
4 · Multiple sampling rules via for_each — per-route tuning
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  sampling_rules = {
    "checkout-api" = {
      resource_arn = "*", priority = 100, fixed_rate = 0.10, reservoir_size = 5
      service_name = "checkout", service_type = "*", host = "*"
      http_method  = "*", url_path = "/checkout/*"
    }
    "payments-api" = {
      resource_arn = "*", priority = 90, fixed_rate = 0.25, reservoir_size = 10
      service_name = "payments", service_type = "*", host = "*"
      http_method  = "*", url_path = "/payments/*"
    }
    "health-checks" = {
      resource_arn = "*", priority = 200, fixed_rate = 0.0, reservoir_size = 0
      service_name = "*", service_type = "*", host = "*"
      http_method  = "GET", url_path = "/healthz"
    }
  }
}
5 · Multiple trace groups via for_each
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  groups = {
    "all-traces"    = { filter_expression = "" }
    "5xx-errors"    = { filter_expression = "http.response.status >= 500" }
    "checkout-slow" = { filter_expression = "responsetime > 5 AND service(\"checkout\")" }
  }
}
6 · Encryption config wiring a customer-managed KMS key from terraform-aws-kms
module "kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  name   = "casey-xray"
  # key policy must grant xray.amazonaws.com AND the Terraform/workload principal
  # kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey.
}

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

  encryption_config = {
    type        = "KMS"
    kms_key_arn = module.kms.arn
  }
}
7 · Encryption config on AWS-managed key material (no CMK supplied)
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  # type defaults to "KMS"; kms_key_arn defaults to null, which uses
  # AWS-managed key material -- still KMS-backed, still an improvement over
  # AWS's own out-of-the-box "NONE" default, but not a caller-controlled CMK.
  encryption_config = {}
}
8 · 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 "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  groups = {
    "checkout-slow" = {
      filter_expression = "responsetime > 5"
      tags              = { Tier = "checkout" } # merged on top of module tags for this group
    }
  }

  tags = {
    Environment = "prod"
    Owner       = "observability-team" # overrides default_tags Owner on groups/sampling rules
  }
}
# module.xray.tags_all["group/checkout-slow"] =>
# { Owner = "observability-team", ManagedBy = "terraform", Environment = "prod", Tier = "checkout" }
# NOTE: aws_xray_encryption_config has NO tags_all entry at all -- it is not a taggable resource.
9 · Secure-by-default opt-out — type = "NONE" (documented exception)
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  groups = {
    "all-traces" = { filter_expression = "" }
  }

  # Explicit opt-out of the secure baseline -- document the compliance
  # exception before merging. Reverts the account/Region encryption posture
  # to AWS's own out-of-the-box default.
  encryption_config = {
    type = "NONE"
  }
}
10 · Secure-by-default opt-out — manage_encryption_config = false (another instance already owns the singleton)
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  groups = {
    "checkout-slow" = { filter_expression = "responsetime > 5" }
  }

  # A separate Terraform configuration (e.g. a shared-services / landing-zone
  # stack) already manages aws_xray_encryption_config for this account/Region.
  # Setting this module instance to false avoids last-write-wins drift.
  manage_encryption_config = true # <- flip to false in the workload stack
}
# When manage_encryption_config = false:
# encryption_config_id => null
# encryption_config_type => null
11 · Import an existing aws_xray_group (import key = group ARN)
import {
  to = aws_xray_group.this["checkout-slow"]
  id = "arn:aws:xray:us-east-1:123456789012:group/checkout-slow/AbCdEfGh123456"
}
12 · Import an existing aws_xray_sampling_rule (import key = rule name, NOT the ARN)
import {
  to = aws_xray_sampling_rule.this["checkout-api"]
  id = "checkout-api" # the rule_name -- the sampling rule's id, not its arn
}
13 · Import the existing aws_xray_encryption_config singleton (import key = Region name)
import {
  to = aws_xray_encryption_config.this["this"]
  id = "us-east-1" # the Region name -- this resource's only exported attribute
}

ℹ️ Three different import key shapes in one module: group ARN, sampling-rule name, Region name. Get the wrong one and Terraform reports "resource not found" against the wrong lookup.

14 · Insights notifications without enabling encryption management (composability)
module "xray" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-xray?ref=v1.0.0"

  groups = {
    "payments-anomalies" = {
      filter_expression = "service(\"payments\")"
      insights_configuration = {
        insights_enabled      = true
        notifications_enabled = true
      }
    }
  }

  manage_encryption_config = false # owned by the landing-zone stack
}
15 · End-to-end composition — groups + sampling rules + KMS-backed encryption (finale)
module "kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  name   = "casey-xray-traces"
  # Key policy grants xray.amazonaws.com and the observability team's role
  # kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey.
}

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

  groups = {
    "checkout-slow" = {
      filter_expression = "responsetime > 5"
      insights_configuration = {
        insights_enabled      = true
        notifications_enabled = true
      }
    }
    "5xx-errors" = {
      filter_expression = "http.response.status >= 500"
    }
  }

  sampling_rules = {
    "checkout-api" = {
      resource_arn   = "*"
      priority       = 100
      fixed_rate     = 0.10
      reservoir_size = 5
      service_name   = "checkout"
      service_type   = "*"
      host           = "*"
      http_method    = "*"
      url_path       = "/checkout/*"
    }
    "payments-api" = {
      resource_arn   = "*"
      priority       = 90
      fixed_rate     = 0.25
      reservoir_size = 10
      service_name   = "payments"
      service_type   = "*"
      host           = "*"
      http_method    = "*"
      url_path       = "/payments/*"
    }
  }

  encryption_config = {
    type        = "KMS"
    kms_key_arn = module.kms.arn
  }

  tags = {
    Environment = "prod"
    App         = "lending-portal"
  }
}

# Scope IAM policy Resource elements to this module's own group/rule ARNs.
data "aws_iam_policy_document" "xray_write" {
  statement {
    actions   = ["xray:PutTraceSegments", "xray:PutTelemetryRecords"]
    resources = ["*"] # PutTraceSegments has no resource-level permission support
  }
  statement {
    actions   = ["xray:GetSamplingRules", "xray:GetSamplingTargets"]
    resources = values(module.xray.sampling_rule_arns)
  }
}

module "app_policy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-iam-policy?ref=v1.0.0"
  name   = "casey-lending-portal-xray"
  policy = data.aws_iam_policy_document.xray_write.json
}

module "app_role" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
  name   = "casey-lending-portal"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
  managed_policy_arns = [module.app_policy.arn]
}

📥 Inputs

ℹ️ High-level grouping:

  • Groups (keystone collection): groupsmap(object({ filter_expression, insights_configuration?, tags? })), default {}
  • Sampling rules (child collection): sampling_rulesmap(object({ resource_arn, priority, fixed_rate, reservoir_size, service_name, service_type, host, http_method, url_path, attributes?, tags? })), default {}
  • Encryption config (singleton toggle): manage_encryption_config (bool, default true), encryption_configobject({ type = optional("KMS"), kms_key_arn = optional(null) })
  • Universal: tags (flows to groups and sampling rules only), timeouts (declared for interface consistency; not wired to any resource — none of the three resource types expose a configurable timeouts block in the provider schema)

🧾 Outputs

  • Groups: group_ids (== group_arns, since id IS the ARN), group_arns, group_names
  • Sampling rules: sampling_rule_ids (the rule_name, not an ARN), sampling_rule_arns
  • Encryption config (singleton, conditionally present): encryption_config_idtry(..., null), null when manage_encryption_config = false; encryption_config_type — same conditional shape
  • Tags: tags_all — map keyed "group/<key>" / "sampling_rule/<key>"; aws_xray_encryption_config has no tags_all entry at all

ℹ️ No outputs are marked sensitive — this module emits no secrets. No scalar id / arn output exists (see Cross-Module Contract).


🧠 Architecture Notes

  • aws_xray_group.id IS the group ARN. Unlike almost every other AWS resource in this library, the group's id attribute is the full ARN string (arn:aws:xray:<region>:<account>:group/<name>/<hash>), not a short identifier. group_ids and group_arns are both surfaced per the primary-outputs convention, but they carry an identical value — do not expect group_ids to be a bare name.
  • aws_xray_sampling_rule.id is the rule_name — the inverse relationship. Its arn attribute is a separate, full ARN (arn:aws:xray:<region>:<account>:sampling-rule/<rule_name>). This is the mirror image of the group's id/arn behavior — wire sampling_rule_ids where you need the bare name (e.g. AWS CLI --rule-name) and sampling_rule_arns where you need the ARN (IAM conditions, cross-account sharing).
  • aws_xray_encryption_config has no arn, no tags, no tags_all at all. Its only exported attribute is id — the Region name (e.g. "us-east-1"), confirmed against the live hashicorp/aws v6.53.0 provider schema. There is no TagResource-style API for this setting; var.tags is never wired into it and it never appears in the tags_all output.
  • Force-new fields: groups map key (group_name) and sampling_rules map key (rule_name) are both (Required, force-new) on their respective resources — renaming a map key destroys and recreates that group/rule under a new identity. version on sampling rules is hardcoded to 1 (the only format AWS currently supports) rather than exposed as a free-form input.
  • tagstags_alldefault_tags: var.tags flows to aws_xray_group and aws_xray_sampling_rule only, merged with each item's own tags (merge(var.tags, try(each.value.tags, {})), per-item wins). tags_all on each reflects the provider-computed merge with default_tags (resource tags win on key conflict). default_tags remains the caller's provider-block concern and is never set inside the module. aws_xray_encryption_config is excluded from all three — it has no tagging surface whatsoever.
  • Eventual consistency: PutEncryptionConfig is asynchronous at the AWS API layer — the setting transitions UPDATINGACTIVE; a terraform apply immediately followed by a trace-ingestion smoke test can race the update. Groups and sampling rules are effectively synchronous by comparison.
  • No destroy-ordering hazard. All three resources are pure control-plane API objects — no ENI, no NAT, no VPC attachment, no cross-references among aws_xray_group, aws_xray_sampling_rule, and aws_xray_encryption_config. Destroy order between them is irrelevant.
  • The account/Region singleton is sticky at the API layer. Per the provider's own documented behavior: "Removing this resource from Terraform has no effect to the encryption configuration within X-Ray." Running terraform destroy (or removing aws_xray_encryption_config from state) does not revert the account/Region to NONE or to a prior key — the last-applied PutEncryptionConfig call remains in effect at AWS until something else calls the API again. Do not assume a destroy is a rollback; if you need to actually revert the posture, terraform apply a configuration with encryption_config.type = "NONE" first, then remove the resource.
  • No us-east-1 constraint. X-Ray is a regional service — the module inherits the caller's provider Region and declares no region variable, even though the underlying resources support the v6 region argument for multi-Region-from-one-provider patterns (out of scope here; use a provider alias at the call site if needed).

🧱 Design Principles

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

Hardened default Behavior Opt-out / control
Encryption config type "KMS" (customer-managed key when kms_key_arn is supplied; AWS-managed key material under type = "KMS" otherwise) — never left at AWS's own out-of-the-box NONE encryption_config.type = "NONE" explicitly (document the exception)
Encryption config management This module instance manages the account/Region singleton by default (manage_encryption_config = true) so a fresh account does not silently ship at NONE manage_encryption_config = false — skips the singleton entirely; both encryption outputs become null
Insights (anomaly detection) insights_enabled defaults to false per group (matches AWS's own default; enabling costs additional CloudWatch/X-Ray charges) set insights_configuration.insights_enabled = true per group
Insights notifications notifications_enabled defaults to false set true only when insights_enabled = true
Tags tags = optional(map(string), {}) on both taggable resources (groups, sampling rules) not opt-out — tagging is a governance invariant here, not a security control

Other principles: the keystone is a for_each collection (not a scalar) because X-Ray has no single "the group" concept; the encryption config is modeled as a bool toggle + nullable object rather than a map(object(...)) because the AWS API itself has exactly one live value per account per Region — a caller-keyed collection would imply a cardinality that doesn't exist; KMS key creation is deliberately excluded (terraform-aws-kms owns CMK lifecycle, this module only consumes an ARN by reference).


🚀 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 group/sampling-rule counts match your groups/sampling_rules maps, and that the encryption-config resource plans to create (or not) based on manage_encryption_config.
  • After apply, confirm encryption_config_type matches what you set, and poll aws xray get-encryption-config until Status reads ACTIVE (it starts UPDATING).
  • Verify group/sampling-rule ARNs resolve correctly in a downstream IAM policy document before wiring workload roles.
  • Because destroying aws_xray_encryption_config does not revert the account posture, a destroy test in a throwaway sandbox account will not show the encryption setting reverting — this is expected, not a bug in the module.

💬 Example Output

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

Outputs:

group_ids = {
 "checkout-slow" = "arn:aws:xray:us-east-1:123456789012:group/checkout-slow/AbCdEfGh123456"
}
group_arns = {
 "checkout-slow" = "arn:aws:xray:us-east-1:123456789012:group/checkout-slow/AbCdEfGh123456"
}
group_names = { "checkout-slow" = "checkout-slow" }
sampling_rule_ids = { "checkout-api" = "checkout-api" }
sampling_rule_arns = { "checkout-api" = "arn:aws:xray:us-east-1:123456789012:sampling-rule/checkout-api" }
encryption_config_id = "us-east-1"
encryption_config_type = "KMS"
tags_all = {
 "group/checkout-slow" = { "Environment" = "prod", "App" = "lending-portal" }
 "sampling_rule/checkout-api" = { "Environment" = "prod", "App" = "lending-portal" }
}

🔍 Troubleshooting

  • Tag drift / unexpected tags: 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, a default_tags entry is colliding. Remember aws_xray_encryption_config never appears in tags_all at all — it is not missing a tag, it simply cannot be tagged.
  • 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.
  • terraform destroy didn't revert encryption to NONE: Expected. Per the provider's documented behavior, removing aws_xray_encryption_config from Terraform has no effect on the live X-Ray setting — the last PutEncryptionConfig call stays in effect. Apply an explicit type = "NONE" configuration first if you actually need to revert.
  • Two Terraform configurations fighting over encryption_config (constant plan diffs): Only one module instance should set manage_encryption_config = true per account/Region — it is a true API-level singleton with no locking. Set the losing instance's manage_encryption_config = false.
  • Trace ingestion silently fails after switching to a customer-managed KMS key: The failure happens at the AWS API layer, not at terraform apply — check that the CMK's key policy grants both the xray.amazonaws.com service principal and the ingesting workload's role kms:Decrypt / kms:GenerateDataKey / kms:DescribeKey. Also confirm the key is symmetric — X-Ray rejects asymmetric KMS keys.
  • IllegalArgumentException importing a sampling rule: The import key is the rule_name (the resource's id), not its ARN — the reverse of the group import key, which IS the ARN. See Example Library entries 11–13 for all three import key shapes side by side.
  • Encryption config Status stuck at UPDATING: PutEncryptionConfig is asynchronous; poll GetEncryptionConfig (or aws xray get-encryption-config) until it reports ACTIVE before assuming the change failed.
  • UnauthorizedOperation / AccessDeniedException on create: An IAM action from the Required IAM Permissions table is missing — commonly xray:TagResource (forgotten when tags is non-empty) or kms:CreateGrant (customer-managed KMS path).

🔗 Related Docs

  • Terraform Registry — hashicorp/aws provider: aws_xray_group, aws_xray_sampling_rule, aws_xray_encryption_config
  • AWS — AWS X-Ray Developer Guide: configuring sampling, groups, and encryption settings with the X-Ray API
  • AWS — X-Ray encryption configuration (console and API behavior, KMS key requirements)
  • AWS — Using sampling rules with the X-Ray API (reservoir/fixed-rate semantics, default rule behavior)
  • terraform-aws-kms, terraform-aws-iam-role, terraform-aws-iam-policy, terraform-aws-cloudwatch-log-group, terraform-aws-cloudwatch-alarm (sibling modules)

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