Skip to content

Latest commit

Β 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🟧 AWS Connect Terraform Module

Provisions a complete Amazon Connect cloud contact center β€” instance, storage configs, hours of operation, queues, security profiles, routing profiles, contact flows, quick connects, the 5-level user hierarchy, agents, claimed phone numbers, and Lambda/Lex/vocabulary integrations β€” secure by default. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • ☎️ One instance, fully wired. Creates aws_connect_instance plus all 16 supporting resource types β€” a working contact center from one module call, not 17 hand-authored resource blocks.
  • πŸŽ™οΈ Recording/transcript storage is first-class. storage_configs covers all 13 valid resource_type values (not the commonly-assumed 6) with a deeply-typed, polymorphic schema across S3 / Kinesis Stream / Kinesis Firehose / Kinesis Video Stream destinations.
  • πŸ” CMK-ready encryption for member-PII streams. CALL_RECORDINGS and CHAT_TRANSCRIPTS storage configs carry a first-class encryption_config β€” wire a customer-managed KMS key from terraform-aws-kms rather than relying on default S3 encryption.
  • πŸ“‹ Audit visibility on by default. contact_flow_logs_enabled defaults to true β€” overriding the provider's own false default β€” because visibility into how a member's call was routed is a compliance control, not a nicety. contact_lens_enabled defaults to true as well.
  • πŸ™ˆ No hardcoded passwords, ever. User passwords are supplied per-user through a dedicated user_passwords map marked sensitive = true β€” never embedded in the main users object, never defaulted.
  • πŸ”€ A resolved Terraform-graph cycle. Amazon Connect allows queues and quick connects to reference each other, but Terraform's static resource graph cannot support both directions in one apply. This module takes queues[*].quick_connect_ids as raw IDs (not module keys) so both real-world wiring directions stay available without a Cycle: error.
  • 🏷️ Tags on every resource that supports them. instance_storage_config, user_hierarchy_structure, phone_number_contact_flow_association, lambda_function_association, and bot_association are the documented exceptions β€” no tags argument exists on those in the current provider schema.

πŸ’‘ Why it matters: Amazon Connect is where member phone calls happen β€” and where their recordings and transcripts, which may contain PII, get stored. Getting the storage-config encryption, audit-logging, and hierarchy story right in one composite module keeps a regulated FI's contact center auditable from day one.


❀️ 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-connect sits above the encryption/storage/streaming/compute layer. It consumes an S3 bucket, a KMS CMK, Kinesis stream/Firehose ARNs, and a Lambda function ARN β€” all by reference β€” and optionally an existing Directory Service directory. It does not touch networking or compute directly; Connect is a fully managed SaaS control plane with no VPC/ENI footprint of its own.

flowchart LR
 s3["terraform-aws-s3-bucket<br/>bucket_name (recordings/transcripts)"]
 kms["terraform-aws-kms<br/>CMK key_id (ARN)"]
 kinesis["terraform-aws-kinesis-stream<br/>stream_arn"]
 firehose["terraform-aws-kinesis-firehose<br/>firehose_arn"]
 lambda["terraform-aws-lambda<br/>function_arn"]
 ds["Directory Service<br/>directory_id (optional)"]
 connect["terraform-aws-connect"]

 s3 -->|"storage_configs.s3_config.bucket_name"| connect
 kms -->|"storage_configs.*.encryption_config.key_id"| connect
 kinesis -->|"storage_configs.kinesis_stream_config.stream_arn"| connect
 firehose -->|"storage_configs.kinesis_firehose_config.firehose_arn"| connect
 lambda -->|"lambda_function_associations.function_arn"| connect
 ds -.->|"directory_id"| connect

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

ℹ️ terraform-aws-kinesis-stream, terraform-aws-kinesis-firehose, and terraform-aws-lambda are Phase 2/7 modules in this repository's roadmap β€” until they exist, wire raw ARNs from wherever those resources are managed.


🧬 What this module builds

