Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS WorkMail Terraform Module

Provisions an Amazon WorkMail organization together with its registered mail domains, default-domain assignment, users (mailboxes), and groups — secure by default. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 📬 One organization, fully wired. Creates aws_workmail_organization plus its registered domains, the default-domain assignment, users (live mailboxes), and groups — a complete tenant from one module call.
  • 🗂️ Directory-flexible. Supply an existing AWS Directory Service directory_id (Simple AD / AD Connector / Managed Microsoft AD) for domain-joined, auditable production use, or leave it null and let WorkMail auto-provision its own directory for lower-friction setups.
  • 🔐 CMK-ready encryption. kms_key_arn defaults to null (AWS-managed key) but accepts a customer-managed key — recommended for a regulated FI so key access is independently auditable via CloudTrail.
  • 🌐 DNS verification is explicit, not implicit. There is no hosted_zone_id argument on aws_workmail_domain — this module registers the domain and emits the exact DNS records AWS requires; wiring them into Route 53 (e.g. via terraform-aws-route53-zone) is a deliberate, separate step.
  • 🙈 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.
  • 🏷️ Tags on the organization only. aws_workmail_domain, aws_workmail_default_domain, aws_workmail_user, and aws_workmail_group are not taggable in the current provider schema — the documented exception to the universal-tags rule (same shape as terraform-aws-iam-group).

💡 Why it matters: WorkMail mailboxes are live, billed resources the moment they're created, and an organization can't be destroyed cleanly while mail objects still exist underneath it. Getting the directory, encryption, and domain-verification story right in one composite module keeps a regulated FI's email tenant auditable and its destroy path safe.


❤️ 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-workmail sits just above the identity/directory layer. It optionally consumes a Directory Service directory_id and a KMS CMK arn, and it feeds DNS-verification records forward to Route 53. It does not touch networking, compute, or storage modules.

flowchart LR
 ds["terraform-aws-directory-service (planned)<br/>directory_id (optional)"]
 kms["terraform-aws-kms<br/>CMK arn"]
 r53["terraform-aws-route53-zone<br/>DNS verification records"]
 idc["terraform-aws-iam-identity-center<br/>SSO user id (optional)"]
 wm["terraform-aws-workmail"]

 ds -->|"directory_id (optional)"| wm
 kms -->|"kms_key_arn (optional CMK)"| wm
 wm -->|"domains[*].records"| r53
 idc -.->|"identity_provider_user_id"| wm

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

ℹ️ terraform-aws-directory-service is not yet part of this repository's authored catalog — until it exists, supply an existing directory id from wherever your Directory Service directory is managed, or leave directory_id = null and let WorkMail self-provision.


🧬 What this module builds

flowchart TD
 subgraph mod["terraform-aws-workmail"]
 org["aws_workmail_organization.this<br/>(keystone)<br/>alias + directory + kms + interop"]
 dom["aws_workmail_domain.this<br/>for_each domains"]
 defdom["aws_workmail_default_domain.this<br/>guarded for_each"]
 usr["aws_workmail_user.this<br/>for_each users"]
 grp["aws_workmail_group.this<br/>for_each groups"]
 end

 org --> dom
 org --> defdom
 dom --> defdom
 org --> usr
 org --> grp

 style org fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
 style defdom stroke-dasharray: 5 5
Loading
Resource Count Created when
aws_workmail_organization.this 1 always (keystone)
aws_workmail_domain.this 0..N one per domains entry
aws_workmail_default_domain.this 0 or 1 var.default_domain != null
aws_workmail_user.this 0..N one per users entry
aws_workmail_group.this 0..N one per groups 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
workmail:CreateOrganization, workmail:DeleteOrganization, workmail:DescribeOrganization, workmail:ListOrganizations Organization lifecycle Core CRUD
workmail:RegisterMailDomain, workmail:DeleteMailDomain, workmail:DescribeMailDomains, workmail:GetMailDomain Domain registration One call per domains entry
workmail:GetMailDomain Default-domain assignment Reads verification state
workmail:CreateUser, workmail:DeleteUser, workmail:DescribeUser, workmail:ListUsers, workmail:RegisterToWorkMail, workmail:DeregisterFromWorkMail, workmail:ResetPassword, workmail:UpdateUser User/mailbox lifecycle RegisterToWorkMail is what starts billing
workmail:CreateGroup, workmail:DeleteGroup, workmail:DescribeGroup, workmail:ListGroups, workmail:UpdateGroup Group lifecycle —
workmail:TagResource, workmail:UntagResource, workmail:ListTagsForResource Tagging The organization is the only taggable resource here
ds:CreateDirectory, ds:DeleteDirectory, ds:DescribeDirectories, ds:AuthorizeApplication, ds:UnauthorizeApplication Directory lifecycle Needed whenever directory_id = null (auto-provisioned) or delete_directory = true
kms:CreateGrant, kms:DescribeKey CMK-encrypted organization Only when kms_key_arn is supplied
sso:DeleteApplication, sso:DescribeApplication Identity Center cleanup Only when delete_identity_center_application = true

