Skip to content

microsoftexpert/tf-mod-aws-workspaces

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🟧 AWS WorkSpaces Terraform Module

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.

Terraform aws module type resources


🧩 Overview

  • πŸ–₯️ Provisions an aws_workspaces_directory keystone plus its persistent desktops and IP allow-lists: aws_workspaces_workspace (one per end user) and aws_workspaces_ip_group (CIDR allow-lists) β€” one composite call, one ready-to-use WorkSpaces fleet.
  • πŸ“ Registers, never creates, the directory. aws_workspaces_directory calls the WorkSpaces RegisterWorkspaceDirectory action 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 via directory_id.
  • πŸ” Volume encryption on by default β€” root_volume_encryption_enabled and user_volume_encryption_enabled both default to true on every WorkSpace, with a caller-suppliable volume_encryption_key (KMS CMK ARN); null (default) uses the AWS-managed alias/aws/workspaces key.
  • 🌐 No direct internet egress by default β€” workspace_creation_properties.enable_internet_access defaults to false, 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_administrator defaults to false.
  • πŸ”‘ IP access control via aws_workspaces_ip_group β€” a for_each map of named CIDR allow-lists, associated directly on the directory's ip_group_ids attribute (there is no separate association resource in this service).
  • πŸ§‘β€πŸ€β€πŸ§‘ Desktops are a for_each map keyed by username β€” add or remove one user's WorkSpace without perturbing the others; no count.
  • πŸ“¦ No bundle resource β€” aws_workspaces_bundle exists only as a data source in the hashicorp/aws provider (verified against v6.53). Custom bundle creation is a console/API operation with no Terraform coverage; this module exposes bundle_id as a plain string, typically resolved via data.aws_workspaces_bundle.
  • ⚠️ No arn output anywhere β€” verified against the v6.53 schema, none of the three in-scope resources export an arn attribute. This module deviates from the library-wide "id + arn" convention and emits id-family outputs only (see Architecture Notes).
  • 🏷️ Universal tagging β€” var.tags flows to the directory, every WorkSpace, and every IP group (all three expose tags); tags_all surfaced 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.


❀️ 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

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
Loading

🧬 What this module builds

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
Loading
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_ids is a plain attribute on the directory itself, populated from every group this module creates plus any external_ip_group_ids.


βœ… Provider / Versions

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

No provider {} block is declared inside the module β€” the caller's configured provider (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.


πŸ”‘ Required IAM Permissions

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:DeleteWorkspaceBundle are not exercised by this module β€” there is no aws_workspaces_bundle resource in the provider. Grant them separately only if your team manages custom bundles out-of-band via console/CLI/SDK.


πŸ“‹ AWS Prerequisites

⚠️ 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 future tf-mod-aws-directory-service module (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_id accepts the id from any of the three.
  • workspaces_DefaultRole IAM 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 have AmazonWorkSpacesServiceAccess (and, if self-service is used, AmazonWorkSpacesSelfServiceAccess) attached. Create it via tf-mod-aws-iam-role with an explicit depends_on or 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 β€” RegisterWorkspaceDirectory can 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/aws provider has no resource for creating WorkSpaces Pool session desktops; see Architecture Notes.

πŸ“ Module Structure

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

βš™οΈ Quick Start

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_DefaultRole must already exist in the account (see Prerequisites) or registration fails validation. Pin the source with ?ref=v1.0.0 β€” never a branch.


πŸ”Œ Cross-Module Contract

Consumes

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

Emits

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

⚠️ No arn output anywhere β€” see Architecture Notes.


πŸ“š Example Library

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_bundle is a data source only β€” there is no Terraform resource to create custom bundles. This module treats bundle_id as 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" without saml_properties.status != "DISABLED" fails this module's terraform 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 plan after adding the import block and reconcile any drift (especially workspace_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
}

⚠️ The hashicorp/aws provider 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]
}

πŸ“₯ Inputs

ℹ️ 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_each map)
  • Volume-encryption defaults: root_volume_encryption_enabled, user_volume_encryption_enabled, volume_encryption_key
  • Child collection: workspaces (for_each map, PERSONAL mode only)
  • Universal: tags, timeouts (applies to every aws_workspaces_workspace)