flowchart TD
 subgraph mod["terraform-aws-connect"]
 inst["aws_connect_instance.this<br/>(keystone)"]
 store["aws_connect_instance_storage_config.this<br/>for_each storage_configs"]
 hoo["aws_connect_hours_of_operation.this<br/>for_each hours_of_operations"]
 queue["aws_connect_queue.this<br/>for_each queues"]
 secp["aws_connect_security_profile.this<br/>for_each security_profiles"]
 rp["aws_connect_routing_profile.this<br/>for_each routing_profiles"]
 cf["aws_connect_contact_flow.this<br/>for_each contact_flows"]
 cfm["aws_connect_contact_flow_module.this<br/>for_each contact_flow_modules"]
 qc["aws_connect_quick_connect.this<br/>for_each quick_connects"]
 uhs["aws_connect_user_hierarchy_structure.this<br/>guarded for_each"]
 uhg["aws_connect_user_hierarchy_group.this<br/>for_each user_hierarchy_groups"]
 user["aws_connect_user.this<br/>for_each users"]
 phone["aws_connect_phone_number.this<br/>for_each phone_numbers"]
 pnca["aws_connect_phone_number_contact_flow_association.this"]
 lfa["aws_connect_lambda_function_association.this"]
 bot["aws_connect_bot_association.this"]
 vocab["aws_connect_vocabulary.this"]
 end

 inst --> store
 inst --> hoo
 hoo --> queue
 inst --> secp
 queue --> rp
 inst --> cf
 inst --> cfm
 queue --> qc
 cf --> qc
 inst --> uhs
 uhs --> uhg
 rp --> user
 secp --> user
 uhg --> user
 qc --> user
 inst --> phone
 phone --> pnca
 cf --> pnca
 inst --> lfa
 inst --> bot
 inst --> vocab

 style inst fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
 style uhs stroke-dasharray: 5 5
Loading
Resource Count Created when
aws_connect_instance.this 1 always (keystone)
aws_connect_instance_storage_config.this 0..13 one per storage_configs entry
aws_connect_hours_of_operation.this 0..N one per hours_of_operations entry
aws_connect_queue.this 0..N one per queues entry
aws_connect_security_profile.this 0..N one per security_profiles entry
aws_connect_routing_profile.this 0..N one per routing_profiles entry
aws_connect_contact_flow.this 0..N one per contact_flows entry
aws_connect_contact_flow_module.this 0..N one per contact_flow_modules entry
aws_connect_quick_connect.this 0..N one per quick_connects entry
aws_connect_user_hierarchy_structure.this 0 or 1 var.user_hierarchy_structure != null
aws_connect_user_hierarchy_group.this 0..N one per user_hierarchy_groups entry
aws_connect_user.this 0..N one per users entry
aws_connect_phone_number.this 0..N one per phone_numbers entry
aws_connect_phone_number_contact_flow_association.this 0..N one per phone_number_contact_flow_associations entry
aws_connect_lambda_function_association.this 0..N one per lambda_function_associations entry
aws_connect_bot_association.this 0..N one per bot_associations entry
aws_connect_vocabulary.this 0..N one per vocabularies entry

βœ… Provider / Versions

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

The module declares only a required_providers block (providers.tf) and inherits the configured provider. There is no provider {} block and no credential variable β€” credentials resolve through the standard AWS chain at the root/pipeline level (env vars β†’ SSO/shared credentials β†’ assume_role β†’ instance profile / IRSA β†’ OIDC web identity).


πŸ”‘ Required IAM Permissions

Least-privilege actions the Terraform execution identity needs to manage this module.