⚠️ No iam:PassRole is needed — WorkMail calls Directory Service directly under the executing principal's own permissions, not via an assumed service role.

🔒 Scope workmail:* actions to the organization's ARN once it exists, and gate ds:CreateDirectory / ds:DeleteDirectory separately if your org standard requires directories to be provisioned only by a dedicated directory-service pipeline.


📋 AWS Prerequisites

  • Every organization requires an underlying directory. Supply an existing Simple AD / AD Connector / Managed Microsoft AD directory_id, or leave it null to let WorkMail auto-provision and manage its own Simple AD directory. The auto-provisioned directory is not a Terraform-managed resource either way — it is only removed on destroy when delete_directory = true.
  • Domain verification requires manual/out-of-band DNS records. After registering a domain, read domains[*].records (MX, TXT/SPF, CNAME-DKIM) and create matching records in the domain's Route 53 hosted zone (or external DNS). ownership_verification_status / dkim_verification_status stay PENDING until those records propagate and AWS re-checks them — this module cannot force or await that check.
  • WorkMail is a regional service available in a limited set of Regions — confirm the target Region supports WorkMail before use. There is no us-east-1-style global constraint here, but availability is narrower than most services.
  • Costs accrue per registered mailbox the moment aws_workmail_user is created (RegisterToWorkMail runs on create). Size the users map deliberately.
  • Service quotas: default 1 organization per account/Region (soft, raisable); domains/users/groups scale per current WorkMail service quotas.

📁 Module Structure

terraform-aws-workmail/
├── providers.tf # required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # organization_alias → optional org config → domains/default_domain → users/user_passwords → groups → tags → timeouts
├── main.tf # aws_workmail_organization.this + domain / default_domain / user / group for_each
├── outputs.tf # id + arn + name + state + domains/users/groups maps + tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM permissions, prerequisites, gotchas

⚙️ Quick Start

Smallest working call — a self-provisioned directory, AWS-managed encryption, one mailbox:

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

  organization_alias = "casey-corp"

  users = {
    jdoe = {
      name         = "jdoe"
      display_name = "Jane Doe"
      email        = "jdoe@casey-corp.awsapps.com"
    }
  }

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

🔌 Cross-Module Contract

Consumes

Input Type Source module
directory_id string (Directory Service directory id, d-xxxxxxxxxx) terraform-aws-directory-service (or any existing directory)
kms_key_arn string (KMS key ARN) terraform-aws-kms
users[*].identity_provider_user_id string (Identity Center user id) terraform-aws-iam-identity-center

Emits

Output Description Consumed by
id Organization id (m-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) references / CLI
arn Organization ARN — cross-resource reference type IAM policy Resource statements scoping workmail:*
name The organization alias operator tooling
state Organization lifecycle state health checks / readiness gates
default_mail_domain Auto-provisioned <alias>.awsapps.com domain (or configured default) reference / fallback addressing
directory_type Type of the associated directory audit
completed_date RFC3339 timestamp the org became active audit
migration_admin Migration admin user id, when migration is enabled migration tooling
domains Map: id, domain_name, records, dkim_verification_status, ownership_verification_status, is_default, is_test_domain terraform-aws-route53-zone (records), DNS/verification tooling
default_domain_name The domain set as default, or null reference
users Map: id (user_id), name, email, state, lifecycle timestamps mailbox provisioning tooling, Identity Center wiring
groups Map: id (group_id), name, email, state, lifecycle timestamps distribution-list tooling
tags_all All tags incl. provider default_tags — organization only governance/audit

📚 Example Library

