Secure-by-default Amazon WorkSpaces fleet β registers a pre-existing AWS Directory Service directory with WorkSpaces, then provisions persistent virtual desktops and IP access control groups from a single composite call. Root + user volume encryption on by default, no direct internet egress by default, no local admin by default. Built for the AWS provider v6.x.
- π₯οΈ Provisions an
aws_workspaces_directorykeystone plus its persistent desktops and IP allow-lists:aws_workspaces_workspace(one per end user) andaws_workspaces_ip_group(CIDR allow-lists) β one composite call, one ready-to-use WorkSpaces fleet. - π Registers, never creates, the directory.
aws_workspaces_directorycalls the WorkSpacesRegisterWorkspaceDirectoryaction against an already-existing AWS Directory Service directory (AWS Managed Microsoft AD, Simple AD, or AD Connector). This module never creates, modifies, or destroys that directory β provision it out-of-band and pass its id viadirectory_id. - π Volume encryption on by default β
root_volume_encryption_enabledanduser_volume_encryption_enabledboth default totrueon every WorkSpace, with a caller-suppliablevolume_encryption_key(KMS CMK ARN);null(default) uses the AWS-managedalias/aws/workspaceskey. - π No direct internet egress by default β
workspace_creation_properties.enable_internet_accessdefaults tofalse, routing WorkSpaces egress through the VPC's own NAT/proxy path rather than a WorkSpaces-managed public IP. - π« No local admin by default β
workspace_creation_properties.user_enabled_as_local_administratordefaults tofalse. - π IP access control via
aws_workspaces_ip_groupβ afor_eachmap of named CIDR allow-lists, associated directly on the directory'sip_group_idsattribute (there is no separate association resource in this service). - π§βπ€βπ§ Desktops are a
for_eachmap keyed by username β add or remove one user's WorkSpace without perturbing the others; nocount. - π¦ No bundle resource β
aws_workspaces_bundleexists only as a data source in thehashicorp/awsprovider (verified against v6.53). Custom bundle creation is a console/API operation with no Terraform coverage; this module exposesbundle_idas a plain string, typically resolved viadata.aws_workspaces_bundle. β οΈ Noarnoutput anywhere β verified against the v6.53 schema, none of the three in-scope resources export anarnattribute. This module deviates from the library-wide "id + arn" convention and emits id-family outputs only (see Architecture Notes).- π·οΈ Universal tagging β
var.tagsflows to the directory, every WorkSpace, and every IP group (all three exposetags);tags_allsurfaced as an output on the keystone.
π‘ Why it matters: WorkSpaces desktops carry end-user sessions that routinely touch PII. A module that defaults every volume to encrypted, blocks WorkSpaces-managed public IPs, and denies local admin β while still registering cleanly against whatever Directory Service topology (managed AD, Simple AD, or on-prem AD Connector) a regulated FI already runs β keeps the desktop fleet's blast radius small without adding friction to provisioning.
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!
tf-mod-aws-workspaces is a dependent, terminal module: it consumes an externally-provisioned directory plus networking, security-group, KMS, and secrets from upstream foundations, and provisions nothing that other modules consume back (there is no downstream ARN or id this fleet feeds into elsewhere in the catalog).
flowchart LR
ds["AWS Directory Service directory<br/>(external prerequisite β NOT created by any module)"]
vpc["tf-mod-aws-vpc<br/>subnet_ids"]
sg["tf-mod-aws-security-group<br/>custom_security_group_id"]
kms["tf-mod-aws-kms<br/>volume_encryption_key"]
sm["tf-mod-aws-secrets-manager<br/>service_account_secret_arn"]
iam["tf-mod-aws-iam-role<br/>workspaces_DefaultRole"]
bundle["data.aws_workspaces_bundle<br/>(lookup, not a resource)"]
pca["ACM Private CA<br/>certificate_authority_arn (optional)"]
ws["tf-mod-aws-workspaces"]
ds -->|"directory_id"| ws
vpc -->|"subnet_ids"| ws
sg -.->|"custom_security_group_id"| ws
kms -.->|"volume_encryption_key"| ws
sm -.->|"service_account_secret_arn (POOLS)"| ws
iam -.->|"workspaces_DefaultRole must exist first"| ws
bundle -.->|"bundle_id"| ws
pca -.->|"certificate_authority_arn (CBA, optional)"| ws
style ws fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
flowchart TB
subgraph WSMOD["tf-mod-aws-workspaces"]
dir["aws_workspaces_directory.this<br/>(keystone β registers the pre-existing directory)"]
ipg["aws_workspaces_ip_group.this<br/>for_each ip_groups"]
wksp["aws_workspaces_workspace.this<br/>for_each workspaces (PERSONAL mode only)"]
ipg -->|"ip_group_ids attribute"| dir
dir -->|"directory_id"| wksp
end
style dir fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
| Resource | Role | Cardinality | Taggable |
|---|---|---|---|
aws_workspaces_directory.this |
Keystone β registers the pre-existing Directory Service directory | 1 | β |
aws_workspaces_ip_group.this |
IP access control groups (CIDR allow-lists) | per ip_groups entry |
β |
aws_workspaces_workspace.this |
Persistent virtual desktops, one per user | per workspaces entry (PERSONAL mode only) |
β |
βΉοΈ There is no separate association resource wiring an IP group to the directory β
aws_workspaces_directory.ip_group_idsis a plain attribute on the directory itself, populated from every group this module creates plus anyexternal_ip_group_ids.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
No provider {} block is declared inside the module β the caller's configured provider (region, credentials, default_tags) is inherited. Credentials resolve through the standard AWS chain (env β SSO/shared credentials β assume_role β instance profile / IRSA β OIDC) at the root/pipeline level; there are no credential variables and no region variable.
Least-privilege actions the Terraform execution identity needs to create, read, update, and delete everything this module manages:
| Action | Required for | Notes |
|---|---|---|
workspaces:RegisterWorkspaceDirectory, workspaces:DeregisterWorkspaceDirectory, workspaces:DescribeWorkspaceDirectories |
Directory registration lifecycle | Core CRUD on aws_workspaces_directory |
workspaces:ModifyWorkspaceAccessProperties, workspaces:ModifyWorkspaceCreationProperties, workspaces:ModifySelfservicePermissions, workspaces:ModifySamlProperties, workspaces:ModifyCertificateBasedAuthProperties |
Directory-level property configuration | Only when the corresponding variable is set |
workspaces:CreateWorkspaces, workspaces:TerminateWorkspaces, workspaces:DescribeWorkspaces, workspaces:ModifyWorkspaceProperties, workspaces:ModifyWorkspaceState |
WorkSpace (desktop) lifecycle | Only when workspaces is non-empty |
workspaces:CreateIpGroup, workspaces:DeleteIpGroup, workspaces:DescribeIpGroups, workspaces:UpdateRulesOfIpGroup, workspaces:AssociateIpGroups, workspaces:DisassociateIpGroups |
IP access control group lifecycle and association | Only when ip_groups is non-empty |
workspaces:CreateTags, workspaces:DeleteTags, workspaces:DescribeTags |
Tagging the directory, workspaces, and IP groups | Core CRUD |
workspaces:DescribeWorkspaceBundles |
Resolving data.aws_workspaces_bundle lookups callers use to populate bundle_id |
Read-only |
ds:DescribeDirectories, ds:AuthorizeApplication, ds:UnauthorizeApplication |
WorkSpaces service authorization against the pre-existing Directory Service directory during registration/deregistration | Directory Service side of registration |
ec2:DescribeSubnets, ec2:DescribeSecurityGroups, ec2:DescribeVpcs, ec2:CreateNetworkInterface, ec2:DescribeNetworkInterfaces, ec2:DeleteNetworkInterface |
WorkSpaces provisions ENIs into the supplied subnets for each desktop | Exercised by the WorkSpaces service, not the Terraform identity directly, but the identity needs the Describe* actions to plan |
kms:CreateGrant, kms:DescribeKey |
Only when a customer-managed volume_encryption_key is supplied |
|
iam:GetRole, iam:PassRole (scoped to workspaces_DefaultRole) |
WorkSpaces assumes this role to call EC2/other services on the caller's behalf | The role itself is created by tf-mod-aws-iam-role, not by this module β see Prerequisites |
secretsmanager:GetSecretValue (scoped) |
Only for workspace_type = "POOLS" with active_directory_config.service_account_secret_arn set |
|
acm-pca:GetCertificateAuthorityCertificate (scoped) |
Only when certificate_based_auth_properties.certificate_authority_arn is set |
βΉοΈ
workspaces:CreateWorkspaceBundle/workspaces:DeleteWorkspaceBundleare not exercised by this module β there is noaws_workspaces_bundleresource in the provider. Grant them separately only if your team manages custom bundles out-of-band via console/CLI/SDK.
β οΈ This module does NOT create the Directory Service directory, and no module currently does either. A pre-existing AWS Directory Service directory is a hard, out-of-band prerequisite β provision it manually, via a one-off Terraform config outside this library, or via a futuretf-mod-aws-directory-servicemodule (not yet on the Phase 1β7 roadmap).
- Directory Service directory (mandatory, external): one of AWS Managed Microsoft AD (recommended for production/PII β supports trusts, MFA, richer group policy), Simple AD (small deployments, no trusts, being phased out of AWS's new-build guidance), or AD Connector (proxies to on-prem/self-managed AD β no directory data stored in AWS; common for regulated customers keeping identity data on-prem).
directory_idaccepts the id from any of the three. workspaces_DefaultRoleIAM role must exist before directory registration. WorkSpaces looks this role up by its fixed name at the account level; registration fails validation if it is absent. It must haveAmazonWorkSpacesServiceAccess(and, if self-service is used,AmazonWorkSpacesSelfServiceAccess) attached. Create it viatf-mod-aws-iam-rolewith an explicitdepends_onor run ordering before this module β the name is fixed and not parameterizable in the API.- Subnets: directory registration accepts at most 2 subnets, spanning 2 distinct Availability Zones in the directory's VPC (verified against the AWS WorkSpaces API reference β not "at least 2" as a looser reading might suggest).
- Directory registration is eventually consistent β
RegisterWorkspaceDirectorycan take several minutes; rapid destroy/re-apply cycles can fail with directory-still-registering errors. - WorkSpaces service availability is Region-limited β confirm the target Region is enabled before use.
- First-ever registration in an account/Region prompts AWS to provision underlying service network interfaces automatically β can add latency to the first apply.
- Quotas: default WorkSpaces-per-Region and directories-per-Region quotas are soft limits β request increases via Service Quotas before large fleet rollouts.
- BYOL (Bring Your Own License): custom Windows images/bundles require
tenancy = "DEDICATED"and additional directory/licensing prerequisites out of scope for this module. - POOLS mode is registration-only in this module β the
hashicorp/awsprovider has no resource for creating WorkSpaces Pool session desktops; see Architecture Notes.
tf-mod-aws-workspaces/
βββ providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider block
βββ variables.tf # directory_id, subnet_ids, directory config, child maps, tags, timeouts
βββ main.tf # locals + aws_workspaces_directory.this + 2 child resources
βββ outputs.tf # id family (no arn), directory attrs, child maps, tags_all
βββ README.md # this file
βββ SCOPE.md # in/out-of-scope, Consumes/Emits, IAM, prerequisites, gotchas
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf" # pre-existing AWS Managed Microsoft AD / Simple AD / AD Connector
subnet_ids = values(module.vpc.private_subnet_ids) # tf-mod-aws-vpc β exactly 2, 2 distinct AZs
tags = { Environment = "prod", App = "end-user-computing" }
}
β οΈ workspaces_DefaultRolemust already exist in the account (see Prerequisites) or registration fails validation. Pin the source with?ref=v1.0.0β never a branch.
| Input | Type | Source module |
|---|---|---|
directory_id |
string (required for PERSONAL) |
External prerequisite β pre-existing Directory Service directory, provisioned outside this library |
subnet_ids |
list(string), exactly 2 |
tf-mod-aws-vpc |
workspace_creation_properties.custom_security_group_id |
string |
tf-mod-aws-security-group |
volume_encryption_key / workspaces[*].volume_encryption_key |
string (KMS ARN) |
tf-mod-aws-kms |
certificate_based_auth_properties.certificate_authority_arn |
string (ARN) |
Private CA / ACM-PCA (not yet catalogued) |
active_directory_config.service_account_secret_arn |
string (ARN) |
tf-mod-aws-secrets-manager (POOLS + CUSTOMER_MANAGED only) |
workspaces[*].bundle_id |
string |
Caller-supplied β typically data.aws_workspaces_bundle |
| Output | Description | Consumed by |
|---|---|---|
id / directory_id |
WorkSpaces directory identifier | Audit/reference; not consumed by any other cataloged module today |
registration_code |
Code end users enter in the WorkSpaces client | End-user provisioning documentation |
directory_alias, directory_name, directory_type |
Directory metadata | Reference / troubleshooting |
iam_role_id |
workspaces_DefaultRole identifier as seen by WorkSpaces |
Audit / IAM review |
workspace_security_group_id |
Security group WorkSpaces assigns to new desktops | tf-mod-aws-security-group (additional ingress/egress rules) |
ip_group_ids |
Map of IP group key => id | Directory association (internal), audit |
workspace_ids, workspace_ip_addresses, workspace_states, workspace_computer_names |
Per-user desktop inventory | CMDB / operational dashboards |
tags_all |
All tags incl. provider default_tags |
Governance/audit |
β οΈ Noarnoutput anywhere β see Architecture Notes.
1 Β· Minimal β register a pre-existing directory
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf" # pre-existing Directory Service directory
subnet_ids = values(module.vpc.private_subnet_ids) # tf-mod-aws-vpc β exactly 2 subnets, 2 AZs
}2 Β· Resolving bundle_id via data.aws_workspaces_bundle
data "aws_workspaces_bundle" "standard" {
owner = "AMAZON"
name = "Value with Windows 10 and Office 2016"
}
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
workspaces = {
jdoe = {
user_name = "jdoe"
bundle_id = data.aws_workspaces_bundle.standard.id
}
}
}βΉοΈ
aws_workspaces_bundleis a data source only β there is no Terraform resource to create custom bundles. This module treatsbundle_idas an opaque caller-supplied string.
3 Β· Single WorkSpace provisioning
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
workspaces = {
jdoe = {
user_name = "jdoe"
bundle_id = "wsb-bh8rsxt14" # example bundle id
}
}
}4 Β· Multiple WorkSpaces β for_each pattern across users
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
workspaces = {
jdoe = { user_name = "jdoe", bundle_id = "wsb-bh8rsxt14" }
asmith = {
user_name = "asmith"
bundle_id = "wsb-clj85qzj0"
workspace_properties = {
compute_type_name = "PERFORMANCE"
running_mode = "AUTO_STOP"
}
}
mchen = { user_name = "mchen", bundle_id = "wsb-bh8rsxt14" }
}
tags = { Team = "loan-servicing" }
}βΉοΈ Add or remove one user's map entry without perturbing the others β no
count-based re-indexing.
5 Β· IP access control group
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
ip_groups = {
hq_office = {
description = "headquarters egress"
rules = [
{ source = "203.0.113.0/24", description = "HQ office CIDR" },
{ source = "198.51.100.0/28", description = "VPN egress" },
]
}
}
# Combine with a group owned by a sibling module call:
external_ip_group_ids = ["wsipg-abc12345"]
}6 Β· Tags β merge with provider default_tags
provider "aws" {
region = "us-east-1"
default_tags {
tags = { Owner = "euc-platform", ManagedBy = "terraform" }
}
}
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
tags = { Environment = "prod", Owner = "desktop-team" } # overrides default_tags Owner
workspaces = {
jdoe = {
user_name = "jdoe"
bundle_id = "wsb-bh8rsxt14"
tags = { CostCenter = "1042" } # merged over module tags on this workspace
}
}
}
# module.workspaces.tags_all => { Owner="desktop-team", ManagedBy="terraform", Environment="prod" }7 Β· Customer-managed KMS key for volume encryption
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
volume_encryption_key = module.kms.arn # tf-mod-aws-kms β applied to every workspace by default
workspaces = {
jdoe = { user_name = "jdoe", bundle_id = "wsb-bh8rsxt14" }
# Override the module-wide CMK for one workspace only:
asmith = {
user_name = "asmith"
bundle_id = "wsb-bh8rsxt14"
volume_encryption_key = module.kms_finance.arn
}
}
}8 Β· Secure-by-default opt-out β internet access, local admin, unencrypted volumes
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
# OPT-OUT: WorkSpaces-managed public IP instead of routing through the VPC NAT/proxy path.
workspace_creation_properties = {
enable_internet_access = true
user_enabled_as_local_administrator = true
}
workspaces = {
contractor = {
user_name = "contractor1"
bundle_id = "wsb-bh8rsxt14"
root_volume_encryption_enabled = false # OPT-OUT β data at rest is unencrypted
user_volume_encryption_enabled = false # OPT-OUT β data at rest is unencrypted
}
}
}
β οΈ Every flip here requires a documented regulated-industry exception. Unencrypted volumes are a direct PII exposure risk; local admin plus direct internet egress removes two independent containment layers on the same desktop. Do not combine these for PII-handling fleets.
9 Β· SAML 2.0 federation
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
saml_properties = {
status = "ENABLED_WITH_DIRECTORY_LOGIN_FALLBACK"
user_access_url = "https://idp.financialpartners.com/saml/workspaces"
relay_state_parameter_name = "RelayState"
}
}10 Β· Certificate-based authentication (requires SAML enabled)
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
saml_properties = {
status = "ENABLED"
user_access_url = "https://idp.financialpartners.com/saml/workspaces"
}
certificate_based_auth_properties = {
status = "ENABLED"
certificate_authority_arn = "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123" # ACM-PCA
}
}βΉοΈ Setting
certificate_based_auth_properties.status = "ENABLED"withoutsaml_properties.status!="DISABLED"fails this module'sterraform planβ the cross-field validation surfaces the API's undocumented dependency before apply.
11 Β· Self-service permissions customization
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
self_service_permissions = {
increase_volume_size = true
switch_running_mode = true
# change_compute_type / rebuild_workspace left at AWS defaults (false)
# restart_workspace left at AWS default (true β no data/config risk)
}
}12 Β· Device access restriction
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
workspace_access_properties = {
device_type_zeroclient = "DENY" # not on the approved device inventory
device_type_android = "DENY" # personal BYOD phones/tablets out of policy
device_type_ios = "DENY"
device_type_web = "ALLOW"
device_type_windows = "ALLOW"
device_type_osx = "ALLOW"
}
}13 Β· Import block β bring an existing directory registration under management
import {
to = module.workspaces.aws_workspaces_directory.this
id = "d-926722edaf"
}
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf"
subnet_ids = values(module.vpc.private_subnet_ids)
}π‘ Run
terraform planafter adding the import block and reconcile any drift (especiallyworkspace_creation_properties, which this module renders unconditionally) before applying.
14 Β· POOLS mode β directory registration only
module "workspaces_pools" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
workspace_type = "POOLS"
workspace_directory_name = "casey-pools-directory"
user_identity_type = "CUSTOMER_MANAGED"
directory_id = null # AWS auto-generates a wsd-xxxxxxxx id β must be null for POOLS
active_directory_config = {
domain_name = "corp.financialpartners.com"
service_account_secret_arn = module.pools_service_account.arn # tf-mod-aws-secrets-manager
}
workspaces = {} # REQUIRED empty β no aws_workspaces_workspace resource supports POOLS
}
β οΈ Thehashicorp/awsprovider has no resource for creating WorkSpaces Pool session desktops (verified against v6.53). This example registers a pools directory only; provision pool sessions via the AWS console/API/SDK outside Terraform.
15 Β· End-to-end composition β VPC + security group + KMS + IAM role + WorkSpaces (finale)
module "vpc" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
name = "casey-euc"
#... cidr_block, public/private subnets in 2 AZs...
}
module "workspaces_sg" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-security-group?ref=v1.0.0"
name = "casey-workspaces"
vpc_id = module.vpc.vpc_id
}
module "kms" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-kms?ref=v1.0.0"
name = "casey-workspaces-volumes"
}
module "workspaces_default_role" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
name = "workspaces_DefaultRole" # FIXED name β WorkSpaces looks this up by name, not ARN
# assume_role_policy: trusts workspaces.amazonaws.com;
# attach AWS-managed AmazonWorkSpacesServiceAccess + AmazonWorkSpacesSelfServiceAccess
}
module "workspaces" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-workspaces?ref=v1.0.0"
directory_id = "d-926722edaf" # pre-existing AWS Managed Microsoft AD β external prerequisite
subnet_ids = values(module.vpc.private_subnet_ids)
workspace_creation_properties = {
custom_security_group_id = module.workspaces_sg.id
enable_internet_access = false # secure default, explicit here for clarity
}
volume_encryption_key = module.kms.arn
ip_groups = {
hq = {
rules = [{ source = "203.0.113.0/24", description = "HQ office CIDR" }]
}
}
workspaces = {
jdoe = { user_name = "jdoe", bundle_id = "wsb-bh8rsxt14" }
asmith = { user_name = "asmith", bundle_id = "wsb-clj85qzj0" }
}
tags = { Environment = "prod", App = "end-user-computing" }
depends_on = [module.workspaces_default_role]
}βΉοΈ High-level grouping:
- Directory registration (primary identity):
directory_id,subnet_ids - Directory identity / mode:
workspace_type(PERSONAL|POOLS),workspace_directory_name,workspace_directory_description,tenancy,user_identity_type - Directory security & access (optional objects):
active_directory_config,certificate_based_auth_properties,saml_properties,self_service_permissions,workspace_access_properties,workspace_creation_properties - IP access control:
external_ip_group_ids,ip_groups(for_eachmap) - Volume-encryption defaults:
root_volume_encryption_enabled,user_volume_encryption_enabled,volume_encryption_key - Child collection:
workspaces(for_eachmap, PERSONAL mode only) - Universal:
tags,timeouts(applies to everyaws_workspaces_workspace)
- Primary:
id,directory_id(noarnβ see Architecture Notes) - Directory attributes:
registration_code,directory_alias,directory_name,directory_type,directory_customer_user_name,directory_dns_ip_addresses,iam_role_id,workspace_security_group_id - Child maps:
ip_group_ids,workspace_ids,workspace_ip_addresses,workspace_states,workspace_computer_names - Tags:
tags_all
βΉοΈ No outputs are marked
sensitiveβ this module emits no secrets and no credentials.
- No
arnoutput β a deliberate deviation from the library-wide "id + arn" convention. Verified against thehashicorp/awsv6.53 schema: none ofaws_workspaces_directory,aws_workspaces_workspace, oraws_workspaces_ip_groupexport anarnattribute. WorkSpaces cross-references (including IAM policy conditions for this service) use the service-specific id, not an ARN. - ID formats: directory id
d-xxxxxxxxxx(orwsd-xxxxxxxxxxfor a POOLS-generated directory); WorkSpace idws-xxxxxxxxx; IP group idwsipg-xxxxxxxxx. - Force-new / immutable fields:
directory_idon the directory resource (re-pointing a registration is unsupported β destroy/recreate cascades to every dependent WorkSpace);workspace_type(mode switch is not supported post-registration); on each WorkSpace,bundle_id,directory_id,user_name, and all three volume-encryption fields (root_volume_encryption_enabled,user_volume_encryption_enabled,volume_encryption_key) are effectively immutable β changing any of them forces replacement, which is a data-loss risk on the user volume unless backed up first. workspace_creation_properties.custom_security_group_idand.default_oucannot be unset once set. Per AWS provider behavior, removing them from configuration after they've been applied does not clear them on the live directory β Terraform state may drop the value while AWS keeps it. Treat setting either as a one-way door.tagsβtags_allβdefault_tags:var.tagsflows to the directory, every WorkSpace, and every IP group (all three exposetagsin the v6 schema); per-itemtagsmerge over the module-level map.tags_all(surfaced from the directory only) is the provider-computed union overdefault_tags, with resource tags winning.default_tagsis the caller's provider-block concern.- Eventual consistency / destroy ordering:
RegisterWorkspaceDirectorycan take several minutes to reach a usable state; rapid destroy/re-apply cycles can fail with directory-still-registering errors. Everyaws_workspaces_workspacemust be destroyed before the directory can deregister cleanly β Terraform's dependency graph handles this automatically as long as workspaces reference the directory'sidoutput (as this module does) rather than a hardcoded string. certificate_based_auth_properties.status = "ENABLED"requires SAML enabled β the API rejects CBA without SAML even though the raw schema does not enforce it at plan time; this module's cross-fieldvalidation {}catches it before apply.- POOLS mode is registration-only in this module. The provider has no resource for WorkSpaces Pool session desktops, so
workspace_type = "POOLS"forcesworkspaces = {}(enforced by validation) anddirectory_id = null(AWS auto-generates awsd-id). - No
us-east-1constraint β WorkSpaces is a regional service; the module inherits the caller's region and declares noregionvariable.
Secure-by-default posture and the explicit opt-out for each:
| Hardened default | Behavior | Opt-out / control |
|---|---|---|
| Root volume encryption | root_volume_encryption_enabled = true on every WorkSpace |
Set false per-workspace or via the module-wide default β requires a documented regulated-industry exception |
| User volume encryption | user_volume_encryption_enabled = true on every WorkSpace |
Same caveat as root volume encryption |
| Volume encryption key | Caller-suppliable CMK via volume_encryption_key; null uses the AWS-managed alias/aws/workspaces key |
Supply a CMK from tf-mod-aws-kms for key-policy-level audit control |
| Internet access | workspace_creation_properties.enable_internet_access = false β egress routes through the VPC's own NAT/proxy path |
workspace_creation_properties.enable_internet_access = true |
| Local administrator rights | workspace_creation_properties.user_enabled_as_local_administrator = false |
Set true per directory when end users require local admin (document business justification) |
| IP access control | Optional ip_groups / external_ip_group_ids allow-list; omitted by default (no restriction) |
Supply at least one group for production/PII-handling fleets |
| Self-service permissions | Left at AWS defaults (four flags false, restart_workspace true) unless self_service_permissions is supplied |
Enable individually per directory when end-user self-service is desired |
| SAML / certificate-based auth | Both omitted by default (directory-credential login) | Supply saml_properties / certificate_based_auth_properties to enable, wiring an external IdP / ACM-PCA ARN |
| Device access | Unrestricted (AWS default ALLOW for every device type) unless workspace_access_properties is supplied |
Deny device types outside your organization's approved access policy |
Other principles: the module's blast radius stops at the WorkSpaces service boundary β it never creates, modifies, or destroys the underlying Directory Service directory, VPC/subnets, security groups, or KMS keys; those are owned by sibling modules (or, for the directory itself, a process outside this library today) and consumed strictly by reference. workspace_creation_properties is rendered unconditionally (not null-gated) specifically so the module's secure defaults reach the API explicitly rather than relying on an unverified AWS-side default. Child collections (workspaces, ip_groups) are for_each maps keyed by caller-supplied stable strings, never count.
terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires valid AWS credentials (profile / SSO / OIDC) + a region
terraform apply
terraform output
β οΈ plan/applyrequire valid AWS credentials, a region, and a pre-existingworkspaces_DefaultRolein the account. Always pin the module source with?ref=v1.0.0, never a branch.
terraform init -backend=false && terraform validateβ schema + reference integrity (thedirectory_id/workspace_type/user_identity_typecross-field validations surface a PERSONAL/POOLS argument mismatch here, not mid-apply).terraform fmt -checkβ formatting.terraform planagainst a sandbox account β confirm the directory, IP groups, and workspaces plan as expected and thatworkspace_creation_propertiesrenders with the intended secure defaults.- After
apply, confirmregistration_codeis populated and eachworkspace_idsentry reachesAVAILABLEinworkspace_statesbefore handing off to end users. - Destroy test in a throwaway account to validate the workspace-then-directory teardown ordering before relying on it in shared environments.
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
directory_id = "d-926722edaf"
id = "d-926722edaf"
registration_code = "WorkSpaces+ABCD1234"
directory_alias = "d-926722edaf.awsapps.com"
iam_role_id = "arn:aws:iam::123456789012:role/workspaces_DefaultRole"
workspace_security_group_id = "sg-0123456789abcdef0"
workspace_ids = { "jdoe" = "ws-abc123def", "asmith" = "ws-xyz987uvw" }
workspace_states = { "jdoe" = "AVAILABLE", "asmith" = "PENDING" }
ip_group_ids = { "hq_office" = "wsipg-a1b2c3d4e" }
tags_all = { "Environment" = "prod", "App" = "end-user-computing" }
ResourceNotFoundException/ validation failure on directory registration:workspaces_DefaultRoledoes not exist in the account. Create it viatf-mod-aws-iam-role(fixed name,AmazonWorkSpacesServiceAccessattached) before applying this module.InvalidParameterValuesExceptiononsubnet_ids: more than 2 subnets supplied, or the subnets are not in 2 distinct Availability Zones. This module'svalidation {}catches a wrong count at plan time β a wrong-AZ pairing still requires a live API call to surface.- Perpetual drift on
custom_security_group_idordefault_ouafter removing them from configuration: these fields cannot be cleared once set on the live directory (a provider-documented one-way door). Re-add the value to configuration to match reality, or accept the drift. terraform destroyhangs on the directory: one or moreaws_workspaces_workspaceresources still reference it. Confirm all workspaces finished terminating (DescribeWorkspaces) before the directory deregisters β this module's dependency graph handles the ordering automatically as long asdirectory_idisn't hardcoded.certificate_based_auth_propertiesapply fails with an opaque API error:saml_properties.statusis"DISABLED"(or unset). CBA requires SAML enabled; this module's cross-field validation should catch this at plan time β re-runterraform validateif you bypassed it via-target.- A
POOLSapply fails demandingdirectory_id: you setdirectory_idexplicitly. Leave itnullfor POOLS β AWS auto-generates awsd-prefixed id. - New WorkSpace stuck in
PENDINGfar longer than expected: WorkSpaces creation is genuinely slow (provider defaultcreatetimeout is 30 minutes) β raisetimeouts.createfor large batches rather than assuming failure. - Credential-chain failures (
NoCredentialProviders/ExpiredToken): no valid credentials resolved. SetAWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI.
- Terraform Registry β
hashicorp/aws:aws_workspaces_directory,aws_workspaces_workspace,aws_workspaces_ip_group,data.aws_workspaces_bundle - AWS β Amazon WorkSpaces Administration Guide (directory registration,
workspaces_DefaultRole, volume encryption, VPC configuration) - AWS β Amazon WorkSpaces API Reference (
WorkspaceDirectory,DefaultWorkspaceCreationProperties,WorkspaceAccessProperties,SamlProperties,CertificateBasedAuthProperties,SelfservicePermissions,WorkspaceProperties) - AWS β Directory Service Administration Guide (AWS Managed Microsoft AD, Simple AD, AD Connector)
- β
tf-mod-aws-vpc,tf-mod-aws-security-group,tf-mod-aws-kms,tf-mod-aws-iam-role,tf-mod-aws-secrets-manager(sibling modules)
π§‘ "Infrastructure as Code should be standardized, consistent, and secure."