Action Required for Notes
connect:CreateInstance, connect:DeleteInstance, connect:DescribeInstance, connect:ListInstances, connect:UpdateInstanceAttribute Instance lifecycle 100 combined create/delete per 30 days account-wide
connect:AssociateInstanceStorageConfig, connect:DisassociateInstanceStorageConfig, connect:DescribeInstanceStorageConfig, connect:UpdateInstanceStorageConfig, connect:ListInstanceStorageConfigs Storage configs One call per storage_configs entry
connect:CreateHoursOfOperation, connect:DeleteHoursOfOperation, connect:DescribeHoursOfOperation, connect:UpdateHoursOfOperation, connect:ListHoursOfOperations Hours of operation β€”
connect:CreateQueue, connect:DescribeQueue, connect:UpdateQueue*, connect:AssociateQueueQuickConnects, connect:DisassociateQueueQuickConnects, connect:ListQueues Queues Queues are disabled, not deleted, by the API
connect:CreateSecurityProfile, connect:DeleteSecurityProfile, connect:DescribeSecurityProfile, connect:UpdateSecurityProfile, connect:ListSecurityProfiles Security profiles β€”
connect:CreateRoutingProfile, connect:DescribeRoutingProfile, connect:UpdateRoutingProfile*, connect:ListRoutingProfiles, connect:ListRoutingProfileQueues Routing profiles Routing profiles are disabled, not deleted
connect:CreateContactFlow, connect:DeleteContactFlow, connect:DescribeContactFlow, connect:UpdateContactFlowContent, connect:UpdateContactFlowMetadata, connect:ListContactFlows Contact flows β€”
connect:CreateContactFlowModule, connect:DeleteContactFlowModule, connect:DescribeContactFlowModule, connect:UpdateContactFlowModuleContent, connect:UpdateContactFlowModuleMetadata, connect:ListContactFlowModules Contact flow modules β€”
connect:CreateQuickConnect, connect:DeleteQuickConnect, connect:DescribeQuickConnect, connect:UpdateQuickConnectConfig, connect:UpdateQuickConnectName, connect:ListQuickConnects Quick connects β€”
connect:UpdateUserHierarchyStructure, connect:DescribeUserHierarchyStructure User hierarchy structure No delete API β€” see Architecture Notes
connect:CreateUserHierarchyGroup, connect:DeleteUserHierarchyGroup, connect:DescribeUserHierarchyGroup, connect:UpdateUserHierarchyGroupName, connect:ListUserHierarchyGroups Hierarchy groups β€”
connect:CreateUser, connect:DeleteUser, connect:DescribeUser, connect:UpdateUserIdentityInfo, connect:UpdateUserPhoneConfig, connect:UpdateUserRoutingProfile, connect:UpdateUserSecurityProfiles, connect:UpdateUserHierarchy, connect:ListUsers Users/agents β€”
connect:ClaimPhoneNumber, connect:ReleasePhoneNumber, connect:DescribePhoneNumber, connect:UpdatePhoneNumber, connect:ListPhoneNumbersV2, connect:SearchAvailablePhoneNumbers Phone numbers Shares a 30-day rolling rate-limit family with instance create/delete
connect:AssociateContactFlow, connect:DescribePhoneNumber Phone number contact flow associations β€”
connect:AssociateLambdaFunction, connect:DisassociateLambdaFunction, connect:ListLambdaFunctions, lambda:AddPermission Lambda associations Connect adds an internal invoke permission on association
connect:AssociateBot, connect:DisassociateBot, connect:ListBots, lex:GetBot Lex (V1) bot associations Lex V1 only β€” provider limitation
connect:CreateVocabulary, connect:DeleteVocabulary, connect:DescribeVocabulary, connect:ListVocabularies Custom vocabularies β€”
connect:TagResource, connect:UntagResource, connect:ListTagsForResource Tagging All taggable sub-resources
ds:AuthorizeApplication, ds:DescribeDirectories Existing-directory identity management Only when identity_management_type = "EXISTING_DIRECTORY"

⚠️ No iam:PassRole is needed β€” Connect does not assume a caller-supplied execution role for any resource in this module's scope.


πŸ“‹ AWS Prerequisites

  • identity_management_type and directory_id are effectively immutable. There is no in-place update path for either; changing them destroys and recreates the instance and everything under it. Get this decision right before the first apply.
  • 100 combined instance creations/deletions per 30 days, account-wide. Plan create/destroy cycles deliberately, especially in shared/sandbox accounts.
  • Phone number claim/release shares a related 30-day rolling rate-limit family. Treat phone_numbers changes with the same care as instance lifecycle.
  • Call recordings and chat transcripts are a direct compliance/audit concern. Always pair CALL_RECORDINGS/CHAT_TRANSCRIPTS storage configs with a customer-managed KMS key and a retention-appropriate S3 lifecycle policy β€” this module wires the encryption config but does not own the bucket's lifecycle rules.
  • The User Hierarchy Structure must exist before any hierarchy group β€” enforced in this module via an explicit depends_on.
  • No service-linked role required β€” Connect provisions its own internal service role automatically (exposed as service_role).
  • Service quotas for queues, routing profiles, users, and quick connects per instance β€” check current Amazon Connect service quotas before large rollouts; most are raisable via support case.
  • Amazon Lex (V1) only for aws_connect_bot_association β€” Lex V2 is not supported by any aws_connect_* resource in the current provider.

πŸ“ Module Structure

terraform-aws-connect/
β”œβ”€β”€ providers.tf # required_providers (aws >= 6.0, < 7.0); no provider block
β”œβ”€β”€ variables.tf # instance identity β†’ feature toggles β†’ storage_configs β†’ hours_of_operations
β”‚ # β†’ queues β†’ security_profiles β†’ routing_profiles β†’ users/user_passwords
β”‚ # β†’ hierarchy structure/groups β†’ contact_flows/modules β†’ quick_connects
β”‚ # β†’ phone_numbers/associations β†’ lambda/bot/vocabulary β†’ tags β†’ timeouts
β”œβ”€β”€ main.tf # aws_connect_instance.this + 16 for_each child resources
β”œβ”€β”€ outputs.tf # id + arn + service_role/status + 13 child reference maps + tags_all
β”œβ”€β”€ README.md # this file
└── SCOPE.md # in/out-of-scope, IAM permissions, prerequisites, gotchas

βš™οΈ Quick Start