1 · Minimal organization (self-provisioned directory, no domains/users)
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-sandbox"
}
2 · Existing Directory Service directory (production, auditable)
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"
  directory_id       = "d-1234567890" # existing Managed Microsoft AD / Simple AD / AD Connector
}
3 · Customer-managed KMS key (recommended for a regulated FI, wired from terraform-aws-kms)
module "workmail_kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  alias  = "casey/workmail"
}

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

  organization_alias = "casey-corp"
  kms_key_arn        = module.workmail_kms.arn # CMK — auditable/revocable via CloudTrail
}
4 · Secure-by-default opt-out — auto-delete the directory and Identity Center app on destroy
module "workmail_ephemeral" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-dev-ephemeral"

  # OFF by default (directory/app survive destroy) — only safe here because
  # this organization exclusively owns its auto-provisioned directory.
  delete_directory                   = true
  delete_identity_center_application = true
}
5 · Register additional mail domains (multi-domain)
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"

  domains = {
    primary   = { domain_name = "casey-corp.com" }
    marketing = { domain_name = "casey-marketing.com" }
  }
}

# Read the required DNS records and wire them into terraform-aws-route53-zone:
output "workmail_domain_records" {
  value = module.workmail.domains
}
6 · Set the default mail domain (in-place updatable)
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"

  domains = {
    primary = { domain_name = "casey-corp.com" }
  }

  default_domain = "primary" # must be a key in var.domains
}
7 · Bulk users via for_each-friendly map
locals {
  onboarding_users = {
    jdoe   = { name = "jdoe", display_name = "Jane Doe", email = "jdoe@casey-corp.com", department = "Engineering" }
    bsmith = { name = "bsmith", display_name = "Bob Smith", email = "bsmith@casey-corp.com", department = "Finance" }
    akim   = { name = "akim", display_name = "Amy Kim", email = "akim@casey-corp.com", department = "Compliance" }
  }
}

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

  organization_alias = "casey-corp"
  domains            = { primary = { domain_name = "casey-corp.com" } }
  default_domain     = "primary"

  users = local.onboarding_users
}
8 · User with an initial password (sensitive, isolated map)
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"

  users = {
    svc_reporting = {
      name         = "svc-reporting"
      display_name = "Reporting Service Account"
      email        = "svc-reporting@casey-corp.com"
      user_role    = "SYSTEM_USER"
    }
  }

  # Marked sensitive = true — never appears in plan output. Supply from a
  # secrets manager / generated-password module, never a hardcoded literal.
  user_passwords = {
    svc_reporting = var.svc_reporting_initial_password
  }
}
9 · Resource mailbox / system user roles
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"

  users = {
    conf_room_a = {
      name         = "conf-room-a"
      display_name = "Conference Room A"
      email        = "conf-room-a@casey-corp.com"
      user_role    = "RESOURCE"
    }
  }
}
10 · Groups (distribution lists)
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"

  groups = {
    engineering = { name = "engineering", email = "engineering@casey-corp.com" }
    compliance  = { name = "compliance", email = "compliance@casey-corp.com", hidden_from_global_address_list = true }
  }
}
11 · 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 "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"

  tags = {
    Environment = "prod" # resource tag — wins over default_tags on key conflict
    DataClass   = "internal"
  }
}

# module.workmail.tags_all == { Owner, ManagedBy, Environment, DataClass }
# (organization only — domains/users/groups are not taggable)
12 · Import an existing organization
import {
  to = module.workmail.aws_workmail_organization.this
  id = "m-1234567890abcdef1234567890abcdef"
}
13 · Exchange interoperability enabled
module "workmail_hybrid" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias       = "casey-hybrid"
  directory_id             = "d-1234567890"
  interoperability_enabled = true # FORCE-NEW — only for a documented hybrid-Exchange requirement
}
14 · End-to-end composition — CMK, existing directory, multi-domain, users, groups
# Customer-managed CMK for organization data
module "workmail_kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  alias  = "casey/workmail"
}

# This module — the WorkMail tenant
module "workmail" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-workmail?ref=v1.0.0"

  organization_alias = "casey-corp"
  directory_id       = "d-1234567890" # existing Directory Service directory
  kms_key_arn        = module.workmail_kms.arn

  domains = {
    primary = { domain_name = "casey-corp.com" }
  }
  default_domain = "primary"

  users = {
    jdoe = {
      name         = "jdoe"
      display_name = "Jane Doe"
      email        = "jdoe@casey-corp.com"
      department   = "Engineering"
    }
  }
  user_passwords = {
    jdoe = var.jdoe_initial_password # sensitive — supplied by caller, never hardcoded
  }

  groups = {
    engineering = { name = "engineering", email = "engineering@casey-corp.com" }
  }

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

