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.
- 📬 One organization, fully wired. Creates
aws_workmail_organizationplus 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 itnulland let WorkMail auto-provision its own directory for lower-friction setups. - 🔐 CMK-ready encryption.
kms_key_arndefaults tonull(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_idargument onaws_workmail_domain— this module registers the domain and emits the exact DNS records AWS requires; wiring them into Route 53 (e.g. viaterraform-aws-route53-zone) is a deliberate, separate step. - 🙈 No hardcoded passwords, ever. User passwords are supplied per-user through a dedicated
user_passwordsmap markedsensitive = true— never embedded in the mainusersobject, never defaulted. - 🏷️ Tags on the organization only.
aws_workmail_domain,aws_workmail_default_domain,aws_workmail_user, andaws_workmail_groupare not taggable in the current provider schema — the documented exception to the universal-tags rule (same shape asterraform-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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
ℹ️
terraform-aws-directory-serviceis 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 leavedirectory_id = nulland let WorkMail self-provision.
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
| 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 |
| 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).
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 |
⚠️ Noiam:PassRoleis 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 gateds:CreateDirectory/ds:DeleteDirectoryseparately if your org standard requires directories to be provisioned only by a dedicated directory-service pipeline.
- Every organization requires an underlying directory. Supply an existing Simple AD / AD Connector / Managed Microsoft AD
directory_id, or leave itnullto 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 whendelete_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_statusstayPENDINGuntil 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_useris created (RegisterToWorkMailruns on create). Size theusersmap deliberately. - Service quotas: default 1 organization per account/Region (soft, raisable); domains/users/groups scale per current WorkMail service quotas.
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
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"
}
}| 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 |
| 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 |
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
}| 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.
| 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_allis emitted for domains, the default-domain assignment, users, or groups — none of those resources support tags in the current provider schema.
- ID/ARN format: organization
idism-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;arnis the cross-resource reference type for scopingworkmail:*IAM policy statements. - Force-new fields:
organization_alias,directory_id,kms_key_arn, andinteroperability_enabledare all FORCE-NEW on the organization — changing any of them destroys and recreates the entire tenant.domain_nameandorganization_idare FORCE-NEW onaws_workmail_domain.emailis FORCE-NEW onaws_workmail_user. tags↔tags_all↔default_tags: onlyaws_workmail_organizationsupports tags in the current provider schema.var.tagsflows to it and merges with providerdefault_tags(resource tags win on key conflict);tags_allis the computed merge. Domains, the default-domain assignment, users, and groups are not taggable — the documented exception, matching theterraform-aws-iam-groupprecedent.- No
hosted_zone_idonaws_workmail_domain. DNS verification records are exported, not consumed. This module's contract ends at registering the domain and exposingrecords; creating the matching Route 53 records isterraform-aws-route53-zone's job (or any DNS provider) one layer up. aws_workmail_default_domainhas 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_nameis an in-place-updatable argument on this resource (onlyorganization_idis FORCE-NEW), so swapping which domain is default does not recreate anything.- Eventual consistency: domain ownership/DKIM verification statuses transition from
PENDINGasynchronously as AWS re-checks DNS — expect a delay between creating records and seeingVERIFIED. - 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_userregisters 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.
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, nevercount, for every child collection — keyed by stable caller strings so reorders don't churn the plan. The default-domain assignment uses a guarded single-keyfor_each.- Primary outputs
id+arn, plusname,state, andtags_all(organization only). - Secrets isolated, not nested.
passwordis pulled into its ownsensitive = truemap rather than embedded in theusersobject, 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.
# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check
plan/applyrequire 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.
terraform init -backend=false && terraform validate— schema + reference integrity.terraform fmt -check— canonical formatting.terraform planagainst a sandbox account to confirm the organization, domains, default-domain assignment, users, and groups materialize as expected.- Assert
module.<name>.arn,domains[*].records, andtags_allin your root-module test harness. For user paths, assertuser_passwordsvalues never appear in plan output (Terraform redactssensitivevariables automatically).
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" }
| 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 |
- Amazon WorkMail Administrator Guide
- Amazon WorkMail pricing
- AWS Directory Service — directory types
- Terraform:
aws_workmail_organization·aws_workmail_domain·aws_workmail_default_domain·aws_workmail_user·aws_workmail_group - Sibling modules:
terraform-aws-kms,terraform-aws-route53-zone,terraform-aws-iam-identity-center - Module internals:
SCOPE.md
🧡 "Infrastructure as Code should be standardized, consistent, and secure."