Smallest working call β€” a Connect-managed instance with no queues/users yet:

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

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

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

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
storage_configs[*].storage_config.s3_config.bucket_name string terraform-aws-s3-bucket
storage_configs[*].storage_config.*.encryption_config.key_id string (KMS key ARN) terraform-aws-kms
storage_configs[*].storage_config.kinesis_stream_config.stream_arn string terraform-aws-kinesis-stream
storage_configs[*].storage_config.kinesis_firehose_config.firehose_arn string terraform-aws-kinesis-firehose
directory_id string (d-xxxxxxxxxx) Directory Service (existing directory)
lambda_function_associations[*].function_arn string terraform-aws-lambda

Emits

Output Description Consumed by
id Connect instance id Any hand-authored aws_connect_* resource outside this module
arn Instance ARN β€” cross-resource reference type IAM policy Resource statements scoping connect:*; phone number target_arn
name instance_alias, or null operator tooling
service_role Connect's own service-linked role ARN audit
status Instance lifecycle state health checks
created_time Instance creation timestamp audit
storage_configs Map: id, association_id, resource_type compliance/audit tooling
hours_of_operations Map: id, hours_of_operation_id, arn, name, tags_all queue wiring outside this module
queues Map: id, queue_id, arn, name, tags_all routing/quick-connect wiring
security_profiles Map: id, security_profile_id, arn, organization_resource_id, name, tags_all user-provisioning tooling
routing_profiles Map: id, routing_profile_id, arn, name, tags_all user-provisioning tooling
contact_flows Map: id, contact_flow_id, arn, name, type, tags_all phone-number-association wiring, quick-connect config
contact_flow_modules Map: id, contact_flow_module_id, arn, name, tags_all flow-authoring tooling
quick_connects Map: id, quick_connect_id, arn, name, tags_all queue quick_connect_ids wiring (post-apply)
users Map: id, user_id, arn, tags_all quick-connect user_config, reporting
user_hierarchy_structure Materialized 5-level structure, or null hierarchy-group tooling
user_hierarchy_groups Map: id, hierarchy_group_id, arn, level_id, name, tags_all user provisioning
phone_numbers Map: id, arn, phone_number, status, tags_all telephony documentation, phone-number-association wiring
vocabularies Map: id, vocabulary_id, arn, state, failure_reason, last_modified_time, tags_all Contact Lens accuracy tooling
tags_all All tags incl. provider default_tags governance/audit

πŸ“š Example Library

1 Β· Minimal Connect-managed instance
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-sandbox-contact-center"
  identity_management_type = "CONNECT_MANAGED"
}
2 Β· Existing Active Directory identity management
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  identity_management_type = "EXISTING_DIRECTORY"
  directory_id             = "d-1234567890" # from your Directory Service directory
}
3 Β· Call recordings + chat transcripts with a customer-managed KMS key (compliance)
module "connect_kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  alias  = "casey/connect-recordings"
}