# Wire the DNS verification records into Route 53
module "workmail_dns" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-route53-zone?ref=v1.0.0"
  #... create records from module.workmail.domains["primary"].records
}

📥 Inputs

Name Type Default Description
organization_alias string — required Organization alias. FORCE-NEW. Globally unique, 1-62 lowercase alphanumeric/hyphen chars.
directory_id string null Existing Directory Service directory id. FORCE-NEW. Null lets WorkMail self-provision.
kms_key_arn string (ARN) null Customer-managed KMS key. FORCE-NEW. Null uses the AWS-managed key.
interoperability_enabled bool false Enable Exchange interoperability. FORCE-NEW.
delete_directory bool false Delete the associated directory on organization destroy.
delete_identity_center_application bool false Delete the associated Identity Center app on organization destroy.
domains map(object({ domain_name=string })) {} Additional mail domains to register. domain_name is FORCE-NEW.
default_domain string null Key into domains to set as the default mail domain. Must exist in domains.
users map(object({...})) {} Users/mailboxes keyed by name. See variables.tf for the full profile schema (name, display_name, email, department, user_role, etc.).
user_passwords map(string) {} Initial password per user key. sensitive = true.
groups map(object({ name=string, email=string, hidden_from_global_address_list=optional(bool,false) })) {} Groups/distribution lists keyed by name.
tags map(string) {} Tags for the organization (the only taggable resource here).
timeouts object({ create, delete }) {} Operation timeouts for the organization (no update timeout in the schema).

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


🧾 Outputs

Name Description
id Organization id (m-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).
arn Organization ARN (cross-resource reference type).
name The organization alias.
state Organization lifecycle state.
default_mail_domain The organization's default mail domain.
directory_type Type of the associated directory.
completed_date RFC3339 timestamp the organization became active.
migration_admin Migration admin user id, or null.
domains Map keyed as var.domains: id, domain_name, records, dkim_verification_status, ownership_verification_status, is_default, is_test_domain.
default_domain_name The domain set as default, or null when default_domain is unset.
users Map keyed as var.users: id (user_id), name, email, state, lifecycle timestamps.
groups Map keyed as var.groups: id (group_id), name, email, state, lifecycle timestamps.
tags_all All tags incl. provider default_tags — organization only.

ℹ️ No tags_all is emitted for domains, the default-domain assignment, users, or groups — none of those resources support tags in the current provider schema.


🧠 Architecture Notes

  • ID/ARN format: organization id is m-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; arn is the cross-resource reference type for scoping workmail:* IAM policy statements.
  • Force-new fields: organization_alias, directory_id, kms_key_arn, and interoperability_enabled are all FORCE-NEW on the organization — changing any of them destroys and recreates the entire tenant. domain_name and organization_id are FORCE-NEW on aws_workmail_domain. email is FORCE-NEW on aws_workmail_user.
  • tags ↔ tags_all ↔ default_tags: only aws_workmail_organization supports tags in the current provider schema. var.tags flows to it and merges with provider default_tags (resource tags win on key conflict); tags_all is the computed merge. Domains, the default-domain assignment, users, and groups are not taggable — the documented exception, matching the terraform-aws-iam-group precedent.
  • No hosted_zone_id on aws_workmail_domain. DNS verification records are exported, not consumed. This module's contract ends at registering the domain and exposing records; creating the matching Route 53 records is terraform-aws-route53-zone's job (or any DNS provider) one layer up.
  • aws_workmail_default_domain has no true "unset." There is no API call to revert a default-domain assignment; removing the resource from configuration only stops Terraform from managing it. domain_name is an in-place-updatable argument on this resource (only organization_id is FORCE-NEW), so swapping which domain is default does not recreate anything.
  • Eventual consistency: domain ownership/DKIM verification statuses transition from PENDING asynchronously as AWS re-checks DNS — expect a delay between creating records and seeing VERIFIED.
  • Destroy ordering: Terraform's implicit dependency graph destroys domains/default-domain/users/groups (all reference aws_workmail_organization.this.organization_id) before the organization. Destroying the organization fails if mailboxes, domains, or groups exist outside this module's state — clean those up first.
  • Billing on create: aws_workmail_user registers a live mailbox (RegisterToWorkMail) at creation time and deregisters it on destroy — this is not a lightweight directory-identity resource.
  • us-east-1 globals: N/A — WorkMail has no CloudFront/WAFv2/ACM-style global-resource coupling. It does have narrower Regional availability than most services; confirm the target Region supports WorkMail.