🧾 Outputs

  • Primary: id, directory_id (no arn β€” 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.


🧠 Architecture Notes

  • No arn output β€” a deliberate deviation from the library-wide "id + arn" convention. Verified against the hashicorp/aws v6.53 schema: none of aws_workspaces_directory, aws_workspaces_workspace, or aws_workspaces_ip_group export an arn attribute. WorkSpaces cross-references (including IAM policy conditions for this service) use the service-specific id, not an ARN.
  • ID formats: directory id d-xxxxxxxxxx (or wsd-xxxxxxxxxx for a POOLS-generated directory); WorkSpace id ws-xxxxxxxxx; IP group id wsipg-xxxxxxxxx.
  • Force-new / immutable fields: directory_id on 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_id and .default_ou cannot 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.tags flows to the directory, every WorkSpace, and every IP group (all three expose tags in the v6 schema); per-item tags merge over the module-level map. tags_all (surfaced from the directory only) is the provider-computed union over default_tags, with resource tags winning. default_tags is the caller's provider-block concern.
  • Eventual consistency / destroy ordering: RegisterWorkspaceDirectory can take several minutes to reach a usable state; rapid destroy/re-apply cycles can fail with directory-still-registering errors. Every aws_workspaces_workspace must be destroyed before the directory can deregister cleanly β€” Terraform's dependency graph handles this automatically as long as workspaces reference the directory's id output (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-field validation {} 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" forces workspaces = {} (enforced by validation) and directory_id = null (AWS auto-generates a wsd- id).
  • No us-east-1 constraint β€” WorkSpaces is a regional service; the module inherits the caller's region and declares no region variable.

🧱 Design Principles

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.


πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires valid AWS credentials (profile / SSO / OIDC) + a region
terraform apply
terraform output

⚠️ plan / apply require valid AWS credentials, a region, and a pre-existing workspaces_DefaultRole in the account. Always pin the module source with ?ref=v1.0.0, never a branch.


πŸ§ͺ Testing

  • terraform init -backend=false && terraform validate β€” schema + reference integrity (the directory_id/workspace_type/user_identity_type cross-field validations surface a PERSONAL/POOLS argument mismatch here, not mid-apply).
  • terraform fmt -check β€” formatting.
  • terraform plan against a sandbox account β€” confirm the directory, IP groups, and workspaces plan as expected and that workspace_creation_properties renders with the intended secure defaults.
  • After apply, confirm registration_code is populated and each workspace_ids entry reaches AVAILABLE in workspace_states before 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.

πŸ’¬ Example Output

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" }

πŸ” Troubleshooting

  • ResourceNotFoundException / validation failure on directory registration: workspaces_DefaultRole does not exist in the account. Create it via tf-mod-aws-iam-role (fixed name, AmazonWorkSpacesServiceAccess attached) before applying this module.
  • InvalidParameterValuesException on subnet_ids: more than 2 subnets supplied, or the subnets are not in 2 distinct Availability Zones. This module's validation {} 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_id or default_ou after 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 destroy hangs on the directory: one or more aws_workspaces_workspace resources still reference it. Confirm all workspaces finished terminating (DescribeWorkspaces) before the directory deregisters β€” this module's dependency graph handles the ordering automatically as long as directory_id isn't hardcoded.
  • certificate_based_auth_properties apply fails with an opaque API error: saml_properties.status is "DISABLED" (or unset). CBA requires SAML enabled; this module's cross-field validation should catch this at plan time β€” re-run terraform validate if you bypassed it via -target.
  • A POOLS apply fails demanding directory_id: you set directory_id explicitly. Leave it null for POOLS β€” AWS auto-generates a wsd- prefixed id.
  • New WorkSpace stuck in PENDING far longer than expected: WorkSpaces creation is genuinely slow (provider default create timeout is 30 minutes) β€” raise timeouts.create for large batches rather than assuming failure.
  • Credential-chain failures (NoCredentialProviders / ExpiredToken): no valid credentials resolved. Set AWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI.

πŸ”— Related Docs

  • 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."

Packages

 
 
 

Contributors

Languages