module "connect_recordings_bucket" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-s3-bucket?ref=v1.0.0"
  bucket = "casey-connect-call-recordings"
}

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

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  storage_configs = {
    call_recordings = {
      resource_type = "CALL_RECORDINGS"
      storage_config = {
        storage_type = "S3"
        s3_config = {
          bucket_name   = module.connect_recordings_bucket.id
          bucket_prefix = "call-recordings"
          encryption_config = {
            key_id = module.connect_kms.arn # CMK β€” auditable/revocable via CloudTrail
          }
        }
      }
    }
    chat_transcripts = {
      resource_type = "CHAT_TRANSCRIPTS"
      storage_config = {
        storage_type = "S3"
        s3_config = {
          bucket_name   = module.connect_recordings_bucket.id
          bucket_prefix = "chat-transcripts"
          encryption_config = {
            key_id = module.connect_kms.arn
          }
        }
      }
    }
  }
}
4 Β· Secure-by-default opt-out β€” disable contact flow logging
module "connect_dev" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-dev-sandbox"
  identity_management_type = "CONNECT_MANAGED"

  # OFF by default is contact_flow_logs_enabled = true β€” only disable for a
  # documented, non-production exception.
  contact_flow_logs_enabled = false
}
5 Β· Hours of operation + queue + routing profile (a working call path)
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  hours_of_operations = {
    business_hours = {
      name      = "Business Hours"
      time_zone = "America/Chicago"
      config = [
        { day = "MONDAY", start_time = { hours = 8, minutes = 0 }, end_time = { hours = 17, minutes = 0 } },
        { day = "TUESDAY", start_time = { hours = 8, minutes = 0 }, end_time = { hours = 17, minutes = 0 } },
        { day = "WEDNESDAY", start_time = { hours = 8, minutes = 0 }, end_time = { hours = 17, minutes = 0 } },
        { day = "THURSDAY", start_time = { hours = 8, minutes = 0 }, end_time = { hours = 17, minutes = 0 } },
        { day = "FRIDAY", start_time = { hours = 8, minutes = 0 }, end_time = { hours = 17, minutes = 0 } },
      ]
    }
  }

  queues = {
    support = {
      name                   = "Member Support"
      hours_of_operation_key = "business_hours"
    }
  }

  routing_profiles = {
    standard = {
      name                       = "Standard"
      description                = "Standard voice + chat routing"
      default_outbound_queue_key = "support"
      media_concurrencies = [
        { channel = "VOICE", concurrency = 1 },
        { channel = "CHAT", concurrency = 3 },
      ]
      queue_configs = [
        { queue_key = "support", channel = "VOICE", priority = 1, delay = 0 },
      ]
    }
  }
}
6 Β· Security profiles + users (agents)
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  #... hours_of_operations / queues / routing_profiles as in example 5...

  security_profiles = {
    agent = {
      name        = "Agent"
      permissions = ["BasicAgentAccess", "OutboundCallAccess"]
    }
  }

  users = {
    jdoe = {
      name                  = "jdoe"
      routing_profile_key   = "standard"
      security_profile_keys = ["agent"]
      identity_info = {
        first_name = "Jane"
        last_name  = "Doe"
        email      = "jdoe@casey-corp.com"
      }
      phone_config = {
        phone_type = "SOFT_PHONE"
      }
    }
  }

  # Never hardcoded β€” supplied from a secrets manager or generated-password module
  user_passwords = {
    jdoe = var.jdoe_initial_password
  }
}
7 Β· 5-level user hierarchy structure + groups
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  user_hierarchy_structure = {
    level_one   = { name = "Region" }
    level_two   = { name = "Site" }
    level_three = { name = "Team" }
  }

  user_hierarchy_groups = {
    central_region = { name = "Central Region" }
    omaha_site     = { name = "Omaha", parent_group_key = "central_region" }
    support_team   = { name = "Support", parent_group_key = "omaha_site" }
  }
}
8 Β· Lambda contact-flow integration
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  lambda_function_associations = {
    id_lookup = {
      function_arn = module.member_id_lookup_lambda.arn # from terraform-aws-lambda, no version/alias qualifier
    }
  }

  contact_flows = {
    main_ivr = {
      name = "Main IVR"
      type = "CONTACT_FLOW"
      content = jsonencode({
        Version     = "2019-10-30"
        StartAction = "12345678-1234-1234-1234-123456789012"
        Actions     = []
      })
    }
  }
}
9 Β· Claim a phone number and bind it to a contact flow
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  contact_flows = {
    main_ivr = {
      name    = "Main IVR"
      content = jsonencode({ Version = "2019-10-30", StartAction = "...", Actions = [] })
    }
  }

  phone_numbers = {
    main_line = {
      country_code = "US"
      type         = "DID"
    }
  }

  phone_number_contact_flow_associations = {
    main_line = { phone_number_key = "main_line", contact_flow_key = "main_ivr" }
  }
}
10 Β· Quick connects (transfer targets) β€” phone, queue, and user
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  #... queues / contact_flows / users as in prior examples...

  quick_connects = {
    supervisor_line = {
      name = "Supervisor"
      quick_connect_config = {
        quick_connect_type = "PHONE_NUMBER"
        phone_config       = { phone_number = "+18005551234" }
      }
    }
    transfer_to_support = {
      name = "Transfer to Support"
      quick_connect_config = {
        quick_connect_type = "QUEUE"
        queue_config       = { queue_key = "support", contact_flow_key = "main_ivr" }
      }
    }
  }

  # Wire the quick connect onto a queue by its RAW ID (post-apply), not by
  # module key β€” see Architecture Notes for why the key-based direction
  # would create a Terraform graph cycle.
  # queues.support.quick_connect_ids = [module.connect.quick_connects["supervisor_line"].quick_connect_id]
}
11 Β· Custom vocabulary for Contact Lens transcription accuracy
module "connect" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  vocabularies = {
    casey_terms = {
      language_code = "en-US"
      content       = "Phrase\tIPA\tSoundsLike\tDisplayAs\ncasey\t\t\tcasey\n"
    }
  }
}
12 Β· Tags (merge with provider default_tags)
# Caller's provider block owns default_tags; the module never sets it.
provider "aws" {
  default_tags { tags = { Owner = "platform", ManagedBy = "terraform" } }
}

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

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  tags = {
    Environment = "prod" # resource tag β€” wins over default_tags on key conflict
    DataClass   = "npi"
  }
}

# module.connect.tags_all == { Owner, ManagedBy, Environment, DataClass }
13 Β· Import an existing instance
import {
  to = module.connect.aws_connect_instance.this
  id = "f1288a1f-6193-445a-b47e-af739b2"
}
14 Β· SAML identity management
module "connect_saml" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-connect?ref=v1.0.0"

  instance_alias           = "casey-saml-contact-center"
  identity_management_type = "SAML"
}
15 Β· End-to-end composition β€” CMK recordings, hierarchy, agents, phone number, Lambda
module "connect_kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  alias  = "casey/connect-recordings"
}

module "connect_recordings_bucket" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-s3-bucket?ref=v1.0.0"
  bucket = "casey-connect-call-recordings"
}

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

  instance_alias           = "casey-member-contact-center"
  identity_management_type = "CONNECT_MANAGED"

  storage_configs = {
    call_recordings = {
      resource_type = "CALL_RECORDINGS"
      storage_config = {
        storage_type = "S3"
        s3_config = {
          bucket_name       = module.connect_recordings_bucket.id
          bucket_prefix     = "call-recordings"
          encryption_config = { key_id = module.connect_kms.arn }
        }
      }
    }
  }

  hours_of_operations = {
    business_hours = {
      name      = "Business Hours"
      time_zone = "America/Chicago"
      config    = [{ day = "MONDAY", start_time = { hours = 8, minutes = 0 }, end_time = { hours = 17, minutes = 0 } }]
    }
  }

  queues = {
    support = { name = "Member Support", hours_of_operation_key = "business_hours" }
  }

  security_profiles = {
    agent = { name = "Agent", permissions = ["BasicAgentAccess", "OutboundCallAccess"] }
  }

  routing_profiles = {
    standard = {
      name                       = "Standard"
      description                = "Standard voice + chat routing"
      default_outbound_queue_key = "support"
      media_concurrencies        = [{ channel = "VOICE", concurrency = 1 }]
      queue_configs              = [{ queue_key = "support", channel = "VOICE", priority = 1, delay = 0 }]
    }
  }

  user_hierarchy_structure = {
    level_one = { name = "Region" }
    level_two = { name = "Site" }
  }
  user_hierarchy_groups = {
    central = { name = "Central Region" }
  }

  users = {
    jdoe = {
      name                  = "jdoe"
      routing_profile_key   = "standard"
      security_profile_keys = ["agent"]
      hierarchy_group_key   = "central"
      identity_info         = { first_name = "Jane", last_name = "Doe", email = "jdoe@casey-corp.com" }
      phone_config          = { phone_type = "SOFT_PHONE" }
    }
  }
  user_passwords = { jdoe = var.jdoe_initial_password }

  contact_flows = {
    main_ivr = { name = "Main IVR", content = jsonencode({ Version = "2019-10-30", StartAction = "...", Actions = [] }) }
  }

  lambda_function_associations = {
    id_lookup = { function_arn = module.member_id_lookup_lambda.arn }
  }

  phone_numbers = {
    main_line = { country_code = "US", type = "DID" }
  }
  phone_number_contact_flow_associations = {
    main_line = { phone_number_key = "main_line", contact_flow_key = "main_ivr" }
  }

  tags = { Environment = "prod", DataClass = "npi" }
}

πŸ“₯ Inputs

Name Type Default Description
instance_alias string null Friendly instance name. Required unless directory_id is set. Effectively FORCE-NEW.
identity_management_type string β€” required SAML | CONNECT_MANAGED | EXISTING_DIRECTORY. Effectively FORCE-NEW.
directory_id string null Existing Directory Service directory id. Required iff identity_management_type = "EXISTING_DIRECTORY". Effectively FORCE-NEW.
inbound_calls_enabled / outbound_calls_enabled bool true Instance-level call direction toggles.
contact_flow_logs_enabled bool true (secure default β€” overrides provider's false) Contact flow execution logs to CloudWatch.
contact_lens_enabled bool true Contact Lens conversational analytics.
auto_resolve_best_voices_enabled bool true Auto-resolve best TTS voices.
early_media_enabled bool true Early media for outbound calls.
multi_party_conference_enabled bool false Multi-party calls/conferencing.
storage_configs map(object({...})) {} 13 valid resource_type values; one of S3/Kinesis Stream/Firehose/Video Stream per entry.
hours_of_operations map(object({...})) {} Business-hours schedules.
queues map(object({...})) {} Contact queues; quick_connect_ids is raw IDs, not module keys (cycle avoidance).
security_profiles map(object({...})) {} Permission sets.
routing_profiles map(object({...})) {} Channel concurrency + queue routing. description is required.
contact_flows map(object({...})) {} IVR/call flows; content or filename+content_hash.
contact_flow_modules map(object({...})) {} Reusable flow modules (no type argument).
quick_connects map(object({...})) {} One-click transfer targets: phone / queue / user.
users map(object({...})) {} Agent accounts. security_profile_keys 1-10 entries.
user_passwords map(string) {} Initial password per user key. sensitive = true.
user_hierarchy_structure object({...}) null Up to 5 levels, top-down.
user_hierarchy_groups map(object({...})) {} Hierarchy nodes; parent_group_key self-references.
phone_numbers map(object({...})) {} Claimed DID/toll-free numbers. All fields FORCE-NEW.
phone_number_contact_flow_associations map(object({...})) {} Binds a claimed number to an inbound flow.
lambda_function_associations map(object({...})) {} Lambda functions invokable from contact flows.
bot_associations map(object({...})) {} Lex (V1) bot associations only.
vocabularies map(object({...})) {} Custom Contact Lens transcription vocabularies (map key = vocabulary name).
tags map(string) {} Tags for every taggable resource in this module.
timeouts object({ create, delete }) {} Instance operation timeouts (no update timeout in the schema).

See variables.tf for full heredoc schemas and validation rules.


🧾 Outputs

Name Description
id Connect instance id.
arn Instance ARN (cross-resource reference type).
name instance_alias, or null.
service_role Connect's own service-linked role ARN.
status Instance lifecycle state.
created_time RFC3339 instance creation timestamp.
storage_configs Map: id, association_id, resource_type.
hours_of_operations Map keyed as var.hours_of_operations.
queues Map keyed as var.queues.
security_profiles Map keyed as var.security_profiles.
routing_profiles Map keyed as var.routing_profiles.
contact_flows Map keyed as var.contact_flows.
contact_flow_modules Map keyed as var.contact_flow_modules.
quick_connects Map keyed as var.quick_connects.
users Map keyed as var.users. Never includes the password.
user_hierarchy_structure Materialized structure, or null.
user_hierarchy_groups Map keyed as var.user_hierarchy_groups.
phone_numbers Map keyed as var.phone_numbers.
vocabularies Map keyed as var.vocabularies.
tags_all All tags incl. provider default_tags (instance only computed set; see Architecture Notes for per-resource tags_all).

ℹ️ instance_storage_config, user_hierarchy_structure (the resource itself), phone_number_contact_flow_association, lambda_function_association, and bot_association have no tags_all β€” not taggable in the current provider schema.


🧠 Architecture Notes

  • ID/ARN formats: the instance id is a bare UUID; arn is arn:aws:connect:<region>:<account>:instance/<id>. Most child resources' id is a composite instance_id:resource_id (colon-separated) β€” the module surfaces the unqualified *_id attribute (e.g. queue_id, routing_profile_id) for cross-references, since that is what the provider's own nested-block arguments expect (e.g. queue_configs.queue_id).
  • Force-new / immutable fields: identity_management_type and directory_id have no update API β€” changing either destroys and recreates the entire instance. country_code, type, prefix, and description are FORCE-NEW on aws_connect_phone_number. type is FORCE-NEW on aws_connect_contact_flow (defaults to CONTACT_FLOW); aws_connect_contact_flow_module has no type argument at all.
  • The queue <-> quick-connect Terraform cycle. Amazon Connect allows a queue to list quick connects (quick_connect_ids) and a quick connect to reference a queue (quick_connect_config.queue_config) simultaneously, but Terraform's resource-level dependency graph cannot support both directions being key-resolved in the same module β€” it fails with a bare Cycle: error regardless of whether the specific IDs involved actually loop. This module takes queues[*].quick_connect_ids as raw ID strings (not var.quick_connects keys) to keep both real-world directions usable, at the cost of the queue-side wiring needing a second apply or an externally-managed quick connect ID.
  • tags ↔ tags_all ↔ default_tags: every in-scope resource except instance_storage_config, user_hierarchy_structure, phone_number_contact_flow_association, lambda_function_association, and bot_association supports tags/tags_all. var.tags flows uniformly to every taggable resource and merges with provider default_tags (resource tags win on key conflict).
  • media_concurrencies and queue_configs on aws_connect_routing_profile are repeatable blocks, not list arguments β€” rendered via dynamic blocks. description is REQUIRED on this resource (unlike most other Connect resources).
  • Eventual consistency / no delete API: aws_connect_user_hierarchy_structure has no delete operation β€” removing it from configuration only stops Terraform from managing it, it does not revert the stored hierarchy. aws_connect_queue and aws_connect_routing_profile are disabled rather than truly deleted by the underlying API; recreating under the same name can transiently fail with a duplicate-name error.
  • Destroy ordering: every child resource references the instance's id/arn, so Terraform destroys all queues/users/phone numbers/etc. before the instance. Destroying the instance can still fail if Connect's own async cleanup (e.g. an in-progress phone-number claim) hasn't settled β€” retry.
  • us-east-1 globals: N/A β€” Amazon Connect has no CloudFront/WAFv2/ACM-style global-resource coupling.

🧱 Design Principles

Secure-by-default posture and every opt-out, explicitly:

Posture Default Opt-out
Contact flow execution logging contact_flow_logs_enabled = true (overrides the provider's own false) false β€” only with a documented exception
Contact Lens analytics contact_lens_enabled = true false
Call-recording / chat-transcript encryption storage_configs[*].storage_config.s3_config.encryption_config is a first-class field; always supply a CMK from terraform-aws-kms for PII-bearing streams omitting encryption_config falls back to S3-default encryption β€” discouraged for PII
Multi-party conferencing multi_party_conference_enabled = false true for a documented conferencing use case
User passwords No hardcoded default; supplied per-key via a dedicated sensitive = true map omit the key entirely to leave a password unmanaged
Phone number claiming Always claimed to this module's own instance via target_arn claim to a different instance/traffic distribution group outside this module

Other principles:

  • One composite, one keystone. The instance owns every resource meaningless without it β€” 16 supporting resource types, all for_each, never count.
  • 13 storage resource types modeled explicitly, not the commonly-assumed 6 β€” the single most compliance-relevant setting in the module.
  • Primary outputs id + arn, plus service_role, status, created_time, and 13 child reference maps.
  • Secrets isolated, not nested. password lives in its own sensitive = true map, mirroring terraform-aws-workmail.
  • A resolved cycle, not a removed feature. Both queue-quick-connect wiring directions stay available; the module picks raw IDs on one side rather than dropping either capability.

πŸš€ Runbook

# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check

plan / apply require valid AWS credentials (profile / SSO / OIDC) resolved through the standard provider chain, plus the IAM actions listed above, and a Region where Amazon Connect is available.

⚠️ Always pin the module source with ?ref=v1.0.0 β€” never a branch.


πŸ§ͺ Testing

  • terraform init -backend=false && terraform validate β€” schema + reference integrity (17 resource types).
  • terraform fmt -check β€” canonical formatting.
  • terraform plan against a sandbox account to confirm the instance, queues, routing profiles, users, and phone numbers materialize as expected β€” watch the 100-instance-per-30-days account limit.
  • Assert module.<name>.arn, storage_configs, and tags_all in your root-module test harness. For user paths, assert user_passwords values never appear in plan output.

πŸ’¬ Example Output

module.connect.aws_connect_instance.this: Creation complete after 2m14s [id=f1288a1f-6193-445a-b47e-af739b2]
module.connect.aws_connect_hours_of_operation.this["business_hours"]: Creation complete
module.connect.aws_connect_queue.this["support"]: Creation complete
module.connect.aws_connect_routing_profile.this["standard"]: Creation complete

Outputs:
arn = "arn:aws:connect:us-east-1:123456789012:instance/f1288a1f-6193-445a-b47e-af739b2"
id = "f1288a1f-6193-445a-b47e-af739b2"
service_role = "arn:aws:iam::123456789012:role/aws-service-role/connect.amazonaws.com/..."
status = "ACTIVE"
tags_all = { "DataClass" = "npi", "Environment" = "prod" }

πŸ” Troubleshooting

Symptom Likely cause Fix
Error: Cycle: aws_connect_quick_connect.this, aws_connect_queue.this,... A queue and a quick connect (directly or via routing profile / user) reference each other Use raw IDs for queues[*].quick_connect_ids (this module's design); never rewire it to look up var.quick_connects by key
terraform apply hangs for several minutes on the instance Normal β€” instance creation is asynchronous Wait; raise timeouts.create if it exceeds 5m
LimitExceededException on instance create/delete Account-wide 100-per-30-days limit reached Wait out the rolling window; avoid churn-heavy CI plan/destroy cycles against a shared account
DuplicateResourceException recreating a queue/routing profile under the same name The API disables rather than deletes; the name is still reserved Wait for AWS's own cleanup, or choose a new name
Storage config conflicts for the same resource_type Only one storage_config entry is allowed per resource_type per instance Ensure storage_configs has at most one map entry per resource_type value
password never appears in terraform show/plan Expected β€” user_passwords is sensitive = true Not an error
Tag drift on every plan A tag also set by provider default_tags with a different value Let resource tags win, or remove the overlap from default_tags
InvalidParameterException on aws_connect_user_hierarchy_group Structure not yet created, or parent_group_key points at a nonexistent key Confirm var.user_hierarchy_structure is set; check the parent key spelling
Credentials/region errors Standard AWS provider chain not resolving, or Region doesn't support Connect Confirm AWS_PROFILE/SSO/OIDC and that the target Region is one of Connect's supported Regions
Phone number claim fails with rate-limit error Shares the 30-day rolling window with instance create/delete Space out phone_numbers changes; avoid bulk claim/release in one apply

πŸ”— Related Docs


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