🧱 Design Principles

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

Posture Default Opt-out
Mailbox/data encryption kms_key_arn = null (AWS-managed key) supply a CMK ARN from terraform-aws-kms — recommended for a regulated FI
Directory deletion on destroy delete_directory = false (directory survives) true — only for a directory exclusively owned by this organization
Identity Center app cleanup on destroy delete_identity_center_application = false true
Exchange interoperability interoperability_enabled = false (narrower attack surface) true for a documented hybrid-Exchange requirement
Global address list visibility hidden_from_global_address_list = false (matches AWS default) true per user/group for a restricted mailbox
User passwords No hardcoded default; supplied per-key via a dedicated sensitive = true map omit the key entirely to leave a password unmanaged

Other principles:

  • One composite, one keystone. The organization owns only the resources meaningless without it — domains, the default-domain pointer, users, and groups.
  • for_each, never count, for every child collection — keyed by stable caller strings so reorders don't churn the plan. The default-domain assignment uses a guarded single-key for_each.
  • Primary outputs id + arn, plus name, state, and tags_all (organization only).
  • Secrets isolated, not nested. password is pulled into its own sensitive = true map rather than embedded in the users object, because Terraform can only mark a whole variable sensitive, not a single nested attribute.
  • DNS is out of scope by design. Registering a domain and creating its verification records are different concerns owned by different modules.

🚀 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 WorkMail 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.
  • terraform fmt -check — canonical formatting.
  • terraform plan against a sandbox account to confirm the organization, domains, default-domain assignment, users, and groups materialize as expected.
  • Assert module.<name>.arn, domains[*].records, and tags_all in your root-module test harness. For user paths, assert user_passwords values never appear in plan output (Terraform redacts sensitive variables automatically).

💬 Example Output

module.workmail.aws_workmail_organization.this: Creation complete after 3m12s [id=m-1234567890abcdef1234567890abcdef]
module.workmail.aws_workmail_domain.this["primary"]: Creation complete [id=m-1234.../casey-corp.com]
module.workmail.aws_workmail_default_domain.this["this"]: Creation complete
module.workmail.aws_workmail_user.this["jdoe"]: Creation complete [id=m-1234.../12345678-1234-1234-1234-123456789012]

Outputs:
arn = "arn:aws:workmail:us-east-1:123456789012:organization/m-1234567890abcdef1234567890abcdef"
id = "m-1234567890abcdef1234567890abcdef"
default_mail_domain = "casey-corp.com"
domains = { "primary" = { "domain_name" = "casey-corp.com", "ownership_verification_status" = "VERIFIED",... } }
tags_all = { "DataClass" = "internal", "Environment" = "prod" }

🔍 Troubleshooting

Symptom Likely cause Fix
terraform apply hangs for several minutes on the organization Normal — organization creation (and directory auto-provisioning) is asynchronous and can take minutes Wait; raise timeouts.create if it exceeds the default
ResourceNotFoundException on aws_workmail_domain right after org create Organization not yet fully ACTIVE Ensure the org reaches state = "Active" before registering domains (normal dependency ordering handles this within one apply)
Domain stuck at ownership_verification_status = "PENDING" DNS verification records not yet created/propagated Read domains[*].records and create matching records in the domain's DNS zone; re-check after propagation
UnsupportedOperationException on domain destroy Domain is still the organization's default, or has active mailboxes Reassign/remove default_domain and clean up dependent users first
OrganizationStateException on organization destroy Mailboxes/domains/groups exist outside this module's Terraform state Remove out-of-band WorkMail objects, or import them into this module, before destroying
password never appears in terraform show/plan Expected — user_passwords is sensitive = true Not an error; verify via terraform output -json only when intentionally reading the value
Tag drift on every plan (organization only) A tag also set by provider default_tags with a different value Let resource tags win, or remove the overlap from default_tags
InvalidParameterException: organization_alias Alias not globally unique, or fails the lowercase/hyphen charset Choose a different alias; check for leading/trailing hyphens
Costs higher than expected Every users entry is a live, billed mailbox from creation Audit the users map; remove placeholder/test entries before a shared apply
Credentials/region errors Standard AWS provider chain not resolving, or Region doesn't support WorkMail Confirm AWS_PROFILE/SSO/OIDC and that the target Region is one of WorkMail's supported Regions

🔗 Related Docs


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

Releases

Packages

Contributors